← All engines · Storage
AuditLog (hash-chained)
stable
v1.0.0 · last updated 2026-06-06 · source: cybercli.audit.log.AuditLog
§1
What it does
Append-only hash-chained ledger that records every gate decision, dispatch, license event, and posture change with a SHA-256 chain anyone can re-derive offline.
§2
Why you care
The AuditLog is the cryptographic artifact that turns 'transparent' from a marketing word into a verifiable fact. Every row carries the previous row's hash; every row's own hash is a SHA-256 of its canonical-JSON body. Flip a bit in any row and `cybercli audit verify` catches it. Anyone with the binary and the chain file — your CTO, your insurer, a regulator — can re-derive every entry hash with no network call.
Other vendors in the AI SOC category market transparency. Dropzone calls theirs 'Glass Box.' Charlotte AI says 'traceable.' Panther says 'closes the loop.' None of them sign their audit rows. CyberCLI's chain is the only cryptographic transparency artifact in the category and it lives in your SQLite, not their cloud — we never see your rows. We could not if we wanted to.
The chain is also how the product survives the company. If CyberCLI as a business changes hands or stops shipping, your existing install keeps writing rows to your chain, and `cybercli audit verify` keeps working with no network dependency. The hash chain is locally re-derivable forever — that property is what makes the Founder Pro 'perpetual fallback' clause possible.
§3
How it works
AuditLog sits on top of an SQLite table — `audit_entries` — with one row per AuditEntry. The schema carries `seq`, `decision_id`, `outcome`, `idempotency_key` (unique), `prev_hash`, `entry_hash` (unique), `timestamp`, and a canonical-JSON `payload` blob that holds the full entry contents. Indexes on decision_id + timestamp + outcome.
Writes go through `AuditLog.append(entry)`. The method takes a write-lock (SQLite WAL mode), reads the last row's `entry_hash` as the new entry's `prev_hash` (or `GENESIS_PREV_HASH` = 64 zeros if the chain is empty), computes the new entry's hash as `sha256(canonical_json(entry_with_prev_hash_filled))`, then inserts. Idempotency is enforced by the unique constraint on `idempotency_key` — a duplicate append silently returns the existing row instead of double-writing.
Reads come in three shapes. `head()` returns the latest row. `tail(n)` returns the n most recent. `verify_chain()` walks the chain in seq order, recomputes every `entry_hash` from the row's canonical-JSON form, and asserts that each `prev_hash` matches the previous row's recomputed hash. Verification is O(n) but the WAL-mode SQLite + the indexes keep a 50K-row chain verifying in well under a second.
The canonical-JSON serializer is the one detail that makes the chain re-derivable forever: it pins field order, escapes UTF-8 deterministically, and refuses to emit floats with platform-dependent precision. The serializer's spec is in `cybercli/audit/canonical.py`; it is the only file you must NOT change without minting a new chain — modifying canonicalization breaks every existing chain's verifiability.
§4
Configure & observe
CLI
$ cybercli audit head Show the latest row's seq + entry_hash. Used as a quick 'is the chain advancing' health check.
$ cybercli audit tail -n 20 Show the 20 most recent rows with outcome + decision_id. Default operator drill-in.
$ cybercli audit verify Walk every row and re-derive the chain. Exits 0 if intact; non-zero with a divergence report if any entry_hash mismatches.
Dashboard surfaces
Live tail of audit rows. Filter by outcome, decision_id, or model_route. The chain head hash is the load-bearing block at the top with a one-click copy button.
Audit row shape
any outcome any engine emits The AuditLog itself is not a producer of outcomes — every other engine produces them and append()s. See the upstream engines for their specific outcome lists. Every row carries seq + prev_hash + entry_hash + idempotency_key + a typed payload block whose shape is set by cybercli/audit/entry.py.
§5
Status
Stable — the chain has been on the v1 acceptance install since 2026-06-03; current head at seq 322 with zero divergences across 322 verified rows. The canonical-JSON serializer has been frozen since 2026-05-01.
Known limit: there is no built-in remote backup or off-box replication of the chain. Operators who want geographic redundancy use `cybercli backup` (which snapshots the SQLite file) or wire the chain DB into their own backup tool. Remote chain mirroring with conflict-free merge ships in v1.5 + Enterprise.
Shipped 2026-06-07: `AuditLog.verify_chain_stream()` is a generator that yields progress dicts (`verified_so_far` / `current_seq` / `total_entries` / `done` / `ok` / `reason`) every N rows verified, defaulting to N=1000. The dashboard's `/audit` verify button now has a progress channel for chains past 100K rows; non-streaming `verify_chain()` still ships for scripts that want a single (ok, reason) tuple.
Shipped 2026-06-07: indexed `action_template_id` column with a composite `(action_template_id, outcome)` secondary index. ALTER TABLE ADD COLUMN migration is idempotent — existing databases pick up the column on next open + the append path mirrors the field into it on every new entry. New `AuditLog.tally_by_template_and_outcome(outcomes=None)` method runs as a pure index scan with no per-row JSON parse, replacing the round-11 promotion recommender's `iter_by_outcomes()` + per-row deserialize pattern. Legacy rows with NULL action_template_id are skipped — same fallback as iter_by_outcomes — so a long-running install picks up the speedup only for entries written after the migration.
How this engine talks to the rest