Agent Governance ToolkitAgent Governance Toolkit 4.1.0 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
47 small wins to finish your pathNext lesson →

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.

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

ExampleA fresh chain verifies and proves one entry
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))

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.

ExampleOne changed entry breaks the whole chain
# 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)

Merkle chain vs the flight recorder

Merkle chainFlight recorder
Lives inMemoryA SQLite file
Proves one entryYes, a short proofNo, read the row
Detects a changeYes, via the rootYes, via the chain
Survives a restartNoYes

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.
Watch out. This chain lives in memory and does not survive a restart on its own. It is the proof layer, not the durable store. Keep the flight recorder or the JSONL file for persistence, and use the Merkle root for proofs.
Try it yourself
  • 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.