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ê passeforce=Trueexplicitamente.- Nenhum dos caminhos de instalação em tempo de execução passava esse parâmetro. Pior:
tools/mcp_tool_config._portable_mcp_serverso chamava sem force — então rodarmcp.reloaddepois de uma instalação em tempo de execução recarregava o conjunto antigo de servers. - O
plugins.manage installdo 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.