ADR-026: Tamper-evident audit log: git-native primary, self-chained fallback
Date: 2026-06-12 Status: Accepted (owner-delegated review, 2026-06-12) Author: Rômulo Febasi Revision: rewritten 2026-06-12 after the multi-user / git-versioned review (the original draft assumed a single writer with no external witness: wrong for the common case; see Context).
Context
.synapse/audit.ndjson is append-only and every record carries a
payload_sha256, but syn audit verify only re-hashes each record’s payload, it cannot detect deletion, reordering, or truncation of records. The first draft
of this ADR fixed that with a single linear hash-chain (seq + prev) plus one
ed25519-signed checkpoint. That design assumed one writer, one key, one reader,
no external witness.
A Synapse workspace is created locally and offline, but in practice it is very likely a git repository with more than one person editing the same versioned project. That breaks every assumption of the single-writer draft:
- whose key signs the single checkpoint, when Alice and Bob both append?
- a linear
prevchain assumes total order; git is a DAG (partial order). Two clones extend the chain from the same parent → divergent chains the merge cannot linearize. - an append-only
prev-chained NDJSON is the worst possible format for git merges: every concurrent append conflicts and the conflict breaks the chain.
Crucially, git is itself a Merkle DAG: every commit hashes its tree and
parent(s), giving ordered, tamper-evident, branch/merge-aware, distributed
history for free, which a hand-rolled linear chain cannot. Today Synapse only
reads git (gix in at.rs/capture_git.rs); it never commits, the user
versions the workspace, so git commits are coarser than Synapse mutations and the
audit.ndjson is the fine-grained record committed alongside them.
The conclusion: in a versioned workspace the linear hash-chain + single checkpoint is both redundant with git’s Merkle DAG and harmful to merges. The audit log should be a semantic layer that rides on whatever integrity substrate exists. This ADR covers the log format and verification; the identity/signing/multi-user trust model it relies on is ADR-030.
Decision
A two-tier integrity model, selected by whether the workspace is a git repo
(gix discovery):
Tier 1: git-versioned (primary, multi-user).
- Delegate ordering and sequence tamper-evidence to git’s Merkle DAG. Drop
the global linear
prevchain and the single signed checkpoint: git already provides ordered, tamper-evident, distributed history, and signed commits provide attribution (ADR-030). - Make the audit log merge-safe via per-author segments:
.synapse/audit/<author-id>.ndjson, where<author-id>derives from the writer’s Synapse identity (ADR-016). Each author appends only to their own segment → concurrent appends never conflict, and there is no global chain to break.seqis monotonic per author; the global order is the git commit topology (recordtimestampis an advisory tiebreaker, not authority). Retention (ADR-013) applies per segment. - Each record stays the semantic unit: mutation kind, proposal id,
confidence tier (ADR-022), model/agent attribution, accepting identity
(BR-12.3), and
payload_sha256(content integrity vs the Markdown). It does not carry a cross-record chain. syn audit verifybecomes git-aware: (a) re-hash each record’s payload vs the Markdown blob at the commit that introduced the record, not the working tree, which legitimately changes under later mutations; (b) assert the git commit DAG is intact (gix); (c) delegate signature/identity checks to ADR-030 (commits signed by known, non-revoked identities); (d) optionally compare against the remote to detect a force-push/history rewrite (reuse ADR-011 git-drift).git blameon a segment already attributes each line to a commit + author: verification leverages that.
Tier 2, not versioned (fallback, single local user).
- Here there is no Merkle DAG to lean on, so the original design is correct and
necessary: keep the self-chained log (
seq+prev= SHA-256 of the prior record’s canonical bytes, genesis = zeros) plus an ed25519-signed.synapse/audit.head.jsoncheckpoint ({origin, size, head_hash, timestamp, sig}) refreshed at the end of every mutating command.verifywalks the chain, verifies the checkpoint signature, and assertscount/head_hash, catching edit, reorder, mid-delete, and end-truncation. syn doctorwarns when a workspace is unversioned: “not under git: audit integrity is local-only;git init(and a remote) is recommended.”
Naming and boundary (both tiers): this is tamper-evident, not
tamper-proof. Tier 1’s strength is real only with a shared/pushed remote or
multiple clones (a witness); a single local clone, like Tier 2, is only as strong
as what its owner can rewrite. A key-holder who deliberately rewrites both the
content and its attestation is out of scope. Zero new dependencies (SHA-256 +
ed25519 + gix already in tree). Implemented in SYN-239 (log + verify); the
signing/identity half is SYN-249 / ADR-030.
Consequences
- Improves: the common case (versioned, multi-user) gets ordered,
tamper-evident, distributed history from git’s battle-tested Merkle DAG
instead of a fragile hand-rolled chain; the audit log is conflict-free under
concurrent edits (per-author segments) and
git blame-friendly; the unversioned single-user case keeps full self-contained tamper-evidence. - Worsens: two code paths (versioned vs not) to maintain; per-author segment
layout is a format change with a migration (fold the existing single
audit.ndjsoninto the current author’s segment, or keep it read-only as a pre-split tail); Tier-1 attribution depends on ADR-030 signed commits being adopted, elsegit logauthor is forgeable. - Open: exact
<author-id>derivation (identity fingerprint vs short hash), and its default when no identity is seeded yet (a local anonymous id untilsyn setupruns); whetheraudit verifyshould require a clean git DAG or only warn; bootstrapping which identities are “known” for a shared workspace (an allowlist: ties to ADR-010 federation and ADR-030). Revisit if an external auditor must verify membership without reading the files (then a Merkle history tree earns its keep: still deferred).
Alternatives considered
- The single-writer linear chain + one signed checkpoint (the original draft). Redundant with git’s Merkle DAG and merge-hostile in the common multi-user case. Demoted to the Tier-2 fallback, correct only when the workspace is not versioned.
- A multi-writer CRDT / hash-DAG inside
audit.ndjson. Reinvents git’s commit DAG, badly, inside a file git already version-controls. Rejected: lean on git. - Merkle history tree (Crosby–Wallach) in v1. O(log n) proofs only matter for a remote auditor who won’t read the files; the local verifier reads them anyway. Deferred.
- RSA/bilinear accumulators; Certificate-Transparency witness networks. Trusted setup / multi-party equivocation defense: no surface here. Rejected (full rationale retained in ADR-030).
- Calling it “tamper-proof”/“immutable.” False for a local file. Rejected.
Research grounding
Git object model as a Merkle DAG; Crosby & Wallach, Efficient Data Structures for Tamper-Evident Logging (USENIX Security 2009); Schneier & Kelsey, Secure Audit Logs (1999) and Ma & Tsudik, FssAgg (the truncation result) (both grounding the Tier-2 fallback; RFC 9162 (Certificate Transparency) and Syta et al. CoSi (a single signer cannot self-prevent equivocation) which is why Tier 1 relies on git/remote as the witness). Signing/identity specifics: ADR-030.