# 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 <policy_dir> --payload <payload.json> -o json`,
  parses the native result list, and translates each entry to a PCR dict
  (`engine: "kyverno"`, `ruleId` prefixed `KJ_<policy_name>`, 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/<name>/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 <subcommand>`; the CLI and Lambda functions share a single
> `core/` source tree; and Nova owns its identity layer end-to-end
> (Nova-idp: `nova-idp-auth` + `nova-idp-token-vend` Lambdas, KMS-signed
> OIDC tokens, kyverno-json ABAC token vending, PAT lifecycle). No
> AWS-managed identity services in the path.
>
> 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/<module>.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 <id>` 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/<module>.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-<kj-source-sha>` where `<kj-source-sha>` 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-<sha>` to
`v1.29.x-kj-<sha>`).
**(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) |