---
title: Une sandbox dans la sandbox : migrer mon agent open source vers Hermes sur OpenShell
description: J’ai installé Hermes Agent dans NVIDIA OpenShell sur un vieux Dell et commencé à y migrer Forge, mon agent de contribution open source. Forge exécute le code d’inconnus, et dans une sandbox OpenShell, un processus lancé par l’agent hérite de ses règles réseau : ses workers avaient besoin de leur propre cage. Ce qui s’est installé sans accroc, où vont les clés, les deux règles inattendues et ce qui reste à faire.
date: 2026-10-24
tags: agents IA, sécurité des agents, OpenShell, Hermes Agent, Landlock, sandboxing
---

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](/fr/notes/hope-isnt-security-enterprise-agents.html), 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.

<figure class="fd-fig sx sx-forge" aria-labelledby="sx1-cap" data-desc="Une tâche de Forge en cinq étapes, allumées tour à tour. Un, l’éclaireur : Forge trouve des issues sur des projets populaires dans mes domaines et choisit celles qui valent la peine. Deux, le worker : Claude Code clone le dépôt, écrit le correctif et lance l’installation et les tests, du code écrit par des inconnus ; cette étape est marquée en rouge, c’est celle qui a besoin d’une barrière. Trois, le contrôle : il relance lui-même les vérifications du worker et le renvoie une fois si elles échouent. Quatre, le relecteur : un second modèle lit le diff, et un problème bloquant donne droit à un correctif de plus. Cinq, la livraison : le correctif est poussé sur mon fork et la pull request s’ouvre, et tout ce qui s’y dit ensuite attend mon feu vert. Aujourd’hui, Forge tourne comme agent OpenClaw sur le Mac mini, avec ses workers dans srt sur Seatbelt de macOS, pour cinq dollars par jour et au plus quatre pull requests ouvertes.">
<p class="ea-now ea-say5" aria-hidden="true"><span>Choisir une issue</span><span class="ea-no">Exécuter le code d’un inconnu</span><span>Relancer les vérifications</span><span>Un second modèle relit</span><span>Pousser sur mon fork</span></p>
<ol class="ea-rows ea-cyc5" aria-label="Une tâche de Forge">
<li><b>1</b><span class="ea-who">Éclaireur</span><span>Trouve des issues sur des projets populaires dans mes domaines et choisit celles qui valent la peine.</span></li>
<li class="ea-row-no"><b>2</b><span class="ea-who">Worker</span><span>Claude Code clone le dépôt, écrit le correctif, lance l’installation et les tests : du code écrit par des inconnus.</span></li>
<li><b>3</b><span class="ea-who">Contrôle</span><span>Relance lui-même les vérifications du worker, et le renvoie une fois si elles échouent.</span></li>
<li><b>4</b><span class="ea-who">Relecteur</span><span>Un second modèle lit le diff. Un problème bloquant donne droit à un correctif de plus.</span></li>
<li><b>5</b><span class="ea-who">Livraison</span><span>Pousse sur mon fork et ouvre la pull request. Tout ce qui s’y dit ensuite attend mon feu vert.</span></li>
</ol>
<p class="mp-strip"><b>Aujourd’hui</b><span>OpenClaw sur le Mac mini, workers dans srt. 5 $ par jour, au plus quatre PR ouvertes.</span></p>
<figcaption id="sx1-cap">Une tâche, du début à la fin. L’étape 2 est celle qui a besoin d’une barrière, d’où le rouge.</figcaption>
</figure>

## 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`.

<figure class="fd-fig sx sx-stack" aria-labelledby="sx2-cap" data-desc="La pile sur le Dell, en boîtes imbriquées. La boîte extérieure est un portable Dell Inspiron sous Ubuntu Server 26.04. Dedans, Podman 5.7 en rootless. Dedans, OpenShell 0.0.116 : sa gateway, son proxy et les clés. Dedans, la sandbox, en refus par défaut, et dans celle-ci Hermes Agent v0.21.3, avec Forge prévu comme un profil Hermes à part. Sous la pile, trois chiffres : NemoClaw installé en 78 secondes sans sudo ; une seule étape root, une adresse sur la loopback pour Podman ; et un 403 pour example.com, parce que la sandbox refuse tout par défaut. Docker rootless n’a pas marché : les sandboxes sont dans son namespace réseau et la gateway sur l’hôte, donc elles ne peuvent pas la rappeler.">
<div class="sx-nest">
<div class="sx-l sx-l-host"><p class="sx-lk">Dell Inspiron · Ubuntu Server 26.04</p>
<div class="sx-l"><p class="sx-lk">Podman 5.7 rootless</p>
<div class="sx-l sx-l-os"><p class="sx-lk">OpenShell 0.0.116 · gateway, proxy, clés</p>
<div class="sx-l sx-l-sb"><p class="sx-lk">Sandbox · refus par défaut</p>
<p class="sx-agent">Hermes Agent v0.21.3<span>Forge : son propre profil, ensuite</span></p>
</div>
</div>
</div>
</div>
</div>
<div class="mp-stats">
<p><b>78 s</b><span>installation de NemoClaw, sans sudo</span></p>
<p><b>1</b><span>étape root : une adresse sur la loopback pour Podman</span></p>
<p><b>403</b><span>example.com, refusé par défaut</span></p>
</div>
<p class="mp-strip"><b>Docker rootless</b><span>Non : sandboxes dans son namespace, gateway sur l’hôte, aucun chemin retour.</span></p>
<figcaption id="sx2-cap">Chaque boîte contient la suivante. Les clés sont dans la couche OpenShell, hors de la sandbox.</figcaption>
</figure>

## 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.

<figure class="fd-fig sx sx-key" aria-labelledby="sx3-cap" data-desc="Comment une clé atteint OpenRouter sans entrer dans la sandbox. Dans la sandbox, la variable de la clé ne contient qu’un placeholder qui commence par openshell:resolve:env. Un : Hermes envoie sa requête avec le placeholder dans l’en-tête d’authentification. Deux : le proxy d’OpenShell identifie le programme derrière la connexion. Trois : il vérifie la politique pour ce programme et cet hôte. Quatre : il glisse la vraie clé au passage, si bien que la clé n’entre jamais dans la sandbox. Cinq : un placeholder au mauvais endroit est refusé ; dans le corps d’une requête la connexion est coupée, et le placeholder d’un autre fournisseur dans un en-tête reçoit un 403. Telegram va plus loin : le token du bot est lié à un seul hôte, un seul port et un seul chemin, et partout ailleurs il reste un placeholder.">
<p class="ea-now ea-say5" aria-hidden="true"><span>Hermes envoie le placeholder</span><span>Le proxy identifie le programme</span><span>La politique autorise cet hôte</span><span>La vraie clé sort</span><span class="ea-no">Mauvais endroit : refusé</span></p>
<p class="sx-env"><span>Dans la sandbox</span><code>API_KEY=openshell:resolve:env:…</code></p>
<div class="ea-lane" aria-hidden="true">
<div class="ea-box ea-box-sb"><p class="ea-box-k">Sandbox</p><p class="ea-box-n">Hermes</p></div>
<div class="ea-box ea-box-sv"><p class="ea-box-k">OpenShell</p><p class="ea-box-n">proxy, clés</p></div>
<div class="ea-box"><p class="ea-box-k">Endpoint</p><p class="ea-box-n">Open&shy;Router</p></div>
<i class="ea-req"></i>
</div>
<ol class="ea-rows ea-cyc5" aria-label="Le chemin de la requête">
<li><b>1</b><span class="ea-who">Hermes</span><span>Envoie sa requête avec le placeholder dans l’en-tête d’authentification.</span></li>
<li><b>2</b><span class="ea-who">Proxy</span><span>Identifie le programme derrière la connexion.</span></li>
<li><b>3</b><span class="ea-who">Proxy</span><span>Vérifie la politique : ce programme, cet hôte.</span></li>
<li><b>4</b><span class="ea-who">Proxy</span><span>Glisse la vraie clé au passage. Elle n’entre jamais dans la sandbox.</span></li>
<li class="ea-row-no"><b>5</b><span class="ea-who">Proxy</span><span>Dans le corps d’une requête : connexion coupée. Celui d’un autre fournisseur, dans un en-tête : 403.</span></li>
</ol>
<p class="mp-strip"><b>Telegram</b><span>Le token du bot est lié à un seul hôte, port et chemin. Ailleurs, il reste un placeholder.</span></p>
<figcaption id="sx3-cap">Une requête qui obtient sa clé, puis une qui ne l’obtient pas. Le carré passe au cobalt quand la clé s’ajoute, au rouge quand elle est refusée. Nom de variable raccourci.</figcaption>
</figure>

## 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.

<figure class="fd-fig sx sx-ep" aria-labelledby="sx4-cap" data-desc="Deux cartes. À gauche, ce que je supposais : les clés suivent le programme. gh qui appelle api.github.com reçoit la clé, et le relais qui appelle api.github.com ne reçoit que le placeholder. À droite, ce que fait OpenShell : les clés suivent l’endpoint. gh qui appelle api.github.com reçoit la clé, et le relais, qu’un preset de test laissait passer vers le même hôte, reçoit la clé lui aussi. En dessous, la règle que je suis désormais : un programme qu’un worker peut piloter n’a aucune politique vers un hôte lié à un identifiant.">
<div class="mp-two-row">
<div class="mp-two-card">
<p class="mp-two-who">Ce que je supposais</p>
<p class="mp-two-name">Les clés suivent le programme</p>
<p class="sx-call"><span class="sx-prog">gh</span><span class="sx-host">→ api.github.com</span><b class="sx-res">clé</b></p>
<p class="sx-call"><span class="sx-prog">relais</span><span class="sx-host">→ api.github.com</span><b class="sx-res sx-res-ph">placeholder</b></p>
</div>
<p class="mp-two-vs">vs</p>
<div class="mp-two-card mp-two-peter">
<p class="mp-two-who">Ce que fait OpenShell</p>
<p class="mp-two-name">Les clés suivent l’endpoint</p>
<p class="sx-call"><span class="sx-prog">gh</span><span class="sx-host">→ api.github.com</span><b class="sx-res">clé</b></p>
<p class="sx-call"><span class="sx-prog">relais</span><span class="sx-host">→ api.github.com</span><b class="sx-res sx-res-no">clé</b></p>
<p class="mp-two-line">Un preset de test laissait passer le relais. Ça a suffi.</p>
</div>
</div>
<p class="mp-strip"><b>La règle désormais</b><span>Un programme qu’un worker peut piloter n’a aucune politique vers un hôte lié à un identifiant.</span></p>
<figcaption id="sx4-cap">Les deux mêmes appels selon deux modèles. La clé rouge est celle que je n’attendais pas.</figcaption>
</figure>

## 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.

<figure class="fd-fig sx sx-proc" aria-labelledby="sx5-cap" data-desc="L’arbre des processus dans la sandbox, un processus allumé à la fois. Hermes tourne sur le Python de la sandbox et a deux règles réseau, OpenRouter et Telegram. Le script de Forge, enfant d’Hermes sur le même Python, reçoit les deux mêmes. L’étape de worker, Claude Code lancé par le script, en hérite aussi, même si sa cage l’empêche de joindre le proxy d’OpenShell. Le relais est détaché : il fait un double fork, son parent devient le PID 1, il tourne sur sa propre copie de Python, et il ne garde que ses propres règles, les registres de paquets et Anthropic.">
<p class="ea-now ea-say4" aria-hidden="true"><span>Hermes : OpenRouter, Telegram</span><span>Script de Forge : pareil</span><span>Le worker : pareil</span><span>Relais détaché : les siennes seulement</span></p>
<ol class="sx-tree ea-cyc4" aria-label="L’arbre des processus">
<li class="sx-n"><p class="sx-pn">Hermes<small>le Python de la sandbox</small></p><p class="sx-rules"><i>OpenRouter</i><i>Telegram</i></p></li>
<li class="sx-n sx-d1"><p class="sx-pn">Script de Forge<small>enfant d’Hermes, même Python</small></p><p class="sx-rules"><i>OpenRouter</i><i>Telegram</i></p></li>
<li class="sx-n sx-d2"><p class="sx-pn">Étape de worker<small>Claude Code, lancé par le script</small></p><p class="sx-rules"><i class="sx-inh">OpenRouter</i><i class="sx-inh">Telegram</i><span>héritées ; sa cage ne peut pas joindre le proxy</span></p></li>
<li class="sx-n sx-orphan"><p class="sx-pn">Relais, détaché<small>parent : PID 1, son propre Python</small></p><p class="sx-rules"><i class="sx-own">Registres</i><i class="sx-own">Anthropic</i></p></li>
</ol>
<figcaption id="sx5-cap">L’indentation montre la filiation. Le relais est à la racine exprès : avec son double fork, il n’hérite de rien.</figcaption>
</figure>

## 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.

<figure class="fd-fig sx sx-fence" aria-labelledby="sx6-cap" data-desc="La cage du worker dans la sandbox OpenShell. La sandbox contient Hermes avec Forge, et à côté la cage du worker, Landlock plus seccomp, avec le worker Claude Code dedans. La seule sortie de la cage est le relais, qui autorise les registres de paquets et Anthropic et transmet au proxy d’OpenShell ; c’est là qu’OpenShell ajoute la clé Anthropic de Forge. Puis une ligne par règle. Fichiers : listes Landlock de lecture, écriture et exécution. TCP : Landlock bloque le proxy et la gateway d’OpenShell et les ports d’Hermes. Sockets unix et io_uring : refusés par seccomp. La sortie : le relais, CONNECT uniquement, avec une liste d’hôtes autorisés. Clés : ajoutées par OpenShell sur les appels du relais vers Anthropic, jamais dans la cage. Testé : claude -p a répondu via le relais pour 0,026 dollar sur Haiku, et le relecteur a répondu sur GLM 5.3.">
<div class="sx-cagefig" aria-hidden="true">
<div class="sx-sbx">
<p class="sx-lk">Sandbox OpenShell</p>
<div class="sx-pair">
<div class="ea-box"><p class="ea-box-k">Agent</p><p class="ea-box-n">Hermes + Forge</p></div>
<div class="sx-cage"><p class="sx-lk">Cage · Landlock + seccomp</p><div class="ea-box sx-w"><p class="ea-box-k">Worker</p><p class="ea-box-n">Claude Code</p></div></div>
</div>
<p class="sx-door"><b>Relais</b><span>registres, Anthropic</span></p>
</div>
<p class="sx-out"><b>Proxy OpenShell</b><span>ajoute la clé de Forge pour Anthropic</span></p>
</div>
<ol class="ea-card" aria-label="La cage, règle par règle">
<li><span class="ea-card-k">Fichiers</span><span class="ea-card-v">Landlock : listes de lecture, écriture et exécution</span><b class="ea-pill">Listés</b></li>
<li><span class="ea-card-k">TCP</span><span class="ea-card-v">Landlock : ni le proxy ni la gateway d’OpenShell, ni les ports d’Hermes</span><b class="ea-pill">Bloqué</b></li>
<li><span class="ea-card-k">Sockets</span><span class="ea-card-v">seccomp : sockets unix et io_uring</span><b class="ea-pill ea-pill-no">Refusés</b></li>
<li><span class="ea-card-k">Sortie</span><span class="ea-card-v">Le relais : CONNECT uniquement, liste d’hôtes autorisés</span><b class="ea-pill ea-pill-mid">Une porte</b></li>
<li><span class="ea-card-k">Clés</span><span class="ea-card-v">Ajoutées par OpenShell sur les appels du relais vers Anthropic</span><b class="ea-pill">Jamais dedans</b></li>
</ol>
<p class="mp-strip"><b>Testé</b><span><code>claude -p</code> via le relais, 0,026 $ sur Haiku. Le relecteur sur GLM 5.3.</span></p>
<figcaption id="sx6-cap">Deux barrières : OpenShell autour de tout, la cage autour du worker. Le relais est la seule porte entre les deux.</figcaption>
</figure>

## 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.

<figure class="fd-fig sx sx-traps-fig" aria-labelledby="sx7-cap" data-desc="Quatre pièges, une carte chacun, avec ce qu’on voit et ce qui l’a réglé. L’erreur qu’on ne voit pas : NemoClaw dit OpenShell did not confirm the policy submission, alors que la vraie cause est le même hôte dans deux politiques avec des formes différentes ; un wrapper openshell qui journalise, placé en tête du PATH, montre l’erreur d’OpenShell elle-même, network endpoint ambiguity validation failed. Les programmes ont besoin de leur chargeur : sous Landlock, un binaire dynamique a aussi besoin des droits d’exécution sur le chargeur dynamique, donc autorisez /usr plutôt que des binaires un par un. Le mauvais Python : dans la sandbox, le python3 du PATH est l’environnement virtuel d’Hermes, donc le PATH du worker l’exclut. Deux orthographes pour un proxy : curl préfère https_proxy en minuscules, donc définissez les deux casses.">
<ul class="sx-traps">
<li><p class="sx-t-k">L’erreur qu’on ne voit pas</p><p class="sx-t-say">« OpenShell did not confirm the policy submission »</p><p class="sx-t-fix">Même hôte, deux formes. Un wrapper <code>openshell</code> qui journalise a montré « network endpoint ambiguity validation failed ».</p></li>
<li><p class="sx-t-k">Les programmes ont besoin de leur chargeur</p><p class="sx-t-say">Un binaire de la liste refuse quand même de démarrer</p><p class="sx-t-fix">Il lui faut aussi les droits d’exécution sur le chargeur dynamique. Autorisez <code>/usr</code>.</p></li>
<li><p class="sx-t-k">Le mauvais Python</p><p class="sx-t-say"><code>python3</code> est l’environnement virtuel d’Hermes</p><p class="sx-t-fix">Retirez-le du PATH du worker.</p></li>
<li><p class="sx-t-k">Deux orthographes pour un proxy</p><p class="sx-t-say">curl ignore <code>HTTPS_PROXY</code> quand <code>https_proxy</code> est défini</p><p class="sx-t-fix">Définissez les deux casses.</p></li>
</ul>
<figcaption id="sx7-cap">À quoi ressemble chaque piège, puis ce qui l’a réglé.</figcaption>
</figure>

## 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.

<figure class="fd-fig sx sx-status" aria-labelledby="sx8-cap" data-desc="Où en est la migration le 24 octobre, une ligne par pièce. Hermes dans OpenShell, sur Podman, en refus par défaut, Sonnet 5.5 : en service. Clés et règles, placeholders, endpoints et héritage : cartographiés. La cage du worker, Landlock, seccomp et le relais, avec claude -p et le relecteur qui répondent : testée. Forge comme profil Hermes, avec son propre SOUL.md et un hook qui a bloqué rm : testé. Le portage des scripts de Forge sur le Dell : pas encore. L’essai à blanc, sans push ni message : pas encore. La bascule, qui déplace le bot Telegram et les clés et supprime les copies du mini : pas encore. D’ici là, Forge tourne sur OpenClaw sur le Mac mini, avec ses workers dans srt.">
<ol class="ea-card" aria-label="La migration, pièce par pièce">
<li><span class="ea-card-k">Hermes</span><span class="ea-card-v">Dans OpenShell sur Podman, refus par défaut, Sonnet 5.5</span><b class="ea-pill">En service</b></li>
<li><span class="ea-card-k">Règles</span><span class="ea-card-v">Placeholders, endpoints, héritage</span><b class="ea-pill ea-pill-mid">Cartographiées</b></li>
<li><span class="ea-card-k">Cage</span><span class="ea-card-v">Landlock, seccomp, relais ; <code>claude -p</code> et le relecteur ont répondu</span><b class="ea-pill">Testée</b></li>
<li><span class="ea-card-k">Profil</span><span class="ea-card-v">Son propre SOUL.md ; un hook qui a bloqué <code>rm</code></span><b class="ea-pill">Testé</b></li>
<li class="ea-row-no"><span class="ea-card-k">Portage</span><span class="ea-card-v">Les scripts de Forge sur le Dell</span><b class="ea-pill ea-pill-no">Pas encore</b></li>
<li class="ea-row-no"><span class="ea-card-k">Essai à blanc</span><span class="ea-card-v">Ni push, ni message</span><b class="ea-pill ea-pill-no">Pas encore</b></li>
<li class="ea-row-no"><span class="ea-card-k">Bascule</span><span class="ea-card-v">Le bot et les clés déménagent ; les copies du mini sont supprimées</span><b class="ea-pill ea-pill-no">Pas encore</b></li>
</ol>
<p class="mp-strip"><b>D’ici là</b><span>Forge tourne sur OpenClaw sur le Mac mini, workers dans srt.</span></p>
<figcaption id="sx8-cap">L’état au 24 octobre. Les lignes rouges sont la partie la plus longue.</figcaption>
</figure>

## 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](https://github.com/NVIDIA/OpenShell), sous Apache 2.0, avec la [doc des politiques](https://docs.nvidia.com/openshell/latest/how-it-works/policies/overview)
- [NemoClaw sur GitHub](https://github.com/NVIDIA/NemoClaw), l’installeur que j’ai utilisé
- [Hermes Agent](https://github.com/NousResearch/hermes-agent), de Nous Research
- [Landlock dans la doc du noyau](https://docs.kernel.org/userspace-api/landlock.html), 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](/fr/notes/hope-isnt-security-enterprise-agents.html), 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](/fr/notes/hermes-agent-vs-openclaw.html), 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.
