Le backend du terminal était épinglé — alors pourquoi Hermes tournait-il encore sur l'hôte ?

Vous installez Hermes sur une machine de dev partagée. L’IT écrit terminal.backend: docker dans /etc/hermes/config.yaml, avec l’intention que chaque commande tourne dans un conteneur plutôt que sur l’hôte. Un collègue demande à l’agent d’installer une dépendance, et elle atterrit quand même dans le home de l’hôte. La politique semble appliquée. Elle ne l’est pas.
« Configuré, autorisé, et malgré tout ignoré » est la pire catégorie de bug, parce que rien ne renvoie d’erreur. Deux changements fusionnés (PR #145221 et #145222) corrigent exactement cela : la portée du terminal par session ne lisait jamais le managed scope, si bien qu’un backend épinglé perdait à chaque tour face au réglage propre du profile.
Qui décide où vos commandes s’exécutent
terminal.backend choisit la cible d’exécution : local, docker, ssh, modal, daytona, vercel_sandbox, ou un backend fourni par un plugin. Il n’est pas lu une seule fois au démarrage. En mode gateway, chaque session construit sa propre portée de terminal au moment de sa création, en empilant plusieurs sources :
- les valeurs par défaut intégrées ;
- le
.envdu profile ; - l’overlay de lancement (valeurs passées en ligne de commande) ;
- le
config.yamldu profile.
Cela suffit pour un utilisateur seul : modifiez votre propre config.yaml et c’est réglé. Mais les quatre niveaux sont contrôlés par l’utilisateur, et le managed scope existe précisément pour se placer au-dessus d’eux.
Ce qu’est le managed scope
Le managed scope est le mécanisme de flotte : un répertoire système qui épingle un ensemble de valeurs de configuration et de secrets pour tous les utilisateurs de la machine. Par défaut, c’est /etc/hermes/ :
/etc/hermes/
├── config.yaml # couche de config managée, prioritaire sur ~/.hermes/config.yaml
└── .env # couche d'env managée, prioritaire sur ~/.hermes/.env et sur le shell
Le répertoire et les fichiers appartiennent à root (répertoire 0755, fichiers 0644). Les permissions du système de fichiers sont le mécanisme d’application : tout le monde peut les lire, seul un administrateur peut y écrire. L’emplacement peut être déplacé avec HERMES_MANAGED_DIR pour les conteneurs et les déploiements hors /etc — et cette variable doit être fixée par l’administrateur (inscrite dans le fichier d’unité ou dans l’image). Si un utilisateur ordinaire peut la définir, il peut pointer le managed scope vers un répertoire qui lui appartient, et tout l’édifice s’effondre.
Précédence, pour les clés que la couche managée spécifie réellement :
| Niveau | config.yaml | .env |
|---|---|---|
| 1 | /etc/hermes/config.yaml (managé) |
/etc/hermes/.env (managé) |
| 2 | ~/.hermes/config.yaml (utilisateur) |
~/.hermes/.env (utilisateur) |
| 3 | valeurs par défaut intégrées | environnement shell préexistant |
La fusion se fait au niveau des feuilles : épingler model.default ne fige pas le reste de model.*. C’est ce qui en fait une base de référence plutôt qu’un verrou sur l’ensemble.
La couche qui manquait
load_config() — le chemin de configuration normal — applique bien l’overlay managé. hermes config affiche donc correctement la valeur épinglée. Mais la portée de terminal de chaque session est construite par une autre fonction, build_profile_terminal_scope(), et celle-ci ne lisait que les quatre niveaux ci-dessus. Jamais le niveau managé. Résultat :
- le
terminal.backend: dockerde l’administrateur est visible danshermes config; - l’exécution des commandes utilise toujours le
localdu profile.
Une divergence entre l’affichage et le comportement de ce genre est presque invisible depuis l’interface de configuration. Elle est apparue dans un plugin de sandbox qui lance un hermes serve --isolated mono-profile, épingle terminal.backend via HERMES_MANAGED_DIR et retient le .env du profile : le plugin croyait que les commandes tournaient à l’intérieur de sa frontière ; elles tournaient à l’extérieur.
Le correctif est volontairement minime : le mapping managé terminal: est fusionné en dernier, clé par clé, avec le même deep-merge que celui utilisé par load_config(). Trois comportements valent la peine d’être retenus :
nulln’épingle rien. Undocker_env: nulldans la couche managée laisse la valeur du profile intacte.- Les mappings fusionnent feuille par feuille, pas en bloc. Une entrée managée dans
docker_envconserve les autres entrées du profile. - Seul un
docker_imagenon nul compte comme épinglé, et lui seul marque l’image comme choisie par l’administrateur.
Sans section terminal: managée, la portée est construite exactement comme avant — aucun déploiement existant ne change de comportement.
Deux pièges .env derrière le même bug
Le scénario sandbox a révélé deux problèmes plus étroits, tous deux liés au .env.
Un fichier qui existe mais ne peut pas être lu. Le lanceur de sandbox retient le .env du profile (sous Windows, une liste de refus MXC le rend illisible) et passe les clés autorisées en variables d’environnement à la place. Le fichier existe toujours, donc load_hermes_dotenv l’ouvre, tombe sur une PermissionError, et Hermes meurt à l’import — avant que quoi que ce soit ne s’exécute. La nouvelle règle : si, et seulement si, le managed scope épingle un terminal.backend non vide, un .env de profile présent mais illisible est chargé comme absent. Rien n’en est appliqué, les clés héritées restent, et un unique avertissement nomme le fichier une fois par processus (pas une fois par tour). Sans l’épinglage, l’erreur se propage toujours, car dans ce cas le fichier peut être le seul endroit où le backend est écrit — l’avaler serait pire.
Le .env du projet qui vole des clés. L’ancien code décidait si un .env de répertoire de projet pouvait combler des trous selon que le .env du profile s’était chargé ou non. Il vérifie désormais son existence : tant que le .env du profile est là (lisible ou non), le .env du projet ne peut que combler des trous, jamais écraser. Les clés injectées par le lanceur survivent à un .env de dépôt.
Un dernier détail : quand le .env existe mais ne peut pas être lu, /reload et la méthode reload.env ne lancent plus leur passe de suppression — sinon les clés que le lanceur vient d’injecter seraient effacées.
Comment un administrateur l’épingle
sudo mkdir -p /etc/hermes
sudo chmod 0755 /etc/hermes
sudo tee /etc/hermes/config.yaml >/dev/null <<'YAML'
terminal:
backend: docker
docker_image: registry.example.com/hermes-sandbox:2026-10
docker_env:
NODE_OPTIONS: --max-old-space-size=2048
YAML
sudo chmod 0644 /etc/hermes/config.yaml
Dans un conteneur où /etc pose problème, fixez plutôt le répertoire dans l’unité de service :
export HERMES_MANAGED_DIR=/opt/org/hermes-policy
Les changements s’appliquent au démarrage suivant de Hermes. Un fichier managé mal formé ne bloque jamais le démarrage — il est journalisé à grand bruit et ignoré — vérifiez donc avec hermes doctor que les clés épinglées ont bien été prises en compte.
Comment un utilisateur sait qu’il est managé
Trois commandes en lecture seule :
hermes config # l'en-tête nomme la source managée et les clés épinglées
hermes doctor # répertoire managé résolu et nombre de clés épinglées
hermes config get terminal.backend
Toute tentative de modifier une clé épinglée est refusée, avec la source nommée :
Cannot set 'terminal.backend': it is managed by your administrator
(/etc/hermes/config.yaml) and cannot be changed.
Si les commandes continuent d’atterrir sur l’hôte à l’intérieur d’un conteneur, vérifiez dans cet ordre : ce que terminal.backend affiche dans hermes config ; si le répertoire managé résolu par hermes doctor est bien celui attendu ; si le config.yaml du profile définit la même clé. Pour la liste complète des backends et l’installation des backends de plugin, voir le guide des plugins de backend de terminal ; pour les détails de sécurité du backend docker (bits setuid retirés, entre autres), voir l’article sur le sandbox docker.
Statut et limites
Les deux PR (#145221 pour le chargement du .env au démarrage, #145222 pour la portée du terminal à chaque tour) ont été fusionnés dans main le 2026-10-29 et sont dans la v0.22.6 (2026-11-02) : une mise à jour normale suffit, plus besoin d’une installation basée sur main.
Trois limites valent la peine d’être connues : le .env managé est lisible par tous (0644), n’y mettez donc pas de secrets très sensibles ; les outils de l’agent ne sont pas bloqués de façon stricte quant à la lecture des valeurs d’environnement managées ; et si un utilisateur peut écrire dans le répertoire managé (ou lance Hermes en root), le mécanisme devient consultatif. Il arrête les accidents et les modifications désinvoltes, pas une escalade de privilèges déterminée — pour une vraie isolation, des machines ou des conteneurs séparés restent la meilleure réponse.