---ci--- project: acdl phase: 21 milestone: v1.6 status: execute ---/ci--- Rename the acdl_platform/ package to core/ across the directory, all imports in tests/scripts/pipelines/workflows, and doc references. The package is imported as core.confidence_signal / core.contract_resolver / core.outbox_writer. The deploy workflow's platform-repo checkout dir is renamed acdl-platform/ -> platform/ (workspace path, not the python package). Both .gitea + .github workflows stay byte-identical. Note: the original target name 'platform/' shadows Python's stdlib platform module (pytest's import uuid -> platform.system() fails when the repo root is on sys.path, which every test does). 'core/' avoids the clash while honoring the intent (drop the verbose acdl_platform). Tests: 154 pass. run_ci.sh green.
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-evidenceaudit 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), PKcontractId, SKeventType#eventTs, TTLexpire_at= now + 365d (1-year storage per ARCHITECTURE.md §8). prev_event_hashchain: SHA-256 over canonical JSON (json.dumps(event, sort_keys=True, separators=(",", ":"))), lifted from the v1.0 demo'sevidence_writer.py. Auto-genesis: first event hasprev_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 commitsaudit.jsonto 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
jwsfield 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;GENESISfor 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; TTLexpire_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) readsapprover_qaand compares to the prod-dispatchgitea.actor.