0789c27ca2
Nova Slides Render / render (push) Failing after 14m27s
---ci--- project: acdl phase: 0 milestone: v1.29 status: complete ---/ci---
1059 lines
53 KiB
Markdown
1059 lines
53 KiB
Markdown
# PLAN — v1.28 CLI Canonicalization + Identity Layer
|
||
|
||
> **Milestone:** v1.28 (feature — CLI substrate + Nova-idp identity
|
||
> layer). Tags on the **v1.27.x** line: `v1.27.0` (P0) →
|
||
> `v1.27.1..v1.27.6` (P1..P6) → `v1.27.7` (P7 final = milestone
|
||
> release). The final phase's patch IS the milestone release.
|
||
> **Branch:** `milestone/v1.28-cli-identity`. Phase branches:
|
||
> `phase/00-pre-execution`, `phase/01-cli-substrate`,
|
||
> `phase/02-lambda-packaging`, `phase/03-idp-auth`,
|
||
> `phase/04-token-vend-pat`, `phase/05-docs-integration`,
|
||
> `phase/06-final-review-ship`.
|
||
>
|
||
> **Tags:** `v1.27.0` (P0) → `v1.27.1..v1.27.5` (P1..P5) →
|
||
> `v1.27.6` (P6 final = milestone release). 6 execution phases
|
||
> (P5 idp-setup folded into P4 Wave 8 per grill C-2.1).
|
||
|
||
## Milestone goal
|
||
|
||
The Nova CLI is installable from internal PyPI (CodeArtifact); every
|
||
`core/` module is reachable as a `nova <subcommand>`; the CLI and
|
||
Lambda functions share a single `core/` source tree; and Nova owns its
|
||
identity layer end-to-end (Nova-idp: `nova-idp-auth` +
|
||
`nova-idp-token-vend` Lambdas, KMS-signed OIDC tokens, kyverno-json
|
||
ABAC token vending, PAT lifecycle). No AWS-managed identity services
|
||
in the path (INV-15).
|
||
|
||
## Requirements
|
||
|
||
31 requirements: REQ-323..REQ-353 (full text in
|
||
`.ciagent/REQUIREMENTS.md` §v1.28). 6 capabilities: CAP-033..CAP-038.
|
||
6 invariants: INV-12..INV-17. 6 decisions: D-226..D-231 (CLARIFY) +
|
||
RESEARCH amendments (D-228 fail-closed, D-229 strong-read-on-PK).
|
||
|
||
## Phase breakdown
|
||
|
||
### Phase P1 — cli-substrate (REQ-323..REQ-328)
|
||
|
||
**Goal:** CodeArtifact wheel + Lambda layer pipeline; `nova/` CLI
|
||
package with a subcommand per `core/` module; `nova init`; `nova
|
||
cli-action` composite action; `core/mode_resolver.py`; audit emission
|
||
with `mode` + `selection_reason`. The CLI is installable and every
|
||
`core/` module is reachable.
|
||
|
||
**Exit criterion:** CAP-033 + CAP-034 + CAP-035 Verified + all REQ-323..328
|
||
tests pass. CodeArtifact provisioned (Wave 0 gate).
|
||
|
||
#### Wave 0 — CodeArtifact provisioning (backend-engineer) [C-3.2/C-8.1]
|
||
- **Task 0.1** (backend-engineer): provision CodeArtifact domain
|
||
(`nova`) + repository (`nova-pypi`) in `581513795199`. Verify
|
||
`codeartifact:*` IAM grant on `nova-spike-runner`. **Binary go/no-go
|
||
gate for Wave 4.** If fail: activate Gitea wheel index fallback
|
||
(CLARIFY assumption #1) and document in PLAN.md.
|
||
|
||
#### Wave 1 — pyproject + entry point (cli-engineer)
|
||
- **Task 1.1** (cli-engineer): `pyproject.toml` — add
|
||
`[project.scripts] nova = "nova.cli:main"`; add
|
||
`[tool.setuptools.packages.find]` including `nova`, `nova.*`, `core`,
|
||
`core.*`, `adapters.*`; bump `requires-python` to `>=3.12`; add
|
||
`argon2-cffi`, `cryptography`, `pyjwt`, `hypothesis` to deps/test-deps.
|
||
Verify `pip install -e .` produces a `nova` executable.
|
||
|
||
#### Wave 2 — CLI dispatch + subcommands (cli-engineer)
|
||
- **Task 2.1** (cli-engineer): `nova/__init__.py` + `nova/cli.py`
|
||
(~80 lines, auto-discovers `nova/<module>.py` via `pkgutil.iter_modules`,
|
||
dispatches, emits `cli.invocation` audit event stub with INV-12 fields).
|
||
- **Task 2.2** (cli-engineer): `nova/<module>.py` for each `core/`
|
||
module (≤50 lines, `add_parser` + `run` delegates to `core/`). Cover:
|
||
`resolve`, `decommission`, `env-transition`, `env-check`, `hitl`,
|
||
`onboard`, `outbox`, `publish-outputs`, `policy`, `regression`, `sod`,
|
||
`readiness`, `attestation-matrix`, `confidence`. Skip internal-only
|
||
(`env`, `local_emulators`, `output_publisher` if not user-facing).
|
||
- **Task 2.3** (cli-engineer): `nova/init.py` (REQ-325) — scaffolds
|
||
`.nova/`, `.nova/contract.yml.attestations/`, `.gitignore` (excludes
|
||
secrets, `~/.nova/credentials.json`).
|
||
|
||
#### Wave 3 — mode_resolver + audit (cli-engineer)
|
||
- **Task 3.1** (cli-engineer): `core/mode_resolver.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 `<semver>+<sha7>` for idempotent re-runs.
|
||
|
||
#### Wave 5 — composite action (cli-engineer + backend-engineer)
|
||
- **Task 5.1** (cli-engineer): `.github/actions/nova-cli/action.yml` —
|
||
composite action, `setup-python@v5` (3.12), CodeArtifact login +
|
||
`pip install nova`, `nova ${{ inputs.command }}`. `NOVA_CLIENT_MODE`
|
||
from input.
|
||
- **Task 5.2** (backend-engineer): byte-identical integration test —
|
||
CI matrix runs the action on GitHub `ubuntu-latest` + Gitea
|
||
`act_runner`; assert same stdout/exit code (REQ-326 AC2, NFR-11).
|
||
|
||
#### Wave 6 — CAP-033/034 gate (cli-engineer)
|
||
- **Task 6.1** (cli-engineer): `tests/test_cli_subcommands.py` —
|
||
CAP-033 (`nova --help` lists a subcommand for every `core/` module)
|
||
+ CAP-034 (AST scan: ≤50 lines, ≤3 defs, all calls resolve to `core.`,
|
||
no conditionals beyond `if __name__`). Wire into CI merge gate.
|
||
|
||
### Phase P2 — lambda-packaging (REQ-329, REQ-330, REQ-331)
|
||
|
||
**Goal:** Dual-use `core/lambda/contract_ingestor.py` (Lambda + CLI
|
||
paths share ≥80% code); `core/env.py:+synthesize_local_env()` for
|
||
`nova apply --local`; `.nova/contract.yml.attestations/` scaffolded;
|
||
JWS-from-PAT key derivation (C-5.2).
|
||
|
||
**Exit criterion:** all REQ-329..331 tests pass + JWS-from-PAT KDF
|
||
specified.
|
||
|
||
#### Wave 1 — dual-use refactor (backend-engineer)
|
||
- **Task 1.1** (backend-engineer): refactor
|
||
`core/lambda/contract_ingestor.py` — extract the shared logic into
|
||
importable functions; the Lambda handler + the CLI `__main__` block
|
||
both call them. The `__main__` block already exists (the dual-use
|
||
precedent per RESEARCH §1.2). Verify ≥80% code share (CAP-034 / code
|
||
review). Local path via `core/local_emulators.py:LocalLambdaStub`.
|
||
|
||
#### Wave 2 — local env synthesizer + JWS KDF (backend-engineer)
|
||
- **Task 2.1** (backend-engineer): `core/env.py:+synthesize_local_env()
|
||
` — produces a local env dict (account_id placeholder, region local,
|
||
no real AWS) from a contract + `--local` flag. Mirrors
|
||
`core/onboarding.py:generate_env_file()`.
|
||
- **Task 2.2** (cli-engineer): `nova/apply.py` (≤50 lines) — `nova
|
||
apply --local` delegates to `core.env.synthesize_local_env()` +
|
||
`core.contract_resolver.resolve()`.
|
||
- **Task 2.3** (security-engineer): JWS-from-PAT key derivation
|
||
(C-5.2). HKDF-SHA256(PAT_bytes, salt='nova-local-attestation',
|
||
info='jws-signing-key') → 32-byte symmetric key. The JWS is
|
||
HMAC-SHA256 (symmetric, not asymmetric). The "public key derivable
|
||
from the PAT" AC (REQ-332) is re-interpreted: the *verification key*
|
||
is derived from the PAT via the same KDF (the PAT is the shared
|
||
secret). Document in `docs/developer-guide-auth.md`. Update REQ-332
|
||
AC accordingly.
|
||
|
||
#### Wave 3 — attestations dir (cli-engineer)
|
||
- **Task 3.1** (cli-engineer): verify `nova init` (P1 Wave 2 Task 2.3)
|
||
creates `.nova/contract.yml.attestations/` (empty). REQ-331 test.
|
||
|
||
### Phase P3 — idp-auth (REQ-333, REQ-334, REQ-335)
|
||
|
||
**Goal:** `nova-idp-auth` Lambda (sign-up, sign-in, session) with
|
||
Argon2id hashing + DynamoDB tables. CAP-036 target.
|
||
|
||
**Exit criterion:** CAP-036 Verified (E2E sign-up → sign-in → session
|
||
passes in CI).
|
||
|
||
#### Wave 1 — DynamoDB schema (backend-engineer)
|
||
- **Task 1.1** (backend-engineer): define the 4 DynamoDB table schemas
|
||
(`nova-users`, `nova-sessions`, `nova-password-resets`, `nova-pats`)
|
||
in a CloudFormation snippet (reused by P4 Wave 8 `nova idp setup`).
|
||
PITR enabled on each (REQ-335).
|
||
|
||
#### Wave 2 — Argon2id (security-engineer) [C-1.2/C-7.2]
|
||
- **Task 2.1** (security-engineer): `core/lambda/nova_idp_auth.py` —
|
||
Argon2id password hashing via `argon2-cffi` (D-228: bundled abi3
|
||
wheel; **fail-closed on `ImportError` → 503, no pure-Python
|
||
fallback**). **Parameters: t=3, m=65536 KiB, p=1** (OWASP-recommended
|
||
minimum). Lambda memory ≥512 MB (m=64 MiB + Python overhead fits).
|
||
Raw passwords never in logs/traces/env/DDB (INV-16, REQ-334).
|
||
- **Task 2.2** (security-engineer): `tests/test_argon2_fail_closed.py`
|
||
— mock `argon2.low_level` import failure → assert auth Lambda
|
||
returns 503 (not a crash, not a weak hash). C-1.2.
|
||
|
||
#### Wave 3 — auth Lambda (backend-engineer + security-engineer)
|
||
- **Task 3.1** (backend-engineer): `nova-idp-auth` Lambda handler —
|
||
sign-up, sign-in, session creation endpoints. Function URL + IAM
|
||
auth. DynamoDB via lazy `boto3.resource` (the existing pattern).
|
||
- **Task 3.2** (security-engineer): session token issuance + session
|
||
storage in `nova-sessions` (TTL `expires_at`). Password reset flow
|
||
in `nova-password-resets` (TTL 15m).
|
||
|
||
#### Wave 4 — CAP-036 E2E (backend-engineer)
|
||
- **Task 4.1** (backend-engineer): `tests/test_idp_auth.py` — sign-up
|
||
→ sign-in → session round-trip (moto[dynamodb] for local; deployed
|
||
for CI). CAP-036 verification.
|
||
|
||
### Phase P4 — token-vend-pat (REQ-336..REQ-344, REQ-340, REQ-341) [was P4+P5]
|
||
|
||
**Goal:** `nova-idp-token-vend` Lambda (KMS-signed OIDC, kyverno-json
|
||
ABAC), JWKS endpoint, PAT lifecycle, `nova auth` commands, **and**
|
||
`nova idp setup` (folded from P5 per C-2.1). CAP-037 + CAP-038 target.
|
||
**Highest-risk phase — critical-path.** The kj-binary spike (Wave 1)
|
||
is the single highest-probability schedule slip; Fargate fallback adds
|
||
~1 week (D-227). This is a **double-length phase** (8 waves).
|
||
|
||
**Exit criterion:** CAP-037 + CAP-038 Verified + `nova idp setup
|
||
--check/--apply/--verify` works against a fresh AWS account.
|
||
|
||
#### Wave 1 — kj-binary spike (backend-engineer + security-engineer) [C-8.2]
|
||
- **Task 1.1** (backend-engineer): confirm the `kj` Go binary
|
||
(~40 MB Linux amd64) runs in the Lambda Python 3.12 runtime on
|
||
AL2023. Bundle it in the `nova-cli` layer (`wget` a **pinned
|
||
release** (e.g. `kj@v1.x.y`) + record SHA256 into `layer/kj.sha256`
|
||
— C-8.2, supply-chain safety) into `layer/bin/kj`, `chmod +x`.
|
||
Verify `KyvernoJsonEngine.is_configured()` finds `/opt/bin/kj`.
|
||
**If this fails:** fall back to Fargate for the token-vend Lambda
|
||
(D-227 risk, RESEARCH §7). Escalate to user only if both fail (full
|
||
autonomy: log assumption + proceed with Fargate).
|
||
|
||
#### Wave 2 — ABAC policy (security-engineer) [C-5.1]
|
||
- **Task 2.1** (security-engineer): `platform/abac/token-vend.policy`
|
||
— kyverno-json `ValidatingPolicy` (D-227). Payload:
|
||
`{subject, requested_claims, target_resource, environment, pat_jti,
|
||
policy_version}`. **`requested_claims` = list of claim names** (the
|
||
policy asserts the subject is *allowed* to request those claims; the
|
||
values are assigned by the Lambda, not the requestor — C-5.1).
|
||
JMESPath checks for role/scope/env/owner. Severity `critical` = deny
|
||
on fail.
|
||
- **Task 2.2** (security-engineer): `policy_version` = git SHA of the
|
||
policy file, baked into the Lambda layer (D-231). Recorded in every
|
||
`token.vend.allowed/denied` audit event.
|
||
|
||
#### Wave 3 — KMS signing (security-engineer) [C-1.1]
|
||
- **Task 3.1** (security-engineer): **verify KMS asymmetric key
|
||
support** before implementation: `aws kms create-key --key-spec
|
||
ECC_NIST_P256 --key-usage SIGN_VERIFY` in the target account
|
||
(C-1.1). If fail: fall back to RSA-2048 (also supported, larger
|
||
tokens) or escalate. Do not discover this mid-Wave.
|
||
- **Task 3.2** (security-engineer): KMS key `alias/nova-oidc-signing`
|
||
(ECC_NIST_P256, SIGN_VERIFY). Token-vend Lambda signs via
|
||
`kms.sign(SigningAlgorithm="ECDSA_SHA_256")` → DER→raw ECDSA
|
||
conversion (`decode_dss_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 <jti>`,
|
||
`nova auth status` (active credential, mode, selection_reason).
|
||
All emit audit events (REQ-344). **C-7.3: `~/.nova/credentials.json`
|
||
stores the OIDC token + PAT metadata (jti, exp, type) ONLY — NOT the
|
||
raw PAT.** The raw PAT is entered once at `nova auth login` and not
|
||
persisted (reduces filesystem-compromise blast radius).
|
||
|
||
#### Wave 7 — CAP-037/038 (security-engineer)
|
||
- **Task 7.1** (security-engineer): `tests/test_kms_roundtrip.py`
|
||
(REQ-350, CAP-037) — sign test JWT via token-vend, fetch JWKS,
|
||
verify with `pyjwt`. `tests/test_pat_revocation.py` (REQ-351,
|
||
CAP-038) — issue → vend → revoke → assert 403 within 60s P95.
|
||
|
||
#### Wave 8 — nova idp setup (cli-engineer + backend-engineer) [folded from P5 per C-2.1]
|
||
|
||
**Goal:** `nova idp setup` command with `--check/--apply/--verify`
|
||
modes; CloudFormation template generation + review (REQ-340, REQ-341).
|
||
|
||
- **Task 8.1** (backend-engineer): `nova/idp/setup.py` (+ backend
|
||
helper) — generates the Nova-idp CloudFormation template (raw dict →
|
||
JSON): 2-3 Lambdas, 4 DDB tables, KMS key, function URLs, IAM roles,
|
||
optional CloudFront/WAF/ACM (`--public-jwks-domain` flag).
|
||
- **Task 8.2** (cli-engineer): `--check` (prerequisites + IAM policy
|
||
delta), `--apply` (generate → `$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/<module>.py` ≤50 lines, no business logic, AST scan |
|
||
| CAP-035 | Layer matches wheel | P1 | Lambda layer ARN version matches `nova-cli` wheel version (SSM mapping) |
|
||
| CAP-036 | Nova-idp auth flow works | P3 | E2E test (sign-up → sign-in → session) passes in CI |
|
||
| CAP-037 | Token-vend signs via KMS | P4 | KMS round-trip test (REQ-350) passes in CI |
|
||
| CAP-038 | PAT issuance + revocation | P4 | Issue → vend → revoke → 403 within 60s P95 (REQ-351) passes in CI |
|
||
**Release gate (§6 of the spec):** CAP-001..CAP-032 remain Verified;
|
||
CAP-033..CAP-038 are Verified; all v1.28 release-gate criteria met.
|
||
|
||
---
|
||
|
||
## Test evidence required for v1.28 release
|
||
|
||
- [ ] Code coverage ≥ 80% on new modules (`mode_resolver.py`,
|
||
`nova-idp-auth`, `nova-idp-token-vend`, PAT lifecycle).
|
||
- [ ] CI/CD pipeline GREEN: wheel + Lambda layer publish on every merge
|
||
(REQ-323, CAP-035).
|
||
- [ ] QA sign-off: all four happy-path journeys (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-<sha>`) to GitHub Release for each tag, with matching
|
||
SHA-256 in the body. The M1.5 verification gate tests
|
||
(`test_idp_auth`, `test_kms_roundtrip`, ABAC E2E) are authored.
|
||
|
||
**Exit criterion:** REQ-354 criteria 1-4 pass; KJ-STATIC audit (file(1)
|
||
asserts `statically linked`) runs in CI; ECR image pushed with tag
|
||
`v1.29.x-kj-<sha>` (D-239); GitHub Release body lists image URI + digest
|
||
alongside wheel + layer + Lambda zip; M1.5 gate tests exist + pass in
|
||
moto-DDB (live KMS round-trip is covered-reference, runs in
|
||
nova-platform-ops CI).
|
||
|
||
**Branch:** `phase/01-publish-pipeline`. **Tag:** `v1.28.1`.
|
||
|
||
#### Wave 0 — publish.yml trigger model (backend-engineer)
|
||
- **Task 0.1** (backend-engineer): change `.github/workflows/publish.yml`
|
||
trigger from `push: branches: [main]` to `push: tags: ['v1.29.*']`.
|
||
Preserve the existing wheel + Lambda layer publish steps (REQ-323/
|
||
CAP-035). Add the Lambda zip packaging step
|
||
(`nova-lambda-token-vend-v1.29.x.zip`). Verify the trigger fires on
|
||
`git tag v1.29.0 && git push --tags`.
|
||
|
||
#### Wave 1 — kj source confirmation + static build + ECR image (backend-engineer, security-engineer)
|
||
- **Task 1.0** (backend-engineer): **kj source-fetch confirmation
|
||
(grill CF-4/G-4 — binary go/no-go gate before Wave 1).** Confirm the
|
||
`kj` Go source repo URL + commit at SHA `4ebb9a19...` (read from
|
||
`platform/abac/kj-version.txt`). Record the repo URL as a 3rd line
|
||
in `platform/abac/kj-version.txt`. If unfetchable → P1 fails closed
|
||
→ escalate (this is a spec dependency, not a CIAgent ambiguity). The
|
||
source repo is the `kyverno-json/kj` Go binary project (distinct
|
||
from the kyverno-json Python engine adapter in `adapters/kyverno-
|
||
json/`).
|
||
- **Task 1.1** (backend-engineer): add a `build-kj-image` job to
|
||
`publish.yml` that:
|
||
(a) reads `platform/abac/kj-version.txt` (v0.0.3 + SHA
|
||
`4ebb9a19...`);
|
||
(b) fetches the `kj` Go source at the pinned SHA (RESEARCH §7 —
|
||
source repo confirmed in P1 RESEARCH);
|
||
(c) builds with `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
|
||
-ldflags="-s -w" -o kj ./…`;
|
||
(d) runs `file kj` and asserts output contains `statically linked`
|
||
AND does NOT contain `shared library` (KJ-STATIC — fail build
|
||
otherwise);
|
||
(e) builds the container image from
|
||
`public.ecr.aws/lambda/python:3.12-al2023`, copying `kj` to
|
||
`/opt/kj/kj` with `chmod 0555` owned by `sbx_user:1051`;
|
||
(f) pushes the image to ECR with tag `v1.29.x-kj-<kj-source-sha>`
|
||
(D-239 — assert tag matches `^[a-zA-Z0-9._-]+$` before push);
|
||
(g) records the image URI + digest for the GitHub Release body.
|
||
- **Task 1.2** (security-engineer): add a KJ-STATIC audit step that
|
||
runs `file(1)` + `readelf -d kj` (assert no `NEEDED` entries) as a
|
||
CI gate. If either fails, the publish job fails closed. This is the
|
||
mechanical enforcement of KJ-STATIC (not just a human review).
|
||
|
||
#### Wave 2 — GitHub Release body + SHA-256 (backend-engineer)
|
||
- **Task 2.1** (backend-engineer): extend the `publish.yml` release step
|
||
to attach: (a) `nova-lambda-token-vend-v1.29.x.zip`; (b)
|
||
`nova-cli-layer-v1.29.x.zip`; (c) `nova-1.29.x-py3-none-any.whl`; (d)
|
||
the ECR image URI + digest. Compute SHA-256 for each artifact + list
|
||
in the release body. Verify REQ-354 criteria 1, 2, 4 (artifacts
|
||
appear, independent per tag, image URI + digest listed).
|
||
|
||
#### Wave 3 — M1.5 verification gate tests (security-engineer)
|
||
- **Task 3.1** (security-engineer): author `tests/test_idp_auth.py` —
|
||
sign-up → sign-in → session flow against moto-DDB (covers Edge 5
|
||
item 5). Skip live-KMS assertions (covered-reference — runs in
|
||
nova-platform-ops CI).
|
||
- **Task 3.2** (security-engineer): author
|
||
`tests/test_kms_roundtrip.py` — sign/verify round-trip against
|
||
`alias/nova-oidc-signing`. Mark as `@pytest.mark.live_aws` (skipped in
|
||
acdl CI; runs in nova-platform-ops CI against the live key, REQ-362).
|
||
- **Task 3.3** (security-engineer): author
|
||
`tests/test_abac_e2e.py` — known PAT → ABAC-allowed action → signed
|
||
OIDC token → `jose` verification → green; known PAT + ABAC-denied
|
||
action → 403 with deny reason logged (INV-17 fail-closed, Edge 5
|
||
item 7). Uses moto-DDB + mock KMS.
|
||
|
||
#### Wave 4 — regression + ship (lead-developer)
|
||
- **Task 4.1** (lead-developer): run full test suite; verify 1000+
|
||
tests still pass (no regressions from publish.yml changes). Verify
|
||
CAP-001..038 regression gate green. **Push a `v1.29.0` intermediate
|
||
tag at P1 ship** (grill CF-3/G-3) to trigger `publish.yml` + produce
|
||
the v1.29 artifacts (Lambda zip + layer wheel + Python wheel + ECR
|
||
image). The milestone release tag remains `v1.28.6`; the `v1.29.0`
|
||
artifact tag is a P1 intermediate to produce publish artifacts for
|
||
P5's smoke test. Ship P1 → `v1.28.1`.
|
||
|
||
### Phase P2 — gitea-scrub-decisions (REQ-367, REQ-368)
|
||
|
||
**Goal:** Hard scrub of all Gitea references in `acdl/acdl`; `.gitea/`
|
||
removed; `forge_parity_disabled` CI assertion; pyproject → 1.29.0;
|
||
decisions D-232..238 recorded in PROJECT.md + CLARIFY (already done in
|
||
P0; this phase adds the CI assertion + the actual file scrub).
|
||
|
||
**Exit criterion:** `grep -rni gitea .github/ docs/ pyproject.toml
|
||
README.md .ciagent/` returns zero matches outside the spec archive
|
||
section; `find .gitea` returns nothing; CI `forge_parity_disabled`
|
||
assertion passes; pyproject.toml version = 1.29.0.
|
||
|
||
**Branch:** `phase/02-gitea-scrub-decisions`. **Tag:** `v1.28.2`.
|
||
|
||
#### Wave 0 — pyproject bump (lead-developer)
|
||
- **Task 0.1** (lead-developer): bump `pyproject.toml` version →
|
||
`1.29.0` (spec §7.2). Verify `nova --version` reports `1.29.0`.
|
||
|
||
#### Wave 1 — .gitea/ removal (lead-developer)
|
||
- **Task 1.1** (lead-developer): `rm -rf .gitea/` (7 workflow files +
|
||
README.md, RESEARCH §9d). Remove `scripts/sync_workflows.py` (the
|
||
byte-identical-forges generator — central removal target, D-232).
|
||
Remove Gitea references from `scripts/sync_to_nova.sh` (line 201:
|
||
`--exclude=/.gitea`) + `scripts/rotate_spike_key.sh` (Gitea API
|
||
secret upload). Scrub `terraform/bootstrap/` Gitea OIDC references
|
||
(the OIDC role for act_runner moves to nova-platform-ops; the
|
||
bootstrap here becomes archived reference).
|
||
|
||
#### Wave 2 — Gitea reference scrub (lead-developer)
|
||
- **Task 2.1** (lead-developer): `grep -rni gitea .github/ docs/
|
||
pyproject.toml README.md .ciagent/` — scrub all matches outside the
|
||
spec archive section (`.ciagent/REQUIREMENTS.md` §v1.29 + CLARIFY §v1.29
|
||
+ RESEARCH §v1.29 retain "Gitea" as historical/reference text; these
|
||
are the "spec archive section" exemption per REQ-367 AC 1). Update
|
||
`.github/workflows/ci.yml` to remove any Gitea-specific steps.
|
||
|
||
#### Wave 3 — forge_parity_disabled CI assertion (lead-developer)
|
||
- **Task 3.1** (lead-developer): add a CI step to `.github/workflows/ci.yml`
|
||
that asserts `forge_parity_disabled` — the step runs
|
||
`test ! -d .gitea/` and `! grep -rqi gitea .github/workflows/` and
|
||
exits 0 on success, non-zero with `forge_parity_disabled` message on
|
||
failure (REQ-367 AC 3, D-232). This is the deliberate CI failure that
|
||
documents the abandoned parity.
|
||
|
||
#### Wave 4 — decisions verification + ship (lead-developer)
|
||
- **Task 4.1** (lead-developer): verify D-232..238 + D-239/240 are
|
||
present in PROJECT.md + CLARIFY.md + REQUIREMENTS.md (REQ-368 AC 1-2,
|
||
already authored in P0; this task is a verification, not re-authoring).
|
||
Run full test suite; ship P2 → `v1.28.2`.
|
||
|
||
### Phase P3 — cfn-archive-tf-delegation (REQ-369)
|
||
|
||
**Goal:** Archive the CFN template in `nova/idp/setup.py` +
|
||
`core/lambda/nova_idp_setup.py` to `docs/archive/nova-idp-cfn-v1.28.md`
|
||
(read-only reference); `nova idp setup --apply` delegates to `terraform
|
||
apply` (the CLI detects terraform via `which terraform`; if absent,
|
||
falls back to the CFN path with a deprecation warning).
|
||
|
||
**Exit criterion:** `docs/archive/nova-idp-cfn-v1.28.md` exists +
|
||
contains the CFN template as read-only reference; `nova idp setup
|
||
--apply` invokes `terraform apply` when terraform is on PATH (tested
|
||
with a mock terraform binary); the CFN path emits a deprecation warning
|
||
when terraform is absent.
|
||
|
||
**Branch:** `phase/03-cfn-archive-tf-delegation`. **Tag:** `v1.28.3`.
|
||
|
||
#### Wave 0 — CFN archive (cli-engineer, backend-engineer)
|
||
- **Task 0.1** (backend-engineer): extract the CFN template from
|
||
`core/lambda/nova_idp_setup.py` + write it to
|
||
`docs/archive/nova-idp-cfn-v1.28.md` as a fenced code block with a
|
||
read-only header ("Archived at v1.29.0 — the active path is
|
||
`terraform apply` in `nova-platform-ops`. Deletion is a follow-up
|
||
after Terraform parity is verified.").
|
||
- **Task 0.2** (cli-engineer): mark the CFN generation code path in
|
||
`core/lambda/nova_idp_setup.py` as deprecated (add a
|
||
`DeprecationWarning` when the CFN path is invoked + a docstring
|
||
pointing to the archive + the terraform delegation path).
|
||
|
||
#### Wave 1 — terraform delegation (cli-engineer)
|
||
- **Task 1.1** (cli-engineer): modify `nova/idp/setup.py` `--apply` to
|
||
detect terraform via `shutil.which("terraform")`. If terraform is on
|
||
PATH: delegate to `subprocess.run(["terraform", "apply",
|
||
"-auto-approve"])` in the `nova-platform-ops` checkout (the operator
|
||
runs this from the ops repo root). If terraform is absent: fall back
|
||
to the CFN path with a `DeprecationWarning` ("CFN path is archived;
|
||
install terraform or use nova-platform-ops. See
|
||
docs/archive/nova-idp-cfn-v1.28.md.").
|
||
- **Task 1.2** (cli-engineer): add `nova idp setup --verify` delegation
|
||
to `terraform plan` (same `which terraform` detection). The verify
|
||
path runs `terraform plan` + reports the diff.
|
||
|
||
#### Wave 2 — tests (cli-engineer)
|
||
- **Task 2.1** (cli-engineer): author
|
||
`tests/test_idp_setup_tf_delegation.py` — test the `--apply` path
|
||
with a mock terraform binary on PATH (assert `subprocess.run` called
|
||
with `["terraform", "apply", "-auto-approve"]`); test the fallback
|
||
path with terraform absent (assert `DeprecationWarning` raised + CFN
|
||
path invoked); test `--verify` delegates to `terraform plan`.
|
||
|
||
#### Wave 3 — ship (lead-developer)
|
||
- **Task 3.1** (lead-developer): run full test suite; ship P3 →
|
||
`v1.28.3`.
|
||
|
||
### Phase P4 — operator-guide-reference-tracking (REQ-OPS-GUIDE)
|
||
|
||
**Goal:** `docs/operator-guide-platform-ops.md` covering KMS rotation,
|
||
JWKS reachability via CloudFront edge, PITR restore, PAT revocation,
|
||
edge configuration, Fargate standby health, cost section, artifact-
|
||
mirror fallback, and the M1/M1.5/M2 cutover gates as release-gate
|
||
entries for the covered-reference REQs. ARCHITECTURE.md §12.9. STATE.md
|
||
v1.29 CAPs + invariants. REQUIREMENTS.md covered-reference markers.
|
||
|
||
**Exit criterion:** operator guide exists + covers all sections per
|
||
REQ-OPS-GUIDE AC; ARCHITECTURE.md §12.9 added; STATE.md updated with
|
||
v1.29 rows; covered-reference REQs in REQUIREMENTS.md marked with their
|
||
cutover gate.
|
||
|
||
**Branch:** `phase/04-operator-guide-reference-tracking`. **Tag:**
|
||
`v1.28.4`.
|
||
|
||
#### Wave 0 — operator guide (lead-developer, data-engineer, security-engineer)
|
||
- **Task 0.1** (lead-developer): author
|
||
`docs/operator-guide-platform-ops.md` sections: (a) Overview + the
|
||
reposplit rationale (Vision §4); (b) Day-0 cutover procedure (M1
|
||
steps from spec §3.2 Journey 2); (c) M1.5 verification gate (8-item
|
||
spike, 3 consecutive rebuilds); (d) M2 operational handoff loop
|
||
(tag-pin bump → plan → HITL approval → apply); (e) M2a Fargate
|
||
activation (conditional on M1.5 failure); (f) Rollback procedure
|
||
(D-236 — revert `nova_platform_version` pin); (g) cost section (WAF
|
||
~$5-10/month + Fargate ~$15-20/month, REQ-363b AC 4); (h) artifact-
|
||
mirror fallback (operator-local mirror by SHA-256 when Gitea
|
||
act_runner cannot reach GitHub Releases, Edge 6).
|
||
- **Task 0.2** (data-engineer): author the operator guide data
|
||
sections: (a) DynamoDB PITR restore procedure (per-table); (b)
|
||
DynamoDB import addresses (nova-contracts, nova-change-requests,
|
||
nova-outbox, nova-users, nova-sessions, nova-pats — the
|
||
`importable-resources.tf` map, REQ-361 covered-reference); (c) audit
|
||
outbox bootstrap; (d) JWKS-ROTATION (24-hour overlap window on key
|
||
rotation).
|
||
- **Task 0.3** (security-engineer): author the operator guide security
|
||
sections: (a) KMS rotation (90-day cadence, `alias/nova-oidc-
|
||
signing`, `ECC_NIST_P256`, D-234); (b) JWKS reachability via
|
||
CloudFront edge (OAC pinning, `AuthType: AWS_IAM`, direct Function
|
||
URL → 403, INV-18); (c) PAT revocation (60s SLO, D-229); (d) edge
|
||
configuration (CloudFront + WAF + ACM + Route53 — REQ-364/365/366
|
||
covered-reference); (e) Fargate standby health checks (`GET /health`
|
||
every 10s, `KJ-WARMUP-HEALTH`, 3 consecutive probe failures → alert +
|
||
token-vend fails closed, REQ-363b AC 2); (f) Fargate sunset
|
||
discipline (D-237 — ≥30 consecutive days green before deletion +
|
||
architecture review); (g) IAM scope (IAM-NARROW, REQ-360 covered-
|
||
reference — no `Action: "*"` or `Resource: "*"`); (h) the
|
||
`route53_record_not_resolvable` debugging path (ACM cert status
|
||
check).
|
||
|
||
#### Wave 1 — covered-reference cutover gates (lead-developer)
|
||
- **Task 1.1** (lead-developer): add a "Cutover Gates" section to the
|
||
operator guide listing each covered-reference REQ (355, 356, 357,
|
||
358, 359, 360, 361, 362, 363, 363b, 364, 365, 366, 371) with its
|
||
gate entry (M1/M1.5/M2) + the verification command + a **"Result"
|
||
column** (grill CF-2/G-5). P6 audit verifies every covered-reference
|
||
REQ has a non-empty, green Result. Empty/red → P6 blocks. The Result
|
||
column is populated by the operator attestation (the operator runs
|
||
the verification command in `nova-platform-ops` CI + records the
|
||
outcome). This is the acdl-side evidence surface for covered-
|
||
reference REQs.
|
||
- **Task 1.2** (lead-developer): update REQUIREMENTS.md §v1.29 traceability
|
||
table — mark each covered-reference REQ with its cutover gate in the
|
||
Status column (e.g., `planned (M1 gate: nova-platform-ops)`).
|
||
|
||
#### Wave 2 — ARCHITECTURE.md + STATE.md (lead-developer)
|
||
- **Task 2.1** (lead-developer): add ARCHITECTURE.md §12.9 (Platform
|
||
Ops Reposplit) — the domain boundary (engineering ends at the
|
||
compiled artifact; operations begins at the live platform under
|
||
guardrails), the `kj` substrate (one ECR image digest, KJ-LOCKSTEP),
|
||
the covered-reference REQ tracking pattern, the operator guide
|
||
pointer.
|
||
- **Task 2.2** (lead-developer): update STATE.md — append v1.29
|
||
capability rows (CAP-039: platform-ops-reposplit, CAP-040:
|
||
kj-substrate-lockstep, CAP-041: jwks-edge-only) + bump invariants
|
||
(INV-18 JWKS-EDGE-ONLY + the 10 NFR constraints). Bump "Last
|
||
milestone ship" to v1.29 (pending).
|
||
|
||
#### Wave 3 — ship (lead-developer)
|
||
- **Task 3.1** (lead-developer): run full test suite; ship P4 →
|
||
`v1.28.4`.
|
||
|
||
### Phase P5 — consumer-deploy-bump (REQ-CONSUMER-BUMP, cross-project)
|
||
|
||
**Goal:** Bump `nova-blockchain-exchange` deploy.yml `@v1.25` → `@v1.29`
|
||
in both `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml`
|
||
+ smoke test (sign-up → sign-in → token-vend → apply → audit against
|
||
v1.29 publish artifacts).
|
||
|
||
**Exit criterion:** both deploy.yml files reference `@v1.29`; smoke
|
||
test passes (the chain completes against v1.29 publish artifacts).
|
||
|
||
**Branch:** `nova-blockchain-exchange/phase/05-consumer-deploy-bump`
|
||
(cross-project, multi-project branch naming per branch-strategy.md).
|
||
**Tag:** `v1.28.5`.
|
||
|
||
#### Wave 0 — deploy.yml bump (lead-developer)
|
||
- **Task 0.1** (lead-developer): in the `nova-blockchain-exchange`
|
||
project, update `.github/workflows/deploy.yml` + `.gitea/workflows/
|
||
deploy.yml` `uses:` ref from `acdl/.github/workflows/deploy.yml@v1.25`
|
||
→ `@v1.29` (RESEARCH §9e — the consumer's `.gitea/` is out of scope
|
||
for the acdl REQ-367 scrub; the consumer may keep its Gitea mirror or
|
||
follow suit — this is a consumer-repo decision, not an acdl one).
|
||
|
||
#### Wave 1 — smoke test (lead-developer, security-engineer)
|
||
- **Task 1.1** (security-engineer): author
|
||
`nova-blockchain-exchange/tests/test_v1.29_smoke.py` — sign-up →
|
||
sign-in → token-vend → apply → audit chain against the v1.29 publish
|
||
artifacts (the consumer's contract → `deploy.yml@v1.29` mode=full →
|
||
apply → attest → record against `581513795199`). Uses the existing
|
||
CAP-025 round-trip assertion (v1.26).
|
||
- **Task 1.2** (lead-developer): run the smoke test; verify the chain
|
||
completes against the real v1.29.0 publish artifacts (produced by
|
||
P1's intermediate tag, grill CF-3/G-3). **No hedge** — the smoke
|
||
test MUST run against the published v1.29.x artifacts or P5 fails
|
||
closed. If the artifacts are not available (P1 did not push the
|
||
intermediate tag), P5 blocks until P1 re-ships.
|
||
|
||
#### Wave 2 — ship (lead-developer)
|
||
- **Task 2.1** (lead-developer): ship P5 → `v1.28.5`. The consumer
|
||
project ships independently (merge to the consumer's main, not
|
||
acdl's milestone branch).
|
||
|
||
### Phase P6 — final-review-ship (Final Phase)
|
||
|
||
**Goal:** Multi-persona code review across P1..P5; audit (reconstruction
|
||
test, branch hygiene, commit discipline, file discipline); milestone
|
||
ship (merge `phase/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). |