Files
acdl/core/hitl_matrix_design.md
T
Jon Chery e1be05287b feat(P39): refresh design docs + parameterize adapter (P1-1)
---ci---
project: acdl
phase: 39
milestone: v1.9
status: execute
---/ci---

Phase 39 — design-doc-refresh-and-p1-1-parameterization:

Design docs (REQ-100, REQ-101):
- hitl_matrix_design.md: 'dev-only spike'/'v1.2 wires the gates' framing
  replaced with v1.9 wired-gates reality; 8-concern matrix marked
  implemented (offline-testable subset + signed evidence artifacts,
  D-084); v1.9 wiring section cross-references hitl_gates.py +
  attestation_matrix.py; approver_dr noted.
- audit_ledger_design.md: outbox marked shipped+production since v1.8;
  S3 Object Lock + JWS + async worker + DLQ + daily checkpoints clearly
  labeled 'Deferred to a future milestone (D-083)'; RPO/RTO table updated;
  approver fields note v1.9 hitl_gates.attest.

P1-1 adapter parameterization (REQ-102, D-085):
- ecs-service interface.json: desired_count (default 1), launch_type
  (FARGATE), family (app) inputs added.
- alb interface.json: load_balancer_type (application), target_type (ip).
- adapter.py: hardcoded defaults replaced with inputs.get(<name>, <default>);
  hardcoded 'acdl-microservice-rt'/'acdl-microservice-igw' Name tags
  derive from the VPC name input.
- contract_resolver.py: child_input_map routes wires to the sub-resource
  that declares the input (desired_count → aws:ecs:service, family →
  aws:ecs:task_definition, target_type → targetgroup, etc.).
- microservice composition.json: wires added for the new inputs.

Tests: +21 (test_p1_1_adapter_parameterization.py, test_design_docs_current.py).
371 passed; run_ci.sh green; run_platform.sh --check-only green; v1.1 S3
regression preserved.
2026-07-23 04:24:25 +00:00

9.4 KiB

ACDL Human-in-the-Loop Matrix + Separation-of-Duties Design (REQ-21)

Status: design authored in Phase 07 (milestone v1.1); v1.9 wires the gates (Phase 42). The spike (Phases 08-10) was dev-only; HITL was not exercised then. v1.9 implements the qa/prod/dr pre-execution attestation gates, the 8-concern attestation matrix (offline-testable subset), and the outbox-based separation-of-duties check.

The vision's "Lower Environments are Autonomous; Higher Environments are Attested" tenet [1] and the "deliberate human attestation — not as a rubber stamp" requirement [1] are the binding constraints.

Gate model (ARCHITECTURE.md §10.1)

Pre-execution gates. The contract is held in a "validated but not applied" state until the human attests. qa, prod, dr are attestation gates. No partial deployment to roll back on rejection (qa, prod); dr is a separate deployment against a separate cluster/region. The canary/deployment-rollback model is explicitly not in scope for v1.

Gitea-specific gate mechanics (D-042)

Gitea has no Environments API and ignores environment: blocks (v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate is modeled as a workflow_dispatch with approval inputs:

  • qa gate: workflow_dispatch with approve_qa: true; the dispatch run's gitea.actor is the QA approver.
  • prod gate: workflow_dispatch with approve_prod: true; gitea.actor is the SRE approver.
  • dr gate: workflow_dispatch with approve_dr: true; same.

The approver identity of record = gitea.actor of the dispatch run (D-042). There is no other approval-identity signal in Gitea. The real OIDC path (blocked on go-gitea/gitea#36988) does not change this — OIDC authorizes the runner to AWS, it does not change how the platform records the human approver.

On GitHub, the equivalent is github.actor of the workflow_dispatch run; GitHub Environments with required reviewers are the native gate, but the workflow_dispatch approval-input fallback is used for byte-identical Gitea + GitHub workflows.

Reviewer routing (ARCHITECTURE.md §10.2)

Gitea CODEOWNERS routes the right reviewer to the right gate:

  • qa → QA team
  • prod → SRE team
  • dr → SRE team

CODEOWNERS routes; it does not enforce identity distinctness (that is the platform-internal outbox check in core/separation_of_duties.py).

Full 8-concern attestation matrix (§10.4)

The matrix is implemented in v1.9 as core/attestation_matrix.py (REQ-109, D-084). The concerns split into two tiers:

Offline-testable concerns (run for real, no operator input):

  • Contract NFRs (the platform's own contract validator).
  • Schema validity (jsonschema).
  • Policy pass (Checkov/Wiz/Kyverno PolicyCheckResult records).

Operator-supplied concerns (require an uploaded signed evidence artifact, validated for freshness + schema per D-084):

  • Functional correctness (e2e suite report).
  • Performance baseline (k6 / Gatling / Locust load test report).
  • Security posture (Trivy / Snyk / contract-declared scan + Security on-call signature).
  • Operational readiness (runbook published, dashboard exists, on-call rotation assigned, alerts configured).
  • Incident response (Sev-1 runbook tabletop or live drill completed).
  • Capacity / cost (FinOps forecast for next 30d within budget envelope).
  • Resilience (DR drill, chaos engineering report, backup verified).
  • dr-region deploy (most recent prod-bound dr drill as canary evidence).

The full table (lifted verbatim from §10.4):

Env Concern Evidence artifact Freshness Source Attester
qa Functional correctness Last successful run of contract-declared validation.e2eSuite with pass rate ≥ 99% Last 24h Test runner declared in contract QA
qa Performance baseline Load test report (k6 / Gatling / Locust) showing p99 latency < declared NFR and throughput > declared minimum Last 7d Load test runner declared in contract QA
qa Security posture Vulnerability scan (Trivy, Snyk, or contract-declared equivalent) with no criticals/highs, signed by Security on-call Last 24h Security scanner + Security team signature QA
qa Contract NFRs Platform-generated report: schema valid, NFR assertions (latency, throughput, error rate) within declared bounds At submission Platform contract validator QA
prod Operational readiness Runbook published, dashboard exists, on-call rotation assigned, alerts configured At submission, validated against last 30d history Platform + SRE SRE
prod Incident response Sev-1 runbook tabletop or live drill completed Last 90d SRE drill record SRE
prod Capacity / cost FinOps forecast for next 30d within budget envelope, cost anomaly baseline stored, budget alert configured Forecast valid for next 30d FinOps + SRE SRE
prod Resilience DR drill, chaos engineering report, backup verified DR: 180d; chaos: 90d; backup: 30d SRE + Platform SRE
dr dr-region deploy with the most recent prod-bound dr drill as canary evidence dr drill report Last 180d SRE SRE

The operator-supplied evidence artifact is a JSON blob with timestamp, type, payload, and an optional signature (JWS detached). Freshness is validated against the window above. Signature verification runs when ACDL_ATTESTATION_SIGNING_KEY_ID is set; it is skipped + logged when unset (dev/CI — D-089). The matrix fails loud if an operator-supplied concern is missing or expired for prod/dr.

Timeout behavior (§10.5)

Time State Action
Submission PENDING_ATTESTATION Notify responsible team
1 business day PENDING_ATTESTATION_WARNING Notify team + platform on-call (elevated path); emit PENDING_ATTESTATION_TIMEOUT_WARNING event
2 business days PENDING_ATTESTATION_AUTO_FREEZE Auto-freeze; require re-submission; emit PENDING_ATTESTATION_AUTO_FREEZE event; new submission linked via supersedes

Implementation: a Gitea on: schedule workflow (runs hourly) that scans the DynamoDB outbox for PENDING_ATTESTATION events with ts older than 1/2 business days and emits the warn/freeze events. Not implemented in v1.9 (roadmap item; the attestation gates themselves are wired, the timeout scanner is future work).

Rejection and rollback (§10.6)

Rejection returns the contract to a HELD state with the rejection reason captured as a PROMOTION_REJECTED event. The consumer fixes the cause and re-submits; the new submission is linked to the rejected one via supersedes (a contract-schema field — schemas/contract.schema.json). The audit chain is extended, not torn up (the "Not a mutable audit log" anti-goal). No partial deployment to roll back at any v1 gate.

Separation of duties (§10.3) — pointer to the .py

The identity-distinctness check is platform-internal, not GitHub-native, not Kyverno (in v1). Sequence:

  1. On promotion dev → qa, the platform reads the QA approver's identity from the workflow_dispatch run's gitea.actor (or github.actor) and writes it to the DynamoDB outbox keyed by contractId (attribute approver_qa).
  2. On promotion qa → prod, the platform reads the stored approver_qa from the outbox and the new SRE approver's gitea.actor from the prod-dispatch run.
  3. If approver_qa == approver_prod, the platform blocks the prod promotion, writes a SEPARATION_OF_DUTIES_VIOLATION event to the evidence stream, and routes a halt artifact to the SRE on-call.
  4. The check is implemented in core/separation_of_duties.py (T-7.8). The platform is the only writer to the outbox; the check is in the same process that has authority to block the promotion.

v1.9 implements route_halt_artifact as a real SNS publish (topic acdl-sod-halt, ARN from ACDL_SOD_HALT_TOPIC_ARN) with an outbox-event fallback when the topic ARN is unset (REQ-107). The attestation gate itself is core/hitl_gates.py (attest(contract_id, env, approver, evidence)), which records the approver to the outbox, runs the SoD check on prod, invokes the attestation matrix, and returns (ok, reason).

v1.9 wiring

v1.9 (Phase 41 + Phase 42) wires the gates end-to-end:

  • Phase 41 ships the per-environment CI job structure: one job per environment (dev/qa/prod/dr), each pointing at its respective contract (or the same contract + the environment workflow_call input). The qa/prod/dr caller workflows use workflow_dispatch with the approval inputs above; dev is autonomous (no gate). Promotion = running the matching job; no environment: field editing (D-082).
  • Phase 42 implements core/hitl_gates.py (the attestation gate), core/attestation_matrix.py (the 8-concern matrix), and the real route_halt_artifact (SNS + outbox fallback). scripts/run_platform.sh calls hitl_gates.attest before apply for qa/prod/dr (dev skips).

Decision trail

  • D-042 — approver identity = gitea.actor of the workflow_dispatch run; no Environments API in Gitea. On GitHub, github.actor.
  • D-013 (v1.0) — the workflow_dispatch approval-input fallback, re-used for the real platform's pre-execution gate model.
  • D-084 (v1.9) — 8-concern attestation matrix: offline-testable concerns run for real; operator-supplied concerns accept signed evidence artifacts validated for freshness + schema.
  • D-089 (v1.9) — attestation artifact signature verification is skipped when ACDL_ATTESTATION_SIGNING_KEY_ID is unset (dev/CI); required for prod/dr.