Files
acdl/.ciagent/PLAN.md
T
CIAgent Orchestrator 0789c27ca2
Nova Slides Render / render (push) Failing after 14m27s
docs(P00): complete v1.29 pre-execution — SPECIFY+CLARIFY+RESEARCH+PLAN+GRILL+MVP/UX
---ci---
project: acdl
phase: 0
milestone: v1.29
status: complete
---/ci---
2026-08-20 05:00:35 +00:00

53 KiB
Raw Blame History

PLAN — v1.28 CLI Canonicalization + Identity Layer

Milestone: v1.28 (feature — CLI substrate + Nova-idp identity layer). Tags on the v1.27.x line: v1.27.0 (P0) → v1.27.1..v1.27.6 (P1..P6) → v1.27.7 (P7 final = milestone release). The final phase's patch IS the milestone release. Branch: milestone/v1.28-cli-identity. Phase branches: phase/00-pre-execution, phase/01-cli-substrate, phase/02-lambda-packaging, phase/03-idp-auth, phase/04-token-vend-pat, phase/05-docs-integration, phase/06-final-review-ship.

Tags: v1.27.0 (P0) → v1.27.1..v1.27.5 (P1..P5) → v1.27.6 (P6 final = milestone release). 6 execution phases (P5 idp-setup folded into P4 Wave 8 per grill C-2.1).

Milestone goal

The Nova CLI is installable from internal PyPI (CodeArtifact); every core/ module is reachable as a nova <subcommand>; the CLI and Lambda functions share a single core/ source tree; and Nova owns its identity layer end-to-end (Nova-idp: nova-idp-auth + nova-idp-token-vend Lambdas, KMS-signed OIDC tokens, kyverno-json ABAC token vending, PAT lifecycle). No AWS-managed identity services in the path (INV-15).

Requirements

31 requirements: REQ-323..REQ-353 (full text in .ciagent/REQUIREMENTS.md §v1.28). 6 capabilities: CAP-033..CAP-038. 6 invariants: INV-12..INV-17. 6 decisions: D-226..D-231 (CLARIFY) + RESEARCH amendments (D-228 fail-closed, D-229 strong-read-on-PK).

Phase breakdown

Phase P1 — cli-substrate (REQ-323..REQ-328)

Goal: CodeArtifact wheel + Lambda layer pipeline; nova/ CLI package with a subcommand per core/ module; nova init; nova cli-action composite action; core/mode_resolver.py; audit emission with mode + selection_reason. The CLI is installable and every core/ module is reachable.

Exit criterion: CAP-033 + CAP-034 + CAP-035 Verified + all REQ-323..328 tests pass. CodeArtifact provisioned (Wave 0 gate).

Wave 0 — CodeArtifact provisioning (backend-engineer) [C-3.2/C-8.1]

  • Task 0.1 (backend-engineer): provision CodeArtifact domain (nova) + repository (nova-pypi) in 581513795199. Verify codeartifact:* IAM grant on nova-spike-runner. Binary go/no-go gate for Wave 4. If fail: activate Gitea wheel index fallback (CLARIFY assumption #1) and document in PLAN.md.

Wave 1 — pyproject + entry point (cli-engineer)

  • Task 1.1 (cli-engineer): pyproject.toml — add [project.scripts] nova = "nova.cli:main"; add [tool.setuptools.packages.find] including nova, nova.*, core, core.*, adapters.*; bump requires-python to >=3.12; add argon2-cffi, cryptography, pyjwt, hypothesis to deps/test-deps. Verify pip install -e . produces a nova executable.

Wave 2 — CLI dispatch + subcommands (cli-engineer)

  • Task 2.1 (cli-engineer): nova/__init__.py + nova/cli.py (~80 lines, auto-discovers nova/<module>.py via pkgutil.iter_modules, dispatches, emits cli.invocation audit event stub with INV-12 fields).
  • Task 2.2 (cli-engineer): nova/<module>.py for each core/ module (≤50 lines, add_parser + run delegates to core/). Cover: resolve, decommission, env-transition, env-check, hitl, onboard, outbox, publish-outputs, policy, regression, sod, readiness, attestation-matrix, confidence. Skip internal-only (env, local_emulators, output_publisher if not user-facing).
  • Task 2.3 (cli-engineer): nova/init.py (REQ-325) — scaffolds .nova/, .nova/contract.yml.attestations/, .gitignore (excludes secrets, ~/.nova/credentials.json).

Wave 3 — mode_resolver + audit (cli-engineer)

  • Task 3.1 (cli-engineer): core/mode_resolver.pyresolve_mode(flag, env_var, credential_type, stdin_isatty) per D-226. sys.stdin.isatty() is the TTY check (RESEARCH §11). Invalid env → warn + fall through. Returns (mode, selection_reason).
  • Task 3.2 (cli-engineer): wire mode_resolver into nova/cli.py — resolve mode before dispatch, emit cli.invocation with mode, selection_reason, credential_type, command, args (INV-12, REQ-328).
  • Task 3.3 (cli-engineer): tests/test_mode_resolver.pyhypothesis property tests (REQ-349): deterministic, flag-wins, invalid-env-ignored, no-silent-fallback. Edge cases: TTY + piped stdout, missing credential, conflicting flag/env, invalid env value.

Wave 4 — CodeArtifact + layer pipeline (backend-engineer)

  • Task 4.1 (backend-engineer): .gitea/workflows/publish.yml + .github/workflows/publish.yml (byte-identical) — build wheel → CodeArtifact twine upload → build layer (pip install --target layer/python/ + argon2-cffi + cryptography + pyjwt) → lambda publish-layer-version → SSM /nova/layer/nova-cli/version mapping (CAP-035). Fail either → job fails (merge blocked, REQ-323). Pin version to <semver>+<sha7> for idempotent re-runs.

Wave 5 — composite action (cli-engineer + backend-engineer)

  • Task 5.1 (cli-engineer): .github/actions/nova-cli/action.yml — composite action, setup-python@v5 (3.12), CodeArtifact login + pip install nova, nova ${{ inputs.command }}. NOVA_CLIENT_MODE from input.
  • Task 5.2 (backend-engineer): byte-identical integration test — CI matrix runs the action on GitHub ubuntu-latest + Gitea act_runner; assert same stdout/exit code (REQ-326 AC2, NFR-11).

Wave 6 — CAP-033/034 gate (cli-engineer)

  • Task 6.1 (cli-engineer): tests/test_cli_subcommands.py — CAP-033 (nova --help lists a subcommand for every core/ module)
    • CAP-034 (AST scan: ≤50 lines, ≤3 defs, all calls resolve to core., no conditionals beyond if __name__). Wire into CI merge gate.

Phase P2 — lambda-packaging (REQ-329, REQ-330, REQ-331)

Goal: Dual-use core/lambda/contract_ingestor.py (Lambda + CLI paths share ≥80% code); core/env.py:+synthesize_local_env() for nova apply --local; .nova/contract.yml.attestations/ scaffolded; JWS-from-PAT key derivation (C-5.2).

Exit criterion: all REQ-329..331 tests pass + JWS-from-PAT KDF specified.

Wave 1 — dual-use refactor (backend-engineer)

  • Task 1.1 (backend-engineer): refactor core/lambda/contract_ingestor.py — extract the shared logic into importable functions; the Lambda handler + the CLI __main__ block both call them. The __main__ block already exists (the dual-use precedent per RESEARCH §1.2). Verify ≥80% code share (CAP-034 / code review). Local path via core/local_emulators.py:LocalLambdaStub.

Wave 2 — local env synthesizer + JWS KDF (backend-engineer)

  • Task 2.1 (backend-engineer): core/env.py:+synthesize_local_env() — produces a local env dict (account_id placeholder, region local, no real AWS) from a contract + --local flag. Mirrors core/onboarding.py:generate_env_file().
  • Task 2.2 (cli-engineer): nova/apply.py (≤50 lines) — nova apply --local delegates to core.env.synthesize_local_env() + core.contract_resolver.resolve().
  • Task 2.3 (security-engineer): JWS-from-PAT key derivation (C-5.2). HKDF-SHA256(PAT_bytes, salt='nova-local-attestation', info='jws-signing-key') → 32-byte symmetric key. The JWS is HMAC-SHA256 (symmetric, not asymmetric). The "public key derivable from the PAT" AC (REQ-332) is re-interpreted: the verification key is derived from the PAT via the same KDF (the PAT is the shared secret). Document in docs/developer-guide-auth.md. Update REQ-332 AC accordingly.

Wave 3 — attestations dir (cli-engineer)

  • Task 3.1 (cli-engineer): verify nova init (P1 Wave 2 Task 2.3) creates .nova/contract.yml.attestations/ (empty). REQ-331 test.

Phase P3 — idp-auth (REQ-333, REQ-334, REQ-335)

Goal: nova-idp-auth Lambda (sign-up, sign-in, session) with Argon2id hashing + DynamoDB tables. CAP-036 target.

Exit criterion: CAP-036 Verified (E2E sign-up → sign-in → session passes in CI).

Wave 1 — DynamoDB schema (backend-engineer)

  • Task 1.1 (backend-engineer): define the 4 DynamoDB table schemas (nova-users, nova-sessions, nova-password-resets, nova-pats) in a CloudFormation snippet (reused by P4 Wave 8 nova idp setup). PITR enabled on each (REQ-335).

Wave 2 — Argon2id (security-engineer) [C-1.2/C-7.2]

  • Task 2.1 (security-engineer): core/lambda/nova_idp_auth.py — Argon2id password hashing via argon2-cffi (D-228: bundled abi3 wheel; fail-closed on ImportError → 503, no pure-Python fallback). Parameters: t=3, m=65536 KiB, p=1 (OWASP-recommended minimum). Lambda memory ≥512 MB (m=64 MiB + Python overhead fits). Raw passwords never in logs/traces/env/DDB (INV-16, REQ-334).
  • Task 2.2 (security-engineer): tests/test_argon2_fail_closed.py — mock argon2.low_level import failure → assert auth Lambda returns 503 (not a crash, not a weak hash). C-1.2.

Wave 3 — auth Lambda (backend-engineer + security-engineer)

  • Task 3.1 (backend-engineer): nova-idp-auth Lambda handler — sign-up, sign-in, session creation endpoints. Function URL + IAM auth. DynamoDB via lazy boto3.resource (the existing pattern).
  • Task 3.2 (security-engineer): session token issuance + session storage in nova-sessions (TTL expires_at). Password reset flow in nova-password-resets (TTL 15m).

Wave 4 — CAP-036 E2E (backend-engineer)

  • Task 4.1 (backend-engineer): tests/test_idp_auth.py — sign-up → sign-in → session round-trip (moto[dynamodb] for local; deployed for CI). CAP-036 verification.

Phase P4 — token-vend-pat (REQ-336..REQ-344, REQ-340, REQ-341) [was P4+P5]

Goal: nova-idp-token-vend Lambda (KMS-signed OIDC, kyverno-json ABAC), JWKS endpoint, PAT lifecycle, nova auth commands, and nova idp setup (folded from P5 per C-2.1). CAP-037 + CAP-038 target. Highest-risk phase — critical-path. The kj-binary spike (Wave 1) is the single highest-probability schedule slip; Fargate fallback adds ~1 week (D-227). This is a double-length phase (8 waves).

Exit criterion: CAP-037 + CAP-038 Verified + nova idp setup --check/--apply/--verify works against a fresh AWS account.

Wave 1 — kj-binary spike (backend-engineer + security-engineer) [C-8.2]

  • Task 1.1 (backend-engineer): confirm the kj Go binary (~40 MB Linux amd64) runs in the Lambda Python 3.12 runtime on AL2023. Bundle it in the nova-cli layer (wget a pinned release (e.g. kj@v1.x.y) + record SHA256 into layer/kj.sha256 — C-8.2, supply-chain safety) into layer/bin/kj, chmod +x. Verify KyvernoJsonEngine.is_configured() finds /opt/bin/kj. If this fails: fall back to Fargate for the token-vend Lambda (D-227 risk, RESEARCH §7). Escalate to user only if both fail (full autonomy: log assumption + proceed with Fargate).

Wave 2 — ABAC policy (security-engineer) [C-5.1]

  • Task 2.1 (security-engineer): platform/abac/token-vend.policy — kyverno-json ValidatingPolicy (D-227). Payload: {subject, requested_claims, target_resource, environment, pat_jti, policy_version}. requested_claims = list of claim names (the policy asserts the subject is allowed to request those claims; the values are assigned by the Lambda, not the requestor — C-5.1). JMESPath checks for role/scope/env/owner. Severity critical = deny on fail.
  • Task 2.2 (security-engineer): policy_version = git SHA of the policy file, baked into the Lambda layer (D-231). Recorded in every token.vend.allowed/denied audit event.

Wave 3 — KMS signing (security-engineer) [C-1.1]

  • Task 3.1 (security-engineer): verify KMS asymmetric key support before implementation: aws kms create-key --key-spec ECC_NIST_P256 --key-usage SIGN_VERIFY in the target account (C-1.1). If fail: fall back to RSA-2048 (also supported, larger tokens) or escalate. Do not discover this mid-Wave.
  • Task 3.2 (security-engineer): KMS key alias/nova-oidc-signing (ECC_NIST_P256, SIGN_VERIFY). Token-vend Lambda signs via kms.sign(SigningAlgorithm="ECDSA_SHA_256") → DER→raw ECDSA conversion (decode_dss_signaturer.to_bytes(32) + s.to_bytes(32), RESEARCH §5). JWT header {"alg":"ES256","typ":"JWT","kid":"..."}.

Wave 4 — token-vend Lambda (backend-engineer + security-engineer) [C-6.1/C-7.1 ABAC FAIL-CLOSED]

  • Task 4.1 (backend-engineer): core/lambda/nova_idp_token_vend.py — accepts PAT/session, validates revocation (nova-pats.GetItem(jti, ConsistentRead=True) — D-229), evaluates ABAC (Wave 2), signs (Wave 3), returns OIDC JWT. Audit at every step.
  • Task 4.2 (security-engineer): token claims sub, aud, iss, exp, iat, jti, roles (REQ-336).
  • Task 4.3 (security-engineer) 🔴: ABAC fail-closed. If KyvernoJsonEngine.is_configured() returns false or evaluate() raises, return 403 + audit token.vend.denied (reason: abac_eval_failed). Never fail open. This is INV-17's runtime enforcement (C-6.1/C-7.1 — the grill's #1 finding). Test: tests/test_abac_fail_closed.py — mock kj absent → assert 403 + audit event.

Wave 5 — JWKS endpoint (backend-engineer)

  • Task 5.1 (backend-engineer): core/lambda/nova_idp_jwks.py — function URL AuthType: NONE, Cache-Control: max-age=3600. kms.get_public_key → DER SPKI → JWK via cryptography. Returns {"keys":[...]}. Custom domain + WAF = optional (D-230).

Wave 6 — PAT lifecycle (security-engineer + cli-engineer) [C-7.3]

  • Task 6.1 (security-engineer): PAT issuance — signed JWT (typ: "developer_pat", INV-14), nova-pats PutItem (jti, pat_hash, status=active). Only hash stored (REQ-343). Revoked PATs retained. Max TTL: ≤24h for developer PATs, ≤1h for service-account PATs (C-6.2 threat model).
  • Task 6.2 (cli-engineer): nova/auth/{login,revoke,status}.pynova auth login (session→OIDC token, store in ~/.nova/credentials.json 0600), nova auth revoke --pat <jti>, nova auth status (active credential, mode, selection_reason). All emit audit events (REQ-344). C-7.3: ~/.nova/credentials.json stores the OIDC token + PAT metadata (jti, exp, type) ONLY — NOT the raw PAT. The raw PAT is entered once at nova auth login and not persisted (reduces filesystem-compromise blast radius).

Wave 7 — CAP-037/038 (security-engineer)

  • Task 7.1 (security-engineer): tests/test_kms_roundtrip.py (REQ-350, CAP-037) — sign test JWT via token-vend, fetch JWKS, verify with pyjwt. tests/test_pat_revocation.py (REQ-351, CAP-038) — issue → vend → revoke → assert 403 within 60s P95.

Wave 8 — nova idp setup (cli-engineer + backend-engineer) [folded from P5 per C-2.1]

Goal: nova idp setup command with --check/--apply/--verify modes; CloudFormation template generation + review (REQ-340, REQ-341).

  • Task 8.1 (backend-engineer): nova/idp/setup.py (+ backend helper) — generates the Nova-idp CloudFormation template (raw dict → JSON): 2-3 Lambdas, 4 DDB tables, KMS key, function URLs, IAM roles, optional CloudFront/WAF/ACM (--public-jwks-domain flag).
  • Task 8.2 (cli-engineer): --check (prerequisites + IAM policy delta), --apply (generate → $PAGERy/Ncloudformation deploy --capabilities CAPABILITY_IAM, NFR-10), --dry-run (resource list only), --verify (KMS round-trip, delegates to REQ-350 test).
  • Task 8.3 (backend-engineer): IAM policy delta computation — compares current nova-spike-runner grants to required cloudformation:* + codeartifact:* + kms:* + lambda:* + dynamodb:* + ssm:*.

Phase P5 — docs-integration (REQ-345..REQ-351)

Goal: Operator guide, developer guide, threat model; E2E integration test; property tests; KMS round-trip; PAT revocation SLO.

Exit criterion: all REQ-345..351 tests pass + docs published + threat model reviewed.

Wave 1 — docs (lead-developer + security-engineer) [C-6.2/C-6.3/C-9.2]

  • Task 1.1 (lead-developer): docs/operator-guide-idp.md (REQ-345) — nova idp setup --check/--apply/--verify, prerequisite IAM policy, CloudFormation review flow. C-6.3 additions: KMS key rotation procedure (90 days), Lambda layer update procedure, DDB PITR restore procedure, emergency PAT revocation (DDB-level, not CLI).
  • Task 1.2 (lead-developer): docs/developer-guide-auth.md (REQ-346) — signup, signin, login, mode resolution, TTY vs piped stdout behavior, JWS-from-PAT KDF (P2 Wave 2 Task 2.3).
  • Task 1.3 (security-engineer): docs/threat-model.md (REQ-347) — Argon2id storage, KMS signing, JWKS exposure, PAT revocation SLO, ABAC token vending, no-AWS-managed-identity (INV-15), DER→raw ECDSA gotcha. C-6.2 additions: (a) JWKS unauthenticated endpoint DDoS surface + reserved-concurrency mitigation; (b) PAT theft + max TTL (≤24h dev, ≤1h service-account); (c) ABAC fail-closed guarantee (C-6.1). C-9.2 addition: INV-18..21 compression audit — verify the spec's attestation invariant semantics are fully captured by INV-15/16/17 + REQ-332.

Wave 2 — integration tests (backend-engineer + security-engineer)

  • Task 2.1 (backend-engineer): tests/test_e2e_idp.py (REQ-348) — sign-up → sign-in → token-vend → apply → audit. Verifiable audit chain. Runs in CI against deployed Nova-idp.
  • Task 2.2 (security-engineer): verify REQ-349 (mode_resolver property tests, P1 Wave 3 Task 3.3) + REQ-350 (KMS round-trip, P4 Wave 7 Task 7.1) + REQ-351 (PAT revocation SLO, P4 Wave 7 Task 7.1) pass in CI.

Phase P6 — final-review-ship (Final Phase)

Goal: Multi-persona code review across P1..P5; project-health audit; milestone ship to main; CAP-033..038 Verified.

Wave 1 — review (lead-developer)

  • Task 1.1 (lead-developer): ciagent-review across all phases. Auto-fix P0; flag P1+ for post-hoc. If P1+ found, fix in this phase.

Wave 2 — audit (lead-developer)

  • Task 2.1 (lead-developer): ciagent-audit — reconstruction test (git log ↔ .ciagent/), file/branch/commit discipline. Fix critical issues in this phase.

Wave 3 — milestone ship (lead-developer)

  • Task 3.1 (lead-developer): ciagent-ship — merge phase/06milestone/v1.28-cli-identitymain; tag v1.27.6 (= the v1.28 release); Gitea release with full milestone summary; delete all milestone branches. Update REQUIREMENTS.md (mark REQ-323..353 complete), ROADMAP.md (mark v1.28 complete), STATE.md (append CAP-033..038 + INV-12..17), NORTH_STAR.md.

User-Facing Surface

MVP/UX CHECK §1 (REQ-MVP-UX-001).

  1. CLI flag: nova --help lists every subcommand; nova init scaffolds a project; nova auth login authenticates; nova apply --local runs locally; nova idp setup deploys the identity stack.
  2. README quickstart: docs/developer-guide-auth.md (REQ-346) documents signup → signin → login → nova apply in a quickstart.
  3. .feature Scenario: tests/test_e2e_idp.py (REQ-348) is the E2E happy path (sign-up → sign-in → token-vend → apply → audit).

Happy Path

MVP/UX CHECK §2 (REQ-MVP-UX-001). End-to-end scenario written BEFORE execute.

Journey 2 — Dev authenticates and deploys locally:

  1. nova auth signupnova-idp-auth Lambda → Argon2id hash → nova-users PutItem → session token.
  2. nova auth signinnova-idp-auth → Argon2id verify → session.
  3. nova auth loginnova-idp-token-vend (exchanges session for Nova OIDC token; stores in ~/.nova/credentials.json 0600).
  4. nova init in a project dir → .nova/, .gitignore, .nova/contract.yml.attestations/.
  5. nova apply --local --sign-local-reviewcore.env.synthesize_local_env()core.contract_resolver.resolve() → JWS attestation signed with a key derived from the PAT → local ledger entry.

The E2E test (tests/test_e2e_idp.py, REQ-348) verifies this chain + the audit event chain in CI against a deployed Nova-idp.

UX Acceptance Criteria

MVP/UX CHECK §3 (REQ-MVP-UX-001).

  1. nova --help exits 0 and lists a subcommand for every core/ module (CAP-033).
  2. nova init in an empty dir creates .nova/, .nova/contract.yml.attestations/, .gitignore (secrets excluded).
  3. nova auth login at a TTY resolves mode=interactive, selection_reason=credential:developer_pat (INV-12, INV-14).
  4. nova apply --local produces a JWS attestation verifiable with the public key derived from the PAT (REQ-332).
  5. nova idp setup --check reports prerequisites + IAM policy delta; --apply presents the CloudFormation template for review before any resource is created (NFR-10); --verify confirms the KMS round-trip.
  6. The Forge action (nova cli-action) runs nova apply in mode=agent, selection_reason=credential:service_account_pat with no TTY dependency (Journey 3, INV-12).
  7. PAT revocation takes effect within 60s P95 (NFR-4, CAP-038).

Capability gate (CAP-033..CAP-038)

CAP Name Phase Gate rule
CAP-033 CLI subcommand surface exists P1 nova --help lists a subcommand for every core/ module
CAP-034 Subcommand delegates to core/ P1 Every nova/<module>.py ≤50 lines, no business logic, AST scan
CAP-035 Layer matches wheel P1 Lambda layer ARN version matches nova-cli wheel version (SSM mapping)
CAP-036 Nova-idp auth flow works P3 E2E test (sign-up → sign-in → session) passes in CI
CAP-037 Token-vend signs via KMS P4 KMS round-trip test (REQ-350) passes in CI
CAP-038 PAT issuance + revocation P4 Issue → vend → revoke → 403 within 60s P95 (REQ-351) passes in CI
Release gate (§6 of the spec): CAP-001..CAP-032 remain Verified;
CAP-033..CAP-038 are Verified; all v1.28 release-gate criteria met.

Test evidence required for v1.28 release

  • Code coverage ≥ 80% on new modules (mode_resolver.py, nova-idp-auth, nova-idp-token-vend, PAT lifecycle).
  • CI/CD pipeline GREEN: wheel + Lambda layer publish on every merge (REQ-323, CAP-035).
  • QA sign-off: all four happy-path journeys (J1J4) pass integration tests in CI.
  • Security/compliance review: threat model published, Argon2id verified, ABAC policy reviewed.
  • Capability gate GREEN: CAP-001..032 remain Verified; CAP-033..038 Verified.
  • Mode resolver property tests pass (all four priority levels + edge cases; REQ-349).
  • KMS round-trip test passes against deployed JWKS (REQ-350).
  • PAT revocation SLO verified: ≤60s P95 in CI (REQ-351, NFR-4).
  • Operator + developer guides published.
  • nova idp setup succeeds in a fresh AWS account.
  • Byte-identical Forge action on GitHub + Gitea (REQ-326, NFR-11).

Plan completeness checklist

  • Every REQ-323..353 mapped to a phase + wave + task.
  • Every CAP-033..038 mapped to a phase + gate rule.
  • Every INV-12..17 referenced in persona constraints.
  • Every D-226..231 referenced in task rationale.
  • Vertical slices: each phase ships independently (P1 CLI substrate is useful before P2 packaging; P2 before P3 auth; etc.).
  • Wave ordering within phases (no wave N+1 depends on wave N work in the same phase).
  • Persona assignments per task (4 active personas).
  • MVP/UX CHECK: 3 sections present (User-Facing Surface, Happy Path, UX Acceptance Criteria).
  • Highest-risk item flagged (P4 Wave 1 kj-binary spike).
  • Grill conditions applied (3 critical + 16 tracked; see GRILL.md).

Cost envelope (C-3.1)

Monthly estimate for the default (no CloudFront) Nova-idp deployment in account 581513795199:

Resource Quantity Pricing Est. monthly
DynamoDB (on-demand) 4 tables $1.25/1M write, $0.25/1M read ~$1 (pilot volume)
DynamoDB PITR 4 tables $0.20/GB-month ~$1 (small tables)
KMS asymmetric key 1 key $1/key-month + $0.03/10k signs ~$1
Lambda invocations 3 Lambdas $0.20/1M req + $0.000016/GB-s ~$2 (low volume)
Lambda layer storage ~50 MB $0.02/GB-month <$1
CodeArtifact 1 domain + 1 repo $1/domain + $1/repo $2
SSM Parameter 1 $0.05/param (advanced) <$1
Total (default) ~$9/month

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.


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-<sha>) 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-<sha> (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-<kj-source-sha> (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/06milestone/v1.29-reposplit-identitymain; 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/06milestone/v1.29-reposplit-identitymain; 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

  • Every REQ mapped to a phase + wave + task.
  • Covered-reference REQs identified + their verification surface documented (operator guide P4, cutover gates).
  • Decisions D-232..240 referenced in the plan.
  • Invariants + NFR constraints referenced (KJ-STATIC, KJ-LOCKSTEP, INV-18, etc.).
  • Personas assigned to every task (lead-developer, backend-engineer, security-engineer, cli-engineer, data-engineer).
  • User-Facing Surface section (3 surfaces named).
  • Happy Path section (M1.5 8-item spike, written before execute).
  • UX Acceptance Criteria section (M1, M1.5, M2 gates).
  • Test evidence checklist.
  • Phase boundaries + tags (v1.28.0 → v1.28.6).
  • 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).