Table of Contents

🇬🇧 English version

Voir aussi : Conformité VFS · Référence CLI · Retour à l'index

ADR-008 — Les chemins virtuels sont la seule monnaie versée aux agents

Statut : Accepté · Date : 2026-08-25 · Portée : Orkeon.Domain/FileSystem, Orkeon.Hosting, Orkeon.Host, Orkeon.Scripting.Cli, Orkeon.Studio.*

Contexte

La doctrine de système de fichiers d'Orkeon a été fixée tôt et tenue fermement : la règle Q3, « chemins virtuels partout » — aucune API, propriété ou DTO ne porte de chemin physique hors de FileSystemService lui-même — appuyée par l'analyseur Orkeon.Compliance.Vfs, par un MountInfo délibérément sans BasePath, et par des tests vérifiant qu'un message de refus ne nomme jamais un chemin disque. Quand un flag CLI a un jour accepté un chemin réel (--prebuild-index <real-path>), il a été retiré plutôt que toléré : la codebase se monte via -m ../path:/src:ro.

La doctrine tenait à l'intérieur du framework. Elle ne tenait pas à la frontière où les montages se créent.

Le code du framework calcule des chemins physiques absolus avant que le VFS n'existe — le dossier de définition du crew, le dossier --llm-log. Ces chemins doivent ensuite être lisibles à travers le VFS. Le raccourci pris a été de les monter 1:1, physique égal virtuel :

{configDir}:{configDir}:ro          # RunnerExecution
{llmLogPath}:{llmLogPath}:rw        # RunnerExecution, RunCommand
{crewDir}:{crewDir}:ro              # HostCrewMounts, par crew hébergé

pour que le chemin absolu déjà en main se résolve tel quel, « sans gymnastique de préfixe » comme le disait le code. Pour que ces chaînes se parsent, FileSystemMount.IsValidVirtualPath avait été élargi pour accepter un chemin de lecteur Windows comme chemin virtuel. La brèche était donc dans le contrat du VFS lui-même, pas chez un appelant.

Les conséquences n'étaient pas théoriques. Un montage issu d'une mount-string reçoit MountVisibility.AgentFacing — Parse n'a aucun moyen de dire autre chose — donc ces dossiers étaient listés aux agents :

  • list_mounts retournait C:\Users\…\mon-equipe comme chemin de montage ;
  • tout message de refus nomme les montages disponibles, et FileSystemService.CollectBasePaths exemptait délibérément les montages identité de la rédaction (les rédiger aurait produit Available mounts: [REDACTED]), donc le garde-fou était désarmé pour exactement les montages qui en avaient besoin ;
  • la réécriture de chemins de ShellCommandTool travaillait sur la même liste ;
  • là où la table de montages est rendue dans un prompt système, un chemin disque absolu apparaissait sous la phrase « All file operations must use these virtual paths. Absolute or unmounted paths are not allowed. »

Studio reflétait ensuite tout cela fidèlement — y compris dans deux écrans que lit un novice : les chips de dossiers du Composer et, pire, la ligne « Sur quel dossier » de l'éditeur d'agent, qui joignait les mount-strings physique:virtuel:droits brutes sous un libellé parlant de ce que cet agent peut voir.

Le raccourci n'a jamais eu de nécessité technique. Le chemin scripté montait déjà son entrée en /script:ro, le banc de la forge monte /workspace, /forge et /output, et le chargeur de crew est agnostique : il lit le chemin qu'on lui donne via IFileSystemService.

Décision

Un chemin physique n'est jamais un chemin virtuel. Concrètement :

  1. Les runners montent sous un nom. Le dossier du crew est /crew (un crew mono-fichier s'adresse en /crew/<fichier>.yaml), les crews d'un démon sont /crews, /crews-1, …, le dossier d'un script reste /script. Le chargeur reçoit l'orthographe virtuelle.

  2. IsValidVirtualPath est resserré au seul /…. Un chemin virtuel à lettre de lecteur est refusé avec un message qui nomme le remède. Le segment physique garde son traitement de lettre de lecteur — ce côté-là est légitimement un chemin disque.

  3. Les montages d'infrastructure sont invisibles. Orkeon:FileSystem:InternalMounts porte les montages enregistrés en MountVisibility.Internal : résolubles par le VFS, absents de GetAvailableMounts() et donc de list_mounts, des tables de montages des prompts et des messages de refus — un message de refus est lu par le LLM, donc FileSystemRegistry construit ses listes « Available mounts » et « Mounts granting … » à partir de l'ensemble agent, pas de tous les montages. Le journal d'échanges LLM y vit — le VFS doit l'atteindre, aucun agent n'a à l'adresser. C'est une clé de configuration plutôt qu'un service hébergé parce que les runners ne démarrent jamais l'hôte : un IHostedService ne se déclencherait jamais sous --validate ou --list-tools.

    Internal est une frontière, et il a fallu un second accesseur pour que ce soit vrai. Parse n'avait aucun moyen de dire « caché », si bien que la première version de cette décision retirait seulement le montage de toutes les listes : il restait résoluble, et IFileSystemService ne sait pas qui l'appelle. Les noms sont documentés — /llm-logs contient chaque prompt et chaque réponse de l'exécution — donc un seul file_read /llm-logs/llm-exchanges-….jsonl remettait tout l'historique des échanges à un agent. FileSystemRegistry.ResolveAndCheckRights refuse désormais un montage Internal par défaut, dans les deux sens (ToVirtualPath ne le nomme pas davantage, pour qu'une réécriture physique→virtuel d'un outil ne le fuite pas), et le signale exactement comme un chemin inexistant. Les deux composants qui écrivent légitimement sous une racine interne — le journal d'échanges et les bacs à sable de code — demandent PrivilegedFileSystemAccess par son nom : un enregistrement DI distinct plutôt qu'un drapeau sur l'interface que tout le monde tient déjà, de sorte qu'un outil ne peut pas l'obtenir par accident et qu'un relecteur trouve chaque détenteur en cherchant le type.

  4. La règle des écrans est délimitée, pas absolue. Aucun chemin physique dans un contexte agent ou novice. Les surfaces expertes — la table des montages effectifs, l'aperçu de mount-string du sélecteur — continuent d'afficher les chaînes réelles : leur raison d'être est d'énoncer la ligne de commande exacte.

  5. Un --mount utilisateur revendiquant une racine réservée est refusé avec une ligne actionnable, au lieu de remonter en exception « chemins virtuels dupliqués » depuis une fabrique DI.

La dérogation de rédaction survit pour le seul cas restant : un montage Unix écrit pareil des deux côtés (/output:/output:rw, la convention conteneur). Elle ne couvre plus l'injection du runner.

Conséquences

  • Un agent ne peut plus apprendre l'organisation disque de l'opérateur via le VFS, et les messages de refus sont rédigés sans condition pour tout dossier injecté par le runner.
  • Le journal d'échanges cesse d'être annoncé aux agents comme un montage inscriptible — il contient les prompts complets et les charges utiles des API.
  • Rupture : --mount n'accepte plus de chemin virtuel à lettre de lecteur ; le dossier du crew est /crew et les crews hébergés /crews*. Rien dans le dépôt ne reposait sur l'ancienne orthographe, et aucun crew publié ne le peut : les mount-strings sont fournies par lancement, jamais stockées dans un crew.
  • Activer --llm-log ne décale plus Orkeon:FileSystem:Mounts:{i}, le journal ayant sa propre clé. La prédiction d'index de Studio se simplifie et reste vraie.
  • Studio duplique les trois racines virtuelles (il ne peut pas référencer Orkeon.Hosting) ; un test de dérive les épingle sur RunnerMounts.

Ce que cet ADR ne tranche délibérément pas

Un crew ne peut toujours pas lier les dossiers dont il a besoin. Depuis VFS-90 il les nomme : un bloc mounts: (/output, ou <ulid>|/output pour épingler une entrée des settings quand plusieurs déclarent cette racine) liste les racines virtuelles que le crew utilise, sur le modèle du bloc links: existant, et les runners le résolvent face aux settings avant tout host — une racine que rien ne fournit, ou un identifiant qu'aucune entrée ne porte, est refusé en une ligne plutôt qu'au premier appel d'outil. Le bloc sélectionne et valide ; le liage virtuel→physique lui-même reste fourni depuis l'extérieur de l'artefact portable — par une entrée des settings (désormais dotée d'une identité), par un argument --mount, ou par le sidecar de Studio. C'est ce qui referme le trou qui permettait de lancer une équipe promue sans aucun /output alors que ses propres tâches déclaraient deliverable: /output/… : forge promote écrit le bloc, et un orkeon run crew/ nu refuse au lieu d'écrire nulle part.

Un bloc filesystem: qui lierait des dossiers depuis le crew — des chemins physiques dans l'artefact portable — reste hors périmètre, et délibérément : le dossier derrière un nom est l'affaire de la machine, ce qui est tout le propos de cet ADR.

L'appelant privilégié que cet ADR laissait ouvert a depuis été construit : PrivilegedFileSystemAccess est ce second accesseur, et MountVisibility.Internal refuse désormais un agent qui adresse le montage par son nom au lieu de simplement l'omettre des listages. Ce qui reste hors périmètre, c'est une granularité plus fine qu'un bit — des droits par appelant, une capacité remise à un outil précis — qui toucherait le contrat du Domain bien plus profondément qu'un booléen sur le registre.