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

```python
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:

1. **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.
2. **Is ordered.** The chain encodes sequence: entry N depends on entry N-1's root.
3. **Is lightweight.** No external services, no consensus protocol. Just SHA-256 and a list.
4. **Persists across restarts.** Attach SQLite persistence with one line.

## Installation

```bash
pip install merkle-audit
```

No required dependencies — pure Python stdlib.

Optional: SQLite persistence is included. FastAPI integration is available as an extra:

```bash
pip install merkle-audit[fastapi]
```

## Usage

### Basic

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

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

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

```python
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](https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md):
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

```bash
pip install merkle-audit[dev]
pytest
```

## License

MIT
