🔗 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
Metadata
Release files for incident-ledger 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| incident_ledger-1.1.0.tar.gz | 28.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| incident_ledger-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 48.4 kB
Release files / incident_ledger-1.1.0.tar.gz
| Download URL | incident_ledger-1.1.0.tar.gz |
|---|---|
| Size | 28.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
76938ba27f7fd344a6040ad1ed4b02b473cc70790b870f2d3dca0fc6bdc7ce10
|
|
BLAKE2b-256 checksum How to use checksums |
c8dee69d97f071baa7a57f8f6c0edfa494ecab7afcf2c425d4fb614e1cf169a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|
Release files / incident_ledger-1.1.0-py3-none-any.whl
| Download URL | incident_ledger-1.1.0-py3-none-any.whl |
|---|---|
| Size | 20.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
51d442b514caa417ee3385a0bdc6b86e44b7565322a3ae51267b4cd2b091d73f
|
|
BLAKE2b-256 checksum How to use checksums |
78260fd76c42133f4d280558a0638a970ff3c21a0ca476194161b9164b8d1d7f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|