Le Hermes dans Docker a perdu la moitié de son arbre de dépendances — l'image n'avait jamais enregistré de baseline


Vous ouvrez le Hermes de votre conteneur Docker le matin et le serveur API ne démarre pas : aiohttp not installed. Les adaptateurs de messagerie ont disparu, les serveurs MCP ont disparu, les agents hébergés apparaissent comme dégradés. Vous n’avez modifié aucune configuration, l’image n’a pas bougé — la seule chose qui s’est produite, c’est que le conteneur a installé une dépendance optionnelle la veille.

Ce n’est pas une image cassée. C’est un mécanisme plus discret : la première génération de dépendances qu’un conteneur construit pour lui-même devient la baseline de tous les démarrages suivants. Quand les dépendances intégrées à l’image n’ont jamais été enregistrées, la première génération auto-construite les a silencieusement jetées.

Comment fonctionne une « génération de dépendances »

Les dépendances dans Hermes ne sont pas une installation à sens unique qui reste en place. Elles sont gérées par un gestionnaire de dépendances (PM) qui produit une génération à chaque installation ou mise à jour : il enregistre une sélection (quels extras doivent être présents) plus un stamp, et c’est cette génération qui sert de référence aux démarrages suivants.

La conception repose sur une seule hypothèse : la baseline est enregistrée, donc les incréments peuvent se construire dessus. Quand vous installez paresseusement une dépendance optionnelle plus tard, elle devrait s’unir à la baseline enregistrée. Si la baseline est vide, cette seule installation paresseuse devient la baseline et déplace tout le reste.

security.allow_lazy_installs vaut true par défaut, ce qui signifie que les paquets manquants peuvent être installés à la demande. Cette commodité est précisément ce qui amplifie le problème, car toute installation paresseuse peut déclencher « construire une nouvelle génération ».

Pourquoi les conteneurs sont l’endroit idéal pour se faire mordre

L’image Docker fait d’elle-même quelque chose de parfaitement raisonnable — elle intègre huit extras directement dans /opt/hermes/.venv :

all  messaging  otlp  anthropic  bedrock  azure-identity  matrix  google-chat

Dépliés, cela représente le gateway, la télémétrie OTLP, les SDK et credentials Anthropic / Bedrock / Azure, l’adaptateur Matrix et l’adaptateur Google Chat. Tout ce qu’il faut pour réellement tourner.

Le problème, c’est qu’au moment du build de l’image, ces extras n’ont jamais été enregistrés comme sélection de dépendances du PM. La séquence est donc la suivante :

  1. le conteneur construit sa première génération à cause d’une installation paresseuse — un plugin avec ses propres dépendances, ou vous qui activez un canal à la demande comme fal, parallel-web ou exa ;
  2. cette génération est construite uniquement avec l’extra demandé à ce moment-là ;
  3. à partir du démarrage suivant, cette sélection incomplète est la baseline.

C’est la scène d’ouverture : serveur API, adaptateurs de messagerie et serveurs MCP disparus tous en même temps. Depuis la v0.21.6, tout conteneur sur l’image officielle qui a construit une génération à lui a cela devant lui.

Autant le dire clairement : le diagnostic n’a pas été manqué. Un autre contributeur est parvenu indépendamment à la même conclusion au même moment — aucune baseline enregistrée dans l’image — et a proposé un correctif pour les nouvelles installations. Ce qui a été livré fait deux choses de plus que la prévention : cela répare les volumes déjà cassés, et cela enregistre la liste des extras demandés plutôt qu’un inventaire de ce qui a été installé.

Pourquoi la liste demandée et non un inventaire ? Parce que sur un vrai build d’image, l’inventaire s’est révélé omettre les méta-extras comme all, cron et sms — des extras qui ne possèdent aucun paquet propre et ne peuvent donc pas être énumérés, mais qui doivent rester dans la baseline.

Correctif un : le build de l’image enregistre sa baseline

Le build de l’image écrit désormais ces extras dans les faits de dépendances du store, par le même chemin d’enregistrement que celui déjà utilisé par le build bundled. Toute installation ultérieure est donc une union par-dessus cette baseline au lieu d’un nouveau départ.

Correctif deux : chaque démarrage restaure ce qui a disparu

La prévention n’aide que les nouvelles images. Et les volumes déjà cassés ? Ils se réparent au démarrage.

Chaque démarrage compare désormais les extras enregistrés par l’image à la sélection que détient réellement le volume, et restaure ce qui manque via le chemin de synchronisation explicite normal. Un volume réduit à ["fal"], ou à une liste vide, se rétablit à son premier démarrage avec une image corrigée, sans aucune étape manuelle.

Quelques cas limites sont traités avec soin :

  • si la restauration échoue — hors ligne, par exemple — le venv de l’image démarre quand même, et les extras manquants restent enregistrés comme manquants, si bien qu’une installation paresseuse avant le démarrage suivant ne peut pas les faire disparaître de nouveau ;
  • un extra enregistré que la source ne déclare plus est ignoré (correspondance par nom PEP 685) ;
  • un fait enregistré malformé (extras incorrects, stamp manquant ou non-string) signale invalid recorded dependency state au lieu de se lire silencieusement comme « rien d’enregistré » ;
  • enabled-features.json n’est délibérément pas écrit, afin que allow_lazy_installs se comporte exactement comme avant. Ce changement touche la baseline et rien d’autre.

La même forme de défaillance, une seconde fois : l’application desktop qui emporte le TUI

Le même lot contient un autre exemple de « une partie échoue, tout le monde paie » — et cette fois celui qui paie est un serveur headless.

Une mise à jour depuis les sources prépare les dépendances en installant ui-tui, web et apps/desktop en un seul npm ci d’union. Seule l’application desktop tire des addons natifs qui nécessitent un compilateur : node-pty, electron. Un serveur Ubuntu qui n’a jamais exécuté l’application desktop et ne dispose que de g++ 9 — qui rejette -std=gnu++20 — faisait donc échouer toute l’installation des dépendances, et le TUI et l’interface web n’étaient jamais construits alors qu’ils n’ont rien à voir avec l’application desktop. Chaque lancement affichait a source update could not be finished automatically (6 attempts).

Désormais ce cas réessaie une fois sans l’application desktop, construit le TUI et l’interface web comme d’habitude, et enregistre le build du desktop comme dû :

Desktop app build owed: desktop dependencies failed

Il affiche aussi une porte de sortie réellement utile :

ⓘ If this host never runs the desktop app, hermes uninstall --gui removes it and
  future updates stop building it.

Sur une machine headless, c’est la réponse la plus propre — ne pas embarquer du tout l’application desktop :

hermes uninstall --gui

L’indication apparaît dans les deux situations qui l’exigent : après cette nouvelle tentative réussie, et sur toute mise à jour Linux headless (pas de DISPLAY ni de WAYLAND_DISPLAY) qui sélectionne l’application desktop.

Quoi vérifier et quoi faire

Vous tournez dans Docker :

  1. confirmez d’abord que le symptôme correspond — le serveur API, les adaptateurs de messagerie et les serveurs MCP qui disparaissent ensemble, avec des agents signalés comme dégradés ;
  2. tirez l’image corrigée et redémarrez ; un volume cassé restaure ses extras manquants au premier démarrage, et le volume n’a pas besoin d’être recréé ;
  3. si vous ne pouvez pas encore changer d’image, installer explicitement les extras manquants rétablira ce démarrage — mais la baseline est toujours fausse, donc l’installation paresseuse suivante peut de nouveau les faire disparaître.

Vous tournez sur un serveur headless :

  1. si les mises à jour continuent d’échouer avec could not be finished automatically, vérifiez si les dépendances natives de l’application desktop sont le blocage ;
  2. si cet hôte n’exécute jamais l’application desktop, retirez-la du checkout avec hermes uninstall --gui ;
  3. les mises à jour futures cessent alors de compiler node-pty et electron pour un front end que vous n’utilisez pas.

Deux habitudes qui se généralisent :

  • les problèmes de dépendances sont rarement « un paquet a cassé » et bien plus souvent une génération dont la baseline a été écrite de travers. Quand quelque chose perd soudain tout un ensemble de capacités, soupçonnez la baseline avant de réinstaller les paquets un par un ;
  • les installations paresseuses sont commodes précisément parce qu’elles transforment « manquant » en « le nouveau défaut ». Si vous voulez de la stabilité à long terme, une baseline enregistrée bat une installation ad hoc à chaque fois.

En bref

Les deux correctifs de ce lot portent sur une seule chose : l’état des dépendances a besoin d’un enregistrement de baseline fiable. L’image Docker intégrait huit extras sans les enregistrer, si bien que la première génération auto-construite du conteneur les a effacés ; les modules natifs de l’application desktop ont échoué à se compiler et ont emporté avec eux un TUI et une interface web sans rapport. Le premier est résolu en enregistrant au build et en restaurant au démarrage ; le second en dégradant vers la partie réellement nécessaire.

Pour la lecture opérationnelle autour, voir faire tourner Hermes dans un conteneur partagé, le problème WAL de state.db et que faire quand une mise à jour fait exploser votre disque. Les détails des commandes vivent sur les pages de référence hermes uninstall et hermes update, et le guide d’installation est l’endroit où repartir de zéro.