An append-only, hash-chained SQLite evidence log for LLM systems, with deterministic replay.
Project description
🔗 incident-ledger
Turn an AI system's logs into evidence.
An append-only, hash-chained SQLite evidence log for LLM systems — with deterministic replay. Pure Python standard library · zero dependencies · runs anywhere.
When an AI system takes an action — issues a refund, moves money, files a ticket — "we have logs" is not the same as having evidence. incident-ledger turns an inference log into evidence by enforcing three properties in code, not prose:
| Property | Guarantee | |
|---|---|---|
| 🕒 | Contemporaneity | Every row is written at inference time, stamped with the moment it happened. The only mutation is append — no update, no delete. |
| 🔒 | Tamper-evidence | Each row's entry_hash is a SHA-256 over its content and the previous row's hash. Alter or delete any earlier row — even by editing the SQLite file directly — and the chain breaks; verify() reports the exact seq. |
| 🔁 | Reproducibility | A sealed decision reconstructs into a replay bundle (pinned model, seed, context) and reruns — with an honest boundary: exact match at temperature 0, a pre-registered tolerance above it, and an explicit not-replayable verdict when no seed was captured. |
This is the minimal reference implementation accompanying the paper "Implementing Agentic-AI Accountability". It implements the logging / audit-trail control common to the IMDA MGF, EU AI Act, NIST AI RMF, and ISO/IEC 42001 — and it is deliberately small: every line is meant to be read.
How the chain works
graph LR
G["GENESIS<br/>(0 × 64)"] --> R1["seq 1<br/>H(content₁ + GENESIS)"]
R1 --> R2["seq 2<br/>H(content₂ + hash₁)"]
R2 --> R3["seq 3<br/>H(content₃ + hash₂)"]
R3 --> D["…"]
style G fill:#e8e8e8,stroke:#888,color:#000
style R1 fill:#d7ebff,stroke:#3b82f6,color:#000
style R2 fill:#d7ebff,stroke:#3b82f6,color:#000
style R3 fill:#d7ebff,stroke:#3b82f6,color:#000
Each seal depends on the one before it, so history can only be appended — never quietly rewritten.
Install
pip install incident-ledger
Or from source
git clone https://github.com/MaqAnquor/incident-ledger
cd incident-ledger
pip install -e .
Requires Python ≥ 3.10. No third-party dependencies.
Quickstart
from incident_ledger import IncidentLedger
# Open on a file for a durable ledger (":memory:" for tests).
ledger = IncidentLedger("evidence.db")
entry = ledger.append(
query="Is order #9871244 refundable?",
answer="Yes — $487 refund issued.",
confidence=0.91,
model_version="refund-clf-v3@sha-9c1a2f", # pin the exact model; never "latest"
context_snapshot="Policy R-12: refunds within 30 days of delivery.",
sources=("policy://refunds/R-12", "order://9871244"),
disclosed_limits=("partial-refund case untested",),
downstream_action="refund.issue",
trace={"sampling": {"seed": 482991, "temperature": 0.0}},
)
# The chain is intact...
assert ledger.verify().ok
# ...and tampering is detectable, even done directly against the file.
import sqlite3
raw = sqlite3.connect("evidence.db")
raw.execute("UPDATE ledger SET answer = 'No refund' WHERE seq = 1")
raw.commit(); raw.close()
result = IncidentLedger("evidence.db").verify()
assert not result.ok
print(result.broken_seq, result.reason) # 1 "altered content at seq 1: seal does not recompute"
Replay a sealed decision
from incident_ledger import replay_from_ledger
# Reconstruct the bundle from a sealed row and rerun it. The attempt itself
# is sealed back into the ledger as a new contemporaneous entry.
outcome = replay_from_ledger(ledger, seq=1)
print(outcome.mode, outcome.reproduced) # "deterministic" True
A row sealed with no captured seed is reported not-replayable rather than silently treated as deterministic — a gap you disclose, not one you paper over.
Independent verification: checkpoints
A hash chain the deployer controls end-to-end is still only tamper-evident: an owner who edits one row can recompute every later seal and present a clean chain. A checkpoint — a single RFC 6962 Merkle root over the sealed entries — is one short value you can hand to a counterparty or auditor. Once that root is witnessed outside your control, you can no longer rewrite any history it covers.
from incident_ledger import verify_inclusion
cp = ledger.checkpoint() # one root over all entries — publish/witness this
proof = ledger.prove_inclusion(seq=1) # O(log n) path, not the whole ledger
# A third party checks one entry against the root they witnessed — no ledger copy,
# no cooperation from the deployer at check time.
assert verify_inclusion(ledger.get(1), proof, cp.root)
The point, in one line: an owner can rewrite the whole chain so that verify() still passes — but the rewritten ledger's checkpoint root will not match a root witnessed beforehand.
What this does and does not buy. A root you compute locally is still owner-recomputable — by itself that's still tamper-evidence, not independence. This module supplies the machinery (a compact digest and O(log n) proofs); reaching true independent verifiability requires a party who is not the deployer to hold the checkpoint. The code makes witnessing cheap; it cannot make it unnecessary. No external service, no blockchain, no network — stdlib-only.
API
| Symbol | Purpose |
|---|---|
IncidentLedger(path=":memory:", *, now=time.time) |
The store. append(...), verify(), get(seq), entries(), head_hash, len(). |
LedgerEntry |
One immutable sealed row; payload(), recompute_hash(). |
VerificationResult |
Verdict of verify(): ok, entries_checked, broken_seq, reason. |
ReplayBundle |
Everything needed to rerun one decision: model version, seed, context, temperature, prompt. |
replay(bundle, ...) / replay_from_ledger(ledger, seq, ...) |
Rerun a bundle, or reconstruct one from a sealed row and rerun it. |
ReplayResult |
mode (deterministic | bounded-variance | pin-mismatch | not-replayable), reproduced, agreement. |
ledger.checkpoint() → Checkpoint |
A witnessable Merkle root over all entries: n, root, head_hash, created_at. |
ledger.prove_inclusion(seq) → InclusionProof |
An O(log n) proof that one entry sits under the checkpoint root. |
verify_inclusion(entry, proof, root) |
A third party confirms one entry belongs under a witnessed root — no ledger copy needed. |
merkle_root(entry_hashes) |
The RFC 6962 root over a list of seals, for building/checking checkpoints directly. |
Measured overhead
Tamper-evidence is cheap. On a modern laptop (file-backed SQLite, one synchronous commit per append), over 1,000 appends:
| Metric | Value |
|---|---|
| Mean append latency | ~0.47 ms (p95 ~0.60 ms) |
| Storage per 1,000 inferences | ~4.0 MB (payload-dominated) |
Full-chain verify() over 1,000 rows |
~31 ms |
| Hash-chain overhead vs. a plain-insert baseline | under 0.1 ms per append (within run-to-run variance) |
The hash chain adds far less than the SQLite commit already costs. Regenerate the numbers on your own machine:
python benchmarks/ledger_overhead.py --n 1000 --reps 3
Figures are measured on a single machine, single thread, one payload shape and one durability setting; they do not extrapolate to production write-concurrency or a different retention policy.
Tests
pip install pytest
pytest -q
Citing
If you use this software, please cite both the software and the paper — see CITATION.cff.
- Software:
10.5281/zenodo.21387489 - Paper:
10.5281/zenodo.21387487— Implementing Agentic-AI Accountability: A Minimal, From-Scratch Reference Stack Mapped to the IMDA MGF, EU AI Act, NIST, and ISO/IEC 42001
Both the software and the paper are permanently archived on Zenodo under the DOIs above.
License
MIT © Abhijeet Verma
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 incident_ledger-1.1.0.tar.gz.
File metadata
- Download URL: incident_ledger-1.1.0.tar.gz
- Upload date:
- Size: 28.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76938ba27f7fd344a6040ad1ed4b02b473cc70790b870f2d3dca0fc6bdc7ce10
|
|
| MD5 |
938cd8048fad43c69cb52104d6421f8f
|
|
| BLAKE2b-256 |
c8dee69d97f071baa7a57f8f6c0edfa494ecab7afcf2c425d4fb614e1cf169a2
|
File details
Details for the file incident_ledger-1.1.0-py3-none-any.whl.
File metadata
- Download URL: incident_ledger-1.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51d442b514caa417ee3385a0bdc6b86e44b7565322a3ae51267b4cd2b091d73f
|
|
| MD5 |
1d16e569da8839e5ae357607d391a4f1
|
|
| BLAKE2b-256 |
78260fd76c42133f4d280558a0638a970ff3c21a0ca476194161b9164b8d1d7f
|