« Tests passed », aucun fichier sur le disque : déboguer un agent local depuis la plomberie
Mon agent local m'annonçait « Build succeeded » et « Tests passed », et je ne trouvais aucun fichier dans le projet. J'ai accusé le modèle un peu vite : le premier coupable, c'était mon montage Docker. Cet article ouvre la partie de la série consacrée au piège du sandbox Hermes.
Par Nicolas Cousin — Publié le 23 octobre 2026
« Tests passed », aucun fichier sur le disque : déboguer un agent local depuis la plomberie
TL;DR
Mon agent local m'annonçait un build réussi et des tests au vert, et le projet restait vide. J'ai d'abord accusé le modèle. En réalité, le projet n'était même pas monté dans le conteneur de l'agent, et tant que ce n'était pas réglé, je ne pouvais rien dire des modèles. Une fois le montage corrigé, Devstral écrit bel et bien ses fichiers. Qwen2.5-Coder 7B, lui, testé plus tard dans OpenCode, continue d'annoncer un appel d'outil sans l'exécuter, du moins avec mon installation Ollama. Ce que j'en retiens : on vérifie sur le disque, pas dans le compte rendu de l'agent.
Sommaire
- Le décor
- Le symptôme
- Le projet n'était pas dans le conteneur
- Le correctif, et trois détails à ne pas rater
- Devstral écrit vraiment
- Qwen, lui, annonce sans exécuter
- Ce que je ne referais pas
- Dans quel ordre chercher
Le décor
Un VPS sans GPU, avec Ollama et Hermes (l'agent de Nous Research) qui
tournent dans deux conteneurs Docker sur un réseau privé agentic. J'ai
détaillé l'installation dans
un article précédent. Je ne le
savais pas encore en l'écrivant, mais le problème de cet article-ci était
déjà dedans.
Le symptôme
J'ai commencé avec qwen2.5-coder:7b : environ 7,6 milliards de
paramètres, quantifié en Q4_K_M, et
marqué « tools » par Ollama,
donc censé savoir appeler des outils. Dans Hermes, tout avait l'air de
bien se passer. Des write_file … défilaient, puis un
terminal("dotnet build"), puis « Build succeeded » et « Tests passed ».
Dans le projet, pourtant, pas un fichier.
Devstral 24B, de son côté, n'avait pas fait beaucoup mieux sur un benchmark plus ambitieux : une petite API ASP.NET Core avec EF Core, PostgreSQL, un service, des tests xUnit, des scénarios Gherkin et une pipeline GitHub Actions. Environ seize minutes et demie de travail, pour une réponse textuelle au lieu d'une vraie exécution, une route incorrecte, un service oublié dans l'injection de dépendances, une structure de projet incohérente, des GitHub Actions qui ciblaient .NET 6, du Gherkin non exécutable, et aucun build réel démontré. J'en avais tiré une conclusion provisoire : être bon pour générer du code ne fait pas un bon agent.
Je ne vais pas faire semblant : à ce stade, j'étais surtout agacé. Ces benchmarks tournent sur CPU, chaque essai se compte en minutes, et je voyais l'aiguille de ma montre tourner pour rien. Heureusement, pendant ces attentes, j'aidais ma fille à faire ses devoirs. Elles n'ont pas été complètement perdues pour tout le monde.
J'avais donc deux modèles qui semblaient inventer leurs appels d'outils. En fait, je ne me posais pas la bonne question.
Le projet n'était pas dans le conteneur
Le seul montage d'Hermes, c'était ~/.hermes vers /opt/data, son
répertoire d'état. Le projet sur lequel je lui demandais de travailler
n'existait tout simplement pas dans le conteneur. C'est exactement la
commande de l'article d'installation, et c'est aussi la seule que montrent
les exemples de la
documentation Docker d'Hermes
(consultée en octobre 2026). Dans cette doc, /opt/data sert à l'état de
l'agent, pas à son espace de travail. Elle n'a rien de faux, elle ne
couvre simplement que la moitié du besoin.
Quoi que l'agent ait écrit, ça ne pouvait donc pas atterrir dans le projet. Peut-être que les écritures sont parties ailleurs, dans la couche inscriptible du conteneur ou dans le seul répertoire monté, celui de l'état. Peut-être qu'elles n'ont jamais eu lieu. Sans transcription, je ne peux pas trancher, et c'est bien là le problème : tant que l'environnement cache le résultat, on ne peut rien conclure sur le modèle. Autrement dit, ces minutes de benchmark n'avaient pas mesuré grand-chose, à part le temps perdu, ce qui n'a pas arrangé ma frustration.
Le correctif, et trois détails à ne pas rater
Tout tient dans -v "$PWD:/workspace", qui fait du répertoire courant
l'espace de travail de l'agent. Je l'ai rangé dans une fonction zsh :
unalias hermes 2>/dev/null
hermes() {
sudo docker run -it --rm \
--network agentic \
-v "$HOME/.hermes:/opt/data" \
-v "$PWD:/workspace" \
-w /workspace \
-e HERMES_WRITE_SAFE_ROOT=/workspace \
hermes-local "$@"
}
D'abord zsh. J'avais déjà un alias hermes, et au moment de définir une
fonction du même nom, j'ai eu droit à :
defining function based on alias `hermes'
parse error near `()'
Avec la syntaxe nom (), zsh refuse de définir une fonction dont le nom
vient d'un alias, sauf si l'option
ALIAS_FUNC_DEF
est activée. D'où le unalias en première ligne.
Ensuite HERMES_WRITE_SAFE_ROOT, que j'ai réglé sur /workspace au
moment de revalider. La
documentation actuelle
la présente comme une restriction : elle bloque write_file et patch en
dehors des racines listées. Je n'ai pas noté si ma version l'exigeait, ni
pourquoi. Je la garde comme garde-fou explicite pour ces deux outils, en
sachant qu'elle ne couvre pas les commandes shell : là, seules les
permissions font barrière.
Enfin les permissions, justement. Hermes tourne sous l'UID/GID 10000,
alors que le projet appartient à mon utilisateur ubuntu (1000). J'ai
réglé ça avec des ACL :
sudo apt install -y acl
sudo setfacl -R -m u:1000:rwX,u:10000:rwX <projet>
sudo setfacl -R -d -m u:1000:rwx,u:10000:rwx <projet>
Le premier setfacl s'occupe de l'existant (le X majuscule n'ajoute le
droit d'exécution qu'aux répertoires et aux fichiers déjà exécutables). Le
second pose une ACL par défaut, dont hériteront les fichiers créés ensuite,
que ce soit par l'agent ou par moi.
Devstral écrit vraiment
Pour revalider, j'ai demandé un vrai write_file à un modèle cloud
(gpt-5.6-sol), puis à Devstral. Cette fois, deux fichiers sont apparus sur
le VPS : sol-tool-test.txt et devstral-tool-test.txt.
Devstral, que Mistral présente comme pensé pour tourner dans des agents de code, n'était donc pas « incapable d'agir ». Le « aucun build démontré » de mon premier benchmark mesurait surtout mon environnement. Les défauts du code lui-même, en revanche, restent à réévaluer.
Qwen, lui, annonce sans exécuter
Conclure que tout venait du montage serait pourtant faux.
Plus tard, dans OpenCode (un autre harness, lancé dans un Dev Container
rootless), j'ai demandé aux deux modèles de créer un fichier avec un
contenu précis. Devstral 24B a fait un vrai appel write : fichier créé,
contenu correct, en un peu moins de 14 minutes, à environ 0,4 token/s, avec
une erreur de transport et un retry au passage. Qwen2.5-Coder 7B, dans sa
variante 64K, a bien compris la consigne et présenté un write(…) qui
tenait la route, en 8 minutes 35. Aucun fichier créé.
Je précise que je n'ai pas retesté Qwen dans Hermes après le correctif.
Pour autant, je ne vais pas en conclure que Qwen2.5-Coder ne sait pas
appeler d'outils. Une
issue chez Hermes (#5867),
encore ouverte en octobre 2026, décrit le même symptôme avec
qwen2.5-coder:14b via Ollama : les appels reviennent sous forme de JSON
dans le texte de la réponse, au lieu du champ structuré tool_calls, et
Hermes les traite comme une réponse ordinaire, sans jamais les exécuter.
Le contournement signalé consiste à changer de provider côté Hermes. La
documentation function calling de Qwen2.5,
qui ne parle pas de la variante Coder, prévient d'ailleurs que les modèles
ne respectent pas toujours le protocole, même avec le bon prompt et le bon
template.
Ce que je ne referais pas
Juger un modèle sur ce qu'il raconte. « Build succeeded », c'est du texte généré. Ça n'a rien d'un code de sortie.
Compter sur -v pour me prévenir d'une erreur. Quand le chemin source
n'existe pas, -v
le crée sans rien dire,
sous forme de répertoire vide, alors que --mount type=bind,… échoue
franchement. Une simple faute de frappe dans le chemin suffit à reproduire
exactement ce symptôme, sans le moindre message.
Oublier le revers de $PWD. Si on lance la fonction depuis $HOME,
l'agent reçoit tout ce que ses permissions lui laissent atteindre, y
compris ~/.hermes (configuration, jetons OAuth, clés d'API), monté une
deuxième fois sous /workspace, et le safe root couvre alors tout $HOME.
Il faut donc la lancer depuis la racine du projet, et n'appliquer les ACL
qu'au projet, jamais à $HOME ni à une arborescence qui contient des
.env ou d'autres secrets.
Si c'était à refaire, je regarderais d'abord du côté de HERMES_UID et
HERMES_GID, que la documentation et le docker-compose.yml actuels
d'Hermes proposent pour aligner l'utilisateur du conteneur sur le
propriétaire du montage. Je ne sais pas si c'était déjà disponible dans ma
version, ni avec mon image hermes-local construite en local, donc à
vérifier chez vous.
Dans quel ordre chercher
Avec le recul, quand le disque ne confirme pas le succès annoncé, voici l'ordre dans lequel je chercherais :
- L'environnement. Le projet est-il visible dans le conteneur, au bon
chemin, et l'utilisateur de l'agent peut-il y écrire ? Un test bête
(« crée ce fichier avec ce contenu ») et un
lscôté hôte suffisent à le savoir. - Le harness et son parseur. L'appel d'outil arrive-t-il sous forme
structurée, ou comme du texte qui y ressemble ? Si un modèle cloud
réussit le même test, l'environnement et le harness sont hors de cause.
Côté Ollama, il reste à regarder si l'appel revient dans
tool_callsou dans le texte. - Le modèle. Seulement à ce moment-là, et en jugeant sur le résultat concret : le fichier est là, le build relancé à la main passe, le code de sortie est lu.
Et ce n'est pas un travers réservé aux petits modèles locaux. Des travaux récents (Guo et al., ICML 2026, Advani, 2026), menés surtout sur des modèles de pointe, décrivent des agents qui affirment avoir terminé alors que l'environnement dit le contraire.
Le sandbox d'Hermes voit enfin le projet, et un modèle local y écrit pour de vrai. Il me reste à rejouer les tests que l'environnement avait faussés. En attendant, j'ai au moins appris à ne plus croire l'agent sur parole et à aller regarder le disque.