🇬🇧 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.Vfsqui 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
IFileSystemServicedes outils/services sont désormais requises (non-nullables) — les fallbacksEXCEPTION-BACKCOMPATif (_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 (leKnowledgeServiceaudité alors a été remplacé par les loadersOrkeon.Ragen RAG-02 — même règle VFS,FileDocumentLoaderBase). La persistance SQLite (SqliteMemoryProvider,SqliteStateStore) résout désormais son fichierData SourceviaResolveAndValidate(décision 2F-A) ; la découverte batch RaggableTree s'appuie surIFileSystemService.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
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 surDiskBackedFileSystemServiceet 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 enveloppeSystem.IO.FileSystemWatcherpour 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-BACKCOMPATetEXCEPTION-OBSOLETEne sont plus acceptées. Tous les outils/services exigent unIFileSystemServicenon-nullable (un nullable déclenche désormaisORKVFS007) 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
- Déclarer un paramètre de constructeur
IFileSystemServicerequis, non-nullable et l'injecter via la DI (un nullable déclencheORKVFS007). Le valider avecArgumentNullException.ThrowIfNull. - Travailler en chemins virtuels (
/workspace/...,/output/...,/tmp/...). - Ne jamais ajouter de fallback
System.IOpar cheminstring. Il n'existe pas de constructeur de rétro-compatibilité — la DI est la seule voie de construction. - Pour énumérer, streamer, copier ou observer, utiliser les méthodes dédiées de
IFileSystemServiceplutôt que les primitivesSystem.IOéquivalentes (y comprisStreamReader/StreamWriter— envelopper unStreamissu deOpenReadStreamAsync/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-NEWré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.Vfsen place et câblé danssrc/Directory.Build.props. - [x] Le build passe proprement ; un test négatif confirme qu'un
File.ReadAllTextdélibéré dans le code du framework déclencheORKVFS001. - [x] Éliminer les suppressions
EXCEPTION-BACKCOMPATrésiduelles en migrant tous les appelants d'outils vers la DI — fait dans VFS-70 : 0EXCEPTION-BACKCOMPATet 0EXCEPTION-OBSOLETElié au FS ne subsistent danssrc/; tous les outils fichier + le chemin d'ingestion de connaissances (désormais les loadersOrkeon.Rag, RAG-02) exigent unIFileSystemServicenon-nullable ; SQLite gouverné viaResolveAndValidate;ORKVFS004promu en erreur etORKVFS006/ORKVFS007ajoutés pour fermer les angles mortsStreamReader/Writer(string)etIFileSystemServicenullable.