Merkle proofs and CloudEvents
MerkleAuditChain keeps decisions in a Merkle tree, so you can hand someone a short proof that one entry belongs to the log without showing them the whole log.
Last updated: 14 Oct, 2026 · Agent Governance Toolkit 4.1.0
The JSONL logger writes every line in full. A Merkle chain, from agentmesh, does something the others cannot: it produces a compact proof about a single entry, and it detects a change to any entry by comparing one root hash.
Building the chain
Syntax: the chain uses its own AuditEntry from agentmesh, which needs an event type, the agent's DID and the action. Each added entry is hashed and linked into the tree.
from agentmesh.governance.audit import MerkleAuditChain, AuditEntry
chain = MerkleAuditChain()
ids = []
# add three decisions; each is hashed and linked into the tree
for action, decision in [("order_status", "allow"), ("refund", "allow"), ("delete_account", "deny")]:
entry = AuditEntry(event_type="tool_call", agent_did="did:mesh:shop", action=action, policy_decision=decision)
chain.add_entry(entry)
ids.append(entry.entry_id)Verifying the chain and proving one entry
verify_chain checks the whole tree and returns whether it holds. get_root_hash is the single value that stands for the whole log. get_proof returns the few hashes that prove one entry is in the tree, and verify_proof checks them against the root.
first_id = ids[0]
first_hash = chain._entries[0].entry_hash
ok, error = chain.verify_chain()
print("verify_chain:", ok, error)
# the root hash stands for the whole log in one value
root = chain.get_root_hash()
print("root hash length:", len(root))
# a proof is the few hashes needed to place one entry under that root
proof = chain.get_proof(first_id)
print("proof nodes:", len(proof))
print("verify_proof:", chain.verify_proof(first_hash, proof, root))verify_chain: True None root hash length: 64 proof nodes: 2 verify_proof: True
Catching a changed entry
Changing one stored entry breaks its hash, and because every hash feeds the root, verify_chain fails and names the entry that no longer matches.
# tamper with one stored entry, then re-verify the whole chain
chain._entries[0].action = "TAMPERED"
ok, error = chain.verify_chain()
print("verify_chain after tamper:", ok)
print("reason:", error)verify_chain after tamper: False reason: Entry 0 hash mismatch
Merkle chain vs the flight recorder
| Merkle chain | Flight recorder | |
|---|---|---|
| Lives in | Memory | A SQLite file |
| Proves one entry | Yes, a short proof | No, read the row |
| Detects a change | Yes, via the root | Yes, via the chain |
| Survives a restart | No | Yes |
Where you use a Merkle chain
- Handing a third party a proof that one decision is in your log, without revealing the rest.
- A tamper check on a whole log by comparing a single published root hash.
- A mesh of agents that exchange audit entries and need to verify each other's records.
Related
- Previous: The audit log as JSONL
- Next: An agent that can prove who it is
- Add a fourth entry and watch the proof node count change as the tree grows.
- Tamper with the second entry instead of the first and read which index the error names.
- Print the root hash before and after adding an entry and see it change.
This is what real progress feels like.