Documentação oficial desta página · v0.20.6
Criar um plugin de fonte de segredos
Fontes de segredos resolvem credenciais de provedor guardadas num gerenciador externo (um cofre, um gerenciador de senhas, um chaveiro do sistema operacional, um script próprio) e as transformam em variáveis de ambiente na subida do processo — depois que o ~/.hermes/.env é carregado e antes de o Hermes ler as credenciais. Bitwarden, 1Password e uma fonte genérica baseada em comando auxiliar vêm no próprio repositório; todo outro backend é um plugin. Este guia mostra como construir um.
O conjunto embutido é fechado de propósito, mesma política dos provedores de memória: PRs que adicionam novos backends de cofre em agent/secret_sources/ são fechados com um ponteiro para este guia. Publique seu backend como um repositório de plugin independente e divulgue no Discord da Nous Research (#plugins-skills-and-skins).
Momento do bootstrap no primeiro processo
O load_hermes_dotenv() costuma rodar no momento do import, antes de os
plugins se registrarem. O Hermes então busca os segredos de novo depois da
descoberta de plugins, sempre que alguma fonte de segredos de plugin
habilitada estiver configurada. A habilitação usa o contrato
is_enabled(cfg) da fonte; a forma padrão é
secrets.<name>.enabled: true, e a ativação personalizada continua sendo suportada.
Isso fecha a lacuna de primeiro processo do tipo "trocar o Bitwarden pelo meu cofre" (#64177).
- A nova busca é idempotente e falha aberta (nunca trava a subida).
- As fontes só fornecem variáveis de ambiente através do orquestrador; não existe API de plugin para despejar o cofre inteiro do usuário ou de outros plugins além do que a configuração da sua própria fonte permite.
- Ler
os.environdepois da carga é possível para qualquer código no mesmo processo — a fronteira de confiança continua sendo "plugins habilitados rodam com o privilégio do agente".
O que é do framework e o que é seu
O orquestrador (agent.secret_sources.registry.apply_all) é dono de tudo que é sensível a segurança e a precedência, para que um backend não tenha como errar:
| O framework é dono de | Você é dono de |
|---|---|
| Ordenação das fontes, precedência entre mapeado e em massa | Buscar os valores no seu backend |
| Resolução de conflito por primeira reivindicação + avisos | Validar o formato das suas referências |
Semântica do override_existing (nunca cruza fontes) | Conversar com sua CLI/SDK/API |
| Tokens de bootstrap protegidos | Declarar qual variável de ambiente É o seu token de bootstrap |
| Tempo limite de relógio por fonte | Manter o fetch() razoavelmente rápido |
Proveniência por variável + rótulos (from X) | Um label legível por gente |
Escritas em os.environ | Nada — você nunca toca no ambiente |
Estrutura de diretórios
~/.hermes/plugins/meu-cofre/
├── plugin.yaml # nome, descrição
└── __init__.py # subclasse de SecretSource + register(ctx)
A ABC SecretSource
Implemente agent.secret_sources.base.SecretSource. Só um método é obrigatório:
from pathlib import Path
from agent.secret_sources.base import (
ErrorKind,
FetchResult,
SecretSource,
run_secret_cli,
)
class MyVaultSource(SecretSource):
name = "myvault" # chave da seção de config: secrets.myvault
label = "My Vault" # usado nas linhas de subida + rótulos de proveniência
shape = "mapped" # "mapped" (mapa explícito VAR→ref) ou "bulk" (despejo do projeto)
scheme = "mv" # opcional: esquema de URI único que é seu (mv://...)
def fetch(self, cfg: dict, home_path: Path) -> FetchResult:
"""Resolve os segredos. NÃO PODE levantar exceção. NÃO PODE pedir entrada."""
result = FetchResult()
token = os.environ.get("MYVAULT_TOKEN", "").strip()
if not token:
result.error = "secrets.myvault.enabled is true but MYVAULT_TOKEN is not set."
result.error_kind = ErrorKind.NOT_CONFIGURED
return result
try:
proc = run_secret_cli(
["myvault-cli", "export", "--json"],
allow_env=["MYVAULT_TOKEN"], # SÓ as suas variáveis de auth — nunca o os.environ inteiro
timeout=30,
)
except RuntimeError as exc: # falha ao subir o processo / tempo esgotado
result.error = str(exc)
result.error_kind = ErrorKind.BINARY_MISSING
return result
if proc.returncode != 0:
result.error = f"myvault-cli exited {proc.returncode}: {proc.stderr[:200]}"
result.error_kind = ErrorKind.AUTH_FAILED
return result
result.secrets = parse_your_output(proc.stdout) # {VARIAVEL_DE_AMBIENTE: valor}
return result
def protected_env_vars(self, cfg: dict):
# Seu token de bootstrap — nenhuma fonte (inclusive a sua) pode sobrescrever.
return frozenset({"MYVAULT_TOKEN"})
Regras do contrato (impostas, não sugeridas)
- O
fetch()nunca levanta exceção. Os erros vão emresult.error+result.error_kind. Um fetch que levanta exceção é contido pelo orquestrador e reportado comoINTERNAL— isso é violação de contrato, não recurso. - O
fetch()nunca pede entrada. A subida acontece em contextos sem TTY (gateway, cron, Docker). Orun_secret_cli()fecha o stdin, então um auxiliar que pede entrada falha na hora. Autenticação interativa pertence ao fluxo de configuração da sua CLI, nunca ao caminho de subida. - Síncrono e dentro do orçamento. O orquestrador impõe um tempo limite de relógio (padrão de 120s, ajustável pelo usuário via
secrets.<name>.timeout_seconds). Estourar o limite reportaTIMEOUTe o seu resultado é descartado. - Você busca; o orquestrador aplica. Devolva o mapeamento que você contribuiria. Nunca escreva em
os.environpor conta própria — você passaria por cima da precedência, da detecção de conflito e da proveniência. - Versionamento de API. O
SecretSource.api_versionassume por padrão oSECRET_SOURCE_API_VERSIONatual. O registro pula (com um aviso) as fontes construídas contra outra versão em vez de derrubar a subida.
Escolher o seu shape
mapped— o usuário liga explicitamente nomes de variáveis de ambiente a referências na configuração (como o mapaenv:do 1Password). Intenção mais forte: reivindicações mapeadas ganham das reivindicações em massa nas variáveis disputadas.bulk— você injeta implicitamente um projeto/pasta inteiro de segredos (como o Bitwarden BSM). Cede lugar para as fontes mapeadas.
Ganchos opcionais
| Método | Padrão | Sobrescreva quando |
|---|---|---|
is_enabled(cfg) | cfg.get("enabled") | A lógica de ativação for personalizada |
override_existing(cfg) | cfg.get("override_existing", False) | Você quiser outro padrão (as duas fontes embutidas usam True por causa da rotação) |
protected_env_vars(cfg) | vazio | Você tiver um token de bootstrap (quase certamente tem) |
fetch_timeout_seconds(cfg) | 120s | Seu backend precisar de outro orçamento |
config_schema() | {} | Quiser declarar chaves de configuração para as telas de configuração |
remediation(kind, cfg) | dicas genéricas por ErrorKind | Você quiser que os avisos de falha apontem para o seu próprio comando de conserto (por exemplo, as fontes embutidas devolvem Run hermes secrets <name> token… para AUTH_FAILED). Precisa ser um mapeamento puro tipo→texto: sem I/O, nunca levanta exceção. Devolva "" para suprimir a dica. |
Segurança de subprocesso: use o run_secret_cli()
Se o seu backend chama uma CLI, use o auxiliar compartilhado em vez do subprocess.run direto. Ele te dá de graça a postura auditada: só argv (sem shell=True), um ambiente filho mínimo com lista de permissão (quando as fontes rodam, o os.environ já guarda todas as credenciais que o Hermes conhece — nunca entregue isso para um processo filho), NO_COLOR + stderr com ANSI removido, stdin fechado, e tempo esgotado virando um RuntimeError limpo. Passe as referências fornecidas pelo usuário depois de um terminador -- no seu argv, para que nunca possam ser interpretadas como flags.
Registrar
# __init__.py
def register(ctx):
ctx.register_secret_source(MyVaultSource())
O registro é rejeitado (com aviso no log, nunca com queda) para: instâncias que não são SecretSource, nomes inválidos ou duplicados, um scheme que já pertence a outra fonte, api_version errada, ou um shape fora de mapped/bulk.
A descoberta de plugins roda mais tarde na subida do que a primeira chamada de load_hermes_dotenv(). Logo depois da descoberta, o Hermes busca de novo as fontes de segredos de plugin habilitadas (reset_secret_source_cache() + load_hermes_dotenv()), então o processo que fez a descoberta realmente enxerga esses segredos — veja Momento do bootstrap no primeiro processo acima (#64177). Essa nova busca falha aberta e é pulada quando nenhuma fonte de plugin está habilitada. Todo código que lê os.environ durante o import do módulo do plugin ou no register(ctx) continua rodando antes da nova busca e não pode depender de credenciais fornecidas por essa mesma fonte; mantenha o trabalho que usa credenciais dentro do fetch(). Os processos de gateway, cron e subagente executam a mesma sequência de descoberta e nova busca.
Os usuários configuram como em qualquer outra fonte
secrets:
sources: [myvault, bitwarden] # ordenação opcional
myvault:
enabled: true
# ... suas chaves de config_schema
Precedência entre várias fontes, avisos de conflito e rótulos de proveniência (from My Vault) funcionam automaticamente — veja a documentação de segredos para usuários para conhecer a escada de precedência.
Valide com o kit de conformidade
Faça uma subclasse do kit do repositório do Hermes (tests/secret_sources/conformance.py) nos testes do seu plugin:
import pytest
from tests.secret_sources.conformance import SecretSourceConformance
class TestMyVaultConformance(SecretSourceConformance):
@pytest.fixture
def source(self):
return MyVaultSource()
Ele confere as regras que quebram a vida dos outros quando violadas: nunca levantar exceção com configuração malformada, tipos de erro legíveis por máquina, desabilitado por padrão, tempos limite positivos, nomes válidos de variáveis protegidas, e uma volta completa pelo apply_all(). Conformidade verde é a régua de revisão para chamar um backend de compatível com o contrato.
Referência de ErrorKind
| Tipo | Significado |
|---|---|
NOT_CONFIGURED | Habilitado, mas falta token / projeto / mapa |
BINARY_MISSING | CLI auxiliar não encontrada ou sem permissão de execução |
AUTH_FAILED / AUTH_EXPIRED | Credenciais ruins / expiradas |
REF_INVALID | Uma referência de segredo falhou na validação |
NETWORK | Falha no nível de transporte |
EMPTY_VALUE | O backend não devolveu nada para uma referência — nunca aplique "" por cima de uma credencial boa |
TIMEOUT | A busca estourou o orçamento |
INTERNAL | Qualquer outra coisa (bug, formato inesperado) |