See also: VFS compliance · CLI reference · Back to the index
ADR-008 — Virtual paths are the only currency agents are paid in
Status: Accepted · Date: 2026-08-25
· Scope: Orkeon.Domain/FileSystem, Orkeon.Hosting, Orkeon.Host, Orkeon.Scripting.Cli, Orkeon.Studio.*
Context
Orkeon's filesystem doctrine was settled early and enforced hard: rule Q3, "virtual paths
everywhere" — no API, property or DTO carries a physical path outside FileSystemService
itself — backed by the Orkeon.Compliance.Vfs analyzer, by MountInfo being deliberately
BasePath-free, and by tests asserting that denial messages never name a disk path. When a CLI
flag once accepted a real path (--prebuild-index <real-path>), it was removed rather than
tolerated: the codebase is mounted with -m ../path:/src:ro instead.
The doctrine held inside the framework. It did not hold at the boundary where mounts are created.
Framework code computes absolute physical paths before any VFS exists — the crew definition's
own directory, the --llm-log directory. Those paths then have to be readable through the VFS.
The shortcut taken was to mount them 1:1, physical equal to virtual:
{configDir}:{configDir}:ro # RunnerExecution
{llmLogPath}:{llmLogPath}:rw # RunnerExecution, RunCommand
{crewDir}:{crewDir}:ro # HostCrewMounts, per hosted crew
so the absolute path already in hand resolved unchanged, "without any prefix gymnastics" as the
code put it. To make those strings parse, FileSystemMount.IsValidVirtualPath had been widened
to accept a Windows drive path as a virtual path. The breach was therefore in the VFS's own
contract, not in a caller.
The consequences were not theoretical. A mount parsed from a mount string gets
MountVisibility.AgentFacing — Parse has no way to say otherwise — so those directories were
listed to agents:
list_mountsreturnedC:\Users\…\my-teamas a mount path;- every access-denied message names the available mounts, and
FileSystemService.CollectBasePathsdeliberately exempted identity mounts from redaction (redacting them would have producedAvailable mounts: [REDACTED]), so the guard was disarmed for exactly the mounts that needed it; ShellCommandTool's path rewriting worked off the same list;- where the mount table is rendered into a system prompt, an absolute disk path appeared under the sentence "All file operations must use these virtual paths. Absolute or unmounted paths are not allowed."
Studio then mirrored all of it faithfully — including into two screens a novice reads: the
Composer's folder chips and, worse, the agent editor's « Sur quel dossier » line, which joined
raw physical:virtual:rights strings under a label about what that agent can see.
There was never a technical need for the shortcut. The scripting path had always mounted its
input as /script:ro, the forge bench mounts /workspace, /forge and /output, and the crew
loader is path-agnostic: it reads whatever path it is handed through IFileSystemService.
Decision
A physical path is never a virtual path. Concretely:
Runners mount under a name. The crew's directory is
/crew(a single-file crew is addressed as/crew/<file>.yaml), a hosted daemon's crews are/crews,/crews-1, …, a script's directory stays/script. The loader receives the virtual spelling.IsValidVirtualPathis narrowed back to/…only. A drive-letter virtual path is refused with a message that names the fix. The physical segment keeps its drive-letter handling — that side is legitimately a disk path.Infrastructure mounts are invisible.
Orkeon:FileSystem:InternalMountscarries mounts registered withMountVisibility.Internal: resolvable by the VFS, absent fromGetAvailableMounts()and therefore fromlist_mounts, from prompt mount tables and from denial messages — a denial message is read by the LLM, soFileSystemRegistrybuilds its "Available mounts" and "Mounts granting …" lists from the agent-facing set, not from all mounts. The LLM exchange log lives there — the VFS must reach it, no agent has any business addressing it. It is a configuration key rather than a hosted service because the runners never start the host: anIHostedServicewould silently never fire under--validateor--list-tools.Internalis a boundary, and it took a second accessor to make it one.Parsehad no way to say "hidden", so the first version of this decision only removed the mount from every listing: it stayed resolvable, andIFileSystemServicehad no notion of who was calling. The names are documented —/llm-logsholds every prompt and every API response of the run — so a singlefile_read /llm-logs/llm-exchanges-….jsonlhanded an agent the whole exchange history.FileSystemRegistry.ResolveAndCheckRightsnow refuses an Internal mount by default, in both directions (ToVirtualPathwill not name one either, so a tool's physical→virtual output rewrite cannot leak it), and reports it exactly like a path that does not exist. The two components that legitimately write to an internal root — the exchange logger and the code sandboxes — ask forPrivilegedFileSystemAccessby name: a distinct DI registration rather than a flag on the interface everyone already holds, so a tool cannot obtain it by accident and a reviewer can find every holder by searching for the type.The rule for screens is scoped, not absolute. No physical path in an agent-facing or novice context. Expert surfaces — the effective-mounts table, the picker's mount-string preview — keep showing the real strings: their job is to state the exact command line.
A user
--mountclaiming a reserved root is refused with an actionable line, instead of surfacing as a duplicate-virtual-path exception thrown out of a DI factory.
The redaction carve-out survives for the one case left: a Unix mount spelled the same on both
sides (/output:/output:rw, the container convention). It no longer covers runner injection.
Consequences
- An agent can no longer learn the operator's disk layout through the VFS, and denial messages are redacted unconditionally for every runner-injected directory.
- The exchange log stops being advertised to agents as a writable mount — it holds full prompts and API payloads.
- Breaking:
--mountno longer accepts a drive-letter virtual path; the crew directory is/crewand the hosted crews/crews*. Nothing in the repository relied on the old spelling, and no published crew can: mount strings are supplied per launch, not stored in a crew. - Turning
--llm-logon no longer shiftsOrkeon:FileSystem:Mounts:{i}, because the log rides its own key. Studio's index prediction gets simpler and stays true. - Studio duplicates the three virtual roots (it cannot reference
Orkeon.Hosting); a drift test pins them toRunnerMounts.
What this ADR deliberately does not settle
A crew still cannot bind the folders it needs. Since VFS-90 it names them: a mounts:
block (/output, or <ulid>|/output to pin one settings entry when several declare that root)
lists the virtual roots the crew uses, on the model of the existing links: block, and the
runners resolve it against the settings before any host — a root nothing provides, or an id no
entry carries, is refused in one line rather than at the first tool call. The block selects and
validates; the virtual→physical binding itself is still supplied from outside the portable
artifact — by a settings entry (now with an identity of its own), by a --mount argument, or by
Studio's sidecar. That is what closes the gap that let a promoted team launch with no /output
at all while its own tasks declared deliverable: /output/…: forge promote writes the block,
and a bare orkeon run crew/ refuses instead of writing nowhere.
A filesystem: block that would bind folders from the crew — physical paths inside the portable
artifact — remains out of scope, and deliberately so: the folder behind a name is the machine's
business, which is the whole point of this ADR.
The privileged caller this ADR left open has since been built: PrivilegedFileSystemAccess
is the second accessor, and MountVisibility.Internal refuses an agent that addresses the mount
by name instead of merely omitting it from the listings. What remains out of scope is finer
grain than one bit — per-caller rights, a capability handed to a specific tool — which would
touch the Domain contract far more deeply than one boolean on the registry does.