Skip to content

Tamper-evident audit log (prev-hash chain)? #1261

Open

Description

@henriquebalesteros

Context

Reading docs/ARCHITECTURE.md and docs/security-boundaries.md: audit_log
records every mutation ("Every mutation, addressable by at DESC"), and the
schema's own comments describe it as "a separate, append-only trail" — though
nothing in the schema enforces that (the only triggers in the migrations guard
the FTS tables). The integrity story rests on the architecture: "the markdown
wiki is the source of truth, and SQLite is the derived index", with the wiki
owned by git2 and raw segments immutable. The isolation boundary map is
unusually rigorous; what I couldn't find is a mechanism that detects
post-hoc edits to the audit trail itself. If I missed one in the docs, this
question answers itself — please point me at it.

The counter-argument first

The obvious pushback is the architecture's own: SQLite is a derived index, so
tampering with derived rows is detectable by rebuilding, and git history
already gives the wiki tamper-evidence for free. I think that's correct for
page mutations, and I'm not proposing to replace that. Three cases it doesn't
cover, as far as I can tell:

  1. Rebuild-detection is passive. It only fires if someone actually
    rebuilds and diffs. A hash chain turns verification into a cheap
    sequential read — verify recomputing the chain from genesis — so tamper
    detection can run routinely instead of "when someone already suspects".
  2. Not everything is reconstructible from the wiki. audit_log covers
    every mutation, including ones with no wiki artifact (handoffs, feedback,
    reclaim, tombstones, run leases), and the wiki git repo owns wiki/ while
    raw/ lives outside it. A rebuild from the wiki validates pages, not
    those. (I did find the log_ledger behind reclaim-ledger-versions — row
    10c — but that tracks page versions with git as the backstop; the non-page
    mutations listed above have no integrity anchor of their own.)
  3. Local git history is rewriteable. The wiki .git lives in the data
    dir, and nothing inside the machine distinguishes a rewritten history
    from the original. A chain whose head can be compared against an older
    export (or simply noted in a backup) yields a divergence signal git alone
    won't produce.

Honest scope

Boundary 5d says it outright: component checks "do not claim
descriptor-relative protection against a concurrently malicious same-user
process". A chain stored in the same SQLite is no different — this is
tamper-evident, not tamper-proof. The threat it addresses is the
team-trust one on a shared server: a trusted operator (or a botched
restore/migration) edits or drops rows and nothing notices. If that threat is
out of scope by design, closing as wontfix is a completely reasonable answer
and I won't argue.

What this looks like in practice

This isn't hypothetical plumbing. A small local decision-layer project I
maintain has exactly this verify pass, run on this machine today. Intact
ledger — chain recomputed from genesis:

$ verify --ledger pilot.json
entries: 10
chain: intact (prev-hash + entry_hash recomputed from genesis)
verify: PASS

Same ledger, one field of one sealed entry rewritten by hand (a recorded
decision flipped, nothing else touched):

$ verify --ledger ledger-tampered.json
entries: 10
  - seq 0: entry_hash mismatch (entry modified after sealing)
verify: FAIL

Exit code 0 vs 1, and the divergence report points at the exact row.

Scale, on the same box: the same verify pass over a synthetic 10,000-entry
sealed ledger — recomputing every hash from genesis — takes ~45-50 ms and
still pinpoints the hand-edited row at its exact seq, and the chain
mechanics add ~4 µs to each written row over a plain JSON write (~0.5 µs).
Microseconds either way — cheap enough to run routinely, which is the whole
point of case 1. One laptop, benchmark available on request.

Shape (discussion, not implementation)

  • Each audit_log row stores prev_row_hash;
    row_hash = H(prev_row_hash || canonical row bytes); the genesis row is
    anchored at migration time.
  • One extra hash computed inside the existing single-writer transaction; no
    behavior change on the write path.
  • A verify pass recomputes the chain from genesis and reports the first
    divergence — a standard method, nothing novel is being proposed.

For context: that same small project converged on exactly this pattern, and
the verify output above is its real one — which is why I'm asking here rather
than assuming it transfers. ai-memory's rule that every invariant traces to a
prior-art bug is the right bar for deciding whether this earns a slot, and
"it doesn't" may well be the answer. If there's interest, I'm glad to draft
docs/design-tamper-evident-audit.md in the house format ("Status: design
proposal, not implemented") — including the awkward migration question:
existing rows can't be chained retroactively, so a trust-on-first-use anchor
at upgrade time would be an explicit decision the doc should record, not an
accident.

Activity

  1. added
    rfcOpen design discussion / request for comments — excluded from automated iss-audit triage
    on Oct 14, 2026
  2. akitaonrails commented on Oct 14, 2026

    @akitaonrails
    Owner

    Thanks for this. The analysis is fair: "append-only" lives only in comments today, and nothing enforces it. Before it can earn a slot, two problems need a design:

    1. Legitimate rewrites. move-project updates audit_log.workspace_id for the moved project (ops.rs), which would break a naive previous-hash chain. A chain would need either to exclude mutable columns from the hash or to record the move as its own chained event.
    2. Where the head lives. A chain stored in the same SQLite database can be recomputed by anyone able to edit its rows, which is exactly the threat it's meant to detect. It only proves something when the chain head is anchored outside the database, for example written into each backup receipt or exported periodically.

    I'm labeling this rfc. If you'd like to pursue it, a short design note answering those two points is the next step, aimed at 3.1 or later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    rfcOpen design discussion / request for comments — excluded from automated iss-audit triage

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions