Notes · Article
Une sandbox dans la sandbox : migrer mon agent open source vers Hermes sur OpenShell
Forge, mon agent open source, clone les dépôts d’inconnus et lance leurs scripts d’installation et leurs suites de tests à longueur de journée. Quand j’ai commencé à le migrer vers Hermes Agent dans NVIDIA OpenShell, j’ai découvert que la sandbox autour de l’agent ne suffisait pas : dans une même sandbox OpenShell, un processus lancé par l’agent récupère les règles réseau de l’agent. Les workers de Forge ont donc une deuxième barrière à l’intérieur de la première, faite de Landlock, d’un filtre seccomp et d’un petit relais qui est leur seule sortie. La migration n’est pas finie. La cage fonctionne, et OpenShell m’a appris deux règles auxquelles je ne m’attendais pas.
Le 23 octobre, j’écrivais que les conseils de NVIDIA pour les agents commencent par la sandbox, et que ma propre ligne sandbox était vide. Le lendemain, Hermes tournait dans OpenShell sur un vieux portable Dell. Cette note raconte ce qui a suivi l’installation : où vivent les clés, ce dont hérite un processus enfant, et ce qu’une sandbox dans une sandbox doit bloquer.
Exécuter le code d’inconnus, c’est le métier de Forge
Forge est mon agent de contribution open source. Il cherche des issues sur des projets populaires dans mes domaines, choisit celles qui valent la peine d’être traitées, et confie chacune à un worker détaché. Claude Code clone le dépôt, écrit le correctif et lance les tests du projet. Un contrôle relance lui-même les vérifications, un second modèle relit le diff, et le correctif part sur mon fork. Tout ce qui est dit ensuite sur une pull request attend mon feu vert.
Il tourne avec un budget de 5 $ par jour, et au plus quatre pull requests ouvertes en même temps.
L’étape risquée, c’est le build. « Lancer les tests », ça veut dire exécuter tout ce que font les scripts d’installation et les suites de tests d’un dépôt, écrits par des gens que je n’ai jamais rencontrés. Sur le Mac mini, où Forge vit aujourd’hui comme agent OpenClaw, les workers tournent dans srt, un runtime de sandbox qui repose sur Seatbelt, la sandbox de macOS. Passer Forge sous Linux voulait dire reconstruire cette barrière.
- 1ÉclaireurTrouve des issues sur des projets populaires dans mes domaines et choisit celles qui valent la peine.
- 2WorkerClaude Code clone le dépôt, écrit le correctif, lance l’installation et les tests : du code écrit par des inconnus.
- 3ContrôleRelance lui-même les vérifications du worker, et le renvoie une fois si elles échouent.
- 4RelecteurUn second modèle lit le diff. Un problème bloquant donne droit à un correctif de plus.
- 5LivraisonPousse sur mon fork et ouvre la pull request. Tout ce qui s’y dit ensuite attend mon feu vert.
Aujourd’huiOpenClaw sur le Mac mini, workers dans srt. 5 $ par jour, au plus quatre PR ouvertes.
Hermes a emménagé en premier
L’hôte est un vieux portable Dell Inspiron sous Ubuntu Server 26.04, écran rabattu. NemoClaw, l’installeur de NVIDIA pour les agents dans OpenShell, a mis en place la CLI et la gateway en 78 secondes, sans sudo. J’ai figé la version 0.0.137, qui embarque OpenShell 0.0.116, et choisi Hermes comme agent. Il a démarré en Hermes Agent v0.21.3, sur Claude Sonnet 5.5 via OpenRouter.
Le premier mur, c’était Docker. Le Dell fait tourner Docker en rootless, et la gateway d’OpenShell ne peut pas vivre là : les sandboxes sont dans le namespace réseau propre à Docker rootless alors que la gateway écoute sur l’hôte, donc une sandbox ne peut pas la rappeler. La voie rootless qui marche sous Linux, c’est Podman, en version 5 ou plus. Il a fallu une seule étape root, une petite unité système qui ajoute une adresse sur la loopback. Je l’ai lancée à la main, parce que mes agents n’ont pas sudo sur cette machine.
Telle qu’elle est installée, la sandbox refuse tout par défaut. Avec seulement le preset PyPI activé, une requête vers example.com reçoit un 403. L’API et le tableau de bord d’Hermes écoutent sur la loopback, et NemoClaw possède le fichier de config d’Hermes et en vérifie le hash : on ne le modifie pas à la main. Forge tournera plutôt comme un profil Hermes à part, avec son propre SOUL.md et un hook qui voit chaque appel d’outil avant son exécution. Lors d’un test, ce hook a bloqué un rm.
Dell Inspiron · Ubuntu Server 26.04
Podman 5.7 rootless
OpenShell 0.0.116 · gateway, proxy, clés
Sandbox · refus par défaut
Hermes Agent v0.21.3Forge : son propre profil, ensuite
78 sinstallation de NemoClaw, sans sudo
1étape root : une adresse sur la loopback pour Podman
403example.com, refusé par défaut
Docker rootlessNon : sandboxes dans son namespace, gateway sur l’hôte, aucun chemin retour.
Les clés arrivent sous forme de placeholders
Ma dernière note expliquait l’astuce d’OpenShell pour les identifiants, d’après la doc. Voici à quoi elle ressemble de l’intérieur. L’environnement de la sandbox ne contient aucune clé d’API, seulement un placeholder qui commence par openshell:resolve:env:. Quand Hermes appelle OpenRouter avec ce placeholder dans son en-tête d’authentification, le proxy d’OpenShell identifie le programme, vérifie la politique et glisse la vraie clé au passage. Hermes ne la voit jamais.
Puis j’ai mis des placeholders là où ils n’ont rien à faire. Un dans le corps d’une requête : le proxy a coupé la connexion. Le placeholder d’un autre fournisseur dans un en-tête : 403.
Telegram va un cran plus loin. NemoClaw lie le token du bot à un seul hôte, un seul port et un seul chemin, l’API des bots, et partout ailleurs le placeholder reste un placeholder.
Dans la sandboxAPI_KEY=openshell:resolve:env:…
- 1HermesEnvoie sa requête avec le placeholder dans l’en-tête d’authentification.
- 2ProxyIdentifie le programme derrière la connexion.
- 3ProxyVérifie la politique : ce programme, cet hôte.
- 4ProxyGlisse la vraie clé au passage. Elle n’entre jamais dans la sandbox.
- 5ProxyDans le corps d’une requête : connexion coupée. Celui d’un autre fournisseur, dans un en-tête : 403.
TelegramLe token du bot est lié à un seul hôte, port et chemin. Ailleurs, il reste un placeholder.
Les identifiants suivent l’endpoint, pas le programme
Les règles réseau d’OpenShell nomment les deux côtés d’un appel : quel programme a le droit de se connecter, et vers quel hôte. Je supposais que les identifiants marchaient pareil, et qu’une clé prévue pour gh ne serait glissée que dans les requêtes de gh.
Ce n’est pas le cas. La clé d’un fournisseur est résolue pour tout processus qu’une politique, quelle qu’elle soit, laisse passer vers l’endpoint de ce fournisseur. Je l’ai découvert quand mon relais, qu’un preset de test laissait passer vers l’API de GitHub, a reçu le token GitHub dans sa requête. C’était ma propre sonde, donc rien n’a fuité. Mais ça a réécrit ma règle de conception : un programme qu’un worker peut piloter ne doit jamais avoir de politique vers un hôte lié à un identifiant.
Ce que je supposais
Les clés suivent le programme
gh→ api.github.comclé
relais→ api.github.complaceholder
vs
Ce que fait OpenShell
Les clés suivent l’endpoint
gh→ api.github.comclé
relais→ api.github.comclé
Un preset de test laissait passer le relais. Ça a suffi.
La règle désormaisUn programme qu’un worker peut piloter n’a aucune politique vers un hôte lié à un identifiant.
Un enfant hérite du réseau de son parent
La deuxième règle : les politiques suivent l’arbre des processus vivants. Un processus reçoit les règles réseau de son propre programme plus celles de chacun de ses ancêtres, vérifiées au moment où il fait la requête.
Ça compte, parce que les scripts de Forge sont en Python, et que le Python de la sandbox est celui sur lequel tourne Hermes. Chaque script que Forge lance correspond aux règles d’Hermes, OpenRouter et Telegram, et tout ce que ces scripts lancent aussi. Un worker lancé de la façon évidente hérite d’un chemin vers le fournisseur de modèle de l’agent et vers son bot de discussion.
Pour sortir de l’héritage, il faut cesser d’être un descendant. Un processus qui fait un double fork finit enfant du PID 1 et ne garde que les règles de son propre programme. C’est comme ça que démarre le relais de Forge : détaché, sur sa propre copie de Python pour ne pas correspondre non plus aux règles d’Hermes, il surveille l’étape de worker qui l’a lancé et s’arrête quand elle se termine.
Hermesle Python de la sandbox
OpenRouterTelegram
Script de Forgeenfant d’Hermes, même Python
OpenRouterTelegram
Étape de workerClaude Code, lancé par le script
OpenRouterTelegramhéritées ; sa cage ne peut pas joindre le proxy
Relais, détachéparent : PID 1, son propre Python
RegistresAnthropic
La deuxième barrière
Dans OpenShell, chaque étape de worker tourne dans sa propre cage. Elle a trois parties, deux venues du noyau Linux et un relais d’environ deux cents lignes de Python.
- Landlock, la sandbox non privilégiée du noyau, en ABI 8 : des listes de ce que le worker peut lire, écrire et exécuter ; les connexions TCP, autorisées sauf vers le proxy d’OpenShell, sa gateway et les ports d’Hermes ; aucun signal ni socket unix abstrait vers quoi que ce soit hors de la cage.
- Un filtre seccomp qui refuse les sockets unix et io_uring. Landlock ne contrôle pas une connexion à un fichier socket, et la gateway d’Hermes écoute sur l’un d’eux : avec Landlock seul, mon test l’a atteinte. io_uring est refusé lui aussi, parce qu’il pourrait ouvrir ce socket sans passer par l’appel système que le filtre surveille.
- Le relais : un proxy qui n’accepte que CONNECT, avec une liste d’hôtes autorisés, les registres de paquets et l’API d’Anthropic. La sandbox n’a aucune route directe vers l’extérieur, donc le relais est la seule porte qui reste. Il transmet au proxy d’OpenShell, qui voit le relais comme l’appelant.
Le worker peut donc installer des paquets et appeler Claude. Les registres passent en TLS simple, sans inspection et sans clé à ajouter. Sur les appels du relais vers Anthropic, OpenShell ajoute la clé de Forge, pour les endpoints Messages et comptage de tokens uniquement. Le worker dépense des tokens sans jamais détenir la clé. Le relecteur tourne comme un programme à part, avec sa propre règle pour OpenRouter, sur GLM 5.3.
J’ai testé de bout en bout. claude -p dans la cage a répondu via le relais pour 0,026 $ sur Haiku, et le relecteur a répondu via OpenRouter.
- FichiersLandlock : listes de lecture, écriture et exécutionListés
- TCPLandlock : ni le proxy ni la gateway d’OpenShell, ni les ports d’HermesBloqué
- Socketsseccomp : sockets unix et io_uringRefusés
- SortieLe relais : CONNECT uniquement, liste d’hôtes autorisésUne porte
- ClésAjoutées par OpenShell sur les appels du relais vers AnthropicJamais dedans
Testéclaude -p via le relais, 0,026 $ sur Haiku. Le relecteur sur GLM 5.3.
Quatre pièges à connaître avant de commencer
Aucun ne figure dans un guide de démarrage. Chacun m’a bloqué jusqu’à ce que je le trouve.
L’erreur qu’on ne voit pas. Deux politiques qui nomment le même hôte doivent lui donner la même forme : mode TLS, protocole et mode d’application, jokers compris. OpenShell refuse une incohérence avec « network endpoint ambiguity validation failed ». NemoClaw disait seulement « OpenShell did not confirm the policy submission ». Un script wrapper nommé openshell, placé en tête du PATH et qui journalise chaque appel, m’a montré la vraie erreur.
Les programmes ont besoin de leur chargeur. Sous Landlock, un programme lié dynamiquement a aussi besoin des droits d’exécution sur le chargeur dynamique. La liste d’exécution accorde donc /usr, pas des binaires un par un.
Le mauvais Python. Dans la sandbox, le python3 du PATH est l’environnement virtuel d’Hermes. Le PATH du worker l’exclut, et son fichier de config est de toute façon illisible sous Landlock.
Deux orthographes pour un proxy. curl préfère https_proxy en minuscules, donc le worker définit les deux casses.
L’erreur qu’on ne voit pas
« OpenShell did not confirm the policy submission »
Même hôte, deux formes. Un wrapper
openshellqui journalise a montré « network endpoint ambiguity validation failed ».Les programmes ont besoin de leur chargeur
Un binaire de la liste refuse quand même de démarrer
Il lui faut aussi les droits d’exécution sur le chargeur dynamique. Autorisez
/usr.Le mauvais Python
python3est l’environnement virtuel d’HermesRetirez-le du PATH du worker.
Deux orthographes pour un proxy
curl ignore
HTTPS_PROXYquandhttps_proxyest définiDéfinissez les deux casses.
Où on en est
Hermes tourne dans OpenShell, les règles des identifiants et de l’héritage sont cartographiées, et la cage fonctionne. Un profil Forge avec son propre SOUL.md et un hook sur les appels d’outils répond via l’API d’Hermes.
Le reste, c’est l’essentiel du travail. Les scripts de Forge doivent encore être portés sur le Dell. Ensuite un essai à blanc, sans push ni message, et alors seulement la bascule : le bot Telegram @forge déménage, les clés passent dans des providers OpenShell, et les copies sur le mini sont supprimées. Le tableau de bord de Forge reste sur le mini et récupère ses données depuis le Dell.
D’ici là, Forge tourne là où il a toujours tourné : OpenClaw sur le Mac mini, workers dans srt.
- HermesDans OpenShell sur Podman, refus par défaut, Sonnet 5.5En service
- RèglesPlaceholders, endpoints, héritageCartographiées
- CageLandlock, seccomp, relais ;
claude -pet le relecteur ont réponduTestée - ProfilSon propre SOUL.md ; un hook qui a bloqué
rmTesté - PortageLes scripts de Forge sur le DellPas encore
- Essai à blancNi push, ni messagePas encore
- BasculeLe bot et les clés déménagent ; les copies du mini sont suppriméesPas encore
D’ici làForge tourne sur OpenClaw sur le Mac mini, workers dans srt.
Essayez
NemoClaw est le chemin court : il installe OpenShell, met en place OpenClaw ou Hermes dans une sandbox et vous fait démarrer sur une politique de refus par défaut. Lisez le script de bootstrap avant de le lancer, figez une version, et sous Linux sans root, prenez Podman.
Pour aller plus loin :
- OpenShell sur GitHub, sous Apache 2.0, avec la doc des politiques
- NemoClaw sur GitHub, l’installeur que j’ai utilisé
- Hermes Agent, de Nous Research
- Landlock dans la doc du noyau, la sandbox non privilégiée sous la cage
- Espérer n’est pas sécuriser : les cinq pièces d’un agent en entreprise, la note que celle-ci prolonge
- OpenClaw et Hermes Agent offrent les mêmes fonctionnalités, mais pas les mêmes réglages par défaut, où les deux exécutent les outils sur l’hôte par défaut
Si votre agent lance des processus qui exécutent du code que vous n’avez pas écrit, regardez ce dont ces enfants héritent avant de faire confiance à la sandbox autour du parent. Et si vous avez cloisonné des workers dans OpenShell autrement, racontez-moi comment. J’aimerais comparer nos cages.
Texte brut: openshell-sandbox-inside-a-sandbox.md · Flux Atom · Toutes les notes