# Nova — Requirements > **Compressed.** The full v1.0–v1.25 requirement history (REQ-01..REQ-309) > is preserved verbatim at `.ciagent/archive/REQUIREMENTS-v1.0-v1.24.md`. > This file retains only the v1.25 requirement set (the immediate > predecessor milestone whose policy-engine substrate is load-bearing for > v1.26) + a pointer to the active v1.26 requirements, which live in the > consumer subproject at `.ciagent/nova-blockchain-exchange/REQUIREMENTS.md` > (multi-project mode per `config.json`). > > Earlier requirement sets (v1.0–v1.24, REQ-01..REQ-290) remain valid for > the milestones they governed. They are not re-decided by v1.26. Full > text in the archive snapshot + git history. ## v1.25 — kyverno-json Unified Policy Engine (immediate predecessor, complete) > **Feature milestone — complete.** `kyverno-json` becomes the primary > compliance / policy tool, implemented behind a swappable `PolicyEngine` > adapter so OPA (or any other engine) can replace it one day. Tags run > on the **v1.24.x** line (milestone v1.25 → tags v1.24.0..v1.24.5). Tag > `v1.24.5` = the milestone release. > > One problem, one architectural correction: > 1. **Fragmented policy posture.** Nova's compliance rules were split > across Checkov (imperative YAML + a Python custom rule for tagging), > Wiz (API findings), the K8s-only Kyverno adapter (inactive for > Terraform stacks — D-053), and imperative Python in > `core/env_transition.py` + `core/regression_verify.py`. There was no > single declarative place where "what Nova considers compliant" lived. > > The correction: `kyverno-json` (a Kyverno-ecosystem runtime that applies > Kyverno policies to **any** JSON/YAML payload) becomes the **unified > orchestrator** of compliance checks. Checkov and Wiz remain as > raw-finding adapters feeding *into* kyverno-json meta-policies. The > engine is behind a `PolicyEngine` protocol so it is replaceable. The > confidence signal is untouched — it already consumes > `list[PolicyCheckResult]` engine-agnostically. ### Decisions (locked in CLARIFY, full autonomy — load-bearing for v1.26) - **D-115 (C-1):** `kyverno-json` is a runtime dependency installed via `go install github.com/kyverno/kyverno-json/cmd/kj@latest` (pinned in a `scripts/install-kyverno-json.sh` helper; the CI image installs it). Not a Python package — kyverno-json is a Go binary. The `KyvernoJsonEngine.is_configured()` checks `which kj` and skips gracefully when absent (emits `SKIPPED` PCR, mirroring the Wiz adapter). - **D-116 (C-2):** kyverno-json PCR records carry `engine: "kyverno"` (no new enum value). The existing `engine` enum in `schemas/policy_check_result.schema.json` already includes `"kyverno"`; adding `"kyverno-json"` would force a schema change + checkov_adapter test regression for no semantic gain. The `ruleId` prefix `KJ_` distinguishes kyverno-json rules from the K8s Kyverno adapter's `KYVERNO_` prefix where they overlap. - **D-117 (C-3):** Checkov and Wiz adapters keep their current `adapt() -> list[PolicyCheckResult]` signatures. They emit PCRs as today. The meta-policies in `adapters/kyverno-json/policies/meta/` consume the **merged** PCR list (checkov + wiz + kyverno-json) as their input payload, applying Nova-specific posture rules on top. No adapter signature changes. - **D-118 (C-4):** `NOVA_TAG_NAMING` (the Checkov custom rule in `adapters/terraform/policy/custom_rules/nova_tagging.py`) is **kept**. A kyverno-json mirror policy `require-tagging-standard.json` is added in `adapters/kyverno-json/policies/stack-ir/`. The P3 meta-policy `tagging-rules-agree.json` asserts the two engines agree on every resource; divergence emits an `error` PCR (defense-in-depth against rule drift). The Checkov rule stays the source of truth for Terraform-static scanning; the kyverno-json policy covers Stack IR. ### Category: Policy Engine Core (feat) - **REQ-291:** `core/policy_engine.py` defines a `PolicyEngine` Python `Protocol` (PEP 544) with three members: `name -> str`, `is_configured() -> bool`, and `evaluate(payload: dict | str, policy_dir: Path, contract_id: str) -> list[dict]` (where each dict conforms to `schemas/policy_check_result.schema.json`). A `PolicyEngineRegistry` singleton selects the active engine from `config.json`'s new `policy.engine` key (default `"kyverno-json"`); raises `KeyError` on an unknown engine name. The registry exposes `get_engine()` and `register(name, factory)`. Pure stdlib, no engine imports at the protocol layer. - **REQ-292:** `.ciagent/config.json` gains a new top-level `policy` object: `{"engine": "kyverno-json", "policy_root": "adapters/kyverno-json/policies"}`. The registry reads `policy.engine` to select the active engine and `policy.policy_root` as the default policy directory. Backward-compatible: if the `policy` key is absent, the registry returns a `NullEngine` that emits only `SKIPPED` records (so existing tests that don't set the key still pass). ### Category: kyverno-json Engine Adapter (feat) - **REQ-293:** `adapters/kyverno-json/kyverno_json_engine.py` implements `KyvernoJsonEngine` satisfying the `PolicyEngine` protocol. `is_configured()` returns `True` when `which kj` succeeds. `evaluate()` writes the payload to a temp JSON file, invokes `kj scan --policy --payload -o json`, parses the native result list, and translates each entry to a PCR dict (`engine: "kyverno"`, `ruleId` prefixed `KJ_`, severity mapped, `result` mapped pass/fail/skip → pass/fail/skipped). When `is_configured()` is false, `evaluate()` returns a single `SKIPPED` PCR with `ruleId: "KJ_ENGINE_NOT_CONFIGURED"`. Native output parsing is defensive: any kyverno-json output that doesn't match the expected shape produces an `error` PCR, never an exception. - **REQ-294:** `adapters/kyverno-json/__init__.py` exports `KyvernoJsonEngine`. `adapters/kyverno-json/policies/_smoke.json` is a single trivial policy (`require-contract-id`) used to validate the engine round-trip end-to-end in tests. `scripts/install-kyverno-json.sh` runs `go install github.com/kyverno/kyverno-json/cmd/kj@latest` and prints `kj version`; documented in `adapters/kyverno-json/README.md`. The CI image installs Go + kj when `policy.engine == "kyverno-json"`; the install is cached. ### Category: Contract Policies (feat) - **REQ-295:** `adapters/kyverno-json/policies/contract/` holds kyverno-json policies over consumer contract JSON. Four policies mirroring `schemas/contract.schema.json` constraints: `require-id-pattern.json`, `require-env-in-enum.json`, `require-infrastructure-min-1.json`, `forbid-unknown-fields.json`. Each policy is a single Kyverno `Policy` resource with one `validate.assert` rule using JMESPath against the payload root. - **REQ-296:** `core/contract_resolver.py` invokes the `PolicyEngineRegistry.get_engine().evaluate()` with the contract dict and `policies/contract/` **before** resolving (early-fail on contract violations) and emits a `nova.policy.evaluated` metrics event. Failures feed the confidence signal's `policy` input as `fail` PCRs; the resolver does not exit — the confidence signal decides the gate (consistent with the existing `--soft-fail` Checkov pattern). ### Category: Stack-IR Policies (feat) - **REQ-297:** `adapters/kyverno-json/policies/stack-ir/` holds policies over the resolved Target Stack IR dict. `require-tagging-standard.json` (every resource carries `nova:owner` + `nova:environment` tags — ports `nova_tagging.py` into a declarative Kyverno policy). `forbid-public-ingress.json` (no resource has `public_ingress: true`). `require-encryption-by-default.json` (every S3 bucket + EBS volume + KMS-aliased resource carries encryption config — ports the v1.8 D-encryption-default rule). - **REQ-298:** `core/contract_resolver.py` invokes the engine with the resolved Stack IR and `policies/stack-ir/` **after** resolving. The resulting PCRs are appended to the contract-policy PCRs and fed to the confidence signal. The resolver's existing `tests/test_contract_resolver.py` continues to pass (the policy call is additive — it does not change resolver return values or exceptions). - **REQ-299:** `tests/test_stack_ir_policies.py` + fixture `tests/fixtures/stack_ir/` — a passing IR + a failing IR. Tests run the `KyvernoJsonEngine` against real `kj` when `which kj` succeeds, and `pytest.skip("kj not installed")` when absent. ### Category: Plan-JSON Policies + Pipeline Wiring (feat) - **REQ-300:** `adapters/kyverno-json/policies/plan-json/` holds policies over `terraform show -json` output. `forbid-plaintext-secrets.json` (ports `CKV_AWS_41/45/46`). `forbid-iam-wildcard.json` (ports `CKV_AWS_1/40`). `require-kms-reference.json` (ports `CKV_AWS_7/33`). The Checkov `RULE_MAP` in `checkov_adapter.py` is unchanged — these are declarative mirrors, not replacements. - **REQ-301:** `run_platform.sh` Step 5 ("runtime policy scan") gains a parallel kyverno-json pass: after Checkov/Wiz produce raw PCRs, the script runs `kj scan` and pipes through `adapters/kyverno-json/kyverno_json_engine.py` to produce a second PCR list. Both lists are concatenated and fed to the confidence signal's `policy` input. When `which kj` is false, the script logs and proceeds with the Checkov/Wiz list only (no hard failure). - **REQ-302:** `tests/test_plan_json_policies.py` + fixture `tests/fixtures/plan_json/` — a passing + failing plan JSON. `tests/test_run_platform_plan_json_policies.py` asserts `run_platform.sh` has the kyverno-json Step 5 block and that it concatenates PCR lists. ### Category: Meta-Policies (feat) - **REQ-303:** `adapters/kyverno-json/policies/meta/` holds policies whose **payload** is the merged `list[PolicyCheckResult]` itself. `block-on-any-critical.json` — asserts no PCR in the list has `severity: "critical"` + `result: "fail"`; if any does, the meta-policy emits a `fail` PCR with `ruleId: "KJ_META_BLOCK_CRITICAL"` and severity `critical`. This is the **declarative** source of truth for "critical = block"; the `confidence_signal.py` `PENALTY["critical"]: None` hard-override stays as defense-in-depth. `tagging-rules-agree.json` — for every resource in the Stack IR, asserts the Checkov `NOVA_TAG_NAMING` result and the kyverno-json `KJ_REQUIRE_TAGGING_STANDARD` result agree; divergence emits an `error` PCR. `tests/test_meta_policies.py` covers both. ### Category: Regression-Gate Policies (feat, quality improvement from IDEATE) - **REQ-304:** `adapters/kyverno-json/policies/regression/` holds policies over the capability-inventory JSON frontmatter. Three policies port the imperative checks in `core/regression_verify.py`: `cap-013-adapter-dedup.json`, `cap-023-metrics-collector.json`, `cap-024-deck-structure.json`. The existing `core/regression_verify.py` is **kept** (it drives the CI gate); the policies are the **declarative mirror** that makes capability regression auditable as a policy artifact, not imperative Python. Future milestones may switch the gate to the policy version. - **REQ-305:** `tests/test_regression_policies.py` + fixture `tests/fixtures/capability_inventory.json` — a clean inventory (all caps pass) + a drifted inventory. The regression gate (`pytest` suite) continues to pass; the new policy tests are additive. ### Category: Documentation (docs) - **REQ-306:** `adapters/README.md` gains a new row for the `kyverno-json` adapter + a new section "Policy Engine Protocol" documenting the `PolicyEngine` Protocol, the registry, and the swap boundary (how to add an `OpaEngine`). `adapters/kyverno-json/README.md` documents the engine, the install path, the policy directory layout, and the four policy categories. - **REQ-307:** `.ciagent/ARCHITECTURE.md` gains §12.7 "Policy Engine Registry" with the registry diagram. `schemas/README.md` notes the `engine: "kyverno"` value is shared by the K8s Kyverno adapter and the kyverno-json engine (distinguished by `ruleId` prefix). `modules/STANDARDS.md` gains a "Policy authoring standard" section. `docs/METRICS.md` notes the policy engine is now swappable (Strategic Objective #2 — provable trust via a replaceable substrate, not a vendor lock-in). ### Category: Tests (test) - **REQ-308:** `tests/test_policy_engine.py` — protocol conformance, unknown-engine `KeyError`, `NullEngine` fallback when the `policy` key is absent, `KyvernoJsonEngine.is_configured()` returns false when `which kj` fails (mocked). `tests/test_kyverno_json_engine.py` — `evaluate()` returns valid PCR dicts validated against `schemas/policy_check_result.schema.json`; native-output parsing is defensive (malformed → `error` PCR, not exception); `is_configured()==false` → `SKIPPED` PCR with `KJ_ENGINE_NOT_CONFIGURED`. - **REQ-309:** All new tests use `pytest.skip("kj not installed")` when `which kj` is absent, so the suite passes in environments without the binary (CI matrix: with-kj and without-kj). `pyproject.toml` + `requirements-test.txt` unchanged (kyverno-json is a Go binary, not a Python dep). ### Out of Scope (v1.25) - **Removing Checkov or Wiz.** Both stay as raw-finding adapters. - **`OpaEngine` implementation.** The protocol is the swap boundary; the OPA implementation is a future milestone. - **Per-module policies.** `modules//policies/` is documented as the future pattern in `modules/STANDARDS.md` but not populated this milestone. - **kyverno-json as a long-running service.** v1.25 uses the CLI (`kj scan`); the `kj serve` web-app mode is future. - **Replacing the K8s Kyverno adapter.** The K8s adapter (`adapters/kyverno/`) remains documentation-only (D-053). ### v1.25 Traceability | REQ | Phase | Status | |-----|-------|--------| | REQ-291 | P1 | complete | | REQ-292 | P1 | complete | | REQ-293 | P1 | complete | | REQ-294 | P1 | complete | | REQ-295 | P2 | complete | | REQ-296 | P2 | complete | | REQ-297 | P2 | complete | | REQ-298 | P2 | complete | | REQ-299 | P2 | complete | | REQ-300 | P3 | complete | | REQ-301 | P3 | complete | | REQ-302 | P3 | complete | | REQ-303 | P3 | complete | | REQ-304 | P4 | complete | | REQ-305 | P4 | complete | | REQ-306 | P4 | complete | | REQ-307 | P4 | complete | | REQ-308 | P1 | complete | | REQ-309 | P1 | complete | ## v1.26 — Live Pilot Estate Activation (active) > **Feature milestone.** The first real consumer estate (a stock > exchange on a homegrown PoA blockchain, equities only) is activated > against live AWS account `581513795199`, lifting D-096. Tags run on > the **v1.25.x** line: `v1.25.0` (P0) → `v1.25.1..v1.25.4` (P1–P4) → > `v1.25.5` (P5 final = milestone release). > > **Multi-project mode:** the v1.26 requirements live in > `.ciagent/nova-blockchain-exchange/REQUIREMENTS.md` (the consumer > subproject). The platform-side requirement REQ-322 (DynamoDB L1 > primitive) landed in P2 of the platform repo. The 13 requirements > (REQ-310..322) cover: blockchain core (REQ-310), order engine > (REQ-311), settlement (REQ-312), consumer contract (REQ-313), > deploy invocation (REQ-314), settlement-finality policy (REQ-315), > pilot regression CAP (REQ-316), outcome backfill (REQ-317), > escalation reason (REQ-318), env-JSON wiring (REQ-319), > pilot-readiness policy (REQ-320), docs (REQ-321), DynamoDB L1 > primitive (REQ-322). ### v1.26 Traceability (live — see CHECKPOINT.json for authoritative state) | REQ | Phase | Status | |-----|-------|--------| | REQ-310 | P1 | complete (v1.25.1) | | REQ-311 | P1 | complete (v1.25.1) | | REQ-312 | P1 | complete (v1.25.1) | | REQ-322 | P2 | complete (v1.25.2) | | REQ-313 | P2 | complete (v1.25.2) | | REQ-314 | P2 | complete (v1.25.2) | | REQ-315 | P3 | complete (v1.25.3) | | REQ-316 | P3 + P4 | complete (v1.25.3 — CAP-025; v1.25.4 — live-verify complete) | | REQ-317 | P3 | complete (v1.25.3) | | REQ-318 | P3 | complete (v1.25.3) | | REQ-319 | P3 | complete (v1.25.3) | | REQ-320 | P3 | complete (v1.25.3) | | REQ-321 | P4 | complete (v1.25.4) | Full v1.26 requirement text: `.ciagent/nova-blockchain-exchange/REQUIREMENTS.md`. Active phase plan: `.ciagent/PLAN.md`. ## v1.28 — CLI Canonicalization + Identity Layer (complete, tag `v1.27.6`, merged to main 2026-08-19) > **Feature milestone — active.** 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. > > Tags run on the **v1.27.x** line: `v1.27.0` (P0) → > `v1.27.1..v1.27.N` → `v1.27.(N+1)` (final = milestone release). > Milestone branch: `milestone/v1.28-cli-identity`. > > **ID re-mapping (no collisions):** the source spec used `REQ-001..031`, > `CAP-025..030`, `INV-63/64/65/18..21/34`, `D-NEW-26/37..41`, and a `kj` > engine — none of which exist in this repo (CAP-025..032 and > INV-1..11 are already allocated to blockchain/pilot work; the policy > engine is kyverno-json, not `kj`). This file uses the re-mapped IDs: > `REQ-323..353`, `CAP-033..038`, `INV-12..17`, `D-226..231`. The 1:1 > mapping is recorded in CLARIFY.md. Decisions D-226..D-231 are authored > in CLARIFY (full autonomy) — they are not pre-existing "locked inputs". ### Decisions (locked in CLARIFY — full autonomy, load-bearing for v1.28) - **D-226 (Mode resolution priority):** flag → env (`NOVA_CLIENT_MODE`) → credential type → TTY heuristic. Invalid env values are ignored + warned, falling through to credential type. No silent fallbacks (NFR-1). - **D-227 (ABAC engine = kyverno-json):** the token-vend Lambda uses the existing kyverno-json engine (INV-4 swappable) as the ABAC evaluator, not a new `kj` engine. Policy at `platform/abac/token-vend.policy`. - **D-228 (Argon2id in Lambda):** `argon2-cffi` with bundled wheels; if the C extension fails to load, fall back to the pure-Python implementation; if both fail, document the Fargate migration path. - **D-229 (PAT revocation SLO):** strongly-consistent DynamoDB read on every token-vend request; revocation takes effect within 60s P95 (NFR-4). - **D-230 (JWKS endpoint):** Lambda function URL behind a custom domain; rate limiting at the DNS/CDN layer. API Gateway migration deferred to v1.19+ if throttling requirements grow. - **D-231 (ABAC policy ownership + versioning):** Platform Security owns `platform/abac/token-vend.policy`; changes require PR review; the policy version (git SHA) is recorded in every token-vend audit event. ### P1 — CLI Substrate #### REQ-323 — CodeArtifact wheel + Lambda layer pipeline **Journeys:** J3. **Priority:** High. **AC:** Given a merge to `main` affecting `core/`, when CI runs, then both the wheel and the Lambda layer are published to CodeArtifact with identical version strings; if either fails, the merge is rejected. #### REQ-324 — CLI subcommand per `core/` module **Journeys:** J3. **Priority:** High. **AC:** (1) Every module in `core/` has a corresponding `nova/.py` subcommand. (2) Subcommand files are ≤ 50 lines and contain no business logic — they delegate to `core/`. (3) CAP-034 verifies delegation by AST scan. #### REQ-325 — `nova init` scaffolds project **Journeys:** J2. **Priority:** High. **AC:** Given a directory with no `.nova/`, when Dev runs `nova init`, then `.nova/`, `.nova/contract.yml.attestations/`, and `.gitignore` (excluding secrets) are created. #### REQ-326 — `nova cli-action` published **Journeys:** J3. **Priority:** High. **AC:** (1) Action is available on both GitHub and Gitea marketplaces. (2) Integration test verifies byte-identical behavior on both platforms. (3) Python 3.12 is pinned. #### REQ-327 — `mode_resolver.py` priority **Journeys:** J2, J3. **Priority:** High. **AC:** (1) Explicit `--mode=agent|interactive` flag always wins. (2) Otherwise `NOVA_CLIENT_MODE` env var. (3) Otherwise credential type default. (4) Otherwise TTY heuristic. (5) Property tests cover all four levels. (6) INV-13 (mode determinism) enforced at PR time. #### REQ-328 — Audit emission with mode + selection_reason **Journeys:** J3. **Priority:** High. **AC:** Given any CLI invocation, when the CLI runs, then the emitted `cli.invocation` audit event contains `mode`, `selection_reason`, `credential_type`, `command`, and `args`. INV-12 (mode observability) enforced. ### P2 — Lambda Packaging + Identity Layer #### REQ-329 — Dual-use Lambda/CLI import **Journeys:** J2. **Priority:** High. **AC:** Given `core/lambda/contract_ingestor.py`, when imported from the Lambda handler, then it executes the Lambda path; when imported from the CLI, then it executes the local path; and the two paths share ≥ 80% of their code. #### REQ-330 — Local env synthesizer **Journeys:** J2. **Priority:** High. **AC:** Given a contract and a `--local` flag, when `nova apply --local` runs, then a local env is synthesized via `core/env.py:get_env()` without provisioning cloud resources. #### REQ-331 — Attestations directory scaffolded **Journeys:** J2. **Priority:** High. **AC:** Given `nova init` ran, when Dev lists `.nova/contract.yml.attestations/`, then the directory exists and is empty. #### REQ-332 — JWS signing key from PAT **Journeys:** J2. **Priority:** High. **AC:** Given a PAT, when Dev runs `nova apply --local --sign-local-review`, then a JWS attestation is produced; the JWS is HMAC-SHA256 with a key derived from the PAT via `HKDF-SHA256(PAT_bytes, salt='nova-local-attestation', info='jws-signing-key')` → 32-byte symmetric key (C-5.2 grill fix). The verification key is derived from the PAT via the same KDF (the PAT is the shared secret). INV-14..17 (attestation invariants) enforced. #### REQ-333 — `nova-idp-auth` Lambda **Journeys:** J1, J2. **Priority:** High. **AC:** (1) Lambda exposes sign-up, sign-in, and session creation endpoints. (2) Passwords are hashed with Argon2id. (3) Sessions are stored in DynamoDB. (4) CAP-036 verifies end-to-end auth flow. #### REQ-334 — Argon2id password hashing **Journeys:** J1, J2. **Priority:** High. **AC:** Given a sign-up request, when the user record is persisted, then the password is stored as an Argon2id hash; raw passwords never appear in logs, traces, environment variables, or DynamoDB records. #### REQ-335 — DynamoDB tables for identity **Journeys:** J1. **Priority:** High. **AC:** (1) Tables exist: `nova-users`, `nova-sessions`, `nova-password-resets`. (2) Tables are provisioned by `nova idp setup`. (3) Point-in-time recovery is enabled on each. #### REQ-336 — `nova-idp-token-vend` Lambda **Journeys:** J1, J2, J4. **Priority:** High. **AC:** (1) Lambda accepts a PAT (or session token) and returns a KMS-signed OIDC token. (2) Token claims include `sub`, `aud`, `iss`, `exp`, and role claims. (3) ABAC policy is evaluated before signing. #### REQ-337 — KMS-signed OIDC tokens **Journeys:** J1, J4. **Priority:** High. **AC:** (1) Signing key is a KMS asymmetric key (RSA or ECDSA). (2) Token signature is verifiable via the JWKS endpoint. (3) KMS round-trip test passes. CAP-037 verifies. #### REQ-338 — JWKS endpoint as Lambda function URL **Journeys:** J1, J4. **Priority:** High. **AC:** Given the identity stack is deployed, when a client GETs the JWKS URL, then the public key(s) for token verification are returned with `Content-Type: application/json`. #### REQ-339 — kyverno-json ABAC policy file **Journeys:** J1, J4. **Priority:** High. **AC:** (1) Policy at `platform/abac/token-vend.policy`. (2) Policy inputs include subject, requested claims, target resource, and environment. (3) kyverno-json `evaluate` returns allow/deny; the decision is emitted to the audit stream. #### REQ-340 — `nova idp setup` walks admin **Journeys:** J1. **Priority:** High. **AC:** (1) Command supports `--check`, `--apply`, and `--verify` modes. (2) `--check` reports missing prerequisites and the required IAM policy. (3) `--apply` generates a CloudFormation template and requires explicit approval. (4) `--verify` runs the KMS round-trip test. #### REQ-341 — CloudFormation template for review **Journeys:** J1. **Priority:** High. **AC:** Given `nova idp setup --apply`, when the template is generated, then the template is presented for review; resources are not created until the operator approves; `--dry-run` shows the resource list without writing. #### REQ-342 — PAT issuance via portal **Journeys:** J4. **Priority:** High. **AC:** (1) PAT is a signed JWT. (2) PAT hash is stored in DynamoDB. (3) PAT includes a unique `jti` and an expiry claim. (4) Revocation marks the `jti` as revoked. #### REQ-343 — PAT hashes in DynamoDB **Journeys:** J4. **Priority:** High. **AC:** (1) Only the hash (not the raw PAT) is stored. (2) Table supports lookup-by-hash and lookup-by-`jti`. (3) Revoked PATs are retained for audit, not deleted. #### REQ-344 — `nova auth` commands **Journeys:** J2, J4. **Priority:** High. **AC:** (1) `nova auth login` exchanges session → OIDC token, stores locally. (2) `nova auth revoke --pat ` marks a PAT revoked. (3) `nova auth status` shows current credential, mode, and selection_reason. (4) All commands emit audit events. ### P3 — Documentation #### REQ-345 — Operator guide for `nova idp setup` **Priority:** High. **AC:** Guide published covering `--check`, `--apply`, `--verify`, prerequisite IAM policy, and the CloudFormation review flow. #### REQ-346 — Developer guide for `nova auth login` **Priority:** High. **AC:** Guide published covering signup, signin, login, mode resolution, and credential-type behavior at a TTY vs. piped stdout. #### REQ-347 — Identity-layer threat model **Priority:** High. **AC:** Threat model published covering Argon2id storage, KMS signing, JWKS exposure, PAT revocation SLO, ABAC token vending, and the no-AWS- managed-identity constraint (NFR-5). ### P4 — Integration Testing #### REQ-348 — E2E integration test **Priority:** High. **AC:** Given a deployed Nova-idp, when the test runs, then sign-up → sign-in → token-vend → apply → audit completes successfully; the audit event chain is verifiable. #### REQ-349 — Property tests for `mode_resolver` **Priority:** High. **AC:** (1) Property tests cover all four priority levels. (2) Edge cases: TTY but piped stdout, missing credential, conflicting flag/env, invalid env value. (3) INV-13 enforced via test. #### REQ-350 — KMS round-trip test **Priority:** High. **AC:** Given a token signed by the token-vend Lambda, when the test fetches the JWKS and verifies the signature, then verification succeeds. #### REQ-351 — PAT revocation SLO test **Priority:** High. **AC:** Issue PAT → use to vend token → revoke → assert denial within 60s P95. Test passes in CI. ### P5 — Capability Gate #### REQ-352 — CAP-033..038 gate rules wired into CI **Priority:** High. **AC:** (1) CAP-033 (CLI subcommand surface exists): `nova --help` lists a subcommand for every `core/` module. (2) CAP-034 (subcommand delegates to `core/`): every `nova/.py` ≤ 50 lines, no business logic, AST scan. (3) CAP-035 (layer matches wheel): Lambda layer ARN version matches the `nova-cli` wheel version. (4) CAP-036 (Nova-idp auth flow works): E2E test (REQ-348) passes. (5) CAP-037 (token-vend signs via KMS): KMS round-trip (REQ-350) passes. (6) CAP-038 (PAT issuance + revocation): REQ-351 passes. Failure of any → merge blocked. #### REQ-353 — Capability gate GREEN for v1.28 release **Priority:** High. **AC:** CAP-001..CAP-032 remain Verified; CAP-033..CAP-038 are Verified. All v1.28 release-gate criteria in PLAN.md §6 met. ### v1.28 Invariants (new — INV-12..INV-17) - **INV-12 (Mode observability):** Every CLI invocation emits a `cli.invocation` audit event containing `mode`, `selection_reason`, `credential_type`, `command`, and `args`. - **INV-13 (Mode resolution determinism):** Resolution priority is flag → env (`NOVA_CLIENT_MODE`) → credential type → TTY. No silent fallbacks. Deviations rejected at PR time. - **INV-14 (Credential type encodes role):** `developer_pat` / `nova_oidc_token` + TTY present → `interactive`; TTY absent → `agent`. - **INV-15 (No AWS-managed identity in path):** Nova-idp MUST NOT depend on Cognito, IAM Identity Center, or any AWS-managed identity service. - **INV-16 (Password storage):** Passwords hashed with Argon2id; raw passwords never in logs/traces/env/DynamoDB. - **INV-17 (ABAC discipline):** The token-vend Lambda evaluates the kyverno-json ABAC policy before signing; allow/deny + policy inputs emitted to the audit stream. ### v1.28 Traceability (live — see CHECKPOINT.json for authoritative state) | REQ | Phase | Status | |-----|-------|--------| | REQ-323 | P1 | complete (v1.27.1) | | REQ-324 | P1 | complete (v1.27.1) | | REQ-325 | P1 | complete (v1.27.1) | | REQ-326 | P1 | complete (v1.27.1) | | REQ-327 | P1 | complete (v1.27.1) | | REQ-328 | P1 | complete (v1.27.1) | | REQ-329 | P2 | complete (v1.27.2) | | REQ-330 | P2 | complete (v1.27.2) | | REQ-331 | P2 | complete (v1.27.2) | | REQ-332 | P2 | complete (v1.27.2) | | REQ-333 | P3 | complete (v1.27.3) | | REQ-334 | P3 | complete (v1.27.3) | | REQ-335 | P3 | complete (v1.27.3) | | REQ-336 | P4 | complete (v1.27.4) | | REQ-337 | P4 | complete (v1.27.4) | | REQ-338 | P4 | complete (v1.27.4) | | REQ-339 | P4 | complete (v1.27.4) | | REQ-340 | P4 | complete (v1.27.4) | | REQ-341 | P4 | complete (v1.27.4) | | REQ-342 | P4 | complete (v1.27.4) | | REQ-343 | P4 | complete (v1.27.4) | | REQ-344 | P4 | complete (v1.27.4) | | REQ-345 | P5 | complete (v1.27.5) | | REQ-346 | P5 | complete (v1.27.5) | | REQ-347 | P5 | complete (v1.27.5) | | REQ-348 | P5 | complete (v1.27.5) | | REQ-349 | P5 | complete (v1.27.5) | | REQ-350 | P5 | complete (v1.27.5) | | REQ-351 | P5 | complete (v1.27.5) | | REQ-352 | P6 | complete (v1.27.6) | | REQ-353 | P6 | complete (v1.27.6) | --- ## v1.29 — Reposplit + Identity Layer Bring-Live (active, milestone branch `milestone/v1.29-reposplit-identity`) > **Feature milestone — active.** v1.29 extracts all live platform > components into a dedicated Gitea-private Terraform repository > (`nova-platform-ops`), brings Nova-idp live in account `581513795199` > for the first time, and standardizes `acdl/acdl` on GitHub. `kj` (a > compiled Go binary, pinned v0.0.3, distinct from the kyverno-json > engine) has exactly one identity: one ECR image digest shared by the > production Lambda runtime and its defensive Fargate fallback > (KJ-LOCKSTEP, REQ-371). > > Tags run on the **v1.28.x** line: `v1.28.0` (P0) → > `v1.28.1..v1.28.5` (execution) → `v1.28.6` (final = milestone release). > Milestone branch: `milestone/v1.29-reposplit-identity`. > > **Scope split (CLARIFY-grounded, full autonomy):** Terraform module > code is authored out-of-band in `nova-platform-ops`. REQs marked > `[covered-reference]` have their verification surface in the > `nova-platform-ops` cutover gates (M1/M1.5/M2), documented in the > operator guide (`docs/operator-guide-platform-ops.md`). CIAgent in > `acdl` authors only the acdl-side REQs. ### Decisions (locked in CLARIFY — full autonomy, load-bearing for v1.29) - **D-232 (Forge parity abandoned):** the byte-identical-forges CI parity (Gitea + GitHub) is abandoned; `acdl/acdl` standardizes on GitHub. CI fails with `forge_parity_disabled` (deliberate). Rationale: Vision §4 domain boundaries — operations lives in Gitea-private `nova-platform- ops`, engineering lives on GitHub. - **D-233 (JWKS public-read via CloudFront edge):** the JWKS endpoint is the only public read surface of the live platform (INV-18). All other platform endpoints gate with `AuthType: AWS_IAM`. CloudFront + OAC pinning replaces direct Lambda Function URL exposure. - **D-234 (KMS asymmetric key provisioning):** `alias/nova-oidc-signing` provisioned with `KeySpec: ECC_NIST_P256`, `KeyUsage: SIGN_VERIFY`, 90-day rotation cadence (matches per-stack CMK rotation per D-069). - **D-235 (Tag-pin handoff):** engineering hands off to operations via tags. `acdl/acdl` `publish.yml` attaches artifacts to GitHub Releases per tag; `nova-platform-ops` declares `local.nova_platform_version` + `local.kj_source_sha` and resolves substrates through a single `data.aws_ecr_image.kj_image`. - **D-236 (Cutover shape + rollback procedure):** M1 day-0 cutover is conditional on M1.5 verification gate (3 consecutive rebuilds, 12-item spike per grill CF-1). Rollback = revert `nova_platform_version` pin; the prior tag's artifacts remain downloadable. M2a (Fargate toggle) activates only if M1.5 fails 3×. - **D-237 (Fargate sunset discipline):** the always-warm minimal Fargate standby (REQ-363b, ~$15–20/month) may not be deleted unless REQ-363 has been green in production for ≥30 consecutive days. Sunset requires an architecture review. - **D-238 (KJ-LOCKSTEP release-gate invariant):** the ECR image digest running on the Fargate standby MUST equal the digest resolved by `aws_lambda_function.nova_idp_token_vend.image_uri` at every `terraform plan`. Enforced by `lifecycle.precondition` (mechanism) + Gitea Actions `if: steps.plan.outcome == 'success'` (mechanism) + PR comment reporting (observability) + operator review (last, never first). No second pipeline, no second SHA pin. Vision §6 immutability + Vision §5 narrow interfaces. ### P1 — Publish Pipeline #### REQ-354 — `publish.yml` attaches Lambda zip + layer wheel + Python wheel + ECR container image to GitHub Release for each tag **Journeys:** J1, J2 (criteria 3–4). **Priority:** High. **AC:** **(1)** Given a tag `v1.29.x` is pushed to `acdl/acdl` main, when `publish.yml` runs, then the release artifacts `nova-lambda-token-vend- v1.29.x.zip`, `nova-cli-layer-v1.29.x.zip`, and `nova-1.29.x-py3-none- any.whl` appear in GitHub Releases with matching SHA-256 in the body. **(2)** Given two consecutive tags `v1.29.0` and `v1.29.1`, when both releases are queried, then each tag's artifacts are independent and the previous tag's artifacts remain downloadable. **(3)** Given the publish pipeline runs for tag `v1.29.x`, when the image build step executes, then a single ECR image is pushed at tag `v1.29.x-kj-` where `` is read from `platform/abac/kj-version.txt` at build time and embedded in the tag (D-239: ECR tags reject `+`; corrected from `v1.29.x+kj-` to `v1.29.x-kj-`). **(4)** Given the image is pushed, when the GitHub Release body lists artifacts, then the image URI and digest appear alongside the wheel, layer, and Lambda zip. KJ-STATIC: the `kj` binary is compiled `CGO_ENABLED=0 GOOS=linux GOARCH=amd64` and `file(1)` reports `statically linked, no shared library` before embedding. ### P2 — Gitea Scrub + Decisions #### REQ-367 — Hard scrub of all Gitea references in `acdl/acdl` at v1.29.0 **Journeys:** Cross-cutting. **Priority:** Critical. **AC:** **(1)** Given v1.29.0 is cut from main, when `grep -rni gitea .github/ docs/ pyproject.toml README.md .ciagent/` runs, then zero matches outside this spec's archive section. **(2)** Given v1.29.0 ships, when `.gitea/` is checked in the working tree, then `find .gitea` returns nothing. **(3)** Given v1.29.0 ships, when the bit-identical-forges parity is asserted in CI, then CI fails with `forge_parity_disabled` (deliberate; documented in D-232). #### REQ-368 — Decisions D-232..238 recorded in PROJECT.md + CLARIFY **Journeys:** Cross-cutting. **Priority:** High. **AC:** **(1)** Given the milestone is recorded, when loading `PROJECT.md`, then decisions D-232 (forge parity abandoned), D-233 (JWKS public-read via CloudFront edge), D-234 (KMS asymmetric key provisioning), D-235 (tag- pin handoff), D-236 (cutover shape + rollback procedure), D-237 (Fargate sunset discipline ≥30 days → architecture review), D-238 (KJ-LOCKSTEP release-gate invariant) are present with rationale citing Vision §4 domain boundaries. **(2)** Given decisions are present, then each decision references the source statement from the v1.29 spec. ### P3 — CFN Archive + TF Delegation #### REQ-369 — CFN → Terraform conversion of `nova idp setup` **Journeys:** J2. **Priority:** High. **AC:** **(1)** Given the CFN template in `acdl/acdl/nova/idp/setup.py`, when the equivalent Terraform in `nova-platform-ops` runs, then the same resources (Lambdas, DDB tables, IAM roles, KMS key references) are created. [covered-reference: nova-platform-ops] **(2)** Given the conversion, when a new operator runs `nova idp setup --apply`, then the CLI delegates to `terraform apply`; the CFN code path is no longer the active path. **(3)** Given the conversion, the CFN file in `acdl/acdl` is archived to `docs/archive/nova-idp-cfn-v1.28.md` as read-only reference; deletion is a follow-up. ### P4 — Operator Guide + Reference Tracking (docs) #### REQ-OPS-GUIDE — `docs/operator-guide-platform-ops.md` **Journeys:** J2. **Priority:** High. **AC:** Given the operator guide is published, when an operator reads it, then it covers: KMS rotation (90-day cadence, `alias/nova-oidc- signing`), JWKS reachability via CloudFront edge (OAC pinning, public read vs. IAM-gated), PITR restore (DynamoDB point-in-time recovery), PAT revocation (60s SLO), edge configuration (CloudFront + WAF + ACM + Route53), Fargate standby status checks (`GET /health` every 10s, `KJ-WARMUP-HEALTH`), cost section (WAF ~$5–10/month + Fargate ~$15–20/month), artifact-mirror fallback (operator-local mirror by SHA-256 when Gitea `act_runner` cannot reach GitHub Releases), and the M1/M1.5/M2 cutover gates as release-gate entries for the covered- reference REQs. ### P5 — Consumer Deploy Bump (cross-project, Edge 8) #### REQ-CONSUMER-BUMP — `nova-blockchain-exchange` deploy.yml `@v1.25` → `@v1.29` **Journeys:** J1. **Priority:** High. **AC:** **(1)** Given `nova-blockchain-exchange` deploy.yml pins `acdl/.github/workflows/deploy.yml@v1.25`, when the bump is applied, then both `.github/workflows/deploy.yml` and `.gitea/workflows/deploy.yml` reference `@v1.29`. **(2)** Given the bump, when the smoke test runs (sign-up → sign-in → token-vend → apply → audit), then the chain completes successfully against the v1.29 publish artifacts. ### Covered-reference requirements (authored in `nova-platform-ops`, out-of-band) The following REQs are tracked for milestone completeness but their code lands in `nova-platform-ops`. Their verification surface is the M1/M1.5/M2 cutover gates documented in the operator guide. - **REQ-355** — ops repo pins `local.nova_platform_version` + `local.kj_source_sha`; CI resolves matching artifacts + image digest. - **REQ-356** — ops repo CI runs `terraform plan` on every PR; drift fails with `drift_detected`. - **REQ-357** — HITL approver distinct from PR author required for `terraform apply` (INV-3, TFM-HITL). - **REQ-358** — Operator bumps `nova_platform_version` to roll out engineering change; `CodeSha256` matches the artifact SHA-256. - **REQ-359** — ops repo is Gitea-private with no GitHub mirror (OPER-PRIV). - **REQ-360** — ops repo IAM scope is bounded; no AdministratorAccess (IAM-NARROW). - **REQ-361** — Terraform imports existing live resources idempotently (IMPORT-IDEMPOTENT). - **REQ-362** — `alias/nova-oidc-signing` KMS key provisioned (`ECC_NIST_P256`, `SIGN_VERIFY`, 90-day rotation). - **REQ-363** — Nova-idp 3 Lambdas deployed on container image with static `kj` (production substrate, KJ-STATIC). - **REQ-363b** — Fargate defensive fallback — always-warm minimal Fargate standby, **same ECR image** (KJ-LOCKSTEP, KJ-WARMUP-HEALTH). - **REQ-364** — JWKS Function URL reachable only via CloudFront with OAC pinning (INV-18, JWKS-EDGE-ONLY). - **REQ-365** — WAF WebACL rate-limit (3000/5min) + AWS Managed Rules. - **REQ-366** — ACM cert + Route53 alias for the JWKS domain. - **REQ-371** — KJ-LOCKSTEP applied-at-plan mechanism (`lifecycle.precondition` on both image-bearing resources; fail-closed by mechanism, not by discipline). ### v1.29 Invariants + NFR constraints (new) - **INV-18 (JWKS-EDGE-ONLY):** the JWKS endpoint is the only public read surface of the live platform. All other platform endpoints MUST gate with `AuthType: AWS_IAM`. - **KJ-STATIC (NFR):** `kj` compiled `CGO_ENABLED=0`; `file(1)` reports `statically linked, no shared library`; SHA-256 matches `platform/abac/kj-version.txt`; recorded in Terraform state. - **KJ-LOCKSTEP (NFR):** Fargate standby digest == Lambda `image_uri` digest at every `terraform plan`. Detected by `lifecycle.precondition` (mechanism) + CI `if: steps.plan.outcome == 'success'` (mechanism) + PR comment (observability) + operator review (last). No second pipeline, no second SHA pin. - **KJ-WARMUP-HEALTH (NFR):** Fargate standby `READY` probe (`GET /health → 200` every 10s) green before M1 cutover; release-gate entry. - **OPER-PRIV (NFR):** `nova-platform-ops` `private: true`, not mirrored. - **IAM-NARROW (NFR):** Gitea OIDC role bounded per REQ-360; no `Action: "*"` or `Resource: "*"`. - **DRIFT-DETECT (NFR):** `terraform plan` exit 2 (drift) fails the apply workflow; manual reconciliation required. - **IMPORT-IDEMPOTENT (NFR):** re-import exits non-zero with `resource_already_imported`. - **TFM-HITL (NFR):** `terraform apply` against `main` requires Gitea Actions approval from a user distinct from the PR author. - **JWKS-SLO (NFR):** `GET /.well-known/jwks.json` P95 < 200ms same- region; `Cache-Control: max-age=3600` honored. - **JWKS-ROTATION (NFR):** on key rotation, both old + new public keys published during 24-hour overlap window. ### v1.29 Traceability (live — see CHECKPOINT.json for authoritative state) | REQ | Phase | Status | |-----|-------|--------| | REQ-354 | P1 | planned | | REQ-367 | P2 | planned | | REQ-368 | P2 | planned | | REQ-369 | P3 | planned | | REQ-OPS-GUIDE | P4 | planned | | REQ-CONSUMER-BUMP | P5 | planned | | REQ-355 | covered-reference | planned (M1 gate: nova-platform-ops) | | REQ-356 | covered-reference | planned (M1 gate: nova-platform-ops) | | REQ-357 | covered-reference | planned (M1.5 gate: nova-platform-ops) | | REQ-358 | covered-reference | planned (M2 gate: nova-platform-ops) | | REQ-359 | covered-reference | planned (M1 gate: nova-platform-ops) | | REQ-360 | covered-reference | planned (M1.5 gate: nova-platform-ops) | | REQ-361 | covered-reference | planned (M1 gate: nova-platform-ops) | | REQ-362 | covered-reference | planned (M1.5 gate: nova-platform-ops) | | REQ-363 | covered-reference | planned (M1.5 gate: nova-platform-ops) | | REQ-363b | covered-reference | planned (M1.5 gate: nova-platform-ops) | | REQ-364 | covered-reference | planned (M1.5 gate: nova-platform-ops) | | REQ-365 | covered-reference | planned (M1 gate: nova-platform-ops) | | REQ-366 | covered-reference | planned (M1 gate: nova-platform-ops) | | REQ-371 | covered-reference | planned (M2 gate: nova-platform-ops) |