Arcaeon Ledger
Um registro de ações para agentes de IA com encadeamento de hash e evidência de adulteração, além de um proxy stdio que grava chamadas de ferramentas na junção, de modo que o registro não dependa da cooperação do agente.
Documentação
arcaeon-ledger
Ferramentas de observabilidade mostram o que seu agente fez. arcaeon-ledger permite que você prove isso.
Cada registro é encadeado por hash ao anterior. Edite uma linha, exclua uma ou
reordene o histórico, e cada elo posterior se quebra — verify nomeia a linha exata.
Você é dono do registro e pode provar que ele não foi alterado. Zero dependências, um
arquivo JSONL, dois verbos.
pip install arcaeon-ledger # then: from arcaeon_ledger import Ledger
from arcaeon_ledger import Ledger
log = Ledger("agent.log.jsonl")
log.append({"tool": "web.search", "query": "weather in LA", "result_ok": True})
log.append({"tool": "payment", "amount": "49.00", "currency": "USD"})
log.verify() # VerifyResult(ok=True, rows=2, chained=2, ...)
Adulteração é detectada, não apenas esperada:
# someone edits row 2's amount in the file by hand...
log.verify() # VerifyResult(ok=False, first_break="line 2: chain mismatch")
CLI (conecte-o ao CI ou a um gate de pré-lançamento — um log adulterado sai com código diferente de zero, e um log que só pôde ser parcialmente atestado não sai mais como um totalmente verificado):
python -m arcaeon_ledger.cli append agent.log.jsonl '{"tool":"search","ok":true}'
python -m arcaeon_ledger.cli verify agent.log.jsonl
python -m arcaeon_ledger.cli verify --strict agent.log.jsonl
verify códigos de saída (0.5.7):
| saída | significado |
|---|---|
0 | totalmente verificado — cada linha verificada, cadeia intacta (ok: true) |
1 | quebrado — uma quebra foi encontrada (ok: false), ou uso incorreto |
3 | verificado somente dentro do escopo (ok: null em todos os casos): nenhuma quebra encontrada, mas linhas prechain sem encadeamento foram puladas sem verificação (verified_scope: "bounded_prechain_skipped"); ou as únicas quebras são declaradas ("bounded_declared_break"); ou o arquivo tem zero linhas ("empty"). Um prefixo "legado" fabricado cai aqui, nunca em 0. Passe --strict para tornar os dois primeiros um 1 rígido. |
Um gate de CI deve tratar apenas 0 como verde:
python -m arcaeon_ledger.cli verify agent.log.jsonl
case $? in
0) echo "fully verified" ;;
3) echo "chain intact but prechain rows skipped unverified — inspect, or use --strict" ; exit 1 ;;
*) echo "ledger broken" ; exit 1 ;;
esac
Prove quem agiu, não apenas a ordem
Uma cadeia de hash prova integridade de sequência — ela não pode provar quem escreveu cada entrada ou
se eles tinham permissão. Anexe um bloco authority para vincular o ator e sua
superfície de permissão à linha encadeada (com evidência de adulteração):
from arcaeon_ledger import Ledger, authority
log = Ledger("agent.log.jsonl")
log.append(
{"tool": "payment", "amount": "49.00"},
authority=authority(
"agent://billing-7",
capability_version="v3", # what they were allowed to do
tool_schema={"name": "payment", "args": ["amount"]}, # hashed, not just named
time_source="ntp", # trust surface of the clock
),
)
Agora a questão de auditoria se afina de "isso foi editado?" para "isso foi editado e o escritor estava autorizado?" — editar o principal, a capacidade ou o hash do esquema quebra a cadeia como qualquer outra adulteração. Isso compõe evidência de adulteração com replay de permissão. (Enviado em resposta ao feedback da comunidade no lançamento.)
Por que isso existe
A dor não atendida mais forte para construtores de agentes em 2026 é a lacuna de confiabilidade/auditoria:
um agente "conclui" uma tarefa e o resultado está silenciosamente errado, e você não consegue
reconstruir — ou provar — o que realmente aconteceu. Plataformas de observabilidade rastreiam
execuções; nenhuma lhe dá um registro com evidência de adulteração, portátil e de sua propriedade. A regulamentação
também está chegando: a Lei de IA da UE exige que sistemas de alto risco permitam tecnicamente
a gravação automática de eventos ao longo de sua vida útil (Art. 12(1)) e exige
que provedores e implantadores mantenham esses logs, na medida sob seu controle,
por pelo menos seis meses (Art. 19(1), Art. 26(6)). A Lei determina gravação
e retenção — evidência de adulteração não é a palavra dela, é nossa: quando alguém pergunta
se um log retido ainda é o log, essa pergunta precisa de uma resposta mais forte
que confiança. arcaeon-ledger é a menor versão honesta: um log de ações
criptograficamente encadeado que você insere, possui e verifica.
Como a cadeia funciona
chain = sha256(prev_chain + json.dumps(row_without_chain, sort_keys=True, ensure_ascii=False))[:32]
Observe os separadores: o corpo da cadeia usa o espaçamento padrão do Python ", " / ": ",
não a forma compacta json-c14n:v1 que os resumos do artefato usam. Um
verificador entre linguagens tem que reproduzir esse espaçamento exatamente.
O valor da cadeia é truncated_sha256_128 — os primeiros 32 caracteres hexadecimais (128 bits)
de SHA-256, não o resumo completo. Nomeado assim para ninguém citá-lo como SHA-256 completo:
128 bits é suficiente para detecção de edição/acidente, mais fino se você quiser que a cadeia
em si seja cara de processar após uma reescrita (crédito: revisão do atomic-raven).
Cada linha compromete todo o histórico anterior a ela. A primeira linha encadeia a partir de uma
semente fixa "genesis". Linhas sem um campo chain são toleradas apenas antes
da primeira linha encadeada (para que você possa adotá-la em um log existente); uma linha sem encadeamento
aparecendo depois que a cadeia começa é sinalizada. Em uma incompatibilidade, a verificação
continua a partir do valor reivindicado para contar danos posteriores honestamente, em vez de
cascatear uma quebra em ruído.
O que isso prova — e as cinco coisas que não prova
Ser preciso aqui é o produto, não um aviso. Uma cadeia de hash prova que o conteúdo registrado
de cada linha não foi alterado no lugar após a escrita:
edição no meio do arquivo, exclusão e reordenação quebram tudo e verify nomeia a linha.
Uma palavra nessa frase mudou em 0.5.8, e a razão é o tipo de coisa para a qual esta seção existe. Costumava dizer "os bytes registrados", o que reivindica mais do que a cadeia faz. A cadeia é calculada sobre cada linha analisada de volta do arquivo, e o leitor normaliza sequências de bytes que não consegue decodificar — então duas strings de bytes diferentes dentro de tal região leem de forma idêntica e produzem o mesmo veredito. O que é protegido é o significado de cada linha, não os bytes exatos do arquivo. Se você precisa de custódia em nível de byte, faça hash do próprio arquivo junto com isso.
Isso não prova por si só cinco outras coisas:
1. Truncamento. Corte as linhas mais recentes e o que resta verifica limpo — nenhuma cadeia somente de anexação captura isso sozinha. Feche isso publicando a cabeça em algum lugar fora do seu próprio controle, em uma cadência:
pin = log.head().as_pin()
# -> "arcaeon-ledger head chain=9f3c… rows=204 as_of=2026-08-13T17:40:00Z"
# post `pin` to a git commit / public comment / notarization anchor.
# a reader compares a fresh head() against the last pin; a truncated or
# re-minted history disagrees. the MAX gap between pins is your security
# parameter, not the average — an attacker picks the gap.
2. Verdade. A cadeia notariza o que foi escrito — um registro com evidência de adulteração de uma alucinação ainda é uma alucinação com um checksum. Para fazer uma linha falar sobre o mundo, faça hash de um artefato re-buscável (URL+bytes, um snapshot, saída de ferramenta) e armazene esse resumo na linha, para que um terceiro possa re-obtê-lo e comparar.
3. Autoria. authority() (acima) registra quem-reivindicou-o-quê, mas são dados
na linha, não uma assinatura — um reescritor que re-cunha desde a gênese também re-cunha isso.
Ancoragem externa de cabeça (#1) é a coisa que um re-cunhador não pode avançar.
4. Prefixo-legado-fabricado. Linhas sem campo chain são toleradas antes
da primeira linha encadeada — isso é deliberado, para que você possa adotar a cadeia sobre um
log existente sem reescrever seu histórico. Mas linhas puladas são linhas não verificadas,
e o verificador não consegue distinguir histórico legado real de um prefixo fabricado. Então
(0.5.7) uma verificação não estrita que pulou quaisquer linhas nunca cunha um verde: ok é
None — "nenhuma quebra encontrada, verificado dentro do escopo" — falso, com o escopo em banda
(verified_scope: "bounded_prechain_skipped") e a contagem em prechain; o CLI
sai com 3, não 0. Apenas uma varredura que verificou cada linha retorna ok=True. Se seu
log é encadeado desde a gênese e não deve ter linhas legado legítimas, passe
verify(strict=True) / --strict — ele trata qualquer linha sem encadeamento como uma quebra, vermelho
rígido. (Uma linha sem encadeamento inserida depois que a cadeia começa já é sinalizada em
todos os modos.)
5. Completude. Esta é a grande, e é estrutural: o agente decide
o que chamar de append. Um log com evidência de adulteração das chamadas que um agente escolheu
relatar ainda é auto-relato. Nada dentro desta biblioteca pode fechar isso, porque
qualquer coisa que o agente invoca, o agente pode se recusar a invocar.
Feche isso movendo a caneta para fora do alcance do agente — registre na costura, em um processo de SO separado que o agente não possui, não pode pular e não pode ver:
pip install arcaeon-adapter
python -m arcaeon_adapter --ledger seam.log.jsonl -- <your mcp server command...>
arcaeon-adapter é um
proxy stdio que encaminha JSON-RPC byte por byte entre um cliente MCP e servidor,
escrevendo uma linha encadeada por hash por tools/call em seu próprio ledger. Envolvê-lo em torno
do servidor MCP desta própria biblioteca produziu o número que faz o ponto: o
diário do próprio servidor escreveu 0 linhas enquanto o log da costura capturou 5. A lacuna
entre o que um sistema relata sobre si mesmo e o que a costura observou é a
coisa que vale a pena medir.
Escopado honestamente, o primitivo é "este arquivo não foi reescrito no lugar" — pequeno,
verdadeiro e testável. As camadas acima (ancoragem externa via head(), ligação de artefato,
autoria assinada, gravação de costura) são como você o estende em direção a uma
reivindicação de evidência completa.
verify() em ledgers ausentes ou vazios
Os dois parecem a mesma coisa ("sem dados"), e verify() os mantém
separados, de propósito:
Ledger("never/written.jsonl").verify()
# VerifyResult(ok=False, rows=0, first_break="unreadable: [Errno 2] No such file...")
open("touched/empty.jsonl", "w").close()
Ledger("touched/empty.jsonl").verify()
# VerifyResult(ok=None, rows=0, chained=0, first_break=None, verified_scope="empty")
Um caminho que nunca foi criado não pode ser atestado — ok=False, "ilegível,"
igual a qualquer outra falha de leitura. Um caminho que existe e é genuinamente vazio tem
zero linhas para adulterar, mas zero linhas verificadas também não é um verde (desde
0.5.8): ok=None, rows=0, verified_scope="empty", falso, saída CLI 3. Automação
que ramifica em verify().ok obtém um vermelho para o arquivo ausente e um
não-aprovado para o vazio; leia first_break e verified_scope para distinguir
os dois pelo nome.
Quando o log foi escrito fora de banda: declare a quebra, não re-forje
Mais cedo ou mais tarde algo escreve no seu JSONL sem passar por append() —
um script, um incidente, uma pessoa com um editor. A cadeia quebra ali e permanece
quebrada, porque esse é o registro verdadeiro. Suas duas opções óbvias são ambas ruins:
viver com um vermelho permanente que não diz nada ao leitor, ou recalcular a cadeia para
que o arquivo fique verde — o que é forjar, e uma cadeia que você pode silenciosamente re-forjar
não é evidência de nada.
declare_break é a terceira opção. Ela anexa uma linha nomeando a quebra:
from arcaeon_ledger import declare_break, verify_file
declare_break("agent.log.jsonl", 25,
"Written out of band 2026-08-15 by a session hand-appending JSON "
"instead of calling append(). Content is true and preserved verbatim; "
"no chain value was ever computed for it, so none can honestly be supplied.")
r = verify_file("agent.log.jsonl")
r.ok # None — bounded, NOT True. Falsy.
r.verified_scope # "bounded_declared_break"
r.breaks # 0
r.declared # ["line 25: declared break (Written out of band 2026-08-15 ...)"]
A quebra permanece uma quebra, para sempre, em declared. O que muda é que uma quebra conhecida
e explicada deixa de se passar por uma inexplicada — e os bytes exatos do órfão
são fixados por sha256, então editar essa linha depois deixa o arquivo vermelho
novamente. Nunca retorna ok=True. Apenas uma varredura que verificou cada linha faz
isso, e uma linha desculpada não foi verificada. verify(strict=True) ignora
declarações inteiramente.
O que isso não faz, dito claramente: é um dispositivo de registro, não um
criptográfico. Qualquer um que possa escrever o arquivo pode escrever uma declaração, então
não levanta nenhuma barreira contra um atacante que já tem acesso de escrita. Defende
contra esquecimento, não contra adulteração. Não consegue distinguir um anexo honesto
fora de banda de um malicioso — why é uma frase humana não verificada. E só
pode declarar quebras verify() já encontradas; não faz nada sobre quebras que ninguém
notou. Use-o para manter um incidente honesto legível, nunca como uma forma de deixar um
ledger verde.
Vincule o que o agente realmente leu (ligação de artefato)
A cadeia prova que uma linha não foi editada. Ela não prova que a linha alguma vez foi verdadeira —
notarizará uma alucinação tão fielmente quanto um fato. bind_artefact fecha
essa lacuna para os casos em que você pode apontar para uma fonte re-buscável: faça hash dos bytes reais
que o agente leu e armazene esse resumo na linha, para que um terceiro possa
re-obter a fonte e comparar.
from arcaeon_ledger import Ledger, bind_artefact
log = Ledger("agent.log.jsonl")
art = bind_artefact("https://example.com/pricing") # or bytes, a file path, or a dict
log.append({"tool": "web.read", "url": "https://example.com/pricing", "artefact": art})
# art -> {"subject": {"name": "...", "digest": {"sha256": "..."}},
# "recipe": "sha256:raw-bytes:v1",
# "digest": "sha256:raw-bytes:v1:<hex>", "bound_at": "...", "source_meta": {...}}
Resumos são autodescritivos — nunca um hash hexágono puro. Cada um é
sha256:<recipe>:<version>:<hex>, carregando sua própria receita para que um estranho o reproduza
apenas a partir da string: raw-bytes:v1 (bytes opacos como lidos) ou json-c14n:v1
(uma canonicalização JSON fixada e documentada — chaves ordenadas, compacta, UTF-8). Receitas
são congeladas e versionadas somente em anexação, então linhas antigas mantêm sua receita para sempre e uma
regra alterada nunca faz o histórico parecer adulterado.
Verifique honestamente:
from arcaeon_ledger import verify_artefact
verify_artefact(art) # recipe reproducible + string self-consistent
verify_artefact(art, refetch=True) # for a URL: re-fetch and compare
# -> {"verdict": "live_match", # <- THE answer; read this field
# "digest_ok": True, "reason": None,
# "refetch": "match" | "mismatch" | "unavailable" | "skipped", "notes": [...]}
Leia verdict, não apenas digest_ok (0.5.7). digest_ok nomeia apenas a
perna offline — receita reproduzível, string autoconsistente — e permanece True
mesmo quando uma re-busca ao vivo discorda. A tag de nível superior verdict cunha a resposta
inteira em um campo: "digest_consistent" (perna offline passou, nenhuma comparação ao vivo
feita), "live_match", "live_mismatch" (conteúdo ao vivo não corresponde mais —
alterado ou adulterado, indeterminado), "live_unavailable" (a verificação ao vivo
solicitada não pôde ser executada), ou a razão de falha tipada em si quando a perna offline
falha. if out["digest_ok"] após refetch=True costumava ler verde através de uma
incompatibilidade ao vivo; out["verdict"] == "live_match" não pode.
Um rótulo que este build não consegue reproduzir é uma falha tipada, nunca uma aprovação. Se o
digest nomeia um algoritmo, receita ou versão de receita fora do
registry suportado, verify_artefact retorna digest_ok=False com um
reason legível por máquina — um de unknown_algorithm, unknown_recipe, unknown_recipe_version,
malformed_digest, subject_digest_mismatch — e nunca chega ao estágio de re-busca,
então uma receita não verificável não pode voltar como "match". Um digest que não conseguimos
recalcular é um digest que não verificamos, e "não verificado" não deve ser reportado como
"verificado". Versões antigas permanecem verificáveis ao continuarem listadas em
SUPPORTED_RECIPE_VERSIONS quando uma nova é cunhada, então a promessa de receita
append-only se mantém sem que o verificador aprove rótulos que nunca enviou.
O limite honesto, dito em voz alta porque é o ponto: uma re-busca
mismatch significa que o conteúdo mudou ou foi adulterado — indeterminado. Nunca é
reportado como prova de adulteração. A web muta, dá 404, tem paywalls e
personaliza; a vinculação prova "este é o digest dos bytes que o agente disse que
leu no tempo T," nada mais forte. Para uma captura neutra em vez da sua própria
busca, roteie a fonte por um snapshot notarizado; para existia-antes-de-T, ancore
o digest externamente. Cada uma é uma camada que você adiciona — declarada, não implícita.
A verificação externa: uma testemunha externa
A cadeia não consegue detectar truncamento sozinha — corte as linhas mais recentes e o que
resta verifica limpo (declarado em "o que não prova", acima). A correção é uma
testemunha: um guardião de registros fora do seu controle que mantém seu head
(rows, chain) em uma cadência. Uma vez que uma testemunha tem um pin do tempo T, um log
truncado tem menos linhas do que a testemunha viu, e um reescrito tem uma cadeia
diferente na linha testemunhada. Nenhum dos dois pode se esconder.
from arcaeon_ledger import Ledger, WitnessStore, publish_head, verify_against_witness
log = Ledger("agent.log.jsonl")
witness = WitnessStore("witness_pins.jsonl") # ideally on a host you don't control
publish_head(witness, "billing-agent", log) # record the current head — do this on a cadence
# later — did the log survive intact?
v = verify_against_witness(witness, "billing-agent", log)
v.verdict # "consistent" | "truncated" | "rewritten" | "no_record" | "witness_broken" | "local_broken"
bool(v) # truthy ONLY on "consistent" — a missing pin is no_record, never a false ok
# READ THE VERDICT WITH ITS QUALIFIERS, never the bare string alone:
v.witness_self_integrity # "verified" | "unestablished" | "broken"
Um "consistent" puro não é a resposta completa. O veredito também carrega
witness_self_integrity: se o armazenamento da testemunha consegue provar sua própria cadeia de pins
intacta. Um cliente hospedado que só expõe latest() não consegue se auto-verificar,
então seus vereditos leem unestablished — a comparação rodou honestamente, mas um
pin forjado servido por esse armazenamento compararia limpo. verified significa que a
cadeia do próprio armazenamento foi recalculada; broken significa que falhou. Um consumidor que
ramifica em v.verdict == "consistent" sem ler
witness_self_integrity está confiando na honestidade do armazenamento exatamente tanto quanto
confiaria no log — que é o arranjo que uma testemunha existe para substituir.
(Encontrado na auditoria de pré-invite de 2026-08-23, C14; o campo existe para que "não
verificado" nunca possa renderizar como "verificado e ok.")
WitnessStore é a testemunha de referência: um arquivo JSONL append-only de pins. Uma
testemunha hospedada é um wrapper HTTP fino exatamente sobre esse objeto; rode-o localmente
e você tem uma testemunha completa, offline, de custo zero e totalmente sob seu controle (com a
óbvia ressalva de que uma testemunha que você controla é tão independente quanto seu host).
O que isso prova, exatamente. Uma testemunha prova que seu log não foi truncado ou reescrito apenas em relação ao que a testemunha viu, e apenas tão recentemente quanto o último pin. Linhas adicionadas após o último pin estão desprotegidas até o próximo — então o intervalo MÁXIMO entre pins é seu parâmetro real de segurança, não a média, porque um atacante escolhe o intervalo. E não diz nada sobre se o conteúdo registrado era verdadeiro — isso é trabalho da vinculação de artefatos (acima); a testemunha só protege a forma do histórico.
O que a testemunha guarda. Apenas impressões digitais — (namespace, rows, chain, time) —
nunca o conteúdo do seu log. Sem senhas por design: se a testemunha for violada,
não há nada sensível para roubar, apenas hashes inúteis sem o log original.
Integre em qualquer agente MCP
arcaeon-ledger inclui um servidor MCP sem dependências, então qualquer cliente MCP (Claude Code,
etc.) pode dar ao seu agente logging com evidência de adulteração sem código. Conecte-o:
{
"mcpServers": {
"ledger": {
"command": "python",
"args": ["-m", "arcaeon_ledger.mcp_server", "--log", "agent.log.jsonl"]
}
}
}
O agente então tem cinco ferramentas. Duas são ferramentas de operador sobre um arquivo:
ledger_append(record) para registrar uma ação (retorna o hash da cadeia) e
ledger_verify(strict?) para provar que o log está intacto (ou obter a linha adulterada exata
de volta). O veredito de verificação tem três valores, igual à biblioteca:
ok: true = cada linha verificada, ok: null = cadeia intacta, mas linhas
prechain não encadeadas foram puladas sem verificação
(verified_scope: "bounded_prechain_skipped" — não é um verde), ok: false =
quebrado. Passe strict: true para tornar qualquer linha não encadeada uma falha dura.
Três são ferramentas de agente (0.7.0), para quando a saída vai para alguém — um principal que quer prova, ou um par decidindo se confia em você:
| ferramenta | para | retorna |
|---|---|---|
prove_my_conduct(namespace, events) | registrar um lote do que você acabou de fazer e entregar ao seu principal um hash | {rows, head_hash, chain_verified} |
verify_peer_ledger(jsonl_text, strict?) | julgar o log exportado de outro agente apenas pelo texto | {ok, rows, first_break, declared_breaks} |
declare_break(namespace, reason) | seu log quebrou — nomeie-o em vez de re-cunhar uma cadeia | {declared_line, declared_breaks, ...} |
prove_my_conduct re-verifica após anexar, então um agente cujo ledger foi
adulterado recebe chain_verified: false em vez de um head hash com um
verde anexado. verify_peer_ledger retorna first_break como um número de linha
inteiro (ou null) para que um agente chamador possa apontar para a linha ruim exata — ele nunca
toca no seu ledger (a exportação é verificada de um arquivo temporário descartável, e a
própria chamada cai uma linha em <log>.calls.jsonl como qualquer outra chamada de ferramenta), e
uma exportação sem linhas parseáveis retorna ok: null (verified_scope: "empty"),
porque um verde por enviar nada é a falsificação mais barata possível. declare_break recusa quando nada está quebrado, e
nunca restaura um verde.
Ledgers de agente vivem um arquivo por namespace sob --ns-dir (padrão ledgers/
ao lado de --log). Um namespace é um nome, não um caminho: [A-Za-z0-9._-], travessia
recusada em vez de sanitizada.
MCP é JSON-RPC sobre stdio e este servidor fala diretamente — sem SDK, sem instalação extra.
Status
Biblioteca principal, CLI e um servidor MCP plug-and-play, todos testados: a biblioteca
contra adulteração por edição / exclusão / reordenação (test_ledger.py), o servidor MCP
através de tools/list → append → verify no handler de requisição incluindo
detecção de adulteração, e as ferramentas de agente contra travessia de namespace,
adulteração de exportação de par por linha exata, e quebras declaradas (test_agent_tools.py). Extraído de um ledger de ações com cadeia de hash
rodando em produção. Ancoragem externa chega via head() (publique o pin
você mesmo) e a testemunha de referência (WitnessStore, acima); um nível de testemunha
hospedada (retenção, cadência automática de pins, exportação de conformidade) é a próxima camada.
MIT.