Pular para o conteúdo principal

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.

dica

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.environ depois 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 deVocê é dono de
Ordenação das fontes, precedência entre mapeado e em massaBuscar os valores no seu backend
Resolução de conflito por primeira reivindicação + avisosValidar o formato das suas referências
Semântica do override_existing (nunca cruza fontes)Conversar com sua CLI/SDK/API
Tokens de bootstrap protegidosDeclarar qual variável de ambiente É o seu token de bootstrap
Tempo limite de relógio por fonteManter o fetch() razoavelmente rápido
Proveniência por variável + rótulos (from X)Um label legível por gente
Escritas em os.environNada — 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 em result.error + result.error_kind. Um fetch que levanta exceção é contido pelo orquestrador e reportado como INTERNAL — isso é violação de contrato, não recurso.
  • O fetch() nunca pede entrada. A subida acontece em contextos sem TTY (gateway, cron, Docker). O run_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 reporta TIMEOUT e o seu resultado é descartado.
  • Você busca; o orquestrador aplica. Devolva o mapeamento que você contribuiria. Nunca escreva em os.environ por 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_version assume por padrão o SECRET_SOURCE_API_VERSION atual. 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 mapa env: 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étodoPadrãoSobrescreva 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)vazioVocê tiver um token de bootstrap (quase certamente tem)
fetch_timeout_seconds(cfg)120sSeu 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 ErrorKindVocê 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.

Momento

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​

TipoSignificado
NOT_CONFIGUREDHabilitado, mas falta token / projeto / mapa
BINARY_MISSINGCLI auxiliar não encontrada ou sem permissão de execução
AUTH_FAILED / AUTH_EXPIREDCredenciais ruins / expiradas
REF_INVALIDUma referência de segredo falhou na validação
NETWORKFalha no nível de transporte
EMPTY_VALUEO backend não devolveu nada para uma referência — nunca aplique "" por cima de uma credencial boa
TIMEOUTA busca estourou o orçamento
INTERNALQualquer outra coisa (bug, formato inesperado)