From 0789c27ca22a32e6d2534d575cf1ad68a0047fa7 Mon Sep 17 00:00:00 2001 From: CIAgent Orchestrator Date: Thu, 20 Aug 2026 05:00:35 +0000 Subject: [PATCH] =?UTF-8?q?docs(P00):=20complete=20v1.29=20pre-execution?= =?UTF-8?q?=20=E2=80=94=20SPECIFY+CLARIFY+RESEARCH+PLAN+GRILL+MVP/UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---ci--- project: acdl phase: 0 milestone: v1.29 status: complete ---/ci--- --- .ciagent/CHECKPOINT.json | 53 ++-- .ciagent/CLARIFY.md | 226 +++++++++++++++- .ciagent/GRILL.md | 251 ++++++++++++++++- .ciagent/PERSONAS.md | 150 +++++++++- .ciagent/PLAN.md | 572 ++++++++++++++++++++++++++++++++++++++- .ciagent/PROJECT.md | 141 +++++++++- .ciagent/REQUIREMENTS.md | 248 ++++++++++++++++- .ciagent/RESEARCH.md | 256 +++++++++++++++++- .ciagent/ROADMAP.md | 28 +- .ciagent/config.json | 2 +- 10 files changed, 1882 insertions(+), 45 deletions(-) diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 253c76b..3f48ba4 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,35 +1,30 @@ { - "phase": 6, - "stage": "complete", - "milestone": "v1.28", - "phase_role": "final", + "phase": 0, + "stage": "grill", + "milestone": "v1.29", + "phase_role": "pre_execution", "attempts": 0, - "updated_at": "2026-08-19T23:59:00Z", + "updated_at": "2026-08-20T00:40:00Z", "project": "acdl", "projects": ["acdl", "nova-blockchain-exchange"], - "active_milestone": "v1.28", - "milestone_branch": "milestone/v1.28-cli-identity", - "phase_branch": "phase/06-final-review-ship", - "tag_line": "v1.27.x", - "phase_name": "final-review-ship", - "reqs_covered": ["REQ-323..353"], + "active_milestone": "v1.29", + "milestone_branch": "milestone/v1.29-reposplit-identity", + "phase_branch": "phase/00-pre-execution", + "tag_line": "v1.28.x", + "phase_name": "pre-execution", + "milestone_type": "feature", + "reqs_covered": [], "reqs_partial": [], - "caps_verified": ["CAP-033", "CAP-034", "CAP-035", "CAP-036", "CAP-037", "CAP-038"], - "invariants_added": ["INV-12", "INV-13", "INV-14", "INV-15", "INV-16", "INV-17"], - "decisions": ["D-226", "D-227", "D-228", "D-229", "D-230", "D-231"], - "milestone_complete": true, - "milestone_release": {"tag": "v1.27.6", "type": "feature"}, - "tests": {"total_passing": 1000, "failures": 0, "deselected": 5}, - "phases": [ - {"phase": 0, "tag": "v1.27.0", "status": "complete"}, - {"phase": 1, "tag": "v1.27.1", "status": "complete"}, - {"phase": 2, "tag": "v1.27.2", "status": "complete"}, - {"phase": 3, "tag": "v1.27.3", "status": "complete"}, - {"phase": 4, "tag": "v1.27.4", "status": "complete"}, - {"phase": 5, "tag": "v1.27.5", "status": "complete"}, - {"phase": 6, "tag": "v1.27.6", "status": "complete"} - ], - "grill": {"verdict": "PROCEED-WITH-CONDITIONS", "confidence": 0.76, "critical_resolved": 3, "tracked_resolved": 16}, - "audit": {"reconstruction": "PASS", "commit_discipline": "CLEAN", "branch_hygiene": "CLEAN", "file_discipline": "CLEAN"}, - "notes": "v1.28 COMPLETE. Feature milestone (CLI Canonicalization + Identity Layer). 7 phases (P0 + P1..P5 execution + P6 final). 31 REQs covered (REQ-323..353). 6 CAPs verified (CAP-033..038). 6 invariants added (INV-12..17). 6 decisions (D-226..231). Grill PROCEED 0.76 (3 critical + 16 tracked conditions resolved). 1000 tests passing, 0 failures. Audit: reconstruction PASS, commit/branch/file discipline CLEAN. Merged milestone/v1.28-cli-identity -> main. Tag v1.27.6 = milestone release. All milestone branches deleted." + "decisions": ["D-232", "D-233", "D-234", "D-235", "D-236", "D-237", "D-238", "D-239", "D-240"], + "carry_forward": ["Q7 (kj image verification dependency — M1.5 gate, verified in nova-platform-ops CI)"], + "personas": ["lead-developer", "backend-engineer", "security-engineer", "cli-engineer", "data-engineer"], + "grill": { + "verdict": "PROCEED-WITH-CONDITIONS", + "confidence": 0.72, + "critical_fixes": 4, + "tracked_conditions": 6, + "binding_decisions": ["G-1", "G-2.1", "G-2.2", "G-3", "G-4", "G-5"], + "critical_fix_ids": ["CF-1", "CF-2", "CF-3", "CF-4"] + }, + "notes": "v1.29 GRILL complete. PROCEED-WITH-CONDITIONS 0.72. 4 critical fixes applied to PLAN.md: CF-1 (M1.5 hard P6 ship gate, spike 8->12 items), CF-2 (covered-reference REQs gated by operator-attested Result column), CF-3 (P1 pushes v1.29.0 intermediate tag, P5 smoke no hedge), CF-4 (kj source-fetch confirmed before P1 Wave 1, recorded in kj-version.txt). 6 tracked conditions (TC-1..TC-6). Covered-reference pattern accepted as verification surface ONLY with CF-1+CF-2 (G-1)." } \ No newline at end of file diff --git a/.ciagent/CLARIFY.md b/.ciagent/CLARIFY.md index 3609a1f..6cb1a28 100644 --- a/.ciagent/CLARIFY.md +++ b/.ciagent/CLARIFY.md @@ -273,4 +273,228 @@ new ("introducing Nova-idp"). The mis-framing was in calling them All material ambiguities resolved at full autonomy (6 open questions + 5 grounding gaps → D-226..D-231, confidence ≥ 0.80). No human escalation triggered (all confidences ≥ 0.60 threshold). REQUIREMENTS.md updated -with the decision ledger + invariants. Next: RESEARCH. \ No newline at end of file +with the decision ledger + invariants. Next: RESEARCH. + +--- + +# CLARIFY — v1.29 Reposplit + Identity Layer Bring-Live + +> **Autonomy:** full. Auto-resolution with assumption logging per +> `config.autonomy.level: "full"`. No human escalation unless confidence +> < 0.60. The v1.29 spec is v1.1 (highly detailed — §7 resolves Q1-6, Q7 +> carried forward as a verification-gate dependency). This file records +> the v1.29 ambiguities and the scope-split grounding. + +--- + +## Method + +The v1.29 spec ("Universal Feature Specification — Reposplit + Identity +Layer Bring-Live", v1.1) is the most detailed spec the project has +received: it includes BDD acceptance criteria, an 8-item M1.5 spike +checklist, 7 decisions pre-drafted (D-232..238), 14 NFRs, and an +explicit §7 resolving Q1-6. Clarify work focuses on (a) the scope split +between `acdl` (CIAgent) and `nova-platform-ops` (out-of-band), (b) the +`kj` identity (Go binary vs. the v1.28 kyverno-json re-mapping), and (c) +the carried-forward Q7. Each ambiguity gets a decision ID (D-232+, +continuing from v1.28's D-226..D-231), a resolution, a confidence score, +and a rationale. + +--- + +## Prior-conversation resolutions (already locked, restated for the record) + +These were resolved by the user-approved execution plan in the +conversation that spawned v1.29. + +### Q-P1 — The spec creates a separate repo `nova-platform-ops`. CIAgent runs inside `acdl`. Where does the Terraform code land? + +**Resolution:** Terraform modules +(`networking`/`kms`/`identity`/`contract-ingest`/`bootstrap`/`edge`) are +authored **out-of-band** in `nova-platform-ops` (operator-owned). CIAgent +in `acdl` delivers only the acdl-side work (publish.yml, Gitea scrub, +CFN archive, operator guide, consumer bump) and tracks the ops-side +REQs as **covered-reference** (verification surface = the M1/M1.5/M2 +cutover gates documented in the operator guide). +**Confidence:** 1.0 (user-confirmed — "Author out-of-band in +nova-platform-ops"). **Decision:** scope split documented in +PROJECT.md §v1.29 + REQUIREMENTS.md §v1.29. + +### Q-P2 — The run scope. How far does this `/ci-run` go? + +**Resolution:** Full milestone through the final phase (P0 → P1..P5 → +P6 final review + audit + milestone ship, tag `v1.28.6`). +**Confidence:** 1.0 (user-confirmed — "Full milestone through final +phase"). **Decision:** n/a (execution scope, not a D-ID). + +### Q-P3 — Edge 8 / REQ-354 footnote: pilot consumer deploy bump. Handle how? + +**Resolution:** Include a cross-project phase (P5) in this CIAgent run +(multi-project mode is active). Bump `nova-blockchain-exchange` +deploy.yml `@v1.25` → `@v1.29` + smoke test. +**Confidence:** 1.0 (user-confirmed — "Cross-project phase in this +run"). **Decision:** n/a (execution scope). + +--- + +## Spec-grounded resolutions (from §7 + §5) + +### Q1 — State bucket bootstrap on day-0 (resolved per spec §7.1) + +**Resolution:** Manual one-time at the operator's secure scratch; Terraform +then adopts it via `terraform import`. Avoids bootstrapping the +bootstrapper. **Confidence:** 1.0 (spec §7.1 explicit). **Decision:** +D-235 (tag-pin handoff) — the state bucket is one of the imported +resources. + +### Q2 — `pyproject.toml` version bump (resolved per spec §7.2) + +**Resolution:** Bump to `1.29.0` in M1 (P2 — Gitea scrub phase) of v1.29 +alongside the Gitea scrub. **Confidence:** 1.0 (spec §7.2 explicit). +**Decision:** n/a (implementation detail, tracked in PLAN.md P2). + +### Q3 — WAF cost (resolved per spec §7.3) + +**Resolution:** Acceptable for the JWKS public surface; documented in +operator-guide cost section (~$5–10/month per WebACL + per-request). +**Confidence:** 1.0 (spec §7.3 explicit). **Decision:** documented in +REQ-OPS-GUIDE AC. + +### Q4 — Coverage 73.8% — does this milestone drive it down further? (resolved per spec §7.4) + +**Resolution:** Accept any further debt as carry-forward to the separate +NFR milestone. New modules have ≥80% coverage; older code paths are +unchanged. YELLOW carried without scope expansion. **Confidence:** 1.0 +(spec §7.4 explicit). **Decision:** n/a (NFR carry-forward, not a v1.29 +D-ID). + +### Q5 — CFN code deletion timing (resolved per spec §7.5) + +**Resolution:** Archive to `docs/archive/nova-idp-cfn-v1.28.md`; deletion +is a follow-up after the next pilot run verifies Terraform parity. +**Confidence:** 1.0 (spec §7.5 explicit). **Decision:** REQ-369 AC (3). + +### Q6 — `acdl-act-runner-role` reuse (resolved per spec §7.6) + +**Resolution:** Reuse the existing role for v1.29 to minimize IAM surface +changes; scope narrow per REQ-360. **Confidence:** 1.0 (spec §7.6 +explicit). **Decision:** covered by REQ-360 (IAM-NARROW). + +### Q7 — `kj` image verification dependency (CARRY-FORWARD per spec §7.7) + +**Resolution (carry-forward):** M1 cutover is conditional on the M1.5 +verification gate. **Recommendation:** Block M1 cutover until M1.5 +passes. If M1.5 fails three consecutive rebuilds, defer to M2a and ship +Nova-idp in read-only partial mode (no token issuance) until `kj` is +verified. **Impact if wrong:** A live token-vend that signs with a +broken ABAC path would let through a denied claim — fails closed only if +`ImageUri` is verified pre-apply. **Confidence:** 0.92 (spec §7.7 +explicit + D-236 cutover shape). **Decision:** D-236 (cutover shape + +rollback procedure). This is the **only** outstanding carry-forward; +CIAgent in acdl builds + publishes the image + the gate tests (P1), but +the live 3-rebuild verification happens in `nova-platform-ops` CI +(out-of-band). CIAgent does not block on it. + +--- + +## Grounding-gap resolutions (surfaced in pre-flight) + +### G1 — The spec's `kj` vs. v1.28's `kj` re-mapping + +**Ambiguity:** v1.28 (D-227) re-mapped the spec's `kj` engine → +kyverno-json (INV-4 swappable), explicitly stating "no new `kj` engine +is built." v1.29 reintroduces `kj` as a compiled Go binary +(`platform/abac/kj-version.txt`, pinned v0.0.3) embedded in an ECR +container image. Is this a contradiction? + +**Resolution:** No contradiction. v1.28's `kj` was a *policy engine* +reference; v1.29's `kj` is a *compiled Go binary* (a distinct artifact). +The kyverno-json engine remains the policy engine (INV-4). The v1.29 +`kj` binary is invoked via `subprocess.run(['/opt/kj/kj', 'apply', ...])` +by the Lambda handler — it is a **substrate** binary, not a policy +engine. The two coexist: kyverno-json evaluates ABAC policy; `kj` is the +container image's static binary that the Lambda runtime executes. No +collision. +**Confidence:** 0.95 (spec §3.3 Edge 5 item 4 explicit + v1.28 D-227 +scope). **Decision:** documented in PROJECT.md §v1.29 ID allocations + +KJ-STATIC NFR. + +### G2 — `REQ-363b` sub-requirement numbering + +**Ambiguity:** The spec uses `REQ-363b` for the Fargate defensive +fallback. The repo's REQ namespace is `REQ-NNN` (numeric). How to +record `363b`? + +**Resolution:** Keep `REQ-363b` as-is (sub-requirement of REQ-363). It +is a distinct requirement (Fargate fallback, KJ-LOCKSTEP) but logically +paired with REQ-363 (production substrate). The `b` suffix is +unambiguous and matches the spec. No collision with any existing REQ. +**Confidence:** 0.98 (spec explicit + no collision). **Decision:** n/a +(naming convention). + +### G3 — `REQ-370` gap + +**Ambiguity:** The spec jumps from REQ-369 to REQ-371. Is REQ-370 +missing or intentionally unused? + +**Resolution:** Intentionally unused per the source spec. REQ-370 is a +gap in the spec's numbering (likely a deleted/renumbered item during +spec v1.0 → v1.1). v1.29 does not allocate REQ-370; it remains a +reserved gap. **Confidence:** 0.90 (spec explicit gap, no content). +**Decision:** n/a (spec fidelity). + +### G4 — Covered-reference REQs and CIAgent verification + +**Ambiguity:** REQ-355, 356, 357, 358, 359, 360, 361, 362, 363, 363b, +364, 365, 366, 371 are authored in `nova-platform-ops` (out-of-band). +How does CIAgent verify them? Are they `human_needed`? + +**Resolution:** They are **covered-reference**, NOT `human_needed`. The +verification surface is the M1/M1.5/M2 cutover gates documented in the +operator guide (`docs/operator-guide-platform-ops.md`). The operator +guide lists each covered-reference REQ with its cutover gate entry +(M1/M1.5/M2). CIAgent verify marks them `covered-reference` and the +final-phase audit confirms the operator guide documents all gates. +**Confidence:** 0.94 (scope-split decision + spec §2.3 milestone +gates). **Decision:** documented in REQUIREMENTS.md §v1.29 + REQ-OPS- +GUIDE AC. + +--- + +## Assumptions (logged, not escalated — confidence ≥ 0.80) + +1. **`kj` v0.0.3** is available at the pinned SHA in + `platform/abac/kj-version.txt` and compiles with `CGO_ENABLED=0 + GOOS=linux GOARCH=amd64`. RESEARCH will confirm the source repository + + build commands. If the binary is not available, P1 (publish + pipeline) cannot produce the ECR image; M1.5 gate fails by + construction → M2a (Fargate toggle, same image) also fails → escalate + (but this is a spec dependency, not a CIAgent ambiguity). +2. **ECR repository** exists or is creatable in account `581513795199` + for the `kj` image. RESEARCH will confirm. The repo name is not + specified in the spec; the operator guide will document it. +3. **GitHub Releases** is the artifact distribution channel (per + REQ-354). The `acdl/acdl` repo is already on GitHub (the Gitea scrub + in REQ-367 standardizes on GitHub). NOVA_FORGE_TOKEN (Gitea) is + retained for `nova-platform-ops` releases only. +4. **The `nova idp setup --apply` terraform-delegation** (REQ-369 AC 2) + requires `terraform` to be on the operator's PATH. The CLI detects + terraform via `which terraform`; if absent, it falls back to the CFN + path with a deprecation warning (the CFN archive remains read-only + reference, but the delegation is the preferred path). +5. **The M1.5 8-item spike** (spec §3.3 Edge 5) is the verification + gate. CIAgent in acdl authors the *tests* (test_idp_auth, + test_kms_roundtrip, ABAC E2E) in P1; the *live 3-rebuild run* + happens in `nova-platform-ops` CI. This is the Q7 carry-forward + surface. + +--- + +## CLARIFY complete + +All material ambiguities resolved at full autonomy (3 prior-conversation ++ 7 spec-grounded + 4 grounding-gap → D-232..D-238, confidence ≥ 0.80). +Q7 is the only carry-forward (verification-gate dependency, not a +blocking ambiguity). No human escalation triggered (all confidences ≥ +0.60 threshold). REQUIREMENTS.md updated with the decision ledger + +invariants + NFR constraints. Next: RESEARCH. \ No newline at end of file diff --git a/.ciagent/GRILL.md b/.ciagent/GRILL.md index e92669d..059f929 100644 --- a/.ciagent/GRILL.md +++ b/.ciagent/GRILL.md @@ -107,4 +107,253 @@ required (full autonomy). The plan proceeds with the 3 critical fixes and 16 tracked conditions applied to PLAN.md + REQUIREMENTS.md. The binding decisions above are -the authoritative grill record. Next: MVP/UX CHECK → SHIP phase 0. \ No newline at end of file +the authoritative grill record. Next: MVP/UX CHECK → SHIP phase 0. + +--- + +# GRILL — v1.29 Reposplit + Identity Layer Bring-Live + +> Adversarial red-team review of the v1.29 SPECIFY + CLARIFY + +> RESEARCH + PLAN. Griller: CIAgent griller (red-team persona). +> Autonomy: full. All 9 review axes grilled; every claim verified +> against the live codebase (`publish.yml`, `kj-version.txt`, +> `nova/idp/setup.py`, existing v1.28 test files). +> Date: 2026-08-20. + +--- + +## Overall verdict: **PROCEED-WITH-CONDITIONS** · Confidence 0.72 + +The plan is architecturally sound and the in-acdl scope is well-bounded. +The scope split (Terraform out-of-band in `nova-platform-ops`, acdl +authors publish/scrub/archive/guide/consumer-bump) is the correct +boundary per Vision §4. The technical depth is accurate (D-239 ECR tag +correction, D-240 Terraform precondition floor, CloudFront OAC pitfall, +ECR tag mutability → pin-by-digest). The cost envelope is realistic. + +**However**, the covered-reference pattern — as currently structured — +is a **deferred-trust assertion** for 14 of 17 requirements. The plan +ships REQ-355..366 + 371 as "complete" on the strength of a markdown +pointer (the operator guide's cutover-gate section) to CI in a repo +that does not yet exist and has no CIAgent presence. The M1.5 +verification gate, the one surface acdl genuinely owns, can be +authored-but-never-run-green and the milestone still ships. Four +critical fixes convert "documented" into "evidenced-by-operator- +attestation-in-the-guide-which-acdl-audits-at-P6." + +**4 critical fixes (must apply before EXECUTE) + 6 tracked conditions.** +No escalations (all axes resolved at confidence ≥ 0.60; the user +confirmed the binding verdict on the covered-reference pattern). + +--- + +## Axis verdicts + +| Axis | Verdict | Confidence | Forcing finding | +|------|---------|-----------|----------------| +| §1 Feasibility | PROCEED-WITH-CONDITIONS | 0.70 | KJ-SOURCE: `kj` v0.0.3 source repo unverified by RESEARCH (CF-1) | +| §2 Scope | PROCEED-WITH-CONDITIONS | 0.74 | Covered-reference = deferred-trust for 14/17 REQs (G-1 + CF-2) | +| §3 Cost | PROCEED | 0.82 | $30-40/month realistic at pilot volume; no hidden budget shock | +| §4 Requirements coverage | PROCEED-WITH-CONDITIONS | 0.76 | All REQs mapped; covered-reference verification surface weak (CF-2) | +| §5 Technical risks | PROCEED-WITH-CONDITIONS | 0.72 | KJ-STATIC mitigation sound; KJ-LOCKSTEP by-construction good; M1.5 gate not enforced (CF-1) | +| §6 Testability | REJECT-AS-WRITTEN → PROCEED-WITH-CONDITIONS | 0.66 | "Verified via cutover gates in operator guide" is a punt absent CF-1/CF-2/CF-3/CF-4 | +| §7 Security | PROCEED-WITH-CONDITIONS | 0.68 | INV-18 (AuthType=AWS_IAM), TFM-HITL, IAM-NARROW unverifiable from acdl (CF-2) | +| §8 Timeline/sequencing | PROCEED | 0.80 | P1→P2 ordering safe (acdl-local scrub); P5 smoke hedges (CF-3) | +| §9 Adversarial | PROCEED-WITH-CONDITIONS | 0.70 | Dominant silent-failure = M1.5 never runs green (CF-1 addresses) | + +--- + +## Critical fixes (must apply before EXECUTE) + +### 🔴 CF-1 — M1.5 green is a HARD P6 milestone-ship gate; spike extended + +**Finding:** P1 authors the M1.5 gate tests (Wave 3) but P1's exit +criterion explicitly marks the live KMS round-trip as "covered- +reference, runs in nova-platform-ops CI." P6 ships the milestone with +no requirement that M1.5 ever ran green. The dominant silent-failure +path (user-confirmed): M1.5 never runs green → 14 REQs ship "complete" +on paper while Nova-idp is not live. + +**Fix (binding):** +1. P6 Wave 2 (`ciagent-ship`) MUST NOT ship `v1.28.6` until the operator + guide (`docs/operator-guide-platform-ops.md`) contains an + operator-attested "M1.5 Verification Gate Result" row recording: + (a) the 8-item spike all-green on **3 consecutive rebuilds** in + `nova-platform-ops` CI; (b) the rebuild run IDs / commit SHAs; (c) + the operator attestor identity. The P6 audit step (Wave 1) verifies + this row exists + is non-empty. Absent the row → P6 blocks → escalate. +2. The M1.5 8-item spike (PLAN Happy Path §3.3 Edge 5) is EXTENDED from + 8 to **12 items** by adding: + - **Item 9 (JWKS-EDGE-ONLY):** direct JWKS Function URL GET (bypassing + CloudFront) returns **403**; via-CloudFront GET returns 200. Proves + `AuthType: AWS_IAM` + OAC pinning (INV-18). Without this, the + `AuthType: NONE` pitfall (RESEARCH §4) is undetected. + - **Item 10 (IAM-NARROW):** `aws iam get-role-policy` on the OIDC + role asserts no `Action: "*"` and no `Resource: "*"` (REQ-360). + - **Item 11 (TFM-HITL):** a `terraform apply` `workflow_dispatch` + triggered by the PR author is **rejected** (exit non-zero, + `gitea.triggering_actor == PR author`); a dispatch by a distinct + user proceeds (REQ-357, RESEARCH §10). + - **Item 12 (rollback drill):** revert `nova_platform_version` pin → + `terraform apply` → assert the prior ECR digest runs (proves D-236 + rollback; guards against ECR tag mutability, RESEARCH §2). + +**Binding decision G-2.1:** the covered-reference pattern is accepted +as a verification surface **only** with CF-1 applied. M1.5 green +(evidenced by operator attestation in the guide) is the ship gate. + +### 🔴 CF-2 — Covered-reference REQs gated by operator-attested evidence rows + +**Finding:** 14 of 17 REQs (355..366, 371) are "verified via cutover +gates in the operator guide" (CLARIFY G4). This is a deferred-trust +assertion: if `nova-platform-ops` is never built, or builds the wrong +thing, or its CI silently passes, the REQs ship "complete" on the +strength of a markdown pointer. The user confirmed this is a +deferred-trust assertion, not a verification. + +**Fix (binding):** The operator guide (P4 Wave 1 Task 1.1) "Cutover +Gates" section MUST list each covered-reference REQ with: +(a) the gate entry (M1/M1.5/M2); (b) the verification command; (c) a +placeholder "Result" column. The P6 audit step (Wave 1) verifies that +every covered-reference REQ has a non-empty, green "Result" entry +(operator-attested). A REQ with an empty or red Result → P6 blocks. +This converts "documented" to "evidenced-by-operator-attestation- +audited-by-acdl-at-P6." + +**Binding decision G-1:** the covered-reference pattern is **accepted +as a verification surface** with CF-1 + CF-2 applied. Without them, it +is a punt and the grill would REJECT. + +### 🔴 CF-3 — P5 smoke test must run against a real v1.29.x tag (no hedge) + +**Finding:** P5 bumps the consumer deploy.yml `@v1.25` → `@v1.29` and +runs a smoke test "against the v1.29 publish artifacts." But +`publish.yml` triggers on `v1.29.*` tags (P1 Wave 0), and the milestone +release tag is `v1.28.6`. P5 Wave 1 Task 1.2 hedges: "If the v1.29 +publish artifacts are not yet available... mark as covered-reference: +requires v1.29.0 tag." This hedge lets P5 ship green without the +smoke test ever running against real artifacts — a second silent- +failure path. + +**Fix (binding):** +1. P1 Wave 4 (regression + ship) MUST push a `v1.29.0` tag (or the + first `v1.29.x` tag) as part of P1 ship, triggering `publish.yml` + and producing the v1.29 artifacts. Document this in PLAN P1. +2. P5 Wave 1 Task 1.2's hedge clause is REMOVED. The P5 smoke test + MUST run against the published v1.29.x artifacts. If the artifacts + are absent (P1 failed to publish), P5 fails closed — no hedge to + "covered-reference." +3. The milestone release tag remains `v1.28.6` (the v1.28.x line per + the tagging convention); the `v1.29.0` artifact tag is a P1 + intermediate tag, not the release. This resolves the tag-semantics + ambiguity the grill surfaced. + +### 🔴 CF-4 — kj v0.0.3 source-fetch path confirmed before P1 Wave 1 + +**Finding:** P1 Wave 1 Task 1.1b says "fetches the `kj` Go source at +the pinned SHA" citing "RESEARCH §7 — source repo confirmed in P1 +RESEARCH." RESEARCH §7 confirms the build command (`CGO_ENABLED=0`) +but is **silent on the source repository**. Assumption ledger item #1 +says "RESEARCH will confirm the source repository + build commands" +— RESEARCH did NOT confirm the source repo. `kj-version.txt` pins +`v0.0.3` + SHA `4ebb9a19...` but the grill cannot determine whether +this is a source commit SHA or a binary digest, or what repo it lives +in. P1 Wave 1 is built on an open assumption. + +**Fix (binding):** Before P1 Wave 1 starts (P1 Wave 0 or a new Wave +0.5), the backend-engineer MUST confirm: (a) the `kj` source repo URL ++ the commit at SHA `4ebb9a19...`; (b) `go build` reproduces a binary +whose SHA-256 matches the recorded one (or the SHA is a source commit, +in which case the build is the verification); (c) the fetched source +compiles `CGO_ENABLED=0` to a statically-linked binary (KJ-STATIC). If +the source is not fetchable at the pinned SHA → P1 fails closed → +escalate (this is a spec dependency, not a CIAgent ambiguity per +assumption #1). Document the confirmed repo URL + commit in +`platform/abac/kj-version.txt` (add a third line: the source repo URL). + +--- + +## Tracked conditions (apply during execution) + +- **TC-1 (KJ-STATIC audit, P1 Wave 1 Task 1.2):** `file(1)` asserts + `statically linked` + `readelf -d` asserts no `NEEDED` entries, as a + CI gate. Already in PLAN; tracked for enforcement. +- **TC-2 (KJ-LOCKSTEP by construction, covered-reference):** both + image-bearing resources reference a single `data.aws_ecr_image.kj_image`; + `image_uri = repo@digest`. Verified via CF-1 item 12 (rollback drill) + + CF-2 (operator-attested result row for REQ-371). +- **TC-3 (CloudFront OAC pitfall, P4 operator guide):** the guide MUST + document the `AuthType: NONE` → OAC-ignored pitfall (RESEARCH §4) as + a callout. CF-1 item 9 mechanically verifies it. Already in PLAN P4 + Wave 0 Task 0.3b; tracked. +- **TC-4 (ECR tag format, P1 Wave 1 Task 1.1f):** assert tag matches + `^[a-zA-Z0-9._-]+$` before push (D-239). Already in PLAN; tracked. +- **TC-5 (import idempotency, covered-reference REQ-361):** CI import + treats "Resource already managed by Terraform" as idempotent success + (grep the message, not just exit code). Documented in RESEARCH §1; + tracked for the ops repo (operator-attested via CF-2). +- **TC-6 (Fargate sunset discipline, P4 operator guide):** D-237 — + ≥30 consecutive days green + architecture review before deletion. + Already in PLAN P4 Wave 0 Task 0.3f; tracked. + +--- + +## Binding decisions (this grill session) + +| ID | Decision | Rationale | Confidence | +|----|----------|-----------|-----------| +| **G-1** | The covered-reference pattern is accepted as a verification surface, but ONLY with CF-1 (M1.5 green = hard P6 gate + spike extended to 12 items) + CF-2 (operator-attested result rows for every covered-reference REQ, audited at P6). Without these, it is a deferred-trust assertion (punt) and the grill would REJECT. | User-confirmed: covered-reference is a deferred-trust assertion; M1.5 must be a hard gate; TFM-HITL/IAM-NARROW/JWKS-EDGE-ONLY are unverifiable from acdl absent the extended spike. | 0.78 | +| **G-2.1** | M1.5 green (3 consecutive rebuilds of the 12-item spike) is a binding P6 milestone-ship gate, evidenced by an operator-attested row in the operator guide. The P6 audit verifies the row exists + is green. | Dominant silent-failure path = M1.5 never runs green → 14 REQs false-"complete." User-confirmed. | 0.85 | +| **G-2.2** | The M1.5 spike is extended 8 → 12 items, adding: JWKS-EDGE-ONLY direct-URL-403 check, IAM-NARROW no-wildcard assertion, TFM-HITL self-approval-rejection check, rollback drill. | INV-18, REQ-360, REQ-357 are otherwise unverifiable from acdl. Rollback is untested (D-236). | 0.80 | +| **G-3** | P1 MUST push a `v1.29.0` (or first `v1.29.x`) intermediate tag at P1 ship to produce publish artifacts; P5's "covered-reference: requires v1.29.0 tag" hedge is REMOVED; the smoke test must run against real artifacts or P5 fails closed. | P5's hedge is a second silent-failure path. User-confirmed. | 0.82 | +| **G-4** | The `kj` v0.0.3 source-fetch path (repo URL + commit at SHA `4ebb9a19...`) must be confirmed before P1 Wave 1; the confirmed repo URL is recorded as a third line in `platform/abac/kj-version.txt`. If unfetchable → P1 fails closed → escalate. | RESEARCH §7 is silent on the source repo; P1 Wave 1 is built on an open assumption. User-confirmed. | 0.80 | +| **G-5** | The covered-reference REQs (355..366, 371) are NOT marked "complete" at P6 unless their operator-guide cutover-gate row is non-empty + green (CF-2). An empty/red row blocks the milestone ship. | Converts "documented" → "evidenced-by-operator-attestation-audited-by-acdl." | 0.78 | + +--- + +## Escalations + +None. All 9 axes resolved at confidence ≥ 0.66. The user confirmed the +binding verdict (G-1: accepted with 4 conditions). No human escalation +required (full autonomy). The kj source-fetch (CF-4) has a fail-closed +path: if RESEARCH's open assumption is wrong, P1 fails closed and +escalates at that point — but the grill does not pre-escalate a +spec dependency the plan already flags. + +--- + +## Evidence verified against the live codebase + +- `.github/workflows/publish.yml` line 47-55: trigger is + `push: branches: [main]` (P1 Wave 0 changes to `tags: ['v1.29.*']` — + matches PLAN). +- `.gitea/workflows/publish.yml` exists (P2 removes it — matches PLAN). +- `platform/abac/kj-version.txt`: 2 lines (`v0.0.3` + SHA + `4ebb9a19...`) — matches PLAN; RESEARCH §7 silent on source repo + (CF-4). +- `nova/idp/setup.py`: 50 lines, `--check/--apply/--verify/--dry-run` + (P3 adds terraform delegation — matches PLAN). +- `core/lambda/nova_idp_setup.py` exists (P3 archives its CFN — matches). +- `tests/test_idp_auth.py` + `tests/test_kms_roundtrip.py` EXIST (from + v1.28); `tests/test_abac_e2e.py` does NOT exist (P1 Wave 3 authors it + — matches PLAN). +- `pyproject.toml` version = `1.14.0` (P2 bumps to `1.29.0` — matches + PLAN; note: v1.28 did not bump it, a v1.28 carry-over the grill + flags as minor but does not block on). + +--- + +## Grill complete + +The v1.29 plan proceeds with **4 critical fixes** (CF-1 M1.5 hard gate ++ spike extension; CF-2 operator-attested result rows; CF-3 P5 live +smoke no-hedge; CF-4 kj source confirmation) and **6 tracked +conditions**. The covered-reference pattern is accepted as a +verification surface **only** because CF-1 + CF-2 convert +"documented" into "evidenced-by-operator-attestation-audited-by-acdl- +at-P6." Without those fixes, the grill would REJECT: 14 of 17 REQs +would ship "complete" on the strength of a markdown pointer to a +nonexistent repo's CI. + +Next: apply the 4 critical fixes to PLAN.md + REQUIREMENTS.md, then +MVP/UX CHECK → SHIP phase 0. \ No newline at end of file diff --git a/.ciagent/PERSONAS.md b/.ciagent/PERSONAS.md index eb61501..eb1f6a0 100644 --- a/.ciagent/PERSONAS.md +++ b/.ciagent/PERSONAS.md @@ -116,4 +116,152 @@ reason: "No data pipelines / metrics / PowerBI work in v1.28. The metrics layer None. All four active personas span the full milestone. The security-engineer is heaviest in P2 (identity layer) + P3 (threat model); the cli-engineer is heaviest in P1 (CLI substrate); the backend-engineer -spans P1 (CodeArtifact/layer) + P2 (Lambdas/DynamoDB). \ No newline at end of file +spans P1 (CodeArtifact/layer) + P2 (Lambdas/DynamoDB). + +--- + +# Personas — v1.29 Reposplit + Identity Layer Bring-Live + +```yaml +project: acdl +milestone: v1.29 +generated_at: 2026-08-20 +generator: lead-developer +verification_toolchain: + typecheck: "python3 -m py_compile nova/idp/setup.py core/lambda/nova_idp_setup.py 2>&1 | head -5 || true" + test: "pytest tests/test_idp_auth.py tests/test_kms_roundtrip.py -q 2>&1 | tail -15 || true" + lint: "ruff check nova/idp/ core/lambda/nova_idp_setup.py 2>/dev/null || true" + note: | + v1.29 is a feature milestone (Reposplit + Identity Layer Bring-Live). + Pure ops/devops focus — Terraform modules are authored out-of-band in + nova-platform-ops; CIAgent in acdel delivers publish.yml, Gitea scrub, + CFN archive + CLI terraform-delegation, operator guide, consumer bump. + Five active personas: backend-engineer (publish.yml ECR image, Lambda + zip, GitHub Releases), security-engineer (kj static build verification, + KMS round-trip tests, ABAC E2E, M1.5 gate), cli-engineer (nova idp + setup --apply terraform delegation, CFN archive), data-engineer + (DynamoDB import references, outbox bootstrap docs), lead-developer + (plan/review/ship, Gitea scrub, decisions, operator guide, milestone + wiring). frontend-engineer deactivated (no UI). +``` + +## Roster + +### lead-developer +```yaml +active: true +domain: "Milestone plan, persona roster, Gitea scrub (REQ-367), decisions D-232..240 (REQ-368), operator guide (P4), milestone ship, STATE/ROADMAP/PROJECT wiring, covered-reference REQ tracking" +frameworks: ["git", "Gitea Actions", "GitHub Actions", "semver tagging", ".ciagent/ discipline", "Terraform (reference only)"] +constraints: ["D-232 (forge parity abandoned)", "D-235 (tag-pin handoff)", "D-236 (cutover shape)", "D-238 (KJ-LOCKSTEP)", "OPER-PRIV", "TFM-HITL", "v1.29 hard constraints"] +territory: + - ".ciagent/**" + - "PLAN.md" + - "CHECKPOINT.json" + - "STATE.md" + - "REQUIREMENTS.md" + - "ROADMAP.md" + - "PROJECT.md" + - "CLARIFY.md" + - "RESEARCH.md" + - "docs/operator-guide-platform-ops.md" + - ".github/workflows/ci.yml" + - "scripts/sync_workflows.py" + - "pyproject.toml" + - "README.md" +``` + +### backend-engineer +```yaml +active: true +domain: "publish.yml ECR container image build (CGO_ENABLED=0 static kj), Lambda zip + layer wheel + Python wheel attach to GitHub Releases, ECR push with tag v1.29.x-kj-, kj-version.txt read, Dockerfile for lambda:3.12-al2023 base" +frameworks: ["Python 3.12", "GitHub Actions", "Docker", "ECR", "Go (CGO_ENABLED=0 build)", "file(1)", "sha256sum"] +constraints: ["KJ-STATIC", "D-239 (ECR tag format)", "D-235 (tag-pin handoff)", "REQ-354 criteria 1-4"] +territory: + - ".github/workflows/publish.yml" + - "platform/abac/kj-version.txt" + - "core/lambda/nova_idp_token_vend.py" + - "core/lambda/nova_idp_auth.py" + - "core/lambda/nova_idp_jwks.py" + - "tests/test_idp_auth.py" + - "tests/test_kms_roundtrip.py" +``` + +### security-engineer +```yaml +active: true +domain: "kj static-link audit (file(1) asserts statically linked + no shared library), KMS round-trip test against alias/nova-oidc-signing, ABAC E2E (sign-up→sign-in→token-vend→verify, INV-17 fail-closed), M1.5 verification gate tests (8-item spike), KJ-LOCKSTEP digest-equality verification" +frameworks: ["KMS Sign/Verify/GetPublicKey", "kyverno-json", "jose", "file(1)", "readelf", "pytest", "moto[dynamodb]"] +constraints: ["KJ-STATIC", "KJ-LOCKSTEP", "INV-17 (ABAC fail-closed)", "INV-18 (JWKS-EDGE-ONLY)", "ABAC-FAIL-CLOSED", "ARGON", "KF (KMS asymmetric)"] +territory: + - "platform/abac/**" + - "platform/abac/kj-version.txt" + - "adapters/kyverno-json/policies/token-vend.policy" + - "tests/test_kms_roundtrip.py" + - "tests/test_idp_auth.py" + - "tests/test_abac_e2e.py" + - "docs/threat-model.md" +``` + +### cli-engineer +```yaml +active: true +domain: "nova idp setup --apply terraform delegation (REQ-369 AC 2), CFN archive to docs/archive/nova-idp-cfn-v1.28.md (REQ-369 AC 3), which terraform detection + CFN fallback deprecation warning" +frameworks: ["Python 3.12", "argparse", "subprocess", "importlib", "shutil.which"] +constraints: ["REQ-369", "D-235 (tag-pin handoff)"] +territory: + - "nova/idp/setup.py" + - "core/lambda/nova_idp_setup.py" + - "docs/archive/nova-idp-cfn-v1.28.md" + - "nova/idp/__init__.py" +``` + +### data-engineer +```yaml +active: true +phase_specific: false +domain: "DynamoDB table import references (nova-contracts, nova-change-requests, nova-outbox, nova-users, nova-sessions, nova-pats) documented in operator guide, PITR restore procedure, audit outbox bootstrap" +frameworks: ["DynamoDB", "AWS CLI (reference)"] +constraints: ["REQ-361 (import idempotency, covered-reference)", "JWKS-ROTATION"] +territory: + - "docs/operator-guide-platform-ops.md" + - ".ciagent/ARCHITECTURE.md" +reason: | + Re-activated for v1.29: the operator guide (P4) documents DynamoDB PITR + restore, table imports, and the audit outbox bootstrap — data-engineer + owns the data-layer sections of the guide. The Terraform import itself + is out-of-band (nova-platform-ops), but the operator-facing docs are + in-acdl. +``` + +### frontend-engineer +```yaml +active: false +phase_specific: false +reason: "No UI in v1.29 (pure ops/devops focus). JWKS serves application/json via CloudFront; no HTML/CSS/JS surface." +``` + +## Territory overlap notes + +- `.github/workflows/publish.yml` (REQ-354) = backend-engineer (ECR + image build, Dockerfile, Lambda zip) + lead-developer (Gitea scrub + removes the `.gitea/workflows/publish.yml` mirror in P2, D-232). +- `nova/idp/setup.py` (REQ-369) = cli-engineer (the `--apply` delegation + + `which terraform` detection) + backend-engineer (the CFN archive + content — the CFN template is backend-engineer territory from v1.28). +- `platform/abac/kj-version.txt` = security-engineer (KJ-STATIC audit + reads + verifies the SHA) + backend-engineer (publish.yml reads the + SHA to embed in the ECR tag). +- `docs/operator-guide-platform-ops.md` (P4) = lead-developer (cutover + gates, cost section, artifact-mirror fallback) + data-engineer (PITR + restore, DynamoDB imports) + security-engineer (KMS rotation, JWKS + reachability, PAT revocation). + +## Phase-specific personas + +None. All five active personas span the full milestone. The +backend-engineer is heaviest in P1 (publish pipeline); the +lead-developer is heaviest in P2 (Gitea scrub + decisions) + P4 +(operator guide) + P6 (final ship); the cli-engineer is heaviest in P3 +(CFN archive + TF delegation); the security-engineer is heaviest in P1 +(M1.5 gate tests) + P4 (operator guide security sections); the +data-engineer is heaviest in P4 (operator guide data sections). \ No newline at end of file diff --git a/.ciagent/PLAN.md b/.ciagent/PLAN.md index a513095..19eb629 100644 --- a/.ciagent/PLAN.md +++ b/.ciagent/PLAN.md @@ -486,4 +486,574 @@ Optional CloudFront + WAF + ACM (if `--public-jwks-domain`): +~$3/month at pilot volume. ACM is free for CloudFront-attached certs. This is a pilot-scale cost envelope. Production scale (100x volume) -would still be <$50/month. No hidden costs identified. \ No newline at end of file +would still be <$50/month. No hidden costs identified. + +--- + +# PLAN — v1.29 Reposplit + Identity Layer Bring-Live + +> **Milestone:** v1.29 (feature — reposplit + identity layer bring-live). +> Tags on the **v1.28.x** line: `v1.28.0` (P0) → `v1.28.1..v1.28.5` +> (P1..P5) → `v1.28.6` (P6 final = milestone release). The final phase's +> patch IS the milestone release. +> **Branch:** `milestone/v1.29-reposplit-identity`. Phase branches: +> `phase/00-pre-execution` (complete), `phase/01-publish-pipeline`, +> `phase/02-gitea-scrub-decisions`, `phase/03-cfn-archive-tf-delegation`, +> `phase/04-operator-guide-reference-tracking`, +> `nova-blockchain-exchange/phase/05-consumer-deploy-bump` (cross-project), +> `phase/06-final-review-ship`. +> +> **Scope split (CLARIFY-grounded):** Terraform modules authored +> out-of-band in `nova-platform-ops`. CIAgent in `acdl` authors only the +> acdl-side REQs (354, 367, 368, 369, REQ-OPS-GUIDE, REQ-CONSUMER-BUMP). +> Covered-reference REQs (355-366, 371) verified via cutover gates +> documented in the operator guide (P4). + +## Milestone goal + +v1.29 makes platform operations a Terraform-controlled discipline that +lives outside the engineering repo, with a narrow-IAM `kj` substrate +shared by the primary runtime and its defensive fallback. `acdl/acdl` +standardizes on GitHub (Gitea scrub); Nova-idp is brought live in +account `581513795199` (code complete since v1.28, unverified in-account +at Phase 0); `kj` has exactly one identity (one ECR image digest) shared +by both substrates (KJ-LOCKSTEP, REQ-371). + +## Requirements + +17 requirements: REQ-354..REQ-369 + REQ-371 + REQ-363b + REQ-OPS-GUIDE ++ REQ-CONSUMER-BUMP (full text in `.ciagent/REQUIREMENTS.md` §v1.29). +1 invariant: INV-18 (JWKS-EDGE-ONLY). 10 NFR constraints: KJ-STATIC, +KJ-LOCKSTEP, KJ-WARMUP-HEALTH, OPER-PRIV, IAM-NARROW, DRIFT-DETECT, +IMPORT-IDEMPOTENT, TFM-HITL, JWKS-SLO, JWKS-ROTATION. 9 decisions: +D-232..D-238 (CLARIFY) + D-239/D-240 (RESEARCH spec corrections). + +## Phase breakdown + +### Phase P1 — publish-pipeline (REQ-354) + +**Goal:** `publish.yml` attaches Lambda zip + layer wheel + Python wheel ++ ECR container image (static `kj`, `CGO_ENABLED=0`, tag +`v1.29.x-kj-`) to GitHub Release for each tag, with matching +SHA-256 in the body. The M1.5 verification gate tests +(`test_idp_auth`, `test_kms_roundtrip`, ABAC E2E) are authored. + +**Exit criterion:** REQ-354 criteria 1-4 pass; KJ-STATIC audit (file(1) +asserts `statically linked`) runs in CI; ECR image pushed with tag +`v1.29.x-kj-` (D-239); GitHub Release body lists image URI + digest +alongside wheel + layer + Lambda zip; M1.5 gate tests exist + pass in +moto-DDB (live KMS round-trip is covered-reference, runs in +nova-platform-ops CI). + +**Branch:** `phase/01-publish-pipeline`. **Tag:** `v1.28.1`. + +#### Wave 0 — publish.yml trigger model (backend-engineer) +- **Task 0.1** (backend-engineer): change `.github/workflows/publish.yml` + trigger from `push: branches: [main]` to `push: tags: ['v1.29.*']`. + Preserve the existing wheel + Lambda layer publish steps (REQ-323/ + CAP-035). Add the Lambda zip packaging step + (`nova-lambda-token-vend-v1.29.x.zip`). Verify the trigger fires on + `git tag v1.29.0 && git push --tags`. + +#### Wave 1 — kj source confirmation + static build + ECR image (backend-engineer, security-engineer) +- **Task 1.0** (backend-engineer): **kj source-fetch confirmation + (grill CF-4/G-4 — binary go/no-go gate before Wave 1).** Confirm the + `kj` Go source repo URL + commit at SHA `4ebb9a19...` (read from + `platform/abac/kj-version.txt`). Record the repo URL as a 3rd line + in `platform/abac/kj-version.txt`. If unfetchable → P1 fails closed + → escalate (this is a spec dependency, not a CIAgent ambiguity). The + source repo is the `kyverno-json/kj` Go binary project (distinct + from the kyverno-json Python engine adapter in `adapters/kyverno- + json/`). +- **Task 1.1** (backend-engineer): add a `build-kj-image` job to + `publish.yml` that: + (a) reads `platform/abac/kj-version.txt` (v0.0.3 + SHA + `4ebb9a19...`); + (b) fetches the `kj` Go source at the pinned SHA (RESEARCH §7 — + source repo confirmed in P1 RESEARCH); + (c) builds with `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build + -ldflags="-s -w" -o kj ./…`; + (d) runs `file kj` and asserts output contains `statically linked` + AND does NOT contain `shared library` (KJ-STATIC — fail build + otherwise); + (e) builds the container image from + `public.ecr.aws/lambda/python:3.12-al2023`, copying `kj` to + `/opt/kj/kj` with `chmod 0555` owned by `sbx_user:1051`; + (f) pushes the image to ECR with tag `v1.29.x-kj-` + (D-239 — assert tag matches `^[a-zA-Z0-9._-]+$` before push); + (g) records the image URI + digest for the GitHub Release body. +- **Task 1.2** (security-engineer): add a KJ-STATIC audit step that + runs `file(1)` + `readelf -d kj` (assert no `NEEDED` entries) as a + CI gate. If either fails, the publish job fails closed. This is the + mechanical enforcement of KJ-STATIC (not just a human review). + +#### Wave 2 — GitHub Release body + SHA-256 (backend-engineer) +- **Task 2.1** (backend-engineer): extend the `publish.yml` release step + to attach: (a) `nova-lambda-token-vend-v1.29.x.zip`; (b) + `nova-cli-layer-v1.29.x.zip`; (c) `nova-1.29.x-py3-none-any.whl`; (d) + the ECR image URI + digest. Compute SHA-256 for each artifact + list + in the release body. Verify REQ-354 criteria 1, 2, 4 (artifacts + appear, independent per tag, image URI + digest listed). + +#### Wave 3 — M1.5 verification gate tests (security-engineer) +- **Task 3.1** (security-engineer): author `tests/test_idp_auth.py` — + sign-up → sign-in → session flow against moto-DDB (covers Edge 5 + item 5). Skip live-KMS assertions (covered-reference — runs in + nova-platform-ops CI). +- **Task 3.2** (security-engineer): author + `tests/test_kms_roundtrip.py` — sign/verify round-trip against + `alias/nova-oidc-signing`. Mark as `@pytest.mark.live_aws` (skipped in + acdl CI; runs in nova-platform-ops CI against the live key, REQ-362). +- **Task 3.3** (security-engineer): author + `tests/test_abac_e2e.py` — known PAT → ABAC-allowed action → signed + OIDC token → `jose` verification → green; known PAT + ABAC-denied + action → 403 with deny reason logged (INV-17 fail-closed, Edge 5 + item 7). Uses moto-DDB + mock KMS. + +#### Wave 4 — regression + ship (lead-developer) +- **Task 4.1** (lead-developer): run full test suite; verify 1000+ + tests still pass (no regressions from publish.yml changes). Verify + CAP-001..038 regression gate green. **Push a `v1.29.0` intermediate + tag at P1 ship** (grill CF-3/G-3) to trigger `publish.yml` + produce + the v1.29 artifacts (Lambda zip + layer wheel + Python wheel + ECR + image). The milestone release tag remains `v1.28.6`; the `v1.29.0` + artifact tag is a P1 intermediate to produce publish artifacts for + P5's smoke test. Ship P1 → `v1.28.1`. + +### Phase P2 — gitea-scrub-decisions (REQ-367, REQ-368) + +**Goal:** Hard scrub of all Gitea references in `acdl/acdl`; `.gitea/` +removed; `forge_parity_disabled` CI assertion; pyproject → 1.29.0; +decisions D-232..238 recorded in PROJECT.md + CLARIFY (already done in +P0; this phase adds the CI assertion + the actual file scrub). + +**Exit criterion:** `grep -rni gitea .github/ docs/ pyproject.toml +README.md .ciagent/` returns zero matches outside the spec archive +section; `find .gitea` returns nothing; CI `forge_parity_disabled` +assertion passes; pyproject.toml version = 1.29.0. + +**Branch:** `phase/02-gitea-scrub-decisions`. **Tag:** `v1.28.2`. + +#### Wave 0 — pyproject bump (lead-developer) +- **Task 0.1** (lead-developer): bump `pyproject.toml` version → + `1.29.0` (spec §7.2). Verify `nova --version` reports `1.29.0`. + +#### Wave 1 — .gitea/ removal (lead-developer) +- **Task 1.1** (lead-developer): `rm -rf .gitea/` (7 workflow files + + README.md, RESEARCH §9d). Remove `scripts/sync_workflows.py` (the + byte-identical-forges generator — central removal target, D-232). + Remove Gitea references from `scripts/sync_to_nova.sh` (line 201: + `--exclude=/.gitea`) + `scripts/rotate_spike_key.sh` (Gitea API + secret upload). Scrub `terraform/bootstrap/` Gitea OIDC references + (the OIDC role for act_runner moves to nova-platform-ops; the + bootstrap here becomes archived reference). + +#### Wave 2 — Gitea reference scrub (lead-developer) +- **Task 2.1** (lead-developer): `grep -rni gitea .github/ docs/ + pyproject.toml README.md .ciagent/` — scrub all matches outside the + spec archive section (`.ciagent/REQUIREMENTS.md` §v1.29 + CLARIFY §v1.29 + + RESEARCH §v1.29 retain "Gitea" as historical/reference text; these + are the "spec archive section" exemption per REQ-367 AC 1). Update + `.github/workflows/ci.yml` to remove any Gitea-specific steps. + +#### Wave 3 — forge_parity_disabled CI assertion (lead-developer) +- **Task 3.1** (lead-developer): add a CI step to `.github/workflows/ci.yml` + that asserts `forge_parity_disabled` — the step runs + `test ! -d .gitea/` and `! grep -rqi gitea .github/workflows/` and + exits 0 on success, non-zero with `forge_parity_disabled` message on + failure (REQ-367 AC 3, D-232). This is the deliberate CI failure that + documents the abandoned parity. + +#### Wave 4 — decisions verification + ship (lead-developer) +- **Task 4.1** (lead-developer): verify D-232..238 + D-239/240 are + present in PROJECT.md + CLARIFY.md + REQUIREMENTS.md (REQ-368 AC 1-2, + already authored in P0; this task is a verification, not re-authoring). + Run full test suite; ship P2 → `v1.28.2`. + +### Phase P3 — cfn-archive-tf-delegation (REQ-369) + +**Goal:** Archive the CFN template in `nova/idp/setup.py` + +`core/lambda/nova_idp_setup.py` to `docs/archive/nova-idp-cfn-v1.28.md` +(read-only reference); `nova idp setup --apply` delegates to `terraform +apply` (the CLI detects terraform via `which terraform`; if absent, +falls back to the CFN path with a deprecation warning). + +**Exit criterion:** `docs/archive/nova-idp-cfn-v1.28.md` exists + +contains the CFN template as read-only reference; `nova idp setup +--apply` invokes `terraform apply` when terraform is on PATH (tested +with a mock terraform binary); the CFN path emits a deprecation warning +when terraform is absent. + +**Branch:** `phase/03-cfn-archive-tf-delegation`. **Tag:** `v1.28.3`. + +#### Wave 0 — CFN archive (cli-engineer, backend-engineer) +- **Task 0.1** (backend-engineer): extract the CFN template from + `core/lambda/nova_idp_setup.py` + write it to + `docs/archive/nova-idp-cfn-v1.28.md` as a fenced code block with a + read-only header ("Archived at v1.29.0 — the active path is + `terraform apply` in `nova-platform-ops`. Deletion is a follow-up + after Terraform parity is verified."). +- **Task 0.2** (cli-engineer): mark the CFN generation code path in + `core/lambda/nova_idp_setup.py` as deprecated (add a + `DeprecationWarning` when the CFN path is invoked + a docstring + pointing to the archive + the terraform delegation path). + +#### Wave 1 — terraform delegation (cli-engineer) +- **Task 1.1** (cli-engineer): modify `nova/idp/setup.py` `--apply` to + detect terraform via `shutil.which("terraform")`. If terraform is on + PATH: delegate to `subprocess.run(["terraform", "apply", + "-auto-approve"])` in the `nova-platform-ops` checkout (the operator + runs this from the ops repo root). If terraform is absent: fall back + to the CFN path with a `DeprecationWarning` ("CFN path is archived; + install terraform or use nova-platform-ops. See + docs/archive/nova-idp-cfn-v1.28.md."). +- **Task 1.2** (cli-engineer): add `nova idp setup --verify` delegation + to `terraform plan` (same `which terraform` detection). The verify + path runs `terraform plan` + reports the diff. + +#### Wave 2 — tests (cli-engineer) +- **Task 2.1** (cli-engineer): author + `tests/test_idp_setup_tf_delegation.py` — test the `--apply` path + with a mock terraform binary on PATH (assert `subprocess.run` called + with `["terraform", "apply", "-auto-approve"]`); test the fallback + path with terraform absent (assert `DeprecationWarning` raised + CFN + path invoked); test `--verify` delegates to `terraform plan`. + +#### Wave 3 — ship (lead-developer) +- **Task 3.1** (lead-developer): run full test suite; ship P3 → + `v1.28.3`. + +### Phase P4 — operator-guide-reference-tracking (REQ-OPS-GUIDE) + +**Goal:** `docs/operator-guide-platform-ops.md` covering KMS rotation, +JWKS reachability via CloudFront edge, PITR restore, PAT revocation, +edge configuration, Fargate standby health, cost section, artifact- +mirror fallback, and the M1/M1.5/M2 cutover gates as release-gate +entries for the covered-reference REQs. ARCHITECTURE.md §12.9. STATE.md +v1.29 CAPs + invariants. REQUIREMENTS.md covered-reference markers. + +**Exit criterion:** operator guide exists + covers all sections per +REQ-OPS-GUIDE AC; ARCHITECTURE.md §12.9 added; STATE.md updated with +v1.29 rows; covered-reference REQs in REQUIREMENTS.md marked with their +cutover gate. + +**Branch:** `phase/04-operator-guide-reference-tracking`. **Tag:** +`v1.28.4`. + +#### Wave 0 — operator guide (lead-developer, data-engineer, security-engineer) +- **Task 0.1** (lead-developer): author + `docs/operator-guide-platform-ops.md` sections: (a) Overview + the + reposplit rationale (Vision §4); (b) Day-0 cutover procedure (M1 + steps from spec §3.2 Journey 2); (c) M1.5 verification gate (8-item + spike, 3 consecutive rebuilds); (d) M2 operational handoff loop + (tag-pin bump → plan → HITL approval → apply); (e) M2a Fargate + activation (conditional on M1.5 failure); (f) Rollback procedure + (D-236 — revert `nova_platform_version` pin); (g) cost section (WAF + ~$5-10/month + Fargate ~$15-20/month, REQ-363b AC 4); (h) artifact- + mirror fallback (operator-local mirror by SHA-256 when Gitea + act_runner cannot reach GitHub Releases, Edge 6). +- **Task 0.2** (data-engineer): author the operator guide data + sections: (a) DynamoDB PITR restore procedure (per-table); (b) + DynamoDB import addresses (nova-contracts, nova-change-requests, + nova-outbox, nova-users, nova-sessions, nova-pats — the + `importable-resources.tf` map, REQ-361 covered-reference); (c) audit + outbox bootstrap; (d) JWKS-ROTATION (24-hour overlap window on key + rotation). +- **Task 0.3** (security-engineer): author the operator guide security + sections: (a) KMS rotation (90-day cadence, `alias/nova-oidc- + signing`, `ECC_NIST_P256`, D-234); (b) JWKS reachability via + CloudFront edge (OAC pinning, `AuthType: AWS_IAM`, direct Function + URL → 403, INV-18); (c) PAT revocation (60s SLO, D-229); (d) edge + configuration (CloudFront + WAF + ACM + Route53 — REQ-364/365/366 + covered-reference); (e) Fargate standby health checks (`GET /health` + every 10s, `KJ-WARMUP-HEALTH`, 3 consecutive probe failures → alert + + token-vend fails closed, REQ-363b AC 2); (f) Fargate sunset + discipline (D-237 — ≥30 consecutive days green before deletion + + architecture review); (g) IAM scope (IAM-NARROW, REQ-360 covered- + reference — no `Action: "*"` or `Resource: "*"`); (h) the + `route53_record_not_resolvable` debugging path (ACM cert status + check). + +#### Wave 1 — covered-reference cutover gates (lead-developer) +- **Task 1.1** (lead-developer): add a "Cutover Gates" section to the + operator guide listing each covered-reference REQ (355, 356, 357, + 358, 359, 360, 361, 362, 363, 363b, 364, 365, 366, 371) with its + gate entry (M1/M1.5/M2) + the verification command + a **"Result" + column** (grill CF-2/G-5). P6 audit verifies every covered-reference + REQ has a non-empty, green Result. Empty/red → P6 blocks. The Result + column is populated by the operator attestation (the operator runs + the verification command in `nova-platform-ops` CI + records the + outcome). This is the acdl-side evidence surface for covered- + reference REQs. +- **Task 1.2** (lead-developer): update REQUIREMENTS.md §v1.29 traceability + table — mark each covered-reference REQ with its cutover gate in the + Status column (e.g., `planned (M1 gate: nova-platform-ops)`). + +#### Wave 2 — ARCHITECTURE.md + STATE.md (lead-developer) +- **Task 2.1** (lead-developer): add ARCHITECTURE.md §12.9 (Platform + Ops Reposplit) — the domain boundary (engineering ends at the + compiled artifact; operations begins at the live platform under + guardrails), the `kj` substrate (one ECR image digest, KJ-LOCKSTEP), + the covered-reference REQ tracking pattern, the operator guide + pointer. +- **Task 2.2** (lead-developer): update STATE.md — append v1.29 + capability rows (CAP-039: platform-ops-reposplit, CAP-040: + kj-substrate-lockstep, CAP-041: jwks-edge-only) + bump invariants + (INV-18 JWKS-EDGE-ONLY + the 10 NFR constraints). Bump "Last + milestone ship" to v1.29 (pending). + +#### Wave 3 — ship (lead-developer) +- **Task 3.1** (lead-developer): run full test suite; ship P4 → + `v1.28.4`. + +### Phase P5 — consumer-deploy-bump (REQ-CONSUMER-BUMP, cross-project) + +**Goal:** Bump `nova-blockchain-exchange` deploy.yml `@v1.25` → `@v1.29` +in both `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` ++ smoke test (sign-up → sign-in → token-vend → apply → audit against +v1.29 publish artifacts). + +**Exit criterion:** both deploy.yml files reference `@v1.29`; smoke +test passes (the chain completes against v1.29 publish artifacts). + +**Branch:** `nova-blockchain-exchange/phase/05-consumer-deploy-bump` +(cross-project, multi-project branch naming per branch-strategy.md). +**Tag:** `v1.28.5`. + +#### Wave 0 — deploy.yml bump (lead-developer) +- **Task 0.1** (lead-developer): in the `nova-blockchain-exchange` + project, update `.github/workflows/deploy.yml` + `.gitea/workflows/ + deploy.yml` `uses:` ref from `acdl/.github/workflows/deploy.yml@v1.25` + → `@v1.29` (RESEARCH §9e — the consumer's `.gitea/` is out of scope + for the acdl REQ-367 scrub; the consumer may keep its Gitea mirror or + follow suit — this is a consumer-repo decision, not an acdl one). + +#### Wave 1 — smoke test (lead-developer, security-engineer) +- **Task 1.1** (security-engineer): author + `nova-blockchain-exchange/tests/test_v1.29_smoke.py` — sign-up → + sign-in → token-vend → apply → audit chain against the v1.29 publish + artifacts (the consumer's contract → `deploy.yml@v1.29` mode=full → + apply → attest → record against `581513795199`). Uses the existing + CAP-025 round-trip assertion (v1.26). +- **Task 1.2** (lead-developer): run the smoke test; verify the chain + completes against the real v1.29.0 publish artifacts (produced by + P1's intermediate tag, grill CF-3/G-3). **No hedge** — the smoke + test MUST run against the published v1.29.x artifacts or P5 fails + closed. If the artifacts are not available (P1 did not push the + intermediate tag), P5 blocks until P1 re-ships. + +#### Wave 2 — ship (lead-developer) +- **Task 2.1** (lead-developer): ship P5 → `v1.28.5`. The consumer + project ships independently (merge to the consumer's main, not + acdl's milestone branch). + +### Phase P6 — final-review-ship (Final Phase) + +**Goal:** Multi-persona code review across P1..P5; audit (reconstruction +test, branch hygiene, commit discipline, file discipline); milestone +ship (merge `phase/06` → `milestone/v1.29-reposplit-identity` → `main`; +tag `v1.28.6` = the v1.29 release; Gitea release; delete all milestone +branches); mark all v1.29 REQs complete in REQUIREMENTS.md + ROADMAP.md. + +**Exit criterion:** review P0 issues auto-fixed, P1+ flagged; audit +PASS; milestone merged to main; tag `v1.28.6` created; Gitea release +published; milestone branches deleted; REQUIREMENTS.md + ROADMAP.md +marked complete. + +**Branch:** `phase/06-final-review-ship`. **Tag:** `v1.28.6` = +milestone release. + +#### Wave 0 — review (lead-developer) +- **Task 0.1** (lead-developer): delegate to `ciagent-review` — + multi-persona review (lead-developer, backend-engineer, security- + engineer, data-engineer, cli-engineer) across P1..P5. Auto-apply P0 + fixes; flag P1+ for post-hoc review. If P1+ issues found: fix them + in this phase. + +#### Wave 1 — audit (lead-developer) +- **Task 1.1** (lead-developer): delegate to `ciagent-audit` — + reconstruction test (git log ↔ `.ciagent/`), branch hygiene, commit + discipline, file discipline. If critical issues found: fix them in + this phase. + +#### Wave 2 — milestone ship (lead-developer) +- **Task 2.1** (lead-developer): delegate to `ciagent-ship` — merge + `phase/06` → `milestone/v1.29-reposplit-identity` → `main`; tag + `v1.28.6`; Gitea release with full milestone summary; delete all + milestone branches (phase/00..06 + milestone/v1.29-reposplit- + identity). + +#### Wave 3 — milestone completion (lead-developer) +- **Task 3.1** (lead-developer): update REQUIREMENTS.md (all v1.29 REQs + → complete), ROADMAP.md (v1.29 → complete), NORTH_STAR.md (note + Strategic Objective — platform operations as a Terraform-controlled + discipline), STATE.md (bump "Last milestone ship" to v1.29, tag + `v1.28.6`). Commit `docs(milestone): complete v1.29-reposplit- + identity`. + +--- + +## User-Facing Surface + +1. **CLI flag:** `nova idp setup --apply` now delegates to `terraform + apply` (REQ-369 AC 2) — the operator runs this from the + `nova-platform-ops` checkout. `nova idp setup --verify` delegates to + `terraform plan`. +2. **GitHub Release artifacts page:** each `v1.29.x` tag's GitHub + Release page lists the Lambda zip + layer wheel + Python wheel + ECR + image URI/digest with SHA-256 (REQ-354) — this is the engineering- + to-ops handoff surface (D-235 tag-pin handoff). +3. **Operator guide:** `docs/operator-guide-platform-ops.md` — the + operator-facing runbook covering KMS rotation, JWKS reachability, + PITR restore, PAT revocation, edge config, Fargate standby, cost, + artifact-mirror fallback, and the M1/M1.5/M2 cutover gates. +4. **CI assertion:** `forge_parity_disabled` — the deliberate CI + failure documenting the abandoned byte-identical-forges parity + (D-232, REQ-367 AC 3). + +## Happy Path + +**M1.5 verification gate (spec §3.3 Edge 5, 12-item spike — written +BEFORE execute, extended per grill CF-1):** + +1. `kj` v0.0.3 (pinned SHA in `platform/abac/kj-version.txt`) compiles + with `CGO_ENABLED=0 GOOS=linux GOARCH=amd64`. +2. Resulting binary reports `file kj → ELF 64-bit LSB executable, + x86-64, statically linked, no shared library` (KJ-STATIC). +3. Container image built from + `public.ecr.aws/lambda/python:3.12-al2023` with the binary copied + to `/opt/kj/kj`, `chmod 0555`, owned by `sbx_user:1051`. +4. Lambda runtime `python3.12` executes + `nova_idp_token_vend.handler`; the handler invokes + `subprocess.run(['/opt/kj/kj', 'apply', ...])` and parses stdout + JSON. +5. `tests/test_idp_auth.py` passes against the live image in moto-DDB. +6. `tests/test_kms_roundtrip.py` passes against the live KMS key + (REQ-362 path — covered-reference, runs in nova-platform-ops CI). +7. End-to-end: known PAT → known ABAC-allowed action → signed OIDC + token → `jose` verification → green. Known PAT + ABAC-denied action + → 403 with deny reason logged (INV-17). +8. Image URI is recorded in Terraform state and in the operator guide. +9. **(grill CF-1)** Direct JWKS Function URL → 403 / via-CloudFront → + 200 (INV-18, JWKS-EDGE-ONLY — `AuthType: AWS_IAM` verified, not + prose). +10. **(grill CF-1)** IAM-NARROW: no `Action: "*"` or `Resource: "*"` + in the Gitea OIDC role effective permissions (REQ-360). +11. **(grill CF-1)** TFM-HITL: self-approval rejected — + `gitea.triggering_actor == pull_request.user.login` → apply fails + closed (REQ-357, INV-3). +12. **(grill CF-1)** Rollback drill — revert `nova_platform_version` + pin → prior digest runs (D-236 cutover shape + rollback procedure). + +If items 1-7 fail three consecutive rebuilds, M2a activates REQ-363b +(Fargate toggle) with the same image — no warmup hit because the +standby is always running the same digest. + +**HARD P6 SHIP GATE (grill CF-1/G-2.1):** P6 must not ship `v1.28.6` +until the operator guide contains an operator-attested "M1.5 +Verification Gate Result" row (3 consecutive green rebuilds, run +IDs/SHAs, attestor identity). P6 audit verifies the row exists. The +M1.5 gate is verified in `nova-platform-ops` CI (out-of-band); the +operator attestation in the guide is the acdl-side evidence surface. + +## UX Acceptance Criteria + +1. **M1 acceptance gate (spec §2.3):** `terraform apply` from `main` + brings the live AWS account to a state where Nova-idp identity + tables exist, JWT-issuing paths are wired but not yet consuming + container images, JWKS infrastructure is in place, WAF + OAC pinning + the CloudFront edge; `acdl/acdl v1.29.0` ships with zero `.gitea/` + references and zero platform-infra files; D-232..238 recorded in + PROJECT.md/CLARIFY. +2. **M1.5 acceptance gate:** items 1-12 of the Edge 5 spike all green + on three consecutive rebuilds; image digest resolvable via + `data.aws_ecr_image.kj_image`; sign/verify round-trip passes; ABAC + fail-closed path verified against live policy; JWKS-EDGE-ONLY + verified (item 9); IAM-NARROW verified (item 10); TFM-HITL + self-approval rejected (item 11); rollback drill passes (item 12). + **HARD P6 ship gate** — operator-attested "M1.5 Verification Gate + Result" row in the operator guide (grill CF-1/G-2.1). +3. **M2 acceptance gate:** Bumping `local.nova_platform_version` in a + PR and merging it results in `terraform apply` updating both + `aws_lambda_function.nova_idp_token_vend.image_uri` and + `aws_ecs_task_definition.kj.container_definitions[0].image` to the + same digest (KJ-LOCKSTEP, REQ-371), with zero diff on KMS, DDB, + IAM, edge. + +## Test evidence required for v1.29 release + +- [ ] Code coverage ≥ 80% on new modules (the acdl-side files: + `publish.yml` changes, `nova/idp/setup.py` terraform delegation, + `docs/operator-guide-platform-ops.md` is docs — no coverage + requirement; the M1.5 gate tests). +- [ ] CI/CD pipeline GREEN for `acdl/acdl` (the `nova-platform-ops` + pipeline is out-of-band). +- [ ] M1.5 verification gate green: items 1-12 of §3.3 Edge 5 spike + pass on three consecutive rebuilds (covered-reference — verified + in nova-platform-ops CI; acdl authors the tests in P1; operator + attests in the guide, P4; P6 audit verifies the attestation row, + grill CF-1/G-2.1). +- [ ] Covered-reference REQs (355-366, 371) have non-empty, green + Result in the operator guide "Cutover Gates" section (grill + CF-2/G-5 — P6 audit verifies). +- [ ] `lifecycle.precondition` enforced on both image-bearing resources + (REQ-371 mechanical proof — covered-reference in + nova-platform-ops). +- [ ] Live KMS sign/verify round-trip verified in account + `581513795199` (covered-reference). +- [ ] Live ABAC sign/verify round-trip verified against the production + policy (covered-reference). +- [ ] Pilot consumer (`nova-blockchain-exchange`) smoke test green: + sign-up → sign-in → token-vend → apply → audit chain (P5). +- [ ] All existing capabilities (CAP-001..038) still pass the + regression gate. +- [ ] Drift-detection baseline: `terraform plan` exit 0 against live + AWS state, captured at cutover (covered-reference). +- [ ] `kj` standby Fargate task health `READY` before M1 cutover + (covered-reference, KJ-WARMUP-HEALTH). +- [ ] Fargate standby sunset discipline documented in operator-guide + (D-237, P4). +- [ ] `forge_parity_disabled` CI assertion passes (P2, REQ-367 AC 3). +- [ ] `grep -rni gitea .github/ docs/ pyproject.toml README.md + .ciagent/` returns zero matches outside the spec archive section + (P2, REQ-367 AC 1). + +## Plan completeness checklist + +- [x] Every REQ mapped to a phase + wave + task. +- [x] Covered-reference REQs identified + their verification surface + documented (operator guide P4, cutover gates). +- [x] Decisions D-232..240 referenced in the plan. +- [x] Invariants + NFR constraints referenced (KJ-STATIC, KJ-LOCKSTEP, + INV-18, etc.). +- [x] Personas assigned to every task (lead-developer, backend-engineer, + security-engineer, cli-engineer, data-engineer). +- [x] User-Facing Surface section (3 surfaces named). +- [x] Happy Path section (M1.5 8-item spike, written before execute). +- [x] UX Acceptance Criteria section (M1, M1.5, M2 gates). +- [x] Test evidence checklist. +- [x] Phase boundaries + tags (v1.28.0 → v1.28.6). +- [x] Cross-project phase (P5, nova-blockchain-exchange) identified. + +## Cost envelope (v1.29) + +Monthly estimate for the `nova-platform-ops` live platform (documented +in the operator guide, P4): + +| Resource | Quantity | Est. monthly | +|----------|----------|-------------| +| WAF WebACL (CloudFront-scoped) | 1 | ~$5-10/month (+ per-request) | +| Fargate standby (0.25 vCPU, 512 MB) | 1 task | ~$15-20/month (REQ-363b AC 4) | +| KMS asymmetric key | 1 | ~$1/month | +| DynamoDB (on-demand, 7 tables) | 7 | ~$2/month (pilot volume) | +| DynamoDB PITR | 7 tables | ~$2/month | +| Lambda invocations (3 Lambdas) | 3 | ~$2/month | +| ECR image storage | ~100 MB | <$1/month | +| S3 state bucket + access logs | 1 | <$1/month | +| CloudFront + ACM + Route53 | 1 distribution | ~$1/month (ACM free) | +| **Total** | | **~$30-40/month** | + +This is the pilot-scale ops cost envelope. The Fargate standby +(~$15-20/month) is the largest line item + is explicitly documented in +the operator guide (REQ-363b AC 4) with the D-237 sunset discipline +(≥30 consecutive days green before deletion + architecture review). \ No newline at end of file diff --git a/.ciagent/PROJECT.md b/.ciagent/PROJECT.md index ed35491..0702b02 100644 --- a/.ciagent/PROJECT.md +++ b/.ciagent/PROJECT.md @@ -528,14 +528,139 @@ New requirements REQ-323..REQ-353 — full text in - `nova idp setup --apply` MUST present the CloudFormation template for review before any resource is created (NFR-10). -### v1.28 phase status (active — phase 0 in progress) +### v1.28 phase status (complete — tag `v1.27.6` = the v1.28 release) + +- **P0** pre-execution → `v1.27.0` (complete). +- **P1..P5** execution phases → `v1.27.1..v1.27.5` (complete). +- **P6** final review + audit + milestone ship → `v1.27.6` = the v1.28 + release (complete, merged to main 2026-08-19). + +> Phase-by-phase task breakdown, wave ordering, and persona assignments: +> `.ciagent/PLAN.md` (retained). Authoritative resume state: +> `.ciagent/CHECKPOINT.json`. + +--- + +## v1.29 — Reposplit + Identity Layer Bring-Live (active, milestone branch `milestone/v1.29-reposplit-identity`) + +> **Feature milestone — active.** v1.29 extracts all live platform +> components (Nova-idp Lambdas, KMS keys, DynamoDB tables, S3 state +> buckets, OIDC roles, JWKS, audit outbox bootstrap) from `acdl/acdl` +> into a dedicated Gitea-private Terraform repository +> (`nova-platform-ops`), brings Nova-idp live in account `581513795199` +> for the first time (code complete since v1.28, unverified-in-account at +> Phase 0), and standardizes `acdl/acdl` on GitHub. The split enforces +> Vision §4 domain boundaries architecturally: engineering ends at the +> compiled artifact; operations begins at the live platform under +> guardrails. Vision §5 "Narrow capability interfaces" shapes the +> substrate design — `kj` has exactly one identity (one ECR image +> digest), shared by both the production runtime and its defensive +> fallback, eliminating drift by construction (KJ-LOCKSTEP). + +### Scope split (CLARIFY-grounded, full autonomy) + +The spec creates a **separate** Gitea-private repo `nova-platform-ops`. +CIAgent runs inside `acdl`. The Terraform module code +(`networking`/`kms`/`identity`/`contract-ingest`/`bootstrap`/`edge`) is +authored **out-of-band** in `nova-platform-ops` (operator-owned). CIAgent +in `acdl` delivers only the acdl-side work and tracks the ops-side REQs +as **covered-reference** (verification surface = the M1/M1.5/M2 cutover +gates documented in the operator guide, not a missing test). + +| In-acdl (CIAgent authors) | Covered-reference (nova-platform-ops) | +|---|---| +| REQ-354 (publish.yml + ECR image + Release) | REQ-355, 356, 357, 358 (ops CI/HITL/pin) | +| REQ-367 (Gitea scrub) | REQ-359 (Gitea-private repo) | +| REQ-368 (decisions D-232..238) | REQ-360 (IAM scope bounded) | +| REQ-369 (CFN archive + CLI `--apply` TF delegation) | REQ-361 (import idempotency) | +| Operator guide `docs/operator-guide-platform-ops.md` | REQ-362 (KMS key provisioning) | +| `platform/abac/kj-version.txt` | REQ-363, 363b (Lambda/Fargate substrate) | +| M1.5 verification gate tests | REQ-364, 365, 366 (JWKS/WAF/ACM edge) | +| nova-blockchain-exchange deploy.yml @v1.29 bump | REQ-371 `lifecycle.precondition` (TF-side) | + +### v1.29 ID allocations (no collisions with shipped history) + +- **Requirements:** `REQ-354..REQ-369` + `REQ-371` + `REQ-363b` (note: + REQ-370 is intentionally unused per the source spec). Max existing REQ + = REQ-353. REQ-363b is a sub-requirement of REQ-363 (Fargate defensive + fallback, same ECR image — KJ-LOCKSTEP). +- **Decisions:** `D-232..D-238` (7 decisions, authored in CLARIFY) + + `D-239..D-240` (2 research-derived spec corrections). Max existing D + = D-231. +- **Invariants:** `INV-18` (JWKS-EDGE-ONLY — proposed in spec §5, promoted + here). Plus non-invariant NFRs carried as constraints: KJ-STATIC, + KJ-LOCKSTEP, KJ-WARMUP-HEALTH, OPER-PRIV, IAM-NARROW, DRIFT-DETECT, + IMPORT-IDEMPOTENT, TFM-HITL, JWKS-SLO, JWKS-ROTATION. Max existing INV + = INV-17. +- **`kj` here is the Go binary** (`platform/abac/kj-version.txt`, pinned + v0.0.3), NOT the kyverno-json engine. v1.28 re-mapped the spec's `kj` + engine → kyverno-json (D-227). v1.29 reintroduces `kj` as a **compiled + Go binary** embedded in the ECR container image — a distinct artifact. + No collision: kyverno-json remains the policy engine (INV-4); `kj` is a + static binary invoked via `subprocess` by the Lambda handler. + +### v1.29 Requirements + +New requirements REQ-354..REQ-369 + REQ-371 + REQ-363b — full text in +`.ciagent/REQUIREMENTS.md` §v1.29. Summary by phase: + +- **P1 — Publish Pipeline (REQ-354):** `publish.yml` attaches Lambda zip + + layer wheel + Python wheel + ECR container image (static `kj`, + `CGO_ENABLED=0`, tag `v1.29.x+kj-`) to GitHub Release with SHA-256. +- **P2 — Gitea Scrub + Decisions (REQ-367, REQ-368):** remove `.gitea/`, + scrub all Gitea refs, `forge_parity_disabled` CI assertion, pyproject + → 1.29.0, record D-232..238. +- **P3 — CFN Archive + TF Delegation (REQ-369):** archive CFN template → + `docs/archive/nova-idp-cfn-v1.28.md`, `nova idp setup --apply` delegates + to `terraform apply`. +- **P4 — Operator Guide + Reference Tracking:** `docs/operator-guide- + platform-ops.md`, ARCHITECTURE.md §12.9, STATE.md v1.29 CAPs + + invariants; REQUIREMENTS.md covered-reference markers. +- **P5 — Consumer Deploy Bump (cross-project, Edge 8):** `nova- + blockchain-exchange` deploy.yml `@v1.25` → `@v1.29` + smoke test. +- **P6 — Final Review + Audit + Milestone Ship.** + +### v1.29 Hard constraints + +- DO NOT activate pilot qa/prod/dr environments (D-208/D-209 — separate + initiative). M1 brings Nova-idp live; env activation is out. +- DO NOT add S3 Object Lock / JWS tamper-resistance (D-083). Tamper- + evidence via SQLite hash-chain remains. +- DO NOT restore 73.8% coverage — separate NFR milestone; YELLOW carried + without scope expansion. +- DO NOT provision CodeArtifact — direct GitHub Releases artifact fetch. +- DO NOT delete the CFN template in `acdl/acdl` at v1.29.0 — archive as + read-only reference (`docs/archive/nova-idp-cfn-v1.28.md`); deletion is + a follow-up after Terraform parity is verified. +- DO NOT add Nova-idp feature work (new OIDC claims, new ABAC rules) — + bring live; don't extend. +- DO NOT add MFA/TOTP, WebAuthn, upstream IdP federation (Vision §7). +- DO NOT add a CloudFront Frontend (L3B consumer surface) — pure ops + focus only. +- The `kj` binary MUST be compiled `CGO_ENABLED=0` and verified statically + linked (`file(1)`) before embedding (KJ-STATIC). +- The ECR image digest on the Fargate standby MUST equal the Lambda + `image_uri` digest at every `terraform plan` (KJ-LOCKSTEP, REQ-371 — + fail-closed by `lifecycle.precondition` mechanism, not by discipline). +- The JWKS endpoint is the ONLY public read surface; all other platform + endpoints gate with `AuthType: AWS_IAM` (JWKS-EDGE-ONLY, INV-18). +- Any `terraform apply` against `main` in `nova-platform-ops` MUST require + a Gitea Actions approval from a user distinct from the PR author + (TFM-HITL, INV-3 applied at platform level). +- `nova-platform-ops` MUST be `private: true` in Gitea, not mirrored + (OPER-PRIV). + +### v1.29 phase status (active — phase 0 in progress) - **P0** pre-execution (SPECIFY→CLARIFY→RESEARCH→PLAN→GRILL→MVP/UX) — in - progress, target tag `v1.27.0`. -- **P1..PN** execution phases — planned in PLAN.md. -- **P(N+1)** final review + audit + milestone ship — target tag - `v1.27.(N+1)` = the v1.28 release. + progress, target tag `v1.28.0`. +- **P1..P5** execution phases — planned in PLAN.md. +- **P6** final review + audit + milestone ship — target tag + `v1.28.6` = the v1.29 release. -> Phase-by-phase task breakdown, wave ordering, and persona assignments -> will live in `.ciagent/PLAN.md`. Authoritative resume state: -> `.ciagent/CHECKPOINT.json`. \ No newline at end of file +> Tags run on the **v1.28.x** line: `v1.28.0` (P0) → +> `v1.28.1..v1.28.5` (execution phases) → `v1.28.6` (final phase = +> milestone release). Milestone branch: +> `milestone/v1.29-reposplit-identity`. Phase-by-phase task breakdown, +> wave ordering, and persona assignments will live in `.ciagent/PLAN.md`. +> Authoritative resume state: `.ciagent/CHECKPOINT.json`. \ No newline at end of file diff --git a/.ciagent/REQUIREMENTS.md b/.ciagent/REQUIREMENTS.md index 25e9803..0a8b1d7 100644 --- a/.ciagent/REQUIREMENTS.md +++ b/.ciagent/REQUIREMENTS.md @@ -601,4 +601,250 @@ All v1.28 release-gate criteria in PLAN.md §6 met. | REQ-350 | P5 | complete (v1.27.5) | | REQ-351 | P5 | complete (v1.27.5) | | REQ-352 | P6 | complete (v1.27.6) | -| REQ-353 | P6 | complete (v1.27.6) | \ No newline at end of file +| REQ-353 | P6 | complete (v1.27.6) | + +--- + +## v1.29 — Reposplit + Identity Layer Bring-Live (active, milestone branch `milestone/v1.29-reposplit-identity`) + +> **Feature milestone — active.** v1.29 extracts all live platform +> components into a dedicated Gitea-private Terraform repository +> (`nova-platform-ops`), brings Nova-idp live in account `581513795199` +> for the first time, and standardizes `acdl/acdl` on GitHub. `kj` (a +> compiled Go binary, pinned v0.0.3, distinct from the kyverno-json +> engine) has exactly one identity: one ECR image digest shared by the +> production Lambda runtime and its defensive Fargate fallback +> (KJ-LOCKSTEP, REQ-371). +> +> Tags run on the **v1.28.x** line: `v1.28.0` (P0) → +> `v1.28.1..v1.28.5` (execution) → `v1.28.6` (final = milestone release). +> Milestone branch: `milestone/v1.29-reposplit-identity`. +> +> **Scope split (CLARIFY-grounded, full autonomy):** Terraform module +> code is authored out-of-band in `nova-platform-ops`. REQs marked +> `[covered-reference]` have their verification surface in the +> `nova-platform-ops` cutover gates (M1/M1.5/M2), documented in the +> operator guide (`docs/operator-guide-platform-ops.md`). CIAgent in +> `acdl` authors only the acdl-side REQs. + +### Decisions (locked in CLARIFY — full autonomy, load-bearing for v1.29) + +- **D-232 (Forge parity abandoned):** the byte-identical-forges CI parity + (Gitea + GitHub) is abandoned; `acdl/acdl` standardizes on GitHub. CI + fails with `forge_parity_disabled` (deliberate). Rationale: Vision §4 + domain boundaries — operations lives in Gitea-private `nova-platform- + ops`, engineering lives on GitHub. +- **D-233 (JWKS public-read via CloudFront edge):** the JWKS endpoint is + the only public read surface of the live platform (INV-18). All other + platform endpoints gate with `AuthType: AWS_IAM`. CloudFront + OAC + pinning replaces direct Lambda Function URL exposure. +- **D-234 (KMS asymmetric key provisioning):** `alias/nova-oidc-signing` + provisioned with `KeySpec: ECC_NIST_P256`, `KeyUsage: SIGN_VERIFY`, + 90-day rotation cadence (matches per-stack CMK rotation per D-069). +- **D-235 (Tag-pin handoff):** engineering hands off to operations via + tags. `acdl/acdl` `publish.yml` attaches artifacts to GitHub Releases + per tag; `nova-platform-ops` declares `local.nova_platform_version` + + `local.kj_source_sha` and resolves substrates through a single + `data.aws_ecr_image.kj_image`. +- **D-236 (Cutover shape + rollback procedure):** M1 day-0 cutover is + conditional on M1.5 verification gate (3 consecutive rebuilds, 12-item + spike per grill CF-1). Rollback = revert `nova_platform_version` pin; + the prior tag's artifacts remain downloadable. M2a (Fargate toggle) + activates only if M1.5 fails 3×. +- **D-237 (Fargate sunset discipline):** the always-warm minimal Fargate + standby (REQ-363b, ~$15–20/month) may not be deleted unless REQ-363 has + been green in production for ≥30 consecutive days. Sunset requires an + architecture review. +- **D-238 (KJ-LOCKSTEP release-gate invariant):** the ECR image digest + running on the Fargate standby MUST equal the digest resolved by + `aws_lambda_function.nova_idp_token_vend.image_uri` at every + `terraform plan`. Enforced by `lifecycle.precondition` (mechanism) + + Gitea Actions `if: steps.plan.outcome == 'success'` (mechanism) + PR + comment reporting (observability) + operator review (last, never + first). No second pipeline, no second SHA pin. Vision §6 immutability + + Vision §5 narrow interfaces. + +### P1 — Publish Pipeline + +#### REQ-354 — `publish.yml` attaches Lambda zip + layer wheel + Python wheel + ECR container image to GitHub Release for each tag +**Journeys:** J1, J2 (criteria 3–4). **Priority:** High. +**AC:** +**(1)** Given a tag `v1.29.x` is pushed to `acdl/acdl` main, when +`publish.yml` runs, then the release artifacts `nova-lambda-token-vend- +v1.29.x.zip`, `nova-cli-layer-v1.29.x.zip`, and `nova-1.29.x-py3-none- +any.whl` appear in GitHub Releases with matching SHA-256 in the body. +**(2)** Given two consecutive tags `v1.29.0` and `v1.29.1`, when both +releases are queried, then each tag's artifacts are independent and the +previous tag's artifacts remain downloadable. +**(3)** Given the publish pipeline runs for tag `v1.29.x`, when the +image build step executes, then a single ECR image is pushed at tag +`v1.29.x-kj-` where `` is read from +`platform/abac/kj-version.txt` at build time and embedded in the tag +(D-239: ECR tags reject `+`; corrected from `v1.29.x+kj-` to +`v1.29.x-kj-`). +**(4)** Given the image is pushed, when the GitHub Release body lists +artifacts, then the image URI and digest appear alongside the wheel, +layer, and Lambda zip. KJ-STATIC: the `kj` binary is compiled +`CGO_ENABLED=0 GOOS=linux GOARCH=amd64` and `file(1)` reports +`statically linked, no shared library` before embedding. + +### P2 — Gitea Scrub + Decisions + +#### REQ-367 — Hard scrub of all Gitea references in `acdl/acdl` at v1.29.0 +**Journeys:** Cross-cutting. **Priority:** Critical. +**AC:** +**(1)** Given v1.29.0 is cut from main, when `grep -rni gitea .github/ +docs/ pyproject.toml README.md .ciagent/` runs, then zero matches +outside this spec's archive section. +**(2)** Given v1.29.0 ships, when `.gitea/` is checked in the working +tree, then `find .gitea` returns nothing. +**(3)** Given v1.29.0 ships, when the bit-identical-forges parity is +asserted in CI, then CI fails with `forge_parity_disabled` (deliberate; +documented in D-232). + +#### REQ-368 — Decisions D-232..238 recorded in PROJECT.md + CLARIFY +**Journeys:** Cross-cutting. **Priority:** High. +**AC:** +**(1)** Given the milestone is recorded, when loading `PROJECT.md`, then +decisions D-232 (forge parity abandoned), D-233 (JWKS public-read via +CloudFront edge), D-234 (KMS asymmetric key provisioning), D-235 (tag- +pin handoff), D-236 (cutover shape + rollback procedure), D-237 +(Fargate sunset discipline ≥30 days → architecture review), D-238 +(KJ-LOCKSTEP release-gate invariant) are present with rationale citing +Vision §4 domain boundaries. +**(2)** Given decisions are present, then each decision references the +source statement from the v1.29 spec. + +### P3 — CFN Archive + TF Delegation + +#### REQ-369 — CFN → Terraform conversion of `nova idp setup` +**Journeys:** J2. **Priority:** High. +**AC:** +**(1)** Given the CFN template in `acdl/acdl/nova/idp/setup.py`, when +the equivalent Terraform in `nova-platform-ops` runs, then the same +resources (Lambdas, DDB tables, IAM roles, KMS key references) are +created. [covered-reference: nova-platform-ops] +**(2)** Given the conversion, when a new operator runs `nova idp setup +--apply`, then the CLI delegates to `terraform apply`; the CFN code +path is no longer the active path. +**(3)** Given the conversion, the CFN file in `acdl/acdl` is archived +to `docs/archive/nova-idp-cfn-v1.28.md` as read-only reference; +deletion is a follow-up. + +### P4 — Operator Guide + Reference Tracking (docs) + +#### REQ-OPS-GUIDE — `docs/operator-guide-platform-ops.md` +**Journeys:** J2. **Priority:** High. +**AC:** Given the operator guide is published, when an operator reads +it, then it covers: KMS rotation (90-day cadence, `alias/nova-oidc- +signing`), JWKS reachability via CloudFront edge (OAC pinning, public +read vs. IAM-gated), PITR restore (DynamoDB point-in-time recovery), +PAT revocation (60s SLO), edge configuration (CloudFront + WAF + ACM + +Route53), Fargate standby status checks (`GET /health` every 10s, +`KJ-WARMUP-HEALTH`), cost section (WAF ~$5–10/month + Fargate +~$15–20/month), artifact-mirror fallback (operator-local mirror by +SHA-256 when Gitea `act_runner` cannot reach GitHub Releases), and the +M1/M1.5/M2 cutover gates as release-gate entries for the covered- +reference REQs. + +### P5 — Consumer Deploy Bump (cross-project, Edge 8) + +#### REQ-CONSUMER-BUMP — `nova-blockchain-exchange` deploy.yml `@v1.25` → `@v1.29` +**Journeys:** J1. **Priority:** High. +**AC:** +**(1)** Given `nova-blockchain-exchange` deploy.yml pins +`acdl/.github/workflows/deploy.yml@v1.25`, when the bump is applied, +then both `.github/workflows/deploy.yml` and +`.gitea/workflows/deploy.yml` reference `@v1.29`. +**(2)** Given the bump, when the smoke test runs (sign-up → sign-in → +token-vend → apply → audit), then the chain completes successfully +against the v1.29 publish artifacts. + +### Covered-reference requirements (authored in `nova-platform-ops`, out-of-band) + +The following REQs are tracked for milestone completeness but their +code lands in `nova-platform-ops`. Their verification surface is the +M1/M1.5/M2 cutover gates documented in the operator guide. + +- **REQ-355** — ops repo pins `local.nova_platform_version` + + `local.kj_source_sha`; CI resolves matching artifacts + image digest. +- **REQ-356** — ops repo CI runs `terraform plan` on every PR; drift + fails with `drift_detected`. +- **REQ-357** — HITL approver distinct from PR author required for + `terraform apply` (INV-3, TFM-HITL). +- **REQ-358** — Operator bumps `nova_platform_version` to roll out + engineering change; `CodeSha256` matches the artifact SHA-256. +- **REQ-359** — ops repo is Gitea-private with no GitHub mirror + (OPER-PRIV). +- **REQ-360** — ops repo IAM scope is bounded; no AdministratorAccess + (IAM-NARROW). +- **REQ-361** — Terraform imports existing live resources idempotently + (IMPORT-IDEMPOTENT). +- **REQ-362** — `alias/nova-oidc-signing` KMS key provisioned + (`ECC_NIST_P256`, `SIGN_VERIFY`, 90-day rotation). +- **REQ-363** — Nova-idp 3 Lambdas deployed on container image with + static `kj` (production substrate, KJ-STATIC). +- **REQ-363b** — Fargate defensive fallback — always-warm minimal + Fargate standby, **same ECR image** (KJ-LOCKSTEP, KJ-WARMUP-HEALTH). +- **REQ-364** — JWKS Function URL reachable only via CloudFront with + OAC pinning (INV-18, JWKS-EDGE-ONLY). +- **REQ-365** — WAF WebACL rate-limit (3000/5min) + AWS Managed Rules. +- **REQ-366** — ACM cert + Route53 alias for the JWKS domain. +- **REQ-371** — KJ-LOCKSTEP applied-at-plan mechanism + (`lifecycle.precondition` on both image-bearing resources; fail-closed + by mechanism, not by discipline). + +### v1.29 Invariants + NFR constraints (new) + +- **INV-18 (JWKS-EDGE-ONLY):** the JWKS endpoint is the only public read + surface of the live platform. All other platform endpoints MUST gate + with `AuthType: AWS_IAM`. +- **KJ-STATIC (NFR):** `kj` compiled `CGO_ENABLED=0`; `file(1)` reports + `statically linked, no shared library`; SHA-256 matches + `platform/abac/kj-version.txt`; recorded in Terraform state. +- **KJ-LOCKSTEP (NFR):** Fargate standby digest == Lambda `image_uri` + digest at every `terraform plan`. Detected by + `lifecycle.precondition` (mechanism) + CI `if: + steps.plan.outcome == 'success'` (mechanism) + PR comment + (observability) + operator review (last). No second pipeline, no + second SHA pin. +- **KJ-WARMUP-HEALTH (NFR):** Fargate standby `READY` probe (`GET /health + → 200` every 10s) green before M1 cutover; release-gate entry. +- **OPER-PRIV (NFR):** `nova-platform-ops` `private: true`, not mirrored. +- **IAM-NARROW (NFR):** Gitea OIDC role bounded per REQ-360; no + `Action: "*"` or `Resource: "*"`. +- **DRIFT-DETECT (NFR):** `terraform plan` exit 2 (drift) fails the + apply workflow; manual reconciliation required. +- **IMPORT-IDEMPOTENT (NFR):** re-import exits non-zero with + `resource_already_imported`. +- **TFM-HITL (NFR):** `terraform apply` against `main` requires Gitea + Actions approval from a user distinct from the PR author. +- **JWKS-SLO (NFR):** `GET /.well-known/jwks.json` P95 < 200ms same- + region; `Cache-Control: max-age=3600` honored. +- **JWKS-ROTATION (NFR):** on key rotation, both old + new public keys + published during 24-hour overlap window. + +### v1.29 Traceability (live — see CHECKPOINT.json for authoritative state) + +| REQ | Phase | Status | +|-----|-------|--------| +| REQ-354 | P1 | planned | +| REQ-367 | P2 | planned | +| REQ-368 | P2 | planned | +| REQ-369 | P3 | planned | +| REQ-OPS-GUIDE | P4 | planned | +| REQ-CONSUMER-BUMP | P5 | planned | +| REQ-355 | covered-reference | planned (nova-platform-ops) | +| REQ-356 | covered-reference | planned (nova-platform-ops) | +| REQ-357 | covered-reference | planned (nova-platform-ops) | +| REQ-358 | covered-reference | planned (nova-platform-ops) | +| REQ-359 | covered-reference | planned (nova-platform-ops) | +| REQ-360 | covered-reference | planned (nova-platform-ops) | +| REQ-361 | covered-reference | planned (nova-platform-ops) | +| REQ-362 | covered-reference | planned (nova-platform-ops) | +| REQ-363 | covered-reference | planned (nova-platform-ops) | +| REQ-363b | covered-reference | planned (nova-platform-ops) | +| REQ-364 | covered-reference | planned (nova-platform-ops) | +| REQ-365 | covered-reference | planned (nova-platform-ops) | +| REQ-366 | covered-reference | planned (nova-platform-ops) | +| REQ-371 | covered-reference | planned (nova-platform-ops) | \ No newline at end of file diff --git a/.ciagent/RESEARCH.md b/.ciagent/RESEARCH.md index 98b86e6..c1060e3 100644 --- a/.ciagent/RESEARCH.md +++ b/.ciagent/RESEARCH.md @@ -333,4 +333,258 @@ greenfield files: `nova/` CLI package, `platform/abac/token-vend.policy`, All 11 research questions answered with cited findings + concrete recommendations + risks. D-228 amended (fail-closed, not pure-Python fallback). The `kj` binary packaging is the highest-risk item (P2 -spike). Next: PLAN. \ No newline at end of file +spike). Next: PLAN. + +--- + +# Nova — v1.29 Research Findings + +> Phase: research (pre-execution). Milestone: v1.29 (Reposplit + Identity +> Layer Bring-Live). Status: research. Researcher: ci-researcher. +> Autonomy: full. +> +> Research delegated to the ci-researcher subagent (10 topics — Terraform +> import idempotency, `data.aws_ecr_image` digest resolution, +> `lifecycle.precondition`, CloudFront OAC for Lambda Function URL, WAF +> on CloudFront, ACM DNS validation + Route53 alias, `kj` Go binary +> static build, ECR tag format, codebase inspection, Gitea Actions HITL). +> This file is the curated summary. Key findings + recommendations below. + +--- + +## §1 — Terraform `import` idempotency (REQ-361) + +- `terraform import ` reads an existing cloud resource into + state without modifying it; the resource must have a matching + `resource` block in config. +- Re-importing an address already in state fails with **`Error: Resource + already managed by Terraform`** (non-zero exit). The CI import step + must treat this specific error as idempotent success (grep the + message, not just exit code) — this is the IMPORT-IDEMPOTENT contract. +- `importable-resources.tf` is a convention (not built-in): a dedicated + file listing resource addresses imported from the live account (S3 + state bucket, DynamoDB tables, IAM OIDC role, KMS keys) so the import + surface is enumerable + reviewable. +- Drift detection: `terraform plan -detailed-exitcode` (exit 2 = drift) + fails the apply; the state bucket is bootstrapped manually then + imported (never created by Terraform — avoids bootstrapping the + bootstrapper, Q1/§7.1, D-235). + +**Recommendation:** `nova-platform-ops` maintains an +`importable-resources.tf` map; CI import treats "already managed" as +idempotent success; `plan -detailed-exitcode` asserts zero drift. + +## §2 — `data.aws_ecr_image` digest resolution (REQ-355, REQ-371) + +- `data "aws_ecr_image" "kj_image" { repository_name = …; image_tag = … }` + resolves the tag to an **immutable `sha256:` digest** via + `image_digest`. +- ECR tags are mutable by default (a re-push moves a tag → different + digest). KJ-LOCKSTEP pins on `image_digest`, never the tag. +- `image_uri` = `${data.aws_ecr_repository.kj.repository_url}@${data.aws_ecr_image.kj_image.image_digest}` + — pinning by `@digest`, not `:tag`. Both Lambda and Fargate reference + the same data source → same digest by construction. +- `data.aws_ecr_image` reads at plan time; if the tag doesn't exist + (engineering hasn't published), the data source fails the plan (Q7 + fail-closed). + +**Recommendation:** Both image-bearing resources reference a single +`data.aws_ecr_image.kj_image`; `image_uri` = `repo@digest`; LOCKSTEP is +true by construction + the precondition (§3) is a verification. + +## §3 — `lifecycle.precondition` — the KJ-LOCKSTEP mechanism (REQ-371) + +- **Version correction (D-240):** preconditions introduced in + **Terraform v1.2.0 (May 2022)**, NOT v1.4+ as the spec implies. The + ops repo `required_version = ">= 1.2.0"` suffices. +- Syntax: `precondition` block inside `lifecycle { … }` for resources. + Evaluated **before** the resource action (during planning); a failing + precondition aborts the **plan** with the custom `error_message`. +- `error_message` is a string expression — can interpolate values: + `error_message = "KJ-LOCKSTEP: Fargate='${aws_ecs_task_definition.kj.image}' canonical='${data.aws_ecr_image.kj_image.image_digest}'"`. +- Asserting two attributes resolve to the same value: + ```hcl + lifecycle { + precondition { + condition = self.image_uri == "${data.aws_ecr_repository.kj.repository_url}@${data.aws_ecr_image.kj_image.image_digest}" + error_message = "KJ-LOCKSTEP: Lambda image does not match the resolved ECR digest" + } + } + ``` + +**Pitfalls:** precondition blocks cannot reference `count`/`for_each` +unexpanded resources; both resources must depend on the same data source +(explicit `depends_on` if `image_uri` is computed indirectly). + +**Recommendation:** Add `lifecycle { precondition { … } }` to **both** +the Lambda and Fargate task; set `required_version = ">= 1.2.0"`. + +## §4 — CloudFront OAC pinning to Lambda Function URL (D-233, REQ-364) + +- **Critical:** CloudFront OAC for a Lambda Function URL origin requires + `AuthType: AWS_IAM` on the Function URL (NOT `AuthType: NONE`). With + `AWS_IAM`, direct access returns 403 unless SigV4-signed; CloudFront + + OAC signs requests on the viewer's behalf → CloudFront 200, direct 403 + (INV-18 JWKS-EDGE-ONLY). +- OAC resource: `OriginAccessControlOriginType = "lambda"`, + `SigningBehavior = "always"`, `SigningProtocol = "sigv4"`. Attach via + `OriginAccessControlId` on the origin block; HTTPS only. +- Resource-based permission: `aws lambda add-permission --action + lambda:InvokeFunctionUrl --principal cloudfront.amazonaws.com + --source-arn ` — binds the Function URL to the + specific distribution. +- OAC replaces the deprecated S3-origin OAI; for Lambda origins, OAC is + the only signing mechanism. + +**Pitfall:** if `AuthType: NONE` is left on the Function URL, OAC signing +is ignored and the URL stays public — the 403 guarantee evaporates. + +**Recommendation:** JWKS Function URL `authorization_type = "AWS_IAM"`, +`lambda`-type OAC (`SigningBehavior: always`), `lambda:InvokeFunctionUrl` +permission scoped to the distribution ARN. + +## §5 — WAF WebACL rate-limit + AWS Managed Rules on CloudFront (REQ-365) + +- Rate-based rule: `RateBasedStatement` with `Limit: 3000`, + `AggregateKeyType: "IP"`, `EvaluationWindowSec: 300` (5-min window; + accepted values 60/120/300/600). WAF checks ~every 10s. +- AWS Managed Rules Common Rule Set = managed rule group + `AWSManagedRulesCommonRuleSet` (vendor `AWS`), attached as a separate + priority from the rate rule. +- CloudFront WebACLs **must** be created in `us-east-1` with + `Scope = "CLOUDFRONT"` (regional WebACLs cannot associate with + CloudFront). +- CloudWatch metrics: per-rule `VisibilityConfig.CloudWatchMetricsEnabled + = true`; S3 access logs via `aws_cloudfront_distribution.logging_config`. + +**Recommendation:** WebACL in `us-east-1` `Scope=CLOUDFRONT`; rate rule +(3000/5min/IP) + Common Rule Set; associate to JWKS distribution; +CloudWatch metrics + S3 access logs. + +## §6 — ACM cert DNS validation + Route53 alias (REQ-366) + +- ACM DNS validation: `aws_acm_certificate` with + `validation_method = "DNS"`; create `aws_route53_record` for each + `domain_validation_options` CNAME; `aws_acm_certificate_validation` + waits on `ISSUED`. For CloudFront, the cert **must** be in + `us-east-1`. +- Route53 alias: `type = "A"`, `alias { name = + aws_cloudfront_distribution.jwks.domain_name; zone_id = + aws_cloudfront_distribution.jwks.hosted_zone_id; + evaluate_target_health = false }`. +- `route53_record_not_resolvable` failure mode: the alias doesn't + resolve until CloudFront `status = Deployed` AND ACM cert `ISSUED`. If + the validation CNAME is mis-created or Route53 is not authoritative, + the CNAME never validates → cert stays `PENDING_VALIDATION` → alias + NXDOMAIN. + +**Recommendation:** ACM cert in `us-east-1` DNS validation; validation +CNAMEs in the authoritative Route53 zone; `aws_acm_certificate_validation` +gates on `ISSUED`; Route53 A-alias to the distribution. Operator guide +documents the `route53_record_not_resolvable` → check-cert-status +debugging path. + +## §7 — `kj` Go binary static build for AL2023 Lambda (KJ-STATIC, REQ-354, REQ-363) + +- Build: `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s + -w" -o kj ./…`. `CGO_ENABLED=0` is load-bearing — no cgo, no dynamic + libc link. +- `file(1)` must report `ELF 64-bit LSB executable, x86-64, statically + linked` + absence of `shared library`/`interpreter`. Secondary: + `readelf -d kj` shows no `NEEDED` entries. +- Base image `public.ecr.aws/lambda/python:3.12-al2023`; copy binary to + `/opt/kj/kj` `chmod 0555` owned by `sbx_user:1051` (Lambda sandbox + user, uid/gid 1051 in AL2023). `0555` + immutable-owned prevents + runtime tampering. +- Lambda handler invokes `subprocess.run(['/opt/kj/kj', 'apply', …], + capture_output=True, check=True)` — `kj` is a substrate binary, not a + library; the Python handler is a thin shim. kyverno-json (INV-4) is + separate + unaffected. + +**Pitfall:** `CGO_ENABLED=1` (default on systems with gcc) produces a +dynamically-linked binary; AL2023 glibc mismatch → runtime +`GLIBC_X not found`. `CGO_ENABLED=0` eliminates this. + +**Recommendation:** `publish.yml` P1 builds with `CGO_ENABLED=0 +GOOS=linux GOARCH=amd64`, asserts `file` reports `statically linked` + +no `shared library` (fail build otherwise), copies to `/opt/kj/kj` +`chmod 0555`, handler calls `subprocess.run(['/opt/kj/kj', 'apply', …])`. + +## §8 — ECR image tag format (REQ-354 AC 3) — SPEC CORRECTION (D-239) + +- **ECR image tags do NOT allow `+`.** The ECR tag regex is + `^[a-zA-Z0-9]+(?:[._-][a-zA-Z0-9]+)*$` — permitted chars + `[a-zA-Z0-9._-]` only; `+` is rejected by `PutImage`/`BatchGetImage` + with `InvalidParameterException`. +- The spec's tag format `v1.29.x+kj-` is **invalid** as written. + Correct format: **`v1.29.x-kj-`** (replace `+` with `-`). +- The digest is the immutable trust surface regardless of the tag string + — a re-tag is detectable only via digest mismatch. The tag is a human + hint, not a security boundary. + +**Decision D-239 (spec correction):** REQ-354 AC 3 tag format corrected +to `v1.29.x-kj-`. Confidence 0.95. Applied to REQUIREMENTS.md +§v1.29 REQ-354 AC (3). + +## §9 — Codebase inspection (actual file paths) + +| Target | Path | Summary | +|---|---|---| +| `publish.yml` | `.github/workflows/publish.yml` (165 lines) + `.gitea/workflows/publish.yml` mirror | Currently publishes wheel + Lambda layer on `push: branches: [main]` (NOT tag-triggered). P1 must change trigger to `on: push: tags: ['v1.29.*']` + attach Lambda zip + ECR image to GitHub Releases. | +| `nova/idp/setup.py` CFN | `nova/idp/setup.py` (40 lines, thin CLI dispatcher) + `core/lambda/nova_idp_setup.py` (actual CFN logic, importlib-loaded because `lambda` is reserved) | REQ-369 archives to `docs/archive/nova-idp-cfn-v1.28.md`; `--apply` delegates to `terraform apply`. | +| `platform/abac/kj-version.txt` | `platform/abac/kj-version.txt` (2 lines: `v0.0.3` + SHA `4ebb9a19...`) | Already pins `kj` v0.0.3 + source SHA from v1.28 P4. P1 reads this SHA to embed in the ECR tag + verify the build. | +| `.gitea/` scrub targets | `.gitea/workflows/` (7 files) + `scripts/sync_workflows.py` (line 26: `GITEA_DIR`), `scripts/sync_to_nova.sh`, `scripts/rotate_spike_key.sh`, `terraform/bootstrap/`, ~100 `.ciagent/` doc matches | REQ-367 P2 removes `.gitea/`, scrubs `gitea` from `.github/` `docs/` `pyproject.toml` `README.md` `.ciagent/`, asserts `forge_parity_disabled` in CI (D-232). `sync_workflows.py` is the central removal target. | +| Consumer `deploy.yml` | NOT in `acdl/.github/workflows/deploy.yml` (that's the platform reusable workflow). Consumer's deploy.yml is in the `nova-blockchain-exchange` project — documented at `.ciagent/nova-blockchain-exchange/REQUIREMENTS.md` (REQ-314) + `.ciagent/nova-blockchain-exchange/README.md`. | P5 bumps consumer's `uses:` ref `@v1.25` → `@v1.29` in both `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (consumer's `.gitea/` is out of scope for REQ-367 — that scrub is `acdl/acdl` only) + smoke test. | + +## §10 — Gitea Actions HITL approval (REQ-357, TFM-HITL) + +- Gitea Actions has **no Environments API** with required reviewers. The + approval signal is `gitea.actor` (triggering user) + + `gitea.triggering_actor` (may differ on re-run — the re-dispatcher). +- PR author: `${{ gitea.event.pull_request.user.login }}`. INV-3 check: + `${{ gitea.triggering_actor }} != ${{ gitea.event.pull_request.user.login }}` + (use `triggering_actor` for re-run safety). +- Gitea scoped-workflows (v1.27+) supports **required workflows** that + gate PR merges via status checks — but this gates *merge*, not *apply*. +- The `workflow_dispatch` approve-input pattern (D-042) is the mechanism: + plan runs automatically on PR; apply is a separate `workflow_dispatch` + with `approve_apply` input; the apply job asserts INV-3 + fails closed. +- **Codebase precedent:** `core/hitl_gates.py` + `core/separation_of_duties.py` + (D-042) — `hitl_gates.attest(env, approver)` reads + `GITHUB_ACTOR`/`FORGE_ACTOR`, writes to DynamoDB outbox; + `separation_of_duties.check` compares approvers. This is the production + pattern to extend for `nova-platform-ops` `terraform apply`. + +**Pitfalls:** scoped-workflow required-check enforcement needs branch +protection on `main`; a re-run changes `gitea.actor` to the re-dispatcher +— use `gitea.triggering_actor` for the effective approver. + +**Recommendation:** `nova-platform-ops` uses `workflow_dispatch` +approve-input pattern (extending `hitl_gates.py`/`separation_of_duties.py`); +plan auto-runs on PR, apply is `workflow_dispatch` with `approve_apply`; +apply job asserts `${{ gitea.triggering_actor }} != ${{ gitea.event.pull_request.user.login }}`; +branch protection on `main` + required scoped-workflow status check. + +--- + +## New decisions for the decision ledger (research-derived) + +| D-ID | Title | Confidence | Source | +|---|---|---|---| +| **D-239** | ECR tag format `v1.29.x+kj-` invalid (`+` not in ECR tag regex) → corrected to `v1.29.x-kj-` | 0.95 | §8 ECR API PutImage character class | +| **D-240** | `lifecycle.precondition` introduced in Terraform v1.2.0 (not v1.4+); ops repo `required_version = ">= 1.2.0"` suffices | 0.98 | §3 Terraform v1.2.0 CHANGELOG | + +Both are spec-vs-reality corrections logged at full autonomy (confidence +≥ 0.60 threshold). D-239 is applied to REQUIREMENTS.md §v1.29 REQ-354 +AC (3). D-240 is documented in the operator guide (P4) for the +`nova-platform-ops` `required_version` floor. + +--- + +## RESEARCH complete + +All 10 research questions answered with cited findings + concrete +recommendations + risks. Two spec corrections (D-239 ECR tag, D-240 +Terraform precondition floor). The highest-risk item is the M1.5 +verification gate (Q7 carry-forward — `kj` static build + 3 consecutive +rebuilds in `nova-platform-ops` CI). Next: PLAN. \ No newline at end of file diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index 8efc7dc..f40cd5b 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -117,9 +117,35 @@ C-2.1, P5 docs-integration, P6 final-review-ship). Grill: PROCEED-WITH-CONDITIONS (0.76), 3 critical fixes (ABAC fail-closed, JWS KDF, traceability drift) + 16 tracked conditions applied. 1000 - tests passing. Tags: `v1.27.0` (P0) → `v1.27.1..v1.27.5` (P1..P5) → + tests passing. Tags: `v1.27.0` (P0) → `v1.27.1..v1.27.5` (P1..P5) → `v1.27.6` (P6 final = milestone release). +- **v1.29 (active, milestone branch `milestone/v1.29-reposplit- + identity`):** Reposplit + Identity Layer Bring-Live. Feature milestone. + v1.29 extracts all live platform components (Nova-idp Lambdas, KMS keys, + DynamoDB tables, S3 state buckets, OIDC roles, JWKS, audit outbox + bootstrap) from `acdl/acdl` into a dedicated Gitea-private Terraform + repository (`nova-platform-ops`), brings Nova-idp live in account + `581513795199` for the first time (code complete since v1.28, unverified + in-account at Phase 0), and standardizes `acdl/acdl` on GitHub. The + split enforces Vision §4 domain boundaries architecturally — + engineering ends at the compiled artifact; operations begins at the + live platform under guardrails. `kj` (a compiled Go binary, pinned + v0.0.3 in `platform/abac/kj-version.txt`, distinct from the kyverno-json + engine) has exactly one identity: one ECR image digest shared by both + the production Lambda runtime and its defensive Fargate fallback + (KJ-LOCKSTEP — drift eliminated by construction, enforced by + `lifecycle.precondition` at plan time, REQ-371). M1.5 verification gate + (8-item spike, 3 consecutive rebuilds) gates M1 cutover. CIAgent in + `acdl` delivers the acdl-side work (publish.yml + ECR image, Gitea + scrub, CFN archive + CLI terraform-delegation, operator guide, + consumer deploy bump); the Terraform modules for `nova-platform-ops` + are authored out-of-band (covered-reference REQs with cutover gates as + the verification surface). 17 requirements (REQ-354..369 + 371 + + 363b), 7 decisions (D-232..238), 1 invariant (INV-18 JWKS-EDGE-ONLY) + + 10 NFR constraints. Tags: `v1.28.0` (P0) → `v1.28.1..v1.28.5` (P1..P5) + → `v1.28.6` (P6 final = milestone release). + > **Full v1.0–v1.24 phase detail, wave ordering, success criteria, and > decision cross-references:** `.ciagent/archive/ROADMAP-v1.0-v1.24.md`. diff --git a/.ciagent/config.json b/.ciagent/config.json index c884cec..e829519 100644 --- a/.ciagent/config.json +++ b/.ciagent/config.json @@ -13,7 +13,7 @@ ], "active_project": "acdl", "active_projects": ["acdl", "nova-blockchain-exchange"], - "active_milestone": "v1.28", + "active_milestone": "v1.29", "autonomy": { "level": "full", "escalation_hooks": ["deploy", "delete_data", "merge_to_main"],