0d2cbdb423
Genericize forge-detection code: gitea→forge/generic_forge, GITEA_ACTOR→FORGE_ACTOR. Drop .gitea byte-identity test assertions (keep GitHub-side + contract conformance). Add test_no_forge_mentions.py guard test (REQ-230). Delete completed migration docs (NOVA_MIGRATION.md, NOVA_AWS_MIGRATION.md). Move NO_HUMANS_THESIS.md to .ciagent/ (internal artifact). Strip ciagent-internal provenance from synced docs (REQ-/D-/P-/CAP- IDs, milestone headers, .ciagent/PROJECT.md citations). Trim README.md (reusable deploy section, local key rotation paragraph). Fix version-tag drift (@v1.13→@v1.19, acdl/→nova/). ---ci--- project: acdl phase: 1 milestone: v1.20 status: execute requirements: [REQ-230, REQ-231, REQ-232] ---/ci---
119 lines
5.8 KiB
Markdown
119 lines
5.8 KiB
Markdown
# ACDL Tiered Audit Ledger Design (REQ-20)
|
|
|
|
> **Status:** design authored in Phase 07 (milestone v1.1); the
|
|
> hash-chain + DynamoDB-outbox path is **shipped + production since
|
|
> v1.8**. The S3 Object Lock + JWS + async worker + DLQ + daily
|
|
> checkpoints build-out is **deferred to a future milestone (D-083)** —
|
|
> it requires non-offline-testable AWS infrastructure (Object Lock
|
|
> bucket, KMS signing key, SQS DLQ, Lambda worker) and is not in v1.9.
|
|
|
|
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. **Deferred to a future milestone (D-083).**
|
|
- **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`).
|
|
**Shipped + production since v1.8.**
|
|
|
|
## Shipped scope (D-041) — production since v1.8
|
|
|
|
- **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 v1.9 (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).
|
|
- **Evidence event shape:**
|
|
`{seq, ts, stage, event, prev_hash, hash, contractId, environment, stack, score, band}`.
|
|
|
|
## Deferred to a future milestone (D-083)
|
|
|
|
The following build-out was authored as design in Phase 07 and is **not
|
|
in v1.9**. It requires AWS infrastructure that cannot be exercised
|
|
offline (Object Lock bucket, KMS signing key, SQS DLQ, Lambda worker)
|
|
and is deferred to a future milestone. The hash-chain + DynamoDB-outbox
|
|
path above remains the v1.9 production audit record.
|
|
|
|
- **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 when this ships.
|
|
- **Async worker + DLQ:** a Lambda (or a forge 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).
|
|
|
|
## 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 shipped (v1.8+); JWS is deferred (D-083). Together
|
|
they cover both integrity properties the vision's "Not a mutable audit
|
|
log" anti-goal requires.
|
|
|
|
## Outbox item shape (shipped + deferred fields marked)
|
|
|
|
- PK `contractId` (UUID).
|
|
- SK `eventType#eventTs` (e.g. `POLICY_CHECKED#2026-07-21T12:00:00Z`).
|
|
- `payload` (the event body — hash-chained in v1.8+; JWS-signed when
|
|
D-083 ships).
|
|
- `prev_event_hash` (chain link; `GENESIS` for the first event).
|
|
- `hash` (this event's SHA-256 over canonical JSON).
|
|
- `approver_qa` (CI username of the QA approver; populated on
|
|
qa-promotion by v1.9's `hitl_gates.attest` — D-042).
|
|
- `approver_prod` (SRE username; populated on prod-promotion by v1.9's
|
|
`hitl_gates.attest`).
|
|
- `approver_dr` (SRE username; populated on dr-promotion by v1.9's
|
|
`hitl_gates.attest`).
|
|
- `environment`, `stack`, `score`, `band`.
|
|
- `expire_at` (TTL = now + 365d).
|
|
- **Deferred (D-083):** `jws` (detached signature), `checkpoint_ref`.
|
|
|
|
## RPO / RTO table
|
|
|
|
| Phase | RPO | RTO |
|
|
|-------|-----|-----|
|
|
| v1.8+ (production, shipped) | 0 (sync outbox write) | workflow re-run |
|
|
| Future milestone (D-083) | 0 (sync outbox) | async worker DLQ replay |
|
|
|
|
## Decision trail
|
|
|
|
- **D-041** — shipped scope = hash chain + outbox write; Object Lock +
|
|
JWS + worker + DLQ are deferred (D-083).
|
|
- **D-044** — outbox mode `PAY_PER_REQUEST`; PK/SK; TTL `expire_at` =
|
|
now + 365d; no separate async worker in v1.9.
|
|
- **D-042** — approver identities (`approver_qa`, `approver_prod`,
|
|
`approver_dr`) live in the outbox; the separation-of-duties check
|
|
(`core/separation_of_duties.py`) reads `approver_qa` and compares
|
|
to the prod-dispatch CI actor. v1.9's
|
|
`hitl_gates.attest` populates these attributes.
|
|
- **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily
|
|
checkpoints deferred to a future milestone. Requires non-offline-
|
|
testable AWS infra. |