Use Warden

CLI reference

Complete maelys-warden command reference for local and OCI targets, I/O, network frontends, MIR and receipts.

Forme générale

CODE
maelys-warden run SOURCE-DE-POLICY [OPTIONS] -- COMMAND [ARGUMENT...]

La sous-commande run distingue l’exécution des sous-commandes image inspect et image import, ainsi que des futures commandes de compilation de policy. Le séparateur -- termine définitivement les options Warden. Tout ce qui le suit devient l’argv exact du workload.

Sans --image, cette commande crée un TargetSpec local. Avec --image REFERENCE@sha256:DIGEST, elle crée un TargetSpec OCI. --backend apple-container avec une forme locale échoue donc sur le type de cible au lieu de convertir silencieusement la commande en image.

CLI directe et TrustedRunSpec

Pour OCI, la façade construit le quintuplet scellé suivant :

CODE
TargetSpec + plan + DeploymentContext + SelectionPolicy + ExecutionBudget
                              -> TrustedRunSpec

Le cœur Executor produit les preuves ABI 10 et le rapport de sélection pour les deux sortes de cibles. La façade sérialise v1 par défaut pour une commande locale afin de conserver son contrat produit. Une commande OCI exige et produit v7 ; v2 à v6 restent gelés pour vérifier d'anciens receipts, pas comme sorties OCI sélectionnables en 0.40. Elle n'interprète jamais -- command comme une image et ne fabrique jamais de digest implicite.

Images OCI hors ligne

Le support d'image est fourni par le paquet commun optionnel maelys-warden-oci-tools. Il est partagé par les drivers Krun et Containerization direct, sans appartenir à l'un d'eux. Le CLI principal découvre automatiquement maelys-warden-oci-materializer à côté de lui ou sous libexec. S'il manque, les commandes image échouent avec le conseil d'installer maelys-warden-oci-tools; les backends locaux restent utilisables sans ces dépendances.

CODE
# Lecture seule : vérifie la structure et liste les variantes disponibles.
maelys-warden image inspect ./tool.oci

# Écriture : importe l'unique variante dans le store privé local.
maelys-warden image import ./tool.oci

# Source multi-plateforme : sélection explicite, sans contacter de registry.
maelys-warden image import ./bundle.oci.tar \
  --platform linux/arm64 \
  --digest sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

SOURCE peut être un répertoire OCI Image Layout ou une archive contenant ce layout. La détection repose sur oci-layout et index.json, pas sur le suffixe du chemin. inspect ne modifie rien. import vérifie toute la fermeture des descripteurs, applique les layers dans un graphe Linux byte-exact puis fabrique directement un ext4 déterministe. Il ne contacte jamais le réseau.

Pour une image à une seule variante, la plateforme et le digest sont lus dans la source vérifiée. Lorsqu'il existe plusieurs variantes, l'absence de sélecteur est refusée : l'utilisateur copie les valeurs affichées par inspect. Il ne calcule pas lui-même le SHA-256.

Le store est choisi dans cet ordre :

  1. MAELYS_WARDEN_OCI_STORE ;
  2. $XDG_DATA_HOME/maelys-warden/oci ;
  3. $HOME/.local/share/maelys-warden/oci.

Une importation réussie imprime par exemple :

CODE
oci@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef linux-arm64 /Users/alice/.local/share/maelys-warden/oci/objects/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/linux-arm64

Le premier champ donne la référence immuable utilisable par run, le deuxième la variante et le troisième l'artefact local. Celui-ci contient root.ext4, artifact.json pour l'audit et artifact.seal pour la validation mécanique. Pendant l'import, Warden supprime les éventuels /maelys-init, /maelys-bwrap et /maelys-guest-supervisor fournis par l'image et insère les trois helpers arm64 statiques du bundle Warden vérifié. Leurs digests sont inscrits dans le seal MWOCI/3 et couverts par le digest de root.ext4 : l'image OCI ne choisit ni l'init ni le sandbox interne. Le même root.ext4 scellé est consommé par Krun et par le backend apple-containerization direct. Le produit Apple Container service garde son propre store et ne consomme pas cet artefact. K4 livre cette importation et sa validation ; K5 lance ce disque ext4 en lecture seule avec Krun et monte les vues explicitement demandées. K6 sélectionne krun-bubblewrap quand la MIR exige des règles READ/WRITE récursives ; DENY, les règles littérales et PROTECT_CREATE restent refusés. K7 fournit le proxy Egress aux deux candidats sur AF_VSOCK 4520, sans activer TSI ni une carte réseau invitée. K8 confine enfin le VMM lui-même dans un profil Seatbelt hôte deny-default sondé avant l'enregistrement du candidat. image list relit et valide les objets du store privé ; image remove supprime uniquement la variante immuable explicitement nommée, sans suivre de symlink. Ces deux commandes sont livrées depuis K9. image pull reste hors de la surface actuelle : image import demeure strictement hors ligne et ne contacte jamais une registry.

Depuis 0.32, Krun transmet le RunSpec canonique après le boot sur MI/6. L'argv, l'environnement et les credentials ne transitent donc jamais par la cmdline noyau ; le même framing borné est partagé avec Containerization direct.

CLI OCI multiplateforme

La présence de --image est l'unique sélecteur de cible OCI : sans cette option, la commande reste une cible locale.

CODE
maelys-warden run SOURCE-DE-POLICY \
  --image REFERENCE@sha256:DIGEST \
  [--platform linux/arm64|linux/amd64] \
  [--guest-workspace /workspace] \
  [--guest-cwd /workspace] \
  [--backend NAME] \
  [--ephemeral-rw [--ephemeral-rw-size SIZE]] \
  [--vm-cpus N] [--vm-memory SIZE] \
  [--memory SIZE] [--cpu-quota USEC] [--cpu-period USEC] \
  [--tasks N] [--wall-time MS] \
  [--network-connections N] \
  [--network-bytes-per-connection SIZE] \
  [--network-bytes-total SIZE] \
  [--receipt-schema v7] \
  -- COMMAND-DANS-L-IMAGE [ARGUMENT...]

Exécution native arm64, avec le workspace hôte courant monté en lecture seule dans /workspace :

CODE
maelys-warden run --ro \
  --image ghcr.io/maelys/tool@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
  -- /usr/bin/tool --argument

La plateforme peut être omise seulement si le DeploymentContext scellé ne présente qu'une plateforme native : sur Apple Silicon, cela donne linux/arm64 pour les candidats Apple comme pour les deux candidats Krun, ces derniers n'acceptant d'ailleurs aucune autre plateforme. Si plusieurs plateformes natives sont disponibles, ou si seule une traduction est possible, --platform devient obligatoire. La forme longue, recommandée dans un script reproductible, est :

CODE
maelys-warden run --ro \
  --image ghcr.io/maelys/tool@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
  --platform linux/arm64 \
  --guest-workspace /workspace \
  --guest-cwd /workspace \
  -- /usr/bin/tool --argument

Exécution explicite de la variante amd64 sous Rosetta :

CODE
maelys-warden run --rw \
  --image ghcr.io/maelys/tool@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
  --platform linux/amd64 \
  --net github.com:443 \
  --network-frontend proxy \
  --receipt execution.json \
  --network-connections 16 \
  --network-bytes-per-connection 32M \
  --network-bytes-total 256M \
  --receipt-schema v7 \
  -- /usr/bin/tool build

Cette commande signifie précisément :

  1. résoudre la variante linux/amd64 de l'image identifiée par son digest ;
  2. monter le workspace hôte dans /workspace selon la policy --rw ;
  3. demander au planner un candidat OCI satisfaisant la MIR ;
  4. exiger Rosetta disponible, sans installation interactive ni fallback arm64 ;
  5. utiliser Egress via le frontend proxy : Apple emploie son pont UDS/vsock et Krun le port AF_VSOCK 4520, sans réseau IP ambiant dans la VM ;
  6. appliquer les plafonds réseau dans l'instance Egress propre à l'exécution ;
  7. lancer /usr/bin/tool build dans l'image et produire un receipt v7.

Nouvelles options OCI

OptionContrat
--image REFERENCE@sha256:DIGESTcrée un TargetSpec OCI ; digest complet obligatoire, tag mutable seul refusé
--platform linux/arm64|linux/amd64architecture du workload OCI ; inférée seulement si une unique plateforme native est prouvée
--guest-workspace PATHdestination absolue du workspace hôte dans l'invité ; défaut : /workspace
--guest-cwd PATHrépertoire de travail absolu de la commande dans l'invité ; défaut : valeur de --guest-workspace
--backend NAMEautorise exactement ce candidat ; aucune option laisse le planner choisir
--ephemeral-rwexige une racine OCI writable jetable ; aucune racine locale ni fallback readonly
--ephemeral-rw-size SIZEborne exacte du tmpfs upper/work ; suffixes K, M, G, défaut 256 Mio, plage 16 Mio–64 Gio
--vm-cpus Nnombre exact de vCPU, 1–16 ; défaut 2
--vm-memory SIZEmémoire exacte de la VM, alignée au Mio, 512 Mio–64 Gio ; défaut 1 Gio
--memory SIZEmemory.max du workload ; défaut mémoire VM moins max(256 Mio, arrondi_au_Mio_supérieur(mémoire_VM / 32))
--cpu-quota USECquota cgroup CPU par période ; entier décimal, défaut 100000
--cpu-period USECpériode cgroup CPU ; entier décimal 1000–1000000, défaut 100000
--tasks Nplafond pids.max, 16–32768 ; défaut 256
--wall-time MSdeadline après READY, 1 s–7 jours ; entier décimal, défaut 600000
--network-connections Nconnexions Egress simultanées, 1–4096 ; exige une policy médiée
--network-bytes-per-connection SIZEoctets relayés maximum par connexion Egress, 4 Kio–1 Tio
--network-bytes-total SIZEoctets relayés cumulés maximum pour toute l'exécution, 4 Kio–1 Tio ; omission = illimité
--receipt-schema v7contrat JSON obligatoire pour OCI ; v1 reste le défaut local

Les trois options réseau sont des ressources de déploiement, pas une allowlist. Elles n'autorisent aucune destination : au moins un --net HOST:PORT ou une policy MIR/JSON mediated reste nécessaire. Warden les refuse avec network:none, direct, fd4 ou sans --image, car dans ces formes Egress ne pourrait pas prouver leur application.

--image est volontairement une paire référence/digest atomique pour éviter les combinaisons partielles. Le CLI sépare la référence du suffixe @sha256:... avant d'appeler maelys_target_spec_create_oci(). La commande après -- est toujours l'argv invité exact : elle n'est ni l'entrypoint hôte, ni une commande shell implicite.

Pour une cible OCI, --workspace DIR désigne toujours la source hôte fiable. --guest-workspace désigne sa destination dans l'invité. --ro ou --rw contrôle les droits de cette vue et continue de produire une MIR, tandis que le mapping hôte-invité appartient au plan mécanique et non à la décision MIR.

Dans la première CLI OCI, un chemin relatif de --allow-read ou --allow-write est résolu sous le workspace hôte puis projeté sous le workspace invité avec le même suffixe relatif. Un chemin absolu est refusé. Une règle filesystem JSON ou MIR utilisant une racine hôte absolue fournie par l'utilisateur est également refusée ; les racines symboliques fiables de runtime et de répertoire temporaire restent compilées séparément. La CLI n'expose jamais automatiquement /chemin/hôte comme /chemin/hôte dans l'image. Une future syntaxe explicite --mount-ro HOST:GUEST / --mount-rw HOST:GUEST pourra introduire cette autorité ; elle ne fait pas partie de ce premier jalon. L'API C basse reste disponible pour les mappings mécaniques revus par l'intégrateur.

Sélection du backend

Sans --backend, le planner décide à partir de la cible, des capacités MIR et des preuves disponibles :

CODE
TargetSpec OCI linux/arm64 ou linux/amd64
                   │
                   ▼
             candidate planner + budget ABI 10
              ├── apple-container (refus : limites non représentables)
              ├── apple-container-bubblewrap (VM Apple + cgroup Warden)
              ├── apple-containerization
              ├── apple-containerization-bubblewrap
              ├── krun
              └── krun-bubblewrap
                   │
             plateforme + capacités
                   │
                   ▼
              artefact scellé

--backend est un override avancé de reproductibilité et de diagnostic :

CODE
# Le planner choisit le candidat qui satisfait exactement la MIR.
image='ghcr.io/maelys/tool@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
maelys-warden run --ro --image "$image" -- /usr/bin/tool

# Le driver direct est épinglé ; noyau, init ou Rosetta absent => échec.
maelys-warden run --ro --image "$image" \
  --platform linux/amd64 --backend apple-containerization -- /usr/bin/tool

# MicroVM Krun + règles filesystem récursives dans Bubblewrap invité.
maelys-warden run --rw --workspace "$PWD" --image "$image" \
  --platform linux/arm64 --backend krun-bubblewrap -- /usr/bin/tool

# Virtualization.framework direct + noyau, init et ext4 Warden.
maelys-warden run --rootfs-only --image "$image" --platform linux/arm64 \
  --backend apple-containerization -- /usr/bin/tool

# Même VM directe, avec règles récursives abaissées par Bubblewrap invité.
maelys-warden run --ro --workspace "$PWD" --allow-read . \
  --image "$image" --platform linux/arm64 \
  --backend apple-containerization-bubblewrap -- /usr/bin/tool

# Service Apple 1.3 + Bubblewrap et cgroup-v2 Warden dans l'invité.
maelys-warden run --ro --image "$image" --platform linux/arm64 \
  --backend apple-container-bubblewrap -- /usr/bin/tool

Il ne signifie jamais « essayer ce backend puis dégrader ». En 0.40, épingler apple-container produit toujours un rejet avant RUN, car son workload root peut modifier le cgroup du service. Épingler apple-container-bubblewrap sélectionne au contraire le composite ABI 10 : VM dimensionnée par le service, cgroup workload appliqué et observé par le bootstrap Warden, puis vue Bubblewrap read-only. Le record physique conserve les preuves de cette distinction.

Le rejet explique la décision au lieu d'imprimer seulement 0x30000 :

CODE
maelys-warden: backend apple-container rejected

Missing required guarantees:
  - resource-vm-limits (required by resource budget)
  - resource-guest-cgroup (required by resource budget)

Why they are required:
  Warden applies the default OCI resource budget:
    VM:       2 vCPU, 1024 MiB
    workload: 768 MiB, CPU quota 100000/100000,
              256 tasks, wall time 600000 ms

apple-container cannot prove all required guarantees.
Capability masks: required=0x00000000000300cf available=0x00000000000040cf missing=0x0000000000030000

Les valeurs exactes dépendent de la MIR et des options de la commande ; le mot default devient caller-supplied lorsqu'une limite a été fournie. Les masques restent présents pour la corrélation avec les outils, mais les noms et leur provenance sont la surface destinée à l'utilisateur. Il n'existe pas de --budget none : toute cible OCI conserve un budget scellé obligatoire.

--rootfs-only est une troisième source de politique inline, réservée aux cibles OCI. Elle ajoute la racine runtime et /tmp à la MIR, mais aucune règle workspace, refuse --workspace, --guest-workspace, --allow-read et --allow-write, puis choisit / comme cwd invité implicite. Elle sert notamment au candidat direct, dont le vrai NETWORK_NONE interdit toute vue hôte susceptible d'exposer une socket Unix. --ro et --rw conservent exactement leur sens de vue /workspace, sans dégradation vers NETWORK_IP_NONE.

Avec apple-containerization, la racine ext4 est immuable : l'image doit donc déjà contenir le répertoire /workspace lorsque --ro ou --rw est utilisé. Une image construite avec WORKDIR /workspace satisfait naturellement ce contrat. --rootfs-only ne crée et ne requiert aucune vue /workspace.

--ephemeral-rw ne rend jamais root.ext4 modifiable. Warden exige le bit ABI 10 ROOT_EPHEMERAL_WRITE; maelys-init monte un tmpfs privé et borné comme upper/work d'OverlayFS, puis place les vues utilisateur au-dessus. Après le reap, l'overlay est démonté et son résultat apparaît dans le receipt v7 :

CODE
maelys-warden run --rootfs-only --ephemeral-rw-size 512M \
  --image "$image" --platform linux/arm64 \
  --receipt run.json -- /usr/bin/tool build

krun, krun-bubblewrap, apple-containerization et apple-containerization-bubblewrap annoncent cette capacité depuis 0.35. Le composite Apple service est sélectionnable pour une racine read-only, mais refuse --ephemeral-rw plutôt que d'ignorer la demande.

Validations fail-closed

  • --platform sans --image est refusé ;
  • --guest-workspace et --guest-cwd sans --image sont refusés ;
  • un --allow-read/--allow-write absolu ou une règle utilisateur à racine hôte absolue est refusé pour une cible OCI ;
  • référence sans digest, digest malformé ou plateforme inconnue sont refusés avant toute résolution réseau ;
  • linux/amd64 ne retombe jamais sur linux/arm64 ;
  • --network-frontend fd4 est refusé pour les quatre candidats OCI, Apple comme Krun, tant que le broker invité fd4/vsock n'est pas livré : aucun d'eux n'annonce NETWORK_EXTERNAL_CHANNEL ; auto choisit proxy ;
  • --io isolated-duplex est refusé tant que le canal applicatif fd 3 n'est pas transporté explicitement à travers le driver ;
  • une cible OCI produit exclusivement v7 ; une cible locale conserve v1 tant qu'un schéma enrichi n'est pas demandé explicitement ;
  • aucune disparition de driver, helper, runtime ou preuve après prepare ne provoque de resélection au spawn.
CODE
maelys-warden run --ro -- codex --model gpt-5.6 --full-auto
maelys-warden run --rw -- npm test -- --runInBand
maelys-warden run --ro -- /bin/sh -c 'printf "%s\n" hello'

Dans le deuxième exemple, le premier -- appartient à Warden ; le second est transmis à npm test.

Sources de policy

Une et une seule source est obligatoire :

CODE
maelys-warden run --ro -- COMMAND
maelys-warden run --rw -- COMMAND
maelys-warden run --rootfs-only --image REF@sha256:HEX -- COMMAND
maelys-warden run --policy policy.json -- COMMAND
maelys-warden run --plan policy.mir -- COMMAND
SourceRôle
--roconstruit une MIR avec le workspace récursivement lisible
--rwconstruit une MIR avec le workspace récursivement modifiable
--rootfs-onlyconstruit une MIR OCI sans aucune vue du workspace hôte
--policy FILEcompile un JSON source v3 strict en MIR canonique en mémoire
--plan FILEdécode et vérifie une MIR v3 binaire déjà canonique

Ces trois raccourcis ne sont pas des noms de profil et ne chargent aucun fichier caché. La façade appelle directement le builder MIR. Ils ajoutent le runtime minimal en lecture, le répertoire temporaire en écriture, network:none par défaut et l’héritage du confinement aux descendants. --ro/--rw ajoutent le workspace ; --rootfs-only l'omet et reste réservé à OCI.

Il n’existe volontairement ni --profile ni --preset. Une policy durable et nommée est un fichier JSON explicite ; sa forme compilée et reproductible est un fichier .mir fourni à --plan.

Composition inline

Les options inline ne fonctionnent qu’avec --ro, --rw ou --rootfs-only. Les options filesystem sont toutefois refusées avec --rootfs-only, puisqu'elles réintroduiraient précisément la vue hôte omise. Toutes sont refusées avec --policy et --plan, de sorte qu’une policy externe ou scellée ne puisse jamais être élargie implicitement.

Filesystem

CODE
maelys-warden run --ro \
  --allow-read /opt/toolchain \
  --allow-write build \
  -- make all
  • un chemin relatif est résolu sous le workspace fiable ;
  • un chemin absolu désigne un chemin hôte explicite ;
  • la règle est récursive (scope: tree) ;
  • la cible doit exister au moment de la résolution de policy ;
  • les options sont répétables.

--ro signifie « workspace en lecture seule », pas « aucun octet inscriptible sur toute la machine » : le répertoire temporaire reste volontairement inscriptible et --allow-write peut accorder une sous-arborescence précise.

Réseau

CODE
maelys-warden run --ro \
  --net github.com:443 \
  --net registry.npmjs.org:443 \
  -- npm ci

Chaque --net HOST:PORT ajoute une destination TCP à l’allowlist et implique automatiquement network.mode=mediated. Il n’existe donc pas d’option redondante --network mediated. Les hôtes sont des noms DNS canoniques ; les ports doivent être compris entre 1 et 65535.

Sans --net, une policy inline utilise network:none. L’autorisation large est volontairement bruyante :

CODE
maelys-warden run --ro --net direct -- COMMAND

--net direct autorise le réseau IP ambiant et ne peut pas être combiné avec des destinations médiées. Le mot-clé n'est reconnu que lorsqu'il constitue la valeur entière de l'option. --net direct:443 suit donc la grammaire HOST:PORT et désigne l'hôte DNS littéral direct, pas le mode réseau direct.

Frontend réseau

--network-frontend ne donne aucune autorisation. Il choisit seulement l’interface par laquelle une policy médiée est appliquée par Egress.

ValeurClientChemin
autodéfaut produitproxy sur Linux et macOS
proxycurl, git, npm et outils non modifiésvariables HTTP/SOCKS → Egress
fd4programme Maelys-awarerequête fd 4 → Egress → SCM_RIGHTS

Exemple explicite, équivalent au défaut pour un outil standard :

CODE
maelys-warden run --ro \
  --net github.com:443 \
  --network-frontend proxy \
  -- curl https://github.com/

Le frontend est sans effet pour network:none et network:direct. Aucun échec du médiateur ne bascule vers le réseau direct.

Toutes les options d’exécution

OptionEffet
--workspace DIRracine fiable substituée à workspace dans la MIR ; défaut : répertoire courant
--backend NAMEsélection exacte du backend, sans fallback vers POSIX
--network-frontend auto|fd4|proxyfrontend d’une policy médiée ; défaut : auto
--environment inherit|emptybase d’environnement ; défaut inherit en local et empty en OCI ; inherit est refusé en OCI
--io captured-stdio|isolated-duplexchoisit entre les flux Unix ordinaires et un canal protocolaire isolé sur le fd 3 ; voir ci-dessous
--env NAME[=VALUE]overlay répété ; NAME importe sa valeur depuis l’environnement du CLI sans la placer dans argv ; NAME=VALUE est l’override explicite visible dans argv
--invocation-id IDdonne une même clé de corrélation au receipt d’exécution et aux receipts Egress
--emit-mir FILEécrit avant le spawn la MIR canonique effective, en mode 0600
--receipt FILEécrit après l’attente le receipt global d’exécution JSON, en mode 0600
--network-receipts FILEécrit après l’attente un receipt Egress JSON Lines par connexion
--relay-bootstrap FILEremplace explicitement le helper Linux Bubblewrap normalement autodécouvert
--versionaffiche la version
--helpaffiche l’aide

Modes d’entrée/sortie

--io captured-stdio

C’est le mode par défaut et celui des commandes ordinaires : shell, compilateur, curl, git, npm, agent en ligne de commande, etc. Warden crée trois pipes séparés et les relaie sans changer leur signification :

CODE
terminal / appelant                 workload confiné

stdin   ──────────────────────────► fd 0
stdout  ◄────────────────────────── fd 1
stderr  ◄────────────────────────── fd 2

Par exemple, le texte fourni sur stdin atteint cat et sa sortie revient sur le stdout de Warden :

CODE
printf 'bonjour\n' |
  maelys-warden run --ro --io captured-stdio -- /bin/cat

Le résultat est :

CODE
bonjour

L’option peut être omise puisque ce mode est le défaut :

CODE
maelys-warden run --rw -- npm test -- --runInBand

Ici, le second -- est un argument de npm test, et ses sorties restent des sorties terminal ordinaires.

--io isolated-duplex

Ce mode est destiné à un enfant conçu pour parler un protocole sur le fd 3, par exemple un provider MCP ou maelys-code-runner. Executor crée un socketpair full-duplex et installe une extrémité exactement sur le fd 3 de l’enfant. Le CLI transporte des octets opaques sans interpréter JSON, MCP ou le protocole applicatif :

CODE
appelant                           maelys-warden                 enfant

stdin   ─── trames requête ──────► relais ────────────────────► fd 3
stdout  ◄── trames réponse ─────── relais ◄──────────────────── fd 3
stderr  ◄── diagnostics ─────────────────────────────────────── fd 2
                                                              fd 0 = /dev/null
                                                              fd 1 = diagnostics

Le stdout de l’enfant est redirigé vers ses diagnostics afin qu’un printf accidentel ne puisse pas corrompre le protocole. Le stdout du CLI reste, lui, réservé aux octets lus sur le fd 3. Une fermeture de stdin produit une demi-fermeture : l’enfant peut encore envoyer sa dernière réponse.

Exemple minimal d’un enfant conscient du contrat fd 3 :

CODE
printf 'ping\n' |
  maelys-warden run --ro --io isolated-duplex -- \
    /bin/sh -c \
    'IFS= read -r frame <&3; printf "reply:%s\n" "$frame" >&3'

Le stdout protocolaire du CLI contient :

CODE
reply:ping

Une commande standard qui ne lit que le fd 0, telle que cat, ne doit pas être lancée dans ce mode. isolated-duplex est également indépendant du frontend réseau fd4 : le fd 3 transporte le protocole applicatif ; le fd 4 demande à Egress une connexion réseau autorisée.

Combiner --io et --network-frontend

Les deux options répondent à des questions différentes :

  • --io définit comment l’appelant dialogue avec le workload ;
  • --network-frontend définit comment le workload obtient une connexion autorisée par Egress.

Elles sont donc orthogonales. --network-frontend fd4 ne remplace ni stdin, ni stdout, ni le canal protocolaire fd 3.

--ioFrontend réseaufd visibles par le workloadUsage typique
captured-stdioproxy/auto0 stdin, 1 stdout, 2 stderrcurl, git, npm, Codex non modifié
captured-stdiofd40/1/2 ordinaires, 3 neutralisé, 4 contrôle EgressCLI Maelys-aware qui affiche normalement sur stdout
isolated-duplexproxy/auto0 neutralisé, 1 diagnostics, 2 diagnostics, 3 protocoleworker protocolaire utilisant les bibliothèques HTTP/SOCKS standards
isolated-duplexfd40 neutralisé, 1/2 diagnostics, 3 protocole, 4 contrôle Egressworker entièrement Maelys-aware

Le frontend fd4 n’est installé que pour une policy network:mediated. Avec network:none ou --net direct, il n’existe pas de canal réseau fd 4.

captured-stdio + fd4

L’exemple livré example-fd4-client demande une socket à Egress sur le fd 4, mais copie la réponse HTTP sur son stdout ordinaire :

CODE
make examples

maelys-warden run --ro \
  --net example.com:80 \
  --io captured-stdio \
  --network-frontend fd4 \
  -- build/release/bin/example-fd4-client example.com 80

Le chemin fonctionnel est :

CODE
terminal ◄── réponse HTTP ── stdout fd 1 du client
                              client
                                │ demande example.com:80 sur fd 4
                                ▼
                         broker fd4 → Egress
                                │ SCM_RIGHTS
                                ▼
                         nouveau stream connecté

Le code du client appelle maelys_netclient_connect(4, ...). Egress vérifie l’allowlist et lui transfère un nouveau stream connecté avec SCM_RIGHTS; le fd 4 reste le canal de contrôle.

isolated-duplex + fd4

Cette combinaison réserve le fd 3 aux requêtes/réponses applicatives et le fd 4 aux demandes réseau. L’exemple example-fd3-fd4-worker attend la commande fetch sur fd 3, demande example.com:80 à Egress sur fd 4, puis renvoie la réponse HTTP sur fd 3 :

CODE
make examples

printf 'fetch\n' |
  maelys-warden run --ro \
    --net example.com:80 \
    --io isolated-duplex \
    --network-frontend fd4 \
    -- build/release/bin/example-fd3-fd4-worker example.com 80
CODE
appelant
   │ "fetch"                         plan réseau scellé
   ▼                                         │
Warden stdin ──► enfant fd 3                 ▼
                    │                  broker fd4 → Egress
                    │ demande sur fd 4        │
                    │────────────────────────►│ vérifie example.com:80
                    │◄────────────────────────│ SCM_RIGHTS(stream)
                    │
                    ├── HTTP sur le stream reçu
                    │
Warden stdout ◄─────┘ réponse HTTP sur fd 3

Le code du worker illustre cette séparation. Le descripteur connecté rendu par SCM_RIGHTS est un troisième objet, distinct des fd 3 et 4, et doit être fermé par le workload. Les appels qui partagent le canal de contrôle fd 4 doivent être sérialisés.

Corrélation avec --invocation-id

--invocation-id ID attribue une identité logique à une exécution. Warden la copie dans le receipt global et Egress dans chaque receipt de connexion. Cela permet à un orchestrateur ou à une CI de joindre les deux fichiers sans se fonder sur un PID, un nom de fichier ou une proximité temporelle.

CODE
run_id='ci-4821-attempt-2'

maelys-warden run --ro \
  --net github.com:443 \
  --invocation-id "$run_id" \
  --receipt execution.json \
  --network-receipts connections.jsonl \
  -- curl https://github.com/

On retrouve alors la même valeur dans les deux sorties :

CODE
jq -r '.invocationId' execution.json
jq -r '.invocationId' connections.jsonl

L’identifiant contient de 1 à 256 octets et aucun caractère de contrôle. Ce n’est ni un secret, ni un droit, ni une preuve d’identité : c’est seulement une clé de corrélation. Lors d’une exécution médiée sans valeur explicite, l’adaptateur Egress génère un identifiant local ; fournir le sien reste nécessaire pour corréler l’exécution avec un système externe.

MIR et receipts

Les trois options de sortie correspondent à trois moments et trois granularités différents :

CODE
policy inline / JSON / MIR
            │
            ▼
    compilation canonique
            │
            ├──► --emit-mir             avant le spawn, 1 policy binaire
            ▼
          spawn
            │
            ├──► Egress observe 0..N connexions
            ▼
           wait
            │
            ├──► --receipt              1 résultat global JSON
            └──► --network-receipts     0..N résultats réseau JSONL

--emit-mir FILE

Cette option sérialise la policy effective, validée et canonique sous sa forme MIR v3 binaire. Le fichier est écrit atomiquement en mode 0600 avant le spawn. Si l’écriture échoue, le workload n’est pas lancé.

CODE
maelys-warden run --ro \
  --allow-read /opt/toolchain \
  --net github.com:443 \
  --emit-mir effective.mir \
  -- /usr/bin/true

effective.mir contient les décisions de policy : règles filesystem, réseau, médiateur attendu et confinement de l’arbre de processus. Il ne contient ni le résultat du workload, ni le backend finalement sélectionné, ni ses observations. Comme il est canonique, on peut le hasher, l’archiver et le rejouer :

CODE
shasum -a 256 effective.mir
maelys-warden run --plan effective.mir -- /usr/bin/true

Une MIR produite par --ro ou --rw inclut notamment le droit de lire l’exécutable exact. Il faut donc la considérer comme le plan de policy du workload prévu, pas comme une policy universelle pour n’importe quelle commande.

--receipt FILE

Cette option écrit un objet JSON global après l’attente du workload. La CLI locale produit actuellement maelys.execution-receipt/v1, avec :

  • digest MIR/décision et digest du plan mécanique ;
  • backend sélectionné, version, capacités requises et disponibles ;
  • mode, frontend et médiateur réseau ;
  • début, tentative d'arrêt forcé, complétion, code de sortie ou signal ;
  • invocationId, lorsqu’il existe.

Le receipt v2 reste le format gelé du chemin RunSpec ABI 7. Executor ABI 8 produit les preuves nécessaires aux receipts v3/v4 : cible, rapport de candidats, capacités exactes et couches VM, sandbox et traduction. v4 ajoute la fermeture runtime Krun attestée. La CLI locale émet v1 par défaut. Les receipts v3/v4/v5 restent gelés pour les exécutions historiques. Executor ABI 10 et Warden 0.38 ajoutent v6 : budget VM/workload demandé et appliqué, observations cgroup-v2 et preuve de cleanup. Warden 0.39 ajoute v7 sans modifier v6 : quotas Egress demandés/appliqués/observés et preuve du failsafe contrôleur Krun lorsqu'il existe.

CODE
maelys-warden run --ro \
  --invocation-id local-build-7 \
  --receipt build.execution.json \
  -- make test

jq '{invocationId, backend: .backend.name, outcome}' \
  build.execution.json

Cette requête jq vise le receipt v1, qui reste le format par défaut d'une exécution locale. Avec un ancien receipt v3/v4/v5/v6 ou le receipt OCI v7, le backend retenu se lit dans .selection.candidate et la chaîne d'isolation détaillée dans .layers, énumérée du plus extérieur au plus intérieur : pour les candidats Krun, seatbelt-host, puis krun, puis bubblewrap pour le seul composite.

Le receipt décrit l’exécution entière. Il n’est disponible avec son issue finale qu’après wait; il ne peut donc pas être écrit avant le spawn. C’est un document d’observation auditable, pas à lui seul une attestation signée. Le schéma local est dans execution-receipt-v1.schema.json ; le schéma v2 gelé documente ABI 7 ; le schéma v3 gelé documente ABI 8 ; le schéma v4 ajoute la fermeture dynamique Krun (variant, features, components, closureEvidence). Depuis 0.33.0, les couches Krun et Containerization direct ajoutent aussi guestKernel : contrat commun guest-kernel/v1, format raw-arm64, chargeur VMM et preuve SHA-256. Le champ reste optionnel dans le schéma afin que les receipts v4 émis avant 0.33 restent vérifiables.

Le schéma v5 déclare le registre Executor ABI 9. Sa section root atteste read-only ou ephemeral-write, le stockage tmpfs, les bornes demandée/appliquée et le cleanup. Une couche ephemeral-root-overlay contribue exactement la capacité correspondante ; outcome.rusage transporte les compteurs directs de wait4. Les octets root.scratch.requestedBytes/appliedBytes bornent exclusivement le scratch de la racine overlay. Ils ne représentent pas un quota total d'écriture et ne couvrent pas une vue /workspace RW, qui est une ressource hôte séparée avec ses propres bornes.

Le schéma v6 déclare Executor ABI 10. resources.vm et resources.workload séparent les valeurs requested, applied et observed. Le plafond mémoire, le quota CPU, pids.max et la deadline couvrent le cgroup du workload ; ils ne comptabilisent ni le PID 1, ni le relay Egress, ni une vue VirtioFS /workspace RW.

Le schéma v7 ajoute resources.network et resources.controller. Le premier compte seulement les octets de payload admis et relayés par l'Egress de l'exécution. En l'absence de médiation, ses valeurs appliquées sont zéro et sa source vaut not-applicable. Le second distingue le worker VMM Krun d'un service VMM externe, mais reste applied: false/not-applicable dans les deux cas. Les tests physiques ont invalidé RLIMIT_CPU comme mécanisme compatible avec libkrun 1.19.4 ; Warden n'atteste donc aucune protection hôte non prouvée.

ABI 8 ne réinterprète pas v2 : v2 garde une seule couche/candidat. La chaîne VM, Bubblewrap et Rosetta apparaît dans v3/v4. --receipt continue à produire v1 pour une cible locale tant que l'appelant ne choisit pas explicitement un schéma enrichi.

--network-receipts FILE

Cette option écrit du JSON Lines : chaque ligne est un objet JSON autonome pour une tentative de connexion traitée par Egress. Une ligne contient notamment la destination, le résultat, les durées, les compteurs d’octets, le digest de policy, le principal, la génération de policy et invocationId.

CODE
maelys-warden run --ro \
  --net github.com:443 \
  --invocation-id download-19 \
  --network-receipts download.connections.jsonl \
  -- curl https://github.com/

jq -c '{host, port, result, bytesFromClient, bytesToClient}' \
  download.connections.jsonl

Le fichier peut contenir zéro ligne si aucune connexion médiée n’a eu lieu, et plusieurs lignes si le workload ouvre plusieurs connexions. Il n’apporte pas d’observation utile pour network:none ou --net direct, puisque ces modes ne font pas passer les connexions par Egress.

Les trois fichiers sont facultatifs et ne sont jamais mélangés au stdout du workload. Les écritures du CLI sont atomiques et utilisent le mode 0600.

Exemple combiné :

CODE
maelys-warden run \
  --policy examples/policies/github-mediated.json \
  --invocation-id release-2026-08-26 \
  --emit-mir effective.mir \
  --receipt execution.json \
  --network-receipts connections.jsonl \
  -- curl https://github.com/

effective.mir est écrit avant le spawn : il peut être validé, hashé ou réutilisé ensuite avec :

CODE
maelys-warden run --plan effective.mir -- curl https://github.com/

--relay-bootstrap FILE

maelys-guest-supervisor est un petit binaire exécutable livré dans libexec. L'option avancée ci-dessous configure son rôle de relay pour la combinaison suivante :

CODE
Linux + backend Bubblewrap + policy réseau médiée + frontend proxy

Dans le namespace réseau privé Bubblewrap, une instance du helper démarre le relay L4 loopback, attend sa readiness, puis lance le workload. Le fichier est partagé par l’installation, mais chaque exécution possède ses propres processus bootstrap/relay, son socket privé et son instance Egress. Le relay ne décide aucune permission : Egress authentifie et applique l’allowlist.

Le CLI le découvre normalement, notamment à côté de l’installation puis dans son répertoire libexec; l’utilisateur n’a rien à fournir. L’override est réservé aux bundles hermétiques, tests, conteneurs ou installations sous un préfixe non conventionnel :

CODE
maelys-warden run --ro \
  --backend bubblewrap \
  --net github.com:443 \
  --network-frontend proxy \
  --relay-bootstrap /opt/maelys/libexec/maelys-guest-supervisor \
  -- curl https://github.com/

Le chemin explicite doit être absolu, canonique et désigner un fichier régulier exécutable fiable. Avec Seatbelt et une policy médiée/proxy, cette option est refusée : Seatbelt rejoint directement le port loopback privé de Egress. Elle n'est pas consommée par le frontend fd4, network:none ou --net direct : la validation la refuse dans ces combinaisons au lieu de l'ignorer. Pour une cible OCI médiée, elle remplace explicitement le relay du bundle installé ; le chemin reste donc une autorité hôte avancée. Une erreur de découverte ou de readiness interdit le lancement du workload ; il n’existe aucun fallback réseau ambiant.

Le nom de l'option reste centré sur ce rôle réseau. Le même fichier est aussi monté automatiquement comme superviseur ABI 10 du composite Apple Container ; ce second usage ne se configure jamais avec --relay-bootstrap.

Codes de sortie

  • sortie normale : code du workload ;
  • terminaison par signal : 128 + signal ;
  • 64 : ligne de commande invalide ;
  • 70 : préparation, confinement ou supervision impossible ;
  • 74 : workload réussi mais écriture d’un receipt impossible ;
  • 127 peut provenir du workload si son exec interne échoue.