Files
acdl/acdl_platform/audit_ledger_design.md
T
Jon Chery 727c87339b fix(P08 prep): rename platform/ -> acdl_platform/ (stdlib shadow fix)
---ci---
project: acdl
phase: 8
milestone: v1.1
status: plan-as-execute
persona: lead-developer
task: T-8.0
type: prerequisite-fix
---/ci---

The Phase 07 P1 ('platform/ package shadows stdlib platform module')
became a Phase 08 blocker: boto3 imports uuid -> platform.system(),
which fails when the repo's platform/ package is on sys.path[0]. Renamed
platform/ -> acdl_platform/ (the verifier's recommended v1.2 fix, pulled
forward because Phase 08 needs boto3).

- git mv platform/ acdl_platform/ (history preserved)
- verify_phase07.sh: updated paths; removed the /tmp workaround (no
  longer needed; the shadow is gone)
- verify_phase06.sh: updated the new-dirs check for the rename
- README.md: layout table updated

Both verify_phase06.sh and verify_phase07.sh still pass; confidence_signal
now imports + runs correctly from the repo root. boto3 imports clean.
2026-07-21 18:53:41 +00:00

4.9 KiB

ACDL Tiered Audit Ledger Design (REQ-20)

Status: design authored in Phase 07 (milestone v1.1); the spike (Phases 08-10) implements the v1.0 hash chain + DynamoDB outbox write (D-041); the v1.2 build-out implements S3 Object Lock + JWS + async worker + DLQ + daily checkpoints.

The audit stream is the platform's tamper-evident record of every delivery action. The vision's "Audit truth lives outside the repository" bet [1] and "Not a mutable audit log" anti-goal [1] are the binding constraints. Version-control history does not satisfy regulatory evidence; the ledger is the source of truth.

Three tiers

  • Cold tier (source of truth): S3 with Object Lock in compliance mode, 7-year retention (ARCHITECTURE.md §9). No one — including root — can delete or overwrite until retention expires. The regulatory record.
  • Hot tier (query index): the acdl-evidence audit repo (unchanged from the v1.0 demo). Not part of the chain; a queryable mirror the evidence UI (evidence-ui/index.html) reads. Lightweight attestation linkage lives in the repo; the regulatory event body lives in S3.
  • Outbox (write path): DynamoDB, RPO = 0 (synchronous write before contract submission ack). Single-region in v1 (us-east-1).

Spike scope (D-041) — what Phases 08-10 implement

  • DynamoDB outbox: table acdl-outbox, PAY_PER_REQUEST (D-044), PK contractId, SK eventType#eventTs, TTL expire_at = now + 365d (1-year storage per ARCHITECTURE.md §8).
  • prev_event_hash chain: SHA-256 over canonical JSON (json.dumps(event, sort_keys=True, separators=(",", ":"))), lifted from the v1.0 demo's evidence_writer.py. Auto-genesis: first event has prev_hash="GENESIS".
  • Synchronous write via boto3 put_item (strong-consistent by default). No separate async worker / DLQ in the spike (RTO = workflow re-run).
  • Mirror to acdl-evidence: unchanged from v1.0 — the finalize step commits audit.json to the evidence repo (the hot tier).
  • Spike evidence event shape: {seq, ts, stage, event, prev_hash, hash, contractId, environment, stack, score, band}.

v1.2 build-out — what Phase 07 designs but the spike defers

  • S3 Object Lock: bucket acdl-evidence-lock-<account-id>, Object Lock enabled at creation, compliance mode, 7-yr retention (RetainUntilDate = now + 7y). The outbox→S3 path is an async worker that reads from the outbox and writes to Object Lock.
  • JWS detached signature (RFC 7515): the event payload is canonical-JSON-serialized, SHA-256 hashed, signed with a private key; the signature is stored detached alongside the payload. Signing key = platform-level KMS key (not per-contract — a per-contract key would explode the key-management surface), rotated quarterly. The jws field is added to the event shape in v1.2.
  • Async worker + DLQ: a Lambda (or a Gitea Actions scheduled workflow) reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an SQS dead-letter queue for failed writes. RTO = DLQ replay.
  • Daily checkpoints (§9): a daily job reads the last event hash and writes a "checkpoint" event to the ledger (+ optionally to a public notarization service). The spike runs in minutes, not days — no checkpoint in spike.

JWS vs chain — orthogonality note

The prev_event_hash chain gives ordering/tamper-evidence within the log (a deleted event breaks the chain visibly); JWS gives authenticity per event (a forged event is detectable without re-reading the whole chain). The chain is spike-scope; JWS is v1.2. Together they cover both integrity properties the vision's "Not a mutable audit log" anti-goal requires.

Outbox item shape (full, spike + v1.2)

  • PK contractId (UUID).
  • SK eventType#eventTs (e.g. POLICY_CHECKED#2026-07-21T12:00:00Z).
  • payload (the event body — hash-chained in spike, JWS-signed in v1.2).
  • prev_event_hash (chain link; GENESIS for the first event).
  • hash (this event's SHA-256 over canonical JSON).
  • approver_qa (Gitea username of the QA approver; empty in dev-only spike; populated on qa-promotion — D-042).
  • approver_prod (SRE username; empty in spike).
  • environment, stack, score, band.
  • expire_at (TTL = now + 365d).
  • v1.2 only: jws (detached signature), checkpoint_ref.

RPO / RTO table

Phase RPO RTO
Spike (D-041) 0 (sync outbox write) workflow re-run
v1.2 0 (sync outbox) async worker DLQ replay

Decision trail

  • D-041 — spike scope = hash chain + outbox write; Object Lock + JWS
    • worker + DLQ are v1.2.
  • D-044 — outbox mode PAY_PER_REQUEST; PK/SK; TTL expire_at = now + 365d; no separate async worker in spike.
  • D-042 — approver identities (approver_qa, approver_prod) live in the outbox; the separation-of-duties check (platform/separation_of_duties.py) reads approver_qa and compares to the prod-dispatch gitea.actor.