Isolation, scale, agency & sortir de la boucle
Le module du hors-site s'ouvre : pourquoi un conteneur nu ne suffit pas à laisser des agents travailler sans vous, ce qu'une boîte de l'usine doit garantir — jetable, fermée, bornée — et comment sortir de la boucle sans lâcher le contrôle. Premières pièces : le préflight et l'image de base des boîtes.
Votre salle de contrôle est complète depuis hier : chaque run laisse sa trace, coût et jetons
compris. Mais observez-vous pendant un run : vous lancez just adw sdlc, et vous restez là.
Vous jetez un œil au terminal, vous vérifiez un diff, vous gardez une main près du clavier,
parce que tout cela tourne chez vous : votre repo, vos clés, votre réseau. Tant que l’usine
habite votre poste de travail, vous êtes à la fois le directeur et le bâtiment.
Ce chapitre ouvre le module du hors-site. À la fin, vous saurez précisément ce qui manque à votre poste, et à un conteneur lancé à la va-vite, pour laisser des agents travailler des heures sans vous, et ce que « sortir de la boucle » veut dire quand on refuse de perdre le contrôle. Deux pièces aujourd’hui, toutes deux déterministes et gratuites : le préflight du hors-site, et l’image de base des boîtes que vous monterez au chapitre 22, des conteneurs Podman, sur votre machine, fermés par construction. Une VM louée (exe.dev) restera l’option quand il faudra de l’échelle, et passera par le même port.
Pourquoi un conteneur nu ne suffit pas
L’idée en une phrase
L’isolation qui autorise le hors-boucle tient en trois propriétés : jetable (créée et détruite en une seconde, sans regret), fermée (pas de root, aucune capacité, et un réseau qui refuse par défaut : une seule sortie, vers une liste d’hôtes) et bornée par les credentials (ce que la boîte ne peut pas faire = les clés qu’elle n’a pas). Cette frontière est une pièce du côté déterministe de l’usine, jamais une option par défaut de votre moteur de conteneurs.
Points clés
- Le choix assumé du module : les agents vivent DANS la boîte. Harnais installés dans l’image, dans la même pièce que le code, pas dehors à piloter un shell distant commande par commande. Un agent qui vit avec le codebase lit, teste et committe nativement. Un agent qui tend le bras à travers le mur traverse la frontière à chaque geste, et c’est votre environnement qui est au bout du bras.
- L’usine entière voyage. Le repo est copié dans la boîte tel quel : vos scripts PEP 723 du
chapitre 4 tournent sur un système vierge avec
uv runpour tout contrat, votrejustfileest la même surface dedans et dehors. C’est le pari de la portabilité, tenu depuis le module 2, qui rend ce module simplement possible. - Un conteneur nu est jetable, mais ni fermé ni borné.
podman runpar défaut donne au processus tout le réseau sortant, souvent root, et toutes les capacités qu’un noyau accorde : un agent qui y trouve une clé peut l’envoyer n’importe où. La boîte de l’usine retire les trois : utilisateur ordinaire,--cap-drop ALL, et un réseau interne sans route vers l’extérieur, dont la seule sortie est une porte, un second conteneur qui n’ouvre que vers la passerelle de modèles, PyPI et npm, et journalise chaque refus. - Un seul niveau d’imbrication, garanti par les clés et par la porte. La boîte reçoit tout le repo, y compris les recettes d’orchestration, mais ni le socket Podman de votre machine, ni le compte exe.dev, ni la clé qui fabrique des clés (chapitre 23). Une boîte qui tenterait de monter une boîte récolte un refus, pas un conteneur imbriqué.
- Le noyau reste partagé, et le livre le dit. Un conteneur n’est pas une VM : une faille du noyau exploitée depuis la boîte toucherait votre machine. Pour du code généré sur un payload jouet, c’est le risque que presque tout le monde accepte. Pour le fermer aussi, l’option exe.dev (une VM par boîte, un noyau dédié) passe par le même port, et c’est tout l’intérêt de l’avoir fait passer par un port.
Exemple concret
Comparez deux incidents. Un agent local, lancé dans un conteneur nu, réseau ouvert, part en
vrille pendant un build : il a lu .env, il peut l’envoyer où il veut. Vous interrompez, vous
inspectez ce qu’il a touché, vous changez la clé par précaution, et vous y passez votre
soirée, parce que le doute porte sur votre machine et sur votre compte. Le même incident
dans une boîte de l’usine : la porte n’a laissé passer que openrouter.ai, les refus sont
dans son journal, la clé de la boîte est plafonnée (chapitre 23), la trace du module 5 reste
lisible jusqu’à la dernière seconde, et le verdict tient en une commande de démontage :
conteneur, porte et réseau disparaissent, il ne reste rien à nettoyer. Monter une boîte neuve
prendra une seconde au chapitre 22, pour zéro euro. Votre soirée vaut plus que ça.
Quatre environnements face aux trois propriétés
| Propriété | Votre machine | Conteneur nu (podman run) | Boîte de l’usine (Podman fermé) | VM exe.dev (option) |
|---|---|---|---|---|
| Jetable — détruire sans regret | non | oui | oui, en une seconde | oui, en quelques secondes |
| Fermée — pas de root, réseau refusé par défaut | non | non — réseau ouvert | oui : porte + liste d’hôtes | à construire (réseau ouvert) |
| Bornée par les credentials | non — tout est là | à construire | par conception (ch. 23) | par conception (ch. 23) |
| Noyau dédié, loin de votre machine | non | non | non — noyau partagé | oui |
| Coût | — | 0 | 0 | ~20 $/mois |
Config — la frontière, vue d’en haut
Le contrat du module, que les chapitres 22 et 23 rendront exécutable. Rien à copier aujourd’hui, mais chaque ligne deviendra une pièce :
# Ce qui ne quitte JAMAIS l'hôte (votre machine) :
# - le socket Podman (ou le compte exe.dev) : la capacité de créer et détruire des boîtes (ch. 22)
# - OPENROUTER_PROVISIONING_KEY : la capacité de fabriquer des clés (ch. 23)
# Ce qui traverse vers la boîte :
# - le repo entier — l'usine voyage, `uv run` est tout le contrat (ch. 4)
# - une clé d'inférence JETABLE, plafonnée, révoquée au démontage (ch. 23)
# Ce que la boîte ne peut pas faire, par construction :
# - sortir vers un hôte hors liste — la porte refuse, et l'écrit (ch. 22)
# - monter une boîte — un niveau d'imbrication, garanti par les clés absentes
Piège courant : « un conteneur, c’est déjà de l’isolation » est inexact. Un conteneur nu isole un système de fichiers, pas une situation. Il laisse le réseau grand ouvert (une clé lue peut partir n’importe où), tourne souvent en root, et ne borne aucune dépense. Ce que le module construit, c’est la boîte fermée : la même seconde de démarrage, mais un réseau qui refuse par défaut, un utilisateur ordinaire, et une clé qui ne vaut que son plafond.
Sortir l’humain de la boucle sans perdre le contrôle
L’idée en une phrase
Sortir de la boucle ne consiste pas à disparaître mais à déplacer votre contrôle des gestes vers les surfaces déterministes de l’usine : les gates décident « fini », la trace enregistre tout, la porte journalise ce qui sort. Quatre leviers restent humains : le prompt qui lance, la gate qui accepte, la moisson qui rapatrie, le démontage qui détruit.
Points clés
- Un système qui vous exige à chaque tour ne passe pas à l’échelle, et fait de vous le risque homme-clé de votre propre usine : votre présence devient la ressource rare, avant les tokens et avant les machines. La boucle doit pouvoir orbiter sans vous, vous lisez la trace.
- Trois étages de commandement, chacun ne commande que l’étage intérieur. L’orchestrateur hors-boîte, sur votre machine, gère des boîtes : monter, remplir, observer, moissonner, démonter. L’orchestrateur en boîte, une session de harnais vivante dans le conteneur, reçoit le travail délégué et lance l’usine. Et les agents ADW restent ce qu’ils sont depuis le chapitre 8 : des nœuds bornés dans des phases nommées. Le chapitre 24 câblera les deux premiers étages, le troisième est déjà chez vous.
- On observe de l’extérieur, on ne met jamais la main dedans. La trace du module 5 voyage
avec le repo : chaque run dans la boîte écrit son journal SQLite comme il le fait chez vous,
et vous le lisez depuis l’hôte avec
podman exec, jamais un éditeur ouvert dans la boîte. Intervenir dans une boîte en cours de run, c’est réintroduire la main humaine que le module vient de sortir, et invalider ce que la trace raconte. - Le démontage reste une décision humaine, la moisson ne fusionne jamais. Une boîte détruite, c’est la preuve et les artefacts, disparus : rien ne doit enchaîner automatiquement vers le démontage. Et la moisson du chapitre 25 rapatriera les commits à côté de votre branche, jamais dedans : comparer et choisir restent des gestes à vous.
Exemple concret
Prenez le SDLC du chapitre 13 : environ dix minutes, 40 à 60 centimes. Aujourd’hui, ces dix
minutes sont aussi dix minutes de votre attention, vous surveillez au cas où. Le même run
dans une boîte : vous lancez, vous partez faire autre chose, et vous revenez lire runs,
lanes, le grain comptable du chapitre 20 et le journal de la porte, soit une minute de
lecture pour dix minutes de machine. Multipliez : trois runs en parallèle dans trois boîtes
coûtent toujours une minute de lecture chacun, pas trois fois dix minutes de présence, et
trois boîtes Podman sur un portable ordinaire tiennent sans effort. Votre attention cesse
d’être proportionnelle au nombre de runs, c’est toute la promesse du module, et la condition
du best-of-N que vous lancerez au chapitre 25.
Les trois étages de commandement
| Étage | Vit où | Commande quoi |
|---|---|---|
| Orchestrateur hors-boîte | votre machine | les boîtes : monter, remplir, observer, moissonner, démonter |
| Orchestrateur en boîte | le conteneur (ou la VM), une session de harnais vivante | l’usine : lancer les ADW, surveiller, rendre compte |
| Agents ADW | des phases bornées dans le runner | le travail : scout, plan, build, review, document |
Commande — la surface du hors-site, telle qu’elle arrive
Une seule version suffit ici : les pièces du jour sont entièrement déterministes, aucun harnais n’est appelé. Dans la boîte, les deux harnais seront présents, derrière le même port du chapitre 7. Voici la surface que les prochains chapitres poseront, au futur :
# ch. 22 — just/sandbox/lifecycle.just : le cycle de vie d'une boîte
# create → fill → setup → execute → observe → teardown
# ch. 23 — just/sandbox/keys.just : fabriquer, plafonner, révoquer les clés jetables
# ch. 24-25 — just/sandbox/orch.just : déléguer, fan-out, moissonner le best-of-N
# aujourd'hui — le préflight, puis l'image de base des boîtes :
uv run adws/sandbox_preflight.py
podman build -t plume-node .
Piège courant : « sortir de la boucle, c’est lancer et prier » est inexact. C’est l’inverse d’un abandon : le contrôle change de forme, pas de mains. Chaque « fini » reste prononcé par une gate que vous avez écrite, chaque geste est tracé, chaque sortie réseau est passée par une porte que vous avez configurée, la dépense est plafonnée par une clé que vous avez fabriquée, et la destruction n’arrive que si vous la demandez. L’agent n’a jamais été aussi surveillé que depuis que vous ne le regardez plus.
Fil rouge — la pièce posée aujourd’hui
Le plan ouvre sa dernière grande zone : le hors-site, just/sandbox/ sur l’arbre du
chapitre 1, voisine de tout ce qui rend l’usine transportable, les scripts PEP 723 du
chapitre 4, le justfile du chapitre 5, le port harnais du chapitre 7, la trace du module 5.
Deux pièces aujourd’hui : adws/sandbox_preflight.py, le gardien déterministe de cette zone,
qui vérifie l’hôte gratuitement avant de monter une boîte, et Containerfile, l’image
de base des boîtes : tout ce dont l’usine a besoin, installé une fois, réseau ouvert, pour
qu’une boîte puisse ensuite vivre réseau fermé. La loi ne bouge pas, l’agent propose et le code
dispose, et elle gagne un corollaire : la frontière aussi est du code. Aucune enveloppe ne
traverse aujourd’hui, aucun token n’est dépensé. Le préflight coûte quelques secondes, l’image
deux minutes et quelques centaines de mégaoctets, une fois. Ils économisent le seul incident
vraiment cher du module : découvrir un moteur mal configuré, une clé manquante ou un
.gitignore troué après avoir monté des boîtes, ou pire, après y avoir versionné un secret.
Travaux pratiques — la pièce du jour
Deux pièces complètes à poser dans le repo compagnon plume-factory, qui devient, chapitre
après chapitre, votre usine logicielle agentique.
Prérequis : Podman, en mode rootless (sans sudo). Sous Linux, votre gestionnaire de
paquets. Sous macOS et Windows, Podman Desktop ou podman machine init puis podman machine start, une petite VM Linux que Podman pilote pour vous, et dans laquelle toutes les commandes
du module tournent telles quelles. Vérifiez avec podman info : rootless: true,
networkBackend: netavark. Un compte exe.dev n’est pas nécessaire, il le deviendra seulement
si vous choisissez l’option échelle.
Pièce — adws/sandbox_preflight.py
Le préflight du hors-site, cousin du doctor.py du chapitre 4 : entièrement côté déterministe,
zéro token. Il lit le moteur choisi (SANDBOX_BACKEND dans .env, podman par défaut),
vérifie ce que ce moteur exige (Podman rootless sur netavark, ou ssh pour exe.dev), l’image
de base, l’outillage commun, la présence des pièces dont le hors-site dépend, et l’hygiène des
secrets, sans jamais imprimer une valeur : la présence d’une clé se constate, elle ne se
montre pas.
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["rich>=13"]
# ///
"""sandbox_preflight — le préflight du hors-site.
Avant de monter une boîte, une vérification gratuite de l'hôte : le moteur
de boîtes (Podman rootless par défaut, exe.dev en option), l'outillage,
les pièces dont le hors-site dépend, et l'hygiène des secrets. Déterministe
de bout en bout, zéro token. Règle absolue : une valeur de clé ne
s'imprime JAMAIS — sa présence se constate, elle ne se montre pas.
uv run adws/sandbox_preflight.py
"""
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from rich.console import Console
from rich.table import Table
# Le moteur de boîtes : SANDBOX_BACKEND dans .env — podman (défaut) ou exedev.
BACKENDS = {"podman": "Podman rootless : conteneur + réseau interne + porte — ch. 22",
"exedev": "exe.dev : une VM par boîte, pilotée par SSH — l'option échelle"}
IMAGE = "localhost/plume-node:latest"
# (outil, requis dès aujourd'hui, rôle dans le hors-site) — communs aux deux moteurs
TOOLS = [
("git", True, "la moisson rapatriera des commits — ch. 25"),
("uv", True, "l'usine voyage : uv run est tout le contrat — ch. 4"),
("just", True, "la surface du hors-site : just/sandbox/ — ch. 22+"),
("bun", True, "le payload Plume tourne aussi dans la boîte — ch. 2"),
]
# Les pièces déjà posées dont le hors-site dépend : leur absence signale
# un repo incomplet — la boîte recevra ce repo tel quel.
PIECES = [
"justfile",
"Containerfile",
"adws/adw_modules/harness.py",
"adws/adw_modules/runner.py",
"adws/adw_modules/tracer.py",
"adws/obs_export.py",
".env.sample",
]
def version_of(tool: str) -> str | None:
"""Sonde un outil sans jamais planter : absent ou muet, c'est None.
On affirme sur la SORTIE, jamais sur le seul code retour — un outil
peut sortir 0 en n'ayant rien à dire."""
if shutil.which(tool) is None:
return None
try:
out = subprocess.run([tool, "-V" if tool == "ssh" else "--version"],
capture_output=True, text=True, timeout=15)
first = (out.stdout or out.stderr).strip().splitlines()
return first[0][:40] if first else None
except (OSError, subprocess.TimeoutExpired):
return None
def env_keys(path: Path) -> dict[str, bool]:
"""Les clés d'un fichier .env : nom -> « a une valeur non vide ».
Les noms seulement — les valeurs ne quittent jamais cette fonction."""
keys: dict[str, bool] = {}
if not path.is_file():
return keys
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
keys[name.strip()] = bool(value.strip())
return keys
def env_value(name: str, default: str) -> str:
"""Une variable NON secrète (le nom du moteur) : .env puis l'environnement, sans écraser."""
for line in (Path(".env").read_text(encoding="utf-8").splitlines() if Path(".env").is_file() else []):
key, _, value = line.strip().partition("=")
if key.strip() == name and value.strip():
os.environ.setdefault(name, value.strip().strip("'\""))
return os.environ.get(name, default).strip().lower()
def podman_info() -> dict | None:
"""`podman info --format json` : rootless ?, quel backend réseau ? None si Podman ne répond pas."""
try:
out = subprocess.run(["podman", "info", "--format", "json"],
capture_output=True, text=True, timeout=60)
return json.loads(out.stdout) if out.returncode == 0 else None
except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError):
return None
def main() -> int:
console = Console()
problems: list[str] = []
warnings: list[str] = []
table = Table(title="plume-factory — préflight du hors-site")
table.add_column("Vérification")
table.add_column("État")
table.add_column("Rôle")
# Le moteur de boîtes, d'abord : c'est lui qui décide de l'outillage requis.
backend = env_value("SANDBOX_BACKEND", "podman")
if backend not in BACKENDS:
problems.append(f"SANDBOX_BACKEND={backend!r} inconnu — podman ou exedev")
backend = "podman"
table.add_row("SANDBOX_BACKEND", backend, BACKENDS[backend])
if backend == "podman":
version = version_of("podman")
table.add_row("podman", version or "[red]MANQUANT[/red]", "le moteur de boîtes — podman.io")
info = podman_info() if version else None
if not version:
problems.append("podman absent — installez-le (Linux : votre gestionnaire de paquets ;"
" macOS/Windows : podman machine init && podman machine start)")
elif info is None:
problems.append("podman ne répond pas — sous macOS/Windows : podman machine start")
else:
host = info.get("host") or {}
rootless = (host.get("security") or {}).get("rootless")
table.add_row("rootless", "oui" if rootless else "[yellow]non[/yellow]",
"aucun démon root : l'agent n'a jamais plus de droits que vous")
if not rootless:
warnings.append("podman tourne en root — le livre suppose le mode rootless (podman sans sudo)")
net = host.get("networkBackend") or "?"
table.add_row("réseau", net, "netavark : réseaux internes + DNS entre conteneurs")
if net != "netavark":
problems.append(f"backend réseau {net!r} — le réseau interne de la boîte exige netavark (Podman ≥ 4)")
image = subprocess.run(["podman", "image", "exists", IMAGE], capture_output=True).returncode == 0
table.add_row(IMAGE, "présente" if image else "[yellow]absente[/yellow]",
"l'image de base des boîtes — Containerfile, ch. 21")
if not image:
warnings.append(f"image {IMAGE} absente — `podman build -t plume-node .` la construit (une fois)")
else:
version = version_of("ssh")
table.add_row("ssh", version or "[red]MANQUANT[/red]", "la porte des VM exe.dev")
if not version:
problems.append("outil requis absent : ssh")
for name, required, role in TOOLS:
version = version_of(name)
if version:
statut = version
elif required:
statut = "[red]MANQUANT[/red]"
problems.append(f"outil requis absent : {name}")
else:
statut = "[yellow]absent[/yellow]"
table.add_row(name, statut, role)
for piece in PIECES:
present = Path(piece).exists()
table.add_row(piece, "présente" if present else "[red]MANQUANTE[/red]", "pièce de l'usine")
if not present:
problems.append(f"pièce manquante : {piece}")
# L'hygiène des secrets — le seul incident vraiment cher du module.
# 1. Le gabarit versionné ne porte JAMAIS de valeur.
for name, filled in env_keys(Path(".env.sample")).items():
if filled and name != "SANDBOX_BACKEND":
problems.append(f".env.sample porte une valeur pour {name} — "
"un gabarit versionné reste vide, toujours")
# 2. Le .gitignore couvre .env et le runtime — la boîte reçoit le repo.
gitignore = Path(".gitignore")
ignored = gitignore.read_text(encoding="utf-8") if gitignore.is_file() else ""
for pattern in (".env", "adws/adw_data"):
if pattern not in ignored:
problems.append(f".gitignore ne couvre pas {pattern}")
# 3. Les clés attendues — présence constatée, valeur jamais montrée.
env = env_keys(Path(".env"))
if not env.get("OPENROUTER_API_KEY", False):
warnings.append("OPENROUTER_API_KEY absente de .env — requise "
"pour tout run agentique (ch. 15)")
if env.get("OPENROUTER_PROVISIONING_KEY", False):
table.add_row("OPENROUTER_PROVISIONING_KEY", "présente (hôte seulement)",
"fabrique des clés — ch. 23")
else:
warnings.append("OPENROUTER_PROVISIONING_KEY absente — normal "
"aujourd'hui : requise au ch. 23, hôte seulement")
console.print(table)
for p in problems:
console.print(f"[red]✗[/red] {p}")
for w in warnings:
console.print(f"[yellow]![/yellow] {w}")
if problems:
console.print("[red]hors-site : NON prêt[/red]")
return 1
console.print(f"[green]hors-site : OK[/green] ({backend}) — {len(warnings)} avertissement(s)")
return 0
if __name__ == "__main__":
sys.exit(main())
Pièce — Containerfile
L’image de base des boîtes, à la racine du repo : Node (pour les deux harnais), git, sqlite3,
python3 (la porte du chapitre 22 en aura besoin), puis bun, uv et just dans le HOME d’un
utilisateur ordinaire. Tout ce que setup installait dans une VM neuve est ici, une fois pour
toutes, construit avec le réseau ouvert. Une boîte, elle, n’aura plus qu’à recevoir le repo.
pi est épinglé à la version contre laquelle l’annexe écrira le socle.
# Containerfile — l'image de base des boites du hors-site (ch. 21).
# Une boite = ce conteneur, un reseau interne et une porte (ch. 22). Tout ce
# dont l'usine a besoin est installe ICI, une fois, avec le reseau ouvert :
# dans la boite, la porte ne laissera passer que la passerelle, PyPI et npm.
# podman build -t plume-node . (une fois, ~2 min, ~700 Mo)
FROM docker.io/library/node:22-bookworm-slim
# Ce que Debian apporte : git (le commit de reference et le patch), sqlite3
# (la trace), curl et unzip (les installeurs), python3 (la porte, ch. 22).
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git jq python3 sqlite3 unzip \
&& rm -rf /var/lib/apt/lists/*
# Les deux harnais du ch. 7, en global. pi est epingle : le socle .pi/ de
# l'annexe est ecrit contre cette version. Claude Code n'aura pas de cle dans
# une boite Podman (la porte ne connait pas l'API Anthropic) : il est la pour
# l'option exe.dev et pour le stamp du ch. 26, qui suppose les deux.
ARG PI_VERSION=0.84.4
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent@${PI_VERSION} \
&& npm install -g @anthropic-ai/claude-code
# Jamais root dans la boite : l'agent est un utilisateur ordinaire, son HOME
# est le seul endroit qu'il ecrit. ~/app recevra le repo (fill, ch. 22).
RUN useradd --create-home --shell /bin/bash agent
USER agent
WORKDIR /home/agent
ENV PATH=/home/agent/.bun/bin:/home/agent/.local/bin:$PATH \
UV_PYTHON_PREFERENCE=only-system
# bun (Plume, ch. 2), uv (les scripts PEP 723, ch. 4), just (la surface, ch. 5)
# — dans le HOME de l'agent, sans sudo, comme les installeurs officiels le proposent.
RUN curl -fsSL https://bun.sh/install | bash \
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
&& mkdir -p /home/agent/.local/bin \
&& curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /home/agent/.local/bin
# La preuve que l'image est complete — la gate de `just sandbox image`.
RUN uv --version && bun --version && just --version && pi --version && claude --version && python3 --version
# Un conteneur vit en dormant ; les phases entrent par `podman exec`.
CMD ["sleep", "infinity"]
La gate du TP
Trois commandes, une par ligne, depuis la racine de plume-factory :
uv run adws/sandbox_preflight.py
podman build -t plume-node .
podman run --rm localhost/plume-node:latest pi --version
Attendu : le tableau des vérifications, rootless : oui, réseau : netavark, puis
hors-site : OK (podman), avec un avertissement jaune sur l’image absente (normal avant la
deuxième commande) et un ou deux sur les clés (normal avant le chapitre 23). La deuxième
construit l’image : ~2 minutes, quelques centaines de mégaoctets, une fois. La dernière
ligne du Containerfile en est la gate, elle affiche les versions de uv, bun, just, pi et
claude. La troisième démarre un conteneur nu depuis l’image, affiche 0.84.4 et le détruit :
une seconde, zéro token. Si vous préférez l’option exe.dev : SANDBOX_BACKEND=exedev dans
.env, ssh exe.dev whoami une fois à la main, et seule la première commande vous concerne.