Installer un plugin sans redémarrer le gateway : Hermes le câble à la volée


Vous installez un plugin sur votre bot dans un groupe Telegram. Il apporte une nouvelle commande slash. Vous la tapez dans le groupe, plein d’attentes — et le bot fait comme s’il n’avait rien entendu. La doc dit : redémarrez le gateway. Ce qui signifie qu’une réponse en streaming est coupée, que les jobs en arrière-plan meurent et que chaque session repart de zéro. C’est un prix lourd pour une seule commande slash.

Alors la plupart des gens attendent le prochain redémarrage. Cela vient de changer (#119266, qui corrige #87770) : installez un plugin pendant que le gateway tourne, et ses handlers de plateforme sont câblés sur place.

Pourquoi un redémarrage était autrefois obligatoire

Tout le problème tenait à quand les plugins sont découverts :

  • discover_plugins() s’arrête court une fois la découverte effectuée, sauf si vous passez explicitement force=True.
  • Aucun des chemins d’installation à chaud ne le passait. Pire, tools/mcp_tool_config._portable_mcp_servers l’appelait sans forçage — donc exécuter mcp.reload après une installation à chaud rechargeait le vieux jeu de serveurs.
  • Le plugins.manage install de la TUI revenait dès que l’helper d’installation avait fini, sans aucun rescan.
  • Les adaptateurs de plateforme (Telegram, Slack, …) construisaient leur registre de handlers au boot et ne regardaient plus jamais.

Effet net : les fichiers du plugin étaient sur le disque, la config l’avait activé, et le processus en cours n’en savait rien.

Un récapitulatif d’activation : actif maintenant vs session suivante

Chaque chemin de chargement à chaud est désormais un vrai rescan forcé, et il émet un callback depuis l’intérieur même de ce rescan (PluginManager.on_plugin_loaded) portant un récapitulatif d’activation par plugin nouvellement chargé. Ça ressemble à ceci :

{"name": "late-mcp", "key": "late-mcp",
 "activated_now": {"gateway_commands": ["late"], "callbacks": ["telegram"]},
 "deferred": {"tools": ["late_tool"], "prompt": ["late.section"], "mcp_servers": ["worker"]}}

En clair :

Catégorie Quand cela s’applique Ce que cela couvre
Commandes slash du gateway Immédiatement les noms de commandes enregistrés par le plugin
Hooks de transform du gateway Immédiatement transform_llm_output, transform_tool_result, transform_terminal_output, pre_gateway_dispatch, gateway_platform_event, pre_command
Autres hooks Immédiatement les noms de hooks restants
Callbacks de plateforme Immédiatement les plateformes dotées d’une factory register_platform_handler, plus les action ids Slack
Tools Session suivante les noms de tools
Sections de system-prompt Session suivante les ids de sections
Serveurs MCP portables Un mcp.reload, ou la session suivante les noms exactement tels qu’ils apparaissent dans mcp.json

La plus utile des trois lignes « immédiatement » est les callbacks de plateforme : les callbacks de boutons et les transforms de messages entrants d’un plugin fonctionnent désormais sans redémarrage.

Pourquoi le câblage ne double pas et ne perd pas face aux handlers du cœur

« Fonctionne immédiatement » a l’air simple ; deux pièges le rendent moins simple.

Piège un : la double inscription. Un rechargement forcé rend de nouveaux objets fonction, donc l’identité des fonctions ne peut pas vous dire si quelque chose était déjà câblé. Le correctif consiste à indexer chaque câblage sur (plugin, factory qualname) — un plugin, une factory, un câblage. C’est pourquoi la méthode s’appelle un recâblage idempotent (rewire_plugin_handlers()) : elle relit le registre des factories et n’exécute que celles qui ne sont pas encore câblées sur le client natif vivant.

Piège deux : l’ordre. Sur Telegram, les handlers nouvellement ajoutés sont hissés devant les catch-alls du cœur. Cela compte parce que PTB ne dispatche que le premier match par groupe, et le cœur enregistre un catch-all filters.COMMAND — un handler tardif placé derrière ne se déclencherait jamais. Slack fait l’équivalent en dédupliquant register_slack_action_handler par AsyncApp.

Le runner du gateway s’abonne par profile servi au boot, et sur l’événement recâble chaque adaptateur vivant sur la boucle d’événements. Tout cela se passe à l’intérieur du processus gateway, pas dans le processus qui a lancé l’installation.

Chaque surface d’installation vous dit la vérité

CLI, Desktop, TUI ou l’installation REST du dashboard — toutes rapportent exactement ce qui est actif maintenant et ce qui attend :

hermes plugins install late-mcp --enable

La commande pousse d’abord le gateway en cours (un verbe reload-plugins sur la control socket, qui lui demande de rescaner de force le home visé), puis imprime cette séparation actif-vs-différé. Le plugins.manage install/toggle/update du Desktop/TUI renvoie activation plus gateway_reloaded ; restart_required est true uniquement quand aucun gateway n’a répondu.

Il y a une prise de position éditoriale délibérée dans ce design : mieux vaut ne rien faire que faire semblant. D’où :

  • Les tools et les sections de system-prompt d’un plugin chargé tardivement ne s’appliquent pas à la session en cours. Même règle que /skills install — le prompt et le schéma de tools d’une session vivante restent stables en cache, et un changement en plein vol les casserait.
  • Les serveurs MCP portables attendent mcp.reload.
  • Désactiver ne décâble pas. Désactivez un plugin en cours d’exécution et ses handlers déjà câblés restent jusqu’au redémarrage du gateway. Chaque surface le dit, plutôt que de vous laisser croire que c’est instantanément propre.

À quoi cela ressemble en pratique

Une exécution typique :

# 1. installer et activer (pousse le gateway en cours à rescaner)
hermes plugins install late-mcp --enable

# 2. la sortie indique que gateway_commands: ["late"] est actif maintenant
#    donc /late fonctionne déjà dans votre groupe Telegram — sans redémarrage

# 3. si le plugin embarque aussi un serveur MCP, faites-le monter lui aussi
#    (via mcp.reload, ou simplement en démarrant une nouvelle session)

Une mise en garde à intégrer : le gateway doit servir le home où vit le plugin. L’événement se propage par profile, et un gateway qui ne sert pas ce profile ne câblera rien pour lui. Sur les déploiements multi-profile et multi-gateway, c’est plus facile à déclencher que sur une seule machine — pour le contexte, voyez un gateway par hôte.

Limites et mises en garde

  • Cette version ne couvre que les handlers. Les tools, les sections de system-prompt et les serveurs MCP portables restent hors de l’ensemble immédiat, pour les raisons de cache de prompt et de durée de vie des connexions évoquées plus haut.
  • Il n’y a pas de décâblage. Le câblage est unidirectionnel : une installation s’applique immédiatement ; une désactivation ne nettoie vraiment qu’après un redémarrage. Les mises à niveau se comportent de la même façon.
  • La confiance accordée aux plugins ne s’est pas relâchée. C’est un mécanisme de câblage à l’exécution, pas un assouplissement de la revue ; pour savoir d’où viennent les plugins et comment ils sont admis, voyez le modèle de confiance du catalogue de plugins et le SDK de plugins du bureau.
  • Les plugins de backend terminal sont un autre sujet — voyez les plugins de backend terminal.

Pour conclure

« Il suffit de redémarrer après l’installation » est survivable sur un setup jouet, mais sur un gateway qui fait du vrai travail c’est un verrou dur — il repousse chaque mise à niveau à 3 h du matin. La moitié de ce verrou a maintenant disparu : ce qui doit prendre effet immédiatement (handlers de plateforme, commandes slash, hooks de transform) le fait, et ce qui doit attendre un tour (tools, sections de prompt, connexions MCP) le dit tout haut — y compris le fait que désactiver ne décâble pas. L’autre moitié ne bougera pas de sitôt, parce qu’elle se heurte de plein fouet à l’invariant du cache de prompt.