Skip to main content

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.

PyPI Python License: MIT DOI


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.

Both the software and the paper are permanently archived on Zenodo under the DOIs above.

License

MIT © Abhijeet Verma

Built to be read, not just run.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

incident_ledger-1.1.0.tar.gz (28.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

incident_ledger-1.1.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

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

Hashes for incident_ledger-1.1.0.tar.gz
Algorithm Hash digest
SHA256 76938ba27f7fd344a6040ad1ed4b02b473cc70790b870f2d3dca0fc6bdc7ce10
MD5 938cd8048fad43c69cb52104d6421f8f
BLAKE2b-256 c8dee69d97f071baa7a57f8f6c0edfa494ecab7afcf2c425d4fb614e1cf169a2

See more details on using hashes here.

File details

Details for the file incident_ledger-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for incident_ledger-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 51d442b514caa417ee3385a0bdc6b86e44b7565322a3ae51267b4cd2b091d73f
MD5 1d16e569da8839e5ae357607d391a4f1
BLAKE2b-256 78260fd76c42133f4d280558a0638a970ff3c21a0ca476194161b9164b8d1d7f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page