Files
acdl/.ciagent/RESEARCH.md
T
Jon Chery c629809d75 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---
2026-08-19 22:05:24 +00:00

336 lines
14 KiB
Markdown

# Nova — v1.28 Research Findings
> Phase: research (pre-execution). Milestone: v1.28 (CLI Canonicalization
> + Identity Layer). Status: research. Researcher: ci-researcher.
> Autonomy: full.
>
> 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 — Codebase Inventory (grounding)
### 1.1 `core/` modules (the REQ-324 subcommand surface)
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.
### 1.2 Existing Lambda pattern (`core/lambda/contract_ingestor.py`)
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`.
### 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 |
|---|---|---|
| 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 |
**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).
---
## RESEARCH complete
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.