π«π· Version franΓ§aise
VFS Compliance
Orkeon enforces a Virtual File System (VFS) only principle on framework code: all filesystem access must go through IFileSystemService so that mount boundaries, access rights, and path validation are respected uniformly. Direct use of System.IO.File, System.IO.Directory, FileStream, FileInfo, DirectoryInfo, and FileSystemWatcher is forbidden in framework assemblies.
This document describes:
- the
Orkeon.Compliance.VfsRoslyn analyzer that enforces the rule at build time - its seven diagnostic codes and their severities
- the exempt scopes and how to opt out for documented exceptions
- the exit criteria from the VFS migration programme
VFS-70 (2026-06-17): all tool/service
IFileSystemServicedependencies are now required (non-nullable) β theEXCEPTION-BACKCOMPATif (_fs is null) { β¦System.IOβ¦ }fallbacks have been eliminated, the FS-related[Obsolete]shims removed, and the knowledge ingestion path fully routed through the VFS (theKnowledgeServiceaudited then was replaced by theOrkeon.Ragloaders in RAG-02 β same VFS rule,FileDocumentLoaderBase). SQLite persistence (SqliteMemoryProvider,SqliteStateStore) now resolves itsData Sourcefile throughResolveAndValidate(decision 2F-A); the RaggableTree batch discovery runs onIFileSystemService.EnumerateFilesAsync(decision 2E-A). Two new rules (ORKVFS006,ORKVFS007) close the previously-undetected blind spots.
The analyzer
Project: src/analyzers/Orkeon.Compliance.Vfs/
Target: netstandard2.0 Roslyn analyzer, wired into src/Directory.Build.props with OutputItemType=Analyzer.
The analyzer is applied to every project under src/ except itself, so violations break the build immediately. Tests run the same analyzer via the Orkeon.Compliance.Vfs.Tests project.
Diagnostic codes
All diagnostics are defined in src/analyzers/Orkeon.Compliance.Vfs/DiagnosticDescriptors.cs and emit a stable help link ending in #ork-vfs-00N β the anchors in the Code column above are those targets.
Exempt scopes (path-based)
The analyzer skips file paths that match its allowlist (SystemIoUsageAnalyzer.IsExemptByPath). It is intentionally narrow β folder-wide blank checks were replaced by an explicit per-file allowlist so a new file under the same folder is not silently exempted.
Folder exemptions (s_exemptFolderSegments):
/core/Orkeon.Domain/FileSystem/β VFS public contracts (the abstraction itself)/core/Orkeon.Infrastructure/FileSystem/β VFS implementation (disk/fake/watcher)/tests/β fixtures rely onDiskBackedFileSystemServiceand real disk/examples/β out of framework compliance scope
Per-file exemptions (s_exemptFileSuffixes) β each carries inline EXCEPTION-β¦ / OUT-OF-SCOPE markers:
Sandbox/SandboxSession.csβ mounts registered before DI (EXCEPTION-BOOTSTRAP)Sandbox/ProcessIsolationSandbox.cs,Sandbox/DockerSandbox.csβ host-binary probing (OUT-OF-SCOPE)Security/PathValidator.csβ symlink/realpath resolution is its core job
Every other legitimately-raw file relies on a narrow [SuppressVfsCompliance] at the use site rather than a path exemption (see the ratified permanent exceptions below).
Opt-out: [SuppressVfsCompliance]
For documented exceptions that do not match a path-based exemption, apply the attribute from 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 β broad opt-out for a whole project (use sparingly, prefer a narrower scope).
- Class / struct β opt out every member of a type (tool classes with
if (_fs is null)fallbacks). - Method / constructor / property / field β narrowest scope.
The reason argument is mandatory and should name the audit category it belongs to:
EXCEPTION-BOOTSTRAPβ runs before VFS mounts are registered.EXCEPTION-WATCHER-BRIDGEβ adapter that wrapsSystem.IO.FileSystemWatcherto implement an Orkeon watcher port.OUT-OF-SCOPEβ host-level probing (SDK install path, system binary discovery) that never targets a VFS mount.
Retired categories (VFS-70):
EXCEPTION-BACKCOMPATandEXCEPTION-OBSOLETEare no longer accepted. All tools/services require a non-nullableIFileSystemService(a nullable one now tripsORKVFS007) and the FS-related[Obsolete]shims were deleted. Do not reintroduce either category.
Any new class of exception should first be captured in the VFS compliance audit (the maintainers' archive, audit-vfs-compliance-*) before a suppression is added.
Ratified permanent exceptions (2D)
These accesses cannot go through the VFS by nature and are permanently allowed via the inline [SuppressVfsCompliance] markers above (or the path allowlist):
| Area | Location | Category | Why |
|---|---|---|---|
| VFS implementation | Domain/FileSystem/*, Infrastructure/FileSystem/* |
(the abstraction) | It is the VFS |
| Bootstrap (pre-DI) | SandboxSession, ConsoleApp Cli*MountBootstrapper, Hosting/Runner*, Hosting/CrewMountDeclarations (pre-reads a crew's mounts: block, VFS-90) |
EXCEPTION-BOOTSTRAP |
Read appsettings + mount before the VFS exists. Covers provisioning a mount, never exposing a physical path as a virtual one β see ADR-008 |
| Host probing | DockerSandbox, ProcessIsolationSandbox, ProcessGitDiffProvider |
OUT-OF-SCOPE |
Discover docker/dotnet/git on PATH, never a mount |
| Toolchain | Scripting/Toolchain/EsbuildTranspiler.cs |
OUT-OF-SCOPE |
Locates the esbuild binary + transpile temp files; host toolchain |
| Security primitive | Security/PathValidator.cs |
(allowlist) | symlink/realpath resolution is its job |
| Watcher bridge | Analysis/.../FileSystemWatcherCodebaseWatcher.cs |
EXCEPTION-WATCHER-BRIDGE |
Adapts System.IO.FileSystemWatcher to ICodebaseWatcher |
| RaggableTree probe | Analysis/Core/FileSystemDiscoverer.cs (diagnostic probe only) |
OUT-OF-SCOPE |
The batch discovery itself runs on EnumerateFilesAsync; the probe deliberately counts physical entries to distinguish "VFS yielded nothing" from "disk empty" |
| SQLite persistence | SqliteMemoryProvider, SqliteStateStore |
governed | The engine needs a real path; the Data Source is resolved via ResolveAndValidate (decision 2F-A) before reaching the driver |
Adding a new tool or service
- Declare a required, non-nullable constructor parameter of type
IFileSystemServiceand inject it via DI (a nullable one tripsORKVFS007). Validate it withArgumentNullException.ThrowIfNull. - Work in virtual paths (
/workspace/...,/output/...,/tmp/...). - Never add a
string-pathSystem.IOfallback. There is no back-compat ctor β DI is the only construction path. - If you need to enumerate, stream, copy, or watch, use the dedicated
IFileSystemServicemethods rather than the equivalentSystem.IOprimitives (includingStreamReader/StreamWriterβ wrap aStreamfromOpenReadStreamAsync/OpenWriteStreamAsync, never a path).
Per-scope mounts (ambient mount override)
The mount set is a boot-time singleton: AddOrkeonFileSystem builds one FileSystemRegistry from
Orkeon:FileSystem:Mounts, and IFileSystemService is a singleton over it. That is correct for a
single-tenant runner, but a host that runs many crews in one process (e.g. a run engine driving a
different profile per run) needs to give one execution flow its own mounts without disturbing the
others.
IFileSystemService cannot become DI-scoped for this: dozens of singletons inject it, so a scoped
lifetime would be a captive dependency. Instead, per-scope mounts are provided by an ambient
override β IFileSystemScope (registered as a singleton, backed by AsyncLocal<FileSystemRegistry?>,
AsyncLocalFileSystemScope). The singleton FileSystemService reads it per operation: it resolves
against the ambient registry when an execution flow has entered one, and against the boot registry
otherwise. AsyncLocal isolates the value per asynchronous control flow, so concurrent runs never see
each other's mounts, and a host that never enters a scope keeps byte-identical behavior.
// Inside a run scope: install this run's mounts for this async flow only.
var granted = grantedMountStrings.Select(FileSystemMount.Parse).ToList();
using var registry = ScopedMountComposition.ForExecution(bootRegistry, granted);
using var _ = fileSystemScope.Enter(registry); // restored on dispose (nesting supported)
// Every IFileSystemService call on THIS async flow now resolves against `granted`;
// other concurrent runs continue to see the boot mounts.
Compose, never hand-build. Entering a scope REPLACES the mount set β ActiveRegistry is
scope?.Current ?? boot, never a union β so a registry built from one execution's own folders
alone takes the whole boot set down with it for the length of that flow. ForExecution
therefore carries the boot mounts forward and lets the execution's own shadow them at the
virtual paths they claim; substitution rather than addition is the point of the mechanism, since
two crews each granted /output over different folders is exactly what one flat registry cannot
express.
Both halves of the boot set have to survive, for different reasons. The Internal ones β
/llm-logs, /sandbox β are a confidentiality matter: dropping them stops exchange logging and
both code sandboxes resolving, and it drops IsUnderInternalMountUnsafe, the check that stops
either of them gaining a second, agent-reachable address through one of the execution's own
mounts. The agent-facing ones are just as load-bearing: orkeon-host mounts each hosted
crew's directory under /crews and then loads the crew by that virtual path, from inside the
scope. Composing only the execution's own mounts made Orkeon:Host:Crews:*:Mounts β the
feature's own configuration key β unable to load the crew it was set on, and the failure came back
as a generic run-failed message because ExistsAsync swallows the denial.
Who enters one today. orkeon-host is the one shipped composition root that does, once per
hosted crew, from Orkeon:Host:Crews:*:Mounts β so two hosted crews may each address their own
/output over different folders, which the single flat boot registry cannot express. Every other
runner keeps the boot mounts: one process per launch makes the question moot for them.
The guards apply to scoped mounts exactly as to boot mounts, because they run per operation over
whichever registry is active: the registry enforces mount rights + path-traversal containment, and
IPathValidator independently enforces the workspace-root / blocked-path / extension checks
(PathSecurity:DefaultWorkspaceRoot, AdditionalAllowedDirectories). A scoped mount whose physical
base sits outside the allowed workspace root is denied just like a boot mount would be β granting
a folder in Orkeon:Host:Crews:*:Mounts is therefore not sufficient on its own, and
PathSecurity:AdditionalAllowedDirectories is where an operator widens the second gate. The caller owns
the scoped FileSystemRegistry's lifetime (Enter does not dispose it β dispose it yourself, as above).
CI behaviour
All seven diagnostics (ORKVFS001βORKVFS007) are errors, so CI blocks any merge that reintroduces System.IO file access, a string-path StreamReader/StreamWriter, a Path.GetFullPath on user input, or a nullable IFileSystemService in framework code without a justified [SuppressVfsCompliance] attribute.
Exit criteria (from the VFS migration programme)
- [x]
VIOLATION-HISTORIC == 0β the 23 historic violations were eliminated in P5-VFS-50. - [x]
VIOLATION-NEWreduced to documented exceptions behind[Obsolete]/if (_fs is null)guards, all covered by[SuppressVfsCompliance]. - [x]
EXCEPTION-BOOTSTRAPβ€ 15 β currently 7, all legitimate (SandboxSession). - [x] Roslyn analyzer
Orkeon.Compliance.Vfsin place and wired insrc/Directory.Build.props. - [x] Build passes clean; negative test confirms a deliberate
File.ReadAllTextin framework code tripsORKVFS001. - [x] Eliminate residual
EXCEPTION-BACKCOMPATsuppressions by migrating all tool callers to DI β done in VFS-70: 0EXCEPTION-BACKCOMPATand 0 FS-relatedEXCEPTION-OBSOLETEremain insrc/; all file tools + the knowledge ingestion path (now theOrkeon.Ragloaders, RAG-02) require a non-nullableIFileSystemService; SQLite governed viaResolveAndValidate;ORKVFS004promoted to error andORKVFS006/ORKVFS007added to close theStreamReader/Writer(string)and nullable-IFileSystemServiceblind spots.