docs(P00): research findings — v1.28 CLI + identity layer (11 Qs, D-228 amended)

---ci---
project: acdl
phase: 0
milestone: v1.28
status: research
---/ci---
This commit is contained in:
Jon Chery
2026-08-19 22:05:24 +00:00
parent 05efb014d6
commit c629809d75
4 changed files with 505 additions and 190 deletions
+81 -1
View File
@@ -576,4 +576,84 @@ key only after the new one propagates to the consumer's Actions secret
store, verified by a post-PUT GET; on upload/verify failure the old key is
left Active and the run exits non-zero. The synced workflow file is
forge-agnostic (REQ-230): forge base URL / owner / consumer repo come from
repository secrets (`NOVA_FORGE_*`, `NOVA_CONSUMER_REPO`), not literals.
repository secrets (`NOVA_FORGE_*`, `NOVA_CONSUMER_REPO`), not literals.
### §12.10 — Nova-idp Identity Layer (v1.28, current)
Nova owns its identity layer end-to-end. Two (optionally three) Lambda
functions + four DynamoDB tables + one KMS asymmetric signing key + one
kyverno-json ABAC policy. **No Cognito, no IAM Identity Center (INV-15).**
The `nova-cli` Lambda layer carries the Nova wheel + `argon2-cffi` +
`cryptography` + `pyjwt` + the `kj` Go binary, making the same code
importable in both the CLI and the Lambda (REQ-329 dual-use, NFR-7).
**Components:**
- `nova-idp-auth` Lambda — sign-up, sign-in, session creation. Argon2id
password hashing (D-228: bundled abi3 wheel; fail-closed on
`ImportError`, no pure-Python fallback). DynamoDB: `nova-users`
(PK `user_id`, Argon2id `password_hash`), `nova-sessions` (PK
`session_id`, TTL `expires_at`), `nova-password-resets` (PK
`reset_token`, TTL 15m). Function URL with IAM auth.
- `nova-idp-token-vend` Lambda — accepts a PAT (or session token),
validates revocation (`nova-pats.GetItem(jti, ConsistentRead=True)`
D-229, 60s SLO), evaluates the kyverno-json ABAC policy at
`platform/abac/token-vend.policy` (D-227, INV-17), KMS-signs an
ECDSA P-256 JWT (`ES256`), converts DER→raw ECDSA signature (RFC 7515
§3.1.3), returns the OIDC token. The `policy_version` (git SHA,
D-231) is recorded in every `token.vend.allowed/denied` audit event.
- `nova-idp-jwks` Lambda (optional, separation of concerns) — function
URL with `AuthType: NONE` (public key only), `Cache-Control: max-age=3600`.
`kms.get_public_key` → DER SPKI → JWK via `cryptography`. Custom
domain + WAF via CloudFront is OPTIONAL (`--public-jwks-domain` flag
on `nova idp setup`, D-230).
- `nova-pats` DynamoDB table — PK `jti`, GSI1 `sub` (list PATs for
user), GSI2 `pat_hash` (lookup by hash). Only the hash stored (not
raw PAT, REQ-343). Revoked PATs retained for audit.
**CLI surface (`nova` package, greenfield):**
- Entry point: `[project.scripts] nova = "nova.cli:main"` (argparse-only,
no click/typer — repo convention). `nova/cli.py` auto-discovers
`nova/<module>.py` subcommands via `pkgutil.iter_modules`, dispatches,
emits the `cli.invocation` audit event (INV-12) with `mode`,
`selection_reason`, `credential_type`, `command`, `args`.
- Each `nova/<module>.py` is ≤50 lines, delegates to `core/` (CAP-034
AST scan). Subgroups: `nova auth login/revoke/status`, `nova idp
setup --check/--apply/--verify`, `nova init`, `nova apply --local`.
- `core/mode_resolver.py` — flag → env (`NOVA_CLIENT_MODE`) → credential
type → `sys.stdin.isatty()` (D-226). CLI-only; Lambdas don't resolve
modes. Property-tested with `hypothesis` (REQ-349).
- `core/env.py:+synthesize_local_env()` — synthesizes a local env dict
from a contract + `--local` flag (REQ-330). No cloud provisioning.
**Packaging (NFR-6, CAP-035):**
- CI publishes a wheel to CodeArtifact AND a Lambda layer with identical
version strings on every merge affecting `core/`/`adapters/`/`nova/`.
Version mapping recorded in SSM `/nova/layer/nova-cli/version`.
If either publish fails, the merge is blocked (REQ-323).
- `nova cli-action` composite action at
`.github/actions/nova-cli/action.yml`, referenced by both GitHub +
Gitea (`uses: continuous-intelligence/acdl/.github/actions/nova-cli@v1.28`).
Python 3.12 pinned. Byte-identical behavior verified by CI matrix
(REQ-326, NFR-11).
**Data flows:**
1. Sign-up → `nova-idp-auth` → Argon2id → `nova-users` PutItem → session
`nova-sessions` PutItem → return session token.
2. Token vend (hot path) → `nova-idp-token-vend``nova-pats` strong
read (revocation) → kyverno-json ABAC eval → if allow → KMS sign →
DER→raw → return OIDC JWT. Audit at every step.
3. JWKS fetch → `nova-idp-jwks``kms.get_public_key` → DER→JWK →
`{"keys":[...]}`. Cached 1h at CloudFront (if custom domain) / client.
4. PAT revoke → `nova auth revoke --pat <jti>` → `nova-pats.UpdateItem(
status=revoked)` → audit. Strong read on next vend → 403 (within 60s).
**`nova idp setup` (REQ-340, NFR-10):** generates a CloudFormation
template (raw dict → JSON, no troposphere dep), presents for review
(`$PAGER` + resource summary), requires explicit `y/N` approval before
`cloudformation deploy --capabilities CAPABILITY_IAM`. `--check` reports
prerequisites + IAM policy delta; `--verify` runs the KMS round-trip
test. New IAM grants required: `cloudformation:*`, `codeartifact:*`.
+4 -3
View File
@@ -1,10 +1,10 @@
{
"phase": 0,
"stage": "clarify",
"stage": "research",
"milestone": "v1.28",
"phase_role": "pre_execution",
"attempts": 0,
"updated_at": "2026-08-19T20:10:00Z",
"updated_at": "2026-08-19T20:25:00Z",
"project": "acdl",
"projects": ["acdl", "nova-blockchain-exchange"],
"active_milestone": "v1.28",
@@ -13,5 +13,6 @@
"tag_line": "v1.27.x",
"previous_milestone": {"milestone": "v1.27", "tag": "v1.26.3", "status": "complete"},
"decisions": ["D-226", "D-227", "D-228", "D-229", "D-230", "D-231"],
"notes": "v1.28 CLARIFY. 6 open Qs + 5 grounding gaps resolved at full autonomy. D-226..D-231 authored (confidence >= 0.80). kj mapped to kyverno-json (D-227). Cognito-drop reframed as greenfield (G3). CAP-033..038, INV-12..17, REQ-323..353 allocated. Next: RESEARCH."
"personas": ["backend-engineer", "security-engineer", "cli-engineer", "lead-developer"],
"notes": "v1.28 RESEARCH complete. 11 questions answered via ci-researcher subagent. D-228 amended (fail-closed, not pure-Python fallback). kj-binary-in-Lambda-layer is highest risk (P2 spike). KMS = ECC_NIST_P256 / ES256 with DER->raw conversion. nova-pats = 4th DDB table. mode_resolver uses sys.stdin.isatty(). PERSONAS.md + ARCHITECTURE.md §12.10 authored. Next: PLAN."
}
+100 -58
View File
@@ -1,77 +1,119 @@
---
project: acdl
milestone: v1.27
milestone: v1.28
generated_at: 2026-08-19
generator: lead-developer
verification_toolchain:
typecheck: "python3 -m py_compile core/confidence_signal.py 2>&1 | head -5 || true"
test: "bash scripts/run_regression.sh 2>&1 | tail -10 || true"
lint: "ruff check .ciagent/STATE.md 2>/dev/null || true"
typecheck: "python3 -m py_compile core/mode_resolver.py nova/cli.py 2>&1 | head -5 || true"
test: "pytest tests/test_mode_resolver.py tests/test_cli_subcommands.py -q 2>&1 | tail -15 || true"
lint: "ruff check nova/ core/lambda/nova_idp_*.py 2>/dev/null || true"
note: |
v1.27 is an NFR milestone (PO State Catalog & Ciagent Compression) —
a docs/chore milestone. Single active persona: lead-developer owns
the milestone narrative (STATE.md authoring, PROJECT/ROADMAP fixes,
archive moves, PLAN/NORTH_STAR wiring, final review + audit). No
code, no schema, no policy authoring. The pre-existing
core/confidence_signal.py LSP diagnostic is out of scope (not
touched by v1.27). Territory enforcement: warn.
v1.28 is a feature milestone (CLI Canonicalization + Identity Layer).
Four active personas: backend-engineer (Lambda/DynamoDB/KMS/CodeArtifact),
security-engineer (Argon2id/KMS/ABAC/threat model), cli-engineer
(subcommand surface/mode_resolver/argparse/CAP-034), lead-developer
(plan/review/ship/capability gate). frontend-engineer + data-engineer
deactivated (no UI, no data pipelines). The kj-binary-in-Lambda-layer
risk (D-227, RESEARCH §7) is the highest-risk item; P2 spike confirms.
---
# PERSONAS — v1.27 PO State Catalog & Ciagent Compression
# Personas — v1.28 CLI Canonicalization + Identity Layer
> Generated by the lead-developer at the end of RESEARCH. Assesses the
> project domains, activates/deactivates personas, aligns frameworks +
> territory + constraints to the actual project structure.
## Roster
## Active Roster (1)
### backend-engineer
```yaml
active: true
domain: "Lambda functions, DynamoDB, KMS integration, dual-use packaging, CodeArtifact publish, CloudFormation generation"
frameworks: ["Python 3.12", "boto3", "argparse", "pytest", "moto[dynamodb]", "CloudFormation"]
constraints: ["INV-15", "INV-16", "INV-17", "D-228", "D-229", "D-230", "NFR-5", "NFR-6", "NFR-7", "NFR-8"]
territory:
- "core/lambda/**"
- "core/metrics/**"
- "core/env.py"
- "core/outbox_writer.py"
- "terraform/bootstrap/**"
- ".gitea/workflows/publish.yml"
- ".github/workflows/publish.yml"
- ".github/actions/nova-cli/**"
```
### 1. lead-developer (active)
- **active:** true
- **phase_specific:** false
- **reason:** Owns the full v1.27 milestone narrative: STATE.md
authoring (PO-facing capability catalog, 36 entries across 10
domains + 11 invariants), archive moves (11 files to
`.ciagent/archive/` + 1 to consumer archive), PROJECT.md + ROADMAP.md
phase-status corrections, archive/README.md contents update,
PLAN.md + ROADMAP.md + NORTH_STAR.md ship-discipline wiring, final
review + audit.
- **domain:** `.ciagent/` docs (STATE.md, PROJECT.md, ROADMAP.md,
PLAN.md, NORTH_STAR.md, archive/README.md), consumer
`.ciagent/nova-blockchain-exchange/` (PROJECT.md pointer,
archive/ROADMAP-v1.26.md).
- **frameworks:** markdown, JSON (CHECKPOINT.json, config.json).
- **territory:** `.ciagent/`, `docs/`.
- **constraints:** no code changes (NFR milestone, D-220); no schema
changes; archive moves are lossless (byte-identical relocation, git
history preserves authoritative state); STATE.md is additive only.
### security-engineer
```yaml
active: true
domain: "Argon2id hashing, KMS asymmetric signing (ECDSA P-256 / ES256), ABAC policy, JWKS exposure, PAT lifecycle, threat model, DER→raw ECDSA conversion"
frameworks: ["argon2-cffi", "cryptography", "pyjwt", "kyverno-json", "JMESPath", "KMS Sign/Verify/GetPublicKey"]
constraints: ["INV-15", "INV-16", "INV-17", "NFR-5", "NFR-8", "NFR-9", "D-227", "D-231"]
territory:
- "platform/abac/**"
- "core/policy_engine.py"
- "adapters/kyverno-json/**"
- "core/lambda/nova_idp_auth.py"
- "core/lambda/nova_idp_token_vend.py"
- "core/lambda/nova_idp_jwks.py"
- "docs/threat-model.md"
```
## Deactivated (5)
### cli-engineer
```yaml
active: true
domain: "CLI subcommand surface, mode_resolver, argparse, [project.scripts] entry-point, CAP-034 AST scan, nova auth/idp subgroups, property tests"
frameworks: ["Python 3.12", "argparse", "setuptools [project.scripts]", "hypothesis", "pkgutil"]
constraints: ["INV-12", "INV-13", "INV-14", "D-226", "NFR-1", "NFR-2", "NFR-3"]
territory:
- "nova/**"
- "core/mode_resolver.py"
- "pyproject.toml"
- "tests/test_mode_resolver.py"
- "tests/test_cli_subcommands.py"
```
### backend-engineer (inactive)
- **active:** false
- **reason:** No code changes in v1.27. The pre-existing
`core/confidence_signal.py` LSP diagnostic is out of scope (not
touched by v1.27).
### lead-developer
```yaml
active: true
domain: "Phase plan, persona roster, review gates, milestone ship, capability gate (CAP-033..038), ROADMAP/STATE/PROJECT wiring"
frameworks: ["git", "Gitea Actions", "semver tagging", ".ciagent/ discipline"]
constraints: ["INV-1..17 (cross-cutting)", "v1.28 hard constraints", "NFR-6", "NFR-11"]
territory:
- ".ciagent/**"
- "PLAN.md"
- "CHECKPOINT.json"
- "STATE.md"
- "REQUIREMENTS.md"
- "ROADMAP.md"
```
### data-engineer (inactive)
- **active:** false
- **reason:** No schema, migration, or ORM changes.
### frontend-engineer
```yaml
active: false
phase_specific: false
reason: "No UI in v1.28 (CLI + JSON endpoints only). JWKS serves application/json; no HTML/CSS/JS surface."
```
### policy-engineer (inactive)
- **active:** false
- **reason:** No policy authoring. STATE.md Domain 3 catalogues
existing v1.25 + v1.26 policies (descriptive, not authoring).
### data-engineer
```yaml
active: false
phase_specific: false
reason: "No data pipelines / metrics / PowerBI work in v1.28. The metrics layer is v1.17-complete; v1.28 adds audit events but no new fact/dim tables."
```
### frontend-engineer (inactive)
- **active:** false
- **reason:** No UI. Deactivated since v1.26 (PERSONAS.md:141).
## Territory overlap notes
### blockchain-engineer (inactive)
- **active:** false
- **reason:** No chain code. The v1.26 pilot is shipped; v1.27 is
platform-side docs/chore only.
- `core/lambda/contract_ingestor.py` (dual-use refactor, REQ-329) =
backend-engineer territory. `core/lambda/nova_idp_auth.py` +
`nova_idp_token_vend.py` are **co-owned** by backend-engineer (Lambda
plumbing, DynamoDB, function URLs) + security-engineer (crypto, ABAC,
Argon2id logic inside).
- `core/mode_resolver.py` = cli-engineer. `core/policy_engine.py` =
security-engineer (the ABAC evaluation path).
- `nova/idp/setup.py` = cli-engineer (the subcommand + arg parsing) +
backend-engineer (the CloudFormation generation + deploy).
- `nova/auth/*` = cli-engineer (subcommands) + security-engineer (the
token exchange + credential storage logic).
## Territory Enforcement
## Phase-specific personas
- **Mode:** `warn` (the milestone is `.ciagent/`-only; the lead-
developer owns all writes; no cross-territory collisions expected).
None. All four active personas span the full milestone. The
security-engineer is heaviest in P2 (identity layer) + P3 (threat model);
the cli-engineer is heaviest in P1 (CLI substrate); the backend-engineer
spans P1 (CodeArtifact/layer) + P2 (Lambdas/DynamoDB).
+320 -128
View File
@@ -1,144 +1,336 @@
# Nova — v1.27 Research Findings
# Nova — v1.28 Research Findings
> Phase: research (pre-execution). Milestone: v1.27 (PO State Catalog &
> Ciagent Compression). Status: research. Researcher: ci-researcher.
> Phase: research (pre-execution). Milestone: v1.28 (CLI Canonicalization
> + Identity Layer). Status: research. Researcher: ci-researcher.
> Autonomy: full.
v1.27 is an NFR milestone (docs/chore only, no code, no schema). There
is no new domain to research. The research is a codebase-grounded
inventory of (a) the files to archive + their staleness evidence, and
(b) the sources backing the STATE.md capability backfill. This file
records the inventory for the v1.27 record; the active authoring used
these sources directly.
>
> Research delegated to the ci-researcher subagent (full domain/ecosystem
> research with web citations). This file is the curated summary; the
> full 868-line research document is preserved in git history (the
> subagent's task output). Key findings + recommendations are below.
---
## 1. Files to archive (staleness inventory)
## §1 — Codebase Inventory (grounding)
### 1.1 Pre-execution artifacts (v1.26 — shipped, decisions folded)
### 1.1 `core/` modules (the REQ-324 subcommand surface)
| File | Lines | Staleness evidence | Decisions folded into |
|---|---|---|---|
| `CLARIFY.md` | 225 | v1.26 milestone shipped (`v1.25.5`); decisions D-200..D-213 | `PROJECT.md` load-bearing decisions |
| `GRILL.md` | 225 | v1.26 grill verdict PROCEED 0.84; binding revisions applied | `PLAN.md` revisions (G-Q4 REQ-322→P2 W0; G-Q6 enforcement deferred; G-Q9 key-split future) |
| `IDEATE.md` | 193 | all 7 accepted ideas → REQ-315..322 (shipped) | `REQUIREMENTS.md` v1.26 traceability |
| `RESEARCH.md` | 250 | v1.26 domain research (blockchain, deploy, modules, metrics) | `ARCHITECTURE.md` §12.8; shipped REQs |
19 Python files under `core/` (plus `core/lambda/`, `core/metrics/`).
Two already have `_cli.py` companions (`contract_resolver_cli.py` 40
lines, `regression_verify_cli.py` 32 lines) — the thin-delegate
precedent for `nova/<module>.py`. **No `nova/` dir, no `bin/`, no
`[project.scripts]` entry exists today.** The CLI is greenfield.
All four are pre-execution artifacts for a shipped milestone. The next
P0 writes fresh versions. Per D-219 (user-confirmed) + D-222: archive
all four with `-v1.26` suffixes.
### 1.2 Existing Lambda pattern (`core/lambda/contract_ingestor.py`)
### 1.2 Phase verifications + review (v1.26 — shipped, PASS)
521 lines. Function URL + IAM auth (D-051). DynamoDB via lazy
module-global `boto3.resource`. Secrets Manager for tokens. Schema
validation in-Lambda. **`__main__` block already does CLI dispatch**
(`--check-readiness``core.submission_readiness.cli_main`) — this is
the dual-use precedent for REQ-329. Local testing via
`core/local_emulators.py:LocalLambdaStub`.
| File | Lines | Staleness evidence |
### 1.3 `core/env.py` — getter, not synthesizer
31 lines. `get_env(name, default)` reads `NOVA_<name>` from `os.environ`.
**REQ-330 needs a NEW `synthesize_local_env()` function** added here.
The closest existing pattern is `core/onboarding.py:generate_env_file()`.
### 1.4 `PolicyEngine` Protocol + `KyvernoJsonEngine` (the ABAC substrate)
`core/policy_engine.py`: `PolicyEngine` Protocol with `evaluate(payload,
policy_dir, contract_id) -> list[dict]`. `KyvernoJsonEngine` shells to
`kj scan --policy <dir> --payload <file> --output json`. Policy shape =
`ValidatingPolicy` (`apiVersion: json.kyverno.io/v1alpha1`) with
`spec.rules[].assert.all[].check` using JMESPath. Severity from
`metadata.annotations["nova.cloudinit.dev/severity"]`. **The payload
can be ANY JSON** — not just contracts (the v1.25 design point). This
is what makes kyverno-json usable for ABAC token vending (D-227).
### 1.5 `pyproject.toml` state
name `nova`, version `1.14.0`, requires-python `>=3.10` (spec wants
3.12 — bump needed for REQ-326). setuptools build backend. No
`[project.scripts]`, no `[tool.setuptools.packages.find]` — both needed.
Deps: `boto3`, `jsonschema`, `pyyaml`. No `argon2-cffi`, `cryptography`,
`pyjwt`, `click`/`typer`**argparse-only** is the repo convention.
### 1.6 Forge conventions
`.github/workflows/` + `.gitea/workflows/` kept byte-identical. Python
3.12 already pinned via `actions/setup-python@v5`. No composite action
exists yet — `nova cli-action` (REQ-326) is greenfield.
### 1.7 IAM baseline (load-bearing for REQ-340)
`.ciagent/IAM_POLICY.md` + `terraform/bootstrap/spike_runner_policy.json`.
The `nova-spike-runner` principal already has KMS (incl. `CreateKey`,
`Sign`, `GetPublicKey`), Lambda (incl. `PublishLayerVersion`), DynamoDB
grants. **New grants needed:** `cloudformation:*` (for `nova idp setup
--apply`) + `codeartifact:*` (for the wheel publish pipeline). Flagged
for P1/P2.
---
## §2 — CodeArtifact + Lambda Layer Pipeline (REQ-323)
**Recommendation:** single CI job on merge to `main` affecting
`core/**`/`adapters/**`/`nova/**`/`pyproject.toml`. Build wheel
(`python -m build --wheel`) → `twine upload` to CodeArtifact → build
layer (`pip install --target layer/python/ dist/nova-*.whl argon2-cffi
cryptography pyjwt`) → `aws lambda publish-layer-version` → record
version mapping in SSM `/nova/layer/nova-cli/version` (CAP-035). If
either publish fails, the job fails (merge blocked, REQ-323 AC).
**Atomicity:** wheel publish is idempotent (pin version to
`<semver>+<sha7>`); layer publish retries on failure. CAP-035 reads the
SSM parameter to verify layer-version ↔ wheel-version match.
**Risks:** CodeArtifact not yet provisioned in `581513795199` (CLARIFY
assumption #1); `codeartifact:*` grant missing. Fallback: Gitea-hosted
wheel index. Layer `--compatible-architectures`: build x86_64 only for
v1.28 (aarch64 only if Graviton Lambda needed).
---
## §3 — CLI Subcommand Architecture (REQ-324)
**Recommendation:** three-layer. `nova/__init__.py` (marker) →
`nova/cli.py` (~80 lines, auto-discovers `nova/<module>.py` via
`pkgutil.iter_modules`, dispatches, emits `cli.invocation` audit event)
`nova/<module>.py` (≤50 lines each, exports `add_parser(subparsers)`
+ `run(args) -> int`, delegates to `core/`). Entry point:
`[project.scripts] nova = "nova.cli:main"`. **argparse-only** (no
click/typer — repo convention).
**CAP-034 AST scan:** ≤50 lines; ≤3 function defs; every `ast.Call`
resolves to a `core.` import; no conditionals beyond `if __name__`.
**Subcommand groups:** `nova auth`, `nova idp`, `nova metrics` =
nested subparsers (same pattern, one level deeper).
**setuptools:** add `[tool.setuptools.packages.find]` including `nova`,
`nova.*`, `core`, `core.*`, `adapters.*`.
---
## §4 — Argon2id in Lambda Python 3.12 (REQ-334, D-228)
**Findings:** `argon2-cffi-bindings` v25.1.0 ships `cp39-abi3`
manylinux x86_64 + aarch64 wheels — **ABI-stable, compatible with
Python 3.9..3.13**. Lambda Python 3.12 runs Amazon Linux 2023 (glibc
2.34 ≥ 2.28 required). **The abi3 manylinux wheel loads cleanly.**
Confidence: 0.92.
**D-228 AMENDMENT:** the "pure-Python fallback" clause is **weaker than
stated** — there is no maintained pure-Python Argon2 implementation. A
pure-Python crypto fallback is a **liability** (weaker hashing,
violates INV-16's spirit). Revised recommendation:
1. **Primary:** bundled manylinux abi3 wheel in the `nova-cli` Lambda
layer. Works. Confidence 0.92.
2. **Fallback:** detect `ImportError` at Lambda cold-start → **fail
closed** (503, refuse sign-ups). The Lambda health check reports
C-extension status. **Do NOT ship a pure-Python fallback.**
3. **Escape hatch:** Fargate (~1 week, per CLARIFY Q1).
Lambda memory ≥ 512 MB (Argon2id memory_cost ~20 MB + overhead).
---
## §5 — KMS Asymmetric Signing for OIDC Tokens (REQ-337)
**Recommendation: key spec = `ECC_NIST_P256`, alg = `ECDSA_SHA_256`
(JWS `ES256`).** RSA-2048 is larger + slower; P-256 is RFC 7518's
recommended JWT alg. Signature size 64 bytes (vs RSA 256). JWKS
compactness matters (fetched often).
**The #1 gotcha:** KMS returns DER-encoded ECDSA signatures; **JWS
requires raw r‖s concatenation** (RFC 7515 §3.1.3). The token-vend
Lambda converts via `cryptography.hazmat.primitives.asymmetric.utils.
decode_dss_signature``r.to_bytes(32) + s.to_bytes(32)`. ~5 lines.
Flagged for the threat model (REQ-347) + KMS round-trip test (REQ-350).
**Flow:** validate PAT → ABAC eval → build JWT header/payload →
`kms.sign(Message=signing_input, MessageType="RAW", SigningAlgorithm=
"ECDSA_SHA_256")` → DER→raw → JWT. `kid` = KMS key alias.
**Verification:** use `pyjwt` (`jwt.decode` handles JWK→key natively);
`cryptography` only for SPKI→JWK in the JWKS Lambda.
**Rotation:** manual, 90 days (matches D-069 CMK cadence). New key +
re-point alias + JWKS serves both `kid`s during overlap.
---
## §6 — JWKS Endpoint (REQ-338, D-230)
**D-230 confirmed.** Lambda function URL (`AuthType: NONE` — JWKS is
public-key only) + reserved concurrency 10 (max 100 RPS, JWKS is
cached client-side). `Cache-Control: max-age=3600`. Separate tiny
`nova-idp-jwks` Lambda (separation of concerns).
**Custom domain + WAF = OPTIONAL** via `--public-jwks-domain <domain>`
flag on `nova idp setup`. Without it, raw function URL (acceptable for
v1.28 pilot). With it: CloudFront + ACM + WAF rate-based rule (>100
req/5min per IP) + Route53 ALIAS. Adds ~8 CloudFormation resources.
**Defer API Gateway** (D-230) — $3.50/M + complexity for no benefit at
v1.28 volume.
---
## §7 — kyverno-json ABAC Policy (REQ-339, D-227)
**D-227 confirmed.** Policy at `platform/abac/token-vend.policy` =
`ValidatingPolicy` with JMESPath checks against a payload of
`{subject, requested_claims, target_resource, environment, pat_jti,
policy_version}`. Decision logic: any `fail` PCR with severity
`critical` → deny (403 + audit); all pass → allow → KMS sign.
**`policy_version` (D-231):** git SHA of the policy file, baked into
the Lambda layer, recorded in every `token.vend.allowed/denied` audit
event.
**BIGGEST PACKAGING RISK:** the token-vend Lambda needs the `kj` Go
binary (~40 MB) on PATH. Bundle it in the `nova-cli` Lambda layer
(`wget` the Linux amd64 release into `layer/bin/kj`). `KyvernoJsonEngine
.is_configured()` checks `which kj``/opt/bin/kj` (layer mount). P2
spike confirms it runs in AL2023 Lambda. Fallback: Fargate. Confidence
0.75 — needs the spike.
---
## §8 — PAT Lifecycle (REQ-342, REQ-343, REQ-344)
**PAT = signed JWT** (KMS-signed, `typ: "developer_pat"` distinguishes
from `nova_oidc_token` per INV-14). Claims: `iss, sub, typ, jti, iat,
exp, roles, owner`.
**`nova-pats` DynamoDB table** (4th table): PK=`jti`, GSI1=`sub` (list
PATs for user), GSI2=`pat_hash` (lookup by hash). Only the hash stored
(not raw PAT). Revoked PATs retained for audit.
**Revocation (D-229 CLARIFIED):** GSIs don't support strongly-consistent
reads. The token-vend Lambda extracts `jti` from the PAT JWT (decode
without verifying — signature verified separately) →
`GetItem(PK=jti, ConsistentRead=True)` on the main table. Satisfies the
60s SLO. Confidence 0.90.
**CLI:** `nova auth login` (session→OIDC token, store locally),
`nova auth revoke --pat <jti>`, `nova auth status` (active credential,
mode, selection_reason). Local file `~/.nova/credentials.json` (0600,
never to stdout, in `.gitignore`). "Most recent wins" (D-226 Q5) =
`active_credential_jti` field.
---
## §9 — `nova idp setup` CloudFormation (REQ-340, REQ-341)
**Template (raw dict → JSON, no troposphere dep):** 2-3 Lambdas, 4
DynamoDB tables (`nova-users`, `nova-sessions`, `nova-password-resets`,
`nova-pats`), KMS key `alias/nova-oidc-signing` (ECC_NIST_P256),
function URLs, IAM roles, optional CloudFront/WAF/ACM.
**`--check`:** validates prerequisites (AWS creds, CFN perms, KMS perms,
layer exists via CAP-035). Prints required IAM policy delta.
**`--apply`:** generate → print to temp file + resource summary →
`$PAGER``Apply? [y/N]``cloudformation deploy --capabilities
CAPABILITY_IAM`. NFR-10 satisfied by the explicit prompt.
**`--dry-run`:** resource list only, no write.
**`--verify`:** runs the KMS round-trip test (REQ-350).
**New IAM grants needed:** `cloudformation:*`, `iam:CreateRole`/`PassRole`,
`lambda:CreateFunction`/`CreateFunctionUrlConfig`,
`dynamodb:CreateTable`, `kms:CreateKey`/`CreateAlias`, `ssm:PutParameter`.
---
## §10 — GitHub + Gitea Marketplace Composite Action (REQ-326)
**Single `action.yml`** at `.github/actions/nova-cli/action.yml`,
referenced by both GitHub + Gitea via `uses: continuous-intelligence/
acdl/.github/actions/nova-cli@v1.28`. Composite action: `setup-python@v5`
(python 3.12) → CodeArtifact login + `pip install nova``nova
${{ inputs.command }}`. `NOVA_CLIENT_MODE` env from input.
**Byte-identical test (REQ-326 AC2):** CI matrix runs the action on
GitHub `ubuntu-latest` + Gitea `act_runner` with same inputs; assert
same stdout/exit code.
**Risk:** Gitea `actions/checkout`/`setup-python` may need Gitea
mirrors (`https://gitea.com/actions/...`). P1 test on the actual Gitea
instance. Confidence 0.70.
---
## §11 — `mode_resolver` Priority (REQ-327, D-226)
**TTY detection: check `sys.stdin.isatty()`** (NOT stdout). Edge 3
(`nova apply | tee log.txt`): stdout piped, stdin is TTY → user is
present → `interactive` (correct). `sys.stdout.isatty()` would
misresolve to `agent`. **`stdin` answers "is a human at a terminal?"**
**Credential type detection:** read `~/.nova/credentials.json`
`active_credential_jti`'s `type` (`developer_pat`/`nova_oidc_token`).
Both + TTY → `interactive`; + no TTY → `agent` (INV-14).
**Property tests (REQ-349):** `hypothesis` with strategies for
flag/env/cred/tty. Properties: deterministic (INV-13), flag-wins,
invalid-env-ignored, no-silent-fallback (every resolution has a
non-empty `selection_reason`).
**`mode_resolver.py` lives in `core/`** (not `nova/`) so Lambdas could
import it, but **it's CLI-only** — the token-vend Lambda doesn't resolve
modes.
---
## §12 — Persona Assessment
See `.ciagent/PERSONAS.md` for the full YAML roster. Summary:
- **Deactivate** frontend-engineer (no UI) + data-engineer (no data
pipelines in v1.28).
- **Activate** backend-engineer (Lambda/DynamoDB/KMS/CodeArtifact) +
lead-developer (plan/review/ship).
- **Add** security-engineer (Argon2id/KMS/ABAC/threat model) +
cli-engineer (subcommand surface/mode_resolver/argparse/CAP-034).
---
## §13 — Architecture Sketch (ARCHITECTURE.md §12.10)
See `.ciagent/ARCHITECTURE.md` §12.10 (appended this stage). New
greenfield files: `nova/` CLI package, `platform/abac/token-vend.policy`,
`core/mode_resolver.py`, `core/env.py:+synthesize_local_env()`,
`core/lambda/nova_idp_{auth,token_vend,jwks}.py`, `tests/test_*`,
`docs/{operator-guide-idp,developer-guide-auth,threat-model}.md`.
---
## Decisions re-validated / amended
| Decision | Status | Change |
|---|---|---|
| `VERIFY-P03.md` | 39 | v1.26 P3 verification — PASS; shipped `v1.25.3` |
| `VERIFY-P04.md` | 31 | v1.26 P4 verification — PASS; shipped `v1.25.4` |
| `REVIEW-AUDIT-P05.md` | 218 | v1.26 P5 final review + audit — PROCEED; shipped `v1.25.5`; 0 P0 remain; audit CLEAN |
| `P4-PILOT-RUN-EVIDENCE.md` | 46 | v1.26 live apply evidence (`blkex-pilot-apply-v0.2`); summarized in `nova-blockchain-exchange/README.md` §5 + REVIEW-AUDIT-P05 §2.2 |
| D-226 | re-validated + refined | `sys.stdin.isatty()` is the TTY check (not stdout) |
| D-227 | re-validated | `kj` Go binary bundled in Lambda layer — packaging risk flagged |
| D-228 | **amended** | Pure-Python fallback → fail-closed + Fargate (pure-Python crypto is a liability) |
| D-229 | re-validated + clarified | Strong read on main table PK (`jti`), not GSI (GSIs don't support strong reads) |
| D-230 | re-validated | CloudFront/WAF/ACM made optional via `--public-jwks-domain` flag |
| D-231 | re-validated | `policy_version` (git SHA) in the ABAC payload |
### 1.3 Durable references (superseded or stale)
| File | Lines | Staleness evidence | Superseded by |
|---|---|---|---|
| `CAPABILITY_INVENTORY.md` | 120 | dated 2026-07-27; framed as "v1.1→v1.8 re-verification sweep"; predates v1.26 pilot (CAP-025 absent; blockchain capabilities absent) | `STATE.md` (this milestone) |
| `AUTONOMY_THESIS.md` | 65 | "Last refined: v1.21"; thesis fully folded into `NORTH_STAR.md` Vision (lines 1722) + Anti-Goals #2 | `NORTH_STAR.md` |
| `COST.md` | 106 | dated 2026-07-29; framed "v1.0 → v1.14"; predates v1.26 live pilot (ECS + ALB + DynamoDB + S3 costs not reflected) | A future cost milestone writes a fresh report; `STATE.md` Domain 7 notes cost tracking as a capability |
### 1.4 Consumer-side (nova-blockchain-exchange)
| File | Lines | Staleness evidence |
|---|---|---|
| `nova-blockchain-exchange/ROADMAP.md` | 57 | marks P3/P4/P5 as "planned" but v1.26 shipped (`v1.25.5`); phase narrative preserved in platform `ROADMAP.md` v1.26 section |
Per D-221: consumer archives land in
`.ciagent/nova-blockchain-exchange/archive/ROADMAP-v1.26.md`.
**New recommendations for PLAN/GRILL to formalize (no D-ID yet):**
- KMS key spec = `ECC_NIST_P256`, alg `ES256`; DER→raw ECDSA conversion required.
- `nova-cli` Lambda layer bundles the `kj` Go binary (~40 MB).
- `nova-pats` = 4th DynamoDB table; PK=`jti`, GSI1=`sub`, GSI2=`pat_hash`.
- `sys.stdin.isatty()` is the TTY heuristic.
- `[project.scripts] nova = "nova.cli:main"`; argparse-only.
- `cloudformation:*` + `codeartifact:*` = new IAM baseline grants (P1/P2).
---
## 2. Files to keep active (no-edit or fix-only)
## RESEARCH complete
### 2.1 No-edit (live code paths or durable)
| File | Why keep active |
|---|---|
| `CHECKPOINT.json` | Authoritative resume state — never archive |
| `config.json` | Operational config — never archive |
| `REGRESSION_REPORT.json` | Written by `core/regression_verify.py:705`; read by `core/metrics/collector.py:27` + `trust_snapshot.py:21` + metrics views (D-224: regenerates on next `run_regression.sh`) |
| `REGRESSION_REPORT.md` | Written by `core/regression_verify.py:704`; read by `scripts/run_regression.sh` (D-224) |
| `PERSONAS.md` | Regenerated at each milestone P0 by the lead-developer; not stale until then |
| `IAM_POLICY.md` | Live baseline, test-enforced (`tests/test_iam_policy_baseline.py`); D-207 future key-split pending (D-223) |
| `PLAN.md` | Active phase plan; reset to next milestone at next P0 |
| `ARCHITECTURE.md` | Durable target architecture (§1–§12 + §12.7 + §12.8 + §12.9) |
| `NORTH_STAR.md` | PO strategy; loaded every ci-run via `config.strategic_direction_file` |
| `nova-blockchain-exchange/PROJECT.md` | Consumer project charter; D-200..D-205 load-bearing |
| `nova-blockchain-exchange/REQUIREMENTS.md` | REQ-310..322 spec intent (shipped but spec stays for reference) |
| `nova-blockchain-exchange/README.md` | Consumer onboarding guide; still accurate (deploy workflow, secrets, contract shape, verification) |
### 2.2 Fix-only (corrections to stale-but-kept files)
| File | Fix |
|---|---|
| `PROJECT.md` | v1.26 phase-status block (lines 424431): P3/P4/P5 "pending" → "complete" with shipped tags `v1.25.3/4/5`; add STATE.md pointer (D-225: P2 phase) |
| `ROADMAP.md` | v1.26 P3/P4/P5 sections (lines 238, 261, 272) "planned" → "complete" with shipped tags; v1.26 Overview line "active" → "complete"; add STATE.md to P5 ship-update list (D-225: P2 phase) |
| `archive/README.md` | Add the 11 new archived files to the contents tables (P2 phase) |
---
## 3. STATE.md capability backfill sources
The STATE.md backfill (36 capabilities across 10 domains) was sourced
from:
| Source | Used for |
|---|---|
| `core/regression_verify.py` (lines 129768) | CAP-001..CAP-025 IDs, names, tiers, evidence pointers |
| `.ciagent/CAPABILITY_INVENTORY.md` (pre-archive) | CAP-001..022 descriptions, defect notes, evidence |
| `modules/registry.json` | L1/L2 module catalog (13 L1 + 2 L2 entries) |
| `.ciagent/REQUIREMENTS.md` v1.25 traceability | REQ-291..309 → policy-engine capabilities |
| `.ciagent/nova-blockchain-exchange/REQUIREMENTS.md` + `.ciagent/REQUIREMENTS.md` v1.26 traceability | REQ-310..322 → pilot capabilities |
| `.ciagent/CHECKPOINT.json` | shipped tags `v1.25.0..v1.25.5` |
| `git log --all --oneline` | file paths for v1.26 shipped features |
| `.ciagent/PROJECT.md` load-bearing decisions | INV-1..INV-11 invariants |
| `docs/submission-readiness.md` + `schemas/contract.schema.json` | INV-1 contract surface |
---
## 4. Persona assessment
v1.27 is a docs/chore milestone. The active roster:
- **lead-developer** (active): owns the milestone narrative (STATE.md
authoring, PROJECT/ROADMAP fixes, archive README, PLAN/NORTH_STAR
wiring, this RESEARCH, CLARIFY, PLAN, final review + audit). Territory:
`.ciagent/`, `docs/`.
- **backend-engineer** (active, limited): no code changes in v1.27.
Consulted on the `core/confidence_signal.py` LSP diagnostic (pre-
existing, not touched by v1.27). No territory writes.
- **data-engineer** (inactive): no schema/migration/ORM changes.
- **policy-engineer** (inactive): no policy authoring.
- **frontend-engineer** (inactive): no UI.
- **blockchain-engineer** (inactive): no chain code.
Territory enforcement: warn. The milestone is `.ciagent/`-only; the
lead-developer owns all writes.
---
## 5. Risk analysis
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Archive move breaks a relative path reference in an active file | Low | Medium | `grep` for the archived filenames across `.ciagent/` + `docs/` before commit; fix any dangling references in P2 |
| STATE.md capability row is inaccurate (wrong shipped tag / wrong file path) | Medium | Low | The backfill sources are the authoritative registries (regression_verify.py, registry.json, CHECKPOINT.json, git log); citations are direct |
| PROJECT.md phase-status fix conflicts with a future v1.26-era commit | Low | Low | v1.26 is shipped (main has the milestone merge); no v1.26-era commits will arrive |
| REGRESSION_REPORT stale state is mistaken for v1.27 scope | Low | Low | D-224 records the decision; STATE.md Domain 7 notes the current CAP range |
---
## 6. Verdict
v1.27 is feasible, scoped, and the sources are grounded. No new domain,
no new code, no schema breaks. The archive moves are lossless (git
history + archive directory both preserve bytes). The STATE.md
backfill is sourced from authoritative registries. Proceed to PLAN.
All 11 research questions answered with cited findings + concrete
recommendations + risks. D-228 amended (fail-closed, not pure-Python
fallback). The `kj` binary packaging is the highest-risk item (P2
spike). Next: PLAN.