Concept

The audit log.

Tamper-evidence enforced at the database layer, not in application code. Verbatim policy clause text on every event. Hash-chained for replay.

The audit log is the second architectural commitment that makes closegate defensible to your external auditor. It does three things differently from the typical application audit log:

1. Append-only at the database layer

The audit_events table has two SQLite triggers attached:

CREATE TRIGGER audit_no_update
BEFORE UPDATE ON audit_events
BEGIN
  SELECT RAISE(ABORT, 'audit_events is append-only');
END;

CREATE TRIGGER audit_no_delete
BEFORE DELETE ON audit_events
BEGIN
  SELECT RAISE(ABORT, 'audit_events is append-only');
END;

The mutation refusal happens at the database. An insider with code-write access to the application layer can't bypass it. An ops engineer with raw SQL access can't bypass it either — the trigger fires regardless of the connection. Only INSERT succeeds.

2. Every event carries verbatim policy clause text

When the policy gate fires, the event row includes:

  • event_type: OK / POLICY_VIOLATION / ESCALATION_PENDING / etc.
  • actor_id: human:<id> / llm:<session> / engine:<process>
  • policy_version: git commit hash of policy.yaml at decision time
  • clause_text: the exact words of the rule from policy.yaml
  • clause_pointer: JSON-pointer (RFC 6901) into the parsed policy structure
  • match_id / transition_id: the workflow entity
  • amount_usd, entity_id, accounts: the materiality / SoD inputs
  • timestamp_utc: monotonic via gate_now_utc()
  • hash_prev, hash_self: hash chain for tamper detection

Your auditor reads clause_text verbatim into the PBC bundle. Your engineer greps for clause_pointer in logs. Both are the same string of words.

3. Hash-chained for replay-detection

Each row computes hash_self = sha256(prev_hash + canonical_event_json). The chain detects any after-the-fact insertion or row-level rewrite that bypasses the triggers (which is hard, but not impossible at the OS level).

Run closegate-engine audit verify on a clean checkout against your recon.db. It exits 0 on intact, 2 on broken — used as a CI smoke test on the SOC 2 monitoring loop.

Querying the audit log

From the CLI:

# Recent events
closegate-engine audit tail -n 50

# Time-bounded
closegate-engine audit tail --since 2026-03-01 --until 2026-03-31

# By actor / event type
closegate-engine audit tail --actor "human:alice@example.com"
closegate-engine audit tail --event-type POLICY_VIOLATION

# Tamper-check
closegate-engine audit verify

From the MCP query_audit tool (T0, read-only — your AI agent can use it freely):

query_audit(
  since="2026-03-01T00:00:00Z",
  until="2026-03-31T23:59:59Z",
  event_type="POLICY_VIOLATION",
  match_id="m-abc-123"
)

The PBC bundle

For SOC 2 Type 2 or SOX walkthroughs, the bundle is produced by:

closegate-engine audit-evidence-export \
  --since 2026-01-01 --until 2026-03-31 \
  --out evidence-2026-Q1.zip

Seven files: audit sample, actor registry, dead-letters, policy versions, eval runs, sweeper runs, README. Reproducible from any clean checkout. Full walkthrough in For auditors.

What this is not

  • Not a SIEM. The audit log is the book-of-record for state-changing decisions. Pipe to your SIEM for cross-system correlation; the audit log is the source-of-truth.
  • Not a write-ahead log. The WAL is for crash recovery; the audit log is for audit attribution. Both can coexist; they record different things.
  • Not a generic event store. The schema is finance-specific (clause text, materiality, SoD fields). For non-finance event sourcing, use a different store.

Adjacent reading

Inbound

Talk to the maintainer

Two design-partner slots open this quarter. One real workflow, your real policy.yaml, monthly 30-min call, direct line. Apache-2.0, self-hosted, no seat licensing — forever.