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ídasignificado
0totalmente verificado — cada linha verificada, cadeia intacta (ok: true)
1quebrado — uma quebra foi encontrada (ok: false), ou uso incorreto
3verificado 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ê:

ferramentapararetorna
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.