Table of Contents

🇬🇧 English version

Conformité VFS

Orkeon impose au code du framework un principe Virtual File System (VFS) uniquement : tout accès au système de fichiers doit passer par IFileSystemService afin que les frontières de mounts, les droits d'accès et la validation des chemins soient respectés uniformément. L'usage direct de System.IO.File, System.IO.Directory, FileStream, FileInfo, DirectoryInfo et FileSystemWatcher est interdit dans les assemblies du framework.

Ce document décrit :

  • l'analyseur Roslyn Orkeon.Compliance.Vfs qui applique la règle Ă  la compilation
  • ses sept codes de diagnostic et leurs sĂ©vĂ©ritĂ©s
  • les pĂ©rimètres exemptĂ©s et la manière d'opter pour une exception documentĂ©e
  • les critères de sortie du programme de migration VFS

VFS-70 (2026-06-17) : toutes les dépendances IFileSystemService des outils/services sont désormais requises (non-nullables) — les fallbacks EXCEPTION-BACKCOMPAT if (_fs is null) { …System.IO… } ont été éliminés, les shims [Obsolete] liés au FS supprimés, et le chemin d'ingestion de connaissances entièrement routé par le VFS (le KnowledgeService audité alors a été remplacé par les loaders Orkeon.Rag en RAG-02 — même règle VFS, FileDocumentLoaderBase). La persistance SQLite (SqliteMemoryProvider, SqliteStateStore) résout désormais son fichier Data Source via ResolveAndValidate (décision 2F-A) ; la découverte batch RaggableTree s'appuie sur IFileSystemService.EnumerateFilesAsync (décision 2E-A). Deux nouvelles règles (ORKVFS006, ORKVFS007) ferment les angles morts auparavant non détectés.

L'analyseur

Projet : src/analyzers/Orkeon.Compliance.Vfs/ Cible : analyseur Roslyn netstandard2.0, câblé dans src/Directory.Build.props avec OutputItemType=Analyzer.

L'analyseur s'applique à chaque projet sous src/ sauf lui-même, donc les violations cassent le build immédiatement. Les tests exécutent le même analyseur via le projet Orkeon.Compliance.Vfs.Tests.

Codes de diagnostic

Code Sévérité Détecte Correction suggérée
ORKVFS001 Error Appel direct à System.IO.File.* Injecter IFileSystemService et utiliser TryReadAllBytesAsync / WriteAllTextAsync / ExistsAsync / …
ORKVFS002 Error Appel direct Ă  System.IO.Directory.* Utiliser CreateDirectoryAsync, DeleteAsync, EnumerateFilesAsync
ORKVFS003 Error new FileStream(string …), new FileInfo(string), new DirectoryInfo(string) Utiliser OpenReadStreamAsync, OpenWriteStreamAsync, TryGetEntryAsync
ORKVFS004 Error Path.GetFullPath(…) (peut contourner la validation des mounts) Pour une entrée fournie par l'utilisateur, appeler ResolveAndValidate
ORKVFS005 Error new FileSystemWatcher(…) Utiliser l'abstraction watcher du VFS
ORKVFS006 Error new StreamReader(string) / new StreamWriter(string) (surcharges par chemin) Ouvrir via OpenReadStreamAsync / OpenWriteStreamAsync et envelopper le Stream retourné
ORKVFS007 Error Champ ou paramètre IFileSystemService? nullable Injecter IFileSystemService en dépendance requise (non-nullable)

Tous les diagnostics sont définis dans src/analyzers/Orkeon.Compliance.Vfs/DiagnosticDescriptors.cs et émettent un lien d'aide stable finissant en #ork-vfs-00N — les ancres de la colonne Code ci-dessus sont ces cibles.

Périmètres exemptés (par chemin)

L'analyseur ignore les chemins de l'allowlist (SystemIoUsageAnalyzer.IsExemptByPath). Elle est volontairement étroite — les exemptions par dossier entier ont été remplacées par une allowlist explicite par fichier, pour qu'un nouveau fichier sous le même dossier ne soit pas exempté silencieusement.

Exemptions par dossier (s_exemptFolderSegments) :

  • /core/Orkeon.Domain/FileSystem/ — contrats publics du VFS (l'abstraction elle-mĂŞme)
  • /core/Orkeon.Infrastructure/FileSystem/ — implĂ©mentation du VFS (disk/fake/watcher)
  • /tests/ — les fixtures reposent sur DiskBackedFileSystemService et le disque rĂ©el
  • /examples/ — hors pĂ©rimètre de conformitĂ© du framework

Exemptions par fichier (s_exemptFileSuffixes) — chacune porte un marqueur inline EXCEPTION-… / OUT-OF-SCOPE :

  • Sandbox/SandboxSession.cs — mounts enregistrĂ©s avant la DI (EXCEPTION-BOOTSTRAP)
  • Sandbox/ProcessIsolationSandbox.cs, Sandbox/DockerSandbox.cs — sondage de binaires hĂ´te (OUT-OF-SCOPE)
  • Security/PathValidator.cs — la rĂ©solution symlink/realpath est son rĂ´le

Tout autre fichier légitimement « brut » s'appuie sur un [SuppressVfsCompliance] étroit au site d'usage plutôt qu'une exemption par chemin (voir les exceptions permanentes ratifiées ci-dessous).

Opt-out : [SuppressVfsCompliance]

Pour les exceptions documentées qui ne correspondent à aucune exemption par chemin, appliquer l'attribut défini dans Orkeon.Domain.Attributes/SuppressVfsComplianceAttribute.cs :

[Orkeon.Compliance.Vfs.SuppressVfsCompliance("EXCEPTION-BOOTSTRAP: mount registration runs before DI")]
public sealed class SandboxSession { … }

[Obsolete("Use ReadAsync(…) instead")]
[Orkeon.Compliance.Vfs.SuppressVfsCompliance("EXCEPTION-OBSOLETE: transitional API; callers should migrate")]
public static string LoadLegacy(string path) => File.ReadAllText(path);

Placement :

  • Assembly — opt-out large pour un projet entier (Ă  utiliser avec parcimonie, prĂ©fĂ©rer un pĂ©rimètre plus Ă©troit).
  • Classe / struct — exempte tous les membres d'un type (classes d'outils avec fallbacks if (_fs is null)).
  • MĂ©thode / constructeur / propriĂ©tĂ© / champ — pĂ©rimètre le plus Ă©troit.

L'argument reason est obligatoire et doit nommer la catégorie d'audit dont il relève :

  • EXCEPTION-BOOTSTRAP — s'exĂ©cute avant l'enregistrement des mounts VFS.
  • EXCEPTION-WATCHER-BRIDGE — adaptateur qui enveloppe System.IO.FileSystemWatcher pour implĂ©menter un port watcher Orkeon.
  • OUT-OF-SCOPE — sondage au niveau de l'hĂ´te (chemin d'installation du SDK, dĂ©couverte de binaires système) qui ne cible jamais un mount VFS.

Catégories retirées (VFS-70) : EXCEPTION-BACKCOMPAT et EXCEPTION-OBSOLETE ne sont plus acceptées. Tous les outils/services exigent un IFileSystemService non-nullable (un nullable déclenche désormais ORKVFS007) et les shims [Obsolete] liés au FS ont été supprimés. Ne pas réintroduire ces catégories.

Toute nouvelle classe d'exception doit d'abord être consignée dans l'audit de conformité VFS (l'archive des mainteneurs, audit-vfs-compliance-*) avant l'ajout d'une suppression.

Exceptions permanentes ratifiées (2D)

Ces accès ne peuvent pas passer par le VFS par nature et sont définitivement autorisés via les marqueurs [SuppressVfsCompliance] inline (ou l'allowlist par chemin) :

Domaine Emplacement Catégorie Justification
Implémentation VFS Domain/FileSystem/*, Infrastructure/FileSystem/* (l'abstraction) C'est le VFS
Bootstrap (pré-DI) SandboxSession, ConsoleApp Cli*MountBootstrapper, Hosting/Runner*, Hosting/CrewMountDeclarations (pré-lit le bloc mounts: d'une crew, VFS-90) EXCEPTION-BOOTSTRAP Lisent appsettings + montent avant que le VFS existe. Couvre le provisionnement d'un montage, jamais l'exposition d'un chemin physique comme chemin virtuel — voir ADR-008
Sondage hôte DockerSandbox, ProcessIsolationSandbox, ProcessGitDiffProvider OUT-OF-SCOPE Découvrent docker/dotnet/git sur le PATH, jamais un mount
Toolchain Scripting/Toolchain/EsbuildTranspiler.cs OUT-OF-SCOPE Localise le binaire esbuild + fichiers temp de transpilation ; chaîne d'outils hôte
Primitive sécurité Security/PathValidator.cs (allowlist) résolution symlink/realpath = son rôle
Watcher bridge Analysis/.../FileSystemWatcherCodebaseWatcher.cs EXCEPTION-WATCHER-BRIDGE Adapte System.IO.FileSystemWatcher vers ICodebaseWatcher
Sonde RaggableTree Analysis/Core/FileSystemDiscoverer.cs (sonde diagnostique uniquement) OUT-OF-SCOPE La découverte batch elle-même utilise EnumerateFilesAsync ; la sonde compte délibérément les entrées physiques pour distinguer « le VFS n'a rien renvoyé » de « le disque est vide »
Persistance SQLite SqliteMemoryProvider, SqliteStateStore gouverné Le moteur exige un chemin réel ; le Data Source est résolu via ResolveAndValidate (décision 2F-A) avant d'atteindre le driver

Ajouter un nouvel outil ou service

  1. Déclarer un paramètre de constructeur IFileSystemService requis, non-nullable et l'injecter via la DI (un nullable déclenche ORKVFS007). Le valider avec ArgumentNullException.ThrowIfNull.
  2. Travailler en chemins virtuels (/workspace/..., /output/..., /tmp/...).
  3. Ne jamais ajouter de fallback System.IO par chemin string. Il n'existe pas de constructeur de rétro-compatibilité — la DI est la seule voie de construction.
  4. Pour énumérer, streamer, copier ou observer, utiliser les méthodes dédiées de IFileSystemService plutôt que les primitives System.IO équivalentes (y compris StreamReader/StreamWriter — envelopper un Stream issu de OpenReadStreamAsync/OpenWriteStreamAsync, jamais un chemin).

Mounts par scope (surcharge ambiante des mounts)

L'ensemble des mounts est un singleton défini au démarrage : AddOrkeonFileSystem construit un FileSystemRegistry unique à partir de Orkeon:FileSystem:Mounts, et IFileSystemService est un singleton au-dessus. C'est correct pour un runner mono-tenant, mais un hôte qui exécute plusieurs crews dans un même processus (p. ex. un moteur d'exécution pilotant un profil différent par run) doit pouvoir donner ses propres mounts à un flux d'exécution sans perturber les autres.

IFileSystemService ne peut pas devenir DI-scoped pour cela : des dizaines de singletons l'injectent, donc une durée de vie scoped en ferait une dépendance captive. À la place, les mounts par scope sont fournis par une surcharge ambiante — IFileSystemScope (enregistré en singleton, adossé à AsyncLocal<FileSystemRegistry?>, AsyncLocalFileSystemScope). Le singleton FileSystemService la lit à chaque opération : il résout contre le registre ambiant quand un flux d'exécution en a installé un, et contre le registre de démarrage sinon. AsyncLocal isole la valeur par flux de contrôle asynchrone, donc des runs concurrents ne voient jamais les mounts les uns des autres, et un hôte qui n'entre jamais dans un scope conserve un comportement identique à l'octet près.

// Dans un scope de run : installer les mounts de ce run pour ce flux async uniquement.
var granted = grantedMountStrings.Select(FileSystemMount.Parse).ToList();
using var registry = ScopedMountComposition.ForExecution(bootRegistry, granted);
using var _ = fileSystemScope.Enter(registry);          // restauré au dispose (imbrication supportée)

// Tout appel IFileSystemService sur CE flux async résout désormais contre `granted` ;
// les autres runs concurrents continuent de voir les mounts de démarrage.

Composer, jamais construire à la main. Entrer dans un scope REMPLACE le jeu de mounts — ActiveRegistry vaut scope?.Current ?? boot, jamais une union — donc un registre bâti sur les seuls dossiers d'une exécution emporte tout le jeu de boot avec lui pour la durée de ce flux. ForExecution reporte donc les mounts de boot et laisse ceux de l'exécution les masquer aux chemins virtuels qu'ils revendiquent ; la substitution plutôt que l'addition est la raison d'être du mécanisme, puisque deux crews recevant chacun /output sur des dossiers différents est précisément ce qu'un registre plat ne sait pas exprimer.

Les deux moitiés du jeu de boot doivent survivre, pour des raisons différentes. Les mounts internes — /llm-logs, /sandbox — relèvent de la confidentialité : les perdre arrête la journalisation des échanges et les deux sandboxes de code, et fait tomber IsUnderInternalMountUnsafe, le contrôle qui empêche l'un d'eux de gagner une seconde adresse atteignable par un agent via un mount de l'exécution. Les mounts agent-facing portent tout autant : orkeon-host monte le dossier de chaque crew hébergé sous /crews puis charge le crew par ce chemin virtuel, depuis l'intérieur du scope. Ne composer que les mounts de l'exécution rendait Orkeon:Host:Crews:*:Mounts — la clé de configuration de la fonctionnalité elle-même — incapable de charger le crew sur lequel elle était posée, et l'échec revenait sous forme d'un message générique parce qu'ExistsAsync avale le refus.

Qui en entre un aujourd'hui. orkeon-host est la seule racine de composition livrée qui le fasse, une fois par crew hébergé, depuis Orkeon:Host:Crews:*:Mounts — deux crews hébergés peuvent donc adresser chacun son /output sur des dossiers différents, ce que l'unique registre de boot, plat, ne sait pas exprimer. Tous les autres runners gardent les mounts de boot : un processus par lancement rend la question sans objet chez eux.

Les gardes s'appliquent aux mounts scopés exactement comme aux mounts de démarrage, car elles s'exécutent à chaque opération sur le registre actif : le registre applique les droits de mount + le confinement contre la traversée de chemin, et IPathValidator applique indépendamment les contrôles racine-workspace / chemins bloqués / extensions (PathSecurity:DefaultWorkspaceRoot, AdditionalAllowedDirectories). Un mount scopé dont la base physique se situe hors de la racine workspace autorisée est refusé exactement comme le serait un mount de démarrage — accorder un dossier dans Orkeon:Host:Crews:*:Mounts ne suffit donc pas à soi seul, et PathSecurity:AdditionalAllowedDirectories est l'endroit où un opérateur élargit ce second portail. L'appelant possède la durée de vie du FileSystemRegistry scopé (Enter ne le dispose pas — le disposer soi-même, comme ci-dessus).

Comportement en CI

Les sept diagnostics (ORKVFS001–ORKVFS007) sont tous des erreurs, donc la CI bloque tout merge qui réintroduit un accès fichier System.IO, un StreamReader/StreamWriter par chemin, un Path.GetFullPath sur entrée utilisateur, ou un IFileSystemService nullable dans le code du framework sans attribut [SuppressVfsCompliance] justifié.

Critères de sortie (du programme de migration VFS)

  • [x] VIOLATION-HISTORIC == 0 — les 23 violations historiques ont Ă©tĂ© Ă©liminĂ©es dans P5-VFS-50.
  • [x] VIOLATION-NEW rĂ©duit aux exceptions documentĂ©es derrière des gardes [Obsolete] / if (_fs is null), toutes couvertes par [SuppressVfsCompliance].
  • [x] EXCEPTION-BOOTSTRAP ≤ 15 — actuellement 7, toutes lĂ©gitimes (SandboxSession).
  • [x] Analyseur Roslyn Orkeon.Compliance.Vfs en place et câblĂ© dans src/Directory.Build.props.
  • [x] Le build passe proprement ; un test nĂ©gatif confirme qu'un File.ReadAllText dĂ©libĂ©rĂ© dans le code du framework dĂ©clenche ORKVFS001.
  • [x] Éliminer les suppressions EXCEPTION-BACKCOMPAT rĂ©siduelles en migrant tous les appelants d'outils vers la DI — fait dans VFS-70 : 0 EXCEPTION-BACKCOMPAT et 0 EXCEPTION-OBSOLETE liĂ© au FS ne subsistent dans src/ ; tous les outils fichier + le chemin d'ingestion de connaissances (dĂ©sormais les loaders Orkeon.Rag, RAG-02) exigent un IFileSystemService non-nullable ; SQLite gouvernĂ© via ResolveAndValidate ; ORKVFS004 promu en erreur et ORKVFS006/ORKVFS007 ajoutĂ©s pour fermer les angles morts StreamReader/Writer(string) et IFileSystemService nullable.