An append-only, hash-chained SQLite evidence log for LLM systems, with deterministic replay.
Project description
incident-ledger
An append-only, hash-chained SQLite evidence log for LLM systems — with deterministic replay. Pure Python standard library, no 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 makes an inference log into evidence by enforcing two properties in code, not prose:
- Contemporaneity (P2) — each row is written at inference time, stamped with the wall-clock moment it happened. The only mutation is
append: there is noupdateand nodelete. - Tamper-evidence — each row's
entry_hashis a SHA-256 over its content and the previous row's hash. Altering or deleting any earlier row — even by editing the SQLite file directly — breaks the chain, andverify()reports the exactseqwhere it broke. - Reproducibility (P3) — a sealed decision can be reconstructed into a replay bundle (pinned model version, seed, exact context) and rerun, with an honest boundary: exact match at temperature 0, or a pre-registered variance tolerance above it, and an explicit
not-replayableverdict when no seed was captured.
This is the minimal reference implementation accompanying the paper "Implementing Agentic-AI Accountability: A Minimal, From-Scratch Reference Stack Mapped to the IMDA MGF, EU AI Act, NIST, and ISO/IEC 42001" — see CITATION.cff. It implements the logging / audit-trail control common to those frameworks. It is deliberately small: every line is meant to be read.
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.
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. |
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 DOIs are reserved and not yet resolvable — they begin resolving when the Zenodo records are published. If a link above 404s, the deposit has not landed yet.
License
MIT — see LICENSE.
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.0.0.tar.gz.
File metadata
- Download URL: incident_ledger-1.0.0.tar.gz
- Upload date:
- Size: 22.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e98a1204f9f57ea6b36e8a796738a33878572e49188cdcbaf95aa5cb734b150
|
|
| MD5 |
683e5d88badfe27e91e36936dd789a85
|
|
| BLAKE2b-256 |
1fe1d4f1befbee4c46b31aa34c5191173ad4c6d81e146621a6a7b964ed8e2613
|
File details
Details for the file incident_ledger-1.0.0-py3-none-any.whl.
File metadata
- Download URL: incident_ledger-1.0.0-py3-none-any.whl
- Upload date:
- Size: 15.1 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 |
c78e2f408da66bea8e3ef9357baa456897bcc3d7ccb041d5c2996b1d5a7100c0
|
|
| MD5 |
406cf3ff194a18e0408dd465781c4817
|
|
| BLAKE2b-256 |
33b6c85cc70702bcf6cb0685a40b32c5613c76c4286f5ed394d353bb54d3e9d2
|