# 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 `; 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/.py` via `pkgutil.iter_modules`, dispatches, emits `cli.invocation` audit event stub with INV-12 fields). - **Task 2.2** (cli-engineer): `nova/.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.py` — `resolve_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.py` — `hypothesis` 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 `+` 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_signature` → `r.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}.py` — `nova auth login` (session→OIDC token, store in `~/.nova/credentials.json` 0600), `nova auth revoke --pat `, `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 → `$PAGER` → `y/N` → `cloudformation 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/06` → `milestone/v1.28-cli-identity` → `main`; 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 signup` → `nova-idp-auth` Lambda → Argon2id hash → `nova-users` PutItem → session token. 2. `nova auth signin` → `nova-idp-auth` → Argon2id verify → session. 3. `nova auth login` → `nova-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-review` → `core.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/.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 (J1–J4) 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 - [x] Every REQ-323..353 mapped to a phase + wave + task. - [x] Every CAP-033..038 mapped to a phase + gate rule. - [x] Every INV-12..17 referenced in persona constraints. - [x] Every D-226..231 referenced in task rationale. - [x] Vertical slices: each phase ships independently (P1 CLI substrate is useful before P2 packaging; P2 before P3 auth; etc.). - [x] Wave ordering within phases (no wave N+1 depends on wave N work in the same phase). - [x] Persona assignments per task (4 active personas). - [x] MVP/UX CHECK: 3 sections present (User-Facing Surface, Happy Path, UX Acceptance Criteria). - [x] Highest-risk item flagged (P4 Wave 1 kj-binary spike). - [x] 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-`) 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).