# ACDL Human-in-the-Loop Matrix + Separation-of-Duties Design (REQ-21) > **Status:** design authored in Phase 07 (milestone v1.1); v1.2 wires the > gates. The spike (Phases 08-10) is **dev-only**; HITL is not exercised > (the spike contract has `environment: dev`). 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 v1.2 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. ## 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 `platform/separation_of_duties.py`). ## Full 8-concern attestation matrix (§10.4, lifted verbatim) | 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 | ## 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 the spike (dev-only). ## 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` 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 `platform/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. ## Spike scope note The spike is dev-only (REQ-27 contract has `environment: dev`), so HITL is not exercised. Phase 07 authors the design; Phase 10's `verify_phase10.sh` does not assert HITL behavior. v1.2 wires the gates against this design. ## Decision trail - **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch` run; no Environments API in Gitea. - **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback, re-used for the real platform's pre-execution gate model.