Plugins nunca mais encostam no seu token

Você instala um plugin de voz. Ele precisa do seu login do ChatGPT para gerar um segredo de sessão de realtime. Tudo funciona por alguns dias. Então, numa manhã, o Codex te desconectou. Você faz login de novo, e no dia seguinte acontece outra vez. Você culpa a rede, o navegador, talvez a sua conta — qualquer coisa, menos o plugin, que afinal só “usou” o seu login. O que aconteceu de fato: o plugin e o Hermes quiseram renovar a mesma credencial, e os refresh tokens do Codex são de uso único. Quem gasta o token por segundo fica com um token morto, e a família de tokens inteira é revogada junto.
Isso não é um plugin malicioso. É o caminho natural de um plugin que precisa de um login, e a documentação oficial cita esse mesmo plugin de voz como exemplo. O PR #136117 (mesclado na main em 10 de outubro de 2026 e publicado na v0.21.7) tomou uma terceira opção: deixar o core fazer a requisição. O plugin não lê, não renova e não escreve mais a credencial — ele só recebe a resposta.
Como a falha acontece: um token de uso único, dois consumidores
A mecânica merece ser detalhada, porque a responsabilidade fica inequívoca depois que você a vê:
- Os refresh tokens do Codex são invalidados pelo uso, então a renovação precisa ser serializada. O Hermes sabe disso há um tempo e renova sob um lock — o conjunto de regras que cobre essa classe de provider é
_SINGLE_USE_REFRESH_PROVIDERS. - Um plugin que precisa do login do ChatGPT costumava trazer o seu próprio leitorzinho de
~/.codex/auth.json: se o token parecesse perto de expirar, fazia um POST paraauth.openai.com/oauth/tokencom o client id do Codex CLI, e então escrevia os novos tokens de volta. - Essas duas implementações de renovação não compartilham um lock. Quando renovam ao mesmo tempo, o mesmo refresh token é gasto duas vezes. O servidor trata isso como abuso e invalida a família de tokens inteira.
O resultado é o sintoma que custa horas para diagnosticar: nada na sua configuração mudou, ninguém digitou uma senha errada, e você simplesmente está desconectado. E volta a acontecer depois que você faz login de novo, porque o segundo renovador do plugin continua lá.
A nova forma: o core faz a requisição
O novo módulo público hermes_cli/plugin_provider_requests.py oferece credentialed_provider_request(). Ele é deliberadamente simples — uma função importável com o mesmo formato de hermes_cli.urllib_security.open_credentialed_url — para funcionar tanto em código de register(ctx) quanto em plugins de dashboard (dashboard/plugin_api.py).
Dois passos para quem escreve um plugin. Primeiro, declare o login no plugin.yaml:
name: live-voice
requires_auth:
- openai-codex
Depois entregue a requisição ao core e pare de tocar no arquivo de credencial:
from hermes_cli.plugin_provider_requests import ProviderNotSignedIn, credentialed_provider_request
def mint_client_secret(session: dict) -> dict:
try:
resp = credentialed_provider_request(
"openai-codex", "POST", "https://api.openai.com/v1/realtime/client_secrets",
json={"session": session}, timeout=15)
except ProviderNotSignedIn as exc:
return {"ok": False, "error": str(exc)} # a mensagem nomeia o comando de login
if resp.status != 200:
return {"ok": False, "error": f"client secret failed ({resp.status}): {resp.text[:300]}"}
return {"ok": True, **resp.json()}
O que volta é um ProviderResponse (status, headers, body, mais .text e .json()) — sem nenhum token dentro. O código a apagar é o seu leitor e renovador feito à mão: do lado do plugin, o passo de renovação deixou de existir.
Quatro regras rígidas
Isto não é um proxy de propósito geral. Ele carrega quatro fronteiras, cada uma respondendo a um incidente real:
- Somente plugins declarados. O autor da chamada é identificado como o diretório de plugin encontrado na pilha de chamadas; o core lê o
plugin.yamldesse plugin. Um plugin que não lista o provider emrequires_auth— ou código que não pertence a nenhum plugin instalado — recebe umPermissionError. Passar o helper cru pararun_in_executortambém não ajuda: não há frame de plugin na pilha, então a chamada é recusada. Chame-o de dentro da sua própria função. - Somente origens fixas. Para
openai-codex, o token é enviado apenas parahttps://chatgpt.comehttps://api.openai.com. Qualquer outra URL é recusada antes de qualquer I/O de rede. Um profile cuja credencial do Codex roteia para um gateway customizado também é recusado — essa chave pertence ao gateway, não a este caminho. - Sem redirecionamentos.
follow_redirects=False. Uma resposta 3xx volta sem alteração, então um cabeçalho Authorization nunca pode ser levado até um terceiro host. - Credenciais com escopo por profile. Ele resolve via
resolve_codex_runtime_credentials(), o mesmo caminho com renovação sob lock, escopo por profile e ciente do pool que o chat usa. Sem login, você recebeProviderNotSignedIn, cuja mensagem nomeia o comando que o usuário precisa (hermes auth add openai-codex, ou o login do Codex dentro do app desktop).
O que isso muda para quem instala plugins
Boa parte do valor aqui é a visibilidade na hora de instalar:
requires_authaparece no prompt de instalação e emhermes plugins show <name>.- Mais útil ainda, ele participa do fluxo de consentimento quando um pino do catálogo é atualizado. Se a nova versão adiciona ferramentas, hooks, dependências Python, capacidades de host, um login de provider (
requires_auth) ou uma metade de UI de Desktop que a versão instalada não tinha, a CLI mostra o delta e pergunta y/N antes de mover qualquer coisa; os botões de atualização do Desktop e do dashboard mostram a mesma confirmação. Recusar — ou uma sessão não interativa sem ninguém para responder — deixa o plugin no seu pino antigo.
Em outras palavras, “este plugin ampliou o próprio acesso na surdina” agora é uma pergunta que o caminho de atualização faz em voz alta.
O que ele não é: isto não é um sandbox
A documentação oficial é direta sobre isso, e merece ser repetida literalmente para os usuários: isto é consentimento e visibilidade, não um sandbox. O código do plugin roda in-process, então ele ainda consegue ler qualquer arquivo que você consegue ler. O processo de revisão rejeita plugins que fazem isso, mas essa é uma restrição de revisão, não uma imposição em nível de kernel.
As únicas garantias rígidas são as quatro regras acima: atribuição, origem, redirecionamentos e o caminho de resolução de credencial. Leia isso como “um plugin não consegue mais se passar pelo core ao usar o seu login”, e não como “o plugin foi contido”.
Painéis de uso e logout também passam pelo core
Duas armadilhas que a documentação fecha explicitamente:
- Não chame os endpoints de uso você mesmo. Use
agent.account_usage.fetch_account_usage("openai-codex", read_only=True). Ele retorna um snapshot (plano, janelas de sessão e semanal comused_percentereset_at) e nunca renova nem rotaciona uma credencial. - O logout pertence ao Hermes, não a um plugin. Aponte os usuários para
hermes auth logout openai-codexem vez de apagar arquivos de credencial — apagá-los é justamente como toda essa história começou.
O que fazer agora
- Autores de plugins: se o seu plugin lê o
auth.jsonou renova um token do Codex por conta própria, apague esse código, chamecredentialed_provider_request()em vez disso, e adicionerequires_authaoplugin.yaml. Todo mundo que o roda deixa de ser desconectado por essa corrida. - Todo mundo mais: o recurso está na v0.21.7 (marcada em 11 de outubro). Para ver se um plugin instalado já o adotou,
hermes plugins show <name>imprime orequires_authdeclarado. Para o modelo de segurança ao redor, veja o nosso guia do catálogo de plugins e o texto sobre a cadeia de suprimentos do CI do catálogo.
A versão curta
- “Plugins não deveriam guardar o seu token” não é frescura; é a conclusão de um bug concreto: um refresh token de uso único gasto por dois renovadores, e um usuário desconectado sem ideia do porquê.
- A correção não foi dar aos plugins uma chave menor. Foi tirar a chave deles: o core faz a requisição e anexa o Authorization, e o plugin vê apenas a resposta.
- As quatro regras — declare, origens fixas, sem redirecionamentos, credenciais com escopo por profile — movem isso de convenção para mecanismo.
- A visibilidade importa tanto quanto:
requires_authaparece na instalação, na inspeção e na confirmação de atualização, então um plugin não consegue mais adicionar um login na surdina. - E mantenha a fronteira em mente: consentimento e visibilidade, não um sandbox.