Tamper-evident Merkle audit chain for AI agent tool calls
Project description
merkle-audit
Tamper-evident audit logging for AI agent tool calls.
Every action your agent takes — web searches, file writes, API calls, payments — is committed to an append-only hash chain. If any historical entry is modified, all subsequent entries become cryptographically invalid. You get a verifiable record of exactly what your agent did and in what order, with no external infrastructure required.
from merkle_audit import log_event
entry = log_event(
"tool_executed",
{"tool": "web_search", "query": "current ETH price"},
actor="research-agent-1",
)
print(entry.leaf_hash) # sha256 of (prev_root + action + payload + timestamp)
print(entry.mmr_root) # running root — commits to the entire history
Why this matters for AI agents
When an agent takes actions in the world — spending money, sending messages, modifying files — you want a record that:
- Can't be quietly edited. If an agent (or anyone with DB access) modifies a past entry, the hash chain breaks from that point forward.
- Is ordered. The chain encodes sequence: entry N depends on entry N-1's root.
- Is lightweight. No external services, no consensus protocol. Just SHA-256 and a list.
- Persists across restarts. Attach SQLite persistence with one line.
Installation
pip install merkle-audit
No required dependencies — pure Python stdlib.
Optional: SQLite persistence is included. FastAPI integration is available as an extra:
pip install merkle-audit[fastapi]
Usage
Basic
from merkle_audit import AuditChain
chain = AuditChain()
e1 = chain.append("tool_executed", {"tool": "read_file", "path": "/etc/hosts"})
e2 = chain.append("tool_executed", {"tool": "write_file", "path": "/tmp/out.txt"})
e3 = chain.append("approval_requested", {"reason": "about to send email"})
print(f"{chain.leaf_count()} entries, root: {chain.current_root()[:16]}...")
Persist to SQLite
from merkle_audit import get_chain
from merkle_audit.persister import install_sqlite_persister
# Call once at startup — all subsequent log_event() calls are persisted
install_sqlite_persister("/var/lib/myapp/audit.db")
from merkle_audit import log_event
log_event("payment_sent", {"to": "0xabc...", "amount_usd": 50.00}, actor="billing-agent")
Verify an entry
from merkle_audit import AuditChain
chain = AuditChain()
e0 = chain.append("first_action", {"data": "hello"})
e1 = chain.append("second_action", {"data": "world"})
genesis = "0" * 64
assert chain.verify_entry(e0, prev_root=genesis)
assert chain.verify_entry(e1, prev_root=e0.mmr_root)
# Simulate tampering
from dataclasses import replace
tampered = replace(e0, action_type="something_else")
assert not chain.verify_entry(tampered, prev_root=genesis) # False — detected
FastAPI endpoint
from fastapi import FastAPI
from merkle_audit.fastapi import router
app = FastAPI()
app.include_router(router, prefix="/audit")
# GET /audit/chain → {"leaf_count": 42, "current_root": "a3f9..."}
How it works
Each entry commits to everything that came before it:
leaf_hash = SHA256(prev_root + ":" + action_type + ":" + payload_hash + ":" + timestamp)
new_root = SHA256(prev_root + ":" + leaf_hash)
The mmr_root after N entries is a single 32-byte value that cryptographically
summarises the entire history. To verify entry K, you only need its stored
leaf_hash and the mmr_root from the entry immediately before it.
This is a simplified Merkle Mountain Range: append-only, no peak merging, optimised for sequential audit logging rather than inclusion proofs in large trees.
ChainEntry fields
| Field | Type | Description |
|---|---|---|
leaf_index |
int | Sequential position (0-based) |
leaf_hash |
str | SHA-256 hex — tamper-evident commitment |
mmr_root |
str | SHA-256 hex — running root over all entries |
payload_hash |
str | SHA-256 hex of the serialised payload |
action_type |
str | Caller-defined event label |
actor |
str | Who performed the action |
created_at |
float | Unix timestamp |
Running tests
pip install merkle-audit[dev]
pytest
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file merkle_audit-0.1.0.tar.gz.
File metadata
- Download URL: merkle_audit-0.1.0.tar.gz
- Upload date:
- Size: 9.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.12.3 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5e33df2323799e860bccc674a799ad6ab8ad9acfcaf3ad4a6483082bcbf67b8
|
|
| MD5 |
c103f8bfa3ee63e767a524179f68fb0c
|
|
| BLAKE2b-256 |
1aa59c083446984cbbabb63ae9ba6ce0a1cfd878378f0c6cc9f4775c04eaaaca
|
File details
Details for the file merkle_audit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: merkle_audit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.12.3 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a637b5693ac6be8535ccb22843db64178566879cd314472739e696b09ef0dfce
|
|
| MD5 |
b4bcfc4d8c471181ce278477fd8f8c96
|
|
| BLAKE2b-256 |
6c135e47abb636adab6ab5e9f122d96c30fa28fab04e85798ec748bc56c3708e
|