Instalar um plugin sem reiniciar o gateway: o Hermes faz a ligação ao vivo


Você instala um plugin no seu bot em um grupo do Telegram. Ele traz um novo comando de barra. Você digita no grupo, cheio de expectativa — e o bot age como se não tivesse ouvido nada. A documentação diz: reinicie o gateway. O que significa que uma resposta em streaming é cortada, jobs em segundo plano morrem e toda sessão começa de novo. É um preço caro por um único comando de barra.

Então a maioria das pessoas espera pelo próximo reinício. Isso acabou de mudar (#119266, corrigindo #87770): instale um plugin com o gateway em execução, e seus handlers de plataforma são ligados na hora.

Por que um reinício costumava ser obrigatório

O problema todo estava em quando os plugins são descobertos:

  • discover_plugins() curto-circuita depois que a descoberta já aconteceu, a menos que você passe force=True explicitamente.
  • Nenhum dos caminhos de instalação em tempo de execução passava esse parâmetro. Pior: tools/mcp_tool_config._portable_mcp_servers o chamava sem force — então rodar mcp.reload depois de uma instalação em tempo de execução recarregava o conjunto antigo de servers.
  • O plugins.manage install do TUI retornava assim que o helper de instalação acabava, sem nenhum rescan.
  • Os adaptadores de plataforma (Telegram, Slack, …) montavam seu registro de handlers no boot e nunca olhavam de novo.

Efeito líquido: os arquivos do plugin estavam no disco, a config o tinha habilitado, e o processo em execução não sabia de nada.

Um resumo de ativação: ativo agora vs próxima sessão

Todo caminho de carga em tempo de execução agora é um rescan forçado de verdade, e ele emite um callback de dentro desse rescan (PluginManager.on_plugin_loaded) carregando um resumo de ativação por plugin recém carregado. Ele se parece com isto:

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

Em termos simples:

Categoria Quando se aplica O que cobre
Comandos de barra do gateway Imediatamente os nomes de comando que o plugin registrou
Hooks de transform do gateway Imediatamente transform_llm_output, transform_tool_result, transform_terminal_output, pre_gateway_dispatch, gateway_platform_event, pre_command
Outros hooks Imediatamente os nomes de hook restantes
Callbacks de plataforma Imediatamente plataformas com uma fábrica register_platform_handler, mais os action ids do Slack
Ferramentas Próxima sessão nomes de ferramenta
Seções de system prompt Próxima sessão ids de seção
Servers MCP portáteis Um mcp.reload, ou a próxima sessão os nomes exatamente como aparecem em mcp.json

A mais útil das três linhas de “imediatamente” é callbacks de plataforma: os callbacks de botão e os transforms de mensagem de entrada de um plugin agora funcionam sem reinício.

Por que a ligação não duplica nem perde para os handlers do core

“Funciona imediatamente” soa simples; duas armadilhas tornam a coisa menos simples.

Armadilha um: registro duplicado. Um force reload devolve novos objetos de função, então a identidade da função não consegue dizer se algo já foi ligado. A correção é indexar cada ligação por (plugin, factory qualname) — um plugin, uma fábrica, uma ligação. É por isso que o método se chama religação idempotente (rewire_plugin_handlers()): ele relê o registro de fábricas e roda apenas as fábricas que ainda não foram ligadas ao cliente nativo vivo.

Armadilha dois: ordenação. No Telegram, os handlers recém-adicionados são içados para a frente dos catch-alls do core. Isso importa porque o PTB despacha apenas a primeira correspondência por grupo, e o core registra um catch-all filters.COMMAND — um handler atrasado atrás dele nunca dispararia. O Slack faz o equivalente deduplicando register_slack_action_handler por AsyncApp.

O runner do gateway se inscreve por profile servido no boot, e no evento religa cada adaptador vivo no event loop. Tudo isso acontece dentro do processo do gateway, não no processo que rodou a instalação.

Toda superfície de instalação diz a verdade

CLI, Desktop, TUI ou a instalação via REST do dashboard — todas reportam exatamente o que está ativo agora e o que espera:

hermes plugins install late-mcp --enable

O comando primeiro cutuca o gateway em execução (um verbo reload-plugins no control socket, pedindo que ele force um rescan do home solicitado), depois imprime essa divisão entre ativo e adiado. O plugins.manage install/toggle/update do Desktop/TUI retorna activation mais gateway_reloaded; restart_required é true apenas quando nenhum gateway respondeu.

Há uma postura editorial deliberada nesse desenho: melhor não fazer nada do que fingir. Por isso:

  • Ferramentas e seções de system prompt de um plugin carregado tarde não se aplicam à sessão em execução. Mesma regra de /skills install — o prompt e o schema de ferramentas de uma sessão viva ficam estáveis no cache, e uma mudança no meio do voo quebraria isso.
  • Servers MCP portáteis esperam por mcp.reload.
  • Desabilitar não desliga a ligação. Desabilite um plugin em tempo de execução e seus handlers já ligados ficam até o gateway reiniciar. Toda superfície diz isso, em vez de te deixar acreditar que está tudo limpo na hora.

Como isso é na prática

Uma execução típica:

# 1. instala e habilita (cutuca o gateway em execução para fazer rescan)
hermes plugins install late-mcp --enable

# 2. a saída afirma que gateway_commands: ["late"] está ativo agora
#    então /late já funciona no seu grupo do Telegram — sem reinício

# 3. se o plugin também traz um server MCP, suba-o também
#    (via mcp.reload, ou apenas inicie uma nova sessão)

Uma ressalva que vale internalizar: o gateway precisa estar servindo o home onde o plugin vive. O evento se propaga por profile, e um gateway que não serve aquele profile não vai ligar por ele. Em implantações com múltiplos profiles e múltiplos gateways isso é mais fácil de tropeçar do que em uma máquina única — o contexto está em um gateway por host.

Limites e ressalvas

  • Esta versão cobre apenas handlers. Ferramentas, seções de system prompt e servers MCP portáteis ficam fora do conjunto imediato, pelos motivos de prompt cache e ciclo de vida de conexão acima.
  • Não existe desligar. A ligação é unidirecional: uma instalação se aplica imediatamente; um disable só limpa de verdade depois de um reinício. Upgrades se comportam da mesma forma.
  • A confiança em plugins não foi afrouxada. Isto é um mecanismo de ligação em runtime, não um relaxamento da revisão; para saber de onde vêm os plugins e como são admitidos, veja o modelo de confiança do catálogo de plugins e o SDK de plugins do desktop.
  • Plugins de backend de terminal são uma trilha separada — veja plugins de backend de terminal.

Fechando

“Só reinicie depois de instalar” é sobrevivível em um setup de brinquedo, mas em um gateway fazendo trabalho de verdade é uma trava dura — empurra todo upgrade para as 3 da manhã. Metade dessa trava já caiu: o que deveria entrar em vigor imediatamente (handlers de plataforma, comandos de barra, hooks de transform) entra, e o que precisa esperar uma rodada (ferramentas, seções de prompt, conexões MCP) diz isso em voz alta — incluindo o fato de que desabilitar não desliga a ligação. A outra metade não vai mudar tão cedo, porque ela esbarra direto no invariante do prompt cache.