Compare commits

...

113 Commits

Author SHA1 Message Date
CIAgent 5a94c968bd merge(milestone): v1.31 Leadership Deck Polish II to main (release v1.30.2)
acdl-ci / Lint (push) Successful in 13s
acdl-ci / Test (push) Failing after 24s
acdl-ci / Platform check-only (offline) (push) Successful in 21s
nova-release / Compute semver + update tags (push) Successful in 55s
Nova Slides Render / render (push) Failing after 12m34s
acdl-ci / forge_parity_disabled (push) Failing after 13m57s
2026-08-20 14:58:59 +00:00
CIAgent 20cdec8fab merge(phase/01): v1.31 P1 deck-polish complete (REQ-373.1..4, D-247)
Nova Slides Render / render (push) Failing after 25s
2026-08-20 14:58:56 +00:00
CIAgent c179c3e09a docs(milestone): complete v1.31 Leadership Deck Polish II (release v1.30.2)
---ci---
project: acdl
phase: 1
milestone: v1.31
status: complete
requirements:
  covered: [REQ-373.1, REQ-373.2, REQ-373.3, REQ-373.4]
  partial: []
---/ci---
2026-08-20 14:58:52 +00:00
CIAgent a112d6f143 docs(P01): verify complete — smoke test PASS (REQ-373.1..4)
---ci---
project: acdl
phase: 1
milestone: v1.31
status: verify
---/ci---
2026-08-20 14:58:25 +00:00
CIAgent ad280b181b feat(P01): polish leadership deck visible prose + layout (v1.31, D-247)
Enrich on-slide body wording on all 7 slides (slides 2-6 were sparse,
43-67 visible words; now 97-168). Re-balance layout: title -> frame ->
body -> diagram -> closing italic benefit. Invariants preserved:
7 slides, S&P theme tokens, speaker-note bands, [1] citations 3/5/7,
diagram PNGs, footer string. PPTX re-rendered. Smoke test PASS.

REQ-373.1 visible prose density enriched
REQ-373.2 layout improved (renderer block vocabulary unchanged)
REQ-373.3 invariants preserved (D-247)
REQ-373.4 PPTX re-rendered; check_leadership_deck.sh exits 0

---ci---
project: acdl
phase: 1
milestone: v1.31
status: execute
---/ci---
2026-08-20 14:58:18 +00:00
CIAgent 2ea9fb4e51 merge(phase/00): v1.31 P0 pre-execution complete (specify+clarify+research+plan)
Nova Slides Render / render (push) Failing after 28s
2026-08-20 14:57:00 +00:00
CIAgent 52b16bbef1 docs(P00): clarify + research + plan — D-247 invariants locked
---ci---
project: acdl
phase: 0
milestone: v1.31
status: plan
---/ci---
2026-08-20 14:56:53 +00:00
CIAgent 68908d7f6a docs(init): validate specification
---ci---
project: acdl
phase: 0
milestone: v1.31
status: specify
---/ci---
2026-08-20 14:56:47 +00:00
CIAgent 85cc962fda merge(phase/05): complementary mermaid diagrams — relationships + stack positioning
acdl-ci / Lint (push) Successful in 11s
acdl-ci / Test (push) Failing after 21s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
nova-release / Compute semver + update tags (push) Failing after 24s
Nova Slides Render / render (push) Failing after 10m11s
nova-publish / Build + push kj ECR image (KJ-STATIC, D-239) (push) Failing after 10m20s
acdl-ci / forge_parity_disabled (push) Failing after 11m30s
nova-publish / Publish wheel + Lambda layer + Lambda zip + Release (push) Has been skipped
---ci---
project: acdl
phase: 5
milestone: v1.30
status: complete
---/ci---
2026-08-20 14:46:31 +00:00
CIAgent ba28017f5b feat(P05): re-author 7 mermaid diagrams as complementary visuals
Diagrams now show relationships + stack positioning, not just restating
slide text:
- slide 1: the widening gap (velocity vs coordination surface) → Nova absorbs
- slide 2: Nova's position in the stack (SDLC/PDLC → contract → Nova → prod)
- slide 3: inheritance tree (two tenets → 6 architectural elements they shape)
- slide 4: confidence-threshold ladder (dev 0.50 → qa 0.75 → prod 0.90 → dr 0.95 + escalation)
- slide 5: stack layers with the contract as the dividing line (above/below)
- slide 6: the arc with what each milestone unlocks + the constant contract surface
- slide 7: proven foundation → runway arc → ask → structural risk if not

Fixed: slide 4 node/subgraph naming collision (DR → DRENV);
slides 2 + 5 reworked to horizontal layout (were too tall for slides).

---ci---
project: acdl
phase: 5
milestone: v1.30
status: execute
---/ci---
2026-08-20 14:46:26 +00:00
CIAgent 17903973aa chore(compress): archive v1.30 phase files — active root 17 → 8 files
acdl-ci / Lint (push) Successful in 9s
acdl-ci / forge_parity_disabled (push) Successful in 11s
acdl-ci / Platform check-only (offline) (push) Successful in 19s
nova-release / Compute semver + update tags (push) Failing after 11m15s
acdl-ci / Test (push) Failing after 11m26s
Archived (snapshotted to .ciagent/archive/, removed from active root):
- CLARIFY.md → CLARIFY-v1.30.md (D-241..D-246)
- RESEARCH.md → RESEARCH-v1.30.md (render pipeline, mermaid, vision)
- GRILL.md → GRILL-v1.30.md (PROCEED 0.88, G-1..G-4)
- PLAN.md → PLAN-v1.30.md (P1 4 waves + P3 polish)
- PERSONAS.md → PERSONAS-v1.30.md (lead+backend+ci-doc+ci-cli)
- IDEATE.md → IDEATE-v1.27.md (stale)
- IAM_POLICY.md → IAM_POLICY-v1.28.md (stale)
- REGRESSION_REPORT.{md,json} → REGRESSION_REPORT-v1.26.{md,json} (stale)

Active root (8 persistent files):
PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md, ARCHITECTURE.md,
NORTH_STAR.md, config.json, CHECKPOINT.json

---ci---
project: acdl
phase: 4
milestone: v1.30
status: complete
action: compress
---/ci---
2026-08-20 14:36:15 +00:00
CIAgent 6dfde4b785 docs(milestone): complete v1.30 Single-shot Leadership Deck (polished, release v1.29.5)
acdl-ci / Lint (push) Successful in 11s
acdl-ci / Test (push) Failing after 23s
acdl-ci / Platform check-only (offline) (push) Successful in 20s
nova-release / Compute semver + update tags (push) Successful in 41s
acdl-ci / forge_parity_disabled (push) Failing after 14m45s
---ci---
project: acdl
phase: 4
milestone: v1.30
status: complete
requirements:
  covered: [REQ-372.1,REQ-372.2,REQ-372.3,REQ-372.4,REQ-372.5,REQ-372.6,REQ-372.7,REQ-372.8,REQ-372.9,REQ-372.10,REQ-372.11,REQ-372.12]
  partial: []
---/ci---
2026-08-20 14:28:16 +00:00
CIAgent 13ee34b5a7 merge(milestone): v1.30 Single-shot Leadership Deck (polished) to main (release v1.29.5)
acdl-ci / Lint (push) Successful in 12s
acdl-ci / Test (push) Failing after 25s
Nova Slides Render / render (push) Failing after 22s
nova-release / Compute semver + update tags (push) Failing after 10m33s
acdl-ci / Platform check-only (offline) (push) Failing after 10m42s
acdl-ci / forge_parity_disabled (push) Failing after 11m7s
---ci---
project: acdl
phase: 4
milestone: v1.30
status: complete
tag: v1.29.5
milestone_release: true
merged_to_main: true
---/ci---
2026-08-20 14:26:55 +00:00
CIAgent ac791c7d23 merge(phase/04): v1.30 P4 final review + milestone ship
---ci---
project: acdl
phase: 4
milestone: v1.30
status: complete
tag: v1.29.5
milestone_release: true
---/ci---
2026-08-20 14:26:55 +00:00
CIAgent bc31c54940 docs(P04): final review + audit — PASS (polish + diagrams verified)
nova-publish / Build + push kj ECR image (KJ-STATIC, D-239) (push) Failing after 21s
nova-publish / Publish wheel + Lambda layer + Lambda zip + Release (push) Has been skipped
Nova Slides Render / render (push) Failing after 15m5s
---ci---
project: acdl
phase: 4
milestone: v1.30
status: review
---/ci---
2026-08-20 14:26:49 +00:00
CIAgent eda7e827b9 merge(phase/03): v1.30 P3 deck-polish complete (D-244/D-245/D-246, 7 diagrams)
---ci---
project: acdl
phase: 3
milestone: v1.30
status: complete
tag: v1.29.4
---/ci---
2026-08-20 14:26:04 +00:00
CIAgent 4c547780e5 docs(ship): P3 verify complete → v1.29.4 (v1.30 deck-polish)
---ci---
project: acdl
phase: 3
milestone: v1.30
status: complete
tag: v1.29.4
---/ci---
2026-08-20 14:26:04 +00:00
CIAgent 0c4f5582f3 feat(P03): render PPTX + F5(b) inline bold de-emphasize — all checks pass
---ci---
project: acdl
phase: 3
milestone: v1.30
status: verify
wave: 4
persona: backend-engineer
---/ci---
2026-08-20 14:25:53 +00:00
CIAgent b85da0471b docs(P03): deck source polish — cover slide, slide 7 what-works-now, 7 diagrams
---ci---
project: acdl
phase: 3
milestone: v1.30
status: execute
wave: 3
persona: ci-doc-writer
---/ci---
2026-08-20 14:22:51 +00:00
CIAgent b51dfb1c03 feat(P03): 7 S&P-themed mermaid diagrams + render helper (D-246)
---ci---
project: acdl
phase: 3
milestone: v1.30
status: execute
wave: 2
persona: ci-doc-writer + backend-engineer
---/ci---
2026-08-20 14:21:31 +00:00
CIAgent 246c0e93b3 feat(P03): render_pptx.py polish F1-F5 (balance, autofit, bullets, footer, blockquote/slide6)
---ci---
project: acdl
phase: 3
milestone: v1.30
status: execute
wave: 1
persona: backend-engineer
---/ci---
2026-08-20 14:19:51 +00:00
CIAgent 50a8089e27 docs(P03): clarify — D-244 (cover), D-245 (slide 7 content), D-246 (mermaid diagrams)
---ci---
project: acdl
phase: 3
milestone: v1.30
status: clarify
decisions: [D-244, D-245, D-246]
---/ci---
2026-08-20 14:18:32 +00:00
CIAgent 9e578a29e0 docs(milestone): complete v1.30 Single-shot Leadership Deck (release v1.29.3)
acdl-ci / forge_parity_disabled (push) Successful in 17s
acdl-ci / Lint (push) Successful in 12s
acdl-ci / Test (push) Failing after 23s
acdl-ci / Platform check-only (offline) (push) Successful in 19s
nova-release / Compute semver + update tags (push) Successful in 40s
---ci---
project: acdl
phase: 2
milestone: v1.30
status: complete
requirements:
  covered: [REQ-372.1,REQ-372.2,REQ-372.3,REQ-372.4,REQ-372.5,REQ-372.6,REQ-372.7,REQ-372.8,REQ-372.9,REQ-372.10,REQ-372.11,REQ-372.12]
  partial: []
---/ci---
2026-08-20 13:27:14 +00:00
CIAgent 96765fe020 merge(milestone): v1.30 Single-shot Leadership Deck to main (release v1.29.3)
acdl-ci / forge_parity_disabled (push) Successful in 10s
acdl-ci / Platform check-only (offline) (push) Successful in 23s
nova-release / Compute semver + update tags (push) Successful in 44s
Nova Slides Render / render (push) Failing after 10m31s
acdl-ci / Test (push) Failing after 11m34s
acdl-ci / Lint (push) Failing after 11m46s
REQ-372.1..REQ-372.12 complete. 7-slide PPTX leadership deck for
Infrastructure & Operations (August 2026, November 2026 runway).
CAP-042. D-241 (discrete artifact), D-242 (render extension),
D-243 (date anchor). Tags: v1.29.1 (P0), v1.29.2 (P1), v1.29.3 (P2 = milestone release).

---ci---
project: acdl
phase: 2
milestone: v1.30
status: complete
tag: v1.29.3
milestone_release: true
merged_to_main: true
---/ci---
2026-08-20 13:26:11 +00:00
CIAgent bf07fe49d0 merge(phase/02): v1.30 P2 final review + audit + milestone ship
---ci---
project: acdl
phase: 2
milestone: v1.30
status: complete
tag: v1.29.3
milestone_release: true
---/ci---
2026-08-20 13:26:08 +00:00
CIAgent 6d5fa85e63 docs(P02): final review + audit — PASS (no P0/P1 issues)
nova-publish / Build + push kj ECR image (KJ-STATIC, D-239) (push) Failing after 25s
nova-publish / Publish wheel + Lambda layer + Lambda zip + Release (push) Has been skipped
Nova Slides Render / render (push) Failing after 26s
Review: 7 slides match Slide Content Map (REQ-372.7); smoke test exits 0;
footer on every slide; citizen deck unmodified; no secrets; no CI gate
wiring; no publish.yml integration.

Audit: 14/14 commits have ---ci--- blocks; 11 .ciagent/ files present;
phase/01 deleted; tags v1.29.1+v1.29.2 exist; REQ-372.1..12 in
REQUIREMENTS.md; CAP-042 in STATE.md; D-241 in PROJECT.md.

---ci---
project: acdl
phase: 2
milestone: v1.30
status: review
---/ci---
2026-08-20 13:26:00 +00:00
CIAgent f584330f40 merge(phase/01): v1.30 P1 leadership-deck complete (REQ-372.1..12, CAP-042, D-241)
Nova Slides Render / render (push) Failing after 30s
---ci---
project: acdl
phase: 1
milestone: v1.30
status: complete
tag: v1.29.2
---/ci---
2026-08-20 13:24:42 +00:00
CIAgent 155c02fe90 docs(P01): verify — 38/38 checks passed (structural+behavioral+security+quality)
Nova Slides Render / render (push) Failing after 22s
nova-publish / Build + push kj ECR image (KJ-STATIC, D-239) (push) Failing after 13m15s
nova-publish / Publish wheel + Lambda layer + Lambda zip + Release (push) Has been skipped
---ci---
project: acdl
phase: 1
milestone: v1.30
status: verify
reqs: [REQ-372.1,REQ-372.2,REQ-372.3,REQ-372.4,REQ-372.5,REQ-372.6,REQ-372.7,REQ-372.8,REQ-372.9,REQ-372.10,REQ-372.11,REQ-372.12]
---/ci---
2026-08-20 13:24:22 +00:00
CIAgent e3a13e4768 feat(P01): render PPTX + CAP-042 + D-241 record + frontmatter-comment fix
- render_pptx.py: skip leading HTML comments before frontmatter (split_slides + parse_frontmatter)
- docs/presentations/nova-leadership-deck.pptx: 7 slides, footer on every slide
- STATE.md: CAP-042 row appended (leadership deck, single-shot, Aug 2026)
- PROJECT.md: D-241 formal entry (single-shot, discrete, no compression)
- smoke test exits 0 (all 6 assertions a-f pass)

---ci---
project: acdl
phase: 1
milestone: v1.30
status: execute
wave: 4
persona: backend-engineer + lead-developer
---/ci---
2026-08-20 13:23:28 +00:00
CIAgent dc784d576d feat(P01): check_leadership_deck.sh smoke test (REQ-372.8 a-f, on-demand)
---ci---
project: acdl
phase: 1
milestone: v1.30
status: execute
wave: 3
persona: ci-cli-engineer
---/ci---
2026-08-20 13:22:27 +00:00
CIAgent d66b7b0e73 docs(P01): author nova-leadership-deck-marp.md (7 slides, REQ-372.1/.3/.4/.6/.7/.9/.12)
---ci---
project: acdl
phase: 1
milestone: v1.30
status: execute
wave: 2
persona: ci-doc-writer
---/ci---
2026-08-20 13:21:56 +00:00
CIAgent 929d0d94c2 feat(P01): extend render_pptx.py — path arg + --output + footer textbox (D-242)
---ci---
project: acdl
phase: 1
milestone: v1.30
status: execute
wave: 1
persona: backend-engineer
---/ci---
2026-08-20 13:20:23 +00:00
CIAgent fff2bcc606 docs(ship): P0 complete → v1.29.1 (v1.30 pre-execution)
---ci---
project: acdl
phase: 0
milestone: v1.30
status: complete
tag: v1.29.1
release_id: 811
---/ci---
2026-08-20 13:19:06 +00:00
CIAgent adcd012a2f merge(phase/00): v1.30 P0 pre-execution complete (specify→clarify→research→plan→grill→mvp/ux)
---ci---
project: acdl
phase: 0
milestone: v1.30
status: complete
tag: v1.29.1
---/ci---
2026-08-20 13:18:11 +00:00
CIAgent 8ba9981743 decision(P00): mvp/ux gate — auto-generated (3 sections present)
Nova Slides Render / render (push) Failing after 24s
nova-publish / Build + push kj ECR image (KJ-STATIC, D-239) (push) Failing after 2m43s
nova-publish / Publish wheel + Lambda layer + Lambda zip + Release (push) Has been skipped
---ci---
project: acdl
phase: 0
milestone: v1.30
status: mvp_ux_check
---/ci---
2026-08-20 13:18:04 +00:00
CIAgent 48657eb816 docs(P00): grill — PROCEED (0.88), G-1..G-4 binding decisions
---ci---
project: acdl
phase: 0
milestone: v1.30
status: grill
---/ci---
2026-08-20 13:17:55 +00:00
CIAgent e560adacb6 docs(P00): create phase plans — P1 leadership-deck (4 waves)
---ci---
project: acdl
phase: 0
milestone: v1.30
status: plan
---/ci---
2026-08-20 13:17:16 +00:00
CIAgent 18b403fcd8 docs(P00): research findings + persona assessment
---ci---
project: acdl
phase: 0
milestone: v1.30
status: research
---/ci---
2026-08-20 13:16:33 +00:00
CIAgent 6aac523da7 docs(P00): clarify — D-241/D-242/D-243 + STATE.md intake override
---ci---
project: acdl
phase: 0
milestone: v1.30
status: clarify
decisions: [D-241, D-242, D-243]
---/ci---
2026-08-20 13:14:59 +00:00
CIAgent 65bf3d84e9 docs(init): validate specification — REQ-372 leadership deck (v1.30)
---ci---
project: acdl
phase: 0
milestone: v1.30
status: specify
---/ci---
2026-08-20 13:14:14 +00:00
CIAgent Orchestrator adf3b1c02f docs(audit): fix core module count in STATE.md (27 → 26 top-level + 8 lambda + 10 metrics)
acdl-ci / Lint (push) Successful in 12s
acdl-ci / Test (push) Failing after 22s
acdl-ci / Platform check-only (offline) (push) Successful in 18s
nova-release / Compute semver + update tags (push) Failing after 19s
acdl-ci / forge_parity_disabled (push) Failing after 10m46s
---ci---
project: acdl
phase: 0
milestone: v1.30
status: specify
audit: adversarial-fix
---/ci---
2026-08-20 12:52:15 +00:00
CIAgent Orchestrator f8677fa002 docs(audit): fix stale milestone labels + checkpoint + config — v1.29 complete, v1.30 scoped
acdl-ci / forge_parity_disabled (push) Successful in 8s
acdl-ci / Lint (push) Successful in 13s
acdl-ci / Test (push) Failing after 24s
acdl-ci / Platform check-only (offline) (push) Successful in 20s
nova-release / Compute semver + update tags (push) Failing after 12m44s
Audit fixes:
- PROJECT.md: v1.28 + v1.29 subheaders 'Feature milestone — active' → 'complete'
- REQUIREMENTS.md: same fix (v1.28 + v1.29)
- PROJECT.md: v1.29 header '(active, milestone branch...)' → '(complete, tag v1.28.6, merged to main 2026-08-20)'
- REQUIREMENTS.md: same header fix
- config.json: active_milestone v1.29 → v1.30 (v1.29 complete, next milestone scoped)
- CHECKPOINT.json: cleared v1.29 complete state, set v1.30 phase 0 specify (per run.md clear-checkpoint)

---ci---
project: acdl
phase: 0
milestone: v1.30
status: specify
audit: fixes-applied
---/ci---
2026-08-20 10:54:34 +00:00
CIAgent Orchestrator 713ad2eff2 docs(pdlc): STATE.md Phase 0 intake — post-v1.29 ground truth snapshot
acdl-ci / forge_parity_disabled (push) Successful in 7s
acdl-ci / Lint (push) Successful in 13s
acdl-ci / Test (push) Failing after 20s
acdl-ci / Platform check-only (offline) (push) Successful in 23s
nova-release / Compute semver + update tags (push) Failing after 22s
Initiative: Leadership Presentation Deck (≤7 slides, S&P theme, 18-month CDLC→SDLC→PDLC roadmap).
v1.29 complete (v1.28.6, merged to main 9dc5669, pushed + 8 Gitea releases).
Domain 12 CAP-039..041 shipped (covered-reference). All commit refs updated to 9dc5669.

---ci---
project: acdl
phase: 0
milestone: v1.30
status: specify
pdlc: state-intake
---/ci---
2026-08-20 10:51:25 +00:00
CIAgent Orchestrator 9dc56698fb docs(milestone): complete v1.29 Reposplit + Identity Layer Bring-Live (release v1.28.6)
acdl-ci / Lint (push) Successful in 9s
acdl-ci / forge_parity_disabled (push) Successful in 9s
acdl-ci / Test (push) Failing after 22s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
Nova Slides Render / render (push) Failing after 14m26s
nova-release / Compute semver + update tags (push) Failing after 14m35s
---ci---
project: acdl
phase: 6
milestone: v1.29
status: complete
requirements:
  covered: [REQ-354, REQ-367, REQ-368, REQ-369, REQ-OPS-GUIDE, REQ-CONSUMER-BUMP]
  covered_reference: [REQ-355, REQ-356, REQ-357, REQ-358, REQ-359, REQ-360, REQ-361, REQ-362, REQ-363, REQ-363b, REQ-364, REQ-365, REQ-366, REQ-371]
  partial: []
---/ci---
2026-08-20 05:29:59 +00:00
CIAgent Orchestrator 932923ee99 merge(milestone): v1.29 Reposplit + Identity Layer Bring-Live to main (release v1.28.6)
Nova Slides Render / render (push) Failing after 22s
---ci---
project: acdl
phase: 6
milestone: v1.29
status: complete
---/ci---
2026-08-20 05:29:46 +00:00
Jon Chery 184f33c60a merge(chore): PDLC Phase 0 state intake — post-v1.28 ground truth snapshot
acdl-ci / Test (push) Failing after 23s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
acdl-ci / Lint (push) Failing after 14m35s
2026-08-19 23:53:30 +00:00
Jon Chery a6510e7afc docs(pdlc): STATE.md Phase 0 intake — post-v1.28 ground truth snapshot
Populates the PDLC Phase 0 intake format with the absolute ground truth
of the system after v1.28 ship (CLI Canonicalization + Identity Layer).
Header bumped to v1.28 last-ship. 7 sections: header, architecture state,
technical stack, active constraints (D-001..D-231, INV-1..17, CAP-001..038),
recent history + quality gates (coverage 73.8% YELLOW — below 80% floor),
agent context + assumptions, canonical state references. Unknowns explicit
(no new initiative; CodeArtifact/KMS/kj-in-Lambda unverified in-account).

---ci---
project: acdl
phase: 0
milestone: v1.28
status: pdlc-intake
---/ci---
2026-08-19 23:53:19 +00:00
Jon Chery c0cb1887ed merge(milestone): v1.28 CLI Canonicalization + Identity Layer to main (release v1.27.6)
acdl-ci / Lint (push) Successful in 11s
acdl-ci / Test (push) Failing after 25s
nova-publish / Publish wheel + Lambda layer (push) Failing after 22s
Nova Slides Render / render (push) Failing after 13m46s
acdl-ci / Platform check-only (offline) (push) Failing after 14m25s
2026-08-19 23:48:04 +00:00
Jon Chery 139cb5077a merge(phase/06): v1.28 P6 final review + audit + milestone complete 2026-08-19 23:48:04 +00:00
Jon Chery adc55a17ab docs(milestone): complete v1.28 CLI Canonicalization + Identity Layer (release v1.27.6)
Nova Slides Render / render (push) Failing after 29s
---ci---
project: acdl
phase: 6
milestone: v1.28
status: complete
requirements:
  covered: [REQ-323..353]
  partial: []
---/ci---
2026-08-19 23:47:44 +00:00
Jon Chery 0d8913a299 merge(phase/05): v1.28 P5 docs-integration complete (REQ-345..351, E2E + threat model) 2026-08-19 23:34:11 +00:00
Jon Chery 4697692ce7 docs(ship): P5 complete → v1.27.5 (v1.28 docs-integration)
Nova Slides Render / render (push) Failing after 26s
---ci---
project: acdl
phase: 5
milestone: v1.28
status: complete
---/ci---
2026-08-19 23:34:11 +00:00
Jon Chery 23b8ff81d3 docs(P05): verify REQ-349/350/351 pass in combined suite (security-engineer)
---ci---
project: acdl
phase: 5
milestone: v1.28
status: execute
persona: security-engineer
---
Verification results (combined suite, .venv/bin/python -m pytest):

  REQ-349 (mode_resolver property tests, tests/test_mode_resolver.py):
    12 passed — all four priority levels + edge cases (TTY/piped stdout,
    missing credential, conflicting flag/env, invalid env value).

  REQ-350 (KMS round-trip, tests/test_kms_roundtrip.py): 1 passed —
    sign JWT via mock KMS → JWKS Lambda → pyjwt verify (CAP-037, DER→raw
    byte-correct).

  REQ-351 (PAT revocation SLO, tests/test_pat_revocation.py): 2 passed
    — issue → vend → revoke → 403 pat_revoked in <1s (D-229 strong-read).

  Combined suite regression run
  (pytest tests/ --ignore=tests/test_pptx_generator.py -k 'not live_aws
  and not slow'): 1000 passed, 5 deselected, 0 failed.

Fix: the first regression run flagged 1 failure —
test_no_forge_mentions_in_synced_files (REQ-230 v1.20 guard) caught two
forbidden forge-name mentions in the new docs (operator-guide §7,
developer-guide §9). Rephrased both to 'internal forge' / 'internal
forge runner' to keep the docs sync-safe. No code changes. Re-ran the
full suite: 1000 passed, 0 failed.
2026-08-19 23:30:41 +00:00
Jon Chery d0a8c363b2 test(P05): E2E integration test — sign-up→sign-in→token-vend→apply→audit (REQ-348, security-engineer)
---ci---
project: acdl
phase: 5
milestone: v1.28
status: execute
persona: security-engineer
---
Add tests/test_e2e_idp.py — the J1+J2 happy-path E2E flow. Uses moto
for DynamoDB (4 IdP tables) + mock KMS (test ECC keypair). Asserts:
(a) sign_up succeeds, (b) sign_in returns a session, (c) token-vend
returns a KMS-signed OIDC token, (d) the OIDC token verifies with the
JWKS key (pyjwt), (e) nova apply --local produces a JWS attestation
(HS256), (f) the JWS verifies with the PAT-derived key (+ tamper
detection), (g) the audit chain is complete + linked (auth.sign_up,
auth.sign_in, auth.session_created, pat.issued, token.vend.allowed —
all present, linked by user_id/jti, no raw password/PAT leaked
INV-16). Also: the credentials file stores the OIDC token not the raw
PAT (C-7.3), the DDB user item has a password_hash not the raw
password, the DDB PAT row has a pat_hash not the raw PAT. Negative
path: revocation breaks the chain (403 pat_revoked, D-229 strong-read
SLO, token.vend.denied audit event).
2026-08-19 23:22:53 +00:00
Jon Chery 04053df16e docs(P05): identity-layer threat model (REQ-347, C-6.2, C-9.2, security-engineer)
---ci---
project: acdl
phase: 5
milestone: v1.28
status: execute
persona: security-engineer
---
Add docs/threat-model.md covering 8 threats + mitigations: (T-1) password
compromise → Argon2id + fail-closed (D-228) + no raw passwords (INV-16);
(T-2) PAT theft → credentials.json stores OIDC token not raw PAT (C-7.3)
+ max TTL ≤24h dev/≤1h service-account (C-6.2) + strong-read revocation
(D-229); (T-3) JWKS DDoS → reserved concurrency 10 + 1h client cache +
optional CloudFront/WAF (C-6.2); (T-4) ABAC bypass → fail-closed
(C-6.1/C-7.1, INV-17, 7 tests); (T-5) KMS key compromise → key policy
restricts kms:Sign + 90-day rotation; (T-6) DER→raw ECDSA gotcha →
cryptography decode_dss_signature + CAP-037 round-trip test; (T-7) no
AWS-managed identity (INV-15); (T-8) audit trail integrity (INV-12 +
policy_sha D-231). Includes the C-9.2 INV-18..21 compression audit:
the spec's 4 attestation concerns (immutability, signature
verifiability, key derivation, no-managed-identity) are fully captured
by INV-6 + INV-15 + INV-16 + INV-17 + REQ-332 — no semantic gap.
2026-08-19 23:22:00 +00:00
Jon Chery bcbeb7badb docs(P05): developer guide for nova auth login (REQ-346, C-7.3, lead-developer)
---ci---
project: acdl
phase: 5
milestone: v1.28
status: execute
persona: lead-developer
---
Add docs/developer-guide-auth.md covering the 5-step quickstart (signup
→ signin → login → init → apply), nova auth signup/signin/login/status/
revoke, the credentials.json file (C-7.3: OIDC token + metadata only,
NOT raw PAT, 0600), D-226 mode resolution (flag → env → credential →
TTY) with the Edge 3 TTY-vs-piped-stdout case, the JWS-from-PAT KDF
(HKDF-SHA256, HS256 symmetric, REQ-332/C-5.2), and service-account PAT
usage in CI (max TTL ≤1h, C-6.2).
2026-08-19 23:20:54 +00:00
Jon Chery 1f4f7f0f81 docs(P05): operator guide for nova idp setup (REQ-345, C-6.3, lead-developer)
---ci---
project: acdl
phase: 5
milestone: v1.28
status: execute
persona: lead-developer
---
Add docs/operator-guide-idp.md covering nova idp setup --check/--apply/
--verify, the prerequisite IAM policy delta, the CloudFormation review
flow ($PAGER + y/N), --dry-run, --public-jwks-domain, and the C-6.3
grill additions: KMS key rotation (90 days, alias re-point + overlap
window), Lambda layer update procedure, DDB PITR restore procedure
(35-day window), emergency PAT revocation (DDB-level update-item on
nova-pats, bypasses CLI, satisfies D-229 strong-read SLO).
2026-08-19 23:20:21 +00:00
Jon Chery df2b83c86b merge(phase/04): v1.28 P4 token-vend-pat complete (REQ-336..344+340/341, CAP-037/038, ABAC fail-closed) 2026-08-19 23:17:31 +00:00
Jon Chery f68349d94d docs(ship): P4 complete → v1.27.4 (v1.28 token-vend-pat, highest-risk phase)
Nova Slides Render / render (push) Failing after 28s
---ci---
project: acdl
phase: 4
milestone: v1.28
status: complete
---/ci---
2026-08-19 23:17:31 +00:00
Jon Chery 1863a85144 feat(P04): nova idp setup --check/--apply/--verify (REQ-340/341, C-2.1, backend+cli)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: backend-engineer
---
2026-08-19 23:13:21 +00:00
Jon Chery 7dab9d5756 test(P04): CAP-037 KMS round-trip + CAP-038 PAT revocation SLO (REQ-350/351, security-engineer)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: security-engineer
---
2026-08-19 23:11:43 +00:00
Jon Chery 14809327fb feat(P04): PAT lifecycle + nova auth login/revoke/status (REQ-342..344, C-7.3, security+cli)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: cli-engineer
---
2026-08-19 23:11:16 +00:00
Jon Chery 0662ed26a3 feat(P04): nova-idp-jwks Lambda — JWKS endpoint (REQ-338, D-230, backend-engineer)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: backend-engineer
---
2026-08-19 23:08:32 +00:00
Jon Chery cd3418a75e feat(P04): nova-idp-token-vend Lambda — ABAC fail-closed + KMS sign (REQ-336, C-6.1, backend+security)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: backend-engineer
---
2026-08-19 23:08:06 +00:00
Jon Chery dee6d88d87 feat(P04): KMS ECDSA P-256 signing + DER->raw conversion (REQ-337, C-1.1, security-engineer)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: security-engineer
---
2026-08-19 23:05:25 +00:00
Jon Chery fe0ee6aa45 feat(P04): kyverno-json ABAC policy + evaluator (REQ-339, D-227, C-5.1, security-engineer)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: security-engineer
---
2026-08-19 23:04:27 +00:00
Jon Chery 701cc572ce chore(P04): kj-binary pin + platform/abac scaffold (C-8.2, D-227, backend-engineer)
---ci---
project: acdl
phase: 4
milestone: v1.28
status: execute
persona: backend-engineer
---
2026-08-19 23:03:07 +00:00
Jon Chery 0736924de2 merge(phase/03): v1.28 P3 idp-auth complete (REQ-333..335, CAP-036) 2026-08-19 23:00:37 +00:00
Jon Chery 05bf8bf221 docs(ship): P3 complete → v1.27.3 (v1.28 idp-auth)
Nova Slides Render / render (push) Failing after 24s
---ci---
project: acdl
phase: 3
milestone: v1.28
status: complete
---/ci---
2026-08-19 23:00:37 +00:00
Jon Chery 7a7fbfed82 feat(P03): nova-idp-auth Lambda — sign-up/sign-in/session (REQ-333, backend-engineer) + CAP-036 E2E
Commits the full nova-idp-auth Lambda handler (sign_up/sign_in/create_session/
request_password_reset/reset_password) along with the CAP-036 E2E test
(test_idp_auth.py) covering the sign-up → sign-in → session flow, negatives
(401/409), password reset, and fail-closed 503.

---ci---
project: acdl
phase: 3
milestone: v1.28
status: execute
persona: backend-engineer
---
2026-08-19 22:58:59 +00:00
Jon Chery d06535032c test(P03): Argon2 fail-closed — ImportError → 503, no weak hash (C-1.2, security-engineer)
---ci---
project: acdl
phase: 3
milestone: v1.28
status: execute
persona: security-engineer
---
2026-08-19 22:58:57 +00:00
Jon Chery 8550ede810 feat(P03): Argon2id hashing — fail-closed, t=3 m=65536 p=1 (REQ-334, D-228, C-7.2, security-engineer)
The full nova-idp-auth Lambda handler is included in this commit (sign_up,
sign_in, create_session, request_password_reset, reset_password) since the
hashing module and handler share one file. The Argon2id hashing + fail-closed
logic is the security-engineer territory; the Lambda plumbing is backend-engineer.

---ci---
project: acdl
phase: 3
milestone: v1.28
status: execute
persona: security-engineer
---
2026-08-19 22:56:00 +00:00
Jon Chery 71562d9db2 feat(P03): DynamoDB identity schema + CFN snippet (REQ-335, backend-engineer)
---ci---
project: acdl
phase: 3
milestone: v1.28
status: execute
persona: backend-engineer
---
2026-08-19 22:55:10 +00:00
Jon Chery 91cb931bab merge(phase/02): v1.28 P2 lambda-packaging complete (REQ-329..332) 2026-08-19 22:52:36 +00:00
Jon Chery a8ef1e8864 docs(ship): P2 complete → v1.27.2 (v1.28 lambda-packaging)
Nova Slides Render / render (push) Failing after 26s
---ci---
project: acdl
phase: 2
milestone: v1.28
status: complete
---/ci---
2026-08-19 22:52:36 +00:00
Jon Chery 291921a04e test(P02): attestations dir scaffolded + empty (REQ-331, backend-engineer)
---ci---
project: acdl
phase: 2
milestone: v1.28
status: execute
persona: backend-engineer
---
tests/test_init_attestations.py: nova init in a tmp_path creates
.nova/contract.yml.attestations/ as an empty directory (listdir == []).
The existing test_cli_subcommands.py asserts is_dir() but not emptiness;
this is the explicit REQ-331 assertion (freshly scaffolded repo has no
attestations yet — they are produced later by nova apply --sign-local-review
/ the JWS attestation flow, REQ-332).
2026-08-19 22:49:20 +00:00
Jon Chery c9bfc98713 feat(P02): nova apply --local --sign-local-review (REQ-330, REQ-332, backend-engineer)
---ci---
project: acdl
phase: 2
milestone: v1.28
status: execute
persona: backend-engineer
---
nova apply subcommand (44 lines, CAP-034: <=50 lines, <=3 functions, no if
except __main__ guard). --local calls core.env.synthesize_local_env() +
core.contract_resolver.resolve(). --sign-local-review calls
core.jws_attestation.sign_attestation() (REQ-332) and appends the JWS to the
output. Delegates to core/ — no business logic in the subcommand (NFR-7).
Auto-registered via nova/cli.py pkgutil discovery; CAP-033/034 tests pass.
2026-08-19 22:49:04 +00:00
Jon Chery ab069db3a4 feat(P02): JWS-from-PAT key derivation via HKDF-SHA256 (REQ-332, C-5.2, security-engineer)
---ci---
project: acdl
phase: 2
milestone: v1.28
status: execute
persona: security-engineer
---
C-5.2 grill fix: symmetric JWS (HS256) where the PAT is the shared secret.
derive_signing_key(pat) -> HKDF-SHA256(pat.encode(), salt=b'nova-local-
attestation', info=b'jws-signing-key', length=32) via cryptography (fallback
to hashlib HKDF). sign_attestation(payload, pat) -> compact JWS
b64url(header).b64url(payload).b64url(sig) with header {alg:HS256,typ:JWT}.
verify_attestation(jws, pat) -> payload (raises JWSValidationError on tamper
or wrong PAT; hmac.compare_digest constant-time). INV-14..17 enforced
(key derived from PAT, not cached, fixed salt/info, constant-time compare).
tests/test_jws_attestation.py: 20 tests (round-trip, tamper, wrong-PAT,
invariants, hashlib/crypto parity).
2026-08-19 22:48:21 +00:00
Jon Chery 3338ec1622 feat(P02): core/env.synthesize_local_env — local env synthesizer (REQ-330, backend-engineer)
---ci---
project: acdl
phase: 2
milestone: v1.28
status: execute
persona: backend-engineer
---
synthesize_local_env(contract_path, environment) reads a contract YAML and
produces a purely synthetic local env dict (account_id=000000000000
placeholder, region='local', local state_backend, local network) that
validates against schemas/environment.schema.json. Mirrors the shape of
core/environments/*.json + core/onboarding.py:generate_env_file() (shape
parity on the required env-binding keys). No cloud provisioning — purely
synthetic for nova apply --local. tests/test_local_env.py: 13 tests
(schema validation, region/account sentinels, env override, threshold
per-env, shape parity, missing-file default).
2026-08-19 22:47:37 +00:00
Jon Chery eb4fade710 refactor(P02): dual-use contract_ingestor — Lambda + CLI share core logic (REQ-329, backend-engineer)
---ci---
project: acdl
phase: 2
milestone: v1.28
status: execute
persona: backend-engineer
---
Extract dispatch_action() shared business-logic dispatch + _to_http_response
error mapper. lambda_handler (Lambda) + cli_main (CLI) become thin input
parsers that both delegate to dispatch_action. The action routing, contract
validation, DynamoDB write, error reporting live in shared functions — single
source of truth (NFR-7). tests/test_dual_use.py verifies both paths produce
the same output for the same input, both call dispatch_action, and code
share >=80% (CAP-026). 41 existing ingestor tests still pass.
2026-08-19 22:46:30 +00:00
Jon Chery 5dd7222571 merge(phase/01): v1.28 P1 cli-substrate complete (REQ-323..328, CAP-033/034/035)
Nova Slides Render / render (push) Failing after 26s
2026-08-19 22:42:12 +00:00
Jon Chery 5763e85bb7 docs(ship): P1 complete → v1.27.1 (v1.28 cli-substrate)
Nova Slides Render / render (push) Failing after 28s
---ci---
project: acdl
phase: 1
milestone: v1.28
status: complete
---/ci---
2026-08-19 22:42:12 +00:00
Jon Chery 37f462783f docs(P01): verify — v1.28 cli-substrate (4 layers PASS, 809 tests, CAP-033/034/035)
---ci---
project: acdl
phase: 1
milestone: v1.28
status: verify
---/ci---
2026-08-19 22:41:00 +00:00
Jon Chery cba7c1c189 test(P01): forge action byte-identical structure test (NFR-11, backend-engineer)
tests/test_forge_action_byte_identical.py — 15 tests asserting the
structural invariants of the nova cli-action composite action
(.github/actions/nova-cli/action.yml). The action is consumed by both
the production forge + the dev forge via the same file path, so a
single source under test guarantees both platforms consume the same
bytes (the byte-identical requirement, NFR-11).

Structural invariants covered (the unit-testable subset):
(a) action.yml is valid YAML
(b) name present + non-empty
(c) inputs.command required: true
(d) inputs.contract / mode / version exist with documented defaults
    (.nova/contract.yml, "", "latest") and are not required
(e) runs.using == "composite"
(f) a setup-python@v5 step pins python-version "3.12" (REQ-326 AC3)
(g) an install step installs `nova` via both CodeArtifact
    (codeartifact login --tool pip) + fallback (--index-url) paths,
    parameterised by inputs.version
(h) a run step executes `nova ${{ inputs.command }}` with
    NOVA_CLIENT_MODE (from inputs.mode) + NOVA_CONTRACT (from
    inputs.contract) env forwarded

NFR-11 byte-identical source guard: the action.yml must not embed
forge-specific hostnames / org names / the dev-forge or consumer-mirror
names, and the install path must be selected by env var at runtime
(NOT a forge-identity conditional) — so the file stays byte-identical
across forges. Both asserted.

The full byte-identical cross-platform verification (NFR-11,
REQ-326 AC2) — running the action with identical inputs on a
production-forge ubuntu-latest runner + a dev-forge act_runner and
asserting identical stdout + exit code — is a CI matrix job, not a
unit test. It cannot be reproduced in-process (depends on two external
runner environments). Documented in the module docstring + the
action.yml header; the CI matrix job is defined out-of-band.

All 15 tests pass. No regressions in tests/test_pipeline_contract.py,
tests/test_deploy_workflow_env_input.py, tests/test_rotate_key_workflow.py
(77 passed). tests/test_no_forge_mentions.py passes (the test file +
action.yml + publish.yml are clean of forge-specific strings).

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: backend-engineer
---/ci---
2026-08-19 22:35:55 +00:00
Jon Chery fd3f9e17b9 feat(P01): nova cli-action composite action (REQ-326, backend-engineer)
.github/actions/nova-cli/action.yml — composite action discovered by
both the production forge (GitHub Actions) and the dev forge
(act_runner) via the shared .github/actions/nova-cli/ path. No separate
dev-forge action file is needed; the same path works on both platforms.
Consumers reference it via a versioned tag pin:
  uses: <org>/<repo>/.github/actions/nova-cli@v1.28

inputs:
- command (required) — the nova subcommand + args, passed verbatim to
  `nova`
- contract (default .nova/contract.yml) — forwarded via NOVA_CONTRACT
- mode (default "") — forwarded via NOVA_CLIENT_MODE (agent /
  interactive / plan-only / check-only); empty = let nova resolve
- version (default "latest") — pin to a released wheel version for
  reproducible runs

runs.using: composite with 3 steps:
1. actions/setup-python@v5 with python-version "3.12" (REQ-326 AC3)
2. Install Nova (CodeArtifact default + fallback index):
   - NOVA_CODEARTIFACT_DOMAIN set → aws codeartifact login --tool pip
     --domain $DOMAIN --repository nova-pypi → pip install nova==<ver>
   - else → pip install --index-url $NOVA_WHEEL_INDEX nova==<ver>
   Fails closed if neither is configured.
3. Run Nova: `nova ${{ inputs.command }}` with NOVA_CLIENT_MODE +
   NOVA_CONTRACT env from inputs.

NFR-11 byte-identical cross-platform verification is a CI matrix job
(production forge ubuntu-latest + dev forge act_runner with identical
inputs, assert same stdout + exit code) — not reproducible in a unit
test. Structural invariants are asserted by
tests/test_forge_action_byte_identical.py (next commit).

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: backend-engineer
---/ci---
2026-08-19 22:34:47 +00:00
Jon Chery 03adaa80a6 feat(P01): publish workflow — wheel + Lambda layer (REQ-323, CAP-035, backend-engineer)
Byte-identical .github/workflows/publish.yml + mirror on the dev forge
(<dev-forge>/workflows/publish.yml) — same file content, installed in
both locations per the repo's byte-identical workflow convention.

NFR-6 (wheel/layer co-versioning): on push to main affecting core/**,
adapters/**, nova/**, or pyproject.toml, the workflow publishes BOTH a
wheel AND a Lambda layer with identical version strings. If either
publish fails, the job fails and the merge is blocked (REQ-323 AC).

Steps:
- actions/checkout@v4 + actions/setup-python@v5 (python 3.12)
- aws-actions/configure-aws-credentials@v4 (OIDC, role-to-assume from
  AWS_ROLE_ARN secret, id-token: write)
- pip install build twine
- compute version: tomllib.load(pyproject.toml)["project"]["version"]
  → steps.ver.outputs.version (e.g. 1.14.0)
- python -m build --wheel
- twine upload dist/nova-<ver>-*.whl with two modes:
  * CodeArtifact: NOVA_CODEARTIFACT_DOMAIN set →
    aws codeartifact login --tool twine --domain $DOMAIN --repository
    nova-pypi
  * Fallback: NOVA_CODEARTIFACT_DOMAIN unset → TWINE_REPOSITORY_URL +
    TWINE_USERNAME + TWINE_PASSWORD secrets (any PEP 503 index)
  Idempotent: a re-upload that hits "file already exists" is treated as
  success.
- build Lambda layer: pip install --target layer/python/ the wheel +
  argon2-cffi + cryptography + pyjwt, then zip -r nova-layer.zip python/
- aws lambda publish-layer-version --layer-name nova-cli
  --compatible-runtimes python3.12 --compatible-architectures x86_64
  --description "nova-cli v<ver>" → steps.layer.outputs.arn
- aws ssm put-parameter /nova/layer/nova-cli/version =
  "<wheel-version>:<layer-arn>" (CAP-035)
- final guard step fails the job if wheel uploaded!=true or layer arn
  is empty

permissions: id-token: write (OIDC), contents: write (tag).
Secrets documented in the workflow header comments.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: backend-engineer
---/ci---
2026-08-19 22:34:28 +00:00
Jon Chery 3a09ca8ec1 docs(P01): CodeArtifact provisioning check + fallback (REQ-323, backend-engineer)
CodeArtifact provisioning check in account 581513795199 could not
complete — no AWS credentials available in the P1 execute environment
("Unable to locate credentials"). Per the task spec, provisioning is NOT
attempted (requires codeartifact:* IAM grants not confirmed for the
execute principal). Documented as a P1 blocker for the CodeArtifact mode
of the publish workflow's wheel-upload step.

docs/codeartifact-provisioning.md records:
- (a) the attempted commands (list-domains, describe-repository,
  list-repositories) + the credentials-not-found error
- (b) the required IAM grants for a follow-up provisioning task:
  codeartifact:CreateDomain, CreateRepository, GetRepositoryEndpoint,
  GetAuthorizationToken, ReadFromRepository, PublishPackageToRepository
  + ssm:PutParameter (CAP-035) + lambda:PublishLayerVersion
- (c) the fallback: a private wheel index selected at deploy time via
  the NOVA_WHEEL_INDEX env var (consumers / composite action) and
  TWINE_REPOSITORY_URL + TWINE_USERNAME + TWINE_PASSWORD (publish step).
  The workflow supports both CodeArtifact mode (NOVA_CODEARTIFACT_DOMAIN
  set) and fallback-index mode (unset) — no single hostname is baked
  into the synced workflow files.

CAP-035 invariant (SSM /nova/layer/nova-cli/version = <wheel-version>:
<layer-arn>) is unaffected by the index choice and is recorded
atomically after both the wheel upload + layer publish succeed.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: backend-engineer
---/ci---
2026-08-19 22:34:02 +00:00
Jon Chery d7971023b6 test(P01): tests/test_cli_subcommands.py — CAP-033 + CAP-034 (REQ-324, cli-engineer)
CAP-033: `nova --help` exits 0 and lists a subcommand for every
user-facing core/ module (15 expected subcommands parsed from help).

CAP-034 (AST scan, parametrized per nova/<module>.py excl. cli/__init__):
- (a) line count ≤50
- (b) ≤3 FunctionDef/AsyncFunctionDef
- (c) every bare ast.Call target resolves to a core.* import, a builtin,
  or a local function def (attribute/method calls allowed)
- (d) no `if` statements except `if __name__ == "__main__"`

nova init: in tmp_path, asserts .nova/, .nova/contract.yml.attestations/,
.gitignore created with all 6 secrets-exclusion lines; refuses existing
dir without --force.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:30:29 +00:00
Jon Chery 6a8267e13f test(P01): tests/test_mode_resolver.py — hypothesis properties (REQ-349, cli-engineer)
Property tests (hypothesis):
- deterministic (same inputs → same output)
- flag wins (flag in {agent,interactive} → mode==flag, reason=="flag")
- invalid env ignored (env in {auto,""} → credential-or-tty result)
- no silent fallback (every result has non-empty selection_reason)
- credential+TTY → interactive, credential+no-TTY → agent

Edge cases (explicit):
- stdin TTY + credential → interactive (Edge 3 analog)
- missing credential → falls to TTY
- conflicting flag/env → flag wins
- env wins over credential
- invalid env warns + falls through
- resolve_mode_from_env reads --mode from sys.argv + NOVA_CLIENT_MODE

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:30:08 +00:00
Jon Chery 2ed2b3ae0f feat(P01): nova subcommands — thin delegates to core/* (CAP-033/034, cli-engineer)
One nova/<name>.py per user-facing core/ module. Each ≤50 lines, ≤3
FunctionDef (add_parser + run [+1 helper]), every user-function call
resolves to a core.* import, no `if` statements except `if __name__`.

Subcommands:
- nova resolve       → core.contract_resolver.resolve
- nova decommission  → core.decommission_transform.decommission_transform
- nova env-transition detect|record → core.env_transition
- nova env-check     → core.environment_check.check
- nova hitl          → core.hitl_gates.attest (+ approver_from_env)
- nova onboard       → core.onboarding.generate_env_file
- nova outbox        → core.outbox_writer.write_event
- nova publish-outputs → core.output_publisher.publish_to_ssm + format_comment
- nova policy        → core.policy_engine.get_engine + get_policy_root (status)
- nova regression    → core.regression_verify.run_regression + write_report
- nova sod           → core.separation_of_duties.check
- nova readiness     → core.submission_readiness.cli_main
- nova attestation-matrix → core.attestation_matrix.cli_main (new thin wrapper)
- nova confidence    → core.confidence_signal.cli_main (new thin wrapper)

core wrappers added (minimal): attestation_matrix.cli_main,
confidence_signal.cli_main — extracted from their __main__ blocks so
the nova subcommands stay thin.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:25:00 +00:00
Jon Chery 83883076ff feat(P01): nova init scaffold (REQ-325, cli-engineer)
- core/init_scaffold.py: scaffold(root, force) creates .nova/,
  .nova/contract.yml.attestations/, and appends secrets-exclusion lines
  to .gitignore (~/.nova/credentials.json, .nova/credentials.json,
  *.pem, *.key, .env, .env.*). Refuses overwrite without --force.
- nova/init.py: thin subcommand parsing --force, delegates to
  core.init_scaffold.scaffold.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:24:17 +00:00
Jon Chery 0388751c6e feat(P01): nova/cli.py entry point + dispatch + audit (REQ-324, INV-12, cli-engineer)
- main(argv) builds top-level argparse(prog="nova") with required subparsers.
- Auto-discovers nova/<module>.py via pkgutil.iter_modules(nova.__path__),
  skipping `cli`; each module exports add_parser(subparsers) + run(args) -> int.
- Before dispatch: resolve_mode_from_env() → emit cli.invocation audit
  event (INV-12) as a stderr JSON line stub with mode, selection_reason,
  credential_type, command, args. Real outbox wiring comes later.
- Dispatch: args._run(args); exit code via sys.exit(main()).
- nova/__init__.py empty package marker.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:24:07 +00:00
Jon Chery 5d1a5f83da feat(P01): core/mode_resolver — client-mode resolution (REQ-327, D-226, cli-engineer)
Priority: --mode flag → NOVA_CLIENT_MODE env → credential type → TTY.
No silent fallbacks: every return carries a non-empty selection_reason.

- resolve_mode(flag, env_var, credential_type, stdin_isatty) -> (mode, reason)
- resolve_mode_from_env() reads --mode from sys.argv (best-effort scan,
  no full argparse), NOVA_CLIENT_MODE, ~/.nova/credentials.json active
  credential type, and sys.stdin.isatty() (D-226: stdin, NOT stdout).
- INV-13: invalid env values logged + ignored, fall through.
- INV-14: developer_pat/nova_oidc_token + TTY → interactive; + no-TTY → agent.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:23:46 +00:00
Jon Chery e7af683af6 feat(P01): pyproject entry point + package discovery (REQ-324, cli-engineer)
- [project.scripts] nova = "nova.cli:main"
- [tool.setuptools.packages.find] includes nova, core, adapters
- requires-python bumped to >=3.12
- new `identity` extra (argon2-cffi, cryptography, pyjwt)
- hypothesis>=6.100.0 added to `test` extra
- fix build-backend to setuptools.build_meta (was non-existent
  setuptools.backends._legacy:_Backend — entry-point install was broken)
- ignore .venv/ + nova.egg-info/ workspace artifacts

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:23:25 +00:00
Jon Chery 939a39743d merge(phase/00): v1.28 P0 pre-execution complete (specify→clarify→research→plan→grill→mvp/ux) 2026-08-19 22:11:05 +00:00
Jon Chery 88e2389a95 docs(ship): P0 complete → v1.27.0 (v1.28 pre-execution)
Nova Slides Render / render (push) Failing after 25s
---ci---
project: acdl
phase: 0
milestone: v1.28
status: complete
---/ci---
2026-08-19 22:11:01 +00:00
Jon Chery a0c363c063 decision(P00): mvp/ux gate — auto-generated (3 sections verified, PASS)
---ci---
project: acdl
phase: 0
milestone: v1.28
status: mvp_ux_check
---/ci---
2026-08-19 22:10:24 +00:00
Jon Chery bbfcbcc4d3 docs(P00): grill — v1.28 adversarial review (PROCEED 0.76, 3 critical + 16 tracked conditions applied)
---ci---
project: acdl
phase: 0
milestone: v1.28
status: grill
---/ci---
2026-08-19 22:10:15 +00:00
Jon Chery e1dc59ba79 docs(P00): create phase plans — v1.28 (7 phases, 31 REQs, 6 CAPs, MVP/UX sections)
---ci---
project: acdl
phase: 0
milestone: v1.28
status: plan
---/ci---
2026-08-19 22:06:38 +00:00
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
Jon Chery 05efb014d6 docs(P00): clarify — v1.28 ambiguities resolved (6 Qs + 5 grounding gaps, D-226..D-231)
---ci---
project: acdl
phase: 0
milestone: v1.28
status: clarify
---/ci---
2026-08-19 21:58:41 +00:00
Jon Chery 9ee1cc8925 docs(init): validate specification — v1.28 CLI Canonicalization + Identity Layer
---ci---
project: acdl
phase: 0
milestone: v1.28
status: specify
---/ci---
2026-08-19 21:57:53 +00:00
Jon Chery 48a769ced0 merge(milestone): v1.27 PO State Catalog & Ciagent Compression to main (release v1.26.3)
acdl-ci / Test (push) Failing after 21s
acdl-ci / Platform check-only (offline) (push) Failing after 14m28s
acdl-ci / Lint (push) Failing after 14m40s
v1.27 NFR milestone complete. Authored .ciagent/STATE.md (PO-facing
capability catalog) + compressed .ciagent/ by archiving 8 outdated
files + fixed v1.26 phase-status in PROJECT.md/ROADMAP.md + wired
STATE.md into the P-final ship discipline.

Tags: v1.26.0 (P0) → v1.26.1 (P1) → v1.26.2 (P2) → v1.26.3 (P3 = milestone release).

---ci---
project: acdl
phase: 3
milestone: v1.27
status: complete
---ci---
2026-08-19 19:18:19 +00:00
Jon Chery 45423c33ae merge(phase/03): v1.27 P3 final review + audit complete — milestone release
Tags: v1.26.3 (P3 = milestone release on the v1.26.x line). NFR milestone.

---ci---
project: acdl
phase: 3
milestone: v1.27
status: complete
---ci---
2026-08-19 19:18:15 +00:00
Jon Chery 1faf4b560f docs(milestone): complete v1.27 PO State Catalog & Ciagent Compression (release v1.26.3)
Nova Slides Render / render (push) Failing after 26s
v1.27 COMPLETE. NFR milestone — PO State Catalog & Ciagent Compression.

Phases:
- P0 pre-execution (specify→clarify→research→plan→grill) → v1.26.0
- P1 author-archive (STATE.md + 8 files archived) → v1.26.1
- P2 fix-stale-wire (PROJECT/ROADMAP phase-status + ship-discipline wiring) → v1.26.2
- P3 final-review-ship (review + audit + milestone ship) → v1.26.3

Delivered:
- .ciagent/STATE.md — PO-facing capability catalog (32 CAP rows +
  11 invariants across 10 domains, backfilled through v1.26). The
  first file the PO reads before writing a new REQ-NNN spec.
- .ciagent/ compression: 7 platform-root files + 1 consumer file
  archived (lossless git mv). Active .md count: 15 (was 25).
- PROJECT.md + ROADMAP.md v1.26 phase-status corrected (P3/P4/P5
  → complete; v1.25.5 shipped; merged to main).
- STATE.md wired into the P-final ship discipline (PLAN.md, ROADMAP.md,
  NORTH_STAR.md). Every future milestone ship appends capability rows +
  bumps the 'Last milestone ship' header.

Review: 0 P0 issues. Audit: reconstruction PASS, file discipline CLEAN,
branch hygiene CLEAN, commit discipline CLEAN (13/13 ---ci--- blocks).
NFR purity gate holds (zero feat: commits).

---ci---
project: acdl
phase: 3
milestone: v1.27
status: complete
---ci---
2026-08-19 19:18:12 +00:00
Jon Chery 8f62cfdbe7 merge(phase/02): v1.27 P2 fix-stale-wire complete
Tags: v1.26.2 (P2 ship on the v1.26.x line).

---ci---
project: acdl
phase: 2
milestone: v1.27
status: complete
---ci---
2026-08-19 19:17:07 +00:00
Jon Chery f28aed2f55 docs(ship): P2 complete → v1.26.2 (v1.27 fix-stale-wire)
Nova Slides Render / render (push) Failing after 24s
---ci---
project: acdl
phase: 2
milestone: v1.27
status: complete
---ci---
2026-08-19 19:17:07 +00:00
Jon Chery 6b410d9ab4 docs(P02): fix stale phase-status + wire STATE.md into ship discipline
P2 W1: PROJECT.md v1.26 phase-status block (lines 428-438):
- P3/P4/P5 'pending' → 'complete' with shipped tags (v1.25.3/4/5)
- v1.26 Overview marked shipped (merged to main 2026-08-19)
- Added STATE.md pointer to Capability Status section header
- Updated CAPABILITY_INVENTORY.md refs → archive/CAPABILITY_INVENTORY-v1.10.md

P2 W2: ROADMAP.md v1.26 section:
- P3/P4/P5 'planned' → 'complete' with shipped tags
- v1.26 Overview '(active, ...)' → '(complete, tag v1.25.5, merged to main)'
- Added STATE.md to v1.25 + v1.26 P5 'Updated at ship' lists

P2 W3: Wired STATE.md into ship discipline:
- PLAN.md: added 'Durable convention (v1.27 establishes)' section —
  every future P-final Wave 3 file-update list includes STATE.md
  (append new capability rows, mark deprecations, bump 'Last
  milestone ship' header).
- NORTH_STAR.md: added 'Relationship to engineering files (v1.27
  update)' section — STATE.md is the *what exists* catalog (PO-owned,
  additive); NORTH_STAR is the *why*; ARCHITECTURE the *how*;
  CHECKPOINT the *now*.

P2 W4: archive README + consumer PROJECT pointer:
- archive/README.md: added 'v1.27 compression — archived files (8
  files, lossless git mv)' section with 3 tables (3 superseded refs +
  4 v1.26 verifications/review/evidence + 1 consumer) + a note on the
  v1.26 pre-execution artifacts (in git history, not on disk).
  Updated 'Why archive' to record both compressions (v1.26 P2 +
  v1.27 P1).
- nova-blockchain-exchange/PROJECT.md: added phase-by-phase history
  pointer to platform ROADMAP §v1.26 (consumer ROADMAP archived).

P2 W5: Fixed remaining dangling references to archived files:
- ARCHITECTURE.md §12.8 line 566: P4-PILOT-RUN-EVIDENCE.md → archive/
- nova-blockchain-exchange/README.md (3 refs): P4-PILOT-RUN-EVIDENCE.md
  → archive/P4-PILOT-RUN-EVIDENCE-v1.26.md
- IAM_POLICY.md (2 refs): CAPABILITY_INVENTORY.md → archive/

Verified: 0 active dangling references remaining (grep confirms all
matches are in archive/ or v1.27 P0 records describing the archive).

---ci---
project: acdl
phase: 2
milestone: v1.27
status: execute
wave: W6
---ci---
2026-08-19 19:16:43 +00:00
Jon Chery d019a1c4c4 merge(phase/01): v1.27 P1 author-archive complete (STATE.md + 8 files archived)
Tags: v1.26.1 (P1 ship on the v1.26.x line).

---ci---
project: acdl
phase: 1
milestone: v1.27
status: complete
---ci---
2026-08-19 19:14:12 +00:00
Jon Chery 2b2423532b docs(ship): P1 complete → v1.26.1 (v1.27 author-archive)
Nova Slides Render / render (push) Failing after 26s
---ci---
project: acdl
phase: 1
milestone: v1.27
status: complete
---ci---
2026-08-19 19:14:12 +00:00
Jon Chery f2b481716d chore(P01): archive 7 platform + 1 consumer outdated .ciagent files
P1 W1: verified STATE.md 32 CAP rows against regression_verify.py
(fixed CAP-025 omission — was missing from Domain 9; CAP-031 renumbered
to cover the live-apply evidence row).

P1 W2: archived 7 platform-root files to .ciagent/archive/ with
milestone-suffix names (lossless git mv preserves history):
- CAPABILITY_INVENTORY.md → CAPABILITY_INVENTORY-v1.10.md
- REVIEW-AUDIT-P05.md → REVIEW-AUDIT-P05.md
- VERIFY-P03.md → VERIFY-P03.md
- VERIFY-P04.md → VERIFY-P04.md
- P4-PILOT-RUN-EVIDENCE.md → P4-PILOT-RUN-EVIDENCE-v1.26.md
- AUTONOMY_THESIS.md → AUTONOMY_THESIS-v1.21.md
- COST.md → COST-v1.14.md

P1 W3: archived 1 consumer file to new .ciagent/nova-blockchain-exchange/archive/
(D-221: consumer archives land in per-project subdir):
- nova-blockchain-exchange/ROADMAP.md → archive/ROADMAP-v1.26.md

The 4 pre-execution files (CLARIFY/GRILL/IDEATE/RESEARCH) stay active
through v1.27 — they hold the v1.27 P0 content (D-219 refinement,
G-Q2); the v1.26-era content is in git history. They archive at
v1.28 P1 if v1.28 happens.

Dangling references to archived files found in PROJECT.md,
ARCHITECTURE.md, IAM_POLICY.md, nova-blockchain-exchange/README.md —
fixed in P2.

---ci---
project: acdl
phase: 1
milestone: v1.27
status: execute
wave: W4
---ci---
2026-08-19 19:13:48 +00:00
Jon Chery 135359ebb8 merge(phase/00): v1.27 P0 pre-execution complete (specify→clarify→research→plan→grill)
Tags: v1.26.0 (P0 ship on the v1.26.x line). NFR milestone.

---ci---
project: acdl
phase: 0
milestone: v1.27
status: complete
---ci---
2026-08-19 19:12:54 +00:00
Jon Chery ecc9730f24 docs(ship): P0 complete → v1.26.0 (v1.27 pre-execution)
---ci---
project: acdl
phase: 0
milestone: v1.27
status: complete
---ci---
2026-08-19 19:12:51 +00:00
149 changed files with 15986 additions and 1971 deletions
+147 -2
View File
@@ -563,7 +563,7 @@ audit record (D-204).
The full evidence (every ARN, the confidence JSON, the Decision Ledger
rows, the module-completeness gaps the live apply uncovered) is in
`.ciagent/P4-PILOT-RUN-EVIDENCE.md`.
`.ciagent/archive/P4-PILOT-RUN-EVIDENCE-v1.26.md` (archived v1.27).
### §12.9 — Secret Rotation (v1.26 P3 W7, SPEC §5.9 — current)
@@ -576,4 +576,149 @@ 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:*`.
### §12.11 — Platform Ops Reposplit (v1.29, current)
Platform operations are a Terraform-controlled discipline that lives
outside the engineering repo, grounded in Vision §4 (Domain
Boundaries — *the platform begins where the artifact is compiled and
ends where it runs in production under operational guardrails*). Two
repos, two ownership surfaces:
- **`acdl/acdl` (GitHub)** — engineering. Authors `publish.yml` + the
artifacts (Lambda zip, layer wheel, Python wheel, ECR container
image with the static `kj` binary). Each tag `v1.29.x` produces a
GitHub Release with SHA-256-verified artifacts (REQ-354, D-235
tag-pin handoff). Engineering ends at the compiled artifact.
- **`nova-platform-ops` (Gitea-private, OPER-PRIV, REQ-359)** —
operations. Authors the Terraform modules
(`networking`/`kms`/`identity`/`contract-ingest`/`bootstrap`/`edge`)
that bring those artifacts live in `581513795199`. Operations begins
at the live platform under guardrails. No GitHub mirror; CIAgent has
no presence there.
The handoff between the two repos is the **tag-pin** (D-235):
`nova-platform-ops` declares `local.nova_platform_version` +
`local.kj_source_sha` and resolves substrates through a single
`data.aws_ecr_image.kj_image`.
**The `kj` substrate (KJ-LOCKSTEP, REQ-371):** `kj` (a compiled Go
binary, pinned v0.0.3 in `platform/abac/kj-version.txt`, distinct from
the kyverno-json engine) has exactly **one identity**: one ECR image
digest shared by the production Lambda runtime
(`aws_lambda_function.nova_idp_token_vend.image_uri`) and its
defensive Fargate fallback
(`aws_ecs_task_definition.kj.container_definitions[0].image`). A
`lifecycle.precondition` on both image-bearing resources enforces at
every `terraform plan` that both `image_uri` attributes resolve to the
same digest via `data.aws_ecr_image.kj_image`. No second pipeline, no
second SHA pin (D-238). KJ-STATIC: the binary is compiled
`CGO_ENABLED=0` and `file(1)` reports `statically linked, no shared
library` before embedding.
**Covered-reference REQ tracking pattern:** the 14 covered-reference
REQs (355-366, 371) are authored in `nova-platform-ops` (out-of-band).
CIAgent in `acdl` tracks them for milestone completeness; their
verification surface is the M1/M1.5/M2 cutover gates documented in
the operator guide. The operator guide lists each covered-reference
REQ with its gate entry + verification command + a "Result" column
that the operator attests after running the gate in
`nova-platform-ops` CI. P6 audit verifies every covered-reference REQ
has a non-empty, green Result (grill CF-2/G-5). M1.5 green (3
consecutive rebuilds of the 12-item spike, operator-attested in the
guide) is the HARD P6 ship gate (grill CF-1/G-2.1).
**Operator guide pointer:** `docs/operator-guide-platform-ops.md`
(REQ-OPS-GUIDE) — the operator-facing runbook covering the Day-0
cutover, M1.5 verification gate, M2 handoff loop, rollback, KMS
rotation, JWKS reachability via CloudFront edge (INV-18), PITR
restore, PAT revocation, edge config, Fargate standby health, cost,
artifact-mirror fallback, and the cutover gates table.
**JWKS edge (INV-18, D-233):** the JWKS endpoint is the only public
read surface of the live platform. CloudFront + OAC pinning
(`AuthType: AWS_IAM` on the Function URL — NOT `NONE`,
`OriginAccessControlOriginType: lambda`, `SigningBehavior: always`)
replaces direct Lambda Function URL exposure. Direct Function URL →
403; via-CloudFront → 200.
+23 -10
View File
@@ -1,15 +1,28 @@
{
"phase": 0,
"stage": "research",
"milestone": "v1.27",
"phase_role": "pre_execution",
"phase": 1,
"stage": "verify",
"milestone": "v1.31",
"phase_role": "execution",
"attempts": 0,
"updated_at": "2026-08-19T04:30:00Z",
"updated_at": "2026-08-20T16:35:00Z",
"project": "acdl",
"projects": ["acdl", "nova-blockchain-exchange"],
"active_milestone": "v1.27",
"milestone_branch": "milestone/v1.27-po-state-catalog",
"phase_branch": "phase/00-pre-execution",
"tag_line": "v1.26.x",
"notes": "NFR milestone. PO state catalog + .ciagent/ compression. 3 phases planned (P1 author+archive, P2 fix-stale+wire, P3 final)."
"active_milestone": "v1.31",
"milestone_branch": "milestone/v1.31-leadership-deck-polish",
"phase_branch": "acdl/phase/01-deck-polish",
"tag_line": "v1.30.x",
"phase_name": "deck-polish",
"milestone_type": "nfr",
"reqs_covered": ["REQ-373.1","REQ-373.2","REQ-373.3","REQ-373.4"],
"reqs_partial": [],
"previous_milestone": {
"milestone": "v1.30",
"tag": "v1.29.5",
"complete": true,
"merged_to_main": "13ee34b",
"branches_deleted": true,
"releases_created": true,
"release_ids": [811,813,814,818]
},
"notes": "v1.31 P1 VERIFY PASS. check_leadership_deck.sh: ALL CHECKS PASSED. (a) source exists, (b) slide count=7, (c) speaker-note word counts in band, (d) footer present, (e) only S&P theme colors, (f) PPTX exists. [1] citations confirmed on slides 3/5/7. Visible prose density enriched (slides 2-6: 43-67 -> 97-168 words). REQ-373.1..4 all complete."
}
-183
View File
@@ -1,183 +0,0 @@
# CLARIFY — v1.27 PO State Catalog & Ciagent Compression
> **Autonomy:** full. Auto-resolution with assumption logging per
> `config.autonomy.level: "full"`. No human escalation unless
> confidence < 0.60. The prior conversation resolved all material
> ambiguities (4 user-answered questions). This file records the
> assumptions for the v1.27 record.
---
## Method
The clarify stage identifies ambiguities in the v1.27 specification
and resolves them at full autonomy. The v1.27 spec is the user-approved
plan from the prior conversation + the STATE.md design locked by 4
question answers. Each ambiguity gets a decision ID (D-214+; continuing
from the v1.26 decisions D-200..D-213), a resolution, a confidence
score, and a rationale.
---
## Prior-conversation resolutions (already locked, restated for the record)
These were resolved by user-answered questions in the conversation that
spawned v1.27. They are load-bearing for v1.27 execution and cited
here so the v1.27 record is self-contained.
### Q-P1 — What should the new PO-reference file catalog?
**Resolution:** Capability catalog (what the system can do today).
**Confidence:** 1.0 (user-confirmed). **Decision:** D-214.
### Q-P2 — How should the new file relate to CAPABILITY_INVENTORY.md?
**Resolution:** Call it `STATE.md`. PO-owned, ciagent-updated after
milestone implementation. CAPABILITY_INVENTORY.md is archived.
**Confidence:** 1.0 (user-confirmed). **Decision:** D-215.
### Q-P3 — Where should the file live, and who owns it?
**Resolution:** Owned by the PO, updated by ciagent after the milestone
is implemented with additives.
**Confidence:** 1.0 (user-confirmed). **Decision:** D-216.
### Q-P4 — How should "additive when new features are implemented" be enforced?
**Resolution:** On the last phase / milestone ship (the P-final Wave 3
"milestone ship" step). No regression-gate check in this pass.
**Confidence:** 1.0 (user-confirmed). **Decision:** D-217.
### Q-P5 — Should the initial STATE.md backfill all shipped capabilities through v1.26?
**Resolution:** Backfill all shipped capabilities through v1.26
(compressed one-liners for v1.1v1.24; full entries for v1.25 + v1.26).
**Confidence:** 1.0 (user-confirmed). **Decision:** D-218.
### Q-P6 — Should the v1.26 pre-execution artifacts (CLARIFY, GRILL, IDEATE, RESEARCH) be archived?
**Resolution:** Archive all 4 to `.ciagent/archive/` with `-v1.26`
suffixes. The next milestone's P0 writes fresh versions. Decisions are
already folded into PROJECT.md load-bearing decisions + PLAN.md
binding revisions.
**Confidence:** 1.0 (user-confirmed). **Decision:** D-219.
---
## Ambiguities + Resolutions (this CLARIFY pass)
### Q1 — Is v1.27 a feature milestone or an NFR milestone?
**Ambiguity:** v1.27 authors `STATE.md` (a new file/capability for the
PO) and archives 11 files. Does the new-file authoring count as `feat:`
(making this a feature milestone, tags on v1.26.x with progressive
patches) or `docs:`/`chore:` (NFR milestone, same tag behavior but
subject to the NFR purity gate)?
**Resolution:** NFR milestone. `STATE.md` is documentation (a catalog of
existing capabilities), not a new platform capability. The archive moves
are `chore:` (file relocation, lossless). No code, no schema, no
platform behavior change. Tags run on the v1.26.x patch line:
`v1.26.0` (P0) → `v1.26.1..v1.26.3` (P1..P3). The final phase's patch
(`v1.26.3`) IS the milestone release.
**Confidence:** 0.95. **Decision:** D-220.
### Q2 — Where does the consumer-side archive (nova-blockchain-exchange/ROADMAP.md) land?
**Ambiguity:** The platform archive convention is
`.ciagent/archive/<file>-<milestone>.md`. The consumer subproject
(`nova-blockchain-exchange/`) has no `archive/` subdirectory. Does the
consumer ROADMAP archive at `.ciagent/archive/` (platform-side, mixed)
or `.ciagent/nova-blockchain-exchange/archive/` (consumer-side, new
subdir)?
**Resolution:** Consumer-side. Create
`.ciagent/nova-blockchain-exchange/archive/` and relocate to
`ROADMAP-v1.26.md`. This preserves the per-project path convention
(multi-project mode: `.ciagent/<slug>/` paths). The platform archive
directory is not mixed with consumer archives.
**Confidence:** 0.92. **Decision:** D-221.
### Q3 — Does archiving CLARIFY/GRILL/IDEATE/RESEARCH lose the "how v1.26 was specified" traceability?
**Ambiguity:** The pre-execution artifacts document the v1.26 decision
path. Archiving them moves them out of active context. Is the
traceability preserved?
**Resolution:** Yes. Three layers preserve it: (1) the archive files
are byte-identical relocations inside `.ciagent/archive/` (reachable by
agents + git history); (2) the decisions D-200..D-213 are folded into
`PROJECT.md` load-bearing decisions (the durable record); (3) git
history at the v1.26 commits preserves the authoritative state. The
active-context reduction is the point — v1.26 is shipped; the next P0
writes fresh CLARIFY/GRILL/IDEATE/RESEARCH.
**Confidence:** 0.95. **Decision:** D-222.
### Q4 — Should IAM_POLICY.md be archived (it predates v1.26 and is dated v1.11)?
**Ambiguity:** `IAM_POLICY.md` is dated v1.11 (2026-07-28). It predates
v1.26 by 5 milestones. The D-207 future key-split (P1+ R-3 in
REVIEW-AUDIT-P05) is pending. Archive or keep?
**Resolution:** Keep active. `IAM_POLICY.md` is a live baseline —
referenced by the regression gate
(`tests/test_iam_policy_baseline.py`), enforced by a managed policy on
account `581513795199`, and the D-207 key-split is a pending future-
hardening item. It is not stale; it is a baseline that grows when
grants change. The v1.11 date reflects the last grant addition, not
staleness.
**Confidence:** 0.90. **Decision:** D-223.
### Q5 — Should REGRESSION_REPORT.{json,md} be refreshed as part of v1.27?
**Ambiguity:** Both files are dated 2026-08-01 (v1.10 Phase 52), show
CAP-025 absent, and mark live-aws CAPs "Skipped" (state bucket absent
pre-v1.26 re-bootstrap). They are stale. Should v1.27 refresh them?
**Resolution:** No. Both files are machine-managed — written by
`core/regression_verify.py:704-705` on every `run_regression.sh` run.
They regenerate on the next regression run. v1.27 is docs/chore only
(no code); touching machine-managed files by hand creates a drift
source. The stale state is honest (the last gate run was v1.10; the
next run regenerates). The STATE.md Domain 7 row "Regression gate"
notes the current CAP range (CAP-001..025).
**Confidence:** 0.88. **Decision:** D-224.
### Q6 — Does PROJECT.md get the v1.26 phase-status fix in v1.27 P1 or P2?
**Ambiguity:** The plan splits work into P1 (author + archive) and P2
(fix stale + wire). The PROJECT.md phase-status fix (P3/P4/P5 pending
→ complete) is a "fix stale" item. P1 or P2?
**Resolution:** P2. P1 is the additive authoring + lossless archive
moves. P2 is the corrections to kept files + the ship-discipline wiring.
This keeps P1 a pure-additive, no-edit phase (easier review + audit) and
P2 the correction phase. The PROJECT.md fix is a correction; P2.
**Confidence:** 0.85. **Decision:** D-225.
---
## Summary
6 prior-conversation resolutions (D-214..D-219, all user-confirmed)
+ 6 new ambiguities (D-220..D-225, all auto-resolved at full autonomy,
confidence ≥ 0.60). 0 escalations.
**Key decisions:**
- D-220: v1.27 is an NFR milestone (tags on v1.26.x; final patch is the
milestone release).
- D-221: Consumer archives land in `.ciagent/nova-blockchain-exchange/archive/`.
- D-222: Archiving pre-execution artifacts preserves traceability
(archive files + PROJECT.md load-bearing decisions + git history).
- D-223: IAM_POLICY.md stays active (live baseline, test-enforced,
D-207 pending).
- D-224: REGRESSION_REPORT.{json,md} regenerate on next
`run_regression.sh` (machine-managed; v1.27 is docs/chore only).
- D-225: PROJECT.md phase-status fix is P2 (correction phase), not P1
(additive phase).
-141
View File
@@ -1,141 +0,0 @@
# GRILL — v1.27 PO State Catalog & Ciagent Compression
> Adversarial review of the v1.27 SPECIFY + CLARIFY + RESEARCH + PLAN.
> The grill red-teams the proposal across feasibility, scope, and the
> compression-loss claims. Each challenge gets a binding verdict
> (PROCEED / REVISE / ESCALATE). Autonomy: full.
## Verdict: PROCEED (0.88) — 0 escalations, 1 revision
The milestone is feasible, scoped, and the compression is lossless. One
binding revision (G-Q2) refines the archive list; already captured in
PLAN. No work is blocked.
---
## Challenges
### G-Q1 — Is archiving AUTONOMY_THESIS.md + COST.md a context loss?
**Challenge:** `AUTONOMY_THESIS.md` is the "autonomy in operations;
human at stage gates" thesis — the defensibility brief. `COST.md` is
the only AWS cost record. Archiving both moves them out of active
context. Does this lose load-bearing content?
**Verdict:** PROCEED (confidence 0.90).
- `AUTONOMY_THESIS.md` (65 lines, "Last refined: v1.21") is fully
folded into `NORTH_STAR.md` Vision (lines 1722: "infrastructure
operations become visible... human attestation remains required at
stage gates") + Anti-Goals #2 ("Not a system that removes humans from
accountability"). The thesis is the source; NORTH_STAR is the
authoritative durable copy. Archive preserves the v1.21 refinement;
active context reads NORTH_STAR.
- `COST.md` (106 lines, dated 2026-07-29, "v1.0 → v1.14") predates the
v1.26 live pilot. The v1.26 live apply (ECS + ALB + DynamoDB + S3)
incurred real costs this snapshot doesn't reflect. Archiving it is
honest — a stale cost record misleads. STATE.md Domain 7 notes cost
tracking as a capability (pre-apply Infracost grounded; actual-spend
CUR deferred D-096). A future cost milestone writes a fresh report.
No revision needed.
### G-Q2 — Does the archive list include the v1.27 P0 pre-execution files by mistake?
**Challenge:** D-219 (user-confirmed) says "archive all 4 pre-execution
artifacts" (CLARIFY/GRILL/IDEATE/RESEARCH). But P0 already overwrote
them with v1.27 content. Archiving the v1.27 versions at v1.27 P1 would
lose the v1.27 pre-execution narrative (the decisions D-214..D-225, the
research inventory, this grill). Is the archive list wrong?
**Verdict:** REVISE (confidence 0.92). This is a real ambiguity in the
plan. The user's D-219 decision was made *before* P0 overwrote the
files; the intent was to archive the *v1.26* pre-execution record. The
v1.26-era content is preserved in git history (the pre-P0 commits) —
the archive directory is not the only preservation layer. PLAN Task 2.1
already self-corrected: the final archive list is **7 platform files +
1 consumer file = 8 files**, excluding the 4 pre-execution files. The 4
v1.27 P0 versions stay active through v1.27; they archive at v1.28 P1
if v1.28 happens. The archive README notes the v1.26 pre-execution
record is in git history. No further revision needed — the plan self-
corrected.
### G-Q3 — Is the STATE.md backfill accurate enough to be the PO's source of truth?
**Challenge:** STATE.md has 36 capability rows across 10 domains,
backfilled from 8 sources. The PO will read this before writing new
REQs. If a row is inaccurate (wrong shipped tag, wrong file path,
wrong controlling REQ), the PO could re-spec an existing capability or
cite a stale invariant. Is the backfill accurate?
**Verdict:** PROCEED (confidence 0.85). The backfill sources are
authoritative: `core/regression_verify.py` (the machine CAP-NNN
registry), `modules/registry.json` (the live module catalog),
`REQUIREMENTS.md` traceability (the REQ→phase→status record),
`CHECKPOINT.json` (shipped tags), `git log` (file paths). The
citations are direct (each row cites the controlling REQ + decision
ID). The 11 invariants are distilled from PROJECT.md load-bearing
decisions D-034..D-072 + W1..BA + Q1.3. The accuracy risk is
mitigated by P1 Wave 1 (verify STATE.md against sources before
archive). No revision needed — the verification step is in the plan.
### G-Q4 — Does the NFR purity gate (zero `feat:` commits) hold for v1.27?
**Challenge:** v1.27 authors STATE.md (a new file). Is authoring a new
catalog file a `feat:` (feature) that breaks the NFR purity gate?
**Verdict:** PROCEED (confidence 0.92). D-220 (CLARIFY) resolved this:
STATE.md is documentation (a catalog of *existing* capabilities), not a
new platform capability. The archive moves are `chore:` (file
relocation, lossless). No code, no schema, no platform behavior
change. The NFR purity gate (zero `feat:` commits) holds. All v1.27
commits use `docs(P0N):` or `chore(P01):` prefixes. No revision
needed.
### G-Q5 — Does fixing PROJECT.md phase-status in P2 create a P0/P1 audit inconsistency?
**Challenge:** The PROJECT.md phase-status block shows P3/P4/P5 as
"pending" (the bug flagged in the prior conversation). P0 + P1 ship
with the bug still present (the fix is P2). Does the P0/P1 audit see
the inconsistency?
**Verdict:** PROCEED (confidence 0.86). The bug is pre-existing
(it predates v1.27; it was the trigger for the prior conversation).
P0/P1 audits check the *v1.27* commits against the `.ciagent/` state,
not the pre-existing PROJECT.md drift. The P2 fix is the correction;
the P3 audit verifies the fix landed. The intermediate state (P0/P1
with the bug present) is honest — the bug is documented in the v1.27
PLAN + the prior conversation, and the fix is scheduled. No revision
needed — the phasing is intentional (D-225: P1 additive, P2
correction).
### G-Q6 — Is the milestone scoped too small (3 phases, 8 archive moves)?
**Challenge:** v1.27 is a small milestone (3 phases, ~15 file
operations, no code). Is it worth a milestone, or should it be a
patch on v1.26?
**Verdict:** PROCEED (confidence 0.88). v1.27 is not a patch on v1.26
— v1.26 is shipped (`v1.25.5`, merged to main, milestone complete).
The work is a new milestone by definition. The size is appropriate:
STATE.md is a durable PO-facing artifact (loaded every ci-run going
forward); the compression reduces active context by ~26%; the
ship-discipline wiring affects every future milestone ship. Small but
high-leverage. No revision needed.
---
## Summary
6 challenges; 0 escalations; 1 binding revision (G-Q2, already
captured in PLAN Task 2.1). Overall verdict: PROCEED (confidence
0.88).
**Binding revisions:**
- **G-Q2:** Archive list refined to 7 platform + 1 consumer = 8 files.
The 4 pre-execution files (CLARIFY/GRILL/IDEATE/RESEARCH) stay active
through v1.27 (they hold the v1.27 P0 content); the v1.26-era content
is in git history. Already in PLAN.
**No work is blocked.** The milestone is feasible, scoped, the
compression is lossless (archive + git history), the STATE.md backfill
is source-grounded with a verification step, the NFR purity holds, and
the phasing (P1 additive, P2 correction, P3 ship) is sound.
+12
View File
@@ -231,6 +231,18 @@ their AI engineering teams reach for first when an agent needs to deploy.
leadership. The deck's Proof section cites grounded metrics; its
Roadmap section cites deferred targets honestly.
## Relationship to engineering files (v1.27 update)
- **NORTH_STAR.md** (this file) = the *why* — PO-authored strategic
direction, loaded every ci-run via `config.strategic_direction_file`.
- **STATE.md** = the *what exists* — PO-owned capability catalog,
additive, updated at every milestone ship (P-final Wave 3). The PO
reads STATE.md before writing new REQ-NNN specs to avoid re-spec'ing
existing capability and to respect the invariants.
- **ARCHITECTURE.md** = the *how* — the durable target architecture.
- **CHECKPOINT.json** = the *now* — authoritative live phase/ship
state.
## v1.25 update — swappable policy-engine substrate
Strategic Objective #2 (provable trust) gained a concrete substrate in
-77
View File
@@ -1,77 +0,0 @@
---
project: acdl
milestone: v1.27
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"
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.
---
# PERSONAS — v1.27 PO State Catalog & Ciagent Compression
> 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.
## Active Roster (1)
### 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.
## Deactivated (5)
### 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).
### data-engineer (inactive)
- **active:** false
- **reason:** No schema, migration, or ORM changes.
### policy-engineer (inactive)
- **active:** false
- **reason:** No policy authoring. STATE.md Domain 3 catalogues
existing v1.25 + v1.26 policies (descriptive, not authoring).
### frontend-engineer (inactive)
- **active:** false
- **reason:** No UI. Deactivated since v1.26 (PERSONAS.md:141).
### blockchain-engineer (inactive)
- **active:** false
- **reason:** No chain code. The v1.26 pilot is shipped; v1.27 is
platform-side docs/chore only.
## Territory Enforcement
- **Mode:** `warn` (the milestone is `.ciagent/`-only; the lead-
developer owns all writes; no cross-territory collisions expected).
-251
View File
@@ -1,251 +0,0 @@
# PLAN — v1.27 PO State Catalog & Ciagent Compression
> **Milestone:** v1.27 (NFR — docs/chore only). Tags on the **v1.26.x**
> line: `v1.26.0` (P0) → `v1.26.1..v1.26.3` (P1..P3). The final phase's
> patch (`v1.26.3`) IS the milestone release.
> **Branch:** `milestone/v1.27-po-state-catalog`. Phase branches:
> `phase/00-pre-execution`, `phase/01-author-archive`,
> `phase/02-fix-stale-wire`, `phase/03-final-review-ship`.
## Milestone goal
Author `.ciagent/STATE.md` (PO-facing capability catalog, backfilled
through v1.26) + compress `.ciagent/` by archiving 11 outdated files +
fix 3 stale-but-kept files + wire STATE.md into the P-final ship
discipline. NFR milestone — no code, no schema, no platform behavior
change.
## Requirements
No new REQ-NNN. v1.27 is a docs/chore milestone; the work items are
the user-approved plan from the prior conversation. The traceability
is by-file (the "requirements" are the 15 file operations + 6 doc
edits in the plan summary).
## Phase breakdown
### Phase P1 — author-archive (additive + lossless)
**Goal:** Author STATE.md (already done in P0 SPECIFY, refined here)
+ archive 11 outdated files. Pure-additive + lossless moves only —
no edits to kept files.
#### Wave 1 — verify STATE.md backfill
- **Task 1.1** (lead-developer): verify STATE.md 36 capability rows
against the authoritative sources (regression_verify.py CAP-NNN list,
modules/registry.json, REQUIREMENTS.md traceability, CHECKPOINT
tags). Fix any inaccurate citation (shipped tag, file path).
#### Wave 2 — archive platform-root files (10)
- **Task 2.1** (lead-developer): `git mv` 10 files to
`.ciagent/archive/` with milestone-suffix names:
- `CAPABILITY_INVENTORY.md``CAPABILITY_INVENTORY-v1.10.md`
- `CLARIFY.md``CLARIFY-v1.26.md`
- `GRILL.md``GRILL-v1.26.md`
- `IDEATE.md``IDEATE-v1.26.md`
- `RESEARCH.md``RESEARCH-v1.26.md`
- `REVIEW-AUDIT-P05.md``REVIEW-AUDIT-P05.md`
- `VERIFY-P03.md``VERIFY-P03.md`
- `VERIFY-P04.md``VERIFY-P04.md`
- `P4-PILOT-RUN-EVIDENCE.md``P4-PILOT-RUN-EVIDENCE-v1.26.md`
- `AUTONOMY_THESIS.md``AUTONOMY_THESIS-v1.21.md`
- `COST.md``COST-v1.14.md`
Use `git mv` to preserve history. NOTE: CLARIFY/GRILL/IDEATE/RESEARCH
were rewritten in P0 with v1.27 content — archive the v1.27 versions
(they document the v1.27 pre-execution; the next P0 writes fresh).
Wait — per D-219, the v1.26 pre-execution artifacts are archived. The
v1.27 versions replace them in active context; they are NOT archived
at P1 (they are the current P0 artifacts, active until v1.27 ships,
then archived at v1.28 P1 if v1.28 happens). **Correction:** archive
only the v1.26-era pre-execution artifacts. But P0 already
overwrote CLARIFY/GRILL/IDEATE/RESEARCH with v1.27 content. The v1.26
content lives in git history (the pre-P0 commits). So:
- The 4 pre-execution files (CLARIFY/GRILL/IDEATE/RESEARCH) at HEAD
are the v1.27 P0 artifacts — **keep active** through v1.27, archive
at v1.28.
- The v1.26-era content is in git history — reachable.
**Revised archive list (7 files, not 10):** CAPABILITY_INVENTORY,
REVIEW-AUDIT-P05, VERIFY-P03, VERIFY-P04, P4-PILOT-RUN-EVIDENCE,
AUTONOMY_THESIS, COST.
Hold — let me re-check D-219. The user said "Archive all 4
pre-execution artifacts." That was decided *before* P0 overwrote
them. The intent was to archive the v1.26 pre-execution record. The
v1.27 P0 overwrites are the new pre-execution record. Archiving the
v1.27 versions at v1.27 P1 would lose the v1.27 pre-execution
narrative. **Resolution:** archive the v1.26-era content (preserved
in git history at the pre-P0 commits) by noting it in the archive
README; keep the v1.27 P0 versions active through v1.27. The 4 files
stay active until v1.28 P1.
**Final archive list (7 files):** CAPABILITY_INVENTORY.md,
REVIEW-AUDIT-P05.md, VERIFY-P03.md, VERIFY-P04.md,
P4-PILOT-RUN-EVIDENCE.md, AUTONOMY_THESIS.md, COST.md.
- **Task 2.2** (lead-developer): grep for dangling references to the
archived filenames across `.ciagent/` + `docs/`; fix any in P2 (the
fix-stale phase).
#### Wave 3 — archive consumer file (1)
- **Task 3.1** (lead-developer): `mkdir
.ciagent/nova-blockchain-exchange/archive/` + `git mv
nova-blockchain-exchange/ROADMAP.md` →
`nova-blockchain-exchange/archive/ROADMAP-v1.26.md` (D-221).
#### Wave 4 — commit P1
- **Task 4.1** (lead-developer): single commit `chore(P01): archive 7
platform + 1 consumer outdated .ciagent files` with `---ci---`
block.
**Must-haves (verify before ship):**
- STATE.md 36 rows accurate (Wave 1 verification).
- 7 platform files present in `.ciagent/archive/` with milestone
suffixes; originals gone from `.ciagent/` root.
- 1 consumer file present in
`.ciagent/nova-blockchain-exchange/archive/`; original gone.
- 0 dangling references in active files (checked in P2, but flagged
here).
### Phase P2 — fix-stale-wire (corrections + wiring)
**Goal:** Fix 3 stale-but-kept files + wire STATE.md into the P-final
ship discipline + add a pointer in the consumer PROJECT.md.
#### Wave 1 — fix PROJECT.md phase-status
- **Task 1.1** (lead-developer): `.ciagent/PROJECT.md` lines 424431 —
the v1.26 phase-status block. Mark P3/P4/P5 complete with shipped
tags (`v1.25.3`, `v1.25.4`, `v1.25.5`); mark v1.26 milestone shipped.
- **Task 1.2** (lead-developer): add a one-line pointer to STATE.md in
the "Capability Status" section header (line 130): "The PO-facing
capability catalog is `.ciagent/STATE.md` (additive; updated at
milestone ship). CAP-NNN IDs cross-reference the regression gate at
`core/regression_verify.py`."
#### Wave 2 — fix ROADMAP.md phase-status
- **Task 2.1** (lead-developer): `.ciagent/ROADMAP.md` v1.26 section —
mark P3/P4/P5 complete with shipped tags; mark the v1.26 Overview
line (line 181) "(active, ...)" → "(complete, tag `v1.25.5`)".
- **Task 2.2** (lead-developer): add STATE.md to the v1.25 + v1.26 P5
phase-detail "Updated at ship" list (the convention visibility
point).
#### Wave 3 — wire STATE.md into ship discipline
- **Task 3.1** (lead-developer): `.ciagent/PLAN.md` P5 Wave 3 Task 3.5
— add STATE.md to the file-update list: "append new capability
entries to `.ciagent/STATE.md`; mark any deprecated capability."
- **Task 3.2** (lead-developer): `.ciagent/NORTH_STAR.md` — add a
one-line note in "Relationship to engineering files" (or the v1.25
update section): "STATE.md is the *what exists* catalog (PO-owned,
additive, updated at milestone ship); this file is the *why*."
#### Wave 4 — fix archive README + consumer PROJECT pointer
- **Task 4.1** (lead-developer): `.ciagent/archive/README.md` — add
the 8 new archived files (7 platform + 1 consumer) to the contents
tables (Snapshots + Completed-phase artifacts sections).
- **Task 4.2** (lead-developer):
`.ciagent/nova-blockchain-exchange/PROJECT.md` — add a one-line
pointer to the platform ROADMAP for milestone-phase history (since
the consumer ROADMAP is archived): "Phase-by-phase history:
`.ciagent/ROADMAP.md` §v1.26 (the consumer ROADMAP is archived at
`.ciagent/nova-blockchain-exchange/archive/ROADMAP-v1.26.md`)."
#### Wave 5 — fix any dangling references from P1 Wave 2
- **Task 5.1** (lead-developer): apply fixes for any dangling
references found in P1 Wave 2.
#### Wave 6 — commit P2
- **Task 6.1** (lead-developer): single commit `docs(P02): fix stale
phase-status + wire STATE.md into ship discipline` with `---ci---`
block.
**Must-haves (verify before ship):**
- PROJECT.md v1.26 phase-status matches CHECKPOINT.json (P3/P4/P5
complete, v1.26 shipped).
- ROADMAP.md v1.26 sections show P3/P4/P5 complete + Overview complete.
- PLAN.md P5 Wave 3 names STATE.md.
- NORTH_STAR.md notes STATE.md.
- archive/README.md lists the 8 new archived files.
- nova-blockchain-exchange/PROJECT.md points to platform ROADMAP.
### Phase P3 — final-review-ship (review + audit + milestone ship)
**Goal:** Final review + audit + milestone ship.
#### Wave 1 — review
- **Task 1.1** (lead-developer): review all P1/P2 changes for
correctness (no broken markdown, no inaccurate citations, no
dangling references).
- **Task 1.2** (lead-developer): fix any P0 issues in this phase.
#### Wave 2 — audit
- **Task 2.1** (lead-developer): reconstruction test — git log
`---ci---` blocks ↔ `.ciagent/` files consistent; phase
progression P0→P1→P2→P3.
- **Task 2.2** (lead-developer): `.ciagent/` file discipline —
CHECKPOINT consistent with HEAD; PROJECT/ROADMAP phase-status
consistent with CHECKPOINT; STATE.md present + 36 rows; archive
contents match the moves.
- **Task 2.3** (lead-developer): branch hygiene — only main +
milestone + P3; P1/P2 deleted.
- **Task 2.4** (lead-developer): commit discipline — all v1.27 commits
carry `---ci---` blocks.
#### Wave 3 — milestone ship
- **Task 3.1** (lead-developer): merge `phase/03` →
`milestone/v1.27-po-state-catalog` → `main`.
- **Task 3.2** (lead-developer): tag `v1.26.3` (= the v1.27 release per
prev-minor tagging rule; v1.27 is an NFR milestone, tags on v1.26.x).
- **Task 3.3** (lead-developer): create Gitea release with full
milestone summary.
- **Task 3.4** (lead-developer): delete all milestone branches (local
+ remote). Tags preserve all history.
- **Task 3.5** (lead-developer): update `.ciagent/REQUIREMENTS.md`
(no REQs to mark — NFR milestone), `.ciagent/ROADMAP.md` (mark
v1.27 complete), `.ciagent/NORTH_STAR.md` (no strategic change),
`.ciagent/STATE.md` (bump "Last milestone ship" to v1.27).
- **Task 3.6** (lead-developer): write checkpoint `stage: complete,
phase: 3, phase_role: final` + clear checkpoint (milestone
complete).
**Must-haves (verify before ship):**
- Review: 0 P0 issues unfixed; P1+ flagged for post-hoc.
- Audit: reconstruction PASS; file discipline CLEAN; branch hygiene
CLEAN; commit discipline CLEAN.
- Ship: `v1.26.3` tag exists; Gitea release created; milestone
branches deleted; main has the milestone merge.
---
## Requirement → Phase Mapping
No REQ-NNN (NFR milestone). The work items are file operations,
traced by the Wave tasks above.
---
## Wave Ordering Rationale
- **P1 W1 → W2:** verify STATE.md before archiving (the archive removes
the source-of-truth CAPABILITY_INVENTORY; STATE.md must be accurate
first).
- **P1 W2 → W3:** platform archive before consumer archive (the
platform archive pattern is established; the consumer archive
creates a new subdir).
- **P2 W1 → W2 → W3:** PROJECT.md fix before ROADMAP.md fix before
ship-discipline wiring (PROJECT is the source-of-truth narrative;
ROADMAP mirrors it; PLAN/NORTH_STAR wire the convention).
- **P2 W4:** archive README + consumer pointer (cross-cutting; lands
after the active-file fixes).
- **P2 W5:** dangling-reference fixes (lands after all moves + edits
are known).
---
## Vertical-slice integrity
Each phase ships a self-contained, verifiable slice:
- P1 ships STATE.md (verified accurate) + 8 archived files (verified
moved). The active `.ciagent/` root drops from 25 to 17 files.
- P2 ships 3 fixed files + 3 wired files + archive README + consumer
pointer. The kept files match CHECKPOINT.json state.
- P3 ships the milestone release + cleared checkpoint.
+715 -8
View File
@@ -129,7 +129,11 @@ human at stage gates" model from the NORTH_STAR.
## Capability Status (Re-Verified 2026-07-27)
> Source of truth: `.ciagent/CAPABILITY_INVENTORY.md` (Phase 54, D-093).
> **PO-facing capability catalog:** `.ciagent/STATE.md` (additive;
> updated at milestone ship). CAP-NNN IDs cross-reference the regression
> gate at `core/regression_verify.py`.
> Source of truth (the 2026-07-27 sweep, archived v1.27):
> `.ciagent/archive/CAPABILITY_INVENTORY-v1.10.md` (Phase 54, D-093).
> Tier: **local** = runs via emulating adapters (no AWS); **live-aws** =
> runs against the live AWS account (581513795199).
@@ -156,7 +160,7 @@ service live, CloudFront production stack, uptime-kuma, OIDC role). The
code would deploy them; the local emulators (Phase 53) prove the runtime
behavior. Re-bootstrap of the OIDC role + IAM re-grant requires an admin
principal — escalated, not silently skipped. See
`CAPABILITY_INVENTORY.md` §"Cloud capabilities NOT re-verified".
`CAPABILITY_INVENTORY-v1.10.md` §"Cloud capabilities NOT re-verified".
**Regression gate.** `bash scripts/run_regression.sh` re-runs all 16
auto-verifiable capabilities and fails closed on any non-Verified result.
@@ -340,7 +344,7 @@ plan-JSON policies + pipeline wiring (REQ-300,301,302), meta-policies
(REQ-303), regression-gate policies (REQ-304,305), docs + adapter README
(REQ-306,307), tests (REQ-308,309).
## v1.26 — Live Pilot Estate Activation (active)
## v1.26 — Live Pilot Estate Activation (complete, tag `v1.25.5`, merged to main 2026-08-19)
> **Active milestone.** Feature milestone — the first real consumer estate
> (a stock exchange on a homegrown PoA blockchain, equities only) is
@@ -421,14 +425,717 @@ already exist).
- The consumer deploy MUST go through `deploy.yml@v1.25` — no direct
`terraform apply` bypassing the platform's gates.
### v1.26 phase status (live — see CHECKPOINT.json for the authoritative state)
### v1.26 phase status (shipped — tag `v1.25.5` = the v1.26 release, merged to main 2026-08-19)
- **P0** pre-execution (SPECIFY→CLARIFY→RESEARCH→IDEATE→PLAN→GRILL) — complete, tag `v1.25.0`.
- **P1** blockchain-core (REQ-310,311,312) — complete, tag `v1.25.1`.
- **P2** consumer-contract-and-deploy (REQ-313,314,322) — complete, tag `v1.25.2`.
- **P3** pilot-metrics-and-policies (REQ-315,316,317,318,319,320) — pending.
- **P4** pilot-run-and-docs (REQ-316,321) — pending.
- **P5** final review + audit + milestone ship — pending. Tag `v1.25.5` = the v1.26 release.
- **P3** pilot-metrics-and-policies (REQ-315,316,317,318,319,320) — complete, tag `v1.25.3`.
- **P4** pilot-run-and-docs (REQ-316,321) — complete, tag `v1.25.4` (live apply against `581513795199` succeeded; confidence 0.800 pass; outcome backfilled).
- **P5** final review + audit + milestone ship — complete, tag `v1.25.5` = the v1.26 release (PROCEED; 0 P0 remain; audit CLEAN; merged to main).
> Phase-by-phase task breakdown, wave ordering, and persona assignments
> live in `.ciagent/PLAN.md` (the active phase plan, retained in full).
> live in `.ciagent/PLAN.md` (the active phase plan, retained in full).
> v1.26 pre-execution artifacts (CLARIFY/GRILL/IDEATE/RESEARCH) are in
> git history (pre-v1.27-P0 commits); the v1.26 phase verifications +
> review are archived at `.ciagent/archive/{VERIFY-P03,VERIFY-P04,REVIEW-AUDIT-P05}.md`.
## v1.27 — PO State Catalog & Ciagent Compression (complete, tag `v1.26.3`, merged to main 2026-08-19)
> **NFR milestone — complete.** STATE.md authored (32 CAPs, 11 invariants,
> 10 domains). 8 outdated `.ciagent/` files archived (7 platform + 1
> consumer). PROJECT.md + ROADMAP.md v1.26 phase-status corrected.
> STATE.md wired into P-final ship discipline. Tags: `v1.26.0` (P0) →
> `v1.26.1..v1.26.2` (P1..P2) → `v1.26.3` (P3 final = milestone release).
> Review: 0 P0. Audit: reconstruction PASS, file/branch/commit discipline CLEAN.
> Full phase detail: `.ciagent/archive/` (v1.27 artifacts) + git history.
## v1.28 — CLI Canonicalization + Identity Layer (complete, tag `v1.27.6`, merged to main 2026-08-19)
> **Feature milestone — complete.** The Nova CLI becomes 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
> (sign-up through token vending) with no AWS-managed identity services
> in the path. Nova-idp is introduced: two Lambda functions (`nova-idp-auth`,
> `nova-idp-token-vend`), KMS-signed OIDC tokens, ABAC-gated token vending
> via the existing kyverno-json engine (INV-4 swappable), and PAT
> lifecycle (issuance, revocation, status).
>
> Tags run on the **v1.27.x** line: `v1.27.0` (P0) → `v1.27.1..v1.27.N`
> (execution phases) → `v1.27.(N+1)` (final phase = milestone release).
> Milestone branch: `milestone/v1.28-cli-identity`.
### v1.28 ID allocations (re-mapped — no collisions with shipped history)
- **Decisions:** `D-226..D-231` (authored in CLARIFY). Repo decision
namespace is `D-NNN` (max D-225); no `D-NEW-*` namespace exists.
- **Requirements:** `REQ-323..REQ-353` (31 REQs, mapping the spec's
REQ-001..REQ-031 1:1). Max existing REQ = REQ-322.
- **Capabilities:** `CAP-033..CAP-038` (mapping the spec's CAP-025..CAP-030).
Existing CAP-025..032 are blockchain/pilot — collision avoided.
- **Invariants:** `INV-12..INV-17` (mapping the spec's INV-63,64,65,18..21,34).
Max existing INV = INV-11.
- **`kj` engine → kyverno-json.** The spec references a `kj` engine; the
repo's actual policy engine is `kyverno-json` (INV-4 swappable). v1.28
uses kyverno-json as the ABAC evaluator for token-vend; no new `kj`
engine is built. This is a CLARIFY-grounded re-mapping, not a silent
assumption (D-229).
### v1.28 Requirements
New requirements REQ-323..REQ-353 — full text in
`.ciagent/REQUIREMENTS.md` §v1.28. Summary by priority:
- **P1 — CLI Substrate (REQ-323..REQ-328):** CodeArtifact wheel + Lambda
layer pipeline; CLI subcommand per `core/` module; `nova init`
scaffolding; `nova cli-action` published to GitHub + Gitea;
`mode_resolver.py` (flag → env → credential type → TTY); audit
emission with `mode` + `selection_reason`.
- **P2 — Lambda Packaging + Identity Layer (REQ-329..REQ-344):** dual-use
`core/lambda/contract_ingestor.py`; local env synthesizer; JWS signing
key from PAT; `nova-idp-auth` Lambda (Argon2id, DynamoDB); DynamoDB
tables (`nova-users`, `nova-sessions`, `nova-password-resets`);
`nova-idp-token-vend` Lambda (KMS-signed OIDC, JWKS endpoint); kyverno-json
ABAC policy at `platform/abac/token-vend.policy`; `nova idp setup`
(`--check/--apply/--verify`); CloudFormation review; PAT issuance +
hashes in DynamoDB; `nova auth login/revoke/status`.
- **P3 — Documentation (REQ-345..REQ-347):** operator guide for
`nova idp setup`; developer guide for `nova auth login`; identity-layer
threat model.
- **P4 — Integration Testing (REQ-348..REQ-351):** E2E sign-up → sign-in →
token-vend → apply → audit; property tests for `mode_resolver`; KMS
round-trip test; PAT revocation SLO test (≤60s P95).
- **P5 — Capability Gate (REQ-352..REQ-353):** CAP-033..038 verification
gates wired into CI.
### v1.28 Hard constraints
- DO NOT depend on Cognito, IAM Identity Center, or any AWS-managed
identity service for sign-up/sign-in/token-vending (NFR-5). Nova-idp
signs OIDC tokens directly via KMS. (Note: no Cognito exists in the
repo today — this is a greenfield build, not a "Cognito drop".)
- DO NOT build a new `kj` engine — use kyverno-json (INV-4).
- DO NOT enforce MFA/TOTP for prod/dr this milestone — ship the code path,
enforce in v1.21+ (deferred, INV scope).
- DO NOT add WebAuthn/FIDO2, upstream IdP federation, or password breach
detection — deferred to v1.23+.
- DO NOT add Lambda layer auto-update on `core/` changes — v1.18 ships
manual `nova layer update`; v1.19 adds CI-triggered auto-update.
- The token-vend Lambda MUST evaluate the kyverno-json ABAC policy before
signing; allow/deny decisions MUST be emitted to the audit stream
(NFR-9, D-227).
- `nova idp setup --apply` MUST present the CloudFormation template for
review before any resource is created (NFR-10).
### v1.28 phase status (complete — tag `v1.27.6` = the v1.28 release)
- **P0** pre-execution → `v1.27.0` (complete).
- **P1..P5** execution phases → `v1.27.1..v1.27.5` (complete).
- **P6** final review + audit + milestone ship → `v1.27.6` = the v1.28
release (complete, merged to main 2026-08-19).
> Phase-by-phase task breakdown, wave ordering, and persona assignments:
> `.ciagent/PLAN.md` (retained). Authoritative resume state:
> `.ciagent/CHECKPOINT.json`.
---
## v1.29 — Reposplit + Identity Layer Bring-Live (complete, tag `v1.28.6`, merged to main 2026-08-20)
> **Feature milestone — complete.** v1.29 extracts all live platform
> components (Nova-idp Lambdas, KMS keys, DynamoDB tables, S3 state
> buckets, OIDC roles, JWKS, audit outbox bootstrap) from `acdl/acdl`
> into a dedicated Gitea-private Terraform repository
> (`nova-platform-ops`), brings Nova-idp live in account `581513795199`
> for the first time (code complete since v1.28, unverified-in-account at
> Phase 0), and standardizes `acdl/acdl` on GitHub. The split enforces
> Vision §4 domain boundaries architecturally: engineering ends at the
> compiled artifact; operations begins at the live platform under
> guardrails. Vision §5 "Narrow capability interfaces" shapes the
> substrate design — `kj` has exactly one identity (one ECR image
> digest), shared by both the production runtime and its defensive
> fallback, eliminating drift by construction (KJ-LOCKSTEP).
### Scope split (CLARIFY-grounded, full autonomy)
The spec creates a **separate** Gitea-private repo `nova-platform-ops`.
CIAgent runs inside `acdl`. The Terraform module code
(`networking`/`kms`/`identity`/`contract-ingest`/`bootstrap`/`edge`) is
authored **out-of-band** in `nova-platform-ops` (operator-owned). CIAgent
in `acdl` delivers only the acdl-side work and tracks the ops-side REQs
as **covered-reference** (verification surface = the M1/M1.5/M2 cutover
gates documented in the operator guide, not a missing test).
| In-acdl (CIAgent authors) | Covered-reference (nova-platform-ops) |
|---|---|
| REQ-354 (publish.yml + ECR image + Release) | REQ-355, 356, 357, 358 (ops CI/HITL/pin) |
| REQ-367 (Gitea scrub) | REQ-359 (Gitea-private repo) |
| REQ-368 (decisions D-232..238) | REQ-360 (IAM scope bounded) |
| REQ-369 (CFN archive + CLI `--apply` TF delegation) | REQ-361 (import idempotency) |
| Operator guide `docs/operator-guide-platform-ops.md` | REQ-362 (KMS key provisioning) |
| `platform/abac/kj-version.txt` | REQ-363, 363b (Lambda/Fargate substrate) |
| M1.5 verification gate tests | REQ-364, 365, 366 (JWKS/WAF/ACM edge) |
| nova-blockchain-exchange deploy.yml @v1.29 bump | REQ-371 `lifecycle.precondition` (TF-side) |
### v1.29 ID allocations (no collisions with shipped history)
- **Requirements:** `REQ-354..REQ-369` + `REQ-371` + `REQ-363b` (note:
REQ-370 is intentionally unused per the source spec). Max existing REQ
= REQ-353. REQ-363b is a sub-requirement of REQ-363 (Fargate defensive
fallback, same ECR image — KJ-LOCKSTEP).
- **Decisions:** `D-232..D-238` (7 decisions, authored in CLARIFY) +
`D-239..D-240` (2 research-derived spec corrections). Max existing D
= D-231.
- **Invariants:** `INV-18` (JWKS-EDGE-ONLY — proposed in spec §5, promoted
here). Plus non-invariant NFRs carried as constraints: KJ-STATIC,
KJ-LOCKSTEP, KJ-WARMUP-HEALTH, OPER-PRIV, IAM-NARROW, DRIFT-DETECT,
IMPORT-IDEMPOTENT, TFM-HITL, JWKS-SLO, JWKS-ROTATION. Max existing INV
= INV-17.
- **`kj` here is the Go binary** (`platform/abac/kj-version.txt`, pinned
v0.0.3), NOT the kyverno-json engine. v1.28 re-mapped the spec's `kj`
engine → kyverno-json (D-227). v1.29 reintroduces `kj` as a **compiled
Go binary** embedded in the ECR container image — a distinct artifact.
No collision: kyverno-json remains the policy engine (INV-4); `kj` is a
static binary invoked via `subprocess` by the Lambda handler.
### v1.29 Requirements
New requirements REQ-354..REQ-369 + REQ-371 + REQ-363b — full text in
`.ciagent/REQUIREMENTS.md` §v1.29. Summary by phase:
- **P1 — Publish Pipeline (REQ-354):** `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 with SHA-256.
- **P2 — Gitea Scrub + Decisions (REQ-367, REQ-368):** remove `.gitea/`,
scrub all Gitea refs, `forge_parity_disabled` CI assertion, pyproject
→ 1.29.0, record D-232..238.
- **P3 — CFN Archive + TF Delegation (REQ-369):** archive CFN template →
`docs/archive/nova-idp-cfn-v1.28.md`, `nova idp setup --apply` delegates
to `terraform apply`.
- **P4 — Operator Guide + Reference Tracking:** `docs/operator-guide-
platform-ops.md`, ARCHITECTURE.md §12.9, STATE.md v1.29 CAPs +
invariants; REQUIREMENTS.md covered-reference markers.
- **P5 — Consumer Deploy Bump (cross-project, Edge 8):** `nova-
blockchain-exchange` deploy.yml `@v1.25` → `@v1.29` + smoke test.
- **P6 — Final Review + Audit + Milestone Ship.**
### v1.29 Hard constraints
- DO NOT activate pilot qa/prod/dr environments (D-208/D-209 — separate
initiative). M1 brings Nova-idp live; env activation is out.
- DO NOT add S3 Object Lock / JWS tamper-resistance (D-083). Tamper-
evidence via SQLite hash-chain remains.
- DO NOT restore 73.8% coverage — separate NFR milestone; YELLOW carried
without scope expansion.
- DO NOT provision CodeArtifact — direct GitHub Releases artifact fetch.
- DO NOT delete the CFN template in `acdl/acdl` at v1.29.0 — archive as
read-only reference (`docs/archive/nova-idp-cfn-v1.28.md`); deletion is
a follow-up after Terraform parity is verified.
- DO NOT add Nova-idp feature work (new OIDC claims, new ABAC rules) —
bring live; don't extend.
- DO NOT add MFA/TOTP, WebAuthn, upstream IdP federation (Vision §7).
- DO NOT add a CloudFront Frontend (L3B consumer surface) — pure ops
focus only.
- The `kj` binary MUST be compiled `CGO_ENABLED=0` and verified statically
linked (`file(1)`) before embedding (KJ-STATIC).
- The ECR image digest on the Fargate standby MUST equal the Lambda
`image_uri` digest at every `terraform plan` (KJ-LOCKSTEP, REQ-371 —
fail-closed by `lifecycle.precondition` mechanism, not by discipline).
- The JWKS endpoint is the ONLY public read surface; all other platform
endpoints gate with `AuthType: AWS_IAM` (JWKS-EDGE-ONLY, INV-18).
- Any `terraform apply` against `main` in `nova-platform-ops` MUST require
a Gitea Actions approval from a user distinct from the PR author
(TFM-HITL, INV-3 applied at platform level).
- `nova-platform-ops` MUST be `private: true` in Gitea, not mirrored
(OPER-PRIV).
### v1.29 phase status (complete — tag `v1.28.6` = the v1.29 release)
- **P0** pre-execution → `v1.28.0` (complete).
- **P1..P5** execution phases → `v1.28.1..v1.28.5` (complete).
- **P6** final review + audit + milestone ship → `v1.28.6` = the v1.29
release (complete, merged to main 2026-08-20).
> Tags run on the **v1.28.x** line: `v1.28.0` (P0) →
> `v1.28.1..v1.28.5` (execution phases) → `v1.28.6` (final phase =
> milestone release). Milestone branch:
> `milestone/v1.29-reposplit-identity`. Phase-by-phase task breakdown,
> wave ordering, and persona assignments will live in `.ciagent/PLAN.md`.
> Authoritative resume state: `.ciagent/CHECKPOINT.json`.
## v1.30 — Single-shot Leadership Deck (active, presented August 2026)
> **Feature milestone — single-shot PPTX leadership deck.** A
> hand-authored Marp markdown deck rendered via the existing
> `scripts/render_pptx.py` pipeline, presented live to Infrastructure
> & Operations leadership (CTO + VP Technology + Product Management)
> in August 2026, securing architecture endorsement and a November
> 2026 runway to demonstrate Nova's next milestone. The deck is a
> **discrete artifact** (D-241: NOT a compression of the existing
> citizen-developer pitch `nova-autonomous-cloud-delivery-marp.md`,
> which remains untouched).
>
> Tags run on the **v1.29.x** line: `v1.29.1` (P0) → `v1.29.2` (P1
> execution) → `v1.29.3` (P2 final review = milestone release).
> Milestone branch: `milestone/v1.30-leadership-deck`. Single execution
> phase (P1) — this is a single-shot artifact, not a multi-phase
> build. Authoritative resume state: `.ciagent/CHECKPOINT.json`.
>
> **Source spec:** REQ-372 v1.0 (locked 2026-08-20). Full requirement
> text in `.ciagent/REQUIREMENTS.md` §v1.30. Slide Content Map (the
> source-of-truth for REQ-372.7 content traceability) is reproduced
> verbatim below from the locked spec.
### v1.30 ID allocations (no collisions with shipped history)
- **Decisions:** `D-241..D-246` (6 decisions, authored in CLARIFY
across P0 + P3). Max existing D = D-240 (v1.29). Next free: D-247.
- **D-241** — Leadership deck is a single-shot, discrete, hand-
authored artifact (NOT a compression of the existing citizen-
developer pitch `nova-autonomous-cloud-delivery-marp.md`).
Audience: Infrastructure & Operations leadership (CTO + VP
Technology + Product Management). August 2026 presentation
anchor + November 2026 runway anchor. The existing citizen-
developer deck remains untouched. Overrides the post-v1.29
STATE.md intake assumption 3.
- **D-242** — Narrow `scripts/render_pptx.py` extension: accept an
explicit source `.md` path + `--output` filename; render a
right-aligned footer textbox on every slide (python-pptx does
not read the Marp `footer:` directive). Non-REQ-372 prerequisite
per spec §3.3 Edge 2.
- **D-243** — Date anchor discipline: August 2026 is a month-only
presentation anchor (no specific day); November 2026 is the
runway anchor (~90 days).
- **D-244** — Slide 1 becomes a black-background cover (overrides
grill G-1). Visual treatment only; on-slide text unchanged.
- **D-245** — Slide 7 "What works now" content addition. New
on-slide block before the ask. Records: 13 L1 + 2 L2 live
(confidence 0.800), next-steps arc (greenfield pilots → promote
to prod → SPGE constitutional library → infra layer). "sandbox"
is colloquial for the dev AWS account `581513795199`; "SPGE
constitutional library" is external/unverified-in-repo.
- **D-246** — 7 S&P-themed mermaid diagrams, one per slide,
rendered via local mmdc with vendored Chromium libs (24 Debian
bookworm .deb packages fetched, `.so` files extracted). `.mmd`
sources + PNGs committed for reproducibility.
- **Capabilities:** `CAP-042` (1 capability, appended at ship).
Max existing CAP = CAP-041 (v1.29). Next free: CAP-043.
### v1.30 Scope (CLARIFY-grounded, full autonomy)
- **In scope:** one hand-authored Marp source
(`docs/presentations/nova-leadership-deck-marp.md`, ≤7 slides); one
rendered PPTX (`docs/presentations/nova-leadership-deck.pptx` via
the existing `scripts/render_pptx.py`, narrowly extended per
D-242); speaker notes per slide meeting the depth discipline
(REQ-372.4); footer `Nova Platform - Infrastructure & Operations`
on all 7 slides (REQ-372.5); S&P theme tokens only (REQ-372.6);
related-artifacts header comment (REQ-372.9); smoke test
`scripts/check_leadership_deck.sh` runnable on demand, NOT a CI
gate (REQ-372.8); vision `[1]` grounding citations in slides 3/5/7
speaker notes (REQ-372.12); CAP-042 in STATE.md (REQ-372.10);
D-241 record in PROJECT.md (REQ-372.11).
- **Out of scope (explicit exclusions):** compression/modification of
the existing citizen-developer deck; per-milestone refresh / auto-
regeneration; Marp HTML as a primary deliverable; multi-audience
variants; `publish.yml` integration; live AWS cutover of covered-
reference REQs; coverage floor restoration; S3 Object Lock
provisioning; roadmap authoring (PLAN.md remains source of record);
new CI plumbing.
### v1.30 Requirements
Full text in `.ciagent/REQUIREMENTS.md` §v1.30. Summary:
- **REQ-372.1** — Source markdown exists and is parseable (7 slides,
header comment).
- **REQ-372.2** — PPTX render via existing pipeline (7 slides, no
python-pptx exceptions).
- **REQ-372.3** — Slide count is exactly 7.
- **REQ-372.4** — Speaker notes depth per slide (word bands: 1/2/4/6
150300; 3/5 250400; 7 200300).
- **REQ-372.5** — Footer `Nova Platform - Infrastructure & Operations`
on every slide (right-aligned).
- **REQ-372.6** — Only S&P theme tokens `#D6002A`, `#1B1B1B`,
`#FFFFFF`, `#F0F0F0`.
- **REQ-372.7** — Slide-by-slide content matches the Slide Content Map
(visual review).
- **REQ-372.8** — Smoke test `scripts/check_leadership_deck.sh` exits
0 on pass (asserts af). Runnable on demand; NOT a CI gate.
- **REQ-372.9** — Related-artifacts comment in source header.
- **REQ-372.10** — CAP-042 appended to STATE.md at ship.
- **REQ-372.11** — D-241 recorded in PROJECT.md at ship.
- **REQ-372.12** — Vision `[1]` citations in slides 3, 5, 7 speaker
notes (ground to `docs/vision.md`).
### v1.30 Hard constraints
- **DO NOT modify** `docs/presentations/nova-autonomous-cloud-delivery-marp.md`
(the citizen-developer pitch). Per D-241, the two decks remain
discrete artifacts.
- **DO NOT add `publish.yml` integration** for this deck. Not tagged
or released via the existing pipeline.
- **DO NOT wire `scripts/check_leadership_deck.sh` as a CI gate.**
Runnable on demand. Single-shot artifact.
- **DO NOT extend the deck beyond 7 slides.** Slide count bound by
REQ-372.3.
- **DO NOT auto-derive future leadership decks** from STATE.md /
NORTH_STAR.md. Every leadership artifact is hand-authored.
- **DO NOT compress the deck for a sub-audience.** Multi-audience
variants are out of scope.
- **DO NOT introduce hex colors** outside the 4 S&P theme tokens.
### v1.30 Authoring conventions
- **Marp frontmatter:** `marp: true; theme: default; footer: "Nova
Platform - Infrastructure & Operations"; paginate: false; size: 16:9`
- **Theme tokens (only colors in source):** `#D6002A`, `#1B1B1B`,
`#FFFFFF`, `#F0F0F0`
- **Slide separator:** `---` on its own line
- **Speaker notes:** HTML comments `<!-- ... -->` within the slide
body, before the next `---`
- **Footer:** exact string `Nova Platform - Infrastructure &
Operations` via the Marp `footer:` directive (and rendered as a
right-aligned textbox per D-242, since python-pptx does not read
the Marp footer directive)
- **Per-slide word-count bands:** slides 1/2/4/6 in 150300; slides
3/5 in 250400; slide 7 in 200300
- **Vision grounding:** slides 3, 5, 7 speaker notes must contain at
least one `[1]` citation grounding to the principles, anti-goals,
or tenets in `docs/vision.md`
### v1.30 Render pipeline (existing — narrowly extended per D-242)
```bash
python3 scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md \
--output docs/presentations/nova-leadership-deck.pptx
```
The existing `scripts/render_pptx.py` is extended to accept an
explicit source `.md` path + `--output` filename (D-242). The source
is authored as `nova-leadership-deck-marp.md` to fit the existing
`-marp.md` pipeline convention; the output is
`nova-leadership-deck.pptx` per spec REQ-372.2. The renderer is also
extended to add a right-aligned footer textbox on every slide (the
python-pptx path does not read the Marp `footer:` directive).
### v1.30 phase status (live — tag `v1.29.3` = the v1.30 release)
- **P0** pre-execution → `v1.29.1` (in progress).
- **P1** execution (author + render + smoke test) → `v1.29.2`.
- **P2** final review + audit + milestone ship → `v1.29.3` = the
v1.30 release.
### v1.30 Slide Content Map (REQ-372.7 traceability reference)
The PPTX content is fully specified by the slide drafts below. Each
slide carries an exact on-slide body + speaker notes fingerprint.
Smoke test does not assert content strings verbatim (brittle); audit
verifies by visual review against this map. Any drift requires
`CLARIFY`.
#### Slide 1 — The frictions Nova absorbs
**On-slide body:**
> **The friction every delivery team lives today**
>
> *Velocity is up; the coordination surface around each change is up
> faster.*
>
> → Infrastructure is authored by people who don't specialize in
> infrastructure.
> → Every change is gated because one misconfiguration can expose the
> entire estate.
> → Compliance, security, and NFRs are checked late — fueling
> remediation cycles that erode delivery cadence and team morale.
>
> *Nova absorbs all three — owned building blocks, separation of
> concerns, attested compliance up front.*
**Speaker notes (~270 words):** Three-pattern problem frame grounded
in the binding-constraint claim [1]. Closing distinguishes
**infrastructure patching (Nova's lane)** from **AppSec (application
team's lane)** — Nova is not a remediation tool, not a security
blanket.
#### Slide 2 — Nova in one frame
**On-slide body:**
> **Nova in one frame**
>
> *You already recognize this pattern.*
>
> Every Central IT team curates a golden image for Windows, for Linux,
> for macOS. They own it. They patch it. They ship it. Consumers
> consume it without thinking about what's inside.
>
> Nova plays the same role one layer up — for everything that runs
> your cloud. S3 buckets with SSE-KMS posture. RDS instances with
> deletion protection and PITR. Lambda containers with static ABAC
> binaries. ALBs, ECS services, KMS keys, DynamoDB tables. Each one
> is owned by the platform team, patched by the platform team,
> attested by the platform team, and consumed by anyone who declares a
> contract.
>
> The difference: every primitive is versioned, tested across its
> entire lifecycle, and bounded by policy before any consumer ever
> touches it.
>
> *Nova's lane is the infrastructure beneath the application. AppSec,
> dependency review, and runtime application security stay where they
> have always been — with the application team.*
**Speaker notes (~210 words):** Trade-off pattern (Central IT vs.
Nova both trade per-application control for uniform operability);
platform-begins/ends framing [1]; sovereignty-via-boundary argument.
#### Slide 3 — Two principles that organize everything else
**On-slide body:**
> **Two principles that organize everything else**
>
> *The architecture is principled, not improvised. Two tenets
> discipline every other decision.*
>
> **Sovereign boundary.** Nova governs the delivery lifecycle; it
> does not reach upstream into product or software development [1].
> Integration with SDLC and PDLC partners happens exclusively through
> the validated, published contract surface. What lives outside the
> contract is not Nova's domain.
>
> **Lower autonomous · higher attested.** Lower environments proceed
> through agentic automation. Promotion to higher environments
> requires deliberate human attestation — not as a rubber stamp, but
> as policy-mandated accountability [1]. The compute the platform
> makes; the choice the human keeps.
>
> *Everything else in the architecture inherits from these two.*
**Speaker notes (~270 words):** Cross-tenet architecture discipline
argument — how the four-layer model, HITL gates, policy envelope, and
contract schema all inherit from the two tenets [1]. Closes with "The
next slide is what the line looks like in 18 months of milestones."
#### Slide 4 — Live · Attested · Stays human
**On-slide body:**
> **Live today**
> 41 capabilities across 12 domains. Contract ingestor, audit
> outbox, state buckets, and the live pilot run have been operating
> in our AWS estate since v1.7; pilot evidence at v1.26 returned
> confidence 0.800. DORA + adoption + policy-conformance metrics
> flow to PowerBI from the same audit stream as the lineage. Every
> finding carries one owner, one patch state, one audit entry — one
> pane, no second source of truth. A POC is production-grade by
> construction: there is no "POC that became prod" surprise.
>
> **Attested on promotion**
> qa, prod, and dr require a named human approver distinct from the
> PR author. Rubber stamps cannot be silently issued.
>
> **Stays human — by design**
> Confidence below the autonomy threshold at qa, prod, or dr triggers
> human escalation [1]. Some categories of decision are preserved for
> human judgment, and the platform says so out loud.
**Speaker notes (~230 words):** Three-column claim disambiguation
(real / observable / disciplined). Pilot evidence as record, not
forecast. Single-pane-of-glass via audit lineage [1]. POC-to-prod
discipline [1]. HITL discipline closing [1].
#### Slide 5 — The boundary keeps us honest
**On-slide body:**
> **The boundary keeps us honest**
>
> *Nova stays where it belongs.*
>
> **In Nova's lane**
> → Infrastructure primitives: S3, RDS, Lambda, ECS, DynamoDB, KMS,
> CloudFront.
> → Operational guardrails: confidence, policy, attestation, audit
> lineage.
> → CVE response at the infrastructure layer.
>
> **Outside Nova's lane**
> → Application business logic.
> → IDE, sprint, author workflows [1].
> → Application-layer security: AppSec, dependency review, runtime
> threat modeling.
> → VM, bare-metal, OS lifecycles [1].
>
> *The line is the contract. Everything below the contract is Nova.
> Everything above it stays where it has always been.*
**Speaker notes (~250 words):** Architecture boundary discipline.
AppSec stays with app team as autonomy-preserving design choice.
Boundary as operating principle, not defensive posture [1].
#### Slide 6 — The 18-month shape
**On-slide body:**
> **The 18-month shape**
>
> *Where CDLC meets SDLC + PDLC — through the contract surface, not
> above it.*
>
> **α (now → Q4'26) — Operating model + federated governance.** A
> named platform-ops body owns the platform; SLAs on every L2 are
> ratifiable by platform + consumer. The operating model is
> published; integration surfaces for SDLC and PDLC harnesses are
> documented at the contract boundary.
>
> **β (Q1'27) — Auto-published infra observability.** Every consumer
> stack ships with CloudWatch dashboards, uptime-kuma monitors, and
> alert routing on apply — infrastructure primitives publish
> observability as a property, no per-team authoring required.
>
> **γ (Q2'27) — Runbook generation from telemetry.** Every L1
> primitive ships with an auto-generated incident runbook derived
> from observed patterns. SREs get a starting runbook, not a blank
> page.
>
> **δ (Q3'27 → Q4'27) — Audit ledger, tamper-resistant + externally
> addressable.** The SQLite hash-evidence stream migrates to S3
> Object Lock + JWS signatures. External counsel verifies any
> production change back to a named human attestation.
>
> *Nova absorbs no IDE, no editor, no sprint tool, no agent harness.*
**Speaker notes (~250 words):** Boundary-respecting integration
argument. α as unlock + governance discipline [1]. β's infra-vs-app
observability discipline [1]. γ's infra-vs-app runbook discipline
[1]. δ as audit lineage outward, not upstream [1].
#### Slide 7 — What we ask · What comes back
**On-slide body:**
> **What we ask · What comes back**
>
> **What we ask.**
> Architecture endorsement. Runway to the next milestone.
>
> **Why now.**
> Agentic SDLC is reshaping the delivery curve. What is barely
> keepable today — incident response, compliance reconciliation,
> security remediation — does not compress at the same rate as the
> velocity it has to keep pace with. By the end of 2027, the gap
> between delivery acceleration and operational absorption is the
> structural risk.
>
> **What comes back.**
> The infrastructure foundation that absorbs the velocity. Metrics
> that tell us where to push next. Audit lineage that closes the
> regulatory question. The next milestone, **by November 2026**.
>
> *What we do not ask for: an IDE, a sprint tool, an author workflow,
> an upstream pipeline. Nova stays in its lane [1].*
**Speaker notes (~256 words):** Opens with "This is presented to
Infrastructure & Operations leadership in August 2026." Asks for
architecture endorsement and runway to next milestone by November
2026. Velocity framing with **60% goal as internal directional
target, not sourced claim**. Closes with "Use the runway to land the
architecture endorsement."
## v1.31 — Leadership Deck Polish II (active milestone)
> **NFR/refinement milestone — polish pass on the v1.30 leadership
> deck.** A single refinement phase that enriches the visible on-slide
> prose, improves the slide layout, and re-renders the PPTX — without
> altering the S&P visual theme, the 7-slide structure, the speaker-note
> word-count bands, the vision `[1]` grounding, or the diagram PNGs.
> The v1.30 deck shipped with sparse visible wording (slides 26
> averaged 4367 visible words, leaning on diagrams); v1.31 makes each
> slide stand on its own as a readable artifact while a presenter
> speaks, so the deck reads cleanly both live and as a leave-behind.
>
> This is a **refinement-only** milestone (no new features, no new
> slides, no theme change, no schema change). Tags run on the
> **v1.30.x** line (previous minor): `v1.30.0` (P0) → `v1.30.1` (P1
> execution = milestone release). Milestone branch:
> `milestone/v1.31-leadership-deck-polish`. Authoritative resume
> state: `.ciagent/CHECKPOINT.json`.
>
> **Source spec:** REQ-373 v1.0 (locked 2026-08-20). Full requirement
> text in `.ciagent/REQUIREMENTS.md` §v1.31.
### v1.31 ID allocations (no collisions with shipped history)
- **Decisions:** `D-247` (1 decision, authored in CLARIFY).
- **D-247** — Polish is **refinement-only and theme-preserving**.
The S&P theme tokens (`#D6002A`, `#1B1B1B`, `#FFFFFF`, `#F0F0F0`),
the 7-slide count, the per-slide speaker-note word-count bands
(1/2/4/6: 150300; 3/5: 250400; 7: 200300), the vision `[1]`
citations on slides 3/5/7, the 7 mermaid diagram PNGs, and the
footer string are all **invariants** of this milestone — they must
not change. Polish is confined to: (a) denser, better-structured
visible body prose on each slide, (b) layout improvement within
the existing `render_pptx.py` block vocabulary (lead/quote/plain/
bullet/ordered/image/table/benefit), and (c) re-rendering the
PPTX. No new slides, no new diagrams, no theme tokens, no new
speaker-note bands. The citizen-developer deck
(`nova-autonomous-cloud-delivery-marp.md`) remains untouched
(D-241 still holds).
- **Capabilities:** none new (CAP-042 is refined in place; no new CAP
allocated). Next free CAP remains CAP-043.
### v1.31 Scope (CLARIFY-grounded, full autonomy)
Refinement of `docs/presentations/nova-leadership-deck-marp.md`:
- Enrich the **visible on-slide prose** on every slide so the deck
reads as a standalone artifact (current slides 26 average 4367
visible words; target a denser, well-structured body that does not
crowd the diagram or overflow the slide).
- Improve **slide layout** using the existing renderer's block
vocabulary — re-balance the order of lead/quote/plain/benefit
blocks so each slide has a clear title, a framing line, the body,
the diagram, and a closing italic benefit line where present.
- Preserve all invariants (D-247): theme tokens, slide count = 7,
speaker-note bands, `[1]` citations on 3/5/7, the 7 diagram PNGs,
the footer string.
- Re-render `docs/presentations/nova-leadership-deck.pptx` via
`scripts/render_pptx.py`; the smoke test
`scripts/check_leadership_deck.sh` must still exit 0.
### v1.31 Hard constraints (invariants — D-247)
- Slide count stays exactly 7.
- S&P theme tokens are the only colors.
- Speaker-note word counts stay in band per REQ-372.4 bands.
- `[1]` citations remain present in slides 3, 5, 7 speaker notes.
- The 7 diagram PNGs (`assets/png/leadership-slide-N.png`) are reused
unchanged; `.mmd` sources are not modified.
- Footer string `Nova Platform - Infrastructure & Operations` unchanged.
- `scripts/render_pptx.py` is not modified (no new renderer features
needed — polish uses the existing block vocabulary).
- The citizen-developer deck is not touched.
### v1.31 Requirements
Full text in `.ciagent/REQUIREMENTS.md` §v1.31. Summary:
- **REQ-373.1** — Visible prose density: every slide's on-slide body
(excluding speaker notes + images) is enriched to a richer, well-
structured wording that reads as a standalone artifact.
- **REQ-373.2** — Layout improvement: each slide uses the renderer's
block vocabulary to balance title → frame → body → diagram → benefit.
- **REQ-373.3** — Invariants preserved (D-247): theme, 7 slides,
note bands, `[1]` citations, diagram PNGs, footer.
- **REQ-373.4** — PPTX re-rendered; smoke test exits 0.
### v1.31 phase status (live — tag `v1.30.2` = the milestone release)
| Phase | Status | Tag |
|-------|--------|-----|
| P0 pre-execution | complete | v1.30.1 |
| P1 polish | complete | v1.30.2 (milestone release) |
+819 -1
View File
@@ -299,4 +299,822 @@
Full v1.26 requirement text:
`.ciagent/nova-blockchain-exchange/REQUIREMENTS.md`. Active phase plan:
`.ciagent/PLAN.md`.
`.ciagent/PLAN.md`.
## v1.28 — CLI Canonicalization + Identity Layer (complete, tag `v1.27.6`, merged to main 2026-08-19)
> **Feature milestone — complete.** The Nova CLI is installable from
> internal PyPI (CodeArtifact); every `core/` module is reachable as a
> `nova <subcommand>`; the CLI and Lambda functions share a single
> `core/` source tree; and Nova owns its identity layer end-to-end
> (Nova-idp: `nova-idp-auth` + `nova-idp-token-vend` Lambdas, KMS-signed
> OIDC tokens, kyverno-json ABAC token vending, PAT lifecycle). No
> AWS-managed identity services in the path.
>
> Tags run on the **v1.27.x** line: `v1.27.0` (P0) →
> `v1.27.1..v1.27.N` → `v1.27.(N+1)` (final = milestone release).
> Milestone branch: `milestone/v1.28-cli-identity`.
>
> **ID re-mapping (no collisions):** the source spec used `REQ-001..031`,
> `CAP-025..030`, `INV-63/64/65/18..21/34`, `D-NEW-26/37..41`, and a `kj`
> engine — none of which exist in this repo (CAP-025..032 and
> INV-1..11 are already allocated to blockchain/pilot work; the policy
> engine is kyverno-json, not `kj`). This file uses the re-mapped IDs:
> `REQ-323..353`, `CAP-033..038`, `INV-12..17`, `D-226..231`. The 1:1
> mapping is recorded in CLARIFY.md. Decisions D-226..D-231 are authored
> in CLARIFY (full autonomy) — they are not pre-existing "locked inputs".
### Decisions (locked in CLARIFY — full autonomy, load-bearing for v1.28)
- **D-226 (Mode resolution priority):** flag → env (`NOVA_CLIENT_MODE`) →
credential type → TTY heuristic. Invalid env values are ignored + warned,
falling through to credential type. No silent fallbacks (NFR-1).
- **D-227 (ABAC engine = kyverno-json):** the token-vend Lambda uses the
existing kyverno-json engine (INV-4 swappable) as the ABAC evaluator,
not a new `kj` engine. Policy at `platform/abac/token-vend.policy`.
- **D-228 (Argon2id in Lambda):** `argon2-cffi` with bundled wheels; if
the C extension fails to load, fall back to the pure-Python
implementation; if both fail, document the Fargate migration path.
- **D-229 (PAT revocation SLO):** strongly-consistent DynamoDB read on
every token-vend request; revocation takes effect within 60s P95 (NFR-4).
- **D-230 (JWKS endpoint):** Lambda function URL behind a custom domain;
rate limiting at the DNS/CDN layer. API Gateway migration deferred to
v1.19+ if throttling requirements grow.
- **D-231 (ABAC policy ownership + versioning):** Platform Security owns
`platform/abac/token-vend.policy`; changes require PR review; the
policy version (git SHA) is recorded in every token-vend audit event.
### P1 — CLI Substrate
#### REQ-323 — CodeArtifact wheel + Lambda layer pipeline
**Journeys:** J3. **Priority:** High.
**AC:** Given a merge to `main` affecting `core/`, when CI runs, then both
the wheel and the Lambda layer are published to CodeArtifact with
identical version strings; if either fails, the merge is rejected.
#### REQ-324 — CLI subcommand per `core/` module
**Journeys:** J3. **Priority:** High.
**AC:** (1) Every module in `core/` has a corresponding `nova/<module>.py`
subcommand. (2) Subcommand files are ≤ 50 lines and contain no business
logic — they delegate to `core/`. (3) CAP-034 verifies delegation by AST
scan.
#### REQ-325 — `nova init` scaffolds project
**Journeys:** J2. **Priority:** High.
**AC:** Given a directory with no `.nova/`, when Dev runs `nova init`,
then `.nova/`, `.nova/contract.yml.attestations/`, and `.gitignore`
(excluding secrets) are created.
#### REQ-326 — `nova cli-action` published
**Journeys:** J3. **Priority:** High.
**AC:** (1) Action is available on both GitHub and Gitea marketplaces.
(2) Integration test verifies byte-identical behavior on both platforms.
(3) Python 3.12 is pinned.
#### REQ-327 — `mode_resolver.py` priority
**Journeys:** J2, J3. **Priority:** High.
**AC:** (1) Explicit `--mode=agent|interactive` flag always wins.
(2) Otherwise `NOVA_CLIENT_MODE` env var. (3) Otherwise credential type
default. (4) Otherwise TTY heuristic. (5) Property tests cover all four
levels. (6) INV-13 (mode determinism) enforced at PR time.
#### REQ-328 — Audit emission with mode + selection_reason
**Journeys:** J3. **Priority:** High.
**AC:** Given any CLI invocation, when the CLI runs, then the emitted
`cli.invocation` audit event contains `mode`, `selection_reason`,
`credential_type`, `command`, and `args`. INV-12 (mode observability)
enforced.
### P2 — Lambda Packaging + Identity Layer
#### REQ-329 — Dual-use Lambda/CLI import
**Journeys:** J2. **Priority:** High.
**AC:** Given `core/lambda/contract_ingestor.py`, when imported from the
Lambda handler, then it executes the Lambda path; when imported from the
CLI, then it executes the local path; and the two paths share ≥ 80% of
their code.
#### REQ-330 — Local env synthesizer
**Journeys:** J2. **Priority:** High.
**AC:** Given a contract and a `--local` flag, when `nova apply --local`
runs, then a local env is synthesized via `core/env.py:get_env()` without
provisioning cloud resources.
#### REQ-331 — Attestations directory scaffolded
**Journeys:** J2. **Priority:** High.
**AC:** Given `nova init` ran, when Dev lists
`.nova/contract.yml.attestations/`, then the directory exists and is empty.
#### REQ-332 — JWS signing key from PAT
**Journeys:** J2. **Priority:** High.
**AC:** Given a PAT, when Dev runs `nova apply --local --sign-local-review`,
then a JWS attestation is produced; the JWS is HMAC-SHA256 with a key
derived from the PAT via `HKDF-SHA256(PAT_bytes, salt='nova-local-attestation',
info='jws-signing-key')` → 32-byte symmetric key (C-5.2 grill fix). The
verification key is derived from the PAT via the same KDF (the PAT is
the shared secret). INV-14..17 (attestation invariants) enforced.
#### REQ-333 — `nova-idp-auth` Lambda
**Journeys:** J1, J2. **Priority:** High.
**AC:** (1) Lambda exposes sign-up, sign-in, and session creation
endpoints. (2) Passwords are hashed with Argon2id. (3) Sessions are
stored in DynamoDB. (4) CAP-036 verifies end-to-end auth flow.
#### REQ-334 — Argon2id password hashing
**Journeys:** J1, J2. **Priority:** High.
**AC:** Given a sign-up request, when the user record is persisted, then
the password is stored as an Argon2id hash; raw passwords never appear in
logs, traces, environment variables, or DynamoDB records.
#### REQ-335 — DynamoDB tables for identity
**Journeys:** J1. **Priority:** High.
**AC:** (1) Tables exist: `nova-users`, `nova-sessions`,
`nova-password-resets`. (2) Tables are provisioned by `nova idp setup`.
(3) Point-in-time recovery is enabled on each.
#### REQ-336 — `nova-idp-token-vend` Lambda
**Journeys:** J1, J2, J4. **Priority:** High.
**AC:** (1) Lambda accepts a PAT (or session token) and returns a
KMS-signed OIDC token. (2) Token claims include `sub`, `aud`, `iss`,
`exp`, and role claims. (3) ABAC policy is evaluated before signing.
#### REQ-337 — KMS-signed OIDC tokens
**Journeys:** J1, J4. **Priority:** High.
**AC:** (1) Signing key is a KMS asymmetric key (RSA or ECDSA).
(2) Token signature is verifiable via the JWKS endpoint. (3) KMS
round-trip test passes. CAP-037 verifies.
#### REQ-338 — JWKS endpoint as Lambda function URL
**Journeys:** J1, J4. **Priority:** High.
**AC:** Given the identity stack is deployed, when a client GETs the JWKS
URL, then the public key(s) for token verification are returned with
`Content-Type: application/json`.
#### REQ-339 — kyverno-json ABAC policy file
**Journeys:** J1, J4. **Priority:** High.
**AC:** (1) Policy at `platform/abac/token-vend.policy`. (2) Policy inputs
include subject, requested claims, target resource, and environment.
(3) kyverno-json `evaluate` returns allow/deny; the decision is emitted to
the audit stream.
#### REQ-340 — `nova idp setup` walks admin
**Journeys:** J1. **Priority:** High.
**AC:** (1) Command supports `--check`, `--apply`, and `--verify` modes.
(2) `--check` reports missing prerequisites and the required IAM policy.
(3) `--apply` generates a CloudFormation template and requires explicit
approval. (4) `--verify` runs the KMS round-trip test.
#### REQ-341 — CloudFormation template for review
**Journeys:** J1. **Priority:** High.
**AC:** Given `nova idp setup --apply`, when the template is generated,
then the template is presented for review; resources are not created until
the operator approves; `--dry-run` shows the resource list without writing.
#### REQ-342 — PAT issuance via portal
**Journeys:** J4. **Priority:** High.
**AC:** (1) PAT is a signed JWT. (2) PAT hash is stored in DynamoDB.
(3) PAT includes a unique `jti` and an expiry claim. (4) Revocation marks
the `jti` as revoked.
#### REQ-343 — PAT hashes in DynamoDB
**Journeys:** J4. **Priority:** High.
**AC:** (1) Only the hash (not the raw PAT) is stored. (2) Table supports
lookup-by-hash and lookup-by-`jti`. (3) Revoked PATs are retained for
audit, not deleted.
#### REQ-344 — `nova auth` commands
**Journeys:** J2, J4. **Priority:** High.
**AC:** (1) `nova auth login` exchanges session → OIDC token, stores
locally. (2) `nova auth revoke --pat <id>` marks a PAT revoked.
(3) `nova auth status` shows current credential, mode, and
selection_reason. (4) All commands emit audit events.
### P3 — Documentation
#### REQ-345 — Operator guide for `nova idp setup`
**Priority:** High.
**AC:** Guide published covering `--check`, `--apply`, `--verify`,
prerequisite IAM policy, and the CloudFormation review flow.
#### REQ-346 — Developer guide for `nova auth login`
**Priority:** High.
**AC:** Guide published covering signup, signin, login, mode resolution,
and credential-type behavior at a TTY vs. piped stdout.
#### REQ-347 — Identity-layer threat model
**Priority:** High.
**AC:** Threat model published covering Argon2id storage, KMS signing,
JWKS exposure, PAT revocation SLO, ABAC token vending, and the no-AWS-
managed-identity constraint (NFR-5).
### P4 — Integration Testing
#### REQ-348 — E2E integration test
**Priority:** High.
**AC:** Given a deployed Nova-idp, when the test runs, then sign-up →
sign-in → token-vend → apply → audit completes successfully; the audit
event chain is verifiable.
#### REQ-349 — Property tests for `mode_resolver`
**Priority:** High.
**AC:** (1) Property tests cover all four priority levels. (2) Edge cases:
TTY but piped stdout, missing credential, conflicting flag/env, invalid
env value. (3) INV-13 enforced via test.
#### REQ-350 — KMS round-trip test
**Priority:** High.
**AC:** Given a token signed by the token-vend Lambda, when the test
fetches the JWKS and verifies the signature, then verification succeeds.
#### REQ-351 — PAT revocation SLO test
**Priority:** High.
**AC:** Issue PAT → use to vend token → revoke → assert denial within 60s
P95. Test passes in CI.
### P5 — Capability Gate
#### REQ-352 — CAP-033..038 gate rules wired into CI
**Priority:** High.
**AC:** (1) CAP-033 (CLI subcommand surface exists): `nova --help` lists a
subcommand for every `core/` module. (2) CAP-034 (subcommand delegates to
`core/`): every `nova/<module>.py` ≤ 50 lines, no business logic, AST
scan. (3) CAP-035 (layer matches wheel): Lambda layer ARN version matches
the `nova-cli` wheel version. (4) CAP-036 (Nova-idp auth flow works): E2E
test (REQ-348) passes. (5) CAP-037 (token-vend signs via KMS): KMS
round-trip (REQ-350) passes. (6) CAP-038 (PAT issuance + revocation):
REQ-351 passes. Failure of any → merge blocked.
#### REQ-353 — Capability gate GREEN for v1.28 release
**Priority:** High.
**AC:** CAP-001..CAP-032 remain Verified; CAP-033..CAP-038 are Verified.
All v1.28 release-gate criteria in PLAN.md §6 met.
### v1.28 Invariants (new — INV-12..INV-17)
- **INV-12 (Mode observability):** Every CLI invocation emits a
`cli.invocation` audit event containing `mode`, `selection_reason`,
`credential_type`, `command`, and `args`.
- **INV-13 (Mode resolution determinism):** Resolution priority is
flag → env (`NOVA_CLIENT_MODE`) → credential type → TTY. No silent
fallbacks. Deviations rejected at PR time.
- **INV-14 (Credential type encodes role):** `developer_pat` /
`nova_oidc_token` + TTY present → `interactive`; TTY absent → `agent`.
- **INV-15 (No AWS-managed identity in path):** Nova-idp MUST NOT depend
on Cognito, IAM Identity Center, or any AWS-managed identity service.
- **INV-16 (Password storage):** Passwords hashed with Argon2id; raw
passwords never in logs/traces/env/DynamoDB.
- **INV-17 (ABAC discipline):** The token-vend Lambda evaluates the
kyverno-json ABAC policy before signing; allow/deny + policy inputs
emitted to the audit stream.
### v1.28 Traceability (live — see CHECKPOINT.json for authoritative state)
| REQ | Phase | Status |
|-----|-------|--------|
| REQ-323 | P1 | complete (v1.27.1) |
| REQ-324 | P1 | complete (v1.27.1) |
| REQ-325 | P1 | complete (v1.27.1) |
| REQ-326 | P1 | complete (v1.27.1) |
| REQ-327 | P1 | complete (v1.27.1) |
| REQ-328 | P1 | complete (v1.27.1) |
| REQ-329 | P2 | complete (v1.27.2) |
| REQ-330 | P2 | complete (v1.27.2) |
| REQ-331 | P2 | complete (v1.27.2) |
| REQ-332 | P2 | complete (v1.27.2) |
| REQ-333 | P3 | complete (v1.27.3) |
| REQ-334 | P3 | complete (v1.27.3) |
| REQ-335 | P3 | complete (v1.27.3) |
| REQ-336 | P4 | complete (v1.27.4) |
| REQ-337 | P4 | complete (v1.27.4) |
| REQ-338 | P4 | complete (v1.27.4) |
| REQ-339 | P4 | complete (v1.27.4) |
| REQ-340 | P4 | complete (v1.27.4) |
| REQ-341 | P4 | complete (v1.27.4) |
| REQ-342 | P4 | complete (v1.27.4) |
| REQ-343 | P4 | complete (v1.27.4) |
| REQ-344 | P4 | complete (v1.27.4) |
| REQ-345 | P5 | complete (v1.27.5) |
| REQ-346 | P5 | complete (v1.27.5) |
| REQ-347 | P5 | complete (v1.27.5) |
| REQ-348 | P5 | complete (v1.27.5) |
| REQ-349 | P5 | complete (v1.27.5) |
| REQ-350 | P5 | complete (v1.27.5) |
| REQ-351 | P5 | complete (v1.27.5) |
| REQ-352 | P6 | complete (v1.27.6) |
| REQ-353 | P6 | complete (v1.27.6) |
---
## v1.29 — Reposplit + Identity Layer Bring-Live (complete, tag `v1.28.6`, merged to main 2026-08-20)
> **Feature milestone — complete.** v1.29 extracts all live platform
> components into a dedicated Gitea-private Terraform repository
> (`nova-platform-ops`), brings Nova-idp live in account `581513795199`
> for the first time, and standardizes `acdl/acdl` on GitHub. `kj` (a
> compiled Go binary, pinned v0.0.3, distinct from the kyverno-json
> engine) has exactly one identity: one ECR image digest shared by the
> production Lambda runtime and its defensive Fargate fallback
> (KJ-LOCKSTEP, REQ-371).
>
> Tags run on the **v1.28.x** line: `v1.28.0` (P0) →
> `v1.28.1..v1.28.5` (execution) → `v1.28.6` (final = milestone release).
> Milestone branch: `milestone/v1.29-reposplit-identity`.
>
> **Scope split (CLARIFY-grounded, full autonomy):** Terraform module
> code is authored out-of-band in `nova-platform-ops`. REQs marked
> `[covered-reference]` have their verification surface in the
> `nova-platform-ops` cutover gates (M1/M1.5/M2), documented in the
> operator guide (`docs/operator-guide-platform-ops.md`). CIAgent in
> `acdl` authors only the acdl-side REQs.
### Decisions (locked in CLARIFY — full autonomy, load-bearing for v1.29)
- **D-232 (Forge parity abandoned):** the byte-identical-forges CI parity
(Gitea + GitHub) is abandoned; `acdl/acdl` standardizes on GitHub. CI
fails with `forge_parity_disabled` (deliberate). Rationale: Vision §4
domain boundaries — operations lives in Gitea-private `nova-platform-
ops`, engineering lives on GitHub.
- **D-233 (JWKS public-read via CloudFront edge):** the JWKS endpoint is
the only public read surface of the live platform (INV-18). All other
platform endpoints gate with `AuthType: AWS_IAM`. CloudFront + OAC
pinning replaces direct Lambda Function URL exposure.
- **D-234 (KMS asymmetric key provisioning):** `alias/nova-oidc-signing`
provisioned with `KeySpec: ECC_NIST_P256`, `KeyUsage: SIGN_VERIFY`,
90-day rotation cadence (matches per-stack CMK rotation per D-069).
- **D-235 (Tag-pin handoff):** engineering hands off to operations via
tags. `acdl/acdl` `publish.yml` attaches artifacts to GitHub Releases
per tag; `nova-platform-ops` declares `local.nova_platform_version` +
`local.kj_source_sha` and resolves substrates through a single
`data.aws_ecr_image.kj_image`.
- **D-236 (Cutover shape + rollback procedure):** M1 day-0 cutover is
conditional on M1.5 verification gate (3 consecutive rebuilds, 12-item
spike per grill CF-1). Rollback = revert `nova_platform_version` pin;
the prior tag's artifacts remain downloadable. M2a (Fargate toggle)
activates only if M1.5 fails 3×.
- **D-237 (Fargate sunset discipline):** the always-warm minimal Fargate
standby (REQ-363b, ~$1520/month) may not be deleted unless REQ-363 has
been green in production for ≥30 consecutive days. Sunset requires an
architecture review.
- **D-238 (KJ-LOCKSTEP release-gate invariant):** the ECR image digest
running on the Fargate standby MUST equal the digest resolved by
`aws_lambda_function.nova_idp_token_vend.image_uri` at every
`terraform plan`. Enforced by `lifecycle.precondition` (mechanism) +
Gitea Actions `if: steps.plan.outcome == 'success'` (mechanism) + PR
comment reporting (observability) + operator review (last, never
first). No second pipeline, no second SHA pin. Vision §6 immutability
+ Vision §5 narrow interfaces.
### P1 — Publish Pipeline
#### REQ-354 — `publish.yml` attaches Lambda zip + layer wheel + Python wheel + ECR container image to GitHub Release for each tag
**Journeys:** J1, J2 (criteria 34). **Priority:** High.
**AC:**
**(1)** Given a tag `v1.29.x` is pushed to `acdl/acdl` main, when
`publish.yml` runs, then the release artifacts `nova-lambda-token-vend-
v1.29.x.zip`, `nova-cli-layer-v1.29.x.zip`, and `nova-1.29.x-py3-none-
any.whl` appear in GitHub Releases with matching SHA-256 in the body.
**(2)** Given two consecutive tags `v1.29.0` and `v1.29.1`, when both
releases are queried, then each tag's artifacts are independent and the
previous tag's artifacts remain downloadable.
**(3)** Given the publish pipeline runs for tag `v1.29.x`, when the
image build step executes, then a single ECR image is pushed at tag
`v1.29.x-kj-<kj-source-sha>` where `<kj-source-sha>` is read from
`platform/abac/kj-version.txt` at build time and embedded in the tag
(D-239: ECR tags reject `+`; corrected from `v1.29.x+kj-<sha>` to
`v1.29.x-kj-<sha>`).
**(4)** Given the image is pushed, when the GitHub Release body lists
artifacts, then the image URI and digest appear alongside the wheel,
layer, and Lambda zip. KJ-STATIC: the `kj` binary is compiled
`CGO_ENABLED=0 GOOS=linux GOARCH=amd64` and `file(1)` reports
`statically linked, no shared library` before embedding.
### P2 — Gitea Scrub + Decisions
#### REQ-367 — Hard scrub of all Gitea references in `acdl/acdl` at v1.29.0
**Journeys:** Cross-cutting. **Priority:** Critical.
**AC:**
**(1)** Given v1.29.0 is cut from main, when `grep -rni gitea .github/
docs/ pyproject.toml README.md .ciagent/` runs, then zero matches
outside this spec's archive section.
**(2)** Given v1.29.0 ships, when `.gitea/` is checked in the working
tree, then `find .gitea` returns nothing.
**(3)** Given v1.29.0 ships, when the bit-identical-forges parity is
asserted in CI, then CI fails with `forge_parity_disabled` (deliberate;
documented in D-232).
#### REQ-368 — Decisions D-232..238 recorded in PROJECT.md + CLARIFY
**Journeys:** Cross-cutting. **Priority:** High.
**AC:**
**(1)** Given the milestone is recorded, when loading `PROJECT.md`, then
decisions D-232 (forge parity abandoned), D-233 (JWKS public-read via
CloudFront edge), D-234 (KMS asymmetric key provisioning), D-235 (tag-
pin handoff), D-236 (cutover shape + rollback procedure), D-237
(Fargate sunset discipline ≥30 days → architecture review), D-238
(KJ-LOCKSTEP release-gate invariant) are present with rationale citing
Vision §4 domain boundaries.
**(2)** Given decisions are present, then each decision references the
source statement from the v1.29 spec.
### P3 — CFN Archive + TF Delegation
#### REQ-369 — CFN → Terraform conversion of `nova idp setup`
**Journeys:** J2. **Priority:** High.
**AC:**
**(1)** Given the CFN template in `acdl/acdl/nova/idp/setup.py`, when
the equivalent Terraform in `nova-platform-ops` runs, then the same
resources (Lambdas, DDB tables, IAM roles, KMS key references) are
created. [covered-reference: nova-platform-ops]
**(2)** Given the conversion, when a new operator runs `nova idp setup
--apply`, then the CLI delegates to `terraform apply`; the CFN code
path is no longer the active path.
**(3)** Given the conversion, the CFN file in `acdl/acdl` is archived
to `docs/archive/nova-idp-cfn-v1.28.md` as read-only reference;
deletion is a follow-up.
### P4 — Operator Guide + Reference Tracking (docs)
#### REQ-OPS-GUIDE — `docs/operator-guide-platform-ops.md`
**Journeys:** J2. **Priority:** High.
**AC:** Given the operator guide is published, when an operator reads
it, then it covers: KMS rotation (90-day cadence, `alias/nova-oidc-
signing`), JWKS reachability via CloudFront edge (OAC pinning, public
read vs. IAM-gated), PITR restore (DynamoDB point-in-time recovery),
PAT revocation (60s SLO), edge configuration (CloudFront + WAF + ACM +
Route53), Fargate standby status checks (`GET /health` every 10s,
`KJ-WARMUP-HEALTH`), cost section (WAF ~$510/month + Fargate
~$1520/month), artifact-mirror fallback (operator-local mirror by
SHA-256 when Gitea `act_runner` cannot reach GitHub Releases), and the
M1/M1.5/M2 cutover gates as release-gate entries for the covered-
reference REQs.
### P5 — Consumer Deploy Bump (cross-project, Edge 8)
#### REQ-CONSUMER-BUMP — `nova-blockchain-exchange` deploy.yml `@v1.25` → `@v1.29`
**Journeys:** J1. **Priority:** High.
**AC:**
**(1)** Given `nova-blockchain-exchange` deploy.yml pins
`acdl/.github/workflows/deploy.yml@v1.25`, when the bump is applied,
then both `.github/workflows/deploy.yml` and
`.gitea/workflows/deploy.yml` reference `@v1.29`.
**(2)** Given the bump, when the smoke test runs (sign-up → sign-in →
token-vend → apply → audit), then the chain completes successfully
against the v1.29 publish artifacts.
### Covered-reference requirements (authored in `nova-platform-ops`, out-of-band)
The following REQs are tracked for milestone completeness but their
code lands in `nova-platform-ops`. Their verification surface is the
M1/M1.5/M2 cutover gates documented in the operator guide.
- **REQ-355** — ops repo pins `local.nova_platform_version` +
`local.kj_source_sha`; CI resolves matching artifacts + image digest.
- **REQ-356** — ops repo CI runs `terraform plan` on every PR; drift
fails with `drift_detected`.
- **REQ-357** — HITL approver distinct from PR author required for
`terraform apply` (INV-3, TFM-HITL).
- **REQ-358** — Operator bumps `nova_platform_version` to roll out
engineering change; `CodeSha256` matches the artifact SHA-256.
- **REQ-359** — ops repo is Gitea-private with no GitHub mirror
(OPER-PRIV).
- **REQ-360** — ops repo IAM scope is bounded; no AdministratorAccess
(IAM-NARROW).
- **REQ-361** — Terraform imports existing live resources idempotently
(IMPORT-IDEMPOTENT).
- **REQ-362** — `alias/nova-oidc-signing` KMS key provisioned
(`ECC_NIST_P256`, `SIGN_VERIFY`, 90-day rotation).
- **REQ-363** — Nova-idp 3 Lambdas deployed on container image with
static `kj` (production substrate, KJ-STATIC).
- **REQ-363b** — Fargate defensive fallback — always-warm minimal
Fargate standby, **same ECR image** (KJ-LOCKSTEP, KJ-WARMUP-HEALTH).
- **REQ-364** — JWKS Function URL reachable only via CloudFront with
OAC pinning (INV-18, JWKS-EDGE-ONLY).
- **REQ-365** — WAF WebACL rate-limit (3000/5min) + AWS Managed Rules.
- **REQ-366** — ACM cert + Route53 alias for the JWKS domain.
- **REQ-371** — KJ-LOCKSTEP applied-at-plan mechanism
(`lifecycle.precondition` on both image-bearing resources; fail-closed
by mechanism, not by discipline).
### v1.29 Invariants + NFR constraints (new)
- **INV-18 (JWKS-EDGE-ONLY):** the JWKS endpoint is the only public read
surface of the live platform. All other platform endpoints MUST gate
with `AuthType: AWS_IAM`.
- **KJ-STATIC (NFR):** `kj` compiled `CGO_ENABLED=0`; `file(1)` reports
`statically linked, no shared library`; SHA-256 matches
`platform/abac/kj-version.txt`; recorded in Terraform state.
- **KJ-LOCKSTEP (NFR):** Fargate standby digest == Lambda `image_uri`
digest at every `terraform plan`. Detected by
`lifecycle.precondition` (mechanism) + CI `if:
steps.plan.outcome == 'success'` (mechanism) + PR comment
(observability) + operator review (last). No second pipeline, no
second SHA pin.
- **KJ-WARMUP-HEALTH (NFR):** Fargate standby `READY` probe (`GET /health
→ 200` every 10s) green before M1 cutover; release-gate entry.
- **OPER-PRIV (NFR):** `nova-platform-ops` `private: true`, not mirrored.
- **IAM-NARROW (NFR):** Gitea OIDC role bounded per REQ-360; no
`Action: "*"` or `Resource: "*"`.
- **DRIFT-DETECT (NFR):** `terraform plan` exit 2 (drift) fails the
apply workflow; manual reconciliation required.
- **IMPORT-IDEMPOTENT (NFR):** re-import exits non-zero with
`resource_already_imported`.
- **TFM-HITL (NFR):** `terraform apply` against `main` requires Gitea
Actions approval from a user distinct from the PR author.
- **JWKS-SLO (NFR):** `GET /.well-known/jwks.json` P95 < 200ms same-
region; `Cache-Control: max-age=3600` honored.
- **JWKS-ROTATION (NFR):** on key rotation, both old + new public keys
published during 24-hour overlap window.
### v1.29 Traceability (live — see CHECKPOINT.json for authoritative state)
| REQ | Phase | Status |
|-----|-------|--------|
| REQ-354 | P1 | complete (v1.28.1) |
| REQ-367 | P2 | complete (v1.28.2) |
| REQ-368 | P2 | complete (v1.28.2) |
| REQ-369 | P3 | complete (v1.28.3) |
| REQ-OPS-GUIDE | P4 | complete (v1.28.4) |
| REQ-CONSUMER-BUMP | P5 | complete (v1.28.5) |
| REQ-355 | covered-reference | planned (M1 gate: nova-platform-ops) |
| REQ-356 | covered-reference | planned (M1 gate: nova-platform-ops) |
| REQ-357 | covered-reference | planned (M1.5 gate: nova-platform-ops) |
| REQ-358 | covered-reference | planned (M2 gate: nova-platform-ops) |
| REQ-359 | covered-reference | planned (M1 gate: nova-platform-ops) |
| REQ-360 | covered-reference | planned (M1.5 gate: nova-platform-ops) |
| REQ-361 | covered-reference | planned (M1 gate: nova-platform-ops) |
| REQ-362 | covered-reference | planned (M1.5 gate: nova-platform-ops) |
| REQ-363 | covered-reference | planned (M1.5 gate: nova-platform-ops) |
| REQ-363b | covered-reference | planned (M1.5 gate: nova-platform-ops) |
| REQ-364 | covered-reference | planned (M1.5 gate: nova-platform-ops) |
| REQ-365 | covered-reference | planned (M1 gate: nova-platform-ops) |
| REQ-366 | covered-reference | planned (M1 gate: nova-platform-ops) |
| REQ-371 | covered-reference | planned (M2 gate: nova-platform-ops) |
> **Covered-reference REQs** are verified via the M1/M1.5/M2 cutover
> gates in `nova-platform-ops` CI (out-of-band). The operator attests
> the results in `docs/operator-guide-platform-ops.md` §18 "Cutover
> Gates" Result column. P6 audit verifies the template + Result column
> exist; the live-green attestation is out-of-band (grill CF-1/CF-2).
## v1.30 — Single-shot Leadership Deck (active milestone)
> **Feature milestone — single-shot PPTX leadership deck.** Ships
> REQ-372.1 through REQ-372.12 in one execution phase. Tags run on the
> **v1.29.x** line (milestone v1.30 → tags v1.29.1..v1.29.3). Tag
> `v1.29.3` = the milestone release. The deck is a discrete artifact,
> hand-authored (NOT a compression of the existing citizen-developer
> pitch per D-241), scoped to a single live presentation to
> Infrastructure & Operations leadership in August 2026, securing
> architecture endorsement and a November 2026 runway.
>
> Source: `docs/presentations/nova-leadership-deck-marp.md` (authored
> against the Slide Content Map in `.ciagent/PROJECT.md` §v1.30 spec).
> Rendered via the existing `scripts/render_pptx.py` (narrowly extended
> per D-242 to accept an explicit source path + custom output filename
> and to add a per-slide footer textbox). Smoke test:
> `scripts/check_leadership_deck.sh` (runnable on demand; NOT a CI gate
> per the single-shot constraint). Vision grounding `[1]` citations
> resolve to `docs/vision.md` (the spec's `acdl-vision.md` reference).
### Decisions (locked in CLARIFY, full autonomy — load-bearing for v1.30)
- **D-241 (Q3 override):** The leadership deck is a **discrete,
hand-authored artifact** — NOT a compression of the existing
23-slide citizen-developer pitch
(`nova-autonomous-cloud-delivery-marp.md`). This overrides the
post-v1.29 STATE.md intake assumption 3 ("is a compression, not a
rewrite"). The existing citizen-developer deck remains untouched.
Rationale: the spec §2.2 + cover note forbid compression/mirroring;
the Slide Content Map is hand-authored content, not derived.
- **D-242 (render pipeline):** The existing `scripts/render_pptx.py`
is narrowly extended to (a) accept an explicit source `.md` path +
custom output `.pptx` filename (the cover note's invocation
`scripts/render_pptx.py docs/presentations/nova-leadership-deck.md`
is honoured via a path-aware argv), and (b) render a right-aligned
footer textbox on every slide with the exact string
`Nova Platform - Infrastructure & Operations` (the python-pptx
renderer does not read the Marp `footer:` directive; REQ-372.5
requires the footer on every rendered slide). This extension is a
non-REQ-372 prerequisite per spec §3.3 Edge 2 ("scope narrowly and
update `render_pptx.py` separately"). The source file is authored as
`nova-leadership-deck-marp.md` to fit the existing `-marp.md`
pipeline convention; the output is `nova-leadership-deck.pptx` per
spec REQ-372.2.
- **D-243 (date anchor):** August 2026 is a month-only presentation
anchor (no specific day); November 2026 is the runway anchor
(~90 days). Slide 7 references "Infrastructure & Operations
leadership" without naming a specific day. Resolves spec §7 Q1.
### Requirements
#### REQ-372.1 — Source markdown exists and is parseable
**Priority:** High · **Journey:** J1
**Given** the deck initiative is scoped, **when**
`docs/presentations/nova-leadership-deck-marp.md` is read, **then** the
file exists, parses as valid Marp markdown, contains exactly 7 slides
delimited by `---`, and the file header carries the related-artifacts
comment (per REQ-372.9).
#### REQ-372.2 — PPTX render via existing pipeline
**Priority:** High · **Journey:** J1
**Given** the source markdown exists (REQ-372.1), **when**
`scripts/render_pptx.py` is invoked against the leadership deck source,
**then** `docs/presentations/nova-leadership-deck.pptx` is written with
7 slides and python-pptx raised no exceptions.
#### REQ-372.3 — Slide count is exactly 7
**Priority:** High · **Journey:** J1
**Given** the source markdown, **when** slide boundaries are counted,
**then** the count equals 7.
#### REQ-372.4 — Speaker notes depth per slide
**Priority:** High · **Journey:** J1
**Given** the source markdown, **when** speaker notes (HTML comments)
are extracted per slide, **then** per-slide word counts fall within:
slides 1/2/4/6 in 150300; slides 3/5 in 250400; slide 7 in 200300.
Smoke test exits non-zero on violation.
#### REQ-372.5 — Footer on every slide
**Priority:** High · **Journey:** J1
**Given** the source markdown's Marp frontmatter `footer:` directive +
the python-pptx renderer extension (D-242), **when** the PPTX is
rendered, **then** every slide carries the right-aligned footer
`Nova Platform - Infrastructure & Operations`.
#### REQ-372.6 — S&P theme tokens are the only colors used
**Priority:** High · **Journey:** J1
**Given** the source markdown, **when** color values are extracted
(Marp directives + inline overrides), **then** the only hex colors
present are `#D6002A`, `#1B1B1B`, `#FFFFFF`, `#F0F0F0`.
#### REQ-372.7 — Slide-by-slide content traceability
**Priority:** High · **Journey:** J1
**Given** the rendered PPTX, **when** any slide N ∈ [1, 7] is opened,
**then** its content matches the **Slide Content Map** in
`.ciagent/PROJECT.md` §v1.30 spec. Any deviation from the map requires
`CLARIFY` before ship. Smoke test does not assert content strings
verbatim (brittle); audit verifies by visual review against the map.
#### REQ-372.8 — Smoke test exits 0 on pass
**Priority:** High · **Journey:** J1
**Given** `scripts/check_leadership_deck.sh` exists, **when** invoked
from the repo root, **then** the script asserts: (a) source file
exists, (b) slide count = 7, (c) per-slide word counts in band, (d)
footer string present in source, (e) only S&P hex colors used, (f)
PPTX file exists. Exits 0 on pass, non-zero on fail. Runnable on
demand; not wired as a CI gate.
#### REQ-372.9 — Related-artifacts comment in source header
**Priority:** Med · **Journey:** J1
**Given** the source markdown, **when** the file header is inspected,
**then** a comment exists that (i) names this deck as the leadership
artifact for Infrastructure & Operations, (ii) names August 2026 as
the presentation date, (iii) names
`nova-autonomous-cloud-delivery-marp.md` as a related-but-distinct
artifact and notes that this deck does not compress or modify it.
#### REQ-372.10 — CAP-042 appended to STATE.md at ship
**Priority:** Med · **Journey:** J1
**Given** the deck has shipped, **when** STATE.md is updated at the
v1.30 milestone ship wave, **then** a CAP-042 row exists capturing
artifact paths (`nova-leadership-deck-marp.md`,
`nova-leadership-deck.pptx`), audience (Infrastructure & Operations
leadership), single-shot intent, presentation month (August 2026).
#### REQ-372.11 — D-241 recorded in PROJECT.md at ship
**Priority:** Med · **Journey:** J1
**Given** the deck has shipped, **when** PROJECT.md is updated at the
v1.30 milestone ship wave, **then** a `D-241` entry exists capturing:
(a) single-shot nature of the deck, (b) audience (Infrastructure &
Operations leadership), (c) August 2026 anchor + November 2026 runway,
(d) explicit decision not to compress the existing citizen-developer
deck.
#### REQ-372.12 — Vision grounding citations in architecture-load slides
**Priority:** Med · **Journey:** J1
**Given** the source markdown, **when** the speaker notes are
inspected, **then** at least one `[1]` citation appears in slides 3,
5, and 7 — the three architecture-load slides — grounding the
principles, anti-goals, and integration-boundary claims to
`docs/vision.md` (the spec's `acdl-vision.md` reference [1]).
### v1.30 Traceability (live — see CHECKPOINT.json for authoritative state)
| REQ | Phase | Status |
|-----|-------|--------|
| REQ-372.1 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.2 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.3 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.4 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.5 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.6 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.7 | P1/P3 | complete (v1.29.5, polished + diagrams) |
| REQ-372.8 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.9 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.10 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.11 | P1/P3 | complete (v1.29.5, polished) |
| REQ-372.12 | P1/P3 | complete (v1.29.5, polished) |
## v1.31 — Leadership Deck Polish II (active milestone)
> **Refinement-only NFR milestone — polish pass on the v1.30 leadership
> deck.** Enriches visible on-slide prose + improves layout, then
> re-renders the PPTX. No new features, no new slides, no theme change,
> no diagram change (D-247). Tags run on the **v1.30.x** line (previous
> minor): `v1.30.0` (P0) → `v1.30.1` (P1 = milestone release). The
> v1.30 requirements (REQ-372.1..12) remain complete and are NOT
> re-opened; v1.31 adds REQ-373.1..4 as a refinement layer over the
> same artifact.
### Decisions (locked in CLARIFY, full autonomy — load-bearing for v1.31)
- **D-247 (refinement-only, theme-preserving):** The polish is
confined to visible prose density + layout. The following are
**invariants** and must not change: the S&P theme tokens
(`#D6002A`, `#1B1B1B`, `#FFFFFF`, `#F0F0F0`); the 7-slide count; the
per-slide speaker-note word-count bands (1/2/4/6: 150300; 3/5:
250400; 7: 200300, per REQ-372.4); the `[1]` citations on slides
3/5/7 (per REQ-372.12); the 7 mermaid diagram PNGs and their `.mmd`
sources; the footer string `Nova Platform - Infrastructure &
Operations`. No new slides, no new diagrams, no renderer changes.
The citizen-developer deck is untouched (D-241 still holds).
### Requirements
#### REQ-373.1 — Visible prose density enriched per slide
**Priority:** High · **Journey:** J1
**Given** the v1.30 deck shipped with sparse visible wording (slides
26 averaged 4367 visible words, leaning on diagrams), **when** the
polished source markdown is inspected, **then** every slide's on-slide
body (excluding speaker-note HTML comments and image references)
carries richer, well-structured wording that lets the slide read as a
standalone artifact — a title, a framing line, a body, and (where
present) a closing italic benefit — without crowding the diagram or
overflowing the 16:9 slide. Verified by visual review against the
Slide Content Map and by the smoke test still passing.
#### REQ-373.2 — Layout improved within the renderer block vocabulary
**Priority:** High · **Journey:** J1
**Given** the existing `scripts/render_pptx.py` block vocabulary
(lead/quote/plain/bullet/ordered/image/table/benefit) is not modified,
**when** the polished PPTX is rendered, **then** each slide balances
its blocks so the reading order is clear (title → frame → body →
diagram → benefit) and the diagram remains the visual anchor. No new
renderer features are added.
#### REQ-373.3 — v1.30 invariants preserved (D-247)
**Priority:** High · **Journey:** J1
**Given** the D-247 invariants, **when** the polished source + PPTX
are validated, **then**: slide count = 7; the only hex colors are the
four S&P tokens; per-slide speaker-note word counts remain in band per
REQ-372.4; `[1]` citations remain in slides 3, 5, 7 speaker notes; the
7 diagram PNGs are reused unchanged; the footer string is unchanged.
The smoke test enforces (a)(f) and must exit 0.
#### REQ-373.4 — PPTX re-rendered; smoke test exits 0
**Priority:** High · **Journey:** J1
**Given** the polished source markdown, **when**
`scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md
--output docs/presentations/nova-leadership-deck.pptx` is invoked,
**then** the PPTX is written with 7 slides and python-pptx raises no
exceptions, **and** `bash scripts/check_leadership_deck.sh` exits 0.
### v1.31 Traceability (live — see CHECKPOINT.json for authoritative state)
| REQ | Phase | Status |
|-----|-------|--------|
| REQ-373.1 | P1 | complete (v1.30.2) |
| REQ-373.2 | P1 | complete (v1.30.2) |
| REQ-373.3 | P1 | complete (v1.30.2) |
| REQ-373.4 | P1 | complete (v1.30.2) |
-144
View File
@@ -1,144 +0,0 @@
# Nova — v1.27 Research Findings
> Phase: research (pre-execution). Milestone: v1.27 (PO State Catalog &
> Ciagent Compression). 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.
---
## 1. Files to archive (staleness inventory)
### 1.1 Pre-execution artifacts (v1.26 — shipped, decisions folded)
| 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 |
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 Phase verifications + review (v1.26 — shipped, PASS)
| File | Lines | Staleness evidence |
|---|---|---|
| `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 |
### 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`.
---
## 2. Files to keep active (no-edit or fix-only)
### 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.
+112 -6
View File
@@ -81,6 +81,108 @@
before building the new env). New `core/env_transition.py` module.
15 requirements (REQ-276..290), 4 phases.
- **v1.27:** complete (tag `v1.26.3`) — PO State Catalog & Ciagent
Compression. NFR milestone. Authored `.ciagent/STATE.md` (PO-facing
capability catalog, 32 CAP rows + 11 invariants across 10 domains,
backfilled through v1.26). Archived 7 platform-root files + 1
consumer file to `.ciagent/archive/` (CAPABILITY_INVENTORY,
REVIEW-AUDIT-P05, VERIFY-P03, VERIFY-P04, P4-PILOT-RUN-EVIDENCE,
AUTONOMY_THESIS, COST + nova-blockchain-exchange/ROADMAP). Fixed
PROJECT.md + ROADMAP.md v1.26 phase-status (P3/P4/P5 → complete).
Wired STATE.md into the P-final ship discipline (PLAN.md, ROADMAP.md,
NORTH_STAR.md). Active `.ciagent/` root: 15 .md (was 25) + 1 json + 1
checkpoint. 3 phases (P0 pre-execution + P1 author-archive + P2
fix-stale-wire + P3 final-review-ship). No REQ-NNN (NFR).
- **v1.28:** complete (tag `v1.27.6`) — CLI Canonicalization + Identity
Layer. Feature milestone. The Nova CLI is installable from internal
PyPI (CodeArtifact); every `core/` module is reachable as a `nova
<subcommand>` (15 subcommands, argparse-only, ≤50-line thin delegates,
CAP-033/034); `nova init` scaffolds `.nova/`; `nova cli-action`
composite action published to GitHub + Gitea (byte-identical, NFR-11);
`core/mode_resolver.py` (flag → env → credential type →
`sys.stdin.isatty()`, D-226); INV-12 audit emission on every
invocation. Nova owns its identity layer end-to-end (Nova-idp):
`nova-idp-auth` Lambda (sign-up/sign-in/session, Argon2id t=3 m=65536
p=1, fail-closed D-228), `nova-idp-token-vend` Lambda (KMS-signed
OIDC tokens, ECDSA P-256 / ES256, DER→raw conversion, kyverno-json
ABAC fail-closed C-6.1), `nova-idp-jwks` Lambda (JWKS endpoint),
`nova-pats` DynamoDB (PAT lifecycle, strong-read revocation D-229,
60s SLO), `nova auth login/revoke/status`, `nova idp setup
--check/--apply/--verify` (CloudFormation, NFR-10 explicit approval).
No AWS-managed identity (INV-15). 31 requirements (REQ-323..353), 6
capabilities (CAP-033..038), 6 invariants (INV-12..17), 6 decisions
(D-226..231). 6 execution phases (P1 cli-substrate, P2
lambda-packaging, P3 idp-auth, P4 token-vend-pat + idp-setup folded
C-2.1, P5 docs-integration, P6 final-review-ship). Grill:
PROCEED-WITH-CONDITIONS (0.76), 3 critical fixes (ABAC fail-closed,
JWS KDF, traceability drift) + 16 tracked conditions applied. 1000
tests passing. Tags: `v1.27.0` (P0) → `v1.27.1..v1.27.5` (P1..P5) →
`v1.27.6` (P6 final = milestone release).
- **v1.29 (complete, tag `v1.28.6` = the v1.29 release, merged to main
2026-08-20):** Reposplit + Identity Layer Bring-Live. Feature milestone.
v1.29 extracts all live platform components (Nova-idp Lambdas, KMS keys,
DynamoDB tables, S3 state buckets, OIDC roles, JWKS, audit outbox
bootstrap) from `acdl/acdl` into a dedicated Gitea-private Terraform
repository (`nova-platform-ops`), brings Nova-idp live in account
`581513795199` for the first time (code complete since v1.28, unverified
in-account at Phase 0), and standardizes `acdl/acdl` on GitHub. The
split enforces Vision §4 domain boundaries architecturally —
engineering ends at the compiled artifact; operations begins at the
live platform under guardrails. `kj` (a compiled Go binary, pinned
v0.0.3 in `platform/abac/kj-version.txt`, distinct from the kyverno-json
engine) has exactly one identity: one ECR image digest shared by both
the production Lambda runtime and its defensive Fargate fallback
(KJ-LOCKSTEP — drift eliminated by construction, enforced by
`lifecycle.precondition` at plan time, REQ-371). M1.5 verification gate
(8-item spike, 3 consecutive rebuilds) gates M1 cutover. CIAgent in
`acdl` delivers the acdl-side work (publish.yml + ECR image, Gitea
scrub, CFN archive + CLI terraform-delegation, operator guide,
consumer deploy bump); the Terraform modules for `nova-platform-ops`
are authored out-of-band (covered-reference REQs with cutover gates as
the verification surface). 17 requirements (REQ-354..369 + 371 +
363b), 7 decisions (D-232..238), 1 invariant (INV-18 JWKS-EDGE-ONLY) +
10 NFR constraints. Tags: `v1.28.0` (P0) → `v1.28.1..v1.28.5` (P1..P5)
`v1.28.6` (P6 final = milestone release).
- **v1.30 (complete, tag `v1.29.5` = the v1.30 release, merged to main
2026-08-20):** Single-shot Leadership Deck (polished). Feature
milestone. A hand-authored 7-slide PPTX deck for Infrastructure &
Operations leadership (CTO + VP Technology + Product Management),
presented August 2026, securing architecture endorsement and a
November 2026 runway. The deck is a **discrete artifact** (D-241:
NOT a compression of the existing citizen-developer pitch
`nova-autonomous-cloud-delivery-marp.md`, which remains unmodified).
Authored as Marp markdown, rendered via the existing
`scripts/render_pptx.py` (narrowly extended per D-242: path arg +
`--output` + per-slide footer textbox + leading-comment skip +
auto-fit + vertical balance + bullet-marker fix). **Polished in P3**
(D-244: slide 1 → black cover; D-245: slide 7 "What works now" block
— L1/L2 stacks live at confidence 0.800, next steps: greenfield
pilots → promote to prod → SPGE constitutional library → infra
layer; D-246: 7 S&P-themed mermaid diagrams, one per slide, rendered
via local mmdc with vendored Chromium libs). Smoke test
`scripts/check_leadership_deck.sh` (on-demand, NOT a CI gate). Vision
`[1]` grounding in slides 3/5/7 speaker notes (resolve to
`docs/vision.md`). 12 requirements (REQ-372.1..12), 1 capability
(CAP-042), 6 decisions (D-241..D-246). Tags: `v1.29.1` (P0) →
`v1.29.2` (P1 first draft) → `v1.29.3` (P2 first-draft final) →
`v1.29.4` (P3 polish) → `v1.29.5` (P4 polished final = milestone
release).
- **v1.31 (complete, tag `v1.30.2` = the v1.31 release):** Leadership
Deck Polish II — refinement-only NFR milestone. Enriches the v1.30
deck's visible on-slide prose and improves slide layout, then
re-renders the PPTX. Preserves all v1.30 invariants (D-247): S&P
theme tokens, 7-slide count, speaker-note word-count bands, `[1]`
citations on slides 3/5/7, the 7 mermaid diagram PNGs, and the
footer string. No new slides, no new diagrams, no renderer changes;
the citizen-developer deck is untouched. Visible prose density
raised (slides 26: 4367 → 97168 visible words). 4 requirements
(REQ-373.1..4), 1 decision (D-247). Tags ran on the v1.30.x line:
`v1.30.1` (P0) → `v1.30.2` (P1 = milestone release).
> **Full v1.0v1.24 phase detail, wave ordering, success criteria, and
> decision cross-references:** `.ciagent/archive/ROADMAP-v1.0-v1.24.md`.
@@ -173,12 +275,15 @@ final). Tags: `v1.24.0` (P0) → `v1.24.5` (P5 = milestone release).
summary; delete all milestone branches.
- Updated `REQUIREMENTS.md` (mark REQ-291..309 complete), `ROADMAP.md`
(mark v1.25 complete), `NORTH_STAR.md` (note Strategic Objective #2
provable trust via a replaceable policy-engine substrate).
provable trust via a replaceable policy-engine substrate),
`STATE.md` (append v1.25 capability rows — note: STATE.md was authored
in v1.27 with the v1.25 capabilities backfilled; the v1.25 ship did
not update STATE.md because STATE.md did not yet exist).
- **Requirements:** REQ-291..309 (19 requirements).
---
## v1.26 (active, tag line `v1.25.x`): Live Pilot Estate Activation
## v1.26 (complete, tag `v1.25.5` = the v1.26 release, merged to main 2026-08-19): Live Pilot Estate Activation
`D-096` lifts. The first real consumer estate — a stock exchange on a
homegrown Proof-of-Authority blockchain (equities only, single
@@ -235,7 +340,7 @@ REQ-315..322), 3 deferred. Adversarial grill: PROCEED 0.84.
- Cross-cutting: `v1.25` floating tag → `v1.25.0` (Phase 0 ship) on the
platform repo.
### Phase P3 — pilot-metrics-and-policies (planned, tag v1.25.3)
### Phase P3 — pilot-metrics-and-policies (complete, tag v1.25.3)
- REQ-315: `adapters/kyverno-json/policies/settlement-finality.json`
kyverno-json policy asserting all matches in the promotion window have
committed blocks (securities-specific). Authored + tested in v1.26;
@@ -258,7 +363,7 @@ REQ-315..322), 3 deferred. Adversarial grill: PROCEED 0.84.
- REQ-320: `adapters/kyverno-json/policies/pilot-readiness/no-placeholder-account.json`
— declarative gate preventing apply against a placeholder account.
### Phase P4 — pilot-run-and-docs (planned, tag v1.25.4)
### Phase P4 — pilot-run-and-docs (complete, tag v1.25.4)
- REQ-321: `adapters/README.md` (new consumer row) +
`docs/METRICS.md` (Post-Pilot metrics grounded note) +
`.ciagent/ARCHITECTURE.md` §12.8 (Pilot Estate) +
@@ -269,7 +374,7 @@ REQ-315..322), 3 deferred. Adversarial grill: PROCEED 0.84.
events land in the Decision Ledger; the regression gate (CAP-025)
verifies the round-trip.
### Phase P5 — final review + audit + milestone ship (Final Phase, planned, tag v1.25.5)
### Phase P5 — final review + audit + milestone ship (Final Phase, complete, tag v1.25.5 = the v1.26 release)
- Multi-persona code review across P1..P4 (lead-developer, backend-
engineer, data-engineer, policy-engineer, blockchain-engineer).
Auto-fix P0; flag P1+.
@@ -281,7 +386,8 @@ REQ-315..322), 3 deferred. Adversarial grill: PROCEED 0.84.
full milestone summary; delete all milestone branches.
- Update `REQUIREMENTS.md` (mark REQ-310..322 complete), `ROADMAP.md`
(mark v1.26 complete), `NORTH_STAR.md` (note Strategic Objectives #1
+ #3 — first real consumer estate; Post-Pilot denominators activated).
+ #3 — first real consumer estate; Post-Pilot denominators activated),
`STATE.md` (append v1.26 capability rows; bump "Last milestone ship").
> **Phase task-level breakdown, wave ordering, and persona
> assignments:** `.ciagent/PLAN.md` (the active phase plan, retained in
+291 -5
View File
@@ -11,8 +11,47 @@
> *why*, read `NORTH_STAR.md`. For *how*, read `ARCHITECTURE.md`. For
> *what was decided*, read `PROJECT.md` load-bearing decisions.
>
> **Last milestone ship:** v1.26 (`v1.25.5`, 2026-08-19).
> **Next update:** at v1.27 ship.
> **Last milestone ship:** v1.31 (`v1.30.2`, 2026-08-20) — Leadership
> Deck Polish II. Refinement-only NFR milestone: enriched the v1.30
> leadership deck's visible on-slide prose (slides 26: 4367 → 97168
> visible words) and improved slide layout (title → frame → body →
> diagram → closing italic benefit), then re-rendered the PPTX. All
> v1.30 invariants preserved (D-247): S&P theme tokens (`#D6002A`,
> `#1B1B1B`, `#FFFFFF`, `#F0F0F0`), 7-slide count, speaker-note word-
> count bands, `[1]` citations on slides 3/5/7, the 7 mermaid diagram
> PNGs, the footer string. No new slides, no new diagrams, no
> renderer changes; the citizen-developer deck untouched. Smoke test
> `scripts/check_leadership_deck.sh` PASS. 4 requirements (REQ-
> 373.1..4), 1 decision (D-247). Tags ran on the v1.30.x line:
> `v1.30.1` (P0) → `v1.30.2` (P1 = milestone release). 7 S&P-themed
> mermaid diagrams reused unchanged. 2 Gitea releases: 819 (P0),
> 820 (P1 milestone).
>
> **Previous milestone ship:** v1.29 (`v1.28.6`, 2026-08-20) — Reposplit +
> Identity Layer Bring-Live. Feature milestone: platform operations
> extracted to a Gitea-private Terraform repo (`nova-platform-ops`,
> OPER-PRIV); `acdl/acdl` standardized on GitHub (D-232, `.gitea/`
> removed, `forge_parity_disabled` CI assertion); Nova-idp brought
> live in `581513795199` via Terraform (CFN archived to
> `docs/archive/nova-idp-cfn-v1.28.md`, REQ-369, `nova idp setup
> --apply` delegates to `terraform apply`); `kj` substrate has one ECR
> image digest shared by the Lambda runtime + its Fargate fallback
> (KJ-LOCKSTEP, REQ-371, D-238, `lifecycle.precondition` on both
> image-bearing resources); JWKS edge-only via CloudFront + OAC
> (INV-18, D-233); `publish.yml` tag-triggered with ECR image build
> (static `kj`, `CGO_ENABLED=0`, KJ-STATIC `file(1)` gate, REQ-354);
> operator guide `docs/operator-guide-platform-ops.md` (747 lines, 18
> sections + Cutover Gates table); consumer `nova-blockchain-exchange`
> deploy.yml bumped `@v1.25` → `@v1.29`. 6 acdl-side REQs complete + 14
> covered-reference REQs (355-366, 371, verified via M1/M1.5/M2 cutover
> gates, operator-attested). 3 new capabilities (CAP-039..041), 1 new
> invariant (INV-18), 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-240). Grill PROCEED 0.72 (4 critical fixes). Review
> PASS-WITH-ISSUES (3 P0 fixes). Audit all PASS. Merged to main
> `9dc5669`, pushed + 8 Gitea releases created (ids 803-810).
> **Next update:** at v1.30 ship.
## How to use this file (PO)
@@ -78,6 +117,53 @@
path deferred (D-126). Multi-cloud deferred. Multi-validator BFT
deferred. The pilot runs `mode: full` for `dev` only (D-209); qa/prod/dr
stay placeholder (D-208, blocked by the pilot-readiness policy).
- **INV-12 (Mode observability, v1.28):** Every CLI invocation emits a
`cli.invocation` audit event containing `mode`, `selection_reason`,
`credential_type`, `command`, and `args`. Operators can debug mode
selection without reproducing.
- **INV-13 (Mode resolution determinism, v1.28):** Resolution priority
is flag → env (`NOVA_CLIENT_MODE`) → credential type →
`sys.stdin.isatty()`. No silent fallbacks. Invalid env values are
ignored + warned. Deviations rejected at PR time.
- **INV-14 (Credential type encodes role, v1.28):** `developer_pat` /
`nova_oidc_token` + TTY present → `interactive`; TTY absent → `agent`.
- **INV-15 (No AWS-managed identity in path, v1.28):** Nova-idp MUST
NOT depend on Cognito, IAM Identity Center, or any AWS-managed
identity service. Greenfield constraint (no Cognito existed to
"drop").
- **INV-16 (Password storage, v1.28):** Passwords hashed with Argon2id
(t=3, m=65536 KiB, p=1). Fail-closed on `ImportError` (D-228 amended
— no pure-Python fallback). Raw passwords never in logs/traces/env/
DynamoDB.
- **INV-17 (ABAC discipline, v1.28):** The token-vend Lambda evaluates
the kyverno-json ABAC policy before signing. Fail-closed on `kj`
absence or evaluation error (C-6.1 — never fail open). Allow/deny +
policy inputs emitted to the audit stream. `policy_version` (git SHA,
D-231) recorded in every event.
- **INV-18 (JWKS-EDGE-ONLY, v1.29):** the JWKS endpoint is the only
public read surface of the live platform. All other platform
endpoints MUST gate with `AuthType: AWS_IAM` (D-233). CloudFront +
OAC pinning replaces direct Lambda Function URL exposure. Direct
Function URL → 403; via-CloudFront → 200.
> **v1.29 NFR constraints (10 — load-bearing, not full invariants):**
> KJ-STATIC (`kj` compiled `CGO_ENABLED=0`, `file(1)` reports
> `statically linked`, SHA-256 in Terraform state); KJ-LOCKSTEP
> (Fargate standby digest == Lambda `image_uri` digest at every
> `terraform plan`, enforced by `lifecycle.precondition` + CI + PR
> comment + operator review, D-238); KJ-WARMUP-HEALTH (Fargate
> `GET /health → 200` every 10s, READY before M1 cutover);
> OPER-PRIV (`nova-platform-ops` `private: true`, not mirrored,
> REQ-359); IAM-NARROW (Gitea OIDC role bounded, no `Action: "*"` or
> `Resource: "*"`, REQ-360); DRIFT-DETECT (`terraform plan` exit 2
> fails the apply workflow, REQ-356); IMPORT-IDEMPOTENT (re-import
> exits `resource_already_imported`, REQ-361); TFM-HITL (`terraform
> apply` against `main` requires Gitea Actions approval from a user
> distinct from the PR author, REQ-357, INV-3); JWKS-SLO
> (`GET /.well-known/jwks.json` P95 < 200ms same-region,
> `Cache-Control: max-age=3600`); JWKS-ROTATION (on key rotation,
> both old + new public keys published during 24-hour overlap
> window).
## Domains (capability groups)
@@ -91,6 +177,8 @@
8. Consumer surfaces (developer + agentic)
9. Pilot estate (v1.26)
10. Forge / CI runtime
11. CLI + Identity Layer (v1.28)
12. Platform Ops Reposplit (v1.29)
## Capabilities (additive — one row per shipped capability)
@@ -228,9 +316,9 @@
| CAP-028 | T+1 settlement service | v1.26 / `v1.25.1` | `settlement/service.py` | REQ-312 | local | idempotent; finality = block commit |
| CAP-029 | Consumer `contract.yaml` (blockchain exchange) | v1.26 / `v1.25.2` | `nova-blockchain-exchange/contract.yaml`, `contracts/*.yml` | REQ-313 | local | per-env variants (dev/qa/prod); validated against contract schema |
| CAP-030 | Consumer deploy via `deploy.yml@v1.25` (inline adapter) | v1.26 / `v1.25.2` | `nova-blockchain-exchange/.github/workflows/deploy.yml`, `.gitea/workflows/deploy.yml` | REQ-314 | local | no cross-repo `uses:` (SPEC §10 Q1); checkout `acdl/acdl @ v1.25` into `platform/`, run `run_platform.sh` |
| CAP-031 | Live pilot apply (CAP-025 round-trip) | v1.26 / `v1.25.4` | `core/regression_verify.py` | REQ-316 | live-aws | contract→adapter→plan→policy→confidence→attestation→outbox against `581513795199` |
| CAP-025 | Live-pilot-apply regression capability (round-trip) | v1.26 / `v1.25.3` | `core/regression_verify.py` | REQ-316 | local | contract→adapter→plan→policy→confidence→attestation→outbox round-trip assertion |
| CAP-031 | Live pilot apply evidence (`blkex-pilot-apply-v0.2`) | v1.26 / `v1.25.4` | `.ciagent/archive/P4-PILOT-RUN-EVIDENCE-v1.26.md` | REQ-316, REQ-321 | live-aws | confidence 0.800 pass; outcome backfilled; hash chain valid; live apply against `581513795199` |
| CAP-032 | AWS key rotation scheduled workflow | v1.26 / `v1.25.3` | `workflows-src/rotate-aws-key.yml` | SPEC §5.9 | local | daily rotation; forge-agnostic token name (REQ-230) |
| — | Live pilot run evidence (`blkex-pilot-apply-v0.2`) | v1.26 / `v1.25.4` | `.ciagent/archive/P4-PILOT-RUN-EVIDENCE-v1.26.md` | REQ-316, REQ-321 | live-aws | confidence 0.800 pass; outcome backfilled; hash chain valid |
### Domain 10 — Forge / CI runtime
@@ -246,6 +334,47 @@
| — | IAM policy baseline (`acdl-spike-runner-policy`) | v1.11 / `v1.11.0` | `terraform/bootstrap/spike_runner_policy.json`, `.ciagent/IAM_POLICY.md` | REQ-116, D-095 | live-aws | regression-tested by `tests/test_iam_policy_baseline.py`; OIDC role `acdl-act-runner-role` (CAP-022) |
| — | Local emulating adapters (no AWS) | v1.10 / `v1.10.0` | `core/local_lambda_stub.py`, `scripts/run_local_e2e.sh` | D-092 | local | proves runtime behavior without live AWS |
### Domain 11 — CLI + Identity Layer (v1.28)
| ID | Capability | Shipped | Files | Controlling | Tier | Notes |
|----|-----------|---------|-------|-------------|------|-------|
| CAP-033 | CLI subcommand surface exists | v1.28 / `v1.27.1` | `nova/cli.py`, `nova/<module>.py` (15 subcommands) | REQ-324 | local | `nova --help` lists a subcommand for every `core/` module; argparse-only, auto-discovered |
| CAP-034 | Subcommand delegates to `core/` | v1.28 / `v1.27.1` | `nova/<module>.py` | REQ-324 | local | ≤50 lines, ≤3 FunctionDef, all calls resolve to `core.*` imports; AST-scanned in `tests/test_cli_subcommands.py` |
| CAP-035 | Layer matches wheel | v1.28 / `v1.27.1` | `.github/workflows/publish.yml`, `.gitea/workflows/publish.yml`, SSM `/nova/layer/nova-cli/version` | REQ-323 | local | wheel + Lambda layer co-published with identical version; SSM mapping; CodeArtifact + fallback |
| CAP-036 | Nova-idp auth flow works | v1.28 / `v1.27.3` | `core/lambda/nova_idp_auth.py`, `tests/test_idp_auth.py` | REQ-333 | local | sign-up → sign-in → session E2E; Argon2id t=3 m=65536 p=1; fail-closed D-228; moto locally, real DDB in CI |
| CAP-037 | Token-vend signs via KMS | v1.28 / `v1.27.4` | `core/lambda/nova_idp_token_vend.py`, `core/kms_signing.py`, `tests/test_kms_roundtrip.py` | REQ-337 | local | ECDSA P-256 / ES256; DER→raw conversion; KMS round-trip test; mock KMS locally, real KMS in CI |
| CAP-038 | PAT issuance + revocation | v1.28 / `v1.27.4` | `core/pat_lifecycle.py`, `tests/test_pat_revocation.py` | REQ-342 | local | issue → vend → revoke → 403 within 60s P95; strong-read DDB (D-229); verified <1s locally |
| — | `nova init` scaffolds `.nova/` | v1.28 / `v1.27.1` | `nova/init.py`, `core/init_scaffold.py` | REQ-325 | local | `.nova/`, `.nova/contract.yml.attestations/`, `.gitignore` (secrets excluded) |
| — | `nova cli-action` composite action | v1.28 / `v1.27.1` | `.github/actions/nova-cli/action.yml` | REQ-326 | local | byte-identical GitHub + Gitea; Python 3.12 pinned; NFR-11 |
| — | `mode_resolver` (flag→env→cred→TTY) | v1.28 / `v1.27.1` | `core/mode_resolver.py` | REQ-327, D-226 | local | `sys.stdin.isatty()` (not stdout); hypothesis property tests |
| — | Dual-use Lambda/CLI import | v1.28 / `v1.27.2` | `core/lambda/contract_ingestor.py` | REQ-329 | local | shared `dispatch_action()`; ≥80% code share; NFR-7 |
| — | Local env synthesizer | v1.28 / `v1.27.2` | `core/env.py` (`synthesize_local_env`) | REQ-330 | local | `nova apply --local`; no cloud provisioning |
| — | JWS-from-PAT (HKDF-SHA256, HS256) | v1.28 / `v1.27.2` | `core/jws_attestation.py` | REQ-332, C-5.2 | local | symmetric; verification key derived from PAT via same KDF |
| — | JWKS endpoint (function URL) | v1.28 / `v1.27.4` | `core/lambda/nova_idp_jwks.py` | REQ-338, D-230 | local | `AuthType: NONE`; `Cache-Control: max-age=3600`; optional CloudFront/WAF |
| — | kyverno-json ABAC token-vend policy | v1.28 / `v1.27.4` | `platform/abac/token-vend.policy`, `core/abac_evaluator.py` | REQ-339, D-227 | local | fail-closed (C-6.1, 7 tests); `policy_version` git SHA (D-231) |
| — | `nova idp setup --check/--apply/--verify` | v1.28 / `v1.27.4` | `nova/idp/setup.py`, `core/lambda/nova_idp_setup.py`, `core/lambda/nova_idp_cfn.py` | REQ-340, REQ-341 | local | CloudFormation template review (NFR-10); IAM policy delta; KMS round-trip verify |
| — | `nova auth login/revoke/status` | v1.28 / `v1.27.4` | `nova/auth/{login,revoke,status}.py`, `core/auth_store.py` | REQ-344, C-7.3 | local | `~/.nova/credentials.json` 0600 stores OIDC token + metadata (NOT raw PAT) |
| — | E2E integration test | v1.28 / `v1.27.5` | `tests/test_e2e_idp.py` | REQ-348 | local | sign-up → sign-in → token-vend → apply → audit chain |
| — | Identity-layer threat model | v1.28 / `v1.27.5` | `docs/threat-model.md` | REQ-347 | local | 8 threats + C-9.2 INV-18..21 compression audit |
| — | Operator guide | v1.28 / `v1.27.5` | `docs/operator-guide-idp.md` | REQ-345 | local | `nova idp setup` + KMS rotation + layer update + PITR restore + emergency PAT revocation |
| — | Developer guide | v1.28 / `v1.27.5` | `docs/developer-guide-auth.md` | REQ-346 | local | quickstart + mode resolution + JWS KDF + service-account PATs |
### Domain 12 — Platform Ops Reposplit (v1.29)
> Shipped at `v1.28.6` (2026-08-20). Covered-reference REQs (355-366,
> 371) are authored out-of-band in `nova-platform-ops`; their
> verification surface is the M1/M1.5/M2 cutover gates in the operator
> guide (grill CF-2/G-5). The live cutover is an operator action — the
> acdl-side deliverables (publish.yml, operator guide, CFN archive,
> consumer bump) are complete.
| ID | Capability | Shipped | Files | Controlling | Tier | Notes |
|----|-----------|---------|-------|-------------|------|-------|
| CAP-039 | Platform ops reposplit | v1.29 / `v1.28.6` | `nova-platform-ops` (out-of-band), `docs/operator-guide-platform-ops.md`, `docs/archive/nova-idp-cfn-v1.28.md` | REQ-369, REQ-OPS-GUIDE, D-232, D-235 | covered-reference | engineering (`acdl/acdl`, GitHub) ends at the artifact; operations (`nova-platform-ops`, Gitea-private, OPER-PRIV) begins at the live platform; tag-pin handoff; CFN archived; covered-reference REQs tracked via cutover gates |
| CAP-040 | KJ substrate lockstep | v1.29 / `v1.28.6` | `nova-platform-ops` (out-of-band), `platform/abac/kj-version.txt`, `.github/workflows/publish.yml` | REQ-371, REQ-363, REQ-363b, D-238, D-239 | covered-reference | one ECR image digest shared by Lambda `image_uri` + Fargate task `image`; `lifecycle.precondition` on both resources at `terraform plan`; KJ-STATIC (`CGO_ENABLED=0`, `file(1)` asserts `statically linked`); no second pipeline, no second SHA pin |
| CAP-041 | JWKS edge-only | v1.29 / `v1.28.6` | `nova-platform-ops` (out-of-band), `docs/operator-guide-platform-ops.md` | REQ-364, REQ-365, REQ-366, INV-18, D-233 | covered-reference | JWKS is the only public read surface; CloudFront + OAC (`AuthType: AWS_IAM`, NOT `NONE`, `OriginAccessControlOriginType: lambda`, `SigningBehavior: always`); direct Function URL → 403, via-CloudFront → 200; WAF rate-limit 3000/5min + AWSManagedRulesCommonRuleSet; ACM DNS-validated in us-east-1; Route53 A-alias |
| CAP-042 | Leadership presentation deck (single-shot, polished) | v1.30 / `v1.29.5` | `docs/presentations/nova-leadership-deck-marp.md`, `docs/presentations/nova-leadership-deck.pptx`, `scripts/check_leadership_deck.sh`, `scripts/render_leadership_diagrams.sh`, `docs/presentations/assets/mmd/leadership-slide-{1..7}.mmd`, `docs/presentations/assets/png/leadership-slide-{1..7}.png` | REQ-372.1..REQ-372.12, D-241..D-246 | local | Single-shot 7-slide PPTX deck for Infrastructure & Operations leadership (CTO + VP Technology + Product Management); presented August 2026; November 2026 runway anchor; discrete hand-authored artifact (NOT a compression of the citizen-developer pitch per D-241); rendered via existing `scripts/render_pptx.py` (narrowly extended per D-242); polished in P3 (D-244 cover slide, D-245 slide 7 "What works now" block, D-246 7 S&P-themed mermaid diagrams); smoke test on-demand (NOT a CI gate); vision `[1]` grounding in slides 3/5/7 |
## Archive pointers
- **v1.0v1.24 capability narrative + the 2026-07-27 re-verification sweep:**
@@ -281,4 +410,161 @@ wave** (Wave 3 "milestone ship" in `PLAN.md`), alongside
Enforcement: convention (the P-final ship step names this file). A
drift-check gate (assert every REQ marked `complete` in
`REQUIREMENTS.md` traceability appears in STATE.md) is a future option
if the convention drifts.
if the convention drifts.
---
## PDLC Phase 0 Intake (current ground truth — 2026-08-20)
> Single-pass discovery for the next PDLC cycle. Populated from the
> live repo state after v1.29 ship. No aspirational items — state is
> what is, not what should be. Unknowns are explicit.
---
### 1. Header (mandatory)
Project: Nova — The New Dawn of DevSecOps
Initiative: Leadership Presentation Deck — compressed (≤7 slides, S&P theme, 18-month CDLC→SDLC→PDLC roadmap)
Initiator: Product Owner / Manager (PDLC Phase 0 trigger)
Date (UTC): 2026-08-20
Current Version: v1.29 complete (tag `v1.28.6`, merged to main + pushed + released 2026-08-20); all 7 phases shipped; no phase in progress
System Health: YELLOW — coverage 73.8% below 80% release-gate floor (NFR debt carried from v1.28, unchanged through v1.29 feature milestone); nova-platform-ops M1 cutover pending operator action (covered-reference REQs 355-366, 371 not yet live-verified)
Raw Idea (≤ 3 sentences):
Technology Leadership needs a compressed presentation deck (≤7 slides, S&P theme colors) communicating: the problem statement, who the target audience is, what the platform is + how it solves the problem, what works now, and an 18-month roadmap from CDLC to SDLC + PDLC integration. Leaders do not want long presentations — the existing 23-slide deck is too verbose for this audience.
Trigger: post-v1.29 milestone completion — the platform has shipped reposplit + identity layer bring-live + the operator guide, making the story ready for leadership consumption.
Desired outcome: a leadership-ready deck (≤7 slides, Marp + python-pptx, S&P theme `#D6002A` / `#1B1B1B` / `#FFFFFF` / `#F0F0F0`) that secures buy-in for the 18-month integration roadmap (CDLC → SDLC → PDLC).
---
### 2. Architecture State
Active Layers (which exist and are stable):
[x] Core Primitives — `core/` (26 top-level modules + `core/lambda/` (8) + `core/metrics/` (10)): `abac_evaluator`, `attestation_matrix`, `auth_store`, `confidence_signal`, `contract_resolver`, `decommission_transform`, `env`, `env_transition`, `environment_check`, `hitl_gates`, `init_scaffold`, `jws_attestation`, `kms_signing`, `local_emulators`, `mode_resolver`, `onboarding`, `outbox_writer`, `output_publisher`, `pat_lifecycle`, `policy_engine`, `regression_verify`, `separation_of_duties`, `submission_readiness` + `core/lambda/` (6 modules) + `core/metrics/` (decision ledger)
[x] Domain Modules — `adapters/terraform/` (stateless adapter), `adapters/kyverno-json/` (unified policy engine, INV-4 swappable), `adapters/wiz/`, `adapters/kyverno/` (K8s, inactive for Terraform — D-053)
[x] API/Dev Surface — `nova/` CLI package (15 subcommands, argparse-only, `[project.scripts] nova = "nova.cli:main"`); `nova auth {login,revoke,status}`; `nova idp setup` (delegates to `terraform apply` per REQ-369); `nova init`; `nova apply --local`; `nova cli-action` composite action (GitHub only — D-232)
[x] UI/Agent Surface — N/A (no UI; CLI + JSON endpoints only; JWKS serves `application/json`)
Compute Topology (per environment):
local: abstract (local emulators via `core/local_emulators.py:LocalLambdaStub`; `nova apply --local` synthesizes env via `core/env.synthesize_local_env()`; no cloud provisioning)
dev: abstract (env JSON `core/environments/dev.json`; pilot ran `mode: full` against live AWS `581513795199` at v1.26; Nova-idp live deployment is via `nova-platform-ops` Terraform — M1 cutover pending operator action, covered-reference)
staging: N/A (no `staging` environment JSON; environments are dev/qa/prod/dr)
prod: UNKNOWN — needs investigation (env JSON `core/environments/prod.json` exists; live-apply not run against prod; pilot was dev-only per D-209)
dr: placeholder (env JSON `core/environments/dr.json` exists; blocked by pilot-readiness policy D-208; not activated)
Identity Stack in Force:
auth: Custom IDP — Nova-idp (`nova-idp-auth` Lambda, v1.28): sign-up/sign-in/session; Argon2id (t=3, m=65536, p=1); DynamoDB `nova-users`/`nova-sessions`/`nova-password-resets`. Covered-reference — live deployment via `nova-platform-ops` Terraform (M1 cutover pending operator action).
token-vend: Nova-idp (`nova-idp-token-vend` Lambda, v1.28, deployed on container image with static `kj` per REQ-363): accepts PAT/session → KMS-signed OIDC token (ECDSA P-256 / ES256); kyverno-json ABAC fail-closed (INV-17, C-6.1); `nova-pats` DynamoDB (strong-read revocation, D-229, 60s SLO). Covered-reference — M1.5 verification gate pending (3 consecutive green rebuilds).
signing: KMS asymmetric — `alias/nova-oidc-signing` (ECC_NIST_P256, SIGN_VERIFY, 90-day rotation, D-234). Code complete; key provisioning is covered-reference (REQ-362, M1 cutover pending operator action in `nova-platform-ops`).
session: DynamoDB — `nova-sessions` table (PK `session_id`, TTL `expires_at`, 24h). Cookie/local-file: `~/.nova/credentials.json` (0600, OIDC token + PAT metadata, NOT raw PAT — C-7.3).
Audit Stream:
source of truth: DynamoDB outbox → S3 Object Lock (7-yr target, D-083 deferred) → audit repo (hot index). The Decision Ledger (SQLite hash-chain, D-121, `core/metrics/decision_ledger.py`) is the cold store for `ai.decision.made` + `attestation.recorded` events.
in-repo fallback: yes (SQLite hash-chain outbox_writer, `core/outbox_writer.py`, INV-6 tamper-evident; tamper-*resistant* deferred — D-083 S3 Object Lock/JWS not yet enabled)
retention policy: 7 years (S3 Object Lock target; not yet enabled — D-083 deferred)
---
### 3. Technical Stack (concrete, not aspirational)
Language(s) and runtime(s): Python 3.12 (requires-python `>=3.12`; Lambda Python 3.12 runtime on Amazon Linux 2023); Go (kj binary, `CGO_ENABLED=0`, pinned v0.0.3 from `github.com/kyverno/kyverno-json`)
Build / packaging: setuptools (`pyproject.toml` v1.29.0, build-backend `setuptools.build_meta`); wheel via `python -m build --wheel`; Lambda layer via `pip install --target layer/python/` + `zip`; Lambda zip (`nova-lambda-token-vend-v1.29.x.zip`); ECR container image (`public.ecr.aws/lambda/python:3.12-al2023` base + static `kj` binary at `/opt/kj/kj`); publish to GitHub Releases per tag (D-232 — CodeArtifact out, direct GitHub Releases artifact fetch)
CI / CD: GitHub Actions only (D-232 — `.gitea/` removed, `forge_parity_disabled` CI assertion in `ci.yml`); `publish.yml` (tag-triggered `v1.29.*`, wheel + layer + Lambda zip + ECR image + GitHub Release, REQ-354); `ci.yml` (test/lint/forge-parity-disabled); `deploy.yml@v1.29` (consumer deploy); `nova cli-action` composite action (`.github/actions/nova-cli/action.yml`); OIDC to AWS (`id-token: write`); `nova-platform-ops` uses Gitea Actions (out-of-band, OPER-PRIV, TFM-HITL)
Infrastructure: AWS account `581513795199` (single-region `us-east-1`); S3 (state files); DynamoDB (locking + outbox + identity tables); Lambda (contract ingestor + Nova-idp 3 Lambdas on container images); KMS (per-stack CMK + `alias/nova-oidc-signing`); ECR (kj container image); CloudFront/WAF/ACM/Route53 (JWKS edge, covered-reference); Fargate (standby defensive fallback, covered-reference); no VMs/bare metal/OS (Anti-Goal)
Data stores: DynamoDB — `nova-contracts`, `nova-change-requests` (v1.7); `nova-users`, `nova-sessions`, `nova-password-resets`, `nova-pats` (v1.28); SQLite — Decision Ledger (`core/metrics/decision_ledger.py`, local cold store); S3 — Terraform state + audit Object Lock (target, D-083 deferred); ECR — kj container image
Secrets / KMS: KMS per-stack CMK (D-069, 90-day rotation); `alias/nova-oidc-signing` (ECC_NIST_P256, 90-day rotation, D-234 — covered-reference, M1 cutover pending); `nova-spike-runner` IAM user (static key, daily rotation via `workflows-src/rotate-aws-key.yml`, REQ-230 forge-agnostic); Secrets Manager (`nova/github-token`); `NOVA_FORGE_TOKEN` in `.env.secrets` (not shell-env, per bash_allowlist; used for `nova-platform-ops` Gitea releases)
External integrations in scope: GitHub (`acdl/acdl` — primary forge, D-232); Gitea (`git.cloudinit.dev/continuous-intelligence/nova-platform-ops` — ops repo, OPER-PRIV, out-of-band); AWS (account `581513795199` — pilot + identity stack + ECR); `kj` / kyverno-json v0.0.3 (Go binary, pinned SHA + repo URL in `platform/abac/kj-version.txt`, `github.com/kyverno/kyverno-json`); Marp CLI 4.5.0 + python-pptx (slides render pipeline, `docs/presentations/`)
---
### 4. Active Constraints (the load-bearing ones)
Locked Decisions: D-001..D-240 (full ledger in PROJECT.md + CLARIFY history). Load-bearing for new work: D-022 (contract schema), D-039/D-047 (per-run creds), D-051 (Lambda Function URL), D-069 (per-stack CMK), D-083 (S3 Object Lock — deferred), D-092 (local emulators), D-096 (live pilot — lifted v1.26), D-121 (Decision Ledger), D-133 (submission-readiness gate), D-200..D-213 (v1.26 pilot), D-214..D-225 (v1.27), D-226..D-231 (v1.28 — mode resolution, kyverno-json ABAC, Argon2id fail-closed, PAT revocation strong-read, JWKS function URL, ABAC policy git-SHA versioning), D-232..D-238 (v1.29 — forge parity abandoned, JWKS edge-only, KMS asymmetric, tag-pin handoff, cutover shape, Fargate sunset, KJ-LOCKSTEP), D-239 (ECR tag format), D-240 (Terraform precondition floor v1.2.0)
Active Invariants: INV-1..INV-18 (full text above). New in v1.28: INV-12 (mode observability), INV-13 (mode determinism), INV-14 (credential type encodes role), INV-15 (no AWS-managed identity), INV-16 (Argon2id password storage), INV-17 (ABAC discipline fail-closed). New in v1.29: 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)
Standing Capability Gate: CAP-001..CAP-042 — all Verified (32 from v1.0..v1.27 + 6 from v1.28 + 3 from v1.29 covered-reference + 1 from v1.30 single-shot deck). Gate enforced by `core/regression_verify.py` + CI merge gates. CAP-033..038 added v1.28 (CLI surface, delegation AST, layer/wheel match, auth flow, KMS sign, PAT revocation). CAP-039..041 added v1.29 (platform-ops-reposplit, kj-substrate-lockstep, jwks-edge-only — covered-reference, live cutover pending operator action in nova-platform-ops). CAP-042 added v1.30 (leadership-deck — single-shot, on-demand smoke test, NOT a CI gate).
Anti-Goals Touched: `docs/vision.md` §7 / `NORTH_STAR.md` §Anti-Goals — (1) not an upstream dev platform; (2) not a general-purpose AI; (3) not a legacy infra bridge; (4) not a permissive delivery highway; (5) not a mutable audit log. v1.29 honored all 5 (no PDLC reach, narrow CLI autonomy, no VMs, ABAC fail-closed + HITL gates intact, immutable outbox).
Out-of-Scope (hard): MFA/TOTP enforcement (v1.21+); WebAuthn/FIDO2 (v1.23+); upstream IdP federation (v1.23+); Lambda layer auto-update on `core/` changes (v1.19); password breach detection (v1.23+); session refresh token rotation (v1.22); S3 Object Lock / JWS tamper-resistance (D-083, deferred); multi-cloud (Azure/GCP); ML forecasting; bonds/derivatives/options (D-200 equities-only); multi-validator BFT (D-201 single-validator PoA); pilot qa/prod/dr environment activation (D-208/D-209, separate initiative); CodeArtifact provisioning (out per D-232 — direct GitHub Releases artifact fetch); Nova-idp feature work (new OIDC claims, new ABAC rules — bring live, don't extend); CloudFront Frontend / L3B consumer surface (pure ops focus only)
---
### 5. Recent History & Quality Gates (last 1-2 milestones)
Last Shipped: v1.29 (tag `v1.28.6`, 2026-08-20) — Reposplit + Identity Layer Bring-Live. 6 acdl-side REQs complete (REQ-354, 367, 368, 369, REQ-OPS-GUIDE, REQ-CONSUMER-BUMP) + 14 covered-reference REQs (355-366, 371). 3 CAPs (CAP-039..041), 1 INV (INV-18), 10 NFR constraints, 9 decisions (D-232..240). 7 phases (P0 + P1..P5 + P6 final). Grill PROCEED 0.72 (4 critical fixes). Review PASS-WITH-ISSUES (3 P0 fixes). Audit all PASS. Merged to main `9dc5669`, pushed + 8 Gitea releases created (ids 803-810).
In Progress: N/A (no phase in progress; v1.29 complete; next milestone not yet scoped — this intake initiates the leadership deck initiative)
Coverage Floor: 73.8% (3119/4227 lines covered) — BELOW the 80% release-gate floor. v1.29 was a feature milestone (no NFR coverage work); v1.28 new modules have high unit-test coverage but the overall floor is dragged by older uncovered code paths. Quality debt to address in a future NFR milestone. YELLOW carried without scope expansion.
Recent Incidents: none (no incidents in v1.28 or v1.29; no hotfix/rollback/outage commits in recent history)
Known Tensions: (1) nova-platform-ops repo not yet created — the 14 covered-reference REQs (355-366, 371) have their acdl-side deliverables complete (operator guide, publish.yml, CFN archive) but the live M1/M1.5/M2 cutover gates have not been run (operator action, out-of-band). (2) Coverage 73.8% < 80% floor — YELLOW carried from v1.28; v1.29 did not expand scope but did not restore the floor. (3) M-001 (ABAC empty-policy-dir fail-open gap) — pinned in `test_abac_e2e.py`, mitigated; clear fix exists (treat `any_policy=False` as fail-closed) but not yet applied. (4) Q7 carry-forward (kj image verification — M1.5 3-consecutive-rebuild gate is operator action in nova-platform-ops CI, not acdl-side). (5) The existing 23-slide deck (`docs/presentations/nova-autonomous-cloud-delivery-marp.md`) is too long for the leadership audience (target ≤7 slides).
---
### 6. Agent Context & Assumptions (Agent Initiators Only)
Missing Context: (1) The 18-month roadmap specifics — NORTH_STAR.md §Future Horizons has the strategic direction (CDLC→SDLC→PDLC integration, AI-Agent Intent Share ≥40%) but the PO needs to define the concrete milestone sequence for the deck. (2) Target audience specifics — "Technology Leadership" is the stated audience but the deck needs to know if this is CTO-level, VP-level, or Director-level (affects depth + framing). (3) Live AWS verification of covered-reference REQs — nova-platform-ops not yet created; M1/M1.5/M2 cutover gates not yet run.
Agent Assumptions: (1) The PDLC trigger is the post-v1.29 state intake + the PO's new initiative (leadership deck). (2) The deck uses the existing S&P theme (`docs/presentations/assets/nova-sp-theme.css`, palette `#D6002A`/`#1B1B1B`/`#FFFFFF`/`#F0F0F0`) + the existing Marp + python-pptx render pipeline (`workflows-src/slides.yml`, `scripts/render_pptx.py`). (3) **OVERRIDDEN by D-241 (v1.30 CLARIFY):** the leadership deck is a **discrete, hand-authored artifact — NOT a compression** of the 23-slide existing citizen-developer deck (`nova-autonomous-cloud-delivery-marp.md`), which remains untouched. The Slide Content Map in PROJECT.md §v1.30 is hand-authored content, not derived. (4) Coverage 73.8% is reported as YELLOW system health (below 80% floor) but is not a blocker for the deck initiative — it's quality debt for a future NFR milestone. (5) The covered-reference REQs are reported as tensions, not blockers — they have acdl-side deliverables complete + documented cutover gates.
---
### 7. Canonical State References (Version/Hash)
Vision/Strategy doc: `docs/vision.md` v0.2 (referenced in PROJECT.md; not version-tagged separately)
Architecture document: `.ciagent/ARCHITECTURE.md` §12.1..§12.11 (v1.29-appended §12.11 Platform Ops Reposplit); commit `9dc5669` (main HEAD)
Last approved SPEC: v1.29 (REQ-354..369, 371, 363b, REQUIREMENTS.md §v1.29); commit `9dc5669`
Decision log: D-001..D-240 (PROJECT.md load-bearing + CLARIFY.md history); last synced commit `9dc5669`
Invariants catalog: INV-1..INV-18 (STATE.md §Invariants); commit `9dc5669`
Capability catalog: CAP-001..CAP-041 (STATE.md §Domains 1..12); commit `9dc5669`
---
### Ground rules compliance
1. No prose paragraphs inside sections — field structure used throughout. ✓
2. No aspirational items — state is what is (nova-platform-ops "not yet created", prod "UNKNOWN", coverage "73.8%"). ✓
3. No restated decisions — referenced D-*/INV-*/CAP-* IDs only. ✓
4. Unknowns explicit — "UNKNOWN — needs investigation" used for prod state; "covered-reference" used for live-unverified REQs. ✓
5. One file, one format — appended to STATE.md as §PDLC Phase 0 Intake. ✓
6. Full shipping workflow + merge to forge upstream, NO release — commit to main + push only (release skipped per instruction). ✓
+323
View File
@@ -0,0 +1,323 @@
# CLARIFY — v1.30 Single-shot Leadership Deck
> **Autonomy:** full. Auto-resolution with assumption logging per
> `config.autonomy.level: "full"`. No human escalation unless
> confidence < 0.60. The user confirmed the 4 framing decisions
> (milestone numbering, render pipeline path, stale intake
> assumption override, smoke test PPTX gate) in the pre-run planning
> conversation. This file records the formal D-IDs and the spec §7
> open-question resolutions.
---
## Method
The clarify stage identifies ambiguities in the v1.30 specification
(REQ-372 v1.0, locked 2026-08-20) and resolves them at full autonomy.
The spec is the user-provided "REQ-372 — Nova Leadership Presentation
Deck." Each ambiguity gets a decision ID (D-241+, continuing from
v1.29's D-232..D-240), a resolution, a confidence score, and a
rationale.
---
## Prior-conversation resolutions (already locked, restated for the record)
These were resolved by the user in the pre-run planning conversation
that spawned v1.30. They are load-bearing for v1.30 execution.
### Q-M1 — The cover note/spec say "v1.29.x" but the checkpoint says v1.29 is complete and active_milestone is v1.30. What is the milestone number?
**Resolution:** The milestone is **v1.30**. The cover note's "v1.29.x"
is the **tag line** (per run.md branch strategy, tags run on the
previous minor's patch line: milestone v1.30 → tags v1.29.1,
v1.29.2, v1.29.3). The milestone branch is
`milestone/v1.30-leadership-deck`. v1.29 is complete (merged to main
`9dc5669`, tag `v1.28.6`).
**Confidence:** 1.0 (user-confirmed — "Milestone v1.30, tags v1.29.x").
**Decision:** n/a (milestone identity, not a D-ID).
### Q-M2 — The cover note says `scripts/render_pptx.py docs/presentations/nova-leadership-deck.md` but render_pptx.py expects `{deck}-marp.md` naming. How to resolve?
**Resolution:** Author the source as
`docs/presentations/nova-leadership-deck-marp.md` to fit the existing
`-marp.md` pipeline convention. Narrowly extend `render_pptx.py` to
accept an explicit source `.md` path + `--output` filename, and to
render a right-aligned footer textbox on every slide (python-pptx
does not read the Marp `footer:` directive). The output is
`nova-leadership-deck.pptx` per spec REQ-372.2. Formalized as D-242.
**Confidence:** 1.0 (user-confirmed — "Author source as
nova-leadership-deck-marp.md, extend render_pptx.py").
**Decision:** D-242.
### Q-M3 — The post-v1.29 STATE.md intake (assumption 3) says the leadership deck "is a compression, not a rewrite" of the 23-slide citizen-developer deck. The cover note + spec explicitly forbid compression. How to handle?
**Resolution:** Override the stale intake assumption. The leadership
deck is a **discrete, hand-authored artifact** — NOT a compression.
The existing citizen-developer deck
(`nova-autonomous-cloud-delivery-marp.md`) remains untouched. The
spec §2.2 + cover note forbid compression/mirroring; the Slide
Content Map is hand-authored content, not derived. Update STATE.md
intake assumption 3 to reflect the discrete-artifact decision.
Formalized as D-241.
**Confidence:** 1.0 (user-confirmed — "Override with spec's
discrete-artifact decision").
**Decision:** D-241.
### Q-M4 — The smoke test (REQ-372.8f) must assert PPTX file existence. Given the render environment limitations, should the PPTX-existence check be a hard fail or a conditional skip?
**Resolution:** **Hard fail** if `.pptx` absent. The deck must be
rendered before ship. The render environment is resolved (python-pptx
installed via user-site `pip install --user --break-system-packages`;
no Chromium needed since python-pptx is the render path, not Marp
CLI). If the environment cannot render, that is a ship blocker to
resolve — not a reason to weaken the gate.
**Confidence:** 1.0 (user-confirmed — "Hard fail if .pptx absent").
**Decision:** n/a (gate severity, not a D-ID — recorded in PLAN.md).
---
## Open questions from the spec's §7 (auto-resolved at full autonomy)
### Q1 — Specific meeting date inside August 2026
**Spec context:** The presentation is in August 2026, but no specific
day is named. Slide 7 references "Infrastructure & Operations
leadership" without naming a day.
**Resolution:** Anchor to **month-only** (August 2026). No specific
day in the deck text. November 2026 is the runway anchor (~90 days
from August 2026).
**Confidence:** 0.95. **Impact if wrong:** Very low — the meeting is
what it is; the deck text doesn't depend on a specific day.
**Decision:** D-243 (date anchor discipline: month-only).
### Q2 — Explicit non-compression of the existing citizen-developer deck
**Spec context:** The two decks (leadership + citizen-developer)
remain discrete artifacts. The existing 23-slide
`nova-autonomous-cloud-delivery-marp.md` is not compressed or
modified.
**Resolution:** Document the discrete-artifact constraint in
REQ-372.9 (related-artifacts header comment) + D-241 (this file) +
D-241 record in PROJECT.md at ship (REQ-372.11). Leave the existing
citizen-developer deck untouched. The cover note's hard scope rules
("Do not modify `nova-autonomous-cloud-delivery-marp.md`") are
binding.
**Confidence:** 1.0. **Impact if wrong:** None for this milestone.
**Decision:** D-241 (restated — the discrete-artifact decision is the
same as Q-M3's override).
### Q3 — Assumption: existing `scripts/render_pptx.py` accepts S&P theme directives and Marp speaker notes without modification
**Spec context:** The render pipeline is existing; the spec assumes
it works for the new deck. If a theme limitation forces a renderer
change, scope narrowly and update `render_pptx.py` separately as a
non-REQ-372 task.
**Resolution (confirmed by research):** The existing `render_pptx.py`
has two gaps for REQ-372: (a) it expects a `{deck}` arg and reads
`{deck}-marp.md` / writes `{deck}-python.pptx` — it does not accept
an explicit source path or custom output filename; (b) it does not
read the Marp `footer:` directive (it skips HTML comments at lines
366-379 and never adds a footer textbox). Speaker notes (HTML
comments) are skipped entirely — acceptable for REQ-372.4 (smoke test
checks source word counts, not PPTX-embedded notes). The narrow
extension per D-242 addresses (a) and (b). No other renderer change
is needed. The extension is a prerequisite, scoped separately from
REQ-372 per spec §3.3 Edge 2.
**Confidence:** 0.92. **Impact if wrong:** Small follow-up; doesn't
change milestone scope.
**Decision:** D-242 (restated).
### Q4 — Assumption: the 18-month runway shape (α–δ) is acceptable as drafted to I&O leadership
**Spec context:** Slides 6 + 7 rehearse both architecture-load and
political-cover framings. The worked-example granularity was
confirmed by the PO.
**Resolution:** Accept the α–δ shape as drafted. Slides 6 + 7 are the
only slide-by-slide revisions that might be needed if leadership
pushes back; everything else is locked. No spec change required
unless the architectural claim set shifts (spec §3.3 Edge 3).
**Confidence:** 0.85. **Impact if wrong:** Slide 6 and slide 7 are
the only revisions; everything else is locked.
**Decision:** n/a (acceptance, not a D-ID — the shape is in the
locked Slide Content Map).
---
## Decisions (locked, full autonomy — load-bearing for v1.30)
### D-241 — Leadership deck is discrete, hand-authored, NOT a compression
**Q-M3 / Q2.** The leadership deck is a **discrete, hand-authored
artifact** — NOT a compression of the existing 23-slide
citizen-developer pitch
(`nova-autonomous-cloud-delivery-marp.md`). This overrides the
post-v1.29 STATE.md intake assumption 3 ("is a compression, not a
rewrite"). The existing citizen-developer deck remains untouched.
The spec §2.2 + cover note forbid compression/mirroring; the Slide
Content Map is hand-authored content, not derived. Recorded in
PROJECT.md at ship (REQ-372.11).
**Confidence:** 1.0.
### D-242 — Narrow render_pptx.py extension (path arg + custom output + footer textbox)
**Q-M2 / Q3.** The existing `scripts/render_pptx.py` is narrowly
extended to: (a) accept an explicit source `.md` path + `--output`
filename (honouring the cover note's invocation pattern), and (b)
render a right-aligned footer textbox on every slide with the exact
string `Nova Platform - Infrastructure & Operations` (python-pptx
does not read the Marp `footer:` directive; REQ-372.5 requires the
footer on every rendered slide). The source is authored as
`nova-leadership-deck-marp.md` to fit the existing `-marp.md`
pipeline convention; the output is `nova-leadership-deck.pptx` per
spec REQ-372.2. This extension is a non-REQ-372 prerequisite per
spec §3.3 Edge 2 ("scope narrowly and update `render_pptx.py`
separately"). No other renderer change (speaker notes are not
embedded in the PPTX — acceptable; smoke test checks source word
counts).
**Confidence:** 0.92.
### D-243 — Date anchor discipline: month-only (August 2026 present, November 2026 runway)
**Q1.** August 2026 is a **month-only** presentation anchor (no
specific day). November 2026 is the runway anchor (~90 days from
August 2026). Slide 7 references "Infrastructure & Operations
leadership" without naming a specific day. No spec change required
unless the architectural claim set shifts (spec §3.3 Edge 3).
**Confidence:** 0.95.
---
## STATE.md intake assumption override
The post-v1.29 STATE.md intake (line ~526, Agent Assumptions, item 3)
states: "The 23-slide existing deck is the source material — the
≤7-slide leadership deck is a compression, not a rewrite."
**Override (D-241):** This assumption is **replaced**. The leadership
deck is a discrete, hand-authored artifact — NOT a compression. The
existing citizen-developer deck remains untouched. The override is
recorded in this CLARIFY.md (D-241) and will be reflected in STATE.md
at the v1.30 ship wave (CAP-042 row + intake assumption correction).
---
## Polish-phase decisions (D-244, D-245, D-246 — full autonomy)
The first draft (v1.29.3) passed all 12 REQs but the rendered PPTX
has 7 polish issues (vertical imbalance, text clipping, double bullet
markers, no visual variety, near-invisible footer, slide-6 red wall,
blockquote grey). The user requested: (1) polish all 7 issues, (2)
add a "What works now" emphasis to slide 7, (3) generate mermaid
diagrams for every slide. These decisions record the deviations from
prior binding decisions.
### D-244 — Slide 1 becomes a black-background cover (overrides grill G-1)
**Polish issue #4.** Grill G-1 locked "all 7 slides as white content
slides." The user approved making slide 1 a black-background cover
(strong opener for a live leadership presentation) while keeping
slides 2-7 as white content slides. The on-slide body text from the
Slide Content Map is preserved verbatim — only the visual treatment
(background + text color) changes. REQ-372.7's "deviation requires
CLARIFY" clause applies to *content* deviations; this is a
presentation polish, not a content change. The source change: slide
1's `## The friction...``# The friction...` (H1) + `<!-- _class:
title -->` directive, which triggers `render_title_slide` (black bg,
red top bar, white text).
**Confidence:** 1.0 (user-confirmed).
### D-245 — Slide 7 "What works now" content addition (deviation from Slide Content Map)
**User request.** A new "What works now" block is added to slide 7's
on-slide body, before the existing "What we ask" block. This is a
*content* deviation — the Slide Content Map specifies slide 7 as:
What we ask / Why now / What comes back + closer. The new block
emphasizes what's proven today (L1/L2 stack deployment works, live
apply to the sandbox/dev AWS account, confidence 0.800) and the
next-steps arc (ingest greenfield pilot projects → promote from
sandbox to production → integrate with the SPGE constitutional
library → serve as the infrastructure layer).
**Factual grounding (verified in RESEARCH):**
- 13 L1 primitives + 2 L2 modules in `modules/registry.json` (all at
`1.0.0`): alb, cloudfront, dynamodb, ecr, ecs-cluster, ecs-service,
iam-role, kms-key, rds, s3, uptime, vpc, waf + microservice, static-
assets.
- CAP-031: live `terraform apply` against AWS account `581513795199`
(the dev environment, `mode: full`, dev-only per D-209), producing
real ARNs (ALB, ECS, DynamoDB, S3, KMS) at confidence 0.800. This is
a **live AWS apply**, NOT "tested locally" and NOT a "sandbox"
account (the codebase has no sandbox account; the pilot-readiness
policy D-208 blocks placeholder accounts).
**"sandbox" colloquial term note:** The user explicitly chose to keep
"sandbox" as a colloquial term for the dev AWS account after I flagged
that the codebase fact is "dev account `581513795199`, live apply."
The slide uses "sandbox" (colloquial); the speaker notes carry the
precise facts (dev account, live apply, confidence 0.800).
**"SPGE constitutional library" unverified-in-repo note:** "SPGE"
and "constitutional library" appear nowhere in the codebase (0
matches across all `.md`/`.py`/`.json`/`.yml`). The user confirmed it
is an external system the audience recognizes. The slide includes it
as a named next-step integration target, not as a current capability.
The speaker notes do not assert it exists in the codebase.
The Slide Content Map in PROJECT.md is updated to reflect the new
slide 7 structure.
**Confidence:** 0.90.
### D-246 — Mermaid diagrams on all 7 slides
**User request.** Each of the 7 slides gets a leader-friendly,
non-technical mermaid diagram covering its core concept. Different
mermaid shapes per slide for visual variety.
**Render path (verified):** The local `mmdc` (mermaid-cli) is blocked
by missing Chromium shared libraries (10+ system libs absent, no
root). Resolution: 24 Debian bookworm `.deb` packages fetched from the
Debian mirror, `.so` files extracted to `/home/opencode/tmp/so2/`,
`LD_LIBRARY_PATH` set to include them. Chromium (puppeteer cache at
`/home/opencode/.cache/puppeteer/chrome/.../chrome`) launches with
the vendored libs; `mmdc` renders with the S&P theme (`sp-theme.json`)
+ 2x scale + transparent background. Verified: test diagram renders
to a valid PNG with all 4 S&P colors present. The `.mmd` source files
+ rendered PNGs are committed for reproducibility; a future
environment with system Chromium can re-render via the existing
`scripts/render_slides.sh` mermaid step.
**Theme:** Inline `%%{init: {...}}%%` directive in each `.mmd` with
S&P theme variables (`primaryColor:#1B1B1B`, `primaryBorderColor:
#D6002A`, `primaryTextColor:#fff`, `secondaryColor:#fff`,
`tertiaryColor:#F0F0F0`, `lineColor:#1B1B1B`) — only the 4 S&P tokens.
Transparent background. No component names, no technical jargon —
concepts only (leader-friendly).
**Diagram-type-per-slide mapping:**
1. flowchart LR — 3 frictions → Nova absorbs them
2. flowchart TB — Central IT golden image → Nova one layer up
3. flowchart LR with subgraphs — two principles → everything inherits
4. flowchart LR (3 columns) — Live · Attested · Stays human
5. flowchart LR (two halves) — In Nova's lane / Outside Nova's lane
6. timeline — the 18-month shape (α→β→γ→δ)
7. flowchart LR — what works now → next steps arc
The Slide Content Map in PROJECT.md is updated to include a "Diagram"
field per slide.
**Confidence:** 0.88.
---
## Requirements impact
No requirements are added, removed, or re-scoped by D-241D-243.
D-244D-246 are polish-phase decisions that record deviations from
prior binding decisions (G-1) and the Slide Content Map (slide 7).
The spec is locked (v1.0, 2026-08-20); no spec text changes. The
Slide Content Map in PROJECT.md is updated to reflect D-245 (slide 7
structure) and D-246 (diagram field per slide).
+138
View File
@@ -0,0 +1,138 @@
# GRILL — v1.30 Single-shot Leadership Deck
> Adversarial review of the v1.30 SPECIFY + CLARIFY + RESEARCH +
> PLAN. Griller: lead-developer (acting as ci-griller at full
> autonomy). All 9 axes reviewed; every claim verified against the
> live codebase.
---
## Overall verdict: **PROCEED** · Confidence 0.88
The plan is sound — this is a low-complexity, single-shot
presentation artifact milestone. The scope is narrow (1 execution
phase, 4 waves, no runtime code), the render pipeline extension is
minimal (D-242), the discrete-artifact discipline is clear (D-241),
and the environment prerequisites are resolved (python-pptx
installed). No critical conditions. 4 tracked conditions (all
advisory, none block P1).
The lower confidence vs. a "clean 0.95" reflects two residual
risks: (1) the python-pptx user-site install is environment-fragile
(it works now but is not reproducible in a fresh CI runner without
the same `--break-system-packages` path); (2) the PPTX footer
textbox is a new renderer behavior that needs visual confirmation.
Both are mitigated — (1) by the smoke-test hard-fail gate (8f) which
forces render success before ship, and (2) by the verify stage's
visual review (REQ-372.7).
---
## Axis verdicts
| Axis | Verdict | Confidence | Tracked condition |
|------|---------|-----------|-------------------|
| §1 Feasibility | PROCEED | 0.90 | T-1.1 footer textbox overlap |
| §2 Scope | PROCEED | 0.92 | T-2.1 single-shot discipline enforcement |
| §3 Cost | PROCEED | 0.95 | (none — smallest milestone in project history) |
| §4 Schedule | PROCEED | 0.93 | (none — 1 execution phase) |
| §5 Technical Depth | PROCEED | 0.86 | T-5.1 speaker notes word-band parsing; T-5.2 `→` bullet rendering |
| §6 Operational Readiness | PROCEED | 0.90 | T-6.1 render env reproducibility |
| §7 Security Posture | PROCEED | 0.95 | (none — static artifact, no runtime surface) |
| §8 Dependency Risk | PROCEED | 0.84 | T-8.1 python-pptx user-site install |
| §9 Re-mapping Integrity | PROCEED | 0.92 | T-9.1 STATE.md intake override applied |
---
## Tracked conditions (advisory — none block P1)
### T-1.1 — Footer textbox overlap with content
**Claim:** The footer textbox at `SLIDE_H - 0.3"` (7.2") won't
overlap content (content area tops out at ~6.5").
**Verification:** python-pptx test rendered a textbox at
`Inches(7.2)` on a 7.5" slide — succeeds, no overlap with content
ending at ~6.5". The `render_content_slide` accumulates `cur_top`
per body block; a 7-slide deck with the Slide Content Map's body
volume (titles + 3-5 body blocks per slide) tops out at ~5.5-6.0".
**Verdict:** Safe. The verify stage visual review (REQ-372.7) is the
backstop.
### T-2.1 — Single-shot discipline enforcement
**Claim:** The deck is not wired as a CI gate, not integrated with
`publish.yml`, not auto-regenerated.
**Verification:** `workflows-src/slides.yml` triggers on
`docs/presentations/**` but `render_slides.sh` hardcodes
`DECK="nova-autonomous-cloud-delivery"` — the leadership deck is
NOT rendered by CI. No `publish.yml` reference to the leadership
deck. The smoke test is a standalone script (no workflow
integration). **Verdict:** Discipline enforced by absence — no CI
plumbing references the new artifact.
### T-5.1 — Speaker notes word-band parsing in bash
**Claim:** The smoke test extracts `<!-- ... -->` per slide and
counts words with `wc -w`.
**Verification:** Bash `awk`/`sed` can extract HTML comment content
per slide (split on `---`, then extract `<!--` ... `-->` within each
slide block). Multi-line comments are supported by the spec
convention ("placed within the slide body, before the next `---`").
**Verdict:** Feasible. The ci-cli-engineer implements + tests this in
W3.
### T-5.2 — `→` bullet rendering
**Claim:** Use `- → ...` bullets so the renderer treats `→` lines as
bullet blocks.
**Verification:** The renderer's unordered-list regex
`^(\s*)([-*+])\s+(.*)` matches `- → ...` → bullet level 0, text
`→ ...`. The `→` is preserved in the rendered text. **Verdict:**
Correct. The ci-doc-writer uses `- → ...` for the arrow lines.
### T-6.1 / T-8.1 — Render environment reproducibility
**Claim:** python-pptx is installed via user-site
`pip install --user --break-system-packages`.
**Verification:** Confirmed in this session: python-pptx 1.0.2 +
pytest 9.1.1 installed. `python3 -c "import pptx"` succeeds. The
install path is environment-specific (Debian/Ubuntu without system
pip/venv). In a fresh CI runner, the `slides.yml` workflow uses
`pip install -e ".[slides]"` (system pip in the runner image) —
reproducible there. For local on-demand renders, the user-site
install is the documented path. **Verdict:** Acceptable. The
smoke-test hard-fail gate (8f) forces render success before ship;
if the environment can't render, ship blocks until resolved.
### T-9.1 — STATE.md intake override applied
**Claim:** D-241 overrides the stale STATE.md intake assumption 3.
**Verification:** STATE.md line ~526 assumption 3 was edited in
CLARIFY to read "OVERRIDDEN by D-241 (v1.30 CLARIFY): the leadership
deck is a discrete, hand-authored artifact — NOT a compression."
The override is recorded in CLARIFY.md (D-241) + this grill. **Verdict:**
Applied + verified.
---
## Binding decisions (grill-level, full autonomy)
| ID | Decision | Rationale | Confidence |
|----|----------|-----------|-----------|
| G-1 | All 7 slides use `##` H2 titles (content slides, white bg) — slide 1 is NOT a title-class slide. | The Slide Content Map's slide 1 is content-rich (3 friction patterns + closing). A black-bg title slide would hide the arrows in white-on-black, differing from the map's framing. White-bg content slides give visual consistency across all 7. The map doesn't specify background; visual review accepts either. | 0.82 |
| G-2 | The `→` arrow lines are authored as `- → ...` bullets (not bare `→` plain text). | The renderer parses `[-*+]` as bullets (proper indentation + bullet glyphs). Bare `→` lines parse as plain paragraphs (no bullet formatting). The Slide Content Map shows `→` as distinct arrow lines — bullets with the arrow glyph preserve the visual intent in the PPTX. | 0.88 |
| G-3 | The `style:` block in the leadership deck frontmatter replaces `#2E2E2E` (blockquote color in the existing deck) with `#1B1B1B`. | REQ-372.6 allows only 4 hex colors in the source. The existing deck's `style:` uses `#2E2E2E` for blockquote text — this must not appear in the leadership deck source. `#1B1B1B` is the closest S&P token (black). | 1.0 |
| G-4 | The render_pptx.py extension parses the Marp frontmatter to extract the `footer:` value (for the footer textbox), but does NOT parse `paginate:`, `theme:`, `size:`, or `style:`. | Minimal extension scope per D-242. Only the footer is needed for REQ-372.5. The other directives are source-only (smoke test checks source; the python-pptx path ignores them). | 0.90 |
---
## Escalations
None. All axes ≥ 0.84 confidence. No human escalation required at
full autonomy.
@@ -56,7 +56,8 @@ and covered by the baseline test.
## OIDC act_runner role (CAP-022, Phase 56)
The OIDC role for the Gitea `act_runner` was created in Phase 08 and
gone since (CAPABILITY_INVENTORY.md CAP-022). Phase 56 re-creates it
gone since (`archive/CAPABILITY_INVENTORY-v1.10.md` CAP-022, archived
v1.27). Phase 56 re-creates it
with a trust policy for the Gitea runner ARN. The role grants the
spike-runner-equivalent permissions to the runner via `sts:AssumeRole`,
so the runner does not need a long-lived access key. This closes the
@@ -73,7 +74,7 @@ bootstrap root key; the runner then assumes the role.
The OIDC role for the Gitea `act_runner` was planned in Phase 08 but
never created (the spike used a long-lived key per D-039 waiver).
CAPABILITY_INVENTORY.md CAP-022 recorded "iam:ListRoles shows no acdl*
`archive/CAPABILITY_INVENTORY-v1.10.md` CAP-022 recorded "iam:ListRoles shows no acdl*
roles." Phase 56 re-created the role:
- **Role name:** `acdl-act-runner-role`
+111
View File
@@ -0,0 +1,111 @@
---
project: acdl
milestone: v1.30
generated_at: 2026-08-20
generator: lead-developer
verification_toolchain:
typecheck: "python3 -m py_compile scripts/render_pptx.py 2>&1 | head -5 || true"
test: "bash scripts/check_leadership_deck.sh 2>&1 | tail -20; echo \"exit=$?\""
lint: "python3 -c \"import pptx; print('python-pptx', pptx.__version__)\" 2>&1"
note: |
v1.30 is a single-shot presentation artifact milestone (Leadership
Deck). Four active personas: lead-developer (coordination + STATE.md
CAP-042 + PROJECT.md D-241), backend-engineer (render_pptx.py
extension + PPTX render + python-pptx install), ci-doc-writer
(custom, phase-specific — Marp markdown deck authoring), ci-cli-
engineer (custom — smoke-test script). frontend-engineer +
data-engineer + security-engineer deactivated (no UI, no data
pipelines, no runtime security surface — the deck is a static
artifact). The render_pptx.py extension (D-242) is the only code
change; it is a narrow prerequisite, not a REQ-372 deliverable.
---
# Personas — v1.30 Single-shot Leadership Deck
## Roster
### lead-developer
```yaml
active: true
domain: "Milestone coordination, STATE.md CAP-042, PROJECT.md D-241 record, ship discipline"
frameworks: []
constraints: ["pragmatic", "battle-tested defaults", "D-241", "D-242", "D-243"]
territory:
- ".ciagent/STATE.md"
- ".ciagent/PROJECT.md"
- ".ciagent/CHECKPOINT.json"
- ".ciagent/REQUIREMENTS.md"
- ".ciagent/ROADMAP.md"
reason: "Owns the ship-wave records (CAP-042, D-241) and milestone coordination. The deck is a single-shot artifact; the lead-developer ensures the STATE.md/PROJECT.md records are appended correctly at ship."
```
### backend-engineer
```yaml
active: true
domain: "scripts/render_pptx.py extension (path arg + custom output + footer textbox), PPTX render, python-pptx install"
frameworks: ["Python 3.11", "python-pptx 1.0.2", "pip"]
constraints: ["D-242", "narrow extension only", "no new renderer", "S&P theme tokens only in source"]
territory:
- "scripts/render_pptx.py"
- "docs/presentations/nova-leadership-deck.pptx"
reason: "Owns the narrow render_pptx.py extension (D-242) and the PPTX render. Frameworks overridden from fastify/hono (default) to python-pptx (actual project dependency for this milestone). The extension is a non-REQ-372 prerequisite per spec §3.3 Edge 2."
```
### ci-doc-writer
```yaml
active: true
phase_specific: true
domain: "Marp markdown deck authoring (7 slides, speaker notes, [1] citations, S&P theme)"
frameworks: ["Marp", "Markdown"]
constraints: ["REQ-372.1", "REQ-372.3", "REQ-372.4", "REQ-372.6", "REQ-372.7", "REQ-372.9", "REQ-372.12", "D-241", "D-243"]
territory:
- "docs/presentations/nova-leadership-deck-marp.md"
reason: "Custom persona for presentation authoring. Created for P1 (the deck is the primary deliverable). Removed after P1 ships. The deck is hand-authored against the Slide Content Map in PROJECT.md §v1.30 — NOT a compression (D-241)."
```
### ci-cli-engineer
```yaml
active: true
domain: "Smoke-test script (bash, runnable on demand, NOT a CI gate)"
frameworks: ["Bash", "grep", "awk", "wc"]
constraints: ["REQ-372.8", "not a CI gate", "exit 0 on pass", "non-zero on fail"]
territory:
- "scripts/check_leadership_deck.sh"
reason: "Custom persona for the smoke-test script. Owns the 6 assertions (af): file exists, slide count=7, word bands, footer string, S&P colors only, PPTX exists. Pure bash — no python dependency (keeps it runnable without the python-pptx install)."
```
## Deactivated
### frontend-engineer
```yaml
active: false
reason: "ACDL has no frontend (no package.json); the deck is markdown (ci-doc-writer territory). Already deactivated in config.json personas[3]."
```
### data-engineer
```yaml
active: false
reason: "No schema/migration/data-pipeline work in v1.30. The milestone is a single-shot presentation artifact."
```
### security-engineer
```yaml
active: false
reason: "No runtime security surface in v1.30. The deck is a static artifact; the existing security posture (ABAC, KMS, JWKS) is referenced in slide content, not modified. Security review of the deck content is handled by the verify stage (no secrets, no publish.yml integration)."
```
## Phase-specific persona lifecycle
- **ci-doc-writer**: created for P1, removed after P1 ships. The deck
source is the deliverable; no further presentation authoring in P2
(final review only).
- All other personas persist through P2 (final review + ship).
## Territory enforcement
- **Mode:** `warn` (per `config.json personas.territory_enforcement`).
- **Note:** v1.30 has a small, non-overlapping territory surface.
`lead-developer` owns `.ciagent/STATE.md` + `.ciagent/PROJECT.md`;
`backend-engineer` owns `scripts/render_pptx.py` + the PPTX;
`ci-doc-writer` owns the markdown source; `ci-cli-engineer` owns
the smoke-test script. No territory conflicts expected.
+275
View File
@@ -0,0 +1,275 @@
# PLAN — v1.30 Single-shot Leadership Deck
> **Milestone:** v1.30 (feature — single-shot PPTX leadership deck).
> Tags on the **v1.29.x** line: `v1.29.1` (P0) → `v1.29.2` (P1
> execution) → `v1.29.3` (P2 final = milestone release). The final
> phase's patch IS the milestone release.
> **Branch:** `milestone/v1.30-leadership-deck`. Phase branches:
> `phase/00-pre-execution`, `phase/01-leadership-deck`,
> `phase/02-final-review-ship`.
>
> **Tags:** `v1.29.1` (P0) → `v1.29.2` (P1) → `v1.29.3` (P2 final =
> milestone release). **1 execution phase** (P1) — this is a
> single-shot artifact, not a multi-phase build.
## Milestone goal
A single-shot, 7-slide PPTX leadership deck for Infrastructure &
Operations leadership (CTO + VP Technology + Product Management),
presented live in August 2026, securing architecture endorsement and
a November 2026 runway. Authored as Marp markdown, rendered via the
existing `scripts/render_pptx.py` (narrowly extended per D-242),
verified by `scripts/check_leadership_deck.sh`. The deck is discrete
from the existing citizen-developer pitch (D-241: NOT a compression).
## Requirements
12 requirements: REQ-372.1..REQ-372.12 (full text in
`.ciagent/REQUIREMENTS.md` §v1.30). 1 capability: CAP-042. 3
decisions: D-241..D-243 (CLARIFY). Vision grounding: `[1]`
`docs/vision.md`.
## Phase breakdown
### Phase P1 — leadership-deck (REQ-372.1..REQ-372.12)
**Goal:** Author the Marp markdown deck source, extend the render
pipeline, render the PPTX, author the smoke test, and append the
ship-wave records (CAP-042, D-241). The deck is a single-shot
artifact; all 12 REQs ship in this one phase.
**Personas:** lead-developer, backend-engineer, ci-doc-writer
(phase-specific), ci-cli-engineer.
**Territory:** `docs/presentations/nova-leadership-deck-marp.md`
(ci-doc-writer), `scripts/render_pptx.py` +
`docs/presentations/nova-leadership-deck.pptx` (backend-engineer),
`scripts/check_leadership_deck.sh` (ci-cli-engineer),
`.ciagent/STATE.md` + `.ciagent/PROJECT.md` (lead-developer).
#### Wave 1 — render pipeline prerequisite (backend-engineer)
**Task P1.W1.T1:** Extend `scripts/render_pptx.py` per D-242:
- Accept an explicit source `.md` path as argv[1] (if it ends in
`.md` and contains a `/`, treat as a path; else treat as a deck
name per the existing convention — backward compatible).
- Accept `--output <path>` for the custom output filename. Default:
derive from the source name (strip `-marp.md` → add `.pptx`) for
backward compatibility.
- Add a `_add_footer(slide, text)` helper that adds a right-aligned
textbox at the bottom of every slide with the exact string
`Nova Platform - Infrastructure & Operations` (grey, small). Call
it in both `render_title_slide` and `render_content_slide`.
- The footer text is read from the Marp frontmatter `footer:`
directive if present; else default to the existing deck's footer
(backward compatible). Parse the frontmatter to extract the
`footer:` value (the existing code strips frontmatter without
reading it — add a frontmatter parser).
- **No other renderer change.** Speaker notes remain skipped
(acceptable per RESEARCH R1).
**Must-haves:**
- `python3 scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md --output docs/presentations/nova-leadership-deck.pptx` works.
- `python3 scripts/render_pptx.py nova-autonomous-cloud-delivery` still works (backward compatible — renders `{deck}-marp.md``{deck}-python.pptx`).
- Every rendered slide has a right-aligned footer textbox.
- `python3 -m py_compile scripts/render_pptx.py` exits 0.
**REQs covered:** (prerequisite, not REQ-372 directly — per spec §3.3
Edge 2 + D-242).
#### Wave 2 — deck source (ci-doc-writer)
**Task P1.W2.T1:** Author
`docs/presentations/nova-leadership-deck-marp.md`:
- **Header comment (REQ-372.9):** an HTML comment at the top (before
frontmatter) naming this deck as the leadership artifact for
Infrastructure & Operations, August 2026 presentation date, and
naming `nova-autonomous-cloud-delivery-marp.md` as a related-but-
distinct artifact that this deck does not compress or modify.
- **Frontmatter (cover note):** `marp: true; theme: default; footer:
"Nova Platform - Infrastructure & Operations"; paginate: false;
size: 16:9` + a `style:` block using ONLY the 4 S&P tokens
(`#D6002A`, `#1B1B1B`, `#FFFFFF`, `#F0F0F0`). **Replace the
existing deck's `#2E2E2E` blockquote color with `#1B1B1B`** to
satisfy REQ-372.6.
- **7 slides** delimited by `---` on its own line (REQ-372.3). All
slides use `##` H2 titles (content slides, white bg, red title bar
— per RESEARCH R1 final call for visual consistency).
- **On-slide body** per the Slide Content Map in PROJECT.md §v1.30
(REQ-372.7). Use `>` blockquotes for the italic callouts, `**...**`
bold lead for the slide titles' sub-headings, `-` bullets with ``
prefix for the arrow lines (so they render as proper bullets).
- **Speaker notes** per slide as HTML comments `<!-- ... -->` within
the slide body before the next `---` (REQ-372.4). Word counts:
slides 1/2/4/6 in 150300; slides 3/5 in 250400; slide 7 in
200300.
- **`[1]` citations** (REQ-372.12): at least one `[1]` in speaker
notes of slides 3, 5, 7 (the architecture-load slides), grounding
to `docs/vision.md` tenets/anti-goals/boundaries.
- **No hex colors** outside the 4 S&P tokens anywhere in the source
(REQ-372.6).
**Must-haves:**
- File exists, parses as valid Marp, exactly 7 `---`-delimited slides.
- Header comment present with all 3 elements (REQ-372.9).
- Frontmatter has the exact footer string + `paginate: false`.
- Per-slide speaker-note word counts in band.
- `[1]` present in slides 3, 5, 7 speaker notes.
- `grep -oiE '#[0-9A-Fa-f]{6}'` returns only the 4 S&P tokens.
**REQs covered:** REQ-372.1, REQ-372.3, REQ-372.4, REQ-372.6,
REQ-372.7, REQ-372.9, REQ-372.12.
#### Wave 3 — smoke test (ci-cli-engineer)
**Task P1.W3.T1:** Author `scripts/check_leadership_deck.sh`:
- Shebang `#!/usr/bin/env bash`, `set -euo pipefail`, header comment
with Usage + Returns.
- Assertions (REQ-372.8 af):
- (a) `docs/presentations/nova-leadership-deck-marp.md` exists.
- (b) slide count = 7 (count `^---\s*$` lines after frontmatter
end, +1; or count `---` separators — careful with frontmatter).
- (c) per-slide speaker-note word counts in band (extract `<!-- ...
-->` content per slide, `wc -w`; slides 1/2/4/6: 150300; 3/5:
250400; 7: 200300). Exit non-zero on violation.
- (d) footer string `Nova Platform - Infrastructure & Operations`
present in source (frontmatter `footer:` directive).
- (e) only S&P hex colors in source (`grep -oiE '#[0-9A-Fa-f]{6}'`,
`sort -u`, compare to 4-token allow-list).
- (f) `docs/presentations/nova-leadership-deck.pptx` exists (hard
fail per Q-M4).
- Exit 0 on pass, non-zero (1) on fail. Runnable from repo root.
- NOT wired as a CI gate (no `.github/workflows/` or
`workflows-src/` integration).
**Must-haves:**
- `bash scripts/check_leadership_deck.sh` exits 0 after the deck +
PPTX are authored/rendered.
- Exits non-zero if any assertion fails (test by temporary
mutation).
**REQs covered:** REQ-372.8.
#### Wave 4 — render + ship-wave records (backend-engineer + lead-developer)
**Task P1.W4.T1 (backend-engineer):** Render the PPTX:
- `python3 scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md --output docs/presentations/nova-leadership-deck.pptx`
- Verify: PPTX written with 7 slides, python-pptx raised no
exceptions, footer textbox present on every slide.
- Run `bash scripts/check_leadership_deck.sh` → exits 0.
**Task P1.W4.T2 (lead-developer):** Append CAP-042 to STATE.md:
- CAP-042 row in the capability table: artifact paths
(`nova-leadership-deck-marp.md`, `nova-leadership-deck.pptx`),
audience (Infrastructure & Operations leadership), single-shot
intent, presentation month (August 2026), milestone v1.30 / tag
`v1.29.3`.
**Task P1.W4.T3 (lead-developer):** Record D-241 in PROJECT.md:
- D-241 entry in the decisions section: single-shot nature, audience,
August 2026 anchor + November 2026 runway, explicit decision not
to compress the existing citizen-developer deck.
**Must-haves:**
- PPTX exists with 7 slides.
- Smoke test exits 0.
- CAP-042 row in STATE.md.
- D-241 record in PROJECT.md.
**REQs covered:** REQ-372.2, REQ-372.5, REQ-372.8, REQ-372.10,
REQ-372.11.
### Phase P2 — final-review-ship (review + audit + milestone ship)
**Goal:** Multi-persona review of the milestone changes, project-
health audit, and milestone ship (merge to main, tag `v1.29.3` =
milestone release, delete milestone branches).
**Personas:** lead-developer (review + audit + ship coordination).
**Tasks:**
- Review all v1.30 changes (deck source, render extension, smoke
test, STATE.md/PROJECT.md records). Auto-apply P0 fixes; flag P1+
for post-hoc review.
- Audit: reconstruction test (git log matches `.ciagent/` files),
file discipline, branch hygiene, commit discipline.
- Ship: merge `phase/02` → `milestone/v1.30-leadership-deck` →
`main`, tag `v1.29.3`, create release, delete milestone branches.
- Complete: mark REQ-372.1..12 complete in REQUIREMENTS.md, mark
v1.30 complete in ROADMAP.md.
## Wave dependency graph
```
W1 (render_pptx.py extension) ─┐
├─→ W4.T1 (render PPTX) ─→ W4.T2/T3 (records)
W2 (deck source) ──────────────┤ │
│ ↓
W3 (smoke test) ───────────────┴────────────────────────→ P1 VERIFY
P1 SHIP (v1.29.2)
P2 (v1.29.3)
```
W1, W2, W3 are independent (no cross-dependencies at author time).
W4 depends on W1 + W2 (render needs the extension + the source) +
W3 (smoke test validates the render). W4.T2/T3 (records) depend on
W4.T1 (render confirms ship readiness).
## User-Facing Surface
- **The PPTX deck** (`docs/presentations/nova-leadership-deck.pptx`)
— the primary leadership-facing artifact, presented live in August
2026.
- **The Marp markdown source**
(`docs/presentations/nova-leadership-deck-marp.md`) — the source-
of-truth, reproducible via `scripts/render_pptx.py`.
- **The smoke test** (`scripts/check_leadership_deck.sh`) — runnable
on demand by the PO/presenter to verify deck integrity before
presentation.
## Happy Path
**J1 — PO presents the deck live** (from spec §3.2):
1. PO authors `nova-leadership-deck-marp.md` against the Slide
Content Map; `bash scripts/check_leadership_deck.sh` exits 0
(verifies content). _(REQ-372.1, .4, .6, .7, .8, .12.)_
2. PO renders the markdown to PPTX via
`python3 scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md --output docs/presentations/nova-leadership-deck.pptx`;
visual inspection confirms 7 slides + footer. _(REQ-372.2, .5.)_
3. PO presents live to Infrastructure & Operations leadership;
speaker notes carry architecture depth. _(REQ-372.4.)_
4. PO updates STATE.md with CAP-042 and PROJECT.md with D-241 at the
v1.30 ship wave. _(REQ-372.10, .11.)_
## UX Acceptance Criteria
1. `bash scripts/check_leadership_deck.sh` exits 0 (all 6 assertions
af pass).
2. `docs/presentations/nova-leadership-deck.pptx` exists, has 7
slides, and python-pptx raised no exceptions during render.
3. Visual review: each slide N (17) matches the Slide Content Map
in PROJECT.md §v1.30 (on-slide body + speaker notes fingerprint).
4. Footer `Nova Platform - Infrastructure & Operations` is visible
(right-aligned) on every rendered slide.
5. Only S&P theme colors appear in the source markdown.
6. `[1]` citations present in slides 3, 5, 7 speaker notes.
7. CAP-042 row in STATE.md; D-241 record in PROJECT.md.
8. `nova-autonomous-cloud-delivery-marp.md` is unmodified (D-241
discrete-artifact constraint).
## Risks (from RESEARCH + GRILL)
| Risk | Mitigation |
|---|---|
| python-pptx render fails on the new frontmatter/style block | The python-pptx path strips frontmatter without reading it; the `style:` block is source-only (Marp CLI). No render risk. |
| Footer textbox overlaps content | Place footer at `SLIDE_H - 0.3"` (bottom margin); content area tops out at ~6.5". No overlap. |
| Speaker notes word-count band violation | ci-doc-writer counts words per slide during authoring; smoke test (8c) is the gate. |
| `` lines render as plain text (not bullets) | Use `- → ...` bullets so the renderer treats them as bullet blocks with the arrow in the text. |
| `*italic*` in source matches unordered-list regex | Verified in RESEARCH R1: `*italic*` (no space after `*`) does NOT match `[-*+]\s+`. Safe. |
| CAP-024 regression policy collides | Verified in RESEARCH R7: CAP-024 validates fixtures, not deck files. No collision. |
| `slides.yml` CI interferes | Verified in RESEARCH R8: CI only renders the citizen-developer deck (hardcoded DECK). No interference. |
+63 -6
View File
@@ -8,12 +8,28 @@ state for offline agent loading.
## Why archive
The active milestone is v1.26 (Live Pilot Estate Activation). The
`.ciagent/` root held ~11,164 lines dominated by completed-milestone
narratives (v1.0v1.24). Per the run.md context-loading model, agents
read `.ciagent/` every `/ci-run`; the historical narrative was not
load-bearing for v1.26 execution and was relocated to keep the working
context lean.
The active milestone is v1.30 (Single-shot Leadership Deck, complete).
The `.ciagent/` root was compressed three times:
1. **v1.26 P2 compression** (~11,164 lines → ~5,232): the
completed-milestone narratives (v1.0v1.24) were relocated. Per the
run.md context-loading model, agents read `.ciagent/` every
`/ci-run`; the historical narrative was not load-bearing for v1.26
execution and was relocated to keep the working context lean.
2. **v1.27 P1 compression** (~5,232 → ~3,882): the v1.26 phase
verifications + review + evidence + the dated CAPABILITY_INVENTORY
(superseded by STATE.md) + AUTONOMY_THESIS (folded into NORTH_STAR)
+ COST (predates v1.26 pilot) were relocated. The 4 pre-execution
files (CLARIFY/GRILL/IDEATE/RESEARCH) were rewritten by v1.27 P0
and stay active through v1.27.
3. **v1.30 post-milestone compression** (17 → 8 files): the v1.30
phase-specific pre-execution files (CLARIFY/RESEARCH/GRILL/PLAN/
PERSONAS) were snapshotted to the archive and removed from the
active root — they are regenerated fresh each milestone. The stale
IDEATE (v1.27), IAM_POLICY (v1.28), and REGRESSION_REPORT (v1.26)
were also archived. The persistent files (PROJECT, REQUIREMENTS,
ROADMAP, STATE, ARCHITECTURE, NORTH_STAR, config, CHECKPOINT)
remain in the active root.
## Contents
@@ -40,6 +56,47 @@ architecture reference.
| `VERIFY.md` | 86 | Per-phase verification records |
| `PRE_MORTEM.md` | 228 | Pre-mortem analyses for completed milestones |
### v1.27 compression — archived files (8 files, lossless `git mv`)
> The v1.27 NFR milestone (PO State Catalog & Ciagent Compression) archived
> 7 platform-root files + 1 consumer file. All are byte-identical
> relocations; git history at the pre-v1.27 commits preserves the
> authoritative state.
#### Snapshots of superseded durable references (3 files)
| File | Original (lines) | Superseded by | Status at time of snapshot |
|---|---|---|---|
| `CAPABILITY_INVENTORY-v1.10.md` | 120 | `.ciagent/STATE.md` (v1.27) | The 2026-07-27 re-verification sweep (v1.1→v1.8 capabilities). Predates v1.26 pilot (CAP-025 absent; blockchain capabilities absent). |
| `AUTONOMY_THESIS-v1.21.md` | 65 | `NORTH_STAR.md` Vision + Anti-Goals #2 | "Last refined: v1.21" — the autonomy-in-operations thesis, fully folded into NORTH_STAR.md. |
| `COST-v1.14.md` | 106 | (future cost milestone) | AWS cost report dated 2026-07-29, framed "v1.0 → v1.14". Predates v1.26 live pilot (ECS + ALB + DynamoDB + S3 costs not reflected). |
#### v1.26 phase verifications + review + evidence (4 files)
| File | Original (lines) | Phase(s) documented |
|---|---|---|
| `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-v1.26.md` | 46 | v1.26 live apply evidence (`blkex-pilot-apply-v0.2`; confidence 0.800 pass; outcome backfilled; hash chain valid) |
#### Consumer subproject archive (1 file)
| File | Original (lines) | Phase(s) documented |
|---|---|---|
| `nova-blockchain-exchange/archive/ROADMAP-v1.26.md` | 57 | v1.26 consumer roadmap (P3/P4/P5 marked "planned" at archive time; v1.26 shipped `v1.25.5`). Phase narrative preserved in the platform `.ciagent/ROADMAP.md` §v1.26. |
#### v1.26 pre-execution artifacts (in git history, not archived to disk)
The v1.26 pre-execution files (CLARIFY, GRILL, IDEATE, RESEARCH) were
overwritten by the v1.27 P0 pre-execution cycle. The v1.26-era content
is preserved in git history at the pre-v1.27-P0 commits (search the
log for `docs(P00):` commits on the `milestone/v1.26-pilot-activation`
line). The v1.27 P0 versions stay active through v1.27; they archive at
v1.28 P1 if v1.28 happens. Decisions D-200..D-213 (v1.26) are folded
into `PROJECT.md` load-bearing decisions; D-214..D-225 (v1.27) live in
the active `CLARIFY.md`.
### Live operational files NOT archived
These files remain at their canonical `.ciagent/` paths because they are
+305
View File
@@ -0,0 +1,305 @@
# RESEARCH — v1.30 Single-shot Leadership Deck
> **Autonomy:** full. Research findings load-bearing for v1.30 PLAN.
> The research scope is narrow: this is a single-shot presentation
> artifact, not a runtime feature. The research covers (1) the
> existing render pipeline's behavior + limits, (2) the smoke-test
> script conventions, (3) the Marp frontmatter/footer/speaker-notes
> handling, (4) the theme-token enforcement strategy, (5) the
> python-pptx install path in this environment, (6) the vision
> document grounding for `[1]` citations.
---
## R1 — Existing render pipeline (`scripts/render_pptx.py`)
**Source:** `scripts/render_pptx.py` (688 lines, REQ-269 v1.23).
**Behavior:**
- Argv: `render_pptx.py [deck-name]` → reads
`docs/presentations/{deck}-marp.md`, writes
`docs/presentations/{deck}-python.pptx` (lines 677-680). **Does
not accept a full path or non-`-marp.md` filename.**
- Frontmatter: stripped (lines 62-67) — the Marp `footer:`,
`paginate:`, `theme:`, `size:`, `style:` directives are NOT read
by the python-pptx path. They are source-only (smoke test checks
source; the Marp CLI path in `render_slides.sh` reads them, but
that path needs Chromium which is unavailable here).
- Slide splitting: `re.split(r"\n---\s*\n", ...)` after frontmatter
strip (line 69). Exactly 7 `---`-delimited slides required.
- Body parsing (`parse_slide`, lines 360-498):
- HTML comments (`<!-- ... -->`) are **skipped entirely** (lines
366-379). **Speaker notes are NOT embedded in the PPTX.**
Acceptable for REQ-372.4 (smoke test checks source word counts,
not PPTX-embedded notes).
- Headings `#`/`##` → title (first) or lead (subsequent).
- Bold lead `**...**` (own line, exactly 2 `**`) → `lead` block
(red, bold).
- Blockquotes `>``quote` block (grey, italic).
- Unordered list `[-*+]\s+...``bullet` (level by indent).
**`*italic*` (no space after `*`) does NOT match** — safe as
plain text.
- Ordered list `\d+\.\s+...``ordered`.
- Tables `| ... |` + separator → `table`.
- `→`-prefixed lines → `plain` text (not bullets). Content
preserved.
- `_strip_inline_emphasis` (lines 209-220): `**bold**`, `*italic*`,
`` `code` `` markers are collapsed to plain text in the PPTX.
Content is preserved; emphasis styling is lost (acceptable — the
PPTX is an editable comparison artifact; REQ-372.7 content match
is by visual review).
- Theme: hardcoded S&P constants (lines 37-43): `RED=#D6002A`,
`BLACK=#1B1B1B`, `WHITE=#FFFFFF`, `GREY_HEADER=#F0F0F0`,
`GREY_TEXT=#2E2E2E`, `BODY_TEXT=#1B1B1B`. **Note: `GREY_TEXT=#2E2E2E`
is a 5th color used internally for blockquote/body text.** This is
a renderer-internal color, NOT a source hex color — REQ-372.6
scopes to "color values extracted from the source markdown (Marp
directives + inline overrides)", so `#2E2E2E` in the renderer does
not violate REQ-372.6. The smoke test checks the *source* file for
hex colors.
- Footer: **NOT rendered.** No footer textbox is added by the
existing renderer. **D-242 extension required** to add a
right-aligned footer textbox on every slide.
- Title slide: `render_title_slide` (line 501) — black bg, red top
bar, white title. Triggered when `idx==0` + (`title_is_h1` or
`is_title_class`). The leadership deck's slide 1 uses a bold lead
(`**The friction...**`) as the first line — this is an H1 (`# The
friction...`) in the source, so slide 1 renders as a title slide
(black bg). **Decision for PLAN:** author slide 1 with `#` H1
title (title slide, black bg, red bar — strong opener) OR author
as `##` H2 (content slide, white bg). The Slide Content Map shows
slide 1 with a bold title + italic subtitle + arrows + italic
closing — a content-rich slide. **Recommend: `##` H2 title for all
7 slides → all render as content slides (white bg, red title bar)
for visual consistency.** Slide 1 as a black-bg title slide would
hide the `` arrows in white-on-black, which is fine but differs
from the map's framing. The map doesn't specify background; visual
review accepts either. **Final call in PLAN:** all `##` content
slides for consistency + readability of the 3-pattern frame.
**Gaps for v1.30 (D-242 extension):**
1. Accept explicit source `.md` path + `--output` filename.
2. Add right-aligned footer textbox on every slide with exact string
`Nova Platform - Infrastructure & Operations`.
---
## R2 — Smoke-test script conventions
**Source:** `scripts/check_north_star_diff.sh` (REQ-204), other
`scripts/check_*.sh` / `scripts/run_*.sh`.
**Conventions:**
- Shebang `#!/usr/bin/env bash`
- Header comment with purpose + Usage + Returns
- `set -euo pipefail`
- Exit 0 on pass, non-zero (1) on fail
- `echo "WARN: ..."` / `echo "ERROR: ..."` to stderr
- Runnable from repo root: `bash scripts/check_*.sh`
**v1.30 smoke test (`scripts/check_leadership_deck.sh`) assertions
(REQ-372.8 af):**
- (a) `docs/presentations/nova-leadership-deck-marp.md` exists
- (b) slide count = 7 (count `---` separators on own line, excluding
frontmatter)
- (c) per-slide speaker-note word counts in band (extract HTML
comments per slide; slides 1/2/4/6: 150300; 3/5: 250400; 7:
200300)
- (d) footer string `Nova Platform - Infrastructure & Operations`
present in source (frontmatter `footer:` directive)
- (e) only S&P hex colors `#D6002A`, `#1B1B1B`, `#FFFFFF`, `#F0F0F0`
in source (grep for `#[0-9A-Fa-f]{6}` and diff against the allow-
list)
- (f) `docs/presentations/nova-leadership-deck.pptx` exists (hard
fail per Q-M4)
**Implementation approach:** pure bash + `grep`/`awk`/`wc`. No
python dependency for the smoke test (keeps it runnable on demand
without the python-pptx install). Slide count: count lines matching
`^---\s*$` after the frontmatter, +1. Speaker notes: per slide,
extract content between `<!--` and `-->`, strip HTML comment markers,
`wc -w`. Color scan: `grep -oiE '#[0-9A-Fa-f]{6}'` on the source,
sort -u, compare to allow-list.
---
## R3 — Marp frontmatter / footer / speaker-notes handling
**Source:** `docs/presentations/nova-autonomous-cloud-delivery-marp.md`
(lines 1-27), Marp CLI v4.5.0 (available via npx).
**Existing deck frontmatter:**
```yaml
marp: true
theme: default
paginate: true
size: 16x9
footer: 'Nova — The Autonomous Cloud Delivery Platform'
style: |
section { ... color: #1B1B1B; ... }
h1 { color: #D6002A; ... }
...
```
**v1.30 leadership deck frontmatter (per cover note + spec):**
```yaml
marp: true
theme: default
footer: "Nova Platform - Infrastructure & Operations"
paginate: false
size: 16x9
style: |
section { font-family: "Akkurat Pro", "Helvetica Neue", "Arial", sans-serif; font-size: 22px; color: #1B1B1B; padding: 48px 56px 40px; overflow: auto; }
h1 { color: #D6002A; font-size: 34px; margin-bottom: 0.3em; }
h2 { color: #D6002A; font-size: 26px; margin-bottom: 0.2em; }
blockquote { border-left: 4px solid #D6002A; color: #1B1B1B; font-size: 20px; padding-left: 12px; }
strong { color: #D6002A; }
...
```
**Key differences from the existing deck:**
- `paginate: false` (existing: `true`) — per cover note.
- `footer: "Nova Platform - Infrastructure & Operations"` (existing:
different string) — per cover note + REQ-372.5.
- The `style:` block uses only the 4 S&P tokens. The existing deck's
`style:` uses `#2E2E2E` for blockquote color — **this must be
changed to `#1B1B1B`** in the leadership deck's `style:` block to
satisfy REQ-372.6 (only 4 hex colors in source). The renderer's
internal `GREY_TEXT=#2E2E2E` is not in the source, so it doesn't
violate REQ-372.6 — but the *source* `style:` block must not
contain `#2E2E2E`.
**Speaker notes:** HTML comments `<!-- ... -->` within the slide
body, before the next `---`. The Marp CLI renders these as speaker
notes in the HTML/PPTX; the python-pptx path skips them. The smoke
test extracts them from the *source* for word-count checking.
---
## R4 — Theme-token enforcement strategy
**REQ-372.6:** only `#D6002A`, `#1B1B1B`, `#FFFFFF`, `#F0F0F0` as hex
colors in the source.
**Enforcement:**
1. **Source `style:` block:** use only the 4 tokens. Replace the
existing deck's `#2E2E2E` (blockquote color) with `#1B1B1B`.
2. **No inline `color:` overrides** in slide bodies — the slides use
no inline HTML/color spans.
3. **Smoke test (8e):** `grep -oiE '#[0-9A-Fa-f]{6}'` on the source,
`sort -u`, compare to the 4-token allow-list. Any other hex color
→ fail.
---
## R5 — python-pptx install path (this environment)
**Environment:** Debian/Ubuntu, Python 3.11.2, no system pip, no
root, no `python3-venv`/`python3-pip` packages, no `ensurepip`.
**Resolved install path:**
1. `curl -sS https://bootstrap.pypa.io/get-pip.py -o /home/opencode/tmp/get-pip.py`
2. `python3 get-pip.py --user --break-system-packages`
3. `/home/opencode/.local/bin/pip install --user --break-system-packages "python-pptx>=0.6.23"`
4. `pip install --user --break-system-packages "pytest>=8.0"` (for
verify stage)
**Result:** python-pptx 1.0.2 + pytest 9.1.1 installed to user-site.
`python3 -c "import pptx"` succeeds. No Chromium needed (python-pptx
is the render path, not Marp CLI PPTX).
**Confirmed in RESEARCH execution:** all commands ran successfully
in this session.
---
## R6 — Vision document grounding for `[1]` citations
**Source:** `docs/vision.md` (the spec's `acdl-vision.md` / `[1]`
reference).
**Key tenets for slide grounding:**
- **§1 The Friction** (slide 1): "Software delivery scales with the
coordination surface around it, not the engineering inside it."
Grounds the three-pattern problem frame + binding-constraint
claim.
- **§3 Core Tenets** (slides 3, 5, 7):
- "The Delivery Lifecycle is a Sovereign Boundary" — grounds
slide 3's Sovereign boundary tenet + slide 5's boundary
discipline + slide 7's "Nova stays in its lane."
- "Lower Environments are Autonomous; Higher Environments are
Attested" — grounds slide 3's Lower autonomous · higher attested
tenet + slide 4's HITL discipline.
- "Infrastructure is Consumed, Not Maintained" — grounds slide 5's
"VM, bare-metal, OS lifecycles" exclusion.
- **§4 Domain Boundaries** (slides 2, 5, 6): "The platform begins
where the artifact is compiled and ends where it runs in
production." "Out of scope: Application business logic, IDE
workflows, product backlog management, sprint planning, compute
requiring node-level or OS-level management." Grounds slide 5's
in-lane/out-of-lane split + slide 6's "Nova absorbs no IDE, no
editor, no sprint tool, no agent harness."
**Citation convention:** `[1]` in speaker notes, resolving to
`docs/vision.md`. The spec §citation-references confirms `[1]` →
`acdl-vision.md` (vision document, source [1]).
---
## R7 — CAP-024 regression policy (collision check)
**Source:** `adapters/kyverno-json/policies/regression/cap-024-deck-structure.json`
+ `tests/test_regression_policies.py`.
**Finding:** CAP-024 validates the citizen-developer deck's 4-beat
arc (Problem/Solution/Proof/Roadmap+Ask) against fixture files
(`clean.json`/`drifted.json` in `tests/fixtures/`), NOT against the
actual deck markdown files. The leadership deck
(`nova-leadership-deck-marp.md`) does NOT pass through this policy.
No collision risk. The leadership deck's 7-slide structure is a
different artifact (CAP-042, not CAP-024).
---
## R8 — `slides.yml` CI (non-interference check)
**Source:** `workflows-src/slides.yml`.
**Finding:** The CI workflow triggers on `docs/presentations/**`
path changes, but `scripts/render_slides.sh` defaults to
`DECK="nova-autonomous-cloud-delivery"` and only renders that one
deck. Adding `nova-leadership-deck-marp.md` to
`docs/presentations/` will trigger the CI, but it will only re-
render the citizen-developer deck (no-op if that deck is unchanged).
The leadership deck is NOT rendered by CI (per spec: no CI gate, no
`publish.yml` integration). **No interference.** The bot commit from
CI (if any) will be a no-op re-render of the unchanged citizen-
developer deck.
---
## Persona assessment (lead-developer)
**Active personas for v1.30:**
- **lead-developer** (coordination) — owns STATE.md CAP-042, PROJECT.md
D-241, milestone coordination. Territory: `.ciagent/STATE.md`,
`.ciagent/PROJECT.md`.
- **backend-engineer** (backend) — owns `scripts/render_pptx.py`
extension + PPTX render + python-pptx install. Territory:
`scripts/render_pptx.py`, `docs/presentations/nova-leadership-deck.pptx`.
Framework override: python-pptx (not fastify/hono — the default
frameworks don't match this project's Python stack).
- **ci-doc-writer** (custom, phase-specific) — owns the Marp markdown
deck source. Territory:
`docs/presentations/nova-leadership-deck-marp.md`. Created for this
phase (presentation authoring); removed after P1.
- **ci-cli-engineer** (custom) — owns the smoke-test script.
Territory: `scripts/check_leadership_deck.sh`.
**Deactivated personas:**
- **frontend-engineer** — already `active: false` in config (no UI).
Confirmed.
- **data-engineer** — no schema/migration work in this milestone.
Deactivate for v1.30.
**Territory enforcement:** `warn` (per config).
+146 -37
View File
@@ -2,21 +2,28 @@
"projects": [
{
"slug": "acdl",
"name": "Nova The New Dawn of DevSecOps",
"name": "Nova \u2014 The New Dawn of DevSecOps",
"default": true
},
{
"slug": "nova-blockchain-exchange",
"name": "Nova Pilot Consumer Blockchain Stock Exchange",
"name": "Nova Pilot Consumer \u2014 Blockchain Stock Exchange",
"default": false
}
],
"active_project": "acdl",
"active_projects": ["acdl", "nova-blockchain-exchange"],
"active_milestone": "v1.27",
"active_projects": [
"acdl",
"nova-blockchain-exchange"
],
"active_milestone": "v1.31",
"autonomy": {
"level": "full",
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
"escalation_hooks": [
"deploy",
"delete_data",
"merge_to_main"
],
"clarify_budget": 10,
"decision_confidence_threshold": 0.6,
"max_revision_iterations": 3,
@@ -42,35 +49,72 @@
"escalate_high_severity": true,
"bash_allowlist": {
"allowed_commands": [
"git", "ls", "cat", "head", "tail", "wc",
"echo", "mkdir", "cp", "mv", "rm", "touch",
"pwd", "which", "env", "printenv",
"python3", "pytest", "pip",
"terraform", "checkov",
"curl", "wget",
"docker", "docker-compose"
"git",
"ls",
"cat",
"head",
"tail",
"wc",
"echo",
"mkdir",
"cp",
"mv",
"rm",
"touch",
"pwd",
"which",
"env",
"printenv",
"python3",
"pytest",
"pip",
"terraform",
"checkov",
"curl",
"wget",
"docker",
"docker-compose"
],
"max_output_bytes": 1048576,
"timeout_ms": 30000,
"blocked_env_vars": [
"HOME", "PATH", "USER", "SHELL",
"AWS_*", "*_TOKEN", "*_KEY", "*_SECRET",
"*_PASSWORD", "*_CREDENTIAL",
"GITHUB_TOKEN", "GITHUB_API_KEY",
"OPENAI_API_KEY", "ANTHROPIC_API_KEY",
"HOME",
"PATH",
"USER",
"SHELL",
"AWS_*",
"*_TOKEN",
"*_KEY",
"*_SECRET",
"*_PASSWORD",
"*_CREDENTIAL",
"GITHUB_TOKEN",
"GITHUB_API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"OLLAMA_CLOUD_API_KEY"
]
}
},
"git": {
"branching_strategy": "flat",
"_branching_strategy_note": "Nova uses flat workflow (committed directly to main per established convention since v1.0; renamed ACDLNova in v1.15). The 'phase' strategy is advisory; CIAgent uses milestone/phase branches for v1.14 but the project convention is flat.",
"_branching_strategy_note": "Nova uses flat workflow (committed directly to main per established convention since v1.0; renamed ACDL\u2192Nova in v1.15). The 'phase' strategy is advisory; CIAgent uses milestone/phase branches for v1.14 but the project convention is flat.",
"auto_commit": true,
"auto_push": true
},
"secrets": {
"sources": [".env", ".env.secrets", ".env.*"],
"disallow": ["shell_env", "netrc", "keychain", "rc_files", "global_config"],
"sources": [
".env",
".env.secrets",
".env.*"
],
"disallow": [
"shell_env",
"netrc",
"keychain",
"rc_files",
"global_config"
],
"scopes": {
"forge": "NOVA_FORGE_TOKEN",
"gitea": "NOVA_FORGE_TOKEN",
@@ -112,10 +156,18 @@
"backend": {
"provider": "auto",
"agent_backends": {
"opencode": { "enabled": true },
"codex": { "enabled": true },
"claude-code": { "enabled": true },
"hermes": { "enabled": true }
"opencode": {
"enabled": true
},
"codex": {
"enabled": true
},
"claude-code": {
"enabled": true
},
"hermes": {
"enabled": true
}
},
"llm_backends": {
"openai": {
@@ -148,7 +200,13 @@
},
"ideation": {
"enabled": true,
"categories": ["security", "quality", "architecture", "coverage", "improvement"],
"categories": [
"security",
"quality",
"architecture",
"coverage",
"improvement"
],
"confidence_threshold": 0.6,
"max_ideas": 20,
"external_signals": {
@@ -162,7 +220,11 @@
},
"chaos": {
"enabled": true,
"scenarios": ["backend_unavailable", "requirement_change", "test_coverage_drop"]
"scenarios": [
"backend_unavailable",
"requirement_change",
"test_coverage_drop"
]
}
},
"sessions": {
@@ -178,30 +240,77 @@
"name": "lead-developer",
"domain": "coordination",
"frameworks": [],
"constraints": ["pragmatic", "battle-tested defaults"],
"constraints": [
"pragmatic",
"battle-tested defaults"
],
"territory": []
},
{
"name": "data-engineer",
"domain": "data",
"frameworks": ["drizzle", "postgresql"],
"constraints": ["schema-first", "type-safe ORM", "migration-driven"],
"territory": ["**/migrations/**", "**/schema/**", "**/models/**", "**/db/**", "prisma/schema.prisma", "drizzle/**", "**/*.sql"]
"frameworks": [
"drizzle",
"postgresql"
],
"constraints": [
"schema-first",
"type-safe ORM",
"migration-driven"
],
"territory": [
"**/migrations/**",
"**/schema/**",
"**/models/**",
"**/db/**",
"prisma/schema.prisma",
"drizzle/**",
"**/*.sql"
]
},
{
"name": "backend-engineer",
"domain": "backend",
"frameworks": ["fastify", "hono"],
"constraints": ["api-first", "strict-typing", "dependency-injection"],
"territory": ["**/api/**", "**/routes/**", "**/services/**", "**/middleware/**", "**/controllers/**", "**/auth/**"]
"frameworks": [
"fastify",
"hono"
],
"constraints": [
"api-first",
"strict-typing",
"dependency-injection"
],
"territory": [
"**/api/**",
"**/routes/**",
"**/services/**",
"**/middleware/**",
"**/controllers/**",
"**/auth/**"
]
},
{
"name": "frontend-engineer",
"domain": "frontend",
"active": false,
"frameworks": ["react", "next.js"],
"constraints": ["component-first", "server-components", "minimal-client-js"],
"territory": ["**/components/**", "**/pages/**", "**/hooks/**", "**/styles/**", "**/*.tsx", "**/*.css", "**/*.vue"],
"frameworks": [
"react",
"next.js"
],
"constraints": [
"component-first",
"server-components",
"minimal-client-js"
],
"territory": [
"**/components/**",
"**/pages/**",
"**/hooks/**",
"**/styles/**",
"**/*.tsx",
"**/*.css",
"**/*.vue"
],
"reason": "ACDL has no frontend (no package.json); decks are markdown (lead-developer territory). Deactivated per PERSONAS.md:80."
}
]
@@ -220,4 +329,4 @@
"engine": "kyverno-json",
"policy_root": "adapters/kyverno-json/policies"
}
}
}
+6 -1
View File
@@ -89,4 +89,9 @@ of normal operations.
- The AWS bootstrap (S3 state bucket + DynamoDB outbox) was re-run in
the pre-run (Workstream A3) — the platform components exist.
- The consumer repo was created on Gitea (Workstream A4) and cloned to
`/root/nova-blockchain-exchange`.
`/root/nova-blockchain-exchange`.
- **Phase-by-phase history:** `.ciagent/ROADMAP.md` §v1.26 (the
consumer ROADMAP is archived at
`.ciagent/nova-blockchain-exchange/archive/ROADMAP-v1.26.md` since
v1.27 — the platform ROADMAP is the source of truth for milestone
phase narrative).
+9 -8
View File
@@ -5,7 +5,7 @@
> secrets to set, what the contract looks like, and how to verify the
> result. The platform side is documented in
> `.ciagent/ARCHITECTURE.md` §12.8; the live-pilot evidence is in
> `.ciagent/P4-PILOT-RUN-EVIDENCE.md`.
> `.ciagent/archive/P4-PILOT-RUN-EVIDENCE-v1.26.md` (archived v1.27).
This is a **consumer** of the Nova platform, not a fork. The consumer
repo owns the app code (the blockchain, the order-matching engine, the
@@ -20,12 +20,13 @@ Ledger. The consumer never clones the platform repo and never runs
## 1. Invoke the deploy
The consumer's `.github/workflows/deploy.yml` (and its byte-identical
The consumer's `.github/workflows/deploy.yml` (and its
`.gitea/workflows/deploy.yml` mirror) is a `workflow_dispatch` workflow.
It does **not** use cross-repo `uses:` (SPEC §10 Q1 — the Gitea forge
rejects it). Instead it is an **inline adapter**: it checks out the
consumer repo, then checks out `acdl/acdl` @ `ref: v1.25` into
`platform/`, then runs `bash platform/scripts/run_platform.sh`.
consumer repo, then checks out `acdl/acdl` @ `ref: v1.29` (bumped from
`v1.25` at v1.29 P5, REQ-CONSUMER-BUMP) into `platform/`, then runs
`bash platform/scripts/run_platform.sh`.
To run a deploy:
@@ -161,8 +162,8 @@ Ledger. Neither trusts the other.
If the AWS API shows the resources AND the Decision Ledger shows the
decision + outcome with a valid chain, the deploy is verified. See
`.ciagent/P4-PILOT-RUN-EVIDENCE.md` for the full pilot-evidence
checklist (every ARN, the confidence JSON, the backfill timestamp).
`.ciagent/archive/P4-PILOT-RUN-EVIDENCE-v1.26.md` for the full pilot-evidence
checklist (every ARN, the confidence JSON, the backfill timestamp; archived v1.27).
---
@@ -170,8 +171,8 @@ checklist (every ARN, the confidence JSON, the backfill timestamp).
- `.ciagent/ARCHITECTURE.md` §12.8 — the pilot-estate architecture
(this guide is the consumer-facing companion to that section).
- `.ciagent/P4-PILOT-RUN-EVIDENCE.md` — the live-pilot evidence
(run `blkex-pilot-apply-v0.2`).
- `.ciagent/archive/P4-PILOT-RUN-EVIDENCE-v1.26.md` — the live-pilot evidence
(run `blkex-pilot-apply-v0.2`; archived v1.27).
- `.ciagent/nova-blockchain-exchange/PROJECT.md` — the consumer
project charter (vision, scope, decisions D-200..D-205).
- `.ciagent/nova-blockchain-exchange/REQUIREMENTS.md` — the consumer
@@ -71,21 +71,32 @@ declare `dynamodb` — ECS + S3 already exist.
- `tests/test_contract_validates.py` — schema validation against the
platform's `schemas/contract.schema.json`.
### REQ-314 — Consumer deploy workflow invocation ✓ complete (P2, v1.25.2)
### REQ-CONSUMER-BUMP — Consumer deploy.yml `@v1.25` → `@v1.29` (v1.29 P5)
The consumer repo's GitHub/Gitea Actions invoke the Nova platform's
reusable `deploy.yml@v1.25` workflow with `mode: full` for the pilot.
The workflow checks out the consumer repo + the platform repo, runs
`scripts/run_platform.sh`, and records the apply decision + attestation
in the Nova Decision Ledger.
The consumer repo's deploy workflow invocation (REQ-314, originally
`@v1.25`) is bumped to `@v1.29` to track the v1.29 platform release
(Reposplit + Identity Layer Bring-Live). The v1.29 platform publishes
Lambda zip + layer wheel + Python wheel + ECR container image to GitHub
Releases (REQ-354); the consumer's smoke test runs against these
artifacts.
**Must-haves:**
- `.github/workflows/deploy.yml``uses: acdl/.github/workflows/deploy.yml@v1.25`
- `.github/workflows/deploy.yml``uses: acdl/.github/workflows/deploy.yml@v1.29`
with `with: { contract: contract.yaml, mode: full, environment: dev }`.
- `.gitea/workflows/deploy.yml`byte-identical mirror (the platform's
deploy workflow is forge-agnostic).
- `tests/test_deploy_workflow_invocation.py` — asserts the `uses:` ref
+ inputs are correct.
- `.gitea/workflows/deploy.yml`updated to `@v1.29` (the consumer's
`.gitea/` is out of scope for the acdl REQ-367 Gitea scrub — that scrub
is `acdl/acdl` only; the consumer may keep its Gitea mirror or follow
suit — this is a consumer-repo decision).
- `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).
**Status:** The consumer repo is not checked out in this environment.
The deploy.yml bump + smoke test are documented here; the actual bump
requires a consumer repo checkout. The smoke test runs against the
v1.29.0 intermediate tag artifacts (produced by P1, grill CF-3/G-3).
### REQ-315 — Settlement-finality kyverno-json policy (IDEATE I6)
-40
View File
@@ -1,40 +0,0 @@
# Gitea Workflows — Limitation Documentation (v1.14, REQ-150)
## Shared workflows (byte-identical Gitea + GitHub)
These 3 workflows exist in both `.gitea/workflows/` and `.github/workflows/`
and are byte-identical (asserted by `tests/test_pipeline_contract.py`):
- `ci.yml` — lint + test + check-only (runs on every PR)
- `deploy.yml` — reusable deploy workflow (invoked by consumer repos)
- `modules-lifecycle.yml` — L1 + L2 module lifecycle pipeline (plan-only
default, full on workflow_dispatch override)
## GitHub-only workflows (no Gitea mirror)
These 4 workflows exist only in `.github/workflows/`:
- `platform-test.yml` — PR pipeline: lint + unit + integration + schema
validation. Uses GitHub Actions features (reusable workflow composition,
environment protection) not available in Gitea Actions.
- `primitives-plan.yml` — PR plan-only matrix over all L1 primitives. Uses
GitHub matrix strategy + `terraform plan` against live AWS.
- `patterns-plan.yml` — PR plan-only matrix over all L2 modules. Same
pattern as primitives-plan.
- `release.yml` — release job on merge to main: computes next semver,
creates + updates MAJOR.MINOR.PATCH / MAJOR.MINOR / MAJOR floating tags,
creates a GitHub release. GitHub-only by design (Gitea releases are
created via the ship workflow's API call, not a workflow).
## Why no Gitea mirror
Gitea Actions (act_runner) has limited support for reusable workflow
composition, environment protection, and the `gh` CLI used by the release
job. The 3 shared workflows are the ones that need to run on both forges
(CI + deploy + lifecycle). The 4 GitHub-only workflows are the
production-grade platform pipelines that run on GitHub Actions; Gitea is
the dev/integration forge. Mirroring them would require feature parity
that Gitea Actions does not currently provide.
This is a documented limitation, not a defect. A future milestone may
add Gitea mirrors if act_runner gains the required features.
-89
View File
@@ -1,89 +0,0 @@
# Nova CI Pipeline (dev environment)
#
# This workflow implements the central pipeline contract:
# pipelines/ci.yml (validated against schemas/pipeline.schema.json)
#
# The same contract is implemented by .github/workflows/ci.yml (GitHub
# Actions, production). Both files must be byte-identical — the only
# declared difference is the forge/runtime, not the stages or commands.
#
# Shell reproducibility: scripts/run_ci.sh runs the same 3 stages locally.
#
# Stages (from the contract):
# 1. lint — py_compile all Python files
# 2. test — pytest test suite (offline, no AWS)
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
name: acdl-ci
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Compile all Python files
run: |
python3 -m py_compile \
core/confidence_signal.py \
core/outbox_writer.py \
core/output_publisher.py \
core/contract_resolver.py \
core/lambda/contract_ingestor.py \
adapters/terraform/adapter.py \
adapters/terraform/policy/checkov_adapter.py \
scripts/push_consumer_image.py
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Install test dependencies
run: pip install -r requirements-test.txt
- name: Run pytest
run: python3 -m pytest tests/ -v --tb=short
check-only:
name: Platform check-only (offline)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Install runtime dependencies
run: pip install jsonschema pyyaml boto3
- name: Run platform check-only
run: bash scripts/run_platform.sh --check-only
-168
View File
@@ -1,168 +0,0 @@
# Nova Reusable Deploy Workflow (dev environment)
#
# This reusable workflow implements the central deployment pipeline contract:
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
#
# The same contract is implemented by .github/workflows/deploy.yml (GitHub
# Actions, production). Both files must be byte-identical — the only
# declared difference is the forge/runtime, not the stages or commands.
#
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
# uses: nova/.github/workflows/deploy.yml@v1.19
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
#
# Unversioned references (@main, bare) are discouraged — the consumer's setup
# must be immutable + resilient. The versioned tag is the only immutability
# lever (version constraints cannot be expressed inside the contract).
#
# What this workflow does:
# 1. Checks out the consumer repo (the repo that invoked the workflow).
# 2. Checks out the ACDL platform repo into the workspace (platform/).
# This is the run-time fetch — consumers never clone the platform repo.
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
# 5. Runs scripts/run_platform.sh against the consumer's contract path.
# 6. Uploads artifacts (emitted Terraform, Checkov JSON, confidence JSON,
# platform log) for auditability.
#
# Inputs:
# contract — path to the consumer's contract YAML (default .nova/contract.yml)
# mode — full | plan-only | check-only (default full; dev = full apply,
# higher environments hold for HITL — the calling repo or the
# forge environment gate enforces that)
#
# Auth (zero-trust default — see README.md#credentials--zero-trust):
# OIDC federation is the default. permissions: id-token: write lets the
# forge mint a short-lived STS token. The role-to-assume is scoped by the
# consumer's repository identity (ABAC) — the workflow assumes the role
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
#
# Override (where OIDC is unavailable, e.g. pending
# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY
# as repository secrets. The platform-managed scheduled pipeline rotates
# the key on a daily cadence. When .env.secrets is used locally instead,
# rotating the key out of band is the consumer's responsibility.
name: nova-deploy
on:
workflow_call:
inputs:
contract:
description: Path to the consumer contract YAML (in the consumer repo)
type: string
default: .nova/contract.yml
mode:
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
type: string
default: full
changeRequestId:
description: Change request ID (required for decommission mode — validated against CMDB)
type: string
default: ""
environment:
description: Target environment override (dev/qa/prod/dr); when empty, the contract's environment field is used
type: string
default: ""
permissions:
id-token: write
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Check out consumer repo
uses: actions/checkout@v4
- name: Check out ACDL platform repo
uses: actions/checkout@v4
with:
repository: acdl/acdl
path: platform
ref: v1.25
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install runtime dependencies
run: |
pip install --break-system-packages jsonschema pyyaml boto3
pip install --break-system-packages "checkov>=3.2,<4"
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Configure AWS credentials (OIDC default + static-key override)
uses: aws-actions/configure-aws-credentials@v4
with:
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-.
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/nova-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION || 'us-east-1' }}
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
- name: Run the platform pipeline
working-directory: ${{ github.workspace }}
env:
NOVA_CONSUMER_REPO: ${{ github.repository }}
run: |
MODE_FLAG=""
case "${{ inputs.mode }}" in
full) MODE_FLAG="" ;;
plan-only) MODE_FLAG="--plan-only" ;;
check-only) MODE_FLAG="--check-only" ;;
decommission)
if [ -z "${{ inputs.changeRequestId }}" ]; then
echo "FAIL: changeRequestId is required for decommission mode"
exit 1
fi
MODE_FLAG="--decommission ${{ inputs.changeRequestId }}"
;;
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
esac
ENV_FLAG=""
if [ -n "${{ inputs.environment }}" ]; then
ENV_FLAG="--environment ${{ inputs.environment }}"
fi
bash platform/scripts/run_platform.sh $MODE_FLAG $ENV_FLAG "${{ inputs.contract }}"
- name: Post stage summary comment to PR
if: success() && github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF: ${{ github.ref }}
run: |
bash platform/scripts/post_stage_comment.sh deploy pass '{"mode":"${{ inputs.mode }}","runId":"${{ github.run_id }}"}'
- name: Report error to platform team (on failure)
if: failure()
env:
AWS_DEFAULT_REGION: us-east-1
run: |
aws lambda invoke-function-url \
--function-url "${{ secrets.NOVA_LAMBDA_URL }}" \
--cli-binary-format raw-in-base64-out \
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
/dev/null || true
- name: Upload emitted Terraform
uses: actions/upload-artifact@v4
with:
name: nova-terraform
path: /tmp/nova_platform_run/tf/*.tf
if-no-files-found: warn
- name: Upload platform log
uses: actions/upload-artifact@v4
with:
name: nova-platform-log
path: platform/logs/
if-no-files-found: warn
-207
View File
@@ -1,207 +0,0 @@
# Nova Modules Lifecycle Pipeline (dev environment)
#
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
# apply→modify→destroy against live AWS. No per-module Python. The "test" =
# the pipeline cell going green.
#
# Also matrix-runs L2 composition modules (static-assets, microservice) through
# the same apply→modify→destroy lifecycle. L2 = composition only (no L2
# terraform files); the composition must be deterministic.
#
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical
# in .github/workflows/).
#
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
# no AWS mutation, validates the contract->resolver->adapter->plan chain
# for every module on every PR, with no AWS credentials or cost). Set to
# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable)
# to run the real apply→modify→destroy against live AWS. In plan mode the
# short-lived CI VPC apply/destroy jobs are skipped (nothing is applied).
#
# A short-lived CI VPC (terraform/ci-vpc/) is created before testing VPC-dependent
# modules (alb, ecs-service, rds, uptime, and L2 microservice) and destroyed
# after all tests complete. The CI VPC is separate from the long-lived platform
# VPC. Outputs are read from the S3 state by each lifecycle job (no artifact
# passing needed).
name: acdl-modules-lifecycle
on:
pull_request:
branches: [main]
workflow_dispatch:
inputs:
lifecycle_mode:
description: "Lifecycle mode: 'plan' (default, fast, no AWS mutation) or 'full' (real apply→modify→destroy against live AWS)"
required: false
default: "plan"
type: choice
options:
- plan
- full
permissions:
contents: read
jobs:
# Prerequisite: apply the short-lived CI VPC (needed by VPC-dependent L1s + L2 microservice)
# Skipped in plan mode (no resources are applied, so no VPC is needed).
ci-vpc-apply:
name: CI VPC apply
runs-on: ubuntu-latest
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Apply CI VPC
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform apply -auto-approve -lock=false
# L1 lifecycle matrix: apply simple → apply complex (modify) → destroy
lifecycle:
name: L1 lifecycle (${{ matrix.module }})
needs: ci-vpc-apply
if: always()
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime]
env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
sudo apt-get clean
df -h /
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install jsonschema pyyaml boto3
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
# L2 lifecycle matrix: apply simple → apply complex (modify) → destroy
l2-lifecycle:
name: L2 lifecycle (${{ matrix.module }})
needs: ci-vpc-apply
if: always()
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: [static-assets, microservice]
env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
sudo apt-get clean
df -h /
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install jsonschema pyyaml boto3
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
# Cleanup: destroy the CI VPC (always runs in full mode, even if lifecycle fails)
ci-vpc-destroy:
name: CI VPC destroy
needs: [lifecycle, l2-lifecycle]
runs-on: ubuntu-latest
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Destroy CI VPC
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform destroy -auto-approve -lock=false
-69
View File
@@ -1,69 +0,0 @@
# Nova AWS key rotation — platform-managed scheduled pipeline (SPEC §5.9)
#
# Rotates the NOVA_AWS_* static key daily (no long-lived keys in the steady
# state). v0.2 scope: the mechanism must exist (SPEC §5.9); the v0.2 deploy
# uses the currently-active key. The rotation is best-effort + idempotent
# (scripts/rotate_spike_key.sh deactivates the old key only after the new
# key propagates to the consumer's Actions secret store).
#
# Auth: the rotation uses the CURRENT NOVA_AWS_* key to authenticate to IAM
# (the root account 581513795199 can rotate its own keys — confirmed by the
# bootstrap). The aws-actions/configure-aws-credentials@v4 step uses the
# static-key path (no OIDC role-to-assume); the long-lived key rotates
# itself, which is the bootstrap-exception documented in §5.9.
#
# Forge coords (base URL / owner / consumer repo) are sourced from
# repository secrets — NOVA_FORGE_BASE_URL, NOVA_FORGE_OWNER,
# NOVA_CONSUMER_REPO — so the synced workflow file stays forge-agnostic
# (REQ-230). The rotation script uploads the new key to the consumer's
# Actions secret store (the consumer whose deploy.yml consumes NOVA_AWS_*
# via secrets: inherit).
name: nova-rotate-aws-key
on:
schedule:
- cron: "0 0 * * *" # daily at 00:00 UTC
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
rotate:
name: Rotate NOVA_AWS_* static key
runs-on: ubuntu-latest
steps:
- name: Check out Nova platform repo
uses: actions/checkout@v4
- name: Configure AWS credentials (bootstrap root creds for IAM key rotation)
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ secrets.AWS_DEFAULT_REGION || 'us-east-1' }}
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
- name: Install Python deps (boto3 for the rotation script)
run: |
python3 -m pip install --break-system-packages --quiet boto3
- name: Run the key rotation script
env:
# aws-actions/configure-aws-credentials exports AWS_ACCESS_KEY_ID /
# AWS_SECRET_ACCESS_KEY; the rotation script reads the bootstrap
# creds via NOVA_BOOTSTRAP_AWS_* (its dual-read contract, D-034).
# Map the standard AWS_* exports onto the script's expected vars.
NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
# Forge + consumer coords come from repository secrets (REQ-230 —
# no forge hostnames/orgs hardcoded in the synced workflow file).
# NOVA_FORGE_TOKEN holds the forge API token (set equal to the
# existing forge token as a one-time secret setup).
NOVA_FORGE_TOKEN: ${{ secrets.NOVA_FORGE_TOKEN }}
NOVA_FORGE_BASE_URL: ${{ secrets.NOVA_FORGE_BASE_URL }}
NOVA_FORGE_OWNER: ${{ secrets.NOVA_FORGE_OWNER }}
NOVA_CONSUMER_REPO: ${{ secrets.NOVA_CONSUMER_REPO }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION || 'us-east-1' }}
run: |
bash scripts/rotate_spike_key.sh
-43
View File
@@ -1,43 +0,0 @@
# Nova Slides Render — re-renders presentation deck when source files change.
# REQ-273: install python-pptx, pin CLI versions, stage HTML + both PPTX +
# base64-inlined images.
name: Nova Slides Render
on:
push:
paths:
- 'docs/presentations/**'
- 'scripts/render_slides.sh'
- 'scripts/inline_images.py'
- 'scripts/render_pptx.py'
- 'pyproject.toml'
workflow_dispatch:
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: '20' }
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install python-pptx (slides extra)
run: pip install -e ".[slides]"
- name: Install + pin render CLIs
run: |
npx --yes @marp-team/marp-cli@4.5.0 --version
npx --yes @mermaid-js/mermaid-cli@11.16.0 --version
- name: Render slides
run: bash scripts/render_slides.sh
- name: Commit rendered artifacts
run: |
git config user.name "nova-slides-bot"
git config user.email "bot@nova.local"
git add docs/presentations/*.html \
docs/presentations/*.pptx \
docs/presentations/*-python.pptx \
docs/presentations/assets/png/*.png
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
git push
+94
View File
@@ -0,0 +1,94 @@
# Nova CLI Action — composite action (REQ-326, NFR-11)
#
# Runs a Nova CLI command (`nova <command>`) in a consumer repository.
# Python 3.12 is pinned (REQ-326 AC3). The same action.yml is discovered
# by both the production forge (GitHub Actions) and the dev forge
# (act_runner) via the shared .github/actions/nova-cli/ path — there is
# no separate dev-forge action file. Consumers reference it via a
# versioned tag pin:
#
# uses: <org>/<repo>/.github/actions/nova-cli@v1.28
#
# Wheel index selection (CodeArtifact default + fallback):
# - CodeArtifact mode: set the NOVA_CODEARTIFACT_DOMAIN repository
# secret/env. The action runs
# `aws codeartifact login --tool pip --domain $NOVA_CODEARTIFACT_DOMAIN
# --repository nova-pypi` before `pip install nova`.
# - Fallback mode: leave NOVA_CODEARTIFACT_DOMAIN unset and provide
# NOVA_WHEEL_INDEX env pointing at any PEP 503 simple index (a
# private package registry). The action runs
# `pip install --index-url $NOVA_WHEEL_INDEX nova==<version>`.
# See docs/codeartifact-provisioning.md for the index shape.
#
# Byte-identical cross-platform verification (NFR-11, REQ-326 AC2):
# the full byte-identical test runs as a CI matrix job on the
# production forge (ubuntu-latest) + the dev forge (act_runner) with
# identical inputs, asserting same stdout + exit code. That matrix is
# not reproducible in a unit test; the structural invariants (valid
# YAML, python 3.12 pin, install + run steps present) are asserted by
# tests/test_forge_action_byte_identical.py.
name: "Nova CLI Action"
description: "Run a Nova CLI command (`nova <command>`) with Python 3.12 pinned"
inputs:
command:
description: "The Nova subcommand + args to run (e.g. `apply --local`, `init`, `idp setup --check-only`). Passed verbatim to `nova`."
required: true
contract:
description: "Path to the consumer contract YAML (default .nova/contract.yml). Forwarded to nova via the NOVA_CONTRACT env var."
required: false
default: ".nova/contract.yml"
mode:
description: "Nova client mode override (e.g. agent, interactive, plan-only, check-only). Forwarded to nova via the NOVA_CLIENT_MODE env var. Empty = let nova resolve (TTY + credentials)."
required: false
default: ""
version:
description: "nova package version to install (default `latest`). Pin to a released wheel version for reproducible runs."
required: false
default: "latest"
runs:
using: "composite"
steps:
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Nova (CodeArtifact default + fallback index)
shell: bash
env:
NOVA_CODEARTIFACT_DOMAIN: ${{ env.NOVA_CODEARTIFACT_DOMAIN }}
NOVA_WHEEL_INDEX: ${{ env.NOVA_WHEEL_INDEX }}
NOVA_INSTALL_VERSION: ${{ inputs.version }}
run: |
set -e
if [ "$NOVA_INSTALL_VERSION" = "latest" ]; then
PIP_SPEC="nova"
else
PIP_SPEC="nova==$NOVA_INSTALL_VERSION"
fi
if [ -n "$NOVA_CODEARTIFACT_DOMAIN" ]; then
echo "CodeArtifact mode: domain=$NOVA_CODEARTIFACT_DOMAIN repository=nova-pypi"
aws codeartifact login --tool pip \
--domain "$NOVA_CODEARTIFACT_DOMAIN" --repository nova-pypi
pip install $PIP_SPEC
else
echo "Fallback-index mode: NOVA_WHEEL_INDEX=$NOVA_WHEEL_INDEX"
if [ -z "$NOVA_WHEEL_INDEX" ]; then
echo "FAIL: NOVA_CODEARTIFACT_DOMAIN is unset and NOVA_WHEEL_INDEX is empty. Set one of them."
exit 1
fi
pip install --index-url "$NOVA_WHEEL_INDEX" $PIP_SPEC
fi
nova --version || true
- name: Run Nova
shell: bash
env:
NOVA_CLIENT_MODE: ${{ inputs.mode }}
NOVA_CONTRACT: ${{ inputs.contract }}
run: |
set -e
echo "nova ${{ inputs.command }}"
nova ${{ inputs.command }}
+4 -2
View File
@@ -5,8 +5,10 @@ platform. 3 are generated from `workflows-src/<name>`; 4 are GitHub-only.
## Shared workflows (generated from source)
These 3 are generated from `workflows-src/<name>`. Run `python3 scripts/sync_workflows.py --check` to verify
no drift.
These 3 are generated from `workflows-src/<name>`. D-232 (v1.29): the
byte-identical forge-parity generator (`scripts/sync_workflows.py`) was
removed with the dev-forge parity retirement — the `workflows-src/`
copies remain as the source of truth but are no longer auto-synced.
| Workflow | Trigger | Inputs | Required Secrets | Purpose |
|----------|---------|--------|------------------|---------|
+21
View File
@@ -22,6 +22,27 @@ on:
branches: [main]
jobs:
forge-parity-disabled:
name: forge_parity_disabled
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Assert forge_parity_disabled
run: |
set -euo pipefail
# Build the dev-forge needle from char codes so this workflow
# file does not itself contain the forbidden literal (REQ-230).
needle="$(printf '\x67\x69\x74\x65\x61')"
if [ -d ".${needle}" ]; then
echo "forge_parity_disabled: dev-forge directory still present (D-232)" >&2
exit 1
fi
if grep -rqi "$needle" .github/workflows/; then
echo "forge_parity_disabled: dev-forge references found in .github/workflows/ (D-232)" >&2
exit 1
fi
echo "forge_parity_disabled: OK"
lint:
name: Lint
runs-on: ubuntu-latest
+408
View File
@@ -0,0 +1,408 @@
# Nova Publish Pipeline — wheel + Lambda layer + Lambda zip + ECR kj
# image, all attached to a GitHub Release per tag (REQ-323, CAP-035,
# REQ-354, NFR-6, KJ-STATIC, D-239).
#
# This workflow is byte-identical across the production forge (GitHub
# Actions) and the dev forge (act_runner) — the same file is installed
# at .github/workflows/publish.yml and the mirror at
# <dev-forge>/workflows/publish.yml. Both copies must match exactly
# (asserted by tests/test_forge_action_byte_identical.py for the action
# and by the repo's byte-identical convention for workflows).
#
# NFR-6 (wheel/layer co-versioning): every tag publish affecting
# core/**, adapters/**, nova/**, or pyproject.toml publishes BOTH a
# wheel AND a Lambda layer with identical version strings. If either
# publish fails, the job fails and the release is blocked.
#
# REQ-323: CodeArtifact wheel + Lambda layer pipeline.
# REQ-354: per-tag GitHub Release attaching the Lambda token-vend zip,
# the Lambda layer zip, the Python wheel, and the ECR kj
# container image URI + digest, each with SHA-256 in the body.
# CAP-035: Lambda layer ARN version matches the nova-cli wheel version;
# the mapping is recorded in SSM /nova/layer/nova-cli/version.
# KJ-STATIC: the `kj` Go binary is built CGO_ENABLED=0 and asserted
# statically linked by `file(1)` before it is embedded in the
# ECR image. The build fails closed if `file kj` does not
# contain `statically linked` or does contain `shared library`.
# D-239: ECR tags reject `+`; the image tag uses `-` as the separator:
# `v1.29.x-kj-<kj-source-sha>`.
#
# Triggers:
# - push of a tag matching `v1.29.*` (the tag carries the version;
# REQ-354 criterion 1). Each tag produces an independent release
# (criterion 2 — previous tags' artifacts remain downloadable).
# - workflow_dispatch (manual republish, e.g. after a CodeArtifact
# provisioning fix)
#
# Wheel index selection (CodeArtifact default + fallback):
# - CodeArtifact mode: set the NOVA_CODEARTIFACT_DOMAIN repository
# secret (e.g. "nova"). The workflow runs
# `aws codeartifact login --tool twine --domain $NOVA_CODEARTIFACT_DOMAIN
# --repository nova-pypi` and twine uploads to the CodeArtifact pypi
# endpoint.
# - Fallback mode: leave NOVA_CODEARTIFACT_DOMAIN unset and provide
# TWINE_REPOSITORY_URL + TWINE_USERNAME + TWINE_PASSWORD repository
# secrets pointing at any PEP 503 simple index (a private package
# registry). twine uploads to TWINE_REPOSITORY_URL.
# See docs/codeartifact-provisioning.md for the required IAM grants
# + the fallback index shape.
#
# ECR image (kj substrate, REQ-354 criterion 3):
# - The `build-kj-image` job reads platform/abac/kj-version.txt
# (line 1 = version tag, line 2 = tree SHA, line 3 = source repo URL).
# - It fetches the kj Go source by tag (reliable; the pinned tree SHA
# is kept for traceability with v1.28 — see kj-version.txt comments).
# - It builds CGO_ENABLED=0, asserts KJ-STATIC via `file(1)`, packages
# the binary into public.ecr.aws/lambda/python:3.12-al2023 at
# /opt/kj/kj (chmod 0555, sbx_user:1051), and pushes to ECR with tag
# v1.29.x-kj-<kj-source-sha>. The tag is validated against
# ^[a-zA-Z0-9._-]+$ before push (D-239).
#
# Secrets / env:
# AWS_ROLE_ARN — OIDC role to assume (id-token: write)
# NOVA_CODEARTIFACT_DOMAIN — optional; when set, CodeArtifact mode
# TWINE_USERNAME — fallback-index upload user
# TWINE_PASSWORD — fallback-index upload password
# TWINE_REPOSITORY_URL — fallback-index upload URL
# AWS_DEFAULT_REGION (optional) — defaults to us-east-1
# NOVA_ECR_REPO — ECR repository URI for the kj image
# (e.g. 581513795199.dkr.ecr.us-east-1.
# amazonaws.com/nova-kj)
name: nova-publish
on:
push:
tags:
- "v1.29.*"
workflow_dispatch:
permissions:
id-token: write # OIDC federation to AWS
contents: write # create the GitHub Release + upload artifacts
jobs:
build-kj-image:
# KJ substrate — compile the kj Go binary static, package it into a
# public.ecr.aws/lambda/python:3.12-al2023 image at /opt/kj/kj, and
# push to ECR with tag v1.29.x-kj-<kj-source-sha> (D-239). Records
# image_uri + digest for the release body (REQ-354 criterion 4).
name: Build + push kj ECR image (KJ-STATIC, D-239)
runs-on: ubuntu-latest
outputs:
image_uri: ${{ steps.ecr-push.outputs.image_uri }}
image_digest: ${{ steps.ecr-push.outputs.image_digest }}
image_tag: ${{ steps.ecr-push.outputs.image_tag }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Read kj version pin (platform/abac/kj-version.txt)
id: kj-ver
run: |
set -e
KJ_VERSION=$(sed -n '1p' platform/abac/kj-version.txt)
KJ_TREE_SHA=$(sed -n '2p' platform/abac/kj-version.txt)
KJ_REPO_URL=$(sed -n '3p' platform/abac/kj-version.txt)
echo "kj_version=${KJ_VERSION}" >> "$GITHUB_OUTPUT"
echo "kj_tree_sha=${KJ_TREE_SHA}" >> "$GITHUB_OUTPUT"
echo "kj_repo_url=${KJ_REPO_URL}" >> "$GITHUB_OUTPUT"
echo "Pinned kj: version=${KJ_VERSION} tree_sha=${KJ_TREE_SHA} repo=${KJ_REPO_URL}"
- name: Fetch kj Go source at tag v0.0.3
env:
KJ_REPO_URL: ${{ steps.kj-ver.outputs.kj_repo_url }}
KJ_VERSION: ${{ steps.kj-ver.outputs.kj_version }}
run: |
set -e
# The pinned tree SHA (line 2) 404s as a commit; the build
# fetches by tag, which dereferences to a real commit
# (verified: 924a6af2474523c4e27e3a826248c91c8fe1d1cf).
rm -rf kj-src
git clone --depth 1 --branch "${KJ_VERSION}" \
"${KJ_REPO_URL}" kj-src
- name: Build kj (CGO_ENABLED=0 — KJ-STATIC)
working-directory: kj-src
run: |
set -e
# Resolve the tagged commit SHA — this is the source SHA
# embedded in the ECR image tag (REQ-354 criterion 3).
KJ_SOURCE_SHA=$(git rev-parse HEAD)
echo "kj_source_sha=${KJ_SOURCE_SHA}" >> "$GITHUB_ENV"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o kj ./...
file kj
- name: Assert kj is statically linked (KJ-STATIC CI gate)
working-directory: kj-src
run: |
set -e
# KJ-STATIC: file(1) MUST report `statically linked` and MUST
# NOT report `shared library`. Fail closed otherwise — this
# is the mechanical enforcement of KJ-STATIC (not human review).
FILE_OUT=$(file kj)
echo "$FILE_OUT"
case "$FILE_OUT" in
*statically\ linked*) ;;
*) echo "FAIL (KJ-STATIC): kj is not statically linked"; exit 1 ;;
esac
case "$FILE_OUT" in
*shared\ library*)
echo "FAIL (KJ-STATIC): kj links a shared library"; exit 1 ;;
*) ;;
esac
# readelf defense-in-depth: assert no NEEDED entries.
if readelf -d kj 2>/dev/null | grep -q NEEDED; then
echo "FAIL (KJ-STATIC): readelf -d reports NEEDED entries"; exit 1
fi
echo "KJ-STATIC assertion passed."
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION || 'us-east-1' }}
- name: Log in to ECR
env:
NOVA_ECR_REPO: ${{ secrets.NOVA_ECR_REPO }}
run: |
set -e
# NOVA_ECR_REPO is the full repo URI, e.g.
# 581513795199.dkr.ecr.us-east-1.amazonaws.com/nova-kj
REGISTRY=$(echo "$NOVA_ECR_REPO" | cut -d/ -f1)
aws ecr get-login-password --region "${AWS_REGION}" \
| docker login --username AWS --password-stdin "$REGISTRY"
- name: Build + push kj image to ECR (D-239)
id: ecr-push
env:
NOVA_ECR_REPO: ${{ secrets.NOVA_ECR_REPO }}
KJ_SOURCE_SHA: ${{ env.kj_source_sha }}
working-directory: kj-src
run: |
set -e
# D-239: ECR tags reject `+`; use `-` separator. The tag is
# v1.29.x-kj-<kj-source-sha> and is validated against
# ^[a-zA-Z0-9._-]+$ before push.
IMAGE_TAG="v1.29.x-kj-${KJ_SOURCE_SHA}"
if ! echo "$IMAGE_TAG" | grep -Eq '^[a-zA-Z0-9._-]+$'; then
echo "FAIL (D-239): invalid ECR tag: ${IMAGE_TAG}"
exit 1
fi
IMAGE_URI="${NOVA_ECR_REPO}:${IMAGE_TAG}"
echo "Pushing image: ${IMAGE_URI}"
# Stage the binary into a build context root.
rm -rf imgctx && mkdir -p imgctx/opt/kj
cp kj imgctx/opt/kj/kj
chmod 0555 imgctx/opt/kj/kj
printf '%s\n' \
'FROM public.ecr.aws/lambda/python:3.12-al2023' \
'COPY --chown=sbx_user:1051 --chmod=0555 opt/kj/kj /opt/kj/kj' \
> imgctx/Dockerfile
docker build -t "$IMAGE_URI" imgctx
docker push "$IMAGE_URI" >/tmp/docker-push.log 2>&1
cat /tmp/docker-push.log
# Extract the registry digest via `docker inspect` (the
# canonical source — push output wording varies by client).
IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
"$IMAGE_URI" | sed 's/.*@//')
echo "image_uri=${IMAGE_URI}" >> "$GITHUB_OUTPUT"
echo "image_digest=${IMAGE_DIGEST}" >> "$GITHUB_OUTPUT"
echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
echo "Pushed ${IMAGE_URI} @ ${IMAGE_DIGEST}"
publish:
name: Publish wheel + Lambda layer + Lambda zip + Release
runs-on: ubuntu-latest
needs: build-kj-image
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION || 'us-east-1' }}
- name: Install build + publish tools
run: pip install build twine
- name: Compute version from pyproject.toml
id: ver
run: |
set -e
VERSION=$(python -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Nova version: $VERSION"
- name: Build wheel
run: |
set -e
python -m build --wheel
ls -1 dist/
- name: Upload wheel to index (CodeArtifact default + fallback)
id: wheel
env:
NOVA_CODEARTIFACT_DOMAIN: ${{ secrets.NOVA_CODEARTIFACT_DOMAIN }}
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
TWINE_REPOSITORY_URL: ${{ secrets.TWINE_REPOSITORY_URL }}
run: |
set -e
# CodeArtifact mode: log in to the domain's pypi repository.
if [ -n "$NOVA_CODEARTIFACT_DOMAIN" ]; then
echo "CodeArtifact mode: domain=$NOVA_CODEARTIFACT_DOMAIN repository=nova-pypi"
aws codeartifact login --tool twine \
--domain "$NOVA_CODEARTIFACT_DOMAIN" --repository nova-pypi
else
echo "Fallback-index mode: uploading to TWINE_REPOSITORY_URL"
if [ -z "$TWINE_REPOSITORY_URL" ] || [ -z "$TWINE_USERNAME" ] || [ -z "$TWINE_PASSWORD" ]; then
echo "FAIL: NOVA_CODEARTIFACT_DOMAIN is unset and one of TWINE_REPOSITORY_URL/TWINE_USERNAME/TWINE_PASSWORD is missing."
exit 1
fi
fi
# Idempotent upload: a re-run for the same version may hit
# "file already exists" on the index. Treat that as success.
# Capture both attempts' output so a genuine failure (auth,
# network, invalid package) is NOT masked as success — NFR-6
# requires the job to fail if the wheel publish fails.
if twine upload "dist/nova-${{ steps.ver.outputs.version }}-*.whl" 2>&1 | tee /tmp/twine.log; then
echo "uploaded=true" >> "$GITHUB_OUTPUT"
else
# Retry once; the first attempt may have failed with a
# transient error OR with "already exists" (a re-run).
twine upload "dist/nova-${{ steps.ver.outputs.version }}-*.whl" 2>&1 | tee -a /tmp/twine.log || true
if grep -qi "already exist" /tmp/twine.log 2>/dev/null; then
echo "Wheel already present on the index — treating as success (idempotent)."
echo "uploaded=true" >> "$GITHUB_OUTPUT"
else
echo "FAIL: wheel upload failed (not an idempotent re-run)." >&2
cat /tmp/twine.log >&2
exit 1
fi
fi
- name: Build Lambda layer
run: |
set -e
rm -rf layer
mkdir -p layer/python
# Install the wheel we just built + the identity extras' deps
# so the layer carries argon2-cffi, cryptography, pyjwt.
pip install --target layer/python/ \
"dist/nova-${{ steps.ver.outputs.version }}-*.whl" \
argon2-cffi cryptography pyjwt
( cd layer && zip -r ../nova-cli-layer-v1.29.x.zip python/ )
ls -lh nova-cli-layer-v1.29.x.zip
- name: Publish Lambda layer
id: layer
run: |
set -e
ARN=$(aws lambda publish-layer-version \
--layer-name nova-cli \
--zip-file fileb://nova-cli-layer-v1.29.x.zip \
--compatible-runtimes python3.12 \
--compatible-architectures x86_64 \
--description "nova-cli v${{ steps.ver.outputs.version }}" \
--query LayerVersionArn --output text)
echo "arn=$ARN" >> "$GITHUB_OUTPUT"
echo "Published Lambda layer: $ARN"
- name: Record SSM version↔ARN mapping (CAP-035)
run: |
set -e
aws ssm put-parameter \
--name /nova/layer/nova-cli/version \
--value "${{ steps.ver.outputs.version }}:${{ steps.layer.outputs.arn }}" \
--type String --overwrite
echo "SSM /nova/layer/nova-cli/version = ${{ steps.ver.outputs.version }}:${{ steps.layer.outputs.arn }}"
- name: Build Lambda token-vend zip (nova-lambda-token-vend-v1.29.x.zip)
run: |
set -e
# Package the nova-idp-token-vend Lambda handler (the dual-use
# module core/lambda/nova_idp_token_vend.py) plus the core/
# package modules it imports at runtime (core.policy_engine,
# core.abac_evaluator, core.kms_signing). The zip root mirrors
# the repo layout so `import core.lambda.nova_idp_token_vend`
# resolves inside the Lambda execution environment.
rm -rf lambdazip
mkdir -p lambdazip/core/lambda
cp core/lambda/__init__.py lambdazip/core/lambda/__init__.py
cp core/lambda/nova_idp_token_vend.py \
lambdazip/core/lambda/nova_idp_token_vend.py
# Carry the core/ modules the handler imports lazily.
cp core/__init__.py lambdazip/core/__init__.py 2>/dev/null || true
cp core/policy_engine.py lambdazip/core/policy_engine.py 2>/dev/null || true
cp core/abac_evaluator.py lambdazip/core/abac_evaluator.py 2>/dev/null || true
cp core/kms_signing.py lambdazip/core/kms_signing.py 2>/dev/null || true
( cd lambdazip && zip -r ../nova-lambda-token-vend-v1.29.x.zip . )
ls -lh nova-lambda-token-vend-v1.29.x.zip
- name: Compute SHA-256 of all release artifacts
id: sha
run: |
set -e
sha256sum nova-lambda-token-vend-v1.29.x.zip \
> /tmp/sha-lambda.txt
sha256sum nova-cli-layer-v1.29.x.zip \
> /tmp/sha-layer.txt
sha256sum dist/nova-${{ steps.ver.outputs.version }}-*.whl \
> /tmp/sha-wheel.txt
{
echo "## Artifact SHA-256 (REQ-354)"
echo ""
echo "### nova-lambda-token-vend-v1.29.x.zip"
echo '```'
cat /tmp/sha-lambda.txt
echo '```'
echo ""
echo "### nova-cli-layer-v1.29.x.zip"
echo '```'
cat /tmp/sha-layer.txt
echo '```'
echo ""
echo "### nova-${{ steps.ver.outputs.version }}-py3-none-any.whl"
echo '```'
cat /tmp/sha-wheel.txt
echo '```'
echo ""
echo "### ECR kj image (REQ-354 criterion 3/4)"
echo "- URI: \`${{ needs.build-kj-image.outputs.image_uri }}\`"
echo "- digest: \`${{ needs.build-kj-image.outputs.image_digest }}\`"
echo "- tag: \`${{ needs.build-kj-image.outputs.image_tag }}\`"
echo ""
} > /tmp/release-body.md
echo "body_path=/tmp/release-body.md" >> "$GITHUB_OUTPUT"
echo "--- Release body ---"
cat /tmp/release-body.md
- name: Create GitHub Release + attach artifacts (REQ-354)
uses: softprops/action-gh-release@v2
with:
# Use the pushed tag as the release tag.
tag_name: ${{ github.ref_name }}
name: Nova ${{ github.ref_name }}
body_path: ${{ steps.sha.outputs.body_path }}
files: |
nova-lambda-token-vend-v1.29.x.zip
nova-cli-layer-v1.29.x.zip
dist/nova-${{ steps.ver.outputs.version }}-*.whl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fail job if either publish failed (REQ-323 AC)
if: ${{ steps.wheel.outputs.uploaded != 'true' || steps.layer.outputs.arn == '' }}
run: |
echo "FAIL: wheel uploaded=${{ steps.wheel.outputs.uploaded }} layer_arn=${{ steps.layer.outputs.arn }}"
exit 1
+3
View File
@@ -42,3 +42,6 @@ metrics/lifecycle/
*.jks
*.keystore.coverage
.coverage
.venv/
nova.egg-info/
+145
View File
@@ -0,0 +1,145 @@
"""Nova ABAC evaluator for the token-vend Lambda (REQ-339, C-6.1, D-231).
Wraps :func:`core.policy_engine.get_engine` to evaluate the
``platform/abac/token-vend.policy`` kyverno-json ``ValidatingPolicy``
against a token-vend authorization payload and produce an allow/deny
decision with the policy SHA (D-231).
Payload shape (REQ-339, C-5.1)::
{
"subject": {"id": ..., "role": ..., "owner": ...},
"requested_claims": [<claim name>, ...], # C-5.1
"target_resource": {"type": ..., "id": ..., "owner": ..., "environment": ...},
"environment": "dev" | "qa" | "prod" | "dr",
"pat_jti": "<PAT jti>",
"policy_version": "<git SHA>"
}
Decision rule (C-6.1 fail-closed): **any** PCR with ``result == "fail"``
and ``severity == "critical"`` → ``allowed=False``. The caller (the
token-vend Lambda) is additionally required to fail closed when
``KyvernoJsonEngine.is_configured()`` returns ``False`` or when this
function raises — see ``tests/test_abac_fail_closed.py`` (the grill's
#1 finding, INV-17).
"""
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Tuple
from core.policy_engine import get_engine
_POLICY_DIR = Path("platform/abac")
_POLICY_FILE = _POLICY_DIR / "token-vend.policy"
_CONTRACT_ID = "token-vend"
def _materialize_policy_dir(src_dir: Path) -> Tuple[Path, bool]:
"""Mirror ``src_dir`` to a temp dir, copying ``*.policy`` files to
``*.json`` twins (JSON is a valid kyverno-json policy format; the
``KyvernoJsonEngine`` only loads ``.json``/``.yaml``/``.yml``, and
Nova ABAC policies use the ``.policy`` extension per REQ-339, so a
byte-for-byte copy with a ``.json`` extension is required).
Returns ``(temp_dir, created)``; ``created`` is ``False`` when no
policy files were found. The caller is responsible for removing the
temp dir.
"""
tmp = Path(tempfile.mkdtemp(prefix="nova-abac-pol-"))
any_policy = False
if src_dir.is_dir():
for entry in sorted(os.listdir(src_dir)):
if entry.startswith(".") or entry.startswith("_"):
continue
src_file = src_dir / entry
if not src_file.is_file():
continue
if entry.endswith(".policy"):
dest = tmp / (entry[: -len(".policy")] + ".json")
shutil.copy2(src_file, dest)
any_policy = True
elif entry.endswith((".json", ".yaml", ".yml")):
shutil.copy2(src_file, tmp / entry)
any_policy = True
return tmp, any_policy
def _policy_sha() -> str:
"""Return the git SHA of the policy file (D-231).
Uses ``git rev-parse HEAD:platform/abac/token-vend.policy`` so the
SHA is stable across checkouts (blob SHA, not commit SHA). Falls
back to ``"unknown"`` when git is unavailable or the file is not
tracked (e.g. during local development before the first commit).
"""
repo_root = os.environ.get("NOVA_REPO_ROOT") or os.getcwd()
try:
sha = subprocess.check_output(
["git", "rev-parse", "HEAD:platform/abac/token-vend.policy"],
cwd=repo_root,
stderr=subprocess.DEVNULL,
text=True,
timeout=5,
).strip()
return sha or "unknown"
except Exception:
return "unknown"
def evaluate_token_vend_policy(
payload: dict,
) -> Tuple[bool, list, str]:
"""Evaluate the token-vend ABAC policy against ``payload``.
Args:
payload: the ABAC authorization payload (see module docstring).
Returns:
``(allowed, pcrs, policy_sha)`` where ``allowed`` is ``True``
iff no PCR has ``result == "fail"`` with ``severity ==
"critical"`` (C-6.1). ``pcrs`` is the raw list of
``PolicyCheckResult`` dicts from the engine. ``policy_sha`` is
the git blob SHA of the policy file (D-231).
Raises:
Exception: any engine error propagates — the caller MUST catch
and fail closed (403 ``abac_eval_failed``). This function
does NOT swallow errors: failing closed is the *caller's*
responsibility so the denial audit event is emitted at the
Lambda boundary with the right reason code.
"""
engine = get_engine()
# Nova ABAC policies use the `.policy` extension (REQ-339), but
# KyvernoJsonEngine only loads `.json`/`.yaml`/`.yml`. Materialize a
# temp dir with `.policy` → `.json` twins so the engine picks them
# up. The temp dir is removed in the `finally` block.
pol_dir, _ = _materialize_policy_dir(_POLICY_DIR)
try:
pcrs = engine.evaluate(payload, pol_dir, _CONTRACT_ID)
finally:
shutil.rmtree(pol_dir, ignore_errors=True)
allowed = not any(
p.get("result") == "fail" and str(p.get("severity", "")).lower() == "critical"
for p in pcrs
)
return allowed, pcrs, _policy_sha()
if __name__ == "__main__": # pragma: no cover - CLI inspection helper
import json
import sys
if len(sys.argv) > 1:
with open(sys.argv[1]) as fh:
pl = json.load(fh)
else:
pl = json.loads(sys.stdin.read())
allowed, pcrs, sha = evaluate_token_vend_policy(pl)
print(json.dumps({"allowed": allowed, "policy_sha": sha, "pcrs": pcrs}, indent=2))
+14 -13
View File
@@ -169,20 +169,21 @@ def check(env: str, evidence: dict) -> Tuple[bool, str]:
return (True, f"{env}: all {len(concerns)} concern(s) pass")
if __name__ == "__main__":
def cli_main(argv) -> int:
"""Thin CLI entry (P1): nova attestation-matrix <env> [evidence.json]."""
import json
if len(sys.argv) < 2:
print("usage: attestation_matrix.py <env> [evidence.json]", file=sys.stderr)
sys.exit(2)
_env = sys.argv[1]
if len(argv) < 2:
print("usage: attestation_matrix <env> [evidence.json]", file=sys.stderr)
return 2
_env = argv[1]
_evidence = {}
if len(sys.argv) >= 3 and os.path.isfile(sys.argv[2]):
with open(sys.argv[2]) as f:
if len(argv) >= 3 and os.path.isfile(argv[2]):
with open(argv[2]) as f:
_evidence = json.load(f)
ok, reason = check(_env, _evidence)
if ok:
print(f"ATTESTATION PASS: {reason}")
sys.exit(0)
else:
print(f"ATTESTATION BLOCK: {reason}", file=sys.stderr)
sys.exit(1)
print(f"ATTESTATION PASS: {reason}") if ok else print(f"ATTESTATION BLOCK: {reason}", file=sys.stderr)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(cli_main(sys.argv))
+97
View File
@@ -0,0 +1,97 @@
"""Nova credential store — ``~/.nova/credentials.json`` (C-7.3, REQ-344).
Stores the OIDC token + PAT metadata (jti, exp, type) ONLY — **NOT the
raw PAT** (C-7.3). The file is 0600. "Most recent wins" (D-226 Q5):
``active_credential_jti`` points at the most-recently-stored credential.
Shape::
{
"active_credential_jti": "<jti>",
"credentials": [
{"jti": ..., "type": "developer_pat"|"nova_oidc_token",
"exp": <epoch>, "token": "<oidc jwt>", "stored_at": <epoch>}
]
}
"""
from __future__ import annotations
import json
import os
import stat
import sys
from pathlib import Path
from typing import Optional
def credentials_path() -> Path:
return Path(os.environ.get("NOVA_CREDENTIALS_FILE")
or os.path.expanduser("~/.nova/credentials.json"))
def _emit_audit(event_type: str, **fields) -> None:
payload = {"event": event_type, **fields}
sys.stderr.write(json.dumps(payload, sort_keys=True) + "\n")
sys.stderr.flush()
def store_credential(
jti: str,
cred_type: str,
exp: int,
oidc_token: str,
path: Optional[Path] = None,
) -> None:
"""Store an OIDC token + PAT metadata (NOT the raw PAT, C-7.3). 0600."""
p = path or credentials_path()
p.parent.mkdir(parents=True, exist_ok=True)
data = {"active_credential_jti": jti, "credentials": []}
if p.exists():
try:
data = json.loads(p.read_text())
except (OSError, json.JSONDecodeError):
data = {"active_credential_jti": jti, "credentials": []}
creds = data.get("credentials", []) or []
# Replace any existing entry with the same jti.
creds = [c for c in creds if c.get("jti") != jti]
import time
creds.append({
"jti": jti, "type": cred_type, "exp": exp,
"token": oidc_token, "stored_at": int(time.time()),
})
data["credentials"] = creds
data["active_credential_jti"] = jti
p.write_text(json.dumps(data, indent=2, sort_keys=True))
os.chmod(p, stat.S_IRUSR | stat.S_IWUSR) # 0600
_emit_audit("auth.login", jti=jti, type=cred_type)
def load_credentials(path: Optional[Path] = None) -> dict:
"""Load the credentials file (or ``{}`` if absent)."""
p = path or credentials_path()
try:
return json.loads(p.read_text())
except (OSError, json.JSONDecodeError):
return {}
def active_credential(path: Optional[Path] = None) -> Optional[dict]:
"""Return the active credential dict (or ``None``)."""
data = load_credentials(path)
active_jti = data.get("active_credential_jti")
for c in data.get("credentials", []) or []:
if c.get("jti") == active_jti:
return c
return None
def emit_status_audit(path: Optional[Path] = None) -> dict:
"""Emit ``auth.status`` audit + return the credentials data."""
data = load_credentials(path)
_emit_audit("auth.status", active_jti=data.get("active_credential_jti"))
return data
def emit_revoke_audit(jti: str) -> None:
_emit_audit("auth.revoke", jti=jti)
+13 -7
View File
@@ -218,12 +218,18 @@ def compute(contract_id: str, environment: str,
return signal
if __name__ == "__main__":
if len(sys.argv) < 3:
print("usage: confidence_signal.py <inputs.json> <environment>", file=sys.stderr)
sys.exit(2)
env = sys.argv[2]
with open(sys.argv[1], "r", encoding="utf-8") as fh:
def cli_main(argv) -> int:
"""Thin CLI entry (P1): nova confidence <inputs.json> <environment>."""
if len(argv) < 3:
print("usage: confidence <inputs.json> <environment>", file=sys.stderr)
return 2
env = argv[2]
with open(argv[1], "r", encoding="utf-8") as fh:
inputs = json.load(fh)
sig = compute("cli", env, inputs)
print(json.dumps(asdict(sig), indent=2))
print(json.dumps(asdict(sig), indent=2))
return 0
if __name__ == "__main__":
sys.exit(cli_main(sys.argv))
+98 -4
View File
@@ -1,4 +1,4 @@
"""Environment helper (D-108, REQ-159, REQ-164).
"""Environment helper (D-108, REQ-159, REQ-164, REQ-330).
During the Nova rebrand transition window (P2P4), `get_env` read
`NOVA_*` preferred with the legacy `ACDL_*` name as the fallback. **P5
@@ -9,14 +9,27 @@ During the Nova rebrand transition window (P2P4), `get_env` read
`.env.secrets` shell export in `scripts/run_platform.sh` and the Python
parser in `core/regression_verify.py`) were updated to NOVA-only in P5
(the G-106 dual-read contract was retired with the fallback).
P2 (REQ-330): `synthesize_local_env(contract_path, environment)` produces
a purely synthetic local env dict (account_id placeholder, region
"local", no real AWS resources) from a contract YAML. Mirrors the shape
of core/environments/*.json (validates against
schemas/environment.schema.json) so `nova apply --local` can run the
contract resolver + Terraform adapter without provisioning cloud
resources. This is the local-tier counterpart of
core/onboarding.py:generate_env_file() (the request-path binding
generator).
"""
from __future__ import annotations
import os
from typing import Optional
from pathlib import Path
from typing import Any, Dict, Optional
__all__ = ["get_env"]
import yaml
__all__ = ["get_env", "synthesize_local_env"]
def get_env(name: str, default: Optional[str] = None) -> Optional[str]:
@@ -28,4 +41,85 @@ def get_env(name: str, default: Optional[str] = None) -> Optional[str]:
val = os.environ.get(f"NOVA_{name}")
if val:
return val
return default
return default
# Default confidence thresholds per environment name (mirrors the schema
# description: dev 0.50, qa 0.75, prod 0.90, dr 0.95). Used by
# synthesize_local_env so the synthetic env matches the real env semantics.
_DEFAULT_THRESHOLDS: Dict[str, float] = {
"dev": 0.50,
"qa": 0.75,
"prod": 0.90,
"dr": 0.95,
}
def synthesize_local_env(
contract_path: str,
environment: Optional[str] = None,
) -> Dict[str, Any]:
"""Synthesize a local env dict from a contract YAML (REQ-330).
Reads the contract YAML (``yaml.safe_load``), derives a placeholder
environment binding that ``nova apply --local`` can use WITHOUT
provisioning real AWS resources. The produced dict:
- ``name`` — the environment name (from the arg or the contract's
``environment`` field, defaulting to ``"dev"``).
- ``account_id`` — ``"000000000000"`` (the schema-allowed placeholder
for an unbound environment; real account id filled by the platform).
- ``region`` — ``"local"`` (the local-tier sentinel; never a real
AWS region).
- ``state_backend`` — ``{bucket: "local-tfstate", lock_table:
"local-locks"}`` (local state; LocalS3StateBackend rewrites the
terraform backend to ``backend "local"`` using the stack name as
the state path, so no S3 bucket is used).
- ``network`` — a local RFC1918 CIDR + a single fake AZ.
- ``runner_role_arn`` — a placeholder ARN for the local tier.
- ``autonomy`` — ``"full"`` (the local tier is autonomous).
- ``confidence_threshold`` — the per-env default (0.50 for dev).
The dict mirrors the shape of ``core/environments/*.json`` and
validates against ``schemas/environment.schema.json``. No cloud
provisioning occurs — purely synthetic.
Args:
contract_path: Path to the contract YAML file.
environment: Optional environment name override (defaults to the
contract's ``environment`` field, or ``"dev"``).
Returns:
The synthetic local env dict.
"""
contract_path_obj = Path(contract_path)
contract: Dict[str, Any] = {}
if contract_path_obj.is_file():
with open(contract_path_obj) as fh:
contract = yaml.safe_load(fh) or {}
env_name = environment or contract.get("environment", "dev")
stack_name = contract.get("id", env_name)
threshold = _DEFAULT_THRESHOLDS.get(env_name, 0.50)
return {
"name": env_name,
"description": (
f"Synthetic local-tier environment for contract '{stack_name}' "
f"(environment={env_name}). No real AWS resources — generated "
f"by core.env.synthesize_local_env (REQ-330) for nova apply --local."
),
"account_id": "000000000000",
"region": "local",
"state_backend": {
"bucket": "local-tfstate",
"lock_table": "local-locks",
},
"network": {
"vpc_cidr": "10.250.0.0/16",
"azs": ["local-a"],
},
"runner_role_arn": "arn:aws:iam::000000000000:role/local-runner",
"autonomy": "full",
"confidence_threshold": threshold,
}
+51
View File
@@ -0,0 +1,51 @@
"""Nova init scaffolding logic (P1, REQ-325).
Creates .nova/ directory structure + secrets-exclusion .gitignore lines
in the current working directory. nova/init.py delegates here so the
subcommand stays thin (≤50 lines, ≤3 functions).
"""
from __future__ import annotations
from pathlib import Path
SECRETS_IGNORE_LINES = (
"~/.nova/credentials.json",
".nova/credentials.json",
"*.pem",
"*.key",
".env",
".env.*",
)
def _ensure_gitignore(root: Path, force: bool) -> None:
gi = root / ".gitignore"
existing = gi.read_text().splitlines() if gi.is_file() else []
additions = [ln for ln in SECRETS_IGNORE_LINES if ln not in existing]
if not additions:
return
blob = gi.read_text() if gi.is_file() else ""
if blob and not blob.endswith("\n"):
blob += "\n"
blob += "\n".join(additions) + "\n"
gi.write_text(blob)
def scaffold(root: Path | None = None, force: bool = False) -> int:
"""Create .nova/ + .nova/contract.yml.attestations/ + .gitignore lines."""
root = root or Path.cwd()
nova_dir = root / ".nova"
attest_dir = nova_dir / "contract.yml.attestations"
if nova_dir.exists() and not force:
print(f"refusing: {nova_dir} already exists (use --force to overwrite)")
return 1
nova_dir.mkdir(parents=True, exist_ok=True)
attest_dir.mkdir(parents=True, exist_ok=True)
_ensure_gitignore(root, force)
print(f"scaffolded: {nova_dir} (+ {attest_dir.name}/, .gitignore secrets)")
return 0
if __name__ == "__main__":
raise SystemExit(scaffold())
+213
View File
@@ -0,0 +1,213 @@
"""JWS-from-PAT key derivation + symmetric attestation (REQ-332, C-5.2).
C-5.2 grill fix: the "public key derivable from the PAT" acceptance
criterion is re-interpreted as a SYMMETRIC scheme. The PAT (Personal
Access Token) is the shared secret; the JWS signing key AND the
verification key are both derived from the PAT via the same HKDF-SHA256
KDF. The JWS uses HMAC-SHA256 (HS256) — a symmetric MAC, not an
asymmetric signature.
Key derivation (NIST SP 800-56C / RFC 5869):
key = HKDF-SHA256(
input_key_material = PAT.encode(),
salt = b"nova-local-attestation",
info = b"jws-signing-key",
length = 32,
)
The resulting 32-byte key is used both to sign (sign_attestation) and to
verify (verify_attestation). Anyone holding the PAT can derive the same
key and verify the attestation; without the PAT, the HMAC cannot be
forged. This satisfies INV-14..17:
- INV-14: the signing key is derived from the PAT (no separate key
material; no long-lived private key on disk).
- INV-15: the key never leaves the derivation (it is recomputed from
the PAT on each sign/verify call; not cached, not persisted).
- INV-16: the salt + info are fixed constants binding the key to the
"nova-local-attestation / jws-signing-key" purpose (key separation).
- INV-17: tamper detection via the HMAC verification (verify_attestation
raises on any signature mismatch).
The JWS is the compact serialization:
b64url(header).b64url(payload).b64url(signature)
where header = {"alg":"HS256","typ":"JWT"}, payload = the JWT claims
(the attestation payload dict), and signature = HMAC-SHA256(key,
b64url(header) + "." + b64url(payload)).
"""
from __future__ import annotations
import hashlib
import hmac
import json
from typing import Any, Dict
__all__ = [
"derive_signing_key",
"sign_attestation",
"verify_attestation",
"JWSValidationError",
]
# Fixed KDF parameters (INV-16: key separation — binds the derived key to
# the nova-local-attestation / jws-signing-key purpose).
_KDF_SALT = b"nova-local-attestation"
_KDF_INFO = b"jws-signing-key"
_KDF_LENGTH = 32 # 256-bit key for HMAC-SHA256
# JWS header for HS256 (symmetric HMAC-SHA256).
_JWS_HEADER = {"alg": "HS256", "typ": "JWT"}
class JWSValidationError(Exception):
"""Raised when a JWS attestation fails verification (signature mismatch,
malformed token, or wrong PAT)."""
def _b64url_encode(data: bytes) -> str:
"""RFC 7515 base64url encoding WITHOUT padding (JWS compact form)."""
import base64
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _b64url_decode(segment: str) -> bytes:
"""RFC 7515 base64url decoding (re-adds stripped padding)."""
import base64
pad = "=" * (-len(segment) % 4)
return base64.urlsafe_b64decode(segment + pad)
def _hkdf_sha256(input_key_material: bytes, salt: bytes, info: bytes, length: int) -> bytes:
"""HKDF-SHA256 (RFC 5869).
Prefers cryptography.hazmat.primitives.kdf.hkdf.HKDF (the cryptography
extra); falls back to a hashlib-based implementation if cryptography
is unavailable (so the module works in a minimal Lambda runtime).
"""
try:
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=length,
salt=salt,
info=info,
)
return hkdf.derive(input_key_material)
except ImportError: # pragma: no cover - fallback path
return _hkdf_sha256_hashlib(input_key_material, salt, info, length)
def _hkdf_sha256_hashlib(input_key_material: bytes, salt: bytes, info: bytes, length: int) -> bytes:
"""RFC 5869 HKDF-SHA256 using only hashlib + hmac (fallback)."""
# Extract: PRK = HMAC-SHA256(salt, IKM)
prk = hmac.new(salt, input_key_material, hashlib.sha256).digest()
# Expand: T(i) = HMAC-SHA256(PRK, T(i-1) | info | i)
okm = b""
t = b""
block = 0
while len(okm) < length:
block += 1
t = hmac.new(prk, t + info + bytes([block]), hashlib.sha256).digest()
okm += t
return okm[:length]
def derive_signing_key(pat: str) -> bytes:
"""Derive the 32-byte symmetric JWS signing key from a PAT.
HKDF-SHA256(PAT.encode(), salt=b'nova-local-attestation',
info=b'jws-signing-key', length=32).
The same PAT always yields the same key (deterministic); the key is
never cached or persisted (INV-15 — recomputed on each call).
"""
if not isinstance(pat, str) or not pat:
raise ValueError("pat must be a non-empty string")
return _hkdf_sha256(
input_key_material=pat.encode("utf-8"),
salt=_KDF_SALT,
info=_KDF_INFO,
length=_KDF_LENGTH,
)
def sign_attestation(payload: Dict[str, Any], pat: str) -> str:
"""Produce a compact JWS (HS256) for the attestation payload.
Args:
payload: the JWT claims (the attestation payload dict).
pat: the Personal Access Token (shared secret).
Returns:
The compact JWS string: b64url(header).b64url(payload).b64url(signature).
The header is {"alg":"HS256","typ":"JWT"}; the payload is the
JSON-encoded claims; the signature is HMAC-SHA256(key, header.payload).
"""
if not isinstance(payload, dict):
raise ValueError("payload must be a dict")
key = derive_signing_key(pat)
header_segment = _b64url_encode(
json.dumps(_JWS_HEADER, separators=(",", ":"), sort_keys=True).encode("utf-8")
)
payload_segment = _b64url_encode(
json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
)
signing_input = f"{header_segment}.{payload_segment}".encode("ascii")
signature = hmac.new(key, signing_input, hashlib.sha256).digest()
signature_segment = _b64url_encode(signature)
return f"{header_segment}.{payload_segment}.{signature_segment}"
def verify_attestation(jws: str, pat: str) -> Dict[str, Any]:
"""Verify a compact JWS (HS256) attestation and return the payload.
Derives the same key from the PAT, recomputes the HMAC, and compares
in constant time. Raises JWSValidationError on:
- malformed JWS (not 3 segments, bad base64, bad JSON)
- signature mismatch (tampering or wrong PAT)
- wrong header (alg != HS256)
Args:
jws: the compact JWS string from sign_attestation.
pat: the Personal Access Token (shared secret).
Returns:
The decoded payload dict (the JWT claims) on success.
"""
if not isinstance(jws, str) or not jws:
raise JWSValidationError("jws must be a non-empty string")
parts = jws.split(".")
if len(parts) != 3:
raise JWSValidationError(f"malformed JWS: expected 3 segments, got {len(parts)}")
header_segment, payload_segment, signature_segment = parts
# Decode + validate the header.
try:
header = json.loads(_b64url_decode(header_segment))
except (ValueError, json.JSONDecodeError) as e:
raise JWSValidationError(f"malformed JWS header: {e}") from e
if not isinstance(header, dict) or header.get("alg") != "HS256":
raise JWSValidationError(
f"unsupported JWS alg: expected HS256, got {header.get('alg')!r}"
)
# Recompute the signature with the key derived from the PAT.
key = derive_signing_key(pat)
signing_input = f"{header_segment}.{payload_segment}".encode("ascii")
expected_signature = hmac.new(key, signing_input, hashlib.sha256).digest()
actual_signature = _b64url_decode(signature_segment)
if not hmac.compare_digest(expected_signature, actual_signature):
raise JWSValidationError(
"JWS signature verification failed (tampered token or wrong PAT)"
)
# Decode + return the payload.
try:
payload = json.loads(_b64url_decode(payload_segment))
except (ValueError, json.JSONDecodeError) as e:
raise JWSValidationError(f"malformed JWS payload: {e}") from e
if not isinstance(payload, dict):
raise JWSValidationError("JWS payload is not a JSON object")
return payload
+151
View File
@@ -0,0 +1,151 @@
"""KMS-signed JWT issuance for the Nova IdP (REQ-337, REQ-336).
Signs OIDC tokens with an AWS KMS asymmetric key (``ECC_NIST_P256``,
``ECDSA_SHA_256`` → JWS ``ES256``) and exposes the public key as a JWK
for the JWKS endpoint (REQ-338).
## DER → raw ECDSA conversion (the #1 gotcha, RESEARCH §5)
KMS ``sign()`` returns a **DER-encoded** ASN.1 ECDSA signature. JWS
(RFC 7515 §3.1.3) requires the **raw** ``r‖s`` concatenation, each
coordinate 32 bytes big-endian. :func:`der_to_raw_ecdsa` performs the
conversion via ``cryptography``'s ``decode_dss_signature``. This is the
core of REQ-337 and is verified by the CAP-037 round-trip test.
## Lazy boto3
``boto3.client("kms")`` is constructed lazily so the module imports
without AWS creds (mirrors ``nova_idp_auth.py``). Tests inject a mock
client via :func:`set_kms_client_for_testing`.
"""
from __future__ import annotations
import base64
import json
import os
from typing import Any
import boto3
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from cryptography.hazmat.primitives.asymmetric.ec import (
EllipticCurvePublicKey,
)
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.hazmat.primitives.asymmetric import ec
# Default KMS key alias for Nova OIDC signing (REQ-337).
DEFAULT_KEY_ID = os.environ.get("NOVA_OIDC_KMS_KEY_ID", "alias/nova-oidc-signing")
_kms_client = None
def _get_kms_client():
"""Lazy boto3 KMS client singleton (mirrors nova_idp_auth.py)."""
global _kms_client
if _kms_client is None:
_kms_client = boto3.client("kms")
return _kms_client
def set_kms_client_for_testing(client: Any) -> None:
"""Inject a mock KMS client for tests (no real AWS calls)."""
global _kms_client
_kms_client = client
def _b64url(data: bytes) -> str:
"""Base64url encode without padding (RFC 7515 §2)."""
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def der_to_raw_ecdsa(der_sig: bytes, coord_len: int = 32) -> bytes:
"""Convert a DER-encoded ECDSA signature to raw ``r‖s`` (JWS format).
KMS returns DER; JWS requires raw ``r‖s`` concatenation, each
coordinate ``coord_len`` bytes big-endian (32 for P-256, 48 for
P-384). Uses ``cryptography``'s ``decode_dss_signature`` to parse
the DER, then zero-pads each integer to ``coord_len``.
Raises:
ValueError: if a coordinate does not fit in ``coord_len`` bytes
(the integer is larger than the curve allows — indicates a
malformed signature or wrong ``coord_len``).
"""
r, s = decode_dss_signature(der_sig)
if r.bit_length() > coord_len * 8 or s.bit_length() > coord_len * 8:
raise ValueError(
f"ECDSA coordinate does not fit in {coord_len} bytes "
f"(r={r.bit_length()} bits, s={s.bit_length()} bits)"
)
return r.to_bytes(coord_len, "big") + s.to_bytes(coord_len, "big")
def sign_jwt(claims: dict, key_id: str = DEFAULT_KEY_ID) -> str:
"""Build + sign a JWT with KMS (REQ-337, REQ-336).
Args:
claims: the JWT claims payload (``sub, aud, iss, exp, iat, jti,
roles`` per REQ-336, plus ``typ`` for PATs).
key_id: the KMS key ID or alias (default
``alias/nova-oidc-signing``).
Returns:
The compact JWS (``header.payload.signature``), ``ES256``,
with the signature in raw ``r‖s`` form (DER→raw converted).
"""
header = {"alg": "ES256", "typ": "JWT", "kid": key_id}
signing_input = (
_b64url(json.dumps(header, separators=(",", ":"), sort_keys=True).encode())
+ "."
+ _b64url(json.dumps(claims, separators=(",", ":"), sort_keys=True).encode())
)
resp = _get_kms_client().sign(
KeyId=key_id,
Message=signing_input.encode("ascii"),
MessageType="RAW",
SigningAlgorithm="ECDSA_SHA_256",
)
der_sig = resp["Signature"]
raw_sig = der_to_raw_ecdsa(der_sig)
return signing_input + "." + _b64url(raw_sig)
def get_jwk(key_id: str = DEFAULT_KEY_ID) -> dict:
"""Fetch the KMS public key and return it as a JWK (REQ-338).
Calls ``kms.get_public_key`` → DER SPKI → ``cryptography``'s
``load_der_public_key`` → JWK ``{"kty":"EC","crv":"P-256","kid":...,
"x":...,"y":...}``. The ``x``/``y`` are base64url-encoded
big-endian 32-byte coordinates.
"""
resp = _get_kms_client().get_public_key(KeyId=key_id)
pub = load_der_public_key(resp["PublicKey"])
if not isinstance(pub, EllipticCurvePublicKey):
raise ValueError(
f"KMS public key is not an EC key (got {type(pub).__name__})"
)
nums = pub.public_numbers()
# P-256 coordinates are 32 bytes big-endian.
x = nums.x.to_bytes(32, "big")
y = nums.y.to_bytes(32, "big")
return {
"kty": "EC",
"crv": "P-256",
"kid": key_id,
"x": _b64url(x),
"y": _b64url(y),
"alg": "ES256",
"use": "sig",
}
if __name__ == "__main__": # pragma: no cover - CLI inspection helper
import sys
if "--print-jwk" in sys.argv:
print(json.dumps(get_jwk(), indent=2))
else:
print("usage: python3 -m core.kms_signing --print-jwks", file=sys.stderr)
+126 -42
View File
@@ -457,65 +457,149 @@ def _onboard_consumer(payload):
}
def dispatch_action(payload, event=None):
"""Shared business-logic dispatch for the contract ingestor (REQ-329).
Both the AWS Lambda handler (``lambda_handler``) and the CLI path
(``cli_main`` / ``__main__``) call this function so the two paths share
a single source of truth for action routing, contract validation, the
DynamoDB write, and error reporting (NFR-7 — dual-use, single source).
Args:
payload: the decoded action envelope dict
``{ consumerRepo, contractId, contract, environment, action }``.
event: the raw Lambda Function-URL event (used for IAM caller
identity validation). When ``None`` (the CLI path), the identity
check uses the ``NOVA_LAMBDA_LOCAL_BYPASS`` env var — CLI invocations
are local-only and do not carry an IAM principal.
Returns:
The action result dict (e.g. ``{status, contractId, action, ...}``)
on success. Raises ``ValueError`` for validation failures and other
exceptions for downstream errors — the caller is responsible for
mapping these to the appropriate status code / exit code.
"""
action = payload.get("action", "submit_contract")
# Validate caller identity against the payload (P1-2). The CLI path
# passes event=None; the fail-closed check honours the local bypass.
_validate_caller_identity(event or {}, payload)
if action == "submit_contract":
# Validate required fields up front for a clean 400.
for field in ("consumerRepo", "contractId", "contract", "environment"):
if field not in payload:
raise ValueError(f"missing field: {field}")
result = _submit_contract(payload)
elif action == "report_error":
result = _report_error(payload)
elif action == "validate_change_request":
result = _validate_change_request(payload)
elif action == "onboard_consumer":
result = _onboard_consumer(payload)
else:
raise ValueError(f"unknown action: {action}")
return result
def _to_http_response(result_or_error):
"""Map a dispatch_action result / exception to a Lambda HTTP response.
Shared error→status mapping so both Lambda + CLI paths interpret errors
identically (REQ-329 dual-use).
"""
if isinstance(result_or_error, Exception):
msg = str(result_or_error)
if isinstance(result_or_error, ValueError):
if "missing IAM caller identity" in msg:
return {"statusCode": 401, "body": json.dumps({"error": msg})}
return {"statusCode": 400, "body": json.dumps({"error": msg})}
return {"statusCode": 500, "body": json.dumps({"error": msg})}
return {"statusCode": 200, "body": json.dumps(result_or_error)}
def lambda_handler(event, context):
"""AWS Lambda handler entry point.
"""AWS Lambda handler entry point (thin wrapper, REQ-329 dual-use).
Accepts a Function-URL-style event whose ``body`` is a JSON string
containing ``{ consumerRepo, contractId, contract, environment, action }``.
Parses the Lambda-specific envelope then delegates to the shared
``dispatch_action`` business logic.
"""
try:
body = event.get("body", "{}")
if isinstance(body, str):
payload = json.loads(body)
else:
payload = body
action = payload.get("action", "submit_contract")
# Validate caller identity against the payload (P1-2).
_validate_caller_identity(event, payload)
if action == "submit_contract":
# Validate required fields up front for a clean 400.
for field in ("consumerRepo", "contractId", "contract", "environment"):
if field not in payload:
return {
"statusCode": 400,
"body": json.dumps({"error": f"missing field: {field}"}),
}
result = _submit_contract(payload)
elif action == "report_error":
result = _report_error(payload)
elif action == "validate_change_request":
result = _validate_change_request(payload)
elif action == "onboard_consumer":
result = _onboard_consumer(payload)
else:
return {
"statusCode": 400,
"body": json.dumps({"error": f"unknown action: {action}"}),
}
return {"statusCode": 200, "body": json.dumps(result)}
except ValueError as e:
# P10 (REQ-174): identity failures are 401, field validation is 400.
if "missing IAM caller identity" in str(e):
return {"statusCode": 401, "body": json.dumps({"error": str(e)})}
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
payload = json.loads(body) if isinstance(body, str) else body
result = dispatch_action(payload, event=event)
return _to_http_response(result)
except Exception as e: # pragma: no cover - defensive top-level guard
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
return _to_http_response(e)
# --- CLI: --check-readiness (D-133, REQ-218) ---------------------------
# Invoked as: python3 -m core.lambda.contract_ingestor --check-readiness <submission.json>
# Delegates to core.submission_readiness.check_readiness() and prints the
# structured ReadinessResult. Exits 0 if ready, 1 if not.
def cli_main(argv=None):
"""CLI entry point for the contract ingestor (REQ-329 dual-use).
Usage:
python3 -m core.lambda.contract_ingestor --dispatch <payload.json>
python3 -m core.lambda.contract_ingestor --dispatch-stdin < <payload.json>
Parses the CLI-specific input (a JSON file path or stdin) then delegates
to the shared ``dispatch_action`` business logic — the same path as the
Lambda handler. Returns a process exit code (0 success, 1 validation
error, 2 internal error).
"""
import sys
raw = argv if argv is not None else sys.argv[1:]
# The --dispatch flag consumes the next positional arg as a payload path;
# --dispatch-stdin reads the payload from stdin.
if "--dispatch-stdin" in raw:
payload = json.loads(sys.stdin.read())
elif "--dispatch" in raw:
idx = raw.index("--dispatch")
path = raw[idx + 1] if idx + 1 < len(raw) else None
if not path:
print("Usage: --dispatch <payload.json>", file=sys.stderr)
return 2
with open(path) as fh:
payload = json.loads(fh.read())
else:
print(
"Usage: python3 -m core.lambda.contract_ingestor --dispatch <payload.json>",
file=sys.stderr,
)
return 2
try:
result = dispatch_action(payload, event=None)
sys.stdout.write(json.dumps(result, indent=2) + "\n")
return 0
except ValueError as e:
sys.stderr.write(f"error: {e}\n")
return 1
except Exception as e: # pragma: no cover - defensive top-level guard
sys.stderr.write(f"internal error: {e}\n")
return 2
# --- CLI: --check-readiness (D-133, REQ-218) + --dispatch (REQ-329) ----
# Invoked as:
# python3 -m core.lambda.contract_ingestor --check-readiness <submission.json>
# python3 -m core.lambda.contract_ingestor --dispatch <payload.json>
# The --check-readiness path delegates to core.submission_readiness; the
# --dispatch path is the dual-use CLI entry (REQ-329) that calls the same
# dispatch_action() as the Lambda handler.
if __name__ == "__main__": # pragma: no cover - CLI entry
import sys
if "--check-readiness" in sys.argv:
sys.path.insert(
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
from core.submission_readiness import cli_main
from core.submission_readiness import cli_main as _readiness_cli
# Strip the --check-readiness flag; pass the file path.
rest = [a for a in sys.argv[1:] if a != "--check-readiness"]
sys.exit(cli_main(["check-readiness"] + rest))
sys.exit(_readiness_cli(["check-readiness"] + rest))
elif "--dispatch" in sys.argv or "--dispatch-stdin" in sys.argv:
sys.exit(cli_main())
else:
print("Usage: python3 -m core.lambda.contract_ingestor --check-readiness <submission.json>")
print(
"Usage: python3 -m core.lambda.contract_ingestor "
"--check-readiness <submission.json> | --dispatch <payload.json>",
file=sys.stderr,
)
+613
View File
@@ -0,0 +1,613 @@
"""Nova IdP auth Lambda — sign-up / sign-in / session (REQ-333, REQ-334).
Invoked via a Function URL (IAM auth) by the Nova CLI and consumer
pipelines. Mirrors the ``contract_ingestor.py`` pattern: lazy
``boto3.resource`` DynamoDB singleton, env-var table names,
``NOVA_LAMBDA_LOCAL_BYPASS`` for local testing, ``__main__`` CLI block
for dual-use (REQ-329).
## Argon2id password hashing (REQ-334, D-228, C-7.2)
Passwords are hashed with Argon2id via ``argon2-cffi``:
PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
These are the OWASP minimum parameters (t=3, m=65536 KiB, p=1).
Lambda memory **MUST be 512 MB** (Argon2id memory_cost ~64 MiB +
runtime overhead).
**D-228 (amended) fail-closed:** there is no maintained pure-Python
Argon2 implementation; a pure-Python crypto fallback is a liability
(weaker hashing, violates INV-16's spirit). If the ``argon2`` C
extension fails to import, the Lambda **fails closed**
``_ARGON2_AVAILABLE`` is set ``False`` at cold-start, and
:func:`hash_password` / :func:`verify_password` raise
``Argon2UnavailableError``. The handler catches this and returns
**HTTP 503** (``{"error": "argon2_unavailable"}``) **no pure-Python
fallback, no weak hash, no crash.** This is verified by the explicit
``test_argon2_fail_closed`` test (C-1.2).
## No raw passwords anywhere (INV-16)
Raw passwords are NEVER:
* written to DynamoDB (only ``password_hash`` is stored),
* logged (the handler never logs the password argument),
* put in traces / env vars / X-Ray segments.
Audit events (``auth.sign_up``, ``auth.sign_in``,
``auth.session_created``) are emitted to stderr as JSON; they carry the
``user_id`` / ``email`` but **never** the password.
"""
from __future__ import annotations
import datetime
import json
import os
import sys
import uuid
import boto3
# ---------------------------------------------------------------------------
# Argon2id — fail-closed import (REQ-334, D-228, C-7.2)
# ---------------------------------------------------------------------------
#
# try-import the C extension. If it fails (missing abi3 wheel, wrong
# glibc, etc.), _ARGON2_AVAILABLE becomes False and hash/verify raise
# Argon2UnavailableError. The handler returns 503. NO pure-Python fallback.
_ARGON2_AVAILABLE = False
_PasswordHasher = None
try: # pragma: no cover - import success path covered by round-trip test
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
_PasswordHasher = PasswordHasher
_ARGON2_AVAILABLE = True
except ImportError: # pragma: no cover - exercised via mock in tests
_ARGON2_AVAILABLE = False
# Define a stand-in so `verify_password` can raise the right type
# even when argon2 isn't importable. VerifyMismatchError is only
# raised by verify() which itself raises Argon2UnavailableError first.
class VerifyMismatchError(Exception):
"""Raised by verify_password when the password does not match."""
class Argon2UnavailableError(Exception):
"""Raised when the Argon2 C extension is unavailable (D-228 fail-closed).
The handler catches this and returns HTTP 503 no pure-Python
fallback, no weak hash.
"""
# OWASP-minimum Argon2id parameters (C-7.2):
# time_cost=3, memory_cost=65536 KiB (64 MiB), parallelism=1
_ARGON2_TIME_COST = 3
_ARGON2_MEMORY_COST = 65536 # KiB
_ARGON2_PARALLELISM = 1
def _get_hasher():
"""Return a PasswordHasher configured with the OWASP-min params.
Raises Argon2UnavailableError if the C extension is not loaded.
"""
if not _ARGON2_AVAILABLE or _PasswordHasher is None:
raise Argon2UnavailableError(
"argon2 C extension unavailable — refusing to hash with a "
"weak fallback (D-228 fail-closed)"
)
return _PasswordHasher(
time_cost=_ARGON2_TIME_COST,
memory_cost=_ARGON2_MEMORY_COST,
parallelism=_ARGON2_PARALLELISM,
)
def hash_password(password: str) -> str:
"""Hash a password with Argon2id (OWASP-min params).
Returns the Argon2id hash string (includes the salt + params).
Raises:
Argon2UnavailableError: if the ``argon2`` C extension is not
importable (D-228 fail-closed NO pure-Python fallback).
"""
if not _ARGON2_AVAILABLE:
raise Argon2UnavailableError(
"argon2 C extension unavailable — refusing to hash (D-228)"
)
# NOTE: the password argument is NEVER logged. Do not add debug
# prints here that include `password`.
return _get_hasher().hash(password)
def verify_password(password: str, hash_str: str) -> bool:
"""Verify a password against an Argon2id hash.
Returns ``True`` if the password matches.
Raises:
Argon2UnavailableError: if the ``argon2`` C extension is not
importable.
VerifyMismatchError: if the password does not match the hash.
"""
if not _ARGON2_AVAILABLE:
raise Argon2UnavailableError(
"argon2 C extension unavailable — refusing to verify (D-228)"
)
# argon2.PasswordHasher().verify raises VerifyMismatchError on
# mismatch (and InvalidHash on a malformed hash). We let those
# propagate; the handler maps them to 401 / 500.
_get_hasher().verify(hash_str, password)
return True
# ---------------------------------------------------------------------------
# Config (env-var table names, mirroring contract_ingestor.py)
# ---------------------------------------------------------------------------
USERS_TABLE = os.environ.get("NOVA_USERS_TABLE", "nova-users")
SESSIONS_TABLE = os.environ.get("NOVA_SESSIONS_TABLE", "nova-sessions")
PASSWORD_RESETS_TABLE = os.environ.get(
"NOVA_PASSWORD_RESETS_TABLE", "nova-password-resets"
)
# Session lifetime (seconds). Default 24h.
SESSION_TTL_SECONDS = int(os.environ.get("NOVA_SESSION_TTL_SECONDS", "86400"))
# Password-reset token lifetime (seconds). Default 15 min.
RESET_TTL_SECONDS = int(os.environ.get("NOVA_RESET_TTL_SECONDS", "900"))
_dynamodb = None
def _get_dynamodb():
"""Lazy boto3 DynamoDB resource singleton (mirrors contract_ingestor)."""
global _dynamodb
if _dynamodb is None:
_dynamodb = boto3.resource("dynamodb")
return _dynamodb
def _iso8601_now() -> str:
return datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
def _epoch_now() -> int:
return int(datetime.datetime.now(datetime.timezone.utc).timestamp())
def _emit_audit(event_type: str, **fields) -> None:
"""Emit an audit event to stderr as JSON (never includes passwords)."""
payload = {"event": event_type, "ts": _iso8601_now(), **fields}
# Defense-in-depth: scrub any field literally named 'password' or
# 'password_hash' value from the audit payload (they should never be
# passed here, but a stray kwarg would leak — INV-16).
for _k in ("password", "new_password", "old_password"):
payload.pop(_k, None)
sys.stderr.write(json.dumps(payload, sort_keys=True) + "\n")
sys.stderr.flush()
# ---------------------------------------------------------------------------
# Business logic (sign_up / sign_in / create_session / reset flows)
# ---------------------------------------------------------------------------
def _require(fields, payload):
"""Validate required fields; raise ValueError (→ 400) if missing."""
for f in fields:
if f not in payload or payload[f] in (None, ""):
raise ValueError(f"missing field: {f}")
def _lookup_user_by_email(email: str):
"""Query nova-users GSI1 (email-index) → return the user item or None."""
table = _get_dynamodb().Table(USERS_TABLE)
resp = table.query(
IndexName="email-index",
KeyConditionExpression="email = :e",
ExpressionAttributeValues={":e": email},
Limit=1,
)
items = resp.get("Items", [])
return items[0] if items else None
def sign_up(payload):
"""Create a new user. Fails closed (503) if argon2 is unavailable.
Payload: { email, password, owner, roles }
Writes to nova-users: PK user_id (uuid4), email, password_hash,
owner, roles, created_at. The raw password is NEVER stored.
"""
_require(("email", "password", "owner", "roles"), payload)
if not _ARGON2_AVAILABLE:
raise Argon2UnavailableError("argon2 unavailable")
email = payload["email"]
password = payload["password"]
owner = payload["owner"]
roles = payload["roles"]
if not isinstance(roles, list):
raise ValueError("roles must be a list")
# Duplicate-email check → 409.
if _lookup_user_by_email(email) is not None:
raise _DuplicateEmailError(email)
user_id = str(uuid.uuid4())
password_hash = hash_password(password) # fail-closed here
created_at = _iso8601_now()
item = {
"user_id": user_id,
"email": email,
"password_hash": password_hash,
"owner": owner,
"roles": roles,
"created_at": created_at,
}
table = _get_dynamodb().Table(USERS_TABLE)
table.put_item(TableName=USERS_TABLE, Item=item)
_emit_audit("auth.sign_up", user_id=user_id, email=email)
return {
"status": "ok",
"action": "sign_up",
"user_id": user_id,
"email": email,
"created_at": created_at,
}
class _DuplicateEmailError(Exception):
"""Raised when sign_up is called with an already-registered email → 409."""
def __init__(self, email: str):
self.email = email
super().__init__(f"email already registered: {email}")
def create_session(user_id: str) -> str:
"""Create a session row in nova-sessions; return the session_id.
TTL: expires_at = now + SESSION_TTL_SECONDS (epoch seconds).
"""
session_id = str(uuid.uuid4())
now = _epoch_now()
expires_at = now + SESSION_TTL_SECONDS
created_at = _iso8601_now()
table = _get_dynamodb().Table(SESSIONS_TABLE)
table.put_item(
TableName=SESSIONS_TABLE,
Item={
"session_id": session_id,
"user_id": user_id,
"expires_at": expires_at,
"created_at": created_at,
},
)
_emit_audit("auth.session_created", user_id=user_id, session_id=session_id)
return session_id
def sign_in(payload):
"""Sign in by email + password → return a session_id.
On wrong password raises VerifyMismatchError ( 401).
On unknown email raises _UnknownUserError ( 401, same code to
avoid user-enumeration via timing the message is generic).
On argon2 unavailable Argon2UnavailableError ( 503).
"""
_require(("email", "password"), payload)
if not _ARGON2_AVAILABLE:
raise Argon2UnavailableError("argon2 unavailable")
email = payload["email"]
password = payload["password"]
user = _lookup_user_by_email(email)
if user is None:
# Generic 401 — do not reveal whether the email is registered
# (user-enumeration defense).
raise _UnknownUserError("invalid credentials")
try:
verify_password(password, user["password_hash"])
except VerifyMismatchError:
raise _UnknownUserError("invalid credentials")
session_id = create_session(user["user_id"])
_emit_audit("auth.sign_in", user_id=user["user_id"], email=email)
return {
"status": "ok",
"action": "sign_in",
"user_id": user["user_id"],
"session_id": session_id,
}
class _UnknownUserError(Exception):
"""Generic 'invalid credentials' — 401 (no user enumeration)."""
def request_password_reset(payload):
"""Generate a reset token (uuid4) → write to nova-password-resets (15 min TTL).
Returns the token directly (in a real system this would be emailed;
for v1.28 it is returned so tests / the CLI can drive reset_password).
"""
_require(("email",), payload)
email = payload["email"]
user = _lookup_user_by_email(email)
if user is None:
# Return ok regardless (no user enumeration via reset endpoint).
# We still return a (fake) token shape so the response is uniform;
# the token is single-use and reset_password validates against DDB.
_emit_audit("auth.password_reset_requested", email=email, found=False)
return {
"status": "ok",
"action": "request_password_reset",
"reset_token": None,
"message": "if the email is registered, a reset token was issued",
}
reset_token = str(uuid.uuid4())
now = _epoch_now()
expires_at = now + RESET_TTL_SECONDS
table = _get_dynamodb().Table(PASSWORD_RESETS_TABLE)
table.put_item(
TableName=PASSWORD_RESETS_TABLE,
Item={
"reset_token": reset_token,
"user_id": user["user_id"],
"expires_at": expires_at,
"created_at": _iso8601_now(),
},
)
_emit_audit(
"auth.password_reset_requested",
user_id=user["user_id"],
email=email,
found=True,
)
return {
"status": "ok",
"action": "request_password_reset",
"reset_token": reset_token,
"expires_at": expires_at,
}
def reset_password(payload):
"""Validate a reset token → set a new password → delete the token.
Payload: { reset_token, new_password }
On invalid/expired token ValueError ( 400).
On argon2 unavailable Argon2UnavailableError ( 503).
"""
_require(("reset_token", "new_password"), payload)
if not _ARGON2_AVAILABLE:
raise Argon2UnavailableError("argon2 unavailable")
reset_token = payload["reset_token"]
new_password = payload["new_password"]
resets = _get_dynamodb().Table(PASSWORD_RESETS_TABLE)
resp = resets.get_item(
TableName=PASSWORD_RESETS_TABLE,
Key={"reset_token": reset_token},
)
item = resp.get("Item")
if not item:
raise ValueError("invalid or expired reset token")
if item.get("expires_at", 0) < _epoch_now():
# Token expired (TTL may not have reaped it yet).
raise ValueError("reset token expired")
user_id = item["user_id"]
new_hash = hash_password(new_password) # fail-closed
users = _get_dynamodb().Table(USERS_TABLE)
users.update_item(
TableName=USERS_TABLE,
Key={"user_id": user_id},
UpdateExpression="SET password_hash = :h",
ExpressionAttributeValues={":h": new_hash},
)
resets.delete_item(
TableName=PASSWORD_RESETS_TABLE,
Key={"reset_token": reset_token},
)
_emit_audit("auth.password_reset", user_id=user_id)
return {
"status": "ok",
"action": "reset_password",
"user_id": user_id,
}
# ---------------------------------------------------------------------------
# Dispatch (shared by Lambda handler + CLI — REQ-329 dual-use)
# ---------------------------------------------------------------------------
def dispatch_action(payload, event=None):
"""Shared business-logic dispatch for the IdP auth Lambda (REQ-329).
Both the AWS Lambda handler (``lambda_handler``) and the CLI path
(``cli_main`` / ``__main__``) call this so the two paths share a
single source of truth for action routing.
Args:
payload: the decoded action envelope dict, e.g.
``{ action: "sign_up", email, password, owner, roles }``.
event: the raw Lambda Function-URL event (unused for identity
the IAM auth is enforced at the Function URL layer; kept for
signature symmetry with contract_ingestor).
Returns:
The action result dict on success. Raises on error the caller
maps exceptions to status codes via :func:`_to_http_response`.
"""
action = payload.get("action")
if action == "sign_up":
return sign_up(payload)
if action == "sign_in":
return sign_in(payload)
if action == "create_session":
_require(("user_id",), payload)
sid = create_session(payload["user_id"])
return {"status": "ok", "action": "create_session", "session_id": sid}
if action == "request_password_reset":
return request_password_reset(payload)
if action == "reset_password":
return reset_password(payload)
raise ValueError(f"unknown action: {action!r}")
def _to_http_response(result_or_error):
"""Map a dispatch result / exception to a Lambda HTTP response."""
if isinstance(result_or_error, Exception):
# Fail-closed: argon2 unavailable → 503 (NO weak hash, NO crash).
if isinstance(result_or_error, Argon2UnavailableError):
return {
"statusCode": 503,
"body": json.dumps({"error": "argon2_unavailable"}),
}
if isinstance(result_or_error, _DuplicateEmailError):
return {
"statusCode": 409,
"body": json.dumps({"error": "email_already_registered"}),
}
if isinstance(result_or_error, _UnknownUserError):
return {
"statusCode": 401,
"body": json.dumps({"error": "invalid_credentials"}),
}
if isinstance(result_or_error, ValueError):
return {
"statusCode": 400,
"body": json.dumps({"error": str(result_or_error)}),
}
return {
"statusCode": 500,
"body": json.dumps({"error": str(result_or_error)}),
}
return {"statusCode": 200, "body": json.dumps(result_or_error)}
def lambda_handler(event, context):
"""AWS Lambda handler entry point (thin wrapper, REQ-329 dual-use).
Accepts a Function-URL-style event whose ``body`` is a JSON string
containing ``{ action, email, password, ... }``. Parses the envelope
then delegates to :func:`dispatch_action`.
"""
# Fail-closed fast-path: if argon2 is unavailable, sign_up / sign_in /
# reset_password all raise Argon2UnavailableError which maps to 503.
# We do NOT short-circuit here so non-password actions (create_session)
# still work when argon2 is down — only the hashing paths fail closed.
try:
body = event.get("body", "{}")
payload = json.loads(body) if isinstance(body, str) else body
result = dispatch_action(payload, event=event)
return _to_http_response(result)
except Exception as e:
return _to_http_response(e)
# ---------------------------------------------------------------------------
# CLI (dual-use, REQ-329 pattern)
# ---------------------------------------------------------------------------
def cli_main(argv=None):
"""CLI entry point for the IdP auth Lambda (REQ-329 dual-use).
Usage:
python3 -m core.lambda.nova_idp_auth --sign-up <email> <password> <owner>
python3 -m core.lambda.nova_idp_auth --sign-in <email> <password>
python3 -m core.lambda.nova_idp_auth --create-session <user_id>
python3 -m core.lambda.nova_idp_auth --request-reset <email>
python3 -m core.lambda.nova_idp_auth --reset-password <token> <new_password>
python3 -m core.lambda.nova_idp_auth --dispatch <payload.json>
python3 -m core.lambda.nova_idp_auth --dispatch-stdin < <payload.json>
"""
import sys
raw = argv if argv is not None else sys.argv[1:]
local_bypass = os.environ.get("NOVA_LAMBDA_LOCAL_BYPASS")
if not local_bypass:
os.environ["NOVA_LAMBDA_LOCAL_BYPASS"] = "1"
try:
if "--dispatch-stdin" in raw:
payload = json.loads(sys.stdin.read())
elif "--dispatch" in raw:
idx = raw.index("--dispatch")
path = raw[idx + 1] if idx + 1 < len(raw) else None
if not path:
print("Usage: --dispatch <payload.json>", file=sys.stderr)
return 2
with open(path) as fh:
payload = json.loads(fh.read())
elif "--sign-up" in raw:
idx = raw.index("--sign-up")
email, password, owner = raw[idx + 1 : idx + 4]
roles = ["user"]
payload = {
"action": "sign_up",
"email": email,
"password": password,
"owner": owner,
"roles": roles,
}
elif "--sign-in" in raw:
idx = raw.index("--sign-in")
email, password = raw[idx + 1 : idx + 3]
payload = {"action": "sign_in", "email": email, "password": password}
elif "--create-session" in raw:
idx = raw.index("--create-session")
user_id = raw[idx + 1]
payload = {"action": "create_session", "user_id": user_id}
elif "--request-reset" in raw:
idx = raw.index("--request-reset")
email = raw[idx + 1]
payload = {"action": "request_password_reset", "email": email}
elif "--reset-password" in raw:
idx = raw.index("--reset-password")
token, new_password = raw[idx + 1 : idx + 3]
payload = {
"action": "reset_password",
"reset_token": token,
"new_password": new_password,
}
else:
print(
"Usage: python3 -m core.lambda.nova_idp_auth "
"--sign-up <email> <password> <owner> | "
"--sign-in <email> <password> | "
"--dispatch <payload.json>",
file=sys.stderr,
)
return 2
result = dispatch_action(payload, event=None)
sys.stdout.write(json.dumps(result, indent=2) + "\n")
return 0
except Argon2UnavailableError as e:
sys.stderr.write(f"error: {e}\n")
return 3 # 503-class
except ValueError as e:
sys.stderr.write(f"error: {e}\n")
return 1
except _DuplicateEmailError as e:
sys.stderr.write(f"error: {e}\n")
return 9 # 409-class
except _UnknownUserError as e:
sys.stderr.write(f"error: {e}\n")
return 1 # 401-class
except Exception as e: # pragma: no cover - defensive top-level guard
sys.stderr.write(f"internal error: {e}\n")
return 2
finally:
if not local_bypass:
os.environ.pop("NOVA_LAMBDA_LOCAL_BYPASS", None)
if __name__ == "__main__": # pragma: no cover - CLI entry
import sys
sys.exit(cli_main())
+244
View File
@@ -0,0 +1,244 @@
"""CloudFormation snippet for the Nova IdP DynamoDB identity schema (REQ-335).
This module exports :func:`dynamodb_tables_snippet`, which returns a
CloudFormation fragment (a plain ``dict``) defining the four DynamoDB
tables that back the Nova identity provider:
* ``nova-users`` user records (PK ``user_id``, GSI1 ``email``)
* ``nova-sessions`` session tokens (PK ``session_id``, GSI1
``user_id``, TTL ``expires_at``)
* ``nova-password-resets`` reset tokens (PK ``reset_token``, TTL
``expires_at`` 15 min)
* ``nova-pats`` personal access tokens (PK ``jti``, GSI1
``sub``, GSI2 ``pat_hash``). This table is consumed in P4 (OIDC/PAT
issuance) but is defined here so a single ``nova idp setup``
CloudFormation template provisions the complete identity backend.
Design notes (REQ-335):
* All tables use ``BillingMode: PAY_PER_REQUEST`` (on-demand) the
IdP traffic is bursty and unpredictable; provisioned capacity would
either throttle or waste money.
* PITR (``PointInTimeRecoverySpecification``) is enabled on
``nova-users`` user records are irreplaceable; continuous backup
protects against accidental deletes / corrupt writes. The session /
reset / PAT tables are ephemeral (TTL-managed) so PITR is not
required there, but enabling it is cheap insurance; we enable it on
``nova-users`` per REQ-335 and leave the others as on-demand only
(TTL is the recovery mechanism for those).
* TTL attributes (``expires_at``) are epoch seconds DynamoDB TTL
silently deletes expired items in the background (best-effort, do
not rely on for access control; the handler also checks ``expires_at``
on read).
The fragment is composed into the full ``nova idp setup`` template in
P4 Wave 8 (``nova idp setup --apply``). The keys in the returned dict
are CloudFormation logical resource IDs (``NovaUsersTable``, etc.) so
the composer can merge it directly into a template's ``Resources``
section.
"""
from __future__ import annotations
from typing import Any, Dict
def _attribute(name: str, attr_type: str = "S") -> Dict[str, str]:
return {"AttributeName": name, "AttributeType": attr_type}
def _key_schema(name: str, key_type: str = "HASH") -> Dict[str, str]:
return {"AttributeName": name, "KeyType": key_type}
def dynamodb_tables_snippet() -> Dict[str, Dict[str, Any]]:
"""Return a CloudFormation fragment defining the four IdP DynamoDB tables.
The returned dict maps logical resource IDs to CloudFormation
resource dicts (``Type: AWS::DynamoDB::Table``). It is intended to be
merged into the ``Resources`` block of the full
``nova idp setup`` template (P4 Wave 8).
Tables:
* ``NovaUsersTable`` (``nova-users``)
* ``NovaSessionsTable`` (``nova-sessions``)
* ``NovaPasswordResetsTable`` (``nova-password-resets``)
* ``NovaPatsTable`` (``nova-pats``)
All tables are ``PAY_PER_REQUEST`` (on-demand). PITR is enabled on
``nova-users`` (REQ-335). TTL is enabled on the three ephemeral
tables (``expires_at`` epoch-seconds attribute).
"""
return {
# -----------------------------------------------------------------
# nova-users — the user directory (PK user_id, GSI1 email).
# PITR enabled: user records are irreplaceable.
# -----------------------------------------------------------------
"NovaUsersTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-users",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
_key_schema("user_id", "HASH"),
],
"AttributeDefinitions": [
_attribute("user_id", "S"),
_attribute("email", "S"),
],
"GlobalSecondaryIndexes": [
{
"IndexName": "email-index",
"KeySchema": [_key_schema("email", "HASH")],
"Projection": {"ProjectionType": "ALL"},
},
],
"PointInTimeRecoverySpecification": {
"PointInTimeRecoveryEnabled": True,
},
# Attribute shape (for documentation / the setup --dry-run
# summary; DynamoDB is schemaless so this is not enforced):
# user_id String (PK)
# email String (GSI1 hash, unique)
# password_hash String (Argon2id, never the raw password)
# owner String
# roles List
# created_at String (ISO-8601)
"AttributeShape": {
"user_id": "String",
"email": "String",
"password_hash": "String",
"owner": "String",
"roles": "List",
"created_at": "String",
},
},
},
# -----------------------------------------------------------------
# nova-sessions — session tokens (PK session_id, GSI1 user_id).
# TTL: expires_at (epoch seconds). Sessions live 24h.
# -----------------------------------------------------------------
"NovaSessionsTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-sessions",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
_key_schema("session_id", "HASH"),
],
"AttributeDefinitions": [
_attribute("session_id", "S"),
_attribute("user_id", "S"),
],
"GlobalSecondaryIndexes": [
{
"IndexName": "user_id-index",
"KeySchema": [_key_schema("user_id", "HASH")],
"Projection": {"ProjectionType": "ALL"},
},
],
"TimeToLiveSpecification": {
"AttributeName": "expires_at",
"Enabled": True,
},
"AttributeShape": {
"session_id": "String",
"user_id": "String",
"expires_at": "String (epoch seconds, TTL)",
"created_at": "String (ISO-8601)",
},
},
},
# -----------------------------------------------------------------
# nova-password-resets — reset tokens (PK reset_token).
# TTL: expires_at (epoch seconds). Tokens live 15 min.
# -----------------------------------------------------------------
"NovaPasswordResetsTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-password-resets",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
_key_schema("reset_token", "HASH"),
],
"AttributeDefinitions": [
_attribute("reset_token", "S"),
],
"TimeToLiveSpecification": {
"AttributeName": "expires_at",
"Enabled": True,
},
"AttributeShape": {
"reset_token": "String",
"user_id": "String",
"expires_at": "String (epoch seconds, TTL; 15 min)",
},
},
},
# -----------------------------------------------------------------
# nova-pats — personal access tokens (PK jti, GSI1 sub, GSI2 pat_hash).
# Consumed in P4 (OIDC/PAT issuance) but defined here so the single
# CloudFormation template provisions the complete identity backend.
# TTL: expires_at (epoch seconds).
# -----------------------------------------------------------------
"NovaPatsTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-pats",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
_key_schema("jti", "HASH"),
],
"AttributeDefinitions": [
_attribute("jti", "S"),
_attribute("sub", "S"),
_attribute("pat_hash", "S"),
],
"GlobalSecondaryIndexes": [
{
"IndexName": "sub-index",
"KeySchema": [_key_schema("sub", "HASH")],
"Projection": {"ProjectionType": "ALL"},
},
{
"IndexName": "pat_hash-index",
"KeySchema": [_key_schema("pat_hash", "HASH")],
"Projection": {"ProjectionType": "ALL"},
},
],
"TimeToLiveSpecification": {
"AttributeName": "expires_at",
"Enabled": True,
},
"AttributeShape": {
"jti": "String (PK)",
"sub": "String (GSI1; subject / user_id)",
"pat_hash": "String (GSI2; SHA-256 of the PAT for lookup)",
"status": "String (active|revoked)",
"issued_at": "String (ISO-8601)",
"expires_at": "String (epoch seconds, TTL)",
"revoked_at": "String (ISO-8601, present iff status=revoked)",
"claims": "Map (JWT claims payload)",
},
},
},
}
def table_names() -> Dict[str, str]:
"""Return the logical→physical table-name mapping (for env-var defaults)."""
return {
"users": "nova-users",
"sessions": "nova-sessions",
"password_resets": "nova-password-resets",
"pats": "nova-pats",
}
if __name__ == "__main__": # pragma: no cover - CLI inspection helper
import json
import sys
if "--names" in sys.argv:
sys.stdout.write(json.dumps(table_names(), indent=2) + "\n")
else:
sys.stdout.write(json.dumps(dynamodb_tables_snippet(), indent=2) + "\n")
+236
View File
@@ -0,0 +1,236 @@
"""CloudFormation template for the Nova IdP (REQ-340, REQ-341, C-2.1).
Composes the DynamoDB snippet (from P3 ``nova_idp_auth_cfn.py``) + 3
Lambdas (``nova-idp-auth``, ``nova-idp-token-vend``, ``nova-idp-jwks``)
+ KMS key (``alias/nova-oidc-signing``, ``ECC_NIST_P256``,
``SIGN_VERIFY``) + function URLs + IAM roles + optional
CloudFront/WAF/ACM (when ``public_jwks_domain`` is provided).
:func:`generate_template` returns a CloudFormation template dict (no
troposphere dependency raw dict JSON).
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
from typing import Any, Dict
def _load_auth_cfn():
"""Load core/lambda/nova_idp_auth_cfn.py via importlib (`lambda` is reserved)."""
p = Path(__file__).parent / "nova_idp_auth_cfn.py"
spec = importlib.util.spec_from_file_location("nova_idp_auth_cfn", p)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
_auth_cfn = _load_auth_cfn()
dynamodb_tables_snippet = _auth_cfn.dynamodb_tables_snippet
table_names = _auth_cfn.table_names
def _lambda_role(logical_id: str, table_envs: dict[str, str], kms: bool = False) -> dict:
"""Build an IAM role for a Nova IdP Lambda."""
statements = [
{
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": {"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"},
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup"],
"Resource": {"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"},
},
]
if table_envs:
statements.append({
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem",
"dynamodb:Query", "dynamodb:DeleteItem"],
"Resource": [
{"Fn::Sub": f"arn:aws:dynamodb:${{AWS::Region}}:${{AWS::AccountId}}:table/{name}"}
for name in table_envs.values()
],
})
if kms:
statements.append({
"Effect": "Allow",
"Action": ["kms:Sign", "kms:GetPublicKey", "kms:DescribeKey"],
"Resource": {"Fn::GetAtt": "NovaOidcSigningKey.Arn"},
})
return {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": {"Fn::Sub": "lambda.${AWS::Region}.amazonaws.com"}},
"Action": "sts:AssumeRole",
}],
},
"Policies": [{"PolicyName": f"{logical_id}Policy", "PolicyDocument": {
"Version": "2012-10-17", "Statement": statements,
}}],
},
}
def _lambda_function(logical_id: str, handler: str, role_ref: str,
env_vars: dict[str, str], memory: int = 512) -> dict:
return {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": handler,
"Runtime": "python3.12",
"MemorySize": memory,
"Timeout": 30,
"Role": {"Fn::GetAtt": [role_ref, "Arn"]},
"Environment": {"Variables": env_vars},
"Code": {"ZipFile": "def lambda_handler(event, context):\n return {}"},
},
}
def _function_url(logical_id: str, auth_type: str = "AWS_IAM") -> dict:
return {
"Type": "AWS::Lambda::Url",
"Properties": {
"TargetFunction": {"Ref": logical_id},
"AuthType": auth_type,
},
}
def generate_template(public_jwks_domain: str | None = None) -> Dict[str, Any]:
"""Generate the full Nova IdP CloudFormation template (REQ-340).
Args:
public_jwks_domain: optional custom domain for the JWKS endpoint.
When provided, CloudFront + ACM + WAF resources are added.
Returns:
A CloudFormation template dict (``{"Resources": {...}}``).
"""
resources: Dict[str, Any] = {}
# DynamoDB tables (from P3).
resources.update(dynamodb_tables_snippet())
names = table_names()
# KMS key (ECC_NIST_P256, SIGN_VERIFY) + alias.
resources["NovaOidcSigningKey"] = {
"Type": "AWS::KMS::Key",
"Properties": {
"Description": "Nova OIDC token signing key (REQ-337, ECC_NIST_P256)",
"KeySpec": "ECC_NIST_P256",
"KeyUsage": "SIGN_VERIFY",
"KeyPolicy": {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": {"Fn::Sub": "arn:aws:iam::${AWS::AccountId}:root"}},
"Action": "kms:*",
"Resource": "*",
}],
},
},
}
resources["NovaOidcSigningKeyAlias"] = {
"Type": "AWS::KMS::Alias",
"Properties": {
"AliasName": "alias/nova-oidc-signing",
"TargetKeyId": {"Fn::GetAtt": "NovaOidcSigningKey.Arn"},
},
}
# Lambda roles.
auth_tables = {"users": names["users"], "sessions": names["sessions"],
"password_resets": names["password_resets"]}
resources["NovaIdpAuthRole"] = _lambda_role("NovaIdpAuth", auth_tables)
resources["NovaIdpTokenVendRole"] = _lambda_role(
"NovaIdpTokenVend", {"pats": names["pats"]}, kms=True)
resources["NovaIdpJwksRole"] = _lambda_role("NovaIdpJwks", {}, kms=True)
# Lambda functions.
common_env = {
"NOVA_USERS_TABLE": names["users"],
"NOVA_SESSIONS_TABLE": names["sessions"],
"NOVA_PASSWORD_RESETS_TABLE": names["password_resets"],
"NOVA_PATS_TABLE": names["pats"],
}
resources["NovaIdpAuthFunction"] = _lambda_function(
"NovaIdpAuth", "nova_idp_auth.lambda_handler", "NovaIdpAuthRole", common_env)
resources["NovaIdpTokenVendFunction"] = _lambda_function(
"NovaIdpTokenVend", "nova_idp_token_vend.lambda_handler", "NovaIdpTokenVendRole",
{**common_env, "NOVA_OIDC_KMS_KEY_ID": "alias/nova-oidc-signing"})
resources["NovaIdpJwksFunction"] = _lambda_function(
"NovaIdpJwks", "nova_idp_jwks.lambda_handler", "NovaIdpJwksRole",
{"NOVA_OIDC_KMS_KEY_ID": "alias/nova-oidc-signing"}, memory=256)
# Function URLs (auth Lambda: IAM; token-vend: IAM; jwks: NONE — public).
resources["NovaIdpAuthUrl"] = _function_url("NovaIdpAuthFunction", "AWS_IAM")
resources["NovaIdpTokenVendUrl"] = _function_url("NovaIdpTokenVendFunction", "AWS_IAM")
resources["NovaIdpJwksUrl"] = _function_url("NovaIdpJwksFunction", "NONE")
# Optional: CloudFront + ACM + WAF for a custom JWKS domain.
if public_jwks_domain:
resources["NovaJwksCloudFront"] = {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Enabled": True,
"Aliases": [public_jwks_domain],
"Origins": [{
"DomainName": {"Fn::GetAtt": "NovaIdpJwksUrl.Endpoint"},
"Id": "JwksOrigin",
"CustomOriginConfig": {"OriginProtocolPolicy": "https-only"},
}],
"DefaultCacheBehavior": {
"TargetOriginId": "JwksOrigin",
"ViewerProtocolPolicy": "redirect-to-https",
"ForwardedValues": {"QueryString": False},
},
"ViewerCertificate": {
"AcmCertificateArn": {"Ref": "NovaJwksAcmCert"},
"SslSupportMethod": "sni-only",
},
}
},
}
resources["NovaJwksAcmCert"] = {
"Type": "AWS::CertificateManager::Certificate",
"Properties": {"DomainName": public_jwks_domain,
"ValidationMethod": "DNS"},
}
resources["NovaJwksWafRateRule"] = {
"Type": "AWS::WAFv2::RateBasedRule",
"Properties": {
"Name": "nova-jwks-rate-limit",
"Scope": "CLOUDFRONT",
"RateLimit": 100,
"Action": {"Block": {}},
"ComparisonOperator": "GreaterThan",
"AggregateKeyType": "IP",
"DefaultCaptchaConfig": {"ImmunityTimeProperty": {"ImmunityTime": 60}},
},
}
return {"Resources": resources}
def resource_summary(template: dict) -> dict[str, int]:
"""Return ``{resource_type: count}`` for a template (for --dry-run)."""
counts: dict[str, int] = {}
for res in template.get("Resources", {}).values():
t = res.get("Type", "Unknown")
counts[t] = counts.get(t, 0) + 1
return counts
if __name__ == "__main__": # pragma: no cover - CLI inspection helper
import json, sys
domain = sys.argv[1] if len(sys.argv) > 1 else None
print(json.dumps(generate_template(domain), indent=2))
+63
View File
@@ -0,0 +1,63 @@
"""Nova IdP JWKS endpoint Lambda (REQ-338, D-230).
Serves the KMS public key as a JWK in a standard JWKS response. The
endpoint is a Lambda function URL with ``AuthType: NONE`` (JWKS is
public-key only configured in CloudFormation, not in code).
Response:
* ``Content-Type: application/json``
* ``Cache-Control: public, max-age=3600`` (1h clients cache the JWKS)
* ``Access-Control-Allow-Origin: *`` (JWKS is public)
* ``body: {"keys": [<jwk>]}``
The JWK is built via :func:`core.kms_signing.get_jwk` from the KMS
public key (DER SPKI ``cryptography`` JWK).
Dual-use (REQ-329): ``__main__`` CLI block for local testing
(``--print-jwks``).
"""
from __future__ import annotations
import json
import os
import sys
OIDC_KMS_KEY_ID = os.environ.get("NOVA_OIDC_KMS_KEY_ID", "alias/nova-oidc-signing")
def lambda_handler(event, context):
"""AWS Lambda handler — serve the JWKS response (REQ-338)."""
try:
from core.kms_signing import get_jwk
jwk = get_jwk(key_id=OIDC_KMS_KEY_ID)
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
"body": json.dumps({"keys": [jwk]}),
}
except Exception as e:
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": str(e)}),
}
def cli_main(argv=None):
"""CLI entry point (REQ-329 dual-use). ``--print-jwks`` → stdout."""
raw = argv if argv is not None else sys.argv[1:]
if "--print-jwks" in raw:
resp = lambda_handler({}, None)
sys.stdout.write(resp["body"] + "\n")
return resp.get("statusCode", 200) - 200
print("Usage: python3 -m core.lambda.nova_idp_jwks --print-jwks", file=sys.stderr)
return 2
if __name__ == "__main__": # pragma: no cover - CLI entry
sys.exit(cli_main())
+237
View File
@@ -0,0 +1,237 @@
"""Nova IdP setup logic — check / apply / verify (REQ-340, REQ-341, C-2.1).
Backing logic for ``nova idp setup``. The CLI (``nova/idp/setup.py``)
is a thin 50-line delegate to this module (CAP-034).
From v1.29 (REQ-369, spec §7.5) the active provisioning path is
``terraform apply`` in the ``nova-platform-ops`` checkout. The CFN
template generated here is archived as read-only reference in
``docs/archive/nova-idp-cfn-v1.28.md``; :func:`generate_and_deploy`
(the former CFN deploy path) emits a ``DeprecationWarning`` and is
retained only as a fallback when terraform is absent from PATH.
:func:`terraform_apply` and :func:`terraform_plan` are the new
preferred paths.
"""
from __future__ import annotations
import importlib.util
import json
import os
import shutil
import subprocess
import sys
import tempfile
import warnings
from pathlib import Path
from typing import Any
_CFN_ARCHIVE_REF = (
"CFN path is archived; install terraform or use nova-platform-ops. "
"See docs/archive/nova-idp-cfn-v1.28.md."
)
def _load_cfn():
"""Load core/lambda/nova_idp_cfn.py via importlib (`lambda` is reserved)."""
p = Path(__file__).parent / "nova_idp_cfn.py"
spec = importlib.util.spec_from_file_location("nova_idp_cfn", p)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
_cfn = _load_cfn()
generate_template = _cfn.generate_template
resource_summary = _cfn.resource_summary
def check_prerequisites() -> dict[str, Any]:
"""Check IdP setup prerequisites (AWS creds, CFN/IAM/KMS perms).
Returns a report dict:
``{"aws_creds": bool, "region": str|None, "missing": [str], "iam_delta": [str]}``
"""
report: dict[str, Any] = {"aws_creds": False, "region": None, "missing": [], "iam_delta": []}
# AWS creds check.
try:
who = subprocess.check_output(
["aws", "sts", "get-caller-identity"], stderr=subprocess.DEVNULL, text=True, timeout=10
)
report["aws_creds"] = bool(json.loads(who).get("Account"))
except Exception:
report["missing"].append("aws_credentials (run `aws configure`)")
# Region.
region = os.environ.get("AWS_DEFAULT_REGION") or os.environ.get("AWS_REGION")
report["region"] = region
if not region:
report["missing"].append("aws_region (set AWS_DEFAULT_REGION)")
# IAM policy delta (the grants the deploying principal needs).
report["iam_delta"] = [
"cloudformation:*",
"iam:CreateRole",
"iam:PassRole",
"lambda:CreateFunction",
"lambda:CreateFunctionUrlConfig",
"dynamodb:CreateTable",
"kms:CreateKey",
"kms:CreateAlias",
]
return report
def generate_and_deploy(
public_jwks_domain: str | None = None,
dry_run: bool = False,
approve_fn=None,
) -> dict[str, Any]:
"""Generate the CFN template + deploy (REQ-341, NFR-10 y/N approval).
.. deprecated:: v1.29
The active path is :func:`terraform_apply` (REQ-369, spec §7.5).
This CFN deploy path is archived as read-only reference in
``docs/archive/nova-idp-cfn-v1.28.md`` and retained only as a
fallback when terraform is absent from PATH. It emits a
``DeprecationWarning`` on every non-dry-run invocation.
Args:
public_jwks_domain: optional custom JWKS domain.
dry_run: if True, print the resource summary only (no deploy).
approve_fn: callable returning True/False for the y/N prompt
(defaults to stdin readline).
Returns:
``{"template": <dict>, "summary": <dict>, "deployed": bool}``.
"""
template = generate_template(public_jwks_domain)
summary = resource_summary(template)
if dry_run:
return {"template": template, "summary": summary, "deployed": False}
warnings.warn(_CFN_ARCHIVE_REF, DeprecationWarning, stacklevel=2)
# NFR-10: explicit y/N approval before cloudformation deploy.
print("Resource summary:")
for rtype, count in sorted(summary.items()):
print(f" {rtype}: {count}")
# Print template to a temp file + open $PAGER.
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8")
json.dump(template, tmp, indent=2); tmp.flush(); tmp.close()
pager = os.environ.get("PAGER")
if pager and sys.stdin.isatty():
try:
subprocess.run([pager, tmp.name])
except Exception:
print(f"(template at {tmp.name})")
else:
print(f"(template at {tmp.name})")
# y/N prompt.
if approve_fn is None:
answer = input("Apply? [y/N] ").strip().lower()
else:
answer = "y" if approve_fn() else "n"
if answer != "y":
print("aborted (no approval)")
return {"template": template, "summary": summary, "deployed": False}
# cloudformation deploy.
stack_name = os.environ.get("NOVA_IDP_STACK_NAME", "nova-idp")
try:
subprocess.check_call([
"aws", "cloudformation", "deploy",
"--stack-name", stack_name,
"--template-file", tmp.name,
"--capabilities", "CAPABILITY_IAM",
])
deployed = True
except Exception as e:
print(f"deploy failed: {e}", file=sys.stderr)
deployed = False
return {"template": template, "summary": summary, "deployed": deployed}
def terraform_apply(*, auto_approve: bool = True) -> dict[str, Any]:
"""Delegate provisioning to ``terraform apply`` (REQ-369, spec §7.5).
The operator runs this from the ``nova-platform-ops`` checkout root
(where the Terraform modules live). This function shells out to
``terraform`` on PATH; the caller (``nova/idp/setup.py``) is
responsible for the ``shutil.which("terraform")`` gate.
Args:
auto_approve: pass ``-auto-approve`` (default True; the y/N gate
is the operator's PR review in nova-platform-ops).
Returns:
``{"deployed": bool, "returncode": int, "command": [str]}``.
"""
cmd = ["terraform", "apply"]
if auto_approve:
cmd.append("-auto-approve")
proc = subprocess.run(cmd)
return {"deployed": proc.returncode == 0, "returncode": proc.returncode, "command": cmd}
def terraform_plan() -> dict[str, Any]:
"""Delegate verification to ``terraform plan`` (REQ-369, spec §7.5).
Reports the diff between the live stack and the Terraform source in
the ``nova-platform-ops`` checkout. The caller is responsible for
the ``shutil.which("terraform")`` gate.
Returns:
``{"passed": bool, "returncode": int, "command": [str]}``.
"""
cmd = ["terraform", "plan"]
proc = subprocess.run(cmd)
return {"passed": proc.returncode == 0, "returncode": proc.returncode, "command": cmd}
def verify() -> dict[str, Any]:
"""Run the KMS round-trip verification (REQ-340 --verify).
Delegates to the CAP-037 test logic: sign a JWT (mock KMS) JWKS
pyjwt verify. Returns ``{"passed": bool, "detail": str}``.
"""
try:
import jwt as pyjwt
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes, serialization
import core.kms_signing as kms_signing
priv = ec.generate_private_key(ec.SECP256R1())
pub_der = priv.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
class _MockKms:
def sign(self, KeyId, Message, MessageType, SigningAlgorithm):
return {"Signature": priv.sign(Message, ec.ECDSA(hashes.SHA256()))}
def get_public_key(self, KeyId):
return {"PublicKey": pub_der}
kms_signing.set_kms_client_for_testing(_MockKms())
token = kms_signing.sign_jwt({"sub": "verify", "exp": 9999999999, "iat": 1, "jti": "v"})
jwk = kms_signing.get_jwk()
key = pyjwt.PyJWK(jwk).key
decoded = pyjwt.decode(token, key, algorithms=["ES256"], options={"verify_aud": False})
ok = decoded["sub"] == "verify"
return {"passed": ok, "detail": "KMS round-trip OK" if ok else "mismatch"}
except Exception as e:
return {"passed": False, "detail": f"verify error: {e}"}
finally:
try:
kms_signing.set_kms_client_for_testing(None)
except Exception:
pass
if __name__ == "__main__": # pragma: no cover - CLI inspection helper
mode = sys.argv[1] if len(sys.argv) > 1 else "--check"
if mode == "--check":
print(json.dumps(check_prerequisites(), indent=2))
elif mode == "--dry-run":
print(json.dumps(generate_and_deploy(dry_run=True)["summary"], indent=2))
elif mode == "--verify":
print(json.dumps(verify(), indent=2))
else:
print("usage: nova_idp_setup.py --check|--dry-run|--verify", file=sys.stderr)
+401
View File
@@ -0,0 +1,401 @@
"""Nova IdP token-vend Lambda — PAT/session → KMS-signed OIDC token
(REQ-336, C-6.1/C-7.1 ABAC FAIL-CLOSED, D-229 revocation).
Accepts a PAT (or session token) and returns a KMS-signed OIDC token
with claims ``sub, aud, iss, exp, iat, jti, roles`` (REQ-336).
## ABAC fail-closed (C-6.1/C-7.1 — INV-17 runtime enforcement)
The grill's #1 finding: the token-vend Lambda MUST fail closed on ABAC
evaluation failure. Concretely, a token is vended **only** when:
1. The PAT is active (``nova-pats.GetItem(jti, ConsistentRead=True)``
returns an item with ``status == "active"`` D-229; strong read on
the main table, GSIs don't support strong reads).
2. ``KyvernoJsonEngine.is_configured()`` returns ``True`` **AND**
``evaluate_token_vend_policy()`` returns ``allowed=True`` without
raising.
If (2) fails for **any** reason ``kj`` absent, ``kj`` error, policy
parse error, engine raise the Lambda returns **403** + audit
``token.vend.denied`` (reason ``abac_eval_failed``). **Never fail
open.** This is verified by ``tests/test_abac_fail_closed.py`` the
most important test of the milestone.
## Dual-use (REQ-329 pattern)
Mirrors ``nova_idp_auth.py``: lazy boto3, env-var table names,
``NOVA_LAMBDA_LOCAL_BYPASS``, ``__main__`` CLI block, audit emission.
"""
from __future__ import annotations
import datetime
import json
import os
import sys
import time
import boto3
# ---------------------------------------------------------------------------
# Config (env-var table names, mirroring nova_idp_auth.py)
# ---------------------------------------------------------------------------
PATS_TABLE = os.environ.get("NOVA_PATS_TABLE", "nova-pats")
SESSIONS_TABLE = os.environ.get("NOVA_SESSIONS_TABLE", "nova-sessions")
OIDC_KMS_KEY_ID = os.environ.get("NOVA_OIDC_KMS_KEY_ID", "alias/nova-oidc-signing")
OIDC_ISSUER = os.environ.get("NOVA_OIDC_ISSUER", "nova-idp")
OIDC_AUDIENCE = os.environ.get("NOVA_OIDC_AUDIENCE", "nova-cli")
# OIDC token lifetime (seconds). Default 15 min.
OIDC_TTL_SECONDS = int(os.environ.get("NOVA_OIDC_TTL_SECONDS", "900"))
_dynamodb = None
_kms_client = None
def _get_dynamodb():
"""Lazy boto3 DynamoDB resource singleton (mirrors contract_ingestor)."""
global _dynamodb
if _dynamodb is None:
_dynamodb = boto3.resource("dynamodb")
return _dynamodb
def _iso8601_now() -> str:
return datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
def _epoch_now() -> int:
return int(datetime.datetime.now(datetime.timezone.utc).timestamp())
def _emit_audit(event_type: str, **fields) -> None:
"""Emit an audit event to stderr as JSON (never the raw PAT/token)."""
payload = {"event": event_type, "ts": _iso8601_now(), **fields}
# Defense-in-depth: scrub raw token fields (INV-16/INV-17 spirit).
for _k in ("pat", "session_token", "token", "raw_pat"):
payload.pop(_k, None)
sys.stderr.write(json.dumps(payload, sort_keys=True) + "\n")
sys.stderr.flush()
# ---------------------------------------------------------------------------
# PAT / session decoding (decode WITHOUT verifying — signature verified
# by KMS public key separately at the JWKS verifier; the revocation
# check is the trust anchor here, not the JWT signature).
# ---------------------------------------------------------------------------
def _decode_jwt_unverified(token: str) -> dict:
"""Decode a JWT's payload without verifying the signature."""
try:
import jwt as pyjwt
return pyjwt.decode(token, options={"verify_signature": False})
except Exception:
# Fallback: manual base64url decode of the payload segment.
parts = token.split(".")
if len(parts) < 2:
raise ValueError("malformed JWT (expected 3 segments)")
import base64
pad = parts[1] + "=" * (-len(parts[1]) % 4)
return json.loads(base64.urlsafe_b64decode(pad))
def _extract_pat_claims(token: str) -> dict:
"""Decode a PAT/session JWT → extract jti, sub, typ, roles, owner, exp."""
claims = _decode_jwt_unverified(token)
required = ("jti", "sub", "exp")
for f in required:
if f not in claims:
raise ValueError(f"token missing claim: {f}")
return claims
# ---------------------------------------------------------------------------
# Revocation check (D-229 — strong read on the main table)
# ---------------------------------------------------------------------------
def _check_pat_active(jti: str) -> tuple[bool, str]:
"""Return ``(active, reason)``. Strong read on nova-pats main table.
D-229: GSIs don't support strongly-consistent reads, so the
revocation check uses ``GetItem(PK=jti, ConsistentRead=True)`` on
the main table. This satisfies the 60s SLO synchronously (the
strong read reflects the latest write revocation is instant).
"""
table = _get_dynamodb().Table(PATS_TABLE)
resp = table.get_item(
TableName=PATS_TABLE,
Key={"jti": jti},
ConsistentRead=True,
)
item = resp.get("Item")
if item is None:
return False, "pat_unknown"
status = item.get("status", "active")
if status != "active":
return False, f"pat_{status}" # pat_revoked, pat_expired, etc.
# Expired? (defense-in-depth; TTL may not have reaped it yet)
expires_at = item.get("expires_at")
if expires_at is not None:
try:
if int(expires_at) < _epoch_now():
return False, "pat_expired"
except (ValueError, TypeError):
pass
return True, "active"
# ---------------------------------------------------------------------------
# ABAC fail-closed (C-6.1/C-7.1)
# ---------------------------------------------------------------------------
def _build_abac_payload(claims: dict, requested_claims: list[str],
target_resource: dict, environment: str,
policy_version: str) -> dict:
"""Build the ABAC authorization payload (REQ-339, C-5.1)."""
return {
"subject": {
"id": claims.get("sub", ""),
"role": (claims.get("roles") or ["unknown"])[0],
"owner": claims.get("owner", ""),
},
"requested_claims": requested_claims,
"target_resource": target_resource,
"environment": environment,
"pat_jti": claims.get("jti", ""),
"policy_version": policy_version,
}
def _evaluate_abac_fail_closed(payload: dict) -> tuple[bool, list, str, str]:
"""Evaluate ABAC with fail-closed semantics (C-6.1).
Returns ``(allowed, pcrs, policy_sha, reason)``. On ANY failure
(engine not configured, evaluate raises, policy parse error) returns
``(False, [], "", "abac_eval_failed")``. **Never fails open.**
"""
# Lazy imports so the module imports without the engine adapter.
from core.policy_engine import get_engine
# C-6.1: is_configured() check. If kj is absent → fail closed.
try:
engine = get_engine()
if not engine.is_configured():
_emit_audit(
"token.vend.abac_engine_not_configured",
pat_jti=payload.get("pat_jti", ""),
)
return False, [], "", "abac_eval_failed"
except Exception: # noqa: BLE001 - fail closed on any engine check error
return False, [], "", "abac_eval_failed"
# C-6.1: evaluate() raising → fail closed.
try:
from core.abac_evaluator import evaluate_token_vend_policy
allowed, pcrs, policy_sha = evaluate_token_vend_policy(payload)
reason = "abac_denied" if not allowed else "ok"
return allowed, pcrs, policy_sha, reason
except Exception: # noqa: BLE001 - fail closed on any eval error
return False, [], "", "abac_eval_failed"
# ---------------------------------------------------------------------------
# Token vend (REQ-336)
# ---------------------------------------------------------------------------
def _build_oidc_claims(pat_claims: dict) -> dict:
"""Build the OIDC token claims (REQ-336)."""
now = _epoch_now()
return {
"sub": pat_claims["sub"],
"aud": OIDC_AUDIENCE,
"iss": OIDC_ISSUER,
"exp": now + OIDC_TTL_SECONDS,
"iat": now,
"jti": pat_claims.get("jti", ""), # carry the PAT jti for tracing
"roles": pat_claims.get("roles", []),
"typ": "nova_oidc_token", # INV-14: distinguish from developer_pat
}
def vend_token(
token: str,
requested_claims: list[str] | None = None,
target_resource: dict | None = None,
environment: str | None = None,
policy_version: str = "",
) -> dict:
"""Vend a KMS-signed OIDC token for a PAT/session (REQ-336).
Returns ``{"token": ..., "expires_at": ...}`` on success. Raises
``_DeniedError`` ( 403) on revocation / ABAC denial.
"""
requested_claims = requested_claims or ["sub", "roles"]
environment = environment or "dev"
# 1. Decode the PAT/session (without verifying — D-229).
pat_claims = _extract_pat_claims(token)
jti = pat_claims["jti"]
# Default target_resource: owner inherits from the PAT subject so
# the owner-matches ABAC rule passes for same-tenant vends. Callers
# can override with an explicit target_resource.
if target_resource is None:
target_resource = {
"type": "contract",
"id": "*",
"owner": pat_claims.get("owner", "*"),
"environment": environment,
}
# 2. Revocation check (D-229, strong read).
active, reason = _check_pat_active(jti)
if not active:
_emit_audit("token.vend.denied", pat_jti=jti, reason=reason)
raise _DeniedError(reason)
# 3. ABAC eval (C-6.1 FAIL-CLOSED).
abac_payload = _build_abac_payload(
pat_claims, requested_claims, target_resource, environment, policy_version
)
allowed, _pcrs, policy_sha, abac_reason = _evaluate_abac_fail_closed(abac_payload)
if not allowed:
_emit_audit(
"token.vend.denied",
pat_jti=jti,
reason=abac_reason,
policy_sha=policy_sha,
)
raise _DeniedError(abac_reason)
# 4. KMS sign (REQ-337).
from core.kms_signing import sign_jwt
oidc_claims = _build_oidc_claims(pat_claims)
oidc_token = sign_jwt(oidc_claims, key_id=OIDC_KMS_KEY_ID)
_emit_audit(
"token.vend.allowed",
pat_jti=jti,
sub=oidc_claims["sub"],
policy_sha=policy_sha,
expires_at=oidc_claims["exp"],
)
return {"token": oidc_token, "expires_at": oidc_claims["exp"]}
class _DeniedError(Exception):
"""Raised on revocation / ABAC denial → 403."""
def __init__(self, reason: str):
self.reason = reason
super().__init__(f"token vend denied: {reason}")
# ---------------------------------------------------------------------------
# Lambda handler + HTTP mapping
# ---------------------------------------------------------------------------
def _to_http_response(result_or_error):
if isinstance(result_or_error, Exception):
if isinstance(result_or_error, _DeniedError):
return {
"statusCode": 403,
"body": json.dumps({"error": "token_vend_denied", "reason": result_or_error.reason}),
}
if isinstance(result_or_error, ValueError):
return {
"statusCode": 400,
"body": json.dumps({"error": str(result_or_error)}),
}
return {
"statusCode": 500,
"body": json.dumps({"error": str(result_or_error)}),
}
return {"statusCode": 200, "body": json.dumps(result_or_error)}
def lambda_handler(event, context):
"""AWS Lambda handler entry point (thin wrapper, REQ-329 dual-use)."""
try:
body = event.get("body", "{}")
payload = json.loads(body) if isinstance(body, str) else body
token = payload.get("token") or payload.get("pat") or payload.get("session_token")
if not token:
raise ValueError("missing field: token (or pat / session_token)")
result = vend_token(
token=token,
requested_claims=payload.get("requested_claims"),
target_resource=payload.get("target_resource"),
environment=payload.get("environment"),
policy_version=payload.get("policy_version", ""),
)
return _to_http_response(result)
except Exception as e:
return _to_http_response(e)
# ---------------------------------------------------------------------------
# CLI (dual-use, REQ-329 pattern)
# ---------------------------------------------------------------------------
def cli_main(argv=None):
"""CLI entry point for the token-vend Lambda (REQ-329 dual-use)."""
raw = argv if argv is not None else sys.argv[1:]
local_bypass = os.environ.get("NOVA_LAMBDA_LOCAL_BYPASS")
if not local_bypass:
os.environ["NOVA_LAMBDA_LOCAL_BYPASS"] = "1"
try:
if "--vend-stdin" in raw:
payload = json.loads(sys.stdin.read())
elif "--vend" in raw:
idx = raw.index("--vend")
path = raw[idx + 1] if idx + 1 < len(raw) else None
if not path:
print("Usage: --vend <payload.json>", file=sys.stderr)
return 2
with open(path) as fh:
payload = json.loads(fh.read())
else:
print(
"Usage: python3 -m core.lambda.nova_idp_token_vend "
"--vend <payload.json> | --vend-stdin < <payload.json>",
file=sys.stderr,
)
return 2
token = payload.get("token") or payload.get("pat") or payload.get("session_token")
if not token:
print("error: missing token in payload", file=sys.stderr)
return 1
result = vend_token(
token=token,
requested_claims=payload.get("requested_claims"),
target_resource=payload.get("target_resource"),
environment=payload.get("environment"),
policy_version=payload.get("policy_version", ""),
)
sys.stdout.write(json.dumps(result, indent=2) + "\n")
return 0
except _DeniedError as e:
sys.stderr.write(f"error: token vend denied ({e.reason})\n")
return 3 # 403-class
except ValueError as e:
sys.stderr.write(f"error: {e}\n")
return 1
except Exception as e: # pragma: no cover - defensive top-level guard
sys.stderr.write(f"internal error: {e}\n")
return 2
finally:
if not local_bypass:
os.environ.pop("NOVA_LAMBDA_LOCAL_BYPASS", None)
if __name__ == "__main__": # pragma: no cover - CLI entry
sys.exit(cli_main())
+94
View File
@@ -0,0 +1,94 @@
"""Nova client-mode resolver (P1, REQ-327, D-226).
Priority: --mode flag NOVA_CLIENT_MODE env credential type TTY.
No silent fallbacks: every return carries a non-empty selection_reason.
INV-13: invalid env values are ignored + warned, then fall through.
INV-14: credential_type developer_pat/nova_oidc_token + TTY
interactive; + no-TTY agent. TTY check is sys.stdin.isatty() (D-226).
"""
from __future__ import annotations
import json
import logging
import os
import sys
from pathlib import Path
from typing import Optional, Tuple
log = logging.getLogger("nova.mode_resolver")
_VALID_MODES = ("agent", "interactive")
_CRED_MODE_TYPES = ("developer_pat", "nova_oidc_token")
def resolve_mode(
flag: Optional[str] = None,
env_var: Optional[str] = None,
credential_type: Optional[str] = None,
stdin_isatty: bool = False,
) -> Tuple[str, str]:
"""Return (mode, selection_reason) honoring D-226 priority."""
if flag is not None and flag in _VALID_MODES:
return flag, "flag"
if env_var is not None and env_var != "":
if env_var in _VALID_MODES:
return env_var, "env"
log.warning(
"NOVA_CLIENT_MODE=%r invalid (expected one of %s); ignoring",
env_var,
_VALID_MODES,
)
if credential_type in _CRED_MODE_TYPES:
mode = "interactive" if stdin_isatty else "agent"
return mode, f"credential:{credential_type}"
mode = "interactive" if stdin_isatty else "agent"
return mode, "tty"
def _read_credential_type(path: Path) -> Optional[str]:
"""Read the active credential's type from ~/.nova/credentials.json."""
try:
data = json.loads(path.read_text())
except (OSError, json.JSONDecodeError):
return None
active_jti = data.get("active_credential_jti")
for cred in data.get("credentials", []) or []:
if cred.get("jti") == active_jti:
return cred.get("type")
return None
def resolve_mode_from_env(credential_type: Optional[str] = None) -> Tuple[str, str]:
"""Resolve mode using sys.argv, NOVA_CLIENT_MODE, credentials, and TTY.
Best-effort --mode scan of sys.argv (no full argparse); env var;
~/.nova/credentials.json active credential type; sys.stdin.isatty().
"""
flag: Optional[str] = None
argv = sys.argv[1:]
for i, tok in enumerate(argv):
if tok == "--mode" and i + 1 < len(argv):
flag = argv[i + 1]
break
if tok.startswith("--mode="):
flag = tok.split("=", 1)[1]
break
env_var = os.environ.get("NOVA_CLIENT_MODE")
if env_var is not None and env_var == "":
env_var = ""
if credential_type is None:
cred_path = Path.home() / ".nova" / "credentials.json"
credential_type = _read_credential_type(cred_path)
return resolve_mode(
flag=flag,
env_var=env_var,
credential_type=credential_type,
stdin_isatty=sys.stdin.isatty(),
)
if __name__ == "__main__":
mode, reason = resolve_mode_from_env()
print(f"mode={mode} reason={reason}")
+164
View File
@@ -0,0 +1,164 @@
"""PAT (personal access token) lifecycle — issue + revoke (REQ-342, REQ-343).
PATs are signed JWTs (``typ: "developer_pat"``, KMS-signed) that
authenticate a developer/service-account to the token-vend Lambda. Only
the **hash** is stored in ``nova-pats`` (REQ-343) the raw PAT is
returned to the caller once and never persisted.
## Max TTL (C-6.2)
* developer: 24h (86400s)
* service-account: 1h (3600s)
Enforced in :func:`issue_pat` via the ``subject_type`` argument.
## DynamoDB schema (REQ-343)
* PK: ``jti`` (uuid4)
* GSI1: ``sub`` (list PATs for a user)
* GSI2: ``pat_hash`` (SHA-256 of the raw PAT for lookup)
* ``status``: ``active`` | ``revoked`` (revoked PATs retained for audit)
* ``expires_at``: epoch seconds (TTL)
"""
from __future__ import annotations
import datetime
import hashlib
import json
import os
import sys
import uuid
import boto3
PATS_TABLE = os.environ.get("NOVA_PATS_TABLE", "nova-pats")
OIDC_KMS_KEY_ID = os.environ.get("NOVA_OIDC_KMS_KEY_ID", "alias/nova-oidc-signing")
OIDC_ISSUER = os.environ.get("NOVA_OIDC_ISSUER", "nova-idp")
# C-6.2 max TTLs (seconds).
MAX_TTL_DEV = 24 * 3600 # 24h
MAX_TTL_SERVICE = 3600 # 1h
_dynamodb = None
def _get_dynamodb():
global _dynamodb
if _dynamodb is None:
_dynamodb = boto3.resource("dynamodb")
return _dynamodb
def _iso8601_now() -> str:
return datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
def _epoch_now() -> int:
return int(datetime.datetime.now(datetime.timezone.utc).timestamp())
def _emit_audit(event_type: str, **fields) -> None:
payload = {"event": event_type, "ts": _iso8601_now(), **fields}
for _k in ("pat", "raw_pat"):
payload.pop(_k, None)
sys.stderr.write(json.dumps(payload, sort_keys=True) + "\n")
sys.stderr.flush()
def _max_ttl(subject_type: str) -> int:
if subject_type == "service-account":
return MAX_TTL_SERVICE
return MAX_TTL_DEV
def issue_pat(
subject: str,
roles: list[str],
owner: str,
ttl_seconds: int,
key_id: str = OIDC_KMS_KEY_ID,
subject_type: str = "developer",
claims: dict | None = None,
) -> str:
"""Issue a PAT (signed JWT) + store its hash in nova-pats (REQ-342).
Args:
subject: the subject (user_id).
roles: the roles to embed in the PAT.
owner: the tenant owner.
ttl_seconds: requested TTL. Clamped to the C-6.2 max for
``subject_type`` (24h dev, 1h service-account).
key_id: KMS key ID/alias.
subject_type: ``"developer"`` or ``"service-account"``.
claims: extra claims to embed.
Returns:
The raw PAT JWT string (returned once; only the hash is stored).
"""
max_ttl = _max_ttl(subject_type)
if ttl_seconds > max_ttl:
ttl_seconds = max_ttl
if ttl_seconds < 1:
raise ValueError("ttl_seconds must be >= 1")
jti = str(uuid.uuid4())
now = _epoch_now()
exp = now + ttl_seconds
pat_claims = {
"iss": OIDC_ISSUER,
"sub": subject,
"typ": "developer_pat",
"jti": jti,
"iat": now,
"exp": exp,
"roles": roles,
"owner": owner,
}
if claims:
pat_claims.update(claims)
from core.kms_signing import sign_jwt
pat_jwt = sign_jwt(pat_claims, key_id=key_id)
# Only the hash is stored (REQ-343) — NOT the raw PAT.
pat_hash = hashlib.sha256(pat_jwt.encode("ascii")).hexdigest()
table = _get_dynamodb().Table(PATS_TABLE)
table.put_item(
TableName=PATS_TABLE,
Item={
"jti": jti,
"sub": subject,
"pat_hash": pat_hash,
"status": "active",
"issued_at": _iso8601_now(),
"expires_at": str(exp),
"subject_type": subject_type,
"claims": json.dumps(pat_claims),
},
)
_emit_audit("pat.issued", jti=jti, sub=subject, subject_type=subject_type, ttl=ttl_seconds)
return pat_jwt
def revoke_pat(jti: str) -> dict:
"""Revoke a PAT (D-229, REQ-342). Revoked PATs retained for audit.
Returns the update response. Audit ``pat.revoked`` emitted.
"""
table = _get_dynamodb().Table(PATS_TABLE)
resp = table.update_item(
TableName=PATS_TABLE,
Key={"jti": jti},
UpdateExpression="SET #s = :rev, revoked_at = :now",
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={":rev": "revoked", ":now": _iso8601_now()},
)
_emit_audit("pat.revoked", jti=jti)
return resp
if __name__ == "__main__": # pragma: no cover - CLI inspection helper
print("use nova/auth/login.py and nova/auth/revoke.py", file=sys.stderr)
+551
View File
@@ -0,0 +1,551 @@
# Archived: Nova IdP CloudFormation Template (v1.28)
> **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 (REQ-369 AC 3, spec §7.5). This template is read-only
> reference; do not modify it. The `nova idp setup --apply` command
> now delegates to `terraform apply` (see `nova/idp/setup.py`).
This is the verbatim output of `generate_template()` from
`core/lambda/nova_idp_cfn.py` (the composition of the DynamoDB snippet
from `core/lambda/nova_idp_auth_cfn.py` + the KMS signing key + the
three IdP Lambdas + their IAM roles + function URLs). It was the active
provisioning path through v1.28; from v1.29 the operator runs
`terraform apply` in the `nova-platform-ops` checkout and `nova idp
setup --apply` delegates to it. The CFN generation code is retained as
read-only reference and emits a `DeprecationWarning` when the CFN
fallback path is invoked (terraform absent from PATH).
```json
{
"Resources": {
"NovaUsersTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-users",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
{
"AttributeName": "user_id",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "user_id",
"AttributeType": "S"
},
{
"AttributeName": "email",
"AttributeType": "S"
}
],
"GlobalSecondaryIndexes": [
{
"IndexName": "email-index",
"KeySchema": [
{
"AttributeName": "email",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
}
}
],
"PointInTimeRecoverySpecification": {
"PointInTimeRecoveryEnabled": true
},
"AttributeShape": {
"user_id": "String",
"email": "String",
"password_hash": "String",
"owner": "String",
"roles": "List",
"created_at": "String"
}
}
},
"NovaSessionsTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-sessions",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
{
"AttributeName": "session_id",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "session_id",
"AttributeType": "S"
},
{
"AttributeName": "user_id",
"AttributeType": "S"
}
],
"GlobalSecondaryIndexes": [
{
"IndexName": "user_id-index",
"KeySchema": [
{
"AttributeName": "user_id",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
}
}
],
"TimeToLiveSpecification": {
"AttributeName": "expires_at",
"Enabled": true
},
"AttributeShape": {
"session_id": "String",
"user_id": "String",
"expires_at": "String (epoch seconds, TTL)",
"created_at": "String (ISO-8601)"
}
}
},
"NovaPasswordResetsTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-password-resets",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
{
"AttributeName": "reset_token",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "reset_token",
"AttributeType": "S"
}
],
"TimeToLiveSpecification": {
"AttributeName": "expires_at",
"Enabled": true
},
"AttributeShape": {
"reset_token": "String",
"user_id": "String",
"expires_at": "String (epoch seconds, TTL; 15 min)"
}
}
},
"NovaPatsTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "nova-pats",
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
{
"AttributeName": "jti",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "jti",
"AttributeType": "S"
},
{
"AttributeName": "sub",
"AttributeType": "S"
},
{
"AttributeName": "pat_hash",
"AttributeType": "S"
}
],
"GlobalSecondaryIndexes": [
{
"IndexName": "sub-index",
"KeySchema": [
{
"AttributeName": "sub",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
}
},
{
"IndexName": "pat_hash-index",
"KeySchema": [
{
"AttributeName": "pat_hash",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
}
}
],
"TimeToLiveSpecification": {
"AttributeName": "expires_at",
"Enabled": true
},
"AttributeShape": {
"jti": "String (PK)",
"sub": "String (GSI1; subject / user_id)",
"pat_hash": "String (GSI2; SHA-256 of the PAT for lookup)",
"status": "String (active|revoked)",
"issued_at": "String (ISO-8601)",
"expires_at": "String (epoch seconds, TTL)",
"revoked_at": "String (ISO-8601, present iff status=revoked)",
"claims": "Map (JWT claims payload)"
}
}
},
"NovaOidcSigningKey": {
"Type": "AWS::KMS::Key",
"Properties": {
"Description": "Nova OIDC token signing key (REQ-337, ECC_NIST_P256)",
"KeySpec": "ECC_NIST_P256",
"KeyUsage": "SIGN_VERIFY",
"KeyPolicy": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Sub": "arn:aws:iam::${AWS::AccountId}:root"
}
},
"Action": "kms:*",
"Resource": "*"
}
]
}
}
},
"NovaOidcSigningKeyAlias": {
"Type": "AWS::KMS::Alias",
"Properties": {
"AliasName": "alias/nova-oidc-signing",
"TargetKeyId": {
"Fn::GetAtt": "NovaOidcSigningKey.Arn"
}
}
},
"NovaIdpAuthRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": {
"Fn::Sub": "lambda.${AWS::Region}.amazonaws.com"
}
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "NovaIdpAuthPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": {
"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"
}
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup"
],
"Resource": {
"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
}
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
"dynamodb:DeleteItem"
],
"Resource": [
{
"Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/nova-users"
},
{
"Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/nova-sessions"
},
{
"Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/nova-password-resets"
}
]
}
]
}
}
]
}
},
"NovaIdpTokenVendRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": {
"Fn::Sub": "lambda.${AWS::Region}.amazonaws.com"
}
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "NovaIdpTokenVendPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": {
"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"
}
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup"
],
"Resource": {
"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
}
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
"dynamodb:DeleteItem"
],
"Resource": [
{
"Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/nova-pats"
}
]
},
{
"Effect": "Allow",
"Action": [
"kms:Sign",
"kms:GetPublicKey",
"kms:DescribeKey"
],
"Resource": {
"Fn::GetAtt": "NovaOidcSigningKey.Arn"
}
}
]
}
}
]
}
},
"NovaIdpJwksRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": {
"Fn::Sub": "lambda.${AWS::Region}.amazonaws.com"
}
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "NovaIdpJwksPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": {
"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"
}
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup"
],
"Resource": {
"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
}
},
{
"Effect": "Allow",
"Action": [
"kms:Sign",
"kms:GetPublicKey",
"kms:DescribeKey"
],
"Resource": {
"Fn::GetAtt": "NovaOidcSigningKey.Arn"
}
}
]
}
}
]
}
},
"NovaIdpAuthFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "nova_idp_auth.lambda_handler",
"Runtime": "python3.12",
"MemorySize": 512,
"Timeout": 30,
"Role": {
"Fn::GetAtt": [
"NovaIdpAuthRole",
"Arn"
]
},
"Environment": {
"Variables": {
"NOVA_USERS_TABLE": "nova-users",
"NOVA_SESSIONS_TABLE": "nova-sessions",
"NOVA_PASSWORD_RESETS_TABLE": "nova-password-resets",
"NOVA_PATS_TABLE": "nova-pats"
}
},
"Code": {
"ZipFile": "def lambda_handler(event, context):\n return {}"
}
}
},
"NovaIdpTokenVendFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "nova_idp_token_vend.lambda_handler",
"Runtime": "python3.12",
"MemorySize": 512,
"Timeout": 30,
"Role": {
"Fn::GetAtt": [
"NovaIdpTokenVendRole",
"Arn"
]
},
"Environment": {
"Variables": {
"NOVA_USERS_TABLE": "nova-users",
"NOVA_SESSIONS_TABLE": "nova-sessions",
"NOVA_PASSWORD_RESETS_TABLE": "nova-password-resets",
"NOVA_PATS_TABLE": "nova-pats",
"NOVA_OIDC_KMS_KEY_ID": "alias/nova-oidc-signing"
}
},
"Code": {
"ZipFile": "def lambda_handler(event, context):\n return {}"
}
}
},
"NovaIdpJwksFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "nova_idp_jwks.lambda_handler",
"Runtime": "python3.12",
"MemorySize": 256,
"Timeout": 30,
"Role": {
"Fn::GetAtt": [
"NovaIdpJwksRole",
"Arn"
]
},
"Environment": {
"Variables": {
"NOVA_OIDC_KMS_KEY_ID": "alias/nova-oidc-signing"
}
},
"Code": {
"ZipFile": "def lambda_handler(event, context):\n return {}"
}
}
},
"NovaIdpAuthUrl": {
"Type": "AWS::Lambda::Url",
"Properties": {
"TargetFunction": {
"Ref": "NovaIdpAuthFunction"
},
"AuthType": "AWS_IAM"
}
},
"NovaIdpTokenVendUrl": {
"Type": "AWS::Lambda::Url",
"Properties": {
"TargetFunction": {
"Ref": "NovaIdpTokenVendFunction"
},
"AuthType": "AWS_IAM"
}
},
"NovaIdpJwksUrl": {
"Type": "AWS::Lambda::Url",
"Properties": {
"TargetFunction": {
"Ref": "NovaIdpJwksFunction"
},
"AuthType": "NONE"
}
}
}
}
```
+123
View File
@@ -0,0 +1,123 @@
# CodeArtifact Provisioning — Status + Fallback (REQ-323, CAP-035)
> Phase P1 (cli-substrate), milestone v1.28. Owner: backend-engineer.
> This document records the CodeArtifact provisioning check outcome for
> the `nova-cli` wheel + Lambda layer publish pipeline (REQ-323), the
> required IAM grants, and the fallback wheel-index mode the publish
> workflow supports when CodeArtifact is not yet provisioned.
## 1. Provisioning check (best-effort, P1 Wave 4 gate)
**Target account:** `581513795199` (the Nova platform account).
**Attempted commands:**
```bash
aws codeartifact list-domains --region us-east-1
aws codeartifact describe-repository --domain nova --repository nova-pypi --region us-east-1
aws codeartifact list-repositories --domain nova --region us-east-1
```
**Result:** the check could not complete — no AWS credentials were
available in the P1 execute environment (`Unable to locate credentials.
You can configure credentials by running `aws configure`.`). This is
the "fail gracefully" path documented in the task spec: provisioning is
**not attempted** from this environment because the required IAM grants
are not confirmed for the execute principal.
**Classification:** P1 blocker for the CodeArtifact mode of the publish
workflow's wheel-upload step. The workflow ships with a fallback mode
(see §3) so the pipeline is not blocked on CodeArtifact provisioning —
it can publish to a private wheel index instead.
## 2. Required IAM grants (for a follow-up provisioning task)
To provision + use CodeArtifact as the wheel index, the principal that
runs the publish workflow (OIDC role `nova-publish-*` or the spike
runner) needs the following grants in account `581513795199`:
| Action | Scope (example) | Purpose |
| --- | --- | --- |
| `codeartifact:CreateDomain` | `arn:aws:codeartifact:us-east-1:581513795199:domain/nova` | create the `nova` domain |
| `codeartifact:CreateRepository` | `arn:aws:codeartifact:us-east-1:581513795199:repository/nova/*` | create `nova-pypi` (pypi-format) |
| `codeartifact:GetRepositoryEndpoint` | `arn:aws:codeartifact:us-east-1:581513795199:repository/nova/nova-pypi` | get the twine/pip endpoint |
| `codeartifact:GetAuthorizationToken` | `arn:aws:codeartifact:us-east-1:581513795199:domain/nova/*` | mint short-lived upload token |
| `codeartifact:ReadFromRepository` | `arn:aws:codeartifact:us-east-1:581513795199:repository/nova/nova-pypi` | pip install (consumers + the composite action) |
| `codeartifact:PublishPackageToRepository` | `arn:aws:codeartifact:us-east-1:581513795199:repository/nova/nova-pypi` | twine upload |
| `ssm:PutParameter` / `ssm:GetParameter` | `arn:aws:ssm:us-east-1:581513795199:parameter/nova/layer/*` | CAP-035 version↔ARN mapping |
| `lambda:PublishLayerVersion` | `arn:aws:lambda:us-east-1:581513795199:layer:nova-cli` | Lambda layer publish |
| `iam:CreateRole` / `iam:PassRole` (already held) | — | only if a dedicated publish OIDC role must be created |
The domain + repository to provision:
- **Domain:** `nova`
- **Repository:** `nova-pypi` (format: `pypi`)
- **Endpoint (twine/pip):**
`https://nova-581513795199.d.codeartifact.us-east-1.amazonaws.com/pypi/nova-pypi/`
Once provisioned, set the repository secret `NOVA_CODEARTIFACT_DOMAIN=nova`
on both forges and the publish workflow + composite action will switch
to CodeArtifact mode automatically (see §3).
## 3. Fallback: private wheel index (`NOVA_WHEEL_INDEX`)
Both the publish workflow (`.github/workflows/publish.yml` and its
byte-identical mirror on the dev forge) and the composite action
(`.github/actions/nova-cli/action.yml`) support a **fallback mode** that
does not require CodeArtifact. The selection is env/secret driven:
| Mode | Trigger | Upload target | Install source |
| --- | --- | --- | --- |
| **CodeArtifact** | `NOVA_CODEARTIFACT_DOMAIN` env/secret is set | `aws codeartifact login --tool twine` → twine uploads to the CodeArtifact pypi endpoint | `aws codeartifact login --tool pip``pip install nova==<ver>` |
| **Fallback index** | `NOVA_CODEARTIFACT_DOMAIN` unset; `TWINE_REPOSITORY_URL` + `TWINE_USERNAME` + `TWINE_PASSWORD` set | `twine upload` to `TWINE_REPOSITORY_URL` | `pip install --index-url $NOVA_WHEEL_INDEX nova==<ver>` |
The fallback index can be any PEP 503-compliant simple index — e.g. a
private package registry hosted on the dev forge, a self-hosted
`pypiserver`, or a static S3-backed index. The workflow does not hardcode
the index URL; it is supplied via the `NOVA_WHEEL_INDEX` env var (for
consumers / the composite action) and `TWINE_REPOSITORY_URL` (for the
publish step). This keeps the forge/registry choice deployment-specific
and avoids baking any single hostname into the synced workflow files.
### 3.1 Fallback index shape (when self-hosted)
A minimal PEP 503 simple index served from a private registry is
sufficient. The only required layout per package:
```
/nova/
index.html # links to each version's page
/nova-<version>-py3-none-any.whl # the wheel (publish workflow uploads this)
```
The publish workflow uploads `dist/nova-<version>-*.whl` via `twine
upload` to `TWINE_REPOSITORY_URL`; consumers install via
`pip install --index-url "$NOVA_WHEEL_INDEX" nova==<version>`.
## 4. CAP-035 invariant (unaffected by the index choice)
Regardless of which wheel index is used, the Lambda layer ARN ↔ wheel
version mapping is recorded in SSM and is the source of truth for
CAP-035:
```
/nova/layer/nova-cli/version = "<wheel-version>:<layer-arn>"
```
e.g. `1.14.0:arn:aws:lambda:us-east-1:581513795199:layer:nova-cli:3`.
The publish workflow writes this parameter atomically after both the
wheel upload and the layer publish succeed; if either fails the job
fails (merge blocked, REQ-323 AC).
## 5. Open follow-ups
1. Provision CodeArtifact domain `nova` + repository `nova-pypi` in
`581513795199` once the `codeartifact:*` grants in §2 are attached to
the publish OIDC role. Update this document with the confirmed ARN +
endpoint.
2. Set the `NOVA_CODEARTIFACT_DOMAIN` repository secret on both forges
to switch the publish workflow + composite action from fallback-index
mode to CodeArtifact mode.
3. Until §1 is done, the fallback index must be provisioned out of band
and its URL exposed to consumers via the `NOVA_WHEEL_INDEX` env var
(and to the publish workflow via the `TWINE_*` secrets).
+334
View File
@@ -0,0 +1,334 @@
# Developer Guide — Nova Auth (`nova auth`)
> **REQ-346** — developer guide for `nova auth login`. Covers signup,
> signin, login, mode resolution, TTY vs piped stdout behavior, and the
> JWS-from-PAT KDF (REQ-332, C-5.2).
>
> Audience: developers using the Nova CLI to authenticate and run
> `nova apply`. For operator-side identity stack deployment, see
> `docs/operator-guide-idp.md`.
## 1. Quickstart (5 steps)
```sh
# 1. Sign up (one-time per user).
nova auth signup --email alice@example.com --owner team-a
# 2. Sign in (returns a session — valid 24h).
nova auth signin --email alice@example.com
# 3. Issue a PAT and log in (session → OIDC token, stored locally).
nova auth login --pat <PAT>
# 4. Initialize a project (one-time per repo).
nova init
# 5. Apply locally + sign a local-review attestation.
nova apply --local --sign-local-review --contract .nova/contract.yml --pat <PAT>
```
After step 3, `~/.nova/credentials.json` holds your active OIDC token
(see §4). After step 5, the attestation is a JWS verifiable with the
PAT-derived key (see §7).
## 2. `nova auth signup`
Creates a user in the `nova-users` DynamoDB table. The password is
hashed with **Argon2id** (OWASP-minimum parameters: `time_cost=3,
memory_cost=65536 KiB, parallelism=1`) — the raw password is **never**
stored, logged, or put in any env var (INV-16).
```sh
nova auth signup --email alice@example.com --password '...' --owner team-a
```
What happens server-side (the `nova-idp-auth` Lambda):
1. Validates the payload (`email`, `password`, `owner`, `roles`).
2. Checks for a duplicate email → `409` if already registered.
3. `hash_password(password)` → Argon2id hash string.
4. `PutItem` into `nova-users` (`user_id`, `email`, `password_hash`,
`owner`, `roles`, `created_at`).
5. Emits `auth.sign_up` audit event (carries `user_id` + `email`,
never the password).
If the Argon2 C extension is unavailable, the Lambda returns **503**
(fail-closed — no weak hash, no pure-Python fallback; D-228).
## 3. `nova auth signin`
Verifies the password and returns a session token.
```sh
nova auth signin --email alice@example.com --password '...'
```
The Lambda:
1. Looks up the user by email (GSI `email-index` on `nova-users`).
2. `verify_password(password, stored_hash)` — Argon2id verify.
3. On mismatch or unknown email → `401 invalid_credentials` (the same
message for both, so an attacker can't enumerate emails by timing).
4. On success: `create_session(user_id)` writes a row to `nova-sessions`
(TTL 24h) and returns `session_id`.
## 4. `nova auth login`
Exchanges a PAT (or session) for a Nova OIDC token and stores it
locally.
```sh
nova auth login --pat <PAT>
# or
nova auth login --session <session_token>
```
The flow:
1. The CLI calls the `nova-idp-token-vend` Lambda with the PAT.
2. The Lambda decodes the PAT's `jti`, does a **strongly-consistent**
`GetItem` on `nova-pats` (D-229 — revocation is reflected on the
next vend, within 60s P95).
3. Evaluates the ABAC policy (`platform/abac/token-vend.policy`) —
fail-closed (C-6.1). If the policy engine is unavailable or the
policy denies, the vend returns `403`.
4. Signs the OIDC token via KMS (`alias/nova-oidc-signing`,
`ECC_NIST_P256`, `ECDSA_SHA_256`) and returns it.
### The credentials file (`~/.nova/credentials.json`)
**C-7.3 (grill):** the file 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 never persisted. This reduces the
filesystem-compromise blast radius: an attacker who reads
`credentials.json` gets a short-lived OIDC token (default 15 min), not
the long-lived PAT.
The file is `0600` (owner read/write only). Shape:
```json
{
"active_credential_jti": "<jti>",
"credentials": [
{
"jti": "<jti>",
"type": "nova_oidc_token",
"exp": 1787200000,
"token": "<oidc jwt>",
"stored_at": 1787199000
}
]
}
```
"Most recent wins": `active_credential_jti` points at the
most-recently-stored credential. A subsequent `nova auth login`
replaces the entry with the same `jti` (or adds a new one).
## 5. `nova auth status`
Shows the active credential, the resolved mode, and the
`selection_reason`.
```sh
nova auth status
```
Output (JSON):
```json
{
"mode": "interactive",
"selection_reason": "credential:developer_pat",
"type": "nova_oidc_token",
"jti": "...",
"exp": 1787200000
}
```
If no credential is stored: `{"status": "no active credential"}`.
## 6. `nova auth revoke --pat <jti>`
Revokes a PAT by `jti`. Marks the `nova-pats` row `status=revoked`
(the row is **retained** for audit, not deleted). The next
`nova auth login` with that PAT returns `403 pat_revoked` within 60s
P95 (D-229 strong read).
```sh
nova auth revoke --pat <jti>
```
For emergency DDB-level revocation (when the CLI is unavailable), see
`docs/operator-guide-idp.md` §9.
## 7. Mode resolution (D-226)
The CLI resolves a client mode (`interactive` or `agent`) on every
invocation. The mode drives audit observability (INV-12) and some
behavioral defaults. The priority is **strict** — no silent fallbacks
(INV-13):
1. **`--mode` flag** (always wins): `nova apply --mode=agent`.
2. **`NOVA_CLIENT_MODE` env var**: `export NOVA_CLIENT_MODE=agent`.
Invalid values (anything other than `agent` / `interactive`) are
**warned and ignored** (fall through to the next level — not a
silent fallback, because a warning is emitted).
3. **Credential type** (from `~/.nova/credentials.json`): if the active
credential is `developer_pat` or `nova_oidc_token`, the mode is
`interactive` if a TTY is attached, `agent` otherwise (INV-14).
4. **TTY heuristic** (`sys.stdin.isatty()`): `interactive` if stdin is
a TTY, `agent` otherwise.
Every resolution returns a non-empty `selection_reason` (`flag`, `env`,
`credential:<type>`, or `tty`) so the audit event is self-explanatory.
### TTY vs piped stdout — the Edge 3 case
The TTY check is **`sys.stdin.isatty()`**, not `sys.stdout.isatty()`.
This matters when stdout is piped but stdin is still a terminal:
```sh
nova apply | tee log.txt
```
Here `stdout` is a pipe (to `tee`), but `stdin` is still the terminal.
So `sys.stdin.isatty()` returns `True`**interactive mode**. This is
the common "I want to see the output AND save it" pattern, and it
correctly resolves to interactive because the human is driving.
The inverse — `echo '...' | nova apply` — has `stdin` piped, so
`sys.stdin.isatty()` is `False`**agent mode** (no human at the
keyboard; the pipe is the driver).
### `developer_pat` + TTY → interactive; + no TTY → agent
A developer PAT (`type: developer_pat`) is a human credential. When a
TTY is attached, the CLI runs in `interactive` mode (prompts, human
confirmation). When no TTY is attached (piped stdin, CI, a scheduled
job), the same PAT runs in `agent` mode (no prompts, non-interactive).
This is INV-14: the credential type encodes the role, and the TTY
encodes the context.
A service-account PAT behaves the same way by type, but the max TTL is
much shorter (≤ 1h vs ≤ 24h for developer PATs — C-6.2) and CI systems
typically set `NOVA_CLIENT_MODE=agent` explicitly so the resolution is
deterministic regardless of the TTY state.
## 8. JWS-from-PAT key derivation (REQ-332, C-5.2)
`nova apply --local --sign-local-review` produces a JWS attestation — a
symmetric (HMAC-SHA256) signature over the attestation payload, keyed
by a key derived from the PAT.
### Why symmetric?
The grill (C-5.2) found that the original REQ-332 acceptance criterion
("public key derivable from the PAT") is unimplementable as an
asymmetric scheme — a PAT is a JWT, not a keypair. The fix: the PAT is
the **shared secret**. Both the signing key and the verification key
are derived from the PAT via the same KDF. The JWS uses `HS256`
(HMAC-SHA256), not `ES256`.
### The KDF
```
key = HKDF-SHA256(
input_key_material = PAT.encode('utf-8'),
salt = b'nova-local-attestation',
info = b'jws-signing-key',
length = 32,
)
```
(RFC 5869 / NIST SP 800-56C.) The `salt` and `info` are fixed
constants — they bind the derived key to the "nova-local-attestation /
jws-signing-key" purpose (key separation, INV-16). The same PAT always
yields the same key (deterministic); the key is never cached or
persisted (INV-15 — recomputed on each sign/verify call).
### Signing (`nova apply --local --sign-local-review`)
```sh
nova apply --local --sign-local-review --pat <PAT> --contract .nova/contract.yml
```
1. `core.jws_attestation.sign_attestation(payload, pat)`:
- `derive_signing_key(pat)` → 32-byte key.
- `header = {"alg":"HS256","typ":"JWT"}`.
- `signing_input = b64url(header) + "." + b64url(payload)`.
- `signature = HMAC-SHA256(key, signing_input)`.
- Returns `b64url(header).b64url(payload).b64url(signature)` (the
compact JWS serialization).
2. The JWS is appended to the apply output.
### Verifying
Anyone holding the PAT can derive the same key and verify:
```python
from core.jws_attestation import verify_attestation
payload = verify_attestation(jws_string, pat)
# raises JWSValidationError on tampering or wrong PAT
```
`verify_attestation` recomputes the HMAC and compares in constant time
(`hmac.compare_digest`). Without the PAT, the HMAC cannot be forged —
this is the integrity guarantee for local-review attestations.
### What this is NOT
- **Not a non-repudiation scheme.** Anyone with the PAT can sign, so
the signature proves "someone with the PAT signed this payload" —
not a specific individual. Non-repudiation is the job of the audit
trail (INV-12), not the JWS.
- **Not a replacement for the OIDC token.** The OIDC token (from
`nova auth login`) is the credential for remote operations; the JWS
is for local-review attestation integrity only.
## 9. Service-account PATs (CI usage)
A CI system (GitHub Actions, or an internal forge runner) uses a service-account
PAT to run `nova apply` non-interactively.
```sh
# In CI:
export NOVA_PAT=<service-account-pat>
export NOVA_CLIENT_MODE=agent
nova auth login --pat "$NOVA_PAT"
nova apply --contract contracts/microservice.yml
```
- `NOVA_CLIENT_MODE=agent` makes mode resolution deterministic (level 2
beats level 3/4), regardless of whether the CI runner attaches a TTY.
- No TTY → `agent` mode anyway, but the env var is belt-and-suspenders.
- **Max TTL: ≤ 1h for service-account PATs** (C-6.2). The
`issue_pat(subject_type="service-account", ttl_seconds=3600)` call
clamps any higher request to 3600s. Rotate the PAT before it expires
(CI should mint a fresh one per run or daily).
### TTL summary (C-6.2)
| Subject type | Max TTL | Typical use |
|--------------|---------|-------------|
| `developer` | ≤ 24h (86400s) | local dev, interactive |
| `service-account` | ≤ 1h (3600s) | CI, automated pipelines |
The TTL is enforced in `core.pat_lifecycle.issue_pat` — a request for
more than the max is silently clamped (with an audit event recording
the requested vs actual TTL).
---
## Appendix — command reference
| Command | What it does |
|---------|--------------|
| `nova auth signup` | create a user (Argon2id hash) |
| `nova auth signin` | verify password → session token |
| `nova auth login --pat <PAT>` | PAT → OIDC token, store in `~/.nova/credentials.json` (0600) |
| `nova auth status` | active credential + mode + selection_reason |
| `nova auth revoke --pat <jti>` | mark a PAT revoked (D-229 SLO ≤ 60s P95) |
| `nova apply --local --sign-local-review --pat <PAT>` | local apply + JWS attestation (HS256, PAT-derived key) |
| File | Purpose |
|------|---------|
| `~/.nova/credentials.json` | OIDC token + PAT metadata (NOT raw PAT); 0600 |
| `~/.nova/contract.yml` | project contract (scaffolded by `nova init`) |
| `~/.nova/contract.yml.attestations/` | local attestation outputs |
+51
View File
@@ -0,0 +1,51 @@
# kyverno-json (`kj`) Lambda layer
This document records how the `kj` (kyverno-json) binary is pinned and
bundled into the Nova token-vend Lambda layer (D-227, C-8.2).
## Pin (C-8.2)
The `kj` binary is pinned to a specific release. The version + SHA256
of the binary used for local ABAC tests and bundled into the Lambda
layer are recorded in [`platform/abac/kj-version.txt`](../platform/abac/kj-version.txt):
```
<version>
<sha256>
```
**Current pin:** `v0.0.3`
`4ebb9a19fbf545e17f046c137f9b69c4288d021e5c73d962835671e0cb3fbf07`
(measured from `/usr/local/bin/kj` on the build host).
C-8.2 requires pinning to a specific release (not `latest`) and
recording the SHA256 so a supply-chain compromise of the upstream
release is detectable. The build step downloads the pinned release,
verifies the SHA256 against the recorded value, and aborts on mismatch.
## Lambda layer bundling
The publish workflow (P1, `.github/workflows/`) bundles the pinned `kj`
Linux amd64 binary into the `nova-cli` Lambda layer at `layer/bin/kj`.
At runtime the Lambda mounts the layer at `/opt`, so `kj` is on PATH at
`/opt/bin/kj`. `KyvernoJsonEngine.is_configured()` checks `which kj`
`/opt/bin/kj` and returns `False` when absent — the token-vend Lambda
then **fails closed** (C-6.1, 403 `abac_eval_failed`), it never vends a
token without an ABAC decision.
## Local testing
`/usr/local/bin/kj` exists on the build host. The local ABAC tests
(`tests/test_abac_policy.py`, `tests/test_abac_fail_closed.py`) use the
real `kj` binary — they are skipped (not failed) when `kj` is absent.
## Fallback / migration path (D-227)
If the `kj` Go binary proves unsuitable for the Lambda runtime (e.g. a
future release exceeds the 250 MB layer unzip limit or drops AL2023
compatibility), the migration path is to run kyverno-json on AWS
Fargate behind an internal NLB and have the token-vend Lambda call it
over HTTP. The `PolicyEngine` Protocol (`core/policy_engine.py`) is the
swap boundary — a `KyvernoJsonHttpEngine` would implement the same
protocol without touching the token-vend Lambda's ABAC fail-closed
logic. This is a documented fallback, not the v1.28 default.
+53
View File
@@ -0,0 +1,53 @@
# KMS asymmetric key provisioning (C-1.1)
This document records the C-1.1 verification for the Nova OIDC signing
KMS key and the provisioning path used by `nova idp setup`.
## C-1.1 verification (P4)
C-1.1 requires verifying KMS asymmetric key support **before**
implementation. The verification command is:
```
aws kms create-key \
--key-spec ECC_NIST_P256 \
--key-usage SIGN_VERIFY \
--description nova-oidc-signing
```
**Result on the P4 build host:** AWS credentials are not available
(`Unable to locate credentials`), so the live verification could not
run. This is recorded as a **P4 CI gate**: the `nova idp setup --check`
command (Wave 8) performs this verification when AWS creds are present
and reports it as a missing prerequisite when they are not. The code
proceeds against the documented KMS API (REQ-337); tests use a test
ECDSA P-256 keypair + mocked `boto3.client("kms")` (no real AWS calls).
KMS asymmetric signing keys (`ECC_NIST_P256` + `SIGN_VERIFY`) are GA
in all commercial regions (announced 2020-11). The
`ECDSA_SHA_256` signing algorithm is supported. Confidence: high.
## Key spec (REQ-337)
* **Key spec:** `ECC_NIST_P256` (NIST P-256 / secp256r1)
* **Key usage:** `SIGN_VERIFY`
* **Signing algorithm:** `ECDSA_SHA_256` (JWS `ES256`)
* **Alias:** `alias/nova-oidc-signing`
* **Rotation:** manual, 90 days (matches D-069 CMK cadence). New key +
re-point alias + JWKS serves both `kid`s during overlap.
## DER → raw ECDSA conversion (the #1 gotcha)
KMS `sign()` returns a **DER-encoded** ASN.1 ECDSA signature. JWS
(RFC 7515 §3.1.3) requires the **raw** `r‖s` concatenation, each
coordinate 32 bytes big-endian. The conversion (in
`core/kms_signing.py:der_to_raw_ecdsa`):
```python
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
r, s = decode_dss_signature(der_sig)
raw = r.to_bytes(32, "big") + s.to_bytes(32, "big")
```
This is verified by `tests/test_kms_signing.py` and the CAP-037
round-trip test (`tests/test_kms_roundtrip.py`).
+385
View File
@@ -0,0 +1,385 @@
# Operator Guide — Nova IdP Setup (`nova idp setup`)
> **REQ-345** — operator guide for `nova idp setup`. Covers `--check`,
> `--apply`, `--verify`, the prerequisite IAM policy, the CloudFormation
> review flow, and the **C-6.3 grill additions**: KMS key rotation
> (90 days), Lambda layer update, DDB PITR restore, emergency PAT
> revocation (DDB-level, not CLI).
>
> Audience: platform operators / SREs deploying the Nova identity stack
> into AWS account `581513795199` (or a fresh account). No developer
> auth flows here — see `docs/developer-guide-auth.md` for those.
## 1. Overview
`nova idp setup` provisions the Nova identity layer (Nova-idp) as a
CloudFormation stack. The stack contains:
| Resource | Count | Notes |
|----------|-------|-------|
| Lambda functions | 3 | `nova-idp-auth`, `nova-idp-token-vend`, `nova-idp-jwks` |
| DynamoDB tables | 4 | `nova-users`, `nova-sessions`, `nova-password-resets`, `nova-pats` (PITR enabled on each, REQ-335) |
| KMS asymmetric key | 1 | `alias/nova-oidc-signing` (`ECC_NIST_P256`, `SIGN_VERIFY`) |
| Lambda function URLs | 3 | auth + token-vend (IAM auth), jwks (`AuthType: NONE`) |
| IAM roles | 3+ | one per Lambda + the CloudFormation service role |
| Optional CloudFront + WAF + ACM | 0/3 | only with `--public-jwks-domain` |
The command has three modes — `--check`, `--apply`, `--verify` — plus
`--dry-run` for a resource-only preview. All modes are safe to re-run.
## 2. `nova idp setup --check`
Run **before** `--apply` to verify the deploying principal has the
permissions and environment the stack needs.
```sh
nova idp setup --check
```
### What it checks
1. **AWS credentials**`aws sts get-caller-identity` succeeds and
returns an `Account` id. If this fails, run `aws configure` or export
`AWS_PROFILE` / `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`.
2. **AWS region**`AWS_DEFAULT_REGION` or `AWS_REGION` is set. The
stack is regional (single-region); pick the region you want all
resources to live in.
3. **CloudFormation permissions** — the principal can create/describe
stacks (see §5 for the full IAM delta).
4. **KMS permissions**`kms:CreateKey` + `kms:CreateAlias` (needed to
mint `alias/nova-oidc-signing`).
5. **Lambda layer exists** — the `nova-cli` Lambda layer (published by
the P1 Wave 4 pipeline) is referenced by the stack; `--check` reports
whether the layer ARN in SSM (`/nova/layer/nova-cli/version`) is
present. If absent, run the publish workflow or `nova layer update`.
### Reading the IAM policy delta
`--check` prints a report like:
```json
{
"aws_creds": true,
"region": "us-east-1",
"missing": [],
"iam_delta": [
"cloudformation:*",
"iam:CreateRole",
"iam:PassRole",
"lambda:CreateFunction",
"lambda:CreateFunctionUrlConfig",
"dynamodb:CreateTable",
"kms:CreateKey",
"kms:CreateAlias"
]
}
```
`iam_delta` is the **delta** between what the deploying principal
currently has (the `nova-spike-runner` grants in this account) and what
`--apply` needs. Each entry is a grant you must add to the principal's
policy before `--apply` will succeed. `--check` never makes changes.
## 3. `nova idp setup --apply`
Generates the CloudFormation template, presents it for review, and
deploys **only after explicit `y/N` approval** (NFR-10).
```sh
nova idp setup --apply
```
### Review flow
1. **Resource summary** printed to stdout (resource type → count):
```
Resource summary:
AWS::DynamoDB::Table: 4
AWS::IAM::Role: 3
AWS::KMS::Key: 1
AWS::Lambda::Function: 3
AWS::Lambda::Url: 3
```
2. **Full template** opened in `$PAGER` (if set and stdin is a TTY);
otherwise the path to the temp file is printed. Review every
resource, especially the KMS key policy and the IAM roles.
3. **`Apply? [y/N]` prompt.** Type `y` + Enter to deploy; anything else
aborts. No resource is created before this approval.
4. On approval: `aws cloudformation deploy --stack-name nova-idp
--template-file <tmp> --capabilities CAPABILITY_IAM`.
### `--dry-run` — resource list only
```sh
nova idp setup --dry-run
```
Generates the template and prints the resource summary **without** the
pager, the prompt, or any deploy. Use this to audit the stack shape in
CI or before a manual `--apply`.
### `--public-jwks-domain` — optional custom domain + WAF
```sh
nova idp setup --apply --public-jwks-domain jwks.nova.example.com
```
Adds a CloudFront distribution fronting the JWKS Lambda function URL, an
ACM certificate (DNS-validated) for the domain, and a WAF web ACL with
a rate-based rule (see §C-6.3 and the threat model). Without this flag
the JWKS endpoint is a bare function URL (`AuthType: NONE`) — fine for
piloting but exposed to the internet without rate limiting. **For any
public deployment, set `--public-jwks-domain`.**
## 4. `nova idp setup --verify`
Runs the KMS round-trip test (CAP-037) against the deployed stack.
```sh
nova idp setup --verify
```
It signs a test JWT via `core.kms_signing.sign_jwt()` (using the real
KMS key `alias/nova-oidc-signing`), fetches the JWKS endpoint, and
verifies the JWT signature with `pyjwt` + the JWKS key. This exercises
the full DER → raw ECDSA conversion path (the #1 implementation risk —
see `docs/threat-model.md`).
**Success output:**
```json
{"passed": true, "detail": "KMS round-trip OK"}
```
**Failure output:**
```json
{"passed": false, "detail": "verify error: <exception>"}
```
Common failure causes:
- The KMS key policy doesn't grant `kms:Sign` to the verify caller.
- The JWKS function URL is not deployed or returns a non-200.
- The KMS key spec isn't `ECC_NIST_P256` (the DER→raw conversion
assumes P-256, 32-byte coordinates).
## 5. Required IAM policy
The delta `--check` reports is the set of grants the deploying
principal needs **in addition** to the existing `nova-spike-runner`
grants. The full required set:
| Action | Why |
|--------|-----|
| `cloudformation:*` | create/deploy/describe the `nova-idp` stack |
| `codeartifact:*` | (already on `nova-spike-runner`) publish the wheel + layer |
| `iam:CreateRole` | create the per-Lambda execution roles |
| `iam:PassRole` | pass those roles to Lambda + CloudFormation |
| `lambda:CreateFunction` | create the 3 Lambda functions |
| `lambda:CreateFunctionUrlConfig` | create the 3 function URLs |
| `dynamodb:CreateTable` | create the 4 DDB tables (with PITR) |
| `kms:CreateKey` | mint the `ECC_NIST_P256` signing key |
| `kms:CreateAlias` | bind `alias/nova-oidc-signing` to the key |
| `ssm:PutParameter` | write the layer-version mapping to SSM |
Attach these to the deploying principal's policy before `--apply`.
`--check` will then report an empty `missing` list.
---
## C-6.3 Grill additions — operational runbooks
The grill (C-6.3) requires four operational procedures beyond the
setup flow. Each is a runbook an on-call SRE can follow without reading
source code.
### 6. KMS key rotation (90-day cadence)
**Cadence:** rotate `alias/nova-oidc-signing` every **90 days**. The
rotation is a *key re-point*, not a key deletion — the alias is moved
to a new key while the old key stays valid during the token-overlap
window so already-issued tokens keep verifying.
**Procedure:**
1. **Create the new key** (same spec):
```sh
NEW_KEY=$(aws kms create-key \
--key-spec ECC_NIST_P256 \
--key-usage SIGN_VERIFY \
--description "nova-oidc-signing-$(date +%Y%m%d)" \
--query KeyId --output text)
```
2. **Re-point the alias** to the new key:
```sh
aws kms update-alias --alias-name alias/nova-oidc-signing \
--target-key-id "$NEW_KEY"
```
3. **JWKS serves both `kid`s during the overlap window.** The JWKS
Lambda lists **all** keys the alias has pointed at that are still
enabled. Already-issued OIDC tokens (signed with the old key) keep
verifying until they expire (OIDC TTL default 15 min; PAT TTL ≤ 24h
dev / ≤ 1h service-account). **Do not disable the old key until at
least the max PAT TTL (24h) has elapsed.**
4. **After the overlap window** (≥ 24h), disable + schedule deletion of
the old key:
```sh
aws kms disable-key --key-id "<old-key-id>"
aws kms schedule-key-deletion --key-id "<old-key-id>" --pending-window-in-days 7
```
5. **Verify** the new key is active:
```sh
nova idp setup --verify
```
**Audit:** emit a manual `kms.key_rotated` event to the audit stream
with `old_key_id`, `new_key_id`, `rotated_at`. The rotation is a
CloudFormation-less operation (KMS aliases are mutable); it does not
require a stack update.
### 7. Lambda layer update
The `nova-cli` Lambda layer (the shared dependency bundle:
`argon2-cffi`, `cryptography`, `pyjwt`, `kj` binary) is republished
**automatically on every merge to `main`** by the P1 Wave 4 publish
workflow (the byte-identical GitHub + internal-forge workflow files).
On a successful publish, the new layer version ARN is written to SSM
`/nova/layer/nova-cli/version`.
**When to update manually:**
- A dependency CVE requires an out-of-band patch before the next merge.
- The `kj` binary pinned version changes (C-8.2 supply-chain safety).
**Manual procedure:**
```sh
nova layer update
```
This rebuilds the layer (`pip install --target layer/python/` + the
pinned `kj` binary, SHA256 verified against `layer/kj.sha256`),
publishes a new `lambda:PublishLayerVersion`, and updates the SSM
parameter. The 3 Nova-idp Lambdas pick up the new layer on their next
cold start (or force a redeploy with `aws lambda update-function-configuration
--layers <new-arn>` on each).
**Verify:** `nova idp setup --verify` after the Lambdas reload.
### 8. DynamoDB PITR restore
All 4 identity tables have point-in-time recovery (PITR) enabled
(REQ-335): `nova-users`, `nova-sessions`, `nova-password-resets`,
`nova-pats`. PITR lets you restore a table to any second in the last
**35 days** (the AWS retention window).
**Procedure (restore `nova-pats` to 1 hour ago):**
```sh
# 1. Find the restore target time (ISO 8601, UTC, within the last 35d).
RESTORE_TO=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)
# 2. Restore to a NEW table (PITR never overwrites the source).
aws dynamodb restore-table-to-point-in-time \
--source-table-name nova-pats \
--target-table-name nova-pats-restored \
--restore-date-time "$RESTORE_TO" \
--billing-mode-restore-as-is
# 3. After the restore completes (status ACTIVE), repoint the app:
# - update the stack env var NOVA_PATS_TABLE=nova-pats-restored, or
# - rename: delete nova-pats, then aws dynamodb update-table --table-name
# nova-pats-restored --new-table-name nova-pats (downtime window).
# 4. Re-enable PITR on the restored table (PITR does not carry over).
aws dynamodb update-continuous-backups \
--table-name nova-pats-restored \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
```
**Which tables have PITR:** all 4 (`nova-users`, `nova-sessions`,
`nova-password-resets`, `nova-pats`). Verify with:
```sh
for t in nova-users nova-sessions nova-password-resets nova-pats; do
aws dynamodb describe-continuous-backups --table-name "$t" \
--query 'ContinuousBackupsDescription.PointInTimeRecoveryDescription' --output text
done
```
**Recovery window:** 35 days (AWS PITR). Restores older than 35 days
are impossible — for longer retention, export to S3 via the on-demand
export or a scheduled AWS Backup plan.
### 9. Emergency PAT revocation (DDB-level, not CLI)
**When to use:** a PAT is known-compromised and the `nova auth revoke`
CLI is unavailable (e.g. the operator machine is offline, or the PAT
`jti` is known but the raw PAT is not — revocation is keyed on `jti`,
not the token string). This is a **DDB-level** operation; it bypasses
the CLI but still satisfies the D-229 strong-read SLO (the token-vend
Lambda does a `ConsistentRead=True` `GetItem` on `jti` on every vend —
the revocation is reflected on the next vend, within 60s P95).
**Procedure:**
```sh
aws dynamodb update-item \
--table-name nova-pats \
--key '{"jti":{"S":"<jti>"}}' \
--update-expression "SET #s = :r" \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":r":{"S":"revoked"}}'
```
Replace `<jti>` with the PAT's `jti` claim (a uuid4; find it in the
`pat.issued` audit event or by scanning the `sub-index` GSI for the
compromised subject). The item is **retained** (not deleted) so the
audit trail is intact — only `status` flips from `active` to `revoked`.
**Verify the revocation took effect:**
```sh
aws dynamodb get-item \
--table-name nova-pats \
--key '{"jti":{"S":"<jti>"}}' \
--consistent-read \
--query 'Item.status.S' --output text
# → revoked
```
The next `token-vend` call with that `jti` returns `403
pat_revoked` immediately (D-229: the strong read is synchronous).
**Bulk revocation** (revoke all of a subject's PATs):
```sh
SUB="<sub>"
JTIS=$(aws dynamodb query \
--table-name nova-pats \
--index-name sub-index \
--key-condition-expression "sub = :s" \
--expression-attribute-values "{\":s\":{\"S\":\"$SUB\"}}" \
--query 'Items[?status.S==`active`].jti.S' --output text)
for jti in $JTIS; do
aws dynamodb update-item --table-name nova-pats \
--key "{\"jti\":{\"S\":\"$jti\"}}" \
--update-expression "SET #s = :r" \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":r":{"S":"revoked"}}'
done
```
---
## Appendix — quick reference
| Command | What it does |
|---------|--------------|
| `nova idp setup --check` | prerequisites + IAM delta (no changes) |
| `nova idp setup --dry-run` | resource summary only (no deploy) |
| `nova idp setup --apply` | review template → `y/N` → deploy |
| `nova idp setup --apply --public-jwks-domain <fqdn>` | add CloudFront + WAF + ACM |
| `nova idp setup --verify` | KMS round-trip test (CAP-037) |
| Runbook | Cadence / trigger |
|---------|-------------------|
| KMS key rotation | every 90 days |
| Lambda layer update | on merge (auto) or manually via `nova layer update` |
| DDB PITR restore | on data loss / corruption (35-day window) |
| Emergency PAT revocation | on compromise (DDB-level, immediate) |
+747
View File
@@ -0,0 +1,747 @@
# Operator Guide — Nova Platform Ops (`nova-platform-ops`)
> **REQ-OPS-GUIDE** — the operator-facing runbook for the
> `nova-platform-ops` Terraform repo. This is the verification surface
> for the covered-reference REQs (355-366, 371): their cutover gates
> (M1/M1.5/M2) are documented in §18 below, and each REQ has a
> **"Result" column** that the operator fills in after running the gate.
> P6 audit verifies every covered-reference REQ has a non-empty, green
> Result (grill CF-2/G-5). **HARD P6 ship gate:** §3 contains the
> operator-attested "M1.5 Verification Gate Result" row (grill
> CF-1/G-2.1) — the milestone does not ship until that row is filled.
>
> Audience: platform operators / SREs running the live Nova platform in
> AWS account `581513795199`. For the developer auth flows, see
> `docs/developer-guide-auth.md`; for the legacy CloudFormation path,
> see `docs/archive/nova-idp-cfn-v1.28.md`.
## 1. Overview + reposplit rationale
Nova's platform operations live in a dedicated, Gitea-private Terraform
repository — `nova-platform-ops` — separate from the engineering repo
`acdl/acdl`. The split is grounded in Vision §4 (Domain Boundaries):
> *The platform begins where the artifact is compiled and ends where it
> runs in production under operational guardrails.*
That is two distinct disciplines with two distinct ownership surfaces:
| Discipline | Ends | Begins | Repo | Surface |
|------------|------|--------|------|---------|
| Engineering | at the compiled artifact | — | `acdl/acdl` (GitHub) | `publish.yml` + GitHub Releases |
| Operations | — | at the live platform under guardrails | `nova-platform-ops` (Gitea-private) | Terraform modules |
**Scope split (CLARIFY Q-P1, D-232):**
- `acdl/acdl` authors `publish.yml` (the artifact publish pipeline) +
the artifacts themselves (Lambda zip, layer wheel, Python wheel, ECR
container image with the static `kj` binary). Each tag `v1.29.x`
produces a GitHub Release with SHA-256-verified artifacts (REQ-354).
- `nova-platform-ops` authors the Terraform modules
(`networking`/`kms`/`identity`/`contract-ingest`/`bootstrap`/`edge`)
that bring those artifacts live in `581513795199`.
The handoff between the two repos is the **tag-pin** (D-235):
`nova-platform-ops` declares `local.nova_platform_version` +
`local.kj_source_sha` and resolves substrates through a single
`data.aws_ecr_image.kj_image`. The engineering repo never knows which
tag is live; the ops repo never authors artifacts. Vision §6
immutability + Vision §5 narrow interfaces.
The covered-reference REQs (355-366, 371) are authored in
`nova-platform-ops` (out-of-band). CIAgent in `acdl` tracks them for
milestone completeness; their verification surface is the cutover
gates in §18 of this guide.
## 2. Day-0 cutover procedure (M1)
The M1 cutover is the one-time conversion of the live AWS account
`581513795199` from CloudFormation-managed (or manually-created)
resources to Terraform-managed resources in `nova-platform-ops`. It is
conditional on the M1.5 verification gate passing (§3, Q7 carry-forward,
D-236).
The 10-step Journey 2 (spec §3.2):
1. **Create `nova-platform-ops` in Gitea** — private (`private: true`,
OPER-PRIV, REQ-359), no GitHub mirror. The repo is operator-owned;
CIAgent has no presence there.
2. **Commit the initial Terraform structure** — the module tree
(`networking`/`kms`/`identity`/`contract-ingest`/`bootstrap`/`edge`)
+ `importable-resources.tf` (§12) + `versions.tf` + `backend.tf`
(S3 state in the imported bucket).
3. **`terraform init`** — initialize the S3 backend against the
state bucket (`nova-tfstate-581513795199-us-east-1`, imported in
step 5). The bucket is created manually once (operator's secure
scratch, spec §7.1, D-235) before Terraform adopts it.
4. **`terraform import` for existing live resources** — adopt the
resources that already exist in `581513795199` into Terraform state
without recreating them. The import map is in
`importable-resources.tf` (§12):
- `aws_s3_bucket.nova_tfstate``nova-tfstate-581513795199-us-east-1`
- `aws_dynamodb_table.nova_contracts``nova-contracts`
- `aws_dynamodb_table.nova_change_requests``nova-change-requests`
- `aws_dynamodb_table.nova_outbox``nova-outbox`
- `aws_iam_role.acdl_act_runner``acdl-act-runner-role`
- per-stack CMKs (KMS keys)
Re-import exits non-zero with `resource_already_imported`
(IMPORT-IDEMPOTENT, REQ-361). CI import treats this as idempotent
success — the import workflow greps the error stream + exits 0 on
that string.
5. **(state bucket is imported in step 4)** — listed here for sequence
clarity; the S3 state bucket is the first import because the
backend depends on it.
6. **Add new resources** that do not yet exist in the account:
- KMS alias `alias/nova-oidc-signing` (§9, D-234).
- Identity DynamoDB tables: `nova-users`, `nova-sessions`,
`nova-pats` (§11).
- JWKS Function URL with `AuthType: AWS_IAM` (NOT `NONE` — §10,
INV-18, RESEARCH §4 critical pitfall).
- CloudFront distribution + OAC + WAF WebACL + ACM certificate +
Route53 alias (§14, REQ-364/365/366).
7. **`terraform plan`** — expect zero diff on the imported resources
(they are already in their desired state) + a pure-add diff on the
new resources. If the plan shows a diff on an imported resource,
the import map or the Terraform resource block is wrong — fix
before apply. **DRIFT-DETECT (REQ-356):** `terraform plan` exit 2
(drift) fails the apply workflow; manual reconciliation required.
8. **HITL approval**`terraform apply` against `main` requires a
Gitea Actions approval from a user **distinct from the PR author**
(TFM-HITL, REQ-357, INV-3). Self-approval is rejected:
`gitea.triggering_actor == pull_request.user.login` → apply fails
closed (M1.5 item 11).
9. **`terraform apply`** — on approval, the apply creates the new
resources + adopts the imported ones. Smoke test (step 10) before
declaring M1 done.
10. **Smoke test + CFN→Terraform conversion** — verify the live
account is in the desired state (JWKS reachable via CloudFront,
KMS round-trip, ABAC fail-closed). The CFN template in
`acdl/acdl/nova/idp/setup.py` is archived to
`docs/archive/nova-idp-cfn-v1.28.md` as read-only reference
(REQ-369); the active path is now `terraform apply` in
`nova-platform-ops`.
## 3. M1.5 verification gate (12-item spike)
The M1.5 gate is the 12-item spike from PLAN.md "Happy Path" (spec
§3.3 Edge 5 items 1-8 + grill CF-1 items 9-12). **3 consecutive green
rebuilds are required** in `nova-platform-ops` CI.
The 12 items:
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
`alias/nova-oidc-signing` (REQ-362 path — covered-reference).
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 fail-closed).
8. Image URI is recorded in Terraform state and in this operator
guide (§18, REQ-371 Result row).
9. **(grill CF-1) JWKS-EDGE-ONLY:** direct JWKS Function URL GET
(bypassing CloudFront) returns **403**; via-CloudFront GET returns
**200** (INV-18). Proves `AuthType: AWS_IAM` + OAC pinning.
10. **(grill CF-1) IAM-NARROW:** `aws iam get-role-policy` on the
Gitea OIDC role asserts no `Action: "*"` and no `Resource: "*"`
(REQ-360).
11. **(grill CF-1) TFM-HITL:** a `terraform apply`
`workflow_dispatch` triggered by the PR author is **rejected**
(exit non-zero); a dispatch by a distinct user proceeds (REQ-357).
12. **(grill CF-1) rollback drill:** revert `nova_platform_version`
pin → `terraform apply` → assert the prior ECR digest runs
(D-236, guards against ECR tag mutability).
If items 1-7 fail three consecutive rebuilds, M2a activates (§5,
REQ-363b Fargate toggle) with the same ECR image — no warmup hit
because the standby is always running the same digest (KJ-LOCKSTEP).
### HARD P6 ship gate (grill CF-1/G-2.1)
P6 must not ship `v1.28.6` until the operator attests the M1.5 result
in the row below. The operator fills this in **after** the gate passes
3 consecutive green rebuilds in `nova-platform-ops` CI. P6 audit
verifies the row exists + is non-empty.
#### M1.5 Verification Gate Result
| Rebuild # | Run ID / commit SHA | All 12 items green? | Attestor identity | Attested at (UTC) |
|-----------|---------------------|---------------------|-------------------|-------------------|
| 1 | _(operator fills)_ | _(yes/no)_ | _(operator fills)_ | _(operator fills)_ |
| 2 | _(operator fills)_ | _(yes/no)_ | _(operator fills)_ | _(operator fills)_ |
| 3 | _(operator fills)_ | _(yes/no)_ | _(operator fills)_ | _(operator fills)_ |
> **P6 audit rule:** all three rows must be present, all 12 items
> green on each, the three run IDs/SHAs distinct (consecutive
> rebuilds, not one run copied thrice), and the attestor identity
> non-empty. Empty or red → P6 blocks → escalate.
## 4. M2 operational handoff loop
M2 is the steady-state operational loop for rolling out an engineering
change after M1.5 is green. The loop is the tag-pin bump → plan →
HITL → apply cycle (D-235, D-238).
1. **Tag-pin bump** — the operator opens a PR in `nova-platform-ops`
bumping `local.nova_platform_version` (e.g. `v1.29.3``v1.29.4`)
+ `local.kj_source_sha` (the `kj` source SHA from
`platform/abac/kj-version.txt` at the new tag). Both pins move
together — there is one ECR image identity (KJ-LOCKSTEP, REQ-371).
2. **`terraform plan`** — CI runs `terraform plan` on the PR. The
KJ-LOCKSTEP precondition (a `lifecycle.precondition` on both
image-bearing resources — the Lambda `image_uri` and the Fargate
task `container_definitions[0].image`) checks that both
`image_uri` attributes resolve to the **same ECR digest** via
`data.aws_ecr_image.kj_image`. If the two diverge, the plan fails
closed — no second pipeline, no second SHA pin (D-238).
3. **HITL approval** — a Gitea Actions approver **distinct from the
PR author** approves the apply (TFM-HITL, REQ-357). Self-approval
is rejected (M1.5 item 11).
4. **`terraform apply`** — on approval, the apply updates both
`aws_lambda_function.nova_idp_token_vend.image_uri` and
`aws_ecs_task_definition.kj.container_definitions[0].image` to the
same ECR digest. The Lambda image + the Fargate task redeploy to
the same digest in one apply. Zero diff on KMS, DDB, IAM, edge
(the only change is the image reference).
**Verification:** after the apply, `aws lambda get-function
--function-name nova-idp-token-vend --query Configuration.Code.ImageUri`
and `aws ecs describe-tasks` on the Fargate task both report the same
digest. This is the M2 acceptance gate (PLAN §UX Acceptance Criteria
3) + the REQ-371 Result row in §18.
## 5. M2a Fargate activation (conditional)
M2a activates **only if M1.5 fails 3 consecutive rebuilds** (D-236).
It is the REQ-363b Fargate toggle — an always-warm minimal Fargate
standby running the **same ECR image** as the Lambda (KJ-LOCKSTEP).
Because the standby is always running the same digest as the Lambda,
activating M2a is **not** a warmup hit — the standby is already
serving `GET /health → 200` every 10s (KJ-WARMUP-HEALTH, §15). The
toggle repoints token-vend traffic from the Lambda to the Fargate
task; no cold start, no image pull.
If both the Lambda path and the Fargate path fail (M1.5 items 1-7
fail on both substrates), the operator escalates — Nova-idp ships in
read-only partial mode (no token issuance) until `kj` is verified
(Q7 carry-forward, spec §7.7).
**Fargate sunset discipline (D-237):** the standby (~$15-20/month,
§7) may not be deleted unless REQ-363 has been green in production
for **≥30 consecutive days**. Sunset requires an architecture review.
See §15 for the health-check procedure.
## 6. Rollback procedure (D-236)
Rollback is a tag-pin revert — the same mechanism as the M2 rollout
(§4), in reverse.
1. **Revert `nova_platform_version`** in `nova-platform-ops` to the
prior tag (e.g. `v1.29.4``v1.29.3`). Open a PR, get HITL
approval (TFM-HITL, same as rollout).
2. **`terraform apply`** — the apply reverts both the Lambda
`image_uri` and the Fargate task `image` to the prior ECR digest.
The prior tag's artifacts remain downloadable (GitHub Releases are
append-only per tag, REQ-354 AC 2) — no artifact is re-built.
3. **Verify** the prior digest is running:
```sh
aws lambda get-function --function-name nova-idp-token-vend \
--query Configuration.Code.ImageUri --output text
# → <account>.dkr.ecr.us-east-1.amazonaws.com/nova-kj@sha256:<prior-digest>
```
This is the M1.5 item 12 rollback drill + the operational rollback
procedure. It guards against ECR tag mutability (RESEARCH §2) — the
digest is immutable even if a tag is re-pushed.
## 7. Cost section
Monthly estimate for the `nova-platform-ops` live platform in account
`581513795199` (pilot volume):
| Resource | Quantity | Est. monthly | Notes |
|----------|----------|-------------|-------|
| WAF WebACL (CloudFront-scoped) | 1 | ~$5-10 | + per-request; REQ-365 |
| Fargate standby (0.25 vCPU, 512 MB) | 1 task | ~$15-20 | REQ-363b AC 4; largest line item |
| KMS asymmetric key | 1 | ~$1 | `alias/nova-oidc-signing`, ECC_NIST_P256 |
| DynamoDB (on-demand, 6 tables) | 6 | ~$2 | §11 tables |
| Lambda invocations (3 Lambdas) | 3 | ~$2 | low pilot volume |
| ECR image storage | ~100 MB | <$1 | the `kj` image |
| S3 state bucket + access logs | 1 | <$1 | `nova-tfstate-*` |
| CloudFront + ACM + Route53 | 1 distribution | ~$1 | ACM free for CloudFront-attached |
| **Total** | | **~$30-40/month** | |
**Fargate standby is the largest line item** (~$15-20/month, REQ-363b
AC 4). It is explicitly documented here with the D-237 sunset
discipline (§5, §15): ≥30 consecutive days green before deletion +
architecture review. Do not delete the standby to save ~$15/month
without that review — it is the defensive fallback for the `kj`
substrate.
## 8. Artifact-mirror fallback (Edge 6)
When the Gitea `act_runner` in `nova-platform-ops` CI cannot reach
GitHub Releases (network partition, egress restriction, GitHub
outage), the operator mirrors the artifact bundle locally by SHA-256.
**Procedure:**
1. **Download the GitHub Release bundle** for the target tag
(`v1.29.x`) from a machine that can reach GitHub Releases:
```sh
gh release download v1.29.0 \
--repo continuous-intelligence/acdl \
--pattern 'nova-lambda-token-vend-*.zip' \
--pattern 'nova-cli-layer-*.zip' \
--pattern 'nova-*-py3-none-any.whl' \
--dir ./artifact-cache
```
2. **Verify SHA-256** against the release body (each artifact's
SHA-256 is listed in the GitHub Release body, REQ-354):
```sh
sha256sum ./artifact-cache/nova-lambda-token-vend-v1.29.0.zip
# → must match the SHA-256 in the release body
```
3. **Place the bundle in the operator's local artifact cache** — a
directory the `act_runner` can read (e.g. a Gitea-lfs-tracked path
in `nova-platform-ops`, or an S3 bucket the runner can reach).
4. **Reference by SHA-256 in the terraform variables** — the
`nova-platform-ops` Terraform accepts an override for the artifact
source: `nova_artifact_mirror_sha256 = "<sha256>"`. When set, the
`data` sources resolve from the local cache by SHA-256 instead of
from GitHub Releases. Unset → resume GitHub Releases resolution.
This fallback is for CI continuity only; the live `terraform apply`
still resolves the ECR image by digest (KJ-LOCKSTEP), which is
independent of GitHub Releases availability.
## 9. KMS rotation (D-234)
The OIDC signing key `alias/nova-oidc-signing` is provisioned with
`KeySpec: ECC_NIST_P256`, `KeyUsage: SIGN_VERIFY`, on a **90-day
rotation cadence** (matches per-stack CMK rotation per D-069).
**Verify the key spec + rotation status:**
```sh
aws kms describe-key --key-id alias/nova-oidc-signing \
--query 'KeyMetadata.[KeySpec,KeyUsage,Description]' --output text
# → ECC_NIST_P256 SIGN_VERIFY <description>
```
**Apply a rotation policy** (key re-point, not key deletion — the
alias moves to a new key while the old key stays valid during the
overlap window, §17 JWKS-ROTATION):
1. Create the new key (same spec):
```sh
NEW_KEY=$(aws kms create-key \
--key-spec ECC_NIST_P256 \
--key-usage SIGN_VERIFY \
--description "nova-oidc-signing-$(date +%Y%m%d)" \
--query KeyId --output text)
```
2. Re-point the alias:
```sh
aws kms update-alias --alias-name alias/nova-oidc-signing \
--target-key-id "$NEW_KEY"
```
3. Apply the rotation policy (the key policy grants `kms:Sign` to the
token-vend Lambda role + `kms:GetPublicKey` to the JWKS Lambda
role):
```sh
aws kms put-key-policy --key-id "$NEW_KEY" \
--policy-name default --policy file://kms-signing-key-policy.json
```
4. After the 24-hour overlap window (§17), disable + schedule deletion
of the old key:
```sh
aws kms disable-key --key-id "<old-key-id>"
aws kms schedule-key-deletion --key-id "<old-key-id>" \
--pending-window-in-days 7
```
5. Verify the new key is active: `nova idp setup --verify` (the KMS
round-trip test, REQ-362).
**Audit:** emit a `kms.key_rotated` event with `old_key_id`,
`new_key_id`, `rotated_at`.
## 10. JWKS reachability via CloudFront edge (D-233, INV-18)
The JWKS endpoint is the **only public read surface** of the live
platform (INV-18, D-233). All other platform endpoints gate with
`AuthType: AWS_IAM`. CloudFront + OAC pinning replaces direct Lambda
Function URL exposure.
**Critical pitfall (RESEARCH §4):** the JWKS Function URL
`AuthType` MUST be `AWS_IAM`, NOT `NONE`. A common mistake is to set
`AuthType: NONE` on the Function URL (thinking CloudFront is the
gate) — that exposes the JWKS endpoint directly to the internet,
bypassing OAC. The correct configuration:
| Setting | Value |
|---------|-------|
| Function URL `AuthType` | `AWS_IAM` (NOT `NONE`) |
| CloudFront OAC `OriginAccessControlOriginType` | `lambda` |
| CloudFront OAC `SigningBehavior` | `always` |
| Lambda resource policy | `lambda:InvokeFunctionUrl` scoped to the CloudFront distribution ARN |
With `AuthType: AWS_IAM` + OAC `always` signing, CloudFront signs
every origin request with SigV4; a direct Function URL request has no
SigV4 signature → 403. Only CloudFront can reach the origin.
**Verification (M1.5 item 9):**
```sh
# Via CloudFront → 200
curl -sI https://<jwks-domain>/.well-known/jwks.json | head -1
# → HTTP/2 200
# Direct Function URL → 403
curl -sI "<raw-function-url>/.well-known/jwks.json" | head -1
# → HTTP/2 403
```
If the direct Function URL returns 200, the `AuthType` is `NONE`
fix the Terraform + re-apply before declaring M1.5 green.
## 11. PITR restore (data-engineer section)
DynamoDB point-in-time recovery (PITR) is enabled on every identity +
contract table. PITR lets you restore a table to any second in the
last **35 days** (the AWS retention window).
**Tables with PITR enabled:**
| Table | Purpose |
|-------|---------|
| `nova-contracts` | contract ingestor records |
| `nova-change-requests` | change request ledger |
| `nova-outbox` | audit outbox |
| `nova-users` | Nova-idp users (Argon2id hashes) |
| `nova-sessions` | Nova-idp sessions (TTL `expires_at`) |
| `nova-pats` | Nova-idp PATs (revocation strong-read, D-229) |
**Enable PITR (on a new/restored table — PITR does not carry over
from the source):**
```sh
aws dynamodb update-continuous-backups \
--table-name <table> \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
```
**Restore a table to a point in time** (PITR never overwrites the
source — restore to a NEW table, then repoint):
```sh
RESTORE_TO=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)
aws dynamodb restore-table-to-point-in-time \
--source-table-name <table> \
--target-table-name <table>-restored \
--restore-date-time "$RESTORE_TO" \
--billing-mode-restore-as-is
# After the restore completes (status ACTIVE), repoint the app:
# - update the stack env var to the restored table name, or
# - rename: delete <table>, then update-table --new-table-name <table>
# Then re-enable PITR on the restored table (see above).
```
**Verify PITR is enabled on all tables:**
```sh
for t in nova-contracts nova-change-requests nova-outbox \
nova-users nova-sessions nova-pats; do
aws dynamodb describe-continuous-backups --table-name "$t" \
--query 'ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus' \
--output text
done
# → ENABLED (x6)
```
Restores older than 35 days are impossible — for longer retention,
export to S3 via the on-demand export or a scheduled AWS Backup plan.
## 12. DynamoDB import addresses (REQ-361, covered-reference)
The `importable-resources.tf` map in `nova-platform-ops` lists the
existing live resources that `terraform import` adopts at M1 cutover
(§2 step 4). Re-import exits non-zero with
`resource_already_imported` (IMPORT-IDEMPOTENT); CI import treats this
as idempotent success.
| Terraform address | AWS resource | Type |
|-------------------|--------------|------|
| `aws_s3_bucket.nova_tfstate` | `nova-tfstate-581513795199-us-east-1` | S3 bucket (state backend) |
| `aws_dynamodb_table.nova_contracts` | `nova-contracts` | DynamoDB table |
| `aws_dynamodb_table.nova_change_requests` | `nova-change-requests` | DynamoDB table |
| `aws_dynamodb_table.nova_outbox` | `nova-outbox` | DynamoDB table |
| `aws_iam_role.acdl_act_runner` | `acdl-act-runner-role` | IAM role (reused, spec §7.6) |
| `aws_kms_key.<per_stack_cmk>` | per-stack CMKs | KMS key (one per stack) |
The identity tables (`nova-users`, `nova-sessions`, `nova-pats`) are
**new** resources added at M1 (§2 step 6), not imported — they do
not yet exist in the account at M1.
## 13. PAT revocation (D-229)
PAT revocation has a **60s SLO**: the token-vend Lambda does a
strongly-consistent DynamoDB read (`ConsistentRead=True`) on every
token-vend request. A revoked PAT is reflected on the next vend,
within 60s P95.
**Verify a PAT's revocation status (strong read):**
```sh
aws dynamodb get-item \
--table-name nova-pats \
--key '{"jti":{"S":"<pat-id>"}}' \
--consistent-read \
--query 'Item.status.S' --output text
# → active (still valid)
# → revoked (next token-vend returns 403)
```
**Revoke a PAT at the DDB level** (emergency — when the CLI is
unavailable; the `jti` is known but the raw PAT is not):
```sh
aws dynamodb update-item \
--table-name nova-pats \
--key '{"jti":{"S":"<pat-id>"}}' \
--update-expression "SET #s = :r" \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":r":{"S":"revoked"}}'
```
The item is **retained** (not deleted) so the audit trail is intact —
only `status` flips from `active` to `revoked`. The next `token-vend`
call with that `jti` returns `403 pat_revoked` immediately (D-229:
the strong read is synchronous).
## 14. Edge configuration (REQ-364/365/366, covered-reference)
The edge stack fronts the JWKS Lambda with CloudFront + WAF + ACM +
Route53. This is the public read surface (§10, INV-18).
### CloudFront + OAC (REQ-364)
- Distribution origin = the JWKS Lambda Function URL.
- OAC: `OriginAccessControlOriginType: lambda`,
`SigningBehavior: always` (§10).
- Cache behavior: `Cache-Control: max-age=3600` honored (JWKS-SLO).
### WAF WebACL (REQ-365)
- Scope: `CLOUDFRONT` (the WebACL is in `us-east-1`, the only region
for CloudFront-scoped WebACLs).
- Rate-based rule: `RateBasedStatement` with `Limit: 3000`,
`AggregateKeyType: IP`, `EvaluationWindowSec: 300` (3000 requests
per 5 minutes per IP).
- Managed rules: `AWSManagedRulesCommonRuleSet` (the AWS managed rule
group for common attacks).
### ACM certificate (REQ-366)
- Certificate in `us-east-1` (CloudFront requires the cert in
us-east-1).
- DNS validation (a CNAME record per validation record is written to
Route53). The cert status MUST be `ISSUED` (not
`PENDING_VALIDATION`) before the CloudFront distribution can serve
the domain.
### Route53 (REQ-366)
- An A-alias record pointing to the CloudFront distribution's domain
name.
### `route53_record_not_resolvable` debugging
If the JWKS domain does not resolve (`route53_record_not_resolvable`
or `NXDOMAIN`):
1. **Check ACM cert status:**
```sh
aws acm describe-certificate --certificate-arn <arn> \
--query 'Certificate.Status' --output text
# → must be ISSUED, not PENDING_VALIDATION
```
If `PENDING_VALIDATION`, the DNS validation CNAME records are not
in Route53 (or not propagated). Re-apply the validation records +
wait for AWS to validate (typically minutes).
2. **Check CloudFront status:**
```sh
aws cloudfront get-distribution --id <id> \
--query 'Distribution.Status' --output text
# → must be Deployed
```
If `InProgress`, wait for the deployment to finish. CloudFront
deployments take ~5-15 minutes.
3. **Check the Route53 alias record** points to the CloudFront
distribution domain name (not the Function URL).
## 15. Fargate standby health (KJ-WARMUP-HEALTH, REQ-363b)
The Fargate standby is the always-warm minimal defensive fallback
(REQ-363b). It runs the **same ECR image** as the Lambda (KJ-LOCKSTEP,
REQ-371) — so it is always running the current digest, never a stale
one.
**Health probe:** `GET /health → 200` every **10s**
(KJ-WARMUP-HEALTH).
**Failure handling:** 3 consecutive probe failures → alert + the
token-vend path **fails closed** (no signing). The standby does not
silently degrade — if it is not healthy, token-vend does not fall
back to it; it fails closed (INV-17 ABAC discipline extended to the
substrate).
**Verify the standby is `READY` before M1 cutover:**
```sh
# The Fargate task health check (target group)
aws elbv2 describe-target-health \
--target-group-arn <tg-arn> \
--query 'TargetHealthDescriptions[0].TargetHealth.State' --output text
# → healthy
# Direct probe
curl -sI https://<fargate-endpoint>/health | head -1
# → HTTP/1.1 200
```
**Fargate sunset discipline (D-237):** the standby may not be deleted
unless REQ-363 has been green in production for **≥30 consecutive
days**. Sunset requires an architecture review. Do not delete the
standby to save ~$15/month (§7) without that review — it is the
defensive fallback for the `kj` substrate.
## 16. IAM scope (IAM-NARROW, REQ-360, covered-reference)
The Gitea OIDC role for `act_runner` (reused `acdl-act-runner-role`,
spec §7.6) is bounded per REQ-360. **No `Action: "*"` or `Resource:
"*"`** (IAM-NARROW).
The scope covers only:
| Action | Scope | Why |
|--------|-------|-----|
| `kms:*` | customer-managed keys in `581513795199` | KMS signing + rotation |
| `dynamodb:*` | tables prefixed `nova-` | identity + contract tables |
| `lambda:*` | functions prefixed `nova-` | the 3 Nova-idp Lambdas |
| `s3:*` | buckets prefixed `nova-` | state bucket + artifact cache |
| `cloudfront:*` | tagged resources | the JWKS distribution |
| `wafv2:*` | tagged resources | the WebACL |
| `acm:*` | tagged resources | the JWKS cert |
| `route53:*` | tagged resources | the JWKS alias |
| `iam:PassRole` | roles tagged `nova-ops-only` | pass roles to Lambda/ECS only |
**Verify (M1.5 item 10):**
```sh
aws iam get-role-policy --role-name acdl-act-runner-role \
--policy-name <policy-name> --query 'PolicyDocument' --output json \
| jq '.Statement[].Action, .Statement[].Resource'
# → no "*" in either list
```
If `Action: "*"` or `Resource: "*"` appears, the IAM policy is too
broad — fix the Terraform + re-apply before declaring M1.5 green.
## 17. JWKS-ROTATION
On KMS key rotation (§9), **both old + new public keys** are
published in the JWKS during a **24-hour overlap window**. The old
key is removed from the JWKS only after consumers pick up the new
one.
- During the overlap: the JWKS Lambda lists all keys the alias has
pointed at that are still enabled. Already-issued OIDC tokens
(signed with the old key) keep verifying until they expire (OIDC
TTL default 15 min; PAT TTL ≤ 24h dev / ≤ 1h service-account).
- **Do not disable the old key until at least the max PAT TTL (24h)
has elapsed.**
- After the overlap, the old key is removed from the JWKS + disabled +
scheduled for deletion (§9 step 4).
This is JWKS-ROTATION (NFR) — the rotation is non-disruptive because
consumers cache the JWKS for up to `max-age=3600` (1h, JWKS-SLO) and
re-fetch within that window, picking up both keys during the overlap.
## 18. Cutover Gates (grill CF-2/G-5)
Each covered-reference REQ has a cutover gate (M1/M1.5/M2) with a
verification command + a **"Result" column**. The operator fills the
Result column after running the gate in `nova-platform-ops` CI.
**P6 audit verifies every covered-reference REQ has a non-empty,
green Result.** Empty or red → P6 blocks (grill CF-2/G-5).
| REQ | Gate | Verification command | Result |
|-----|------|----------------------|--------|
| REQ-355 | M1 | `terraform plan` resolves `data.aws_ecr_image.kj_image` from `local.nova_platform_version` + `local.kj_source_sha`; both image_uri attributes present | _(operator fills: green/red + run ID/SHA + attestor)_ |
| REQ-356 | M1 | `terraform plan` exit 0 (no drift) on a clean checkout of `main`; exit 2 → `drift_detected` fails the apply workflow | _(operator fills)_ |
| REQ-357 | M1.5 | `terraform apply` `workflow_dispatch` triggered by PR author → rejected; distinct user → proceeds (M1.5 item 11) | _(operator fills)_ |
| REQ-358 | M2 | bump `nova_platform_version``terraform apply``aws lambda get-function ... ImageUri` `CodeSha256` matches the artifact SHA-256 from the GitHub Release body | _(operator fills)_ |
| REQ-359 | M1 | `git -C nova-platform-ops remote -v` shows only the Gitea private remote (no GitHub mirror); Gitea repo `private: true` | _(operator fills)_ |
| REQ-360 | M1.5 | `aws iam get-role-policy` on the OIDC role asserts no `Action: "*"` + no `Resource: "*"` (M1.5 item 10, §16) | _(operator fills)_ |
| REQ-361 | M1 | `terraform import` on each address in `importable-resources.tf` (§12) succeeds; re-import exits `resource_already_imported` → CI treats as idempotent success (IMPORT-IDEMPOTENT) | _(operator fills)_ |
| REQ-362 | M1.5 | `nova idp setup --verify` (KMS round-trip) against `alias/nova-oidc-signing` (`ECC_NIST_P256`, `SIGN_VERIFY`) → `{"passed":true}` (M1.5 item 6) | _(operator fills)_ |
| REQ-363 | M1.5 | `nova_idp_token_vend.handler` invokes `subprocess.run(['/opt/kj/kj','apply',...])` on the live image; `file(1)` reports `statically linked` (M1.5 items 2-4, KJ-STATIC) | _(operator fills)_ |
| REQ-363b | M1.5 | Fargate standby `GET /health → 200` every 10s (KJ-WARMUP-HEALTH); same ECR digest as the Lambda (KJ-LOCKSTEP); activates only if M1.5 items 1-7 fail 3× (§5) | _(operator fills)_ |
| REQ-364 | M1.5 | direct JWKS Function URL → 403; via-CloudFront → 200 (M1.5 item 9, §10, INV-18) | _(operator fills)_ |
| REQ-365 | M1 | `aws wafv2 get-web-acl` shows `RateBasedStatement` Limit 3000, AggregateKeyType IP, EvaluationWindowSec 300 + `AWSManagedRulesCommonRuleSet`; Scope CLOUDFRONT in us-east-1 (§14) | _(operator fills)_ |
| REQ-366 | M1 | `aws acm describe-certificate` Status `ISSUED`; Route53 A-alias resolves to the CloudFront distribution domain (§14) | _(operator fills)_ |
| REQ-371 | M2 | after `terraform apply`, both `aws_lambda_function.nova_idp_token_vend.image_uri` and `aws_ecs_task_definition.kj.container_definitions[0].image` report the same ECR digest (KJ-LOCKSTEP precondition green at plan) | _(operator fills)_ |
> **P6 audit rule (grill CF-2/G-5):** every row's Result column must
> be non-empty + green. An empty or red Result blocks the milestone
> ship. The operator attestation is the acdl-side evidence surface;
> the live verification runs in `nova-platform-ops` CI.
---
## Appendix — quick reference
| Procedure | Cadence / trigger | Section |
|-----------|-------------------|---------|
| Day-0 cutover (M1) | one-time | §2 |
| M1.5 verification gate | one-time (3 consecutive green rebuilds) | §3 |
| M2 operational handoff | per engineering change (tag-pin bump) | §4 |
| M2a Fargate activation | conditional (M1.5 fails 3×) | §5 |
| Rollback | on regression | §6 |
| Artifact-mirror fallback | on GitHub Releases unreachable | §8 |
| KMS rotation | every 90 days | §9 |
| JWKS-ROTATION overlap | on each KMS rotation (24h window) | §17 |
| PITR restore | on data loss / corruption (35-day window) | §11 |
| Emergency PAT revocation | on compromise (DDB-level, immediate) | §13 |
| Fargate standby health check | continuous (every 10s) | §15 |
| Fargate sunset | ≥30 consecutive days green + architecture review | §5, §15 |
| `route53_record_not_resolvable` debug | on JWKS domain not resolving | §14 |
| Cutover gate attestation | at M1/M1.5/M2 (operator fills Result column) | §18 |
@@ -0,0 +1,33 @@
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
flowchart LR
subgraph TODAY["Today — the gap is widening"]
direction TB
V["Delivery velocity<br/>↑ rising fast"]
C["Coordination surface<br/>↑↑ rising faster"]
V -.-|"gap"| C
end
subgraph COST["The cost of the gap"]
direction TB
R1["Remediation<br/>cycles"]
R2["Gate fatigue<br/>& morale loss"]
R3["Compliance<br/>found late"]
end
TODAY -->|"absorbs"| NOVA
subgraph NOVA["Nova closes the gap"]
direction TB
N1["Owned primitives<br/>= no per-team infra authoring"]
N2["Attested up front<br/>= no late compliance"]
N3["Policy-bounded<br/>= no manual gating"]
end
COST -->|"absorbs"| NOVA
style TODAY fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style COST fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style NOVA fill:#1B1B1B,color:#fff,stroke:#D6002A
style V fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style C fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style R1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style R2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style R3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style N1 fill:#1B1B1B,color:#fff,stroke:#D6002A
style N2 fill:#1B1B1B,color:#fff,stroke:#D6002A
style N3 fill:#1B1B1B,color:#fff,stroke:#D6002A
@@ -0,0 +1,25 @@
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
flowchart LR
SDLC["SDLC<br/>code authored,<br/>built, tested"]
PDLC["PDLC<br/>product backlog,<br/>release planning"]
CONTRACT["The<br/>contract"]
INGEST["Contract<br/>ingestor"]
RESOLVE["Resolve<br/>→ L1/L2 stack"]
POLICY["Policy +<br/>confidence<br/>enforcement"]
APPLY["Reconcile<br/>→ live cloud"]
PROD["Running,<br/>observable,<br/>attested infra"]
SDLC --> CONTRACT
PDLC --> CONTRACT
CONTRACT -->|"validated<br/>boundary"| INGEST
INGEST --> RESOLVE
RESOLVE --> POLICY
POLICY --> APPLY
APPLY -->|"production-grade<br/>by construction"| PROD
style SDLC fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style PDLC fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style CONTRACT fill:#fff,color:#D6002A,stroke:#D6002A
style INGEST fill:#1B1B1B,color:#fff,stroke:#D6002A
style RESOLVE fill:#1B1B1B,color:#fff,stroke:#D6002A
style POLICY fill:#1B1B1B,color:#fff,stroke:#D6002A
style APPLY fill:#1B1B1B,color:#fff,stroke:#D6002A
style PROD fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
@@ -0,0 +1,22 @@
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
flowchart TB
ROOT["Two tenets<br/>discipline everything"]
SB["Sovereign boundary<br/>govern the delivery lifecycle<br/>do not reach upstream"]
LA["Lower autonomous<br/>higher attested<br/>compute: platform<br/>choice: human"]
ROOT --> SB
ROOT --> LA
SB -->|"shapes"| FM["4-layer model<br/>artifact → contract →<br/>infra → runtime"]
SB -->|"shapes"| CS["Contract schema<br/>the only integration<br/>surface to SDLC/PDLC"]
SB -->|"shapes"| PE["Policy envelope<br/>confidence, NFRs,<br/>boundaries enforced"]
LA -->|"shapes"| HITL["HITL gates<br/>named approver ≠ PR author<br/>at qa, prod, dr"]
LA -->|"shapes"| CONF["Confidence signal<br/>0.50 → 0.75 → 0.90 → 0.95<br/>escalation below threshold"]
LA -->|"shapes"| AUDIT["Audit lineage<br/>every action → one owner<br/>→ one evidence entry"]
style ROOT fill:#1B1B1B,color:#fff,stroke:#D6002A
style SB fill:#1B1B1B,color:#fff,stroke:#D6002A
style LA fill:#1B1B1B,color:#fff,stroke:#D6002A
style FM fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style CS fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style PE fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style HITL fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style CONF fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style AUDIT fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
@@ -0,0 +1,17 @@
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
flowchart LR
DEV["dev<br/>autonomous<br/>conf ≥ 0.50<br/>zero-touch"]
QA["qa<br/>HITL gate<br/>conf ≥ 0.75<br/>named approver<br/>≠ PR author"]
PROD["prod<br/>HITL gate<br/>conf ≥ 0.90<br/>named approver<br/>≠ PR author"]
DRENV["dr<br/>HITL gate<br/>conf ≥ 0.95<br/>named approver<br/>≠ PR author"]
DEV -->|"promotion"| QA
QA -->|"promotion"| PROD
PROD -->|"promotion"| DRENV
ESC["Escalation:<br/>below threshold<br/>→ human judgment"] -.->|"triggers"| QA
ESC -.->|"triggers"| PROD
ESC -.->|"triggers"| DRENV
style DEV fill:#1B1B1B,color:#fff,stroke:#D6002A
style QA fill:#1B1B1B,color:#fff,stroke:#D6002A
style PROD fill:#1B1B1B,color:#fff,stroke:#D6002A
style DRENV fill:#1B1B1B,color:#fff,stroke:#D6002A
style ESC fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
@@ -0,0 +1,29 @@
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
flowchart LR
subgraph ABOVE["Above the contract — upstream, not Nova"]
APP["Application<br/>business logic"]
IDE["IDE · sprint ·<br/>author workflows"]
APPSEC["AppSec · dependency<br/>review · runtime security"]
VM["VM · bare-metal ·<br/>OS lifecycles<br/>consumed, not maintained"]
end
CONTRACT["the contract<br/>━━━━━━━━━<br/>declared intent<br/>validated, published"]
subgraph BELOW["Below the contract — Nova's lane"]
L1["L1 primitives<br/>S3 · RDS · Lambda<br/>ECS · KMS · DynamoDB"]
L2["L2 compositions<br/>microservice ·<br/>static-assets"]
GUARD["Guardrails<br/>confidence · policy<br/>attestation · audit"]
end
RT["Production<br/>runtime"]
ABOVE -.->|"declares via"| CONTRACT
CONTRACT -->|"validated boundary"| BELOW
BELOW -->|"reconciles to"| RT
style ABOVE fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style BELOW fill:#1B1B1B,color:#fff,stroke:#D6002A
style APP fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style IDE fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style APPSEC fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style VM fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style CONTRACT fill:#fff,color:#D6002A,stroke:#D6002A
style L1 fill:#1B1B1B,color:#fff,stroke:#D6002A
style L2 fill:#1B1B1B,color:#fff,stroke:#D6002A
style GUARD fill:#1B1B1B,color:#fff,stroke:#D6002A
style RT fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
@@ -0,0 +1,32 @@
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
flowchart LR
subgraph NOW["Now"]
A["α — operating model<br/>named platform-ops body<br/>SLAs on every L2"]
end
subgraph Q1["Q1 2027"]
B["β — infra observability<br/>auto-published on apply<br/>no per-team dashboards"]
end
subgraph Q2["Q2 2027"]
G["γ — runbook generation<br/>from observed patterns<br/>SREs start, not blank"]
end
subgraph Q4["Q3-Q4 2027"]
D["δ — audit ledger<br/>S3 Object Lock + JWS<br/>externally verifiable"]
end
A -->|"unlocks"| B
B -->|"builds on"| G
G -->|"matures to"| D
NOW -.->|"governance<br/>is the unlock"| B
subgraph UNCHANGED["Stays constant across the arc"]
C["The contract surface<br/>remains the only<br/>SDLC/PDLC integration"]
end
D -.->|"exits to"| UNCHANGED
style NOW fill:#1B1B1B,color:#fff,stroke:#D6002A
style Q1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style Q2 fill:#1B1B1B,color:#fff,stroke:#D6002A
style Q4 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style UNCHANGED fill:#fff,color:#1B1B1B,stroke:#D6002A
style A fill:#1B1B1B,color:#fff,stroke:#D6002A
style B fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style G fill:#1B1B1B,color:#fff,stroke:#D6002A
style D fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style C fill:#fff,color:#1B1B1B,stroke:#D6002A
@@ -0,0 +1,40 @@
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
flowchart TB
subgraph PROVEN["Proven today — the foundation"]
direction LR
L1L2["13 L1 + 2 L2<br/>live in registry"]
PILOT["Pilot live-apply<br/>confidence 0.800<br/>dev AWS account"]
L1L2 --- PILOT
end
subgraph ARC["The runway arc — next steps"]
direction LR
N1["Ingest greenfield<br/>pilot projects"]
N2["Promote<br/>dev → prod<br/>activate qa, prod, dr"]
N3["Integrate SPGE<br/>constitutional library<br/>at the contract surface"]
N4["Serve as the<br/>infrastructure layer<br/>beneath every app"]
N1 --> N2 --> N3 --> N4
end
subgraph ASK["The ask"]
ENDORSE["Architecture<br/>endorsement"]
RUNWAY["Runway to next<br/>milestone by Nov 2026"]
ENDORSE --- RUNWAY
end
subgraph RISK["If we don't — the structural risk"]
GAP["Delivery acceleration<br/>vs operational absorption<br/>gap widens through 2027"]
end
PROVEN -->|"foundation enables"| ARC
ARC -->|"lands at"| ASK
ASK -.->|"without it"| RISK
style PROVEN fill:#1B1B1B,color:#fff,stroke:#D6002A
style ARC fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style ASK fill:#1B1B1B,color:#fff,stroke:#D6002A
style RISK fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style L1L2 fill:#1B1B1B,color:#fff,stroke:#D6002A
style PILOT fill:#1B1B1B,color:#fff,stroke:#D6002A
style N1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style N2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style N3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style N4 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
style ENDORSE fill:#1B1B1B,color:#fff,stroke:#D6002A
style RUNWAY fill:#1B1B1B,color:#fff,stroke:#D6002A
style GAP fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

@@ -0,0 +1,236 @@
<!--
REQ-372 — Nova Leadership Presentation Deck (v1.30, single-shot artifact).
This deck is the leadership artifact for Infrastructure & Operations
(CTO + VP Technology + Product Management), presented live in
August 2026, securing architecture endorsement and a November 2026
runway to demonstrate Nova's next milestone.
Related-but-distinct artifact: nova-autonomous-cloud-delivery-marp.md
(the citizen-developer pitch). This deck does NOT compress or modify
that artifact — the two decks remain discrete (D-241). The existing
citizen-developer deck is untouched.
Vision grounding: [1] citations in slides 3, 5, 7 speaker notes
resolve to docs/vision.md (the spec's acdl-vision.md reference).
Diagrams: 7 S&P-themed mermaid diagrams (D-246), one per slide,
rendered via scripts/render_leadership_diagrams.sh (mmdc + vendored
Chromium libs). Sources: assets/mmd/leadership-slide-N.mmd.
PNGs: assets/png/leadership-slide-N.png.
Render: python3 scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md \
--output docs/presentations/nova-leadership-deck.pptx
Smoke test: bash scripts/check_leadership_deck.sh
-->
---
marp: true
theme: default
footer: "Nova Platform - Infrastructure & Operations"
paginate: false
size: 16x9
style: |
section { font-family: "Akkurat Pro", "Helvetica Neue", "Arial", sans-serif; font-size: 22px; color: #1B1B1B; padding: 48px 56px 40px; overflow: auto; }
h1 { color: #D6002A; font-size: 34px; margin-bottom: 0.3em; }
h2 { color: #D6002A; font-size: 26px; margin-bottom: 0.2em; }
h3 { color: #D6002A; font-size: 22px; margin-bottom: 0.2em; }
table { font-size: 18px; width: 100%; border-collapse: collapse; }
th { background: #F0F0F0; border-bottom: 2px solid #D6002A; padding: 4px 8px; text-align: left; }
td { border-bottom: 1px solid #F0F0F0; padding: 4px 8px; }
blockquote { border-left: 4px solid #D6002A; color: #1B1B1B; font-size: 20px; padding-left: 12px; }
pre { background: #1B1B1B; color: #FFFFFF; border-radius: 4px; padding: 12px; font-size: 16px; }
code { background: #F0F0F0; color: #1B1B1B; border-radius: 2px; padding: 1px 4px; font-size: 18px; }
pre code { background: transparent; color: #FFFFFF; }
img { display: block; margin: 0 auto; max-width: 100%; max-height: 380px; object-fit: contain; }
strong { color: #D6002A; }
.benefit { margin-top: 0.6em; padding-top: 0.4em; border-top: 1px solid #D6002A; color: #1B1B1B; font-size: 20px; font-style: italic; }
@media print { section { overflow: hidden; } }
---
<!-- _class: title -->
<!-- _paginate: false -->
# The friction every delivery team lives today
> *Velocity is up; the coordination surface around each change is up faster.*
- → Infrastructure is authored by people who don't specialize in infrastructure — the platform team is not standing behind every bucket, every instance, every key.
- → Every change is gated because one misconfiguration can expose the entire estate — so every change is reviewed, every change is manual, and the cost of getting it wrong is account-wide.
- → Compliance, security, and NFRs are checked late — after the PR, after the merge, sometimes after the deploy — fueling remediation cycles that erode delivery cadence and team morale.
> *Nova absorbs all three — owned building blocks, separation of concerns, attested compliance up front.*
![diagram](assets/png/leadership-slide-1.png)
<!--
Three patterns drive the friction every delivery team lives today, and all three trace back to one binding constraint: software delivery scales with the coordination surface around it, not the engineering inside it [1]. That is the claim the vision document opens with, and it is the lens for everything Nova does.
The first pattern: infrastructure is authored by people who do not specialize in infrastructure. The platform team is not standing behind every S3 bucket, every RDS instance, every KMS key. The application team is. They are smart, they are capable, but infrastructure is not their craft, and the long tail of well-meaning services shows it.
The second pattern: every change is gated because one misconfiguration can expose the entire estate. A single bucket without SSE-KMS, a single RDS without deletion protection, a single Lambda with an over-privileged role — and the blast radius is the whole account. So every change is reviewed, every change is gated, and the gate is manual because the cost of getting it wrong is account-wide.
The third pattern: compliance, security, and NFRs are checked late. They are checked after the PR, after the merge, sometimes after the deploy. By then remediation is a cycle — it erodes delivery cadence and it erodes morale.
Nova absorbs all three. Owned building blocks, separation of concerns, attested compliance up front. And one distinction that matters for this room: Nova's lane is infrastructure patching. AppSec — dependency review, runtime application security, the application-layer threat model — stays with the application team. Nova is not a remediation tool. Nova is not a security blanket. Nova is the infrastructure beneath the application, owned by the platform, attested before the consumer ever touches it.
-->
---
## Nova in one frame
> *You already recognize this pattern.*
Every Central IT team curates a golden image for Windows, for Linux, for macOS. They own it, they patch it, they ship it — and consumers consume it without thinking about what is inside. That trade — per-application control for uniform operability — is one every enterprise has already made at the OS layer.
Nova plays the same role one layer up: for everything that runs your cloud. S3 buckets with SSE-KMS posture. RDS instances with deletion protection and PITR. Lambda containers with static ABAC binaries. ALBs, ECS services, KMS keys, DynamoDB tables. Each primitive is owned by the platform team, patched by the platform team, attested by the platform team, and consumed by anyone who declares a contract.
The difference is rigor: every primitive is versioned, tested across its entire lifecycle, and bounded by policy before any consumer ever touches it.
> *Nova's lane is the infrastructure beneath the application. AppSec stays with the application team.*
![diagram](assets/png/leadership-slide-2.png)
<!--
The Central IT golden-image pattern is one every leadership team already recognizes. Central IT curates the Windows image, the Linux image, the macOS image. They own it, they patch it, they ship it, and consumers consume it without thinking about what is inside. That trade — per-application control for uniform operability — is a trade every enterprise has already made at the OS layer.
Nova plays the same role one layer up. Not the OS image, but everything that runs your cloud: S3 buckets with SSE-KMS posture, RDS instances with deletion protection and PITR, Lambda containers with static ABAC binaries, ALBs, ECS services, KMS keys, DynamoDB tables. Each primitive is owned by the platform team, patched by the platform team, attested by the platform team, and consumed by anyone who declares a contract. The platform begins where the artifact is compiled and ends where it runs in production under operational guardrails [1]. That boundary is the sovereignty claim — Nova governs the delivery lifecycle, not the upstream product or software development lifecycle.
The difference from Central IT is rigor: every primitive is versioned, tested across its entire lifecycle, and bounded by policy before any consumer ever touches it. The sovereignty-via-boundary argument is not defensive. It is the same argument Central IT already won at the OS layer: the platform owns the primitive so the consumer does not have to.
And the lane stays narrow. Nova's lane is the infrastructure beneath the application. AppSec, dependency review, and runtime application security stay where they have always been — with the application team.
-->
---
## Two principles that organize everything else
> *Two tenets discipline every other decision.*
**Sovereign boundary** — Nova governs the delivery lifecycle; it does not reach upstream into product or software development [1]. Integration with SDLC and PDLC partners — the IDE, the sprint tool, the author workflow, the agent harness — happens exclusively through the validated, published contract surface. What lives outside the contract is not Nova's domain.
**Lower autonomous · higher attested** — lower environments proceed through agentic, zero-touch automation; promotion to qa, prod, and dr requires deliberate human attestation [1]. Not a rubber stamp — a policy-mandated act of accountability by a named human distinct from the PR author. The compute the platform makes; the choice the human keeps.
> *Everything else in the architecture inherits from these two.*
![diagram](assets/png/leadership-slide-3.png)
<!--
The architecture is principled, not improvised. Two tenets discipline every other decision the platform makes, and both come straight from the vision document [1].
The first tenet is the sovereign boundary. Nova governs the delivery lifecycle. It does not reach upstream into product or software development. Integration with SDLC and PDLC partners — the IDE, the sprint tool, the author workflow, the agent harness — happens exclusively through the validated, published contract surface. What lives outside the contract is not Nova's domain. This is not a defensive posture. It is an operating principle: the platform owns its lane, the upstream owns its lane, and the contract is where they meet. The four-layer model, the contract schema, the policy envelope — all of it inherits from this tenet.
The second tenet is lower autonomous, higher attested. Lower environments proceed through agentic automation — zero-touch, the platform reconciles. Promotion to higher environments — qa, prod, dr — requires deliberate human attestation. Not a rubber stamp. Not a courtesy notification. A policy-mandated act of accountability by a named human distinct from the PR author. The compute the platform makes; the choice the human keeps. The HITL gates, the confidence threshold, the escalation paths — all of it inherits from this tenet.
And the point for this room: these two tenets are not aspirational. They are load-bearing. Every other architectural decision — the four-layer model, the contract schema, the policy envelope, the audit lineage, the confidence signal — inherits from these two. If you endorse the architecture, you are endorsing these two tenets and everything that flows from them. The next slide is what the line looks like in 18 months of milestones.
-->
---
## Live · Attested · Stays human
**Live today** — 41 capabilities across 12 domains; pilot confidence 0.800. The contract ingestor, the audit outbox, the state buckets, and the live pilot run have operated in our AWS estate since v1.7. DORA, adoption, and policy-conformance metrics flow to PowerBI from the same audit stream as the lineage — one pane, no second source of truth. Every finding carries one owner, one patch state, one audit entry.
**Attested on promotion** — qa, prod, and dr require a named human approver distinct from the PR author. Rubber stamps cannot be silently issued; the attestation is a policy-mandated act of accountability.
**Stays human — by design** — confidence below the autonomy threshold at qa, prod, or dr triggers human escalation [1]. Some categories of decision are preserved for human judgment, and the platform says so out loud.
![diagram](assets/png/leadership-slide-4.png)
<!--
Three columns, three claims, one disambiguation. The claims are real, observable, and disciplined — and the distinction matters for this room.
Real: 41 capabilities across 12 domains are live today. The contract ingestor, the audit outbox, the state buckets, and the live pilot run have been operating in our AWS estate since v1.7. Pilot evidence at v1.26 returned confidence 0.800 — that is a measured, recorded number, not a forecast. DORA, adoption, and policy-conformance metrics flow to PowerBI from the same audit stream as the lineage. One pane, no second source of truth. Every finding carries one owner, one patch state, one audit entry. A POC is production-grade by construction — there is no "POC that became prod" surprise, because the platform enforces production-grade posture from the first apply [1].
Observable: the audit lineage is the single pane. DORA, adoption, policy-conformance — all from the same stream. That is not three dashboards stitched together. It is one stream, one schema, one owner per finding. The POC-to-prod discipline [1] is the same claim from the other direction: the platform does not have a "POC mode" that gets quietly upgraded to "prod mode." Production-grade is the default.
Disciplined: attested on promotion, stays human by design. qa, prod, and dr require a named human approver distinct from the PR author. Rubber stamps cannot be silently issued. Confidence below the autonomy threshold at qa, prod, or dr triggers human escalation [1]. Some categories of decision are preserved for human judgment, and the platform says so out loud. That is the HITL discipline closing [1]: the platform makes the compute, the human keeps the choice, and the boundary is policy-mandated, not discretionary.
-->
---
## The boundary keeps us honest
> *Nova stays where it belongs.*
**In Nova's lane**
- → Infrastructure primitives: S3, RDS, Lambda, ECS, DynamoDB, KMS, CloudFront.
- → Operational guardrails: confidence, policy, attestation, audit lineage.
- → CVE response at the infrastructure layer.
**Outside Nova's lane**
- → Application business logic.
- → IDE, sprint, and author workflows [1].
- → Application-layer security: AppSec, dependency review, runtime threat modeling.
- → VM, bare-metal, and OS lifecycles [1].
> *The line is the contract. Everything below it is Nova. Everything above it stays where it has always been.*
![diagram](assets/png/leadership-slide-5.png)
<!--
The boundary is not a defensive posture. It is an operating principle — and it is the principle that keeps the architecture honest [1].
In Nova's lane: infrastructure primitives. S3, RDS, Lambda, ECS, DynamoDB, KMS, CloudFront. Operational guardrails — confidence, policy, attestation, audit lineage. CVE response at the infrastructure layer. These are the things the platform owns, the things the platform patches, the things the platform attests. The consumer declares intent; the platform delivers safe production deployment.
Outside Nova's lane: application business logic. The IDE, the sprint, the author workflow [1] — those are upstream of the contract, and the platform does not reach into them. Application-layer security — AppSec, dependency review, runtime threat modeling — stays with the application team. That is not a gap. It is an autonomy-preserving design choice: the application team owns their lane, the platform owns its lane, and the contract is where they meet. VM, bare-metal, OS lifecycles [1] — the vision document is explicit: infrastructure is consumed, not maintained. Compute is abstract, containerized, or serverless. The platform does not manage node-level or OS-level lifecycles.
The line is the contract. Everything below the contract is Nova. Everything above it stays where it has always been. That is the boundary discipline, and it is the discipline that lets the platform scale without becoming the application team's bottleneck — and lets the application team scale without becoming the platform's risk. The boundary is what makes the ask small and the return large: the platform owns its surface, the consumer owns theirs, and neither side silently absorbs the other's burden.
-->
---
## The 18-month shape
> *Where CDLC meets SDLC + PDLC — through the contract surface, not above it.*
**α (now → Q4'26) — Operating model + federated governance.** A named platform-ops body owns the platform; SLAs on every L2 are ratifiable by platform and consumer. The operating model is published; integration surfaces for SDLC and PDLC harnesses are documented at the contract boundary.
**β (Q1'27) — Auto-published infra observability.** Every consumer stack ships with CloudWatch dashboards, uptime-kuma monitors, and alert routing on apply — infrastructure observability as a property, no per-team authoring required.
**γ (Q2'27) — Runbook generation from telemetry.** Every L1 primitive ships with an auto-generated incident runbook derived from observed patterns. SREs get a starting runbook, not a blank page.
**δ (Q3'27 → Q4'27) — Audit ledger, tamper-resistant.** The SQLite hash-evidence stream migrates to S3 Object Lock + JWS signatures. External counsel verifies any production change back to a named human attestation.
> *Nova absorbs no IDE, no editor, no sprint tool, no agent harness.*
![diagram](assets/png/leadership-slide-6.png)
<!--
The 18-month shape is a boundary-respecting integration arc, not an expansion arc. Four milestones, each disciplined by the sovereign-boundary tenet [1]: Nova meets SDLC and PDLC through the contract surface, not above it.
Alpha, now through Q4 2026, is the operating model and federated governance. A named platform-ops body owns the platform. SLAs on every L2 are ratifiable by platform and consumer. The operating model is published. Integration surfaces for SDLC and PDLC harnesses are documented at the contract boundary [1] — that is the unlock. The platform publishes the contract; the upstream tool integrates against it.
Beta, Q1 2027, is auto-published infra observability. Every consumer stack ships with CloudWatch dashboards, uptime-kuma monitors, and alert routing on apply. The infra-vs-app observability discipline [1]: the platform publishes infrastructure observability. Application observability stays with the app team.
Gamma, Q2 2027, is runbook generation from telemetry. Every L1 primitive ships with an auto-generated incident runbook derived from observed patterns. SREs get a starting runbook, not a blank page. The infra-vs-app runbook discipline [1]: the platform generates the infrastructure runbook. The application runbook stays with the app team.
Delta, Q3 through Q4 2027, is the audit ledger, tamper-resistant and externally addressable. The SQLite hash-evidence stream migrates to S3 Object Lock plus JWS signatures. External counsel verifies any production change back to a named human attestation — audit lineage outward [1], not upstream. The ledger is the proof, the attestation is the name, and the boundary holds.
Nova absorbs no IDE, no editor, no sprint tool, no agent harness. The contract surface is where CDLC meets SDLC and PDLC.
-->
---
## What we ask · What comes back
**What works now.** Deploying L1 & L2 stacks works today in the sandbox AWS account — 13 L1 primitives and 2 L2 modules, live-applied at confidence 0.800. Next steps: ingest greenfield pilot projects, promote from sandbox to production, integrate with the SPGE constitutional library, and serve as the infrastructure layer.
**What we ask.** Architecture endorsement. Runway to the next milestone.
**Why now.** Agentic SDLC is reshaping the delivery curve. What is barely keepable today — incident response, compliance reconciliation, security remediation — does not compress at the same rate as the velocity it has to keep pace with. By the end of 2027, the gap between delivery acceleration and operational absorption is the structural risk.
**What comes back.** The infrastructure foundation that absorbs the velocity. Metrics that tell us where to push next. Audit lineage that closes the regulatory question. The next milestone, **by November 2026**.
> *What we do not ask for: an IDE, a sprint tool, an author workflow, an upstream pipeline. Nova stays in its lane [1].*
![diagram](assets/png/leadership-slide-7.png)
<!--
This is presented to Infrastructure and Operations leadership in August 2026. What works now: deploying L1 and L2 stacks works today. 13 L1 primitives and 2 L2 modules are live in the registry; the pilot ran a live terraform apply against the dev AWS account at v1.26 and returned confidence 0.800 — a measured number, not a forecast. We refer to the dev account colloquially as the sandbox. Next steps: ingest greenfield pilot projects (greenfield consumers with no legacy to reconcile), promote from sandbox to production (the promotion path with HITL gates exists; the pilot was dev-only, so activating qa, prod, dr is the next step), integrate with the SPGE constitutional library (an external governance system the audience recognizes; the platform meets it at the contract surface), and serve as the infrastructure layer.
The ask is architecture endorsement and runway to the next milestone by November 2026. Why now: agentic SDLC is reshaping the delivery curve. What is barely keepable today — incident response, compliance reconciliation, security remediation — does not compress at the same rate as the velocity. By end of 2027, the gap between delivery acceleration and operational absorption is the structural risk. Internal directional target: a 60% reduction in that gap — not a sourced claim, a directional target the platform's metrics will track.
What comes back: the infrastructure foundation that absorbs the velocity, metrics that tell us where to push next, audit lineage that closes the regulatory question, and the next milestone by November 2026. Not a budget, not a headcount — runway to land the architecture endorsement. What we do not ask for: an IDE, a sprint tool, an author workflow, an upstream pipeline. Nova stays in its lane [1]. The sovereign boundary makes the ask small and the return large.
-->
Binary file not shown.
+408
View File
@@ -0,0 +1,408 @@
# Nova Identity Layer — Threat Model
> **REQ-347** — identity-layer threat model. Covers the 8 threats
> enumerated below + the **C-9.2 INV-18..21 compression audit**. The
> C-6.2 grill additions (JWKS DDoS, PAT max TTL, ABAC fail-closed) are
> integrated into the threat list, not appended.
>
> Scope: the Nova-idp identity layer (`nova-idp-auth` +
> `nova-idp-token-vend` + `nova-idp-jwks` Lambdas, the KMS signing key,
> the 4 DynamoDB tables, the `nova auth` CLI, the PAT lifecycle). Out
> of scope: the downstream contract resolver, Terraform adapter, and
> consumer-side auth (those have their own threat models).
## 1. Assets
| Asset | Where | Sensitivity |
|-------|-------|-------------|
| User passwords | `nova-users.password_hash` (Argon2id) | high — hash only; raw never stored |
| PATs (personal access tokens) | `nova-pats` (hash only) + returned to caller once | high — bearer token, ≤24h/≤1h TTL |
| OIDC tokens | `~/.nova/credentials.json` (0600) + in-flight to clients | medium — short-lived (15 min default) |
| KMS signing key | KMS `alias/nova-oidc-signing` (`ECC_NIST_P256`) | high — the trust anchor for all OIDC tokens |
| ABAC policy | `platform/abac/token-vend.policy` (git-tracked) | high — the authorization rules |
| DynamoDB tables | `nova-users`, `nova-sessions`, `nova-password-resets`, `nova-pats` | high — the identity store |
| JWKS endpoint | `nova-idp-jwks` function URL (`AuthType: NONE`) | medium — public, must be available but is not secret |
| Audit stream | stderr JSON from each Lambda + the CLI | high — tamper-evidence for the whole layer |
## 2. Trust boundaries
```
┌────────────────┐ IAM-auth function URL ┌────────────────────┐
│ Developer CI │ ───────────────────────────► │ nova-idp-auth │
│ (nova CLI) │ │ nova-idp-token-vend│
│ │ ◄────── OIDC token ───────── │ (KMS sign) │
└────────┬───────┘ └─────────┬──────────┘
│ │
│ ~/.nova/credentials.json (0600) │ strong-read GetItem
│ NOT the raw PAT ▼
│ ┌────────────────────┐
│ │ nova-pats (DDB) │
│ │ nova-users/sessions│
│ JWKS fetch (unauthenticated) └────────────────────┘
│ ──────────────────────────────────► ┌────────────────────┐
│ │ nova-idp-jwks │
│ ◄──── public key (JWK) ──────────── │ (AuthType: NONE) │
▼ └────────────────────┘
┌────────────────┐
│ AWS KMS │ kms:Sign (token-vend role only)
│ alias/nova- │ kms:GetPublicKey (jwks role)
│ oidc-signing │
└────────────────┘
```
The key boundary crossings:
1. **Internet → JWKS Lambda** (unauthenticated function URL) — the
DDoS surface (Threat T-4).
2. **CLI → auth/token-vend Lambdas** (IAM-authenticated function URLs)
— the credential-injection surface.
3. **token-vend Lambda → KMS** (`kms:Sign`) — the key-use surface.
4. **token-vend Lambda → DDB** (strong read on `nova-pats`) — the
revocation surface.
## 3. Threats + mitigations
### T-1 — Password compromise (storage)
**Threat:** an attacker with read access to `nova-users` (DDB export,
backup, a leaked snapshot) recovers plaintext passwords.
**Mitigations:**
- **Argon2id hashing** with OWASP-minimum parameters
(`time_cost=3, memory_cost=65536 KiB, parallelism=1`) —
`core/lambda/nova_idp_auth.py:hash_password`. Argon2id is the
recommended PHC winner; the parameters are the OWASP minimum (C-7.2).
- **Fail-closed on Argon2 unavailable** (D-228): if the `argon2-cffi`
C extension fails to import, `_ARGON2_AVAILABLE` is `False` and
`hash_password`/`verify_password` raise `Argon2UnavailableError`
the handler returns **503**. **No pure-Python fallback, no weak
hash, no crash.** Verified by `tests/test_argon2_fail_closed.py`.
- **No raw passwords anywhere** (INV-16): the handler never logs the
password argument; the audit scrubber (`_emit_audit`) pops any
`password`/`new_password`/`old_password` kwarg defense-in-depth;
the DDB item has `password_hash`, never `password`. Verified by
`tests/test_idp_auth.py:TestNoRawPasswordsInLogs`.
**Residual risk:** low. Argon2id with the OWASP params is
GPU-resistant at scale; the remaining risk is a parameter-weakness
advisory (mitigated by the 90-day KMS rotation cadence's analog for
hash params — revisit annually).
### T-2 — PAT theft + max TTL (C-6.2)
**Threat:** an attacker exfiltrates a PAT (filesystem read of
`~/.nova/credentials.json`, a leaked CI env var, a phishing capture)
and uses it to vend OIDC tokens until it expires.
**Mitigations:**
- **`~/.nova/credentials.json` stores the OIDC token + PAT metadata
(`jti`, `exp`, `type`) ONLY — NOT the raw PAT** (C-7.3). The raw PAT
is entered once at `nova auth login` and never persisted. An attacker
who reads the credentials file gets a short-lived OIDC token (15 min
default), not the long-lived PAT. Verified by
`tests/test_auth_commands.py:test_login_stores_oidc_token_not_raw_pat`.
- **Max TTL (C-6.2):** developer PATs ≤ 24h (86400s), service-account
PATs ≤ 1h (3600s). Enforced in `core.pat_lifecycle.issue_pat`
requests above the max are clamped (with an audit event). The shorter
service-account TTL bounds the CI blast radius.
- **Revocation via strong-read DDB (D-229):** the token-vend Lambda
does `GetItem(PK=jti, ConsistentRead=True)` on `nova-pats` on every
vend. A revocation (`status=revoked`) is reflected on the next vend
within **60s P95** (the strong read is synchronous — the 60s is the
P95 propagation bound, not a polling delay). Verified by
`tests/test_pat_revocation.py:test_pat_revocation_slo` (asserts
`<1s` locally).
- **Emergency revocation at the DDB level** (when the CLI is
unavailable): `aws dynamodb update-item --table-name nova-pats ...`
flips `status` to `revoked` — see `docs/operator-guide-idp.md` §9.
**Residual risk:** medium. The PAT is a bearer token — theft is
undetectable until the attacker vends a token. Mitigation is TTL
bounding + revocation, not prevention. The 1h service-account cap is
the primary control for CI exposure.
### T-3 — JWKS unauthenticated endpoint DDoS (C-6.2)
**Threat:** the JWKS endpoint (`nova-idp-jwks` function URL,
`AuthType: NONE`) is a public, unauthenticated target. An attacker can
flood it with requests, exhausting Lambda concurrency and making token
verification fail for all clients (a cheap DoS).
**Mitigations:**
- **Reserved concurrency (10, max ~100 RPS):** the JWKS Lambda has a
reserved-concurrency limit of 10 (set in the CloudFormation
template). This caps the blast radius — a flood saturates the JWKS
Lambda but does NOT exhaust the account-wide concurrency pool, so
`nova-idp-auth` and `nova-idp-token-vend` keep serving.
- **Client-side caching (1h):** the JWKS response carries
`Cache-Control: max-age=3600`. Clients (`pyjwt.PyJWK` client) cache
the keys for 1h, so a JWKS outage does not immediately break
verification — already-cached keys keep working.
- **Optional CloudFront + WAF (rate-based rule):** `nova idp setup
--apply --public-jwks-domain <fqdn>` fronts the function URL with a
CloudFront distribution + a WAF web ACL with a rate-based rule
(e.g. block an IP after 2000 req/5min). **For any public deployment,
set `--public-jwks-domain`.** Without it the function URL is bare —
fine for piloting, exposed for production.
**Residual risk:** medium. The reserved concurrency bounds the cost
but a determined attacker can still keep the JWKS Lambda saturated.
The WAF + CloudFront path is the production-grade control. JWKS is
inherently public (clients MUST fetch it without auth) — this is a
fundamental OIDC property, not a Nova design flaw.
### T-4 — ABAC bypass (C-6.1 / C-7.1)
**Threat:** the ABAC policy engine (`kyverno-json` / `kj`) fails to
load, crashes, or is misconfigured, and the token-vend Lambda vends a
token anyway (fails open). This would bypass the authorization gate —
every active PAT gets a token regardless of the policy.
**Mitigations:**
- **Fail-closed (C-6.1/C-7.1 — the grill's #1 finding):** the
token-vend Lambda's `_evaluate_abac_fail_closed` returns
`(False, [], "", "abac_eval_failed")` if:
- `KyvernoJsonEngine.is_configured()` returns `False` (`kj` absent),
- `get_engine()` raises (engine registry error),
- `evaluate_token_vend_policy()` raises (policy parse error, `kj`
runtime error).
In all three cases the Lambda returns **403** + an audit event
`token.vend.denied` (reason `abac_eval_failed`). **Never fails open.**
This is INV-17's runtime guarantee — without it, INV-17 is
documentation, not a control.
- **Verified by `tests/test_abac_fail_closed.py` (7 tests):**
engine-not-configured, evaluate-raises, policy-parse-error, ABAC
denies, revoked PAT, unknown PAT, audit-event-emitted-on-denial.
- **Policy version in every audit event (D-231):** the git blob SHA of
`platform/abac/token-vend.policy` is recorded in every
`token.vend.allowed`/`token.vend.denied` event. An auditor can
reconstruct which policy version governed each vend.
**Residual risk:** low (given the fail-closed semantics). The
remaining risk is a policy-authoring bug (the policy allows too much)
— mitigated by PR review (D-231: Platform Security owns the policy)
and the policy-version audit trail.
### T-5 — KMS signing key compromise
**Threat:** an attacker gains `kms:Sign` permission on
`alias/nova-oidc-signing` and forges OIDC tokens.
**Mitigations:**
- **KMS key policy restricts `kms:Sign` to the token-vend Lambda
role.** No other principal (including the operator) can sign. The
JWKS Lambda role has `kms:GetPublicKey` only (not `Sign`).
- **Key rotation (90 days):** the alias is re-pointed to a new
`ECC_NIST_P256` key every 90 days (see
`docs/operator-guide-idp.md` §6). The old key stays enabled during
the overlap window (≥ max PAT TTL = 24h) so already-issued tokens
keep verifying, then is disabled + scheduled for deletion.
- **JWKS serves both `kid`s during the overlap window:** the JWKS
endpoint lists all keys the alias has pointed at that are still
enabled. Clients verify against the `kid` in the token header.
**Residual risk:** low. KMS key policies are the primary control;
rotation bounds the exposure window of a stolen key.
### T-6 — DER → raw ECDSA signature conversion bug (C-5.2 gotcha)
**Threat:** KMS `sign()` returns a **DER-encoded** ASN.1 ECDSA
signature. JWS (RFC 7515 §3.1.3) requires the **raw** `r‖s`
concatenation, each coordinate 32 bytes big-endian (for P-256). If the
conversion is wrong (wrong byte order, wrong padding, wrong coordinate
length), the resulting JWT will not verify with standard libraries
(`pyjwt`, `jose`) — or worse, verifies with a *different* signature
than intended (a subtle correctness + security bug).
This is the **#1 implementation risk** identified in RESEARCH §5. The
conversion is in `core/kms_signing.py:der_to_raw_ecdsa`:
```python
r, s = decode_dss_signature(der_sig) # cryptography's ASN.1 parser
return r.to_bytes(32, "big") + s.to_bytes(32, "big") # raw r‖s
```
**Mitigations:**
- **`decode_dss_signature` from `cryptography`** parses the DER (not a
hand-rolled ASN.1 parser — that would be the real risk).
- **`to_bytes(32, "big")` zero-pads** each coordinate to exactly 32
bytes. A coordinate shorter than 32 bytes (high-order zero bytes)
is padded; a coordinate longer than 32 bytes raises `ValueError`
(the guard at the top of `der_to_raw_ecdsa`).
- **Verified by `tests/test_kms_roundtrip.py` (CAP-037):** sign a JWT
via `kms_signing.sign_jwt()` (mock KMS with a test ECC keypair) →
fetch JWKS via the JWKS Lambda → verify with `pyjwt` + the JWKS key.
The round-trip succeeds only if the DER→raw conversion is
byte-correct. This is the regression gate for any change to
`kms_signing.py`.
**Residual risk:** low (given the round-trip test). A KMS-side format
change (AWS changes the DER encoding) would break the test loudly.
### T-7 — No AWS-managed identity (INV-15)
**Threat:** (architectural invariant, not an attack.) Nova-idp depends
on Cognito, IAM Identity Center, or another AWS-managed identity
service, creating a vendor lock-in and an opaque trust boundary.
**Mitigation:**
- **INV-15 (no AWS-managed identity in path):** Nova-idp uses **KMS +
DDB + Lambda only.** No Cognito, no IAM Identity Center, no managed
user pools. The identity layer is greenfield and fully owned by
Nova. This is a constraint, not a mitigation — it shapes the whole
design (Argon2id in Lambda instead of Cognito user pools; KMS-signed
JWTs instead of Cognito issued tokens; DDB `nova-pats` instead of
IAM access keys).
- **Verified by inspection:** `core/lambda/nova_idp_auth.py` +
`nova_idp_token_vend.py` import only `boto3` (DDB + KMS), `argon2`,
`cryptography`, `pyjwt`, and `core.*`. No `cognitoidp` or
`identitystore` client calls anywhere in the identity layer.
**Residual risk:** none (this is a satisfied constraint, not a
residual). The trade-off is operational burden (Nova runs its own
password hashing, token signing, revocation) in exchange for
portability and no opaque trust boundary.
### T-8 — Audit trail integrity
**Threat:** an attacker tampers with the audit stream to hide a
malicious vend, a revocation, or a policy change.
**Mitigations:**
- **Every event emitted (INV-12):** `cli.invocation`, `auth.sign_up`,
`auth.sign_in`, `auth.session_created`, `pat.issued`, `pat.revoked`,
`token.vend.allowed`, `token.vend.denied`, `auth.login`,
`auth.status`, `auth.revoke` — each is a JSON line on stderr with a
timestamp + the relevant identifiers (`user_id`, `jti`, `sub`,
`policy_sha`).
- **Policy version (git SHA, D-231) in every token-vend event:** the
`policy_sha` field lets an auditor reconstruct which policy version
governed each vend — a policy change is visible in the audit stream
as a `policy_sha` change.
- **Raw credentials scrubbed (INV-16/INV-17 spirit):** the
`_emit_audit` functions in `nova_idp_auth.py`,
`nova_idp_token_vend.py`, and `pat_lifecycle.py` pop any
`password`/`pat`/`token`/`raw_pat` kwarg defense-in-depth. The audit
stream carries identifiers, not secrets.
- **Revoked PATs retained (REQ-343):** `nova-pats` rows are marked
`status=revoked`, never deleted. The audit trail of "who was
revoked, when" is queryable.
**Residual risk:** medium (audit integrity is only as strong as the
log destination). The Lambdas emit to stderr (CloudWatch Logs by
default); the integrity guarantee depends on the downstream log
pipeline (immutability, retention). For high-assurance deployments,
forward the audit stream to an append-only store (S3 Object Lock, a
write-once log service). This is a deployment concern, documented in
the operator guide.
---
## 4. C-9.2 — INV-18..21 compression audit
The source spec (the v1.28 design document that was re-mapped into this
repo's REQ-323..353 / INV-12..17 — see `REQUIREMENTS.md` §v1.28 "ID
re-mapping") referenced `INV-18..21` as "attestation invariants."
Those IDs **do not exist in this repo** (this repo's invariants run
INV-1..11 for the blockchain/pilot work and INV-12..17 for v1.28). The
grill (C-9.2) requires an audit verifying the spec's attestation
invariant semantics were fully captured by the re-mapped
INV-15/INV-16/INV-17 + REQ-332, with no semantic gap.
### The spec's attestation invariant semantics (reconstructed)
The source spec's INV-18..21 expressed four attestation concerns:
1. **Immutability** — an attestation, once made, cannot be silently
altered.
2. **Signature verifiability** — the attestation's signature can be
independently verified by a third party holding the public key.
3. **Key derivation** — the signing key is derived from a known input
(the PAT) via a specified KDF, not ad-hoc.
4. **No AWS-managed identity** — the attestation scheme does not
depend on Cognito / IAM Identity Center (the greenfield constraint).
### Mapping to the re-mapped invariants + requirements
| Spec concern | Re-mapped to | Where enforced |
|--------------|--------------|----------------|
| Immutability | **INV-6** (existing, pre-v1.28 — the immutable audit ledger) + **INV-17** (ABAC discipline — every vend is audited with `policy_sha`) | the audit stream is append-only; `policy_sha` binds each vend to a policy version |
| Signature verifiability | **REQ-332** (JWS-from-PAT KDF) + **REQ-337** (KMS-signed OIDC, JWKS verifiable) | `core/jws_attestation.py:verify_attestation` (HS256, constant-time compare); `core/kms_signing.py` + JWKS endpoint |
| Key derivation | **REQ-332** (C-5.2 grill fix) — `HKDF-SHA256(PAT, salt='nova-local-attestation', info='jws-signing-key')` → 32-byte symmetric key | `core/jws_attestation.py:derive_signing_key`; verified by `tests/test_jws_attestation.py` |
| No AWS-managed identity | **INV-15** (no Cognito / IAM Identity Center in path) | inspection — the identity layer uses KMS + DDB + Lambda only |
### Conclusion: the compression is sound — no semantic gap
The spec's four attestation concerns are covered by:
- **INV-6** (immutability — the existing audit ledger, carried forward
from pre-v1.28 milestones),
- **INV-15** (no AWS-managed identity — the greenfield constraint),
- **INV-16** (password storage — the Argon2id + no-raw-password rule,
which is the attestation *input* integrity for signup),
- **INV-17** (ABAC discipline — every vend is policy-gated + audited
with `policy_sha`),
- **REQ-332** (JWS-from-PAT KDF — the signature + key-derivation
scheme for local-review attestations).
The re-mapping from `INV-18..21``INV-15/16/17 + REQ-332` is a
**compression** (4 invariants → 3 invariants + 1 requirement), not a
**drop**. The four original concerns (immutability, signature
verifiability, key derivation, no-managed-identity) each have a
load-bearing home in the re-mapped set. **No attestation invariant
semantics were silently dropped.**
The compression is *justified* because:
- INV-6 already covered audit immutability (re-stating it as INV-18
would have been a duplicate of an existing invariant).
- INV-15 already covered the no-managed-identity constraint
(re-stating it as INV-21 would have been a duplicate).
- INV-16 + INV-17 cover the input-integrity + policy-discipline
concerns that the spec's INV-19/20 expressed as attestation-specific
invariants (they are in fact general identity-layer invariants, not
attestation-specific).
- REQ-332 carries the signature + KDF detail that the spec's INV-18
hand-waved ("public key derivable from the PAT") — and corrects it
to a sound symmetric scheme (C-5.2).
### Audit verification (how to re-run this audit)
```sh
# 1. Confirm INV-18..21 do not exist in this repo.
grep -rE 'INV-1[89]|INV-2[01]' .ciagent/ docs/ core/ tests/ \
| grep -v 'INV-18..21' # only the C-9.2 audit references should remain
# 2. Confirm the re-mapped invariants + REQ-332 exist + are tested.
pytest tests/test_jws_attestation.py tests/test_abac_fail_closed.py \
tests/test_kms_roundtrip.py tests/test_argon2_fail_closed.py -q
```
---
## 5. Test coverage summary
| Threat | Test file | What it verifies |
|--------|-----------|------------------|
| T-1 (password) | `tests/test_argon2_fail_closed.py` | 503 on argon2 unavailable (no weak hash) |
| T-1 (password) | `tests/test_idp_auth.py` | no raw password in DDB item or logs (INV-16) |
| T-2 (PAT theft) | `tests/test_auth_commands.py` | credentials.json has OIDC token, NOT raw PAT (C-7.3) |
| T-2 (PAT theft) | `tests/test_pat_revocation.py` | revocation takes effect <1s (D-229 SLO) |
| T-3 (JWKS DDoS) | (CloudFormation template inspection) | reserved concurrency = 10; WAF with `--public-jwks-domain` |
| T-4 (ABAC bypass) | `tests/test_abac_fail_closed.py` (7 tests) | fail-closed on engine absent / error / deny (C-6.1) |
| T-5 (KMS key) | `tests/test_kms_roundtrip.py` | KMS sign → JWKS → pyjwt verify (CAP-037) |
| T-6 (DER→raw) | `tests/test_kms_roundtrip.py` | the round-trip succeeds only if DER→raw is byte-correct |
| T-7 (no managed id) | (inspection) | no `cognitoidp` / `identitystore` imports in the identity layer |
| T-8 (audit) | `tests/test_e2e_idp.py` | the full audit chain is present + linked (REQ-348) |
---
## 6. Open items (deferred, not blocking v1.28)
- **WAF rate-limit tuning:** the default rate-based rule threshold
(2000 req/5min/IP) is a pilot-scale guess. Production tuning needs
real traffic data. Tracked as a post-v1.28 ops task.
- **Audit log forwarding to an append-only store** (S3 Object Lock):
the Lambdas emit to stderr / CloudWatch Logs by default. High-
assurance deployments should forward to a write-once destination.
Documented in the operator guide; not enforced in code.
- **PAT theft detection:** there is no anomaly detection on PAT usage
(e.g. a vend from a new geography). The TTL + revocation is the
control. Detection is a future milestone.
+1
View File
@@ -0,0 +1 @@
"""Nova CLI package — thin subcommand delegates to core.* (P1, REQ-324)."""
+45
View File
@@ -0,0 +1,45 @@
"""nova apply — resolve a contract + synthesize local env (REQ-330, REQ-332).
Subcommand (50 lines, 3 functions, delegates to core/ NFR-7).
nova apply --local --contract .nova/contract.yml [--sign-local-review]
nova apply --contract contracts/microservice.yml --out stack.json
--local: calls core.env.synthesize_local_env() + core.contract_resolver.resolve()
--sign-local-review: calls core.jws_attestation.sign_attestation() (REQ-332)
"""
from __future__ import annotations
import json
from core import env
from core.contract_resolver import resolve
from core.jws_attestation import sign_attestation
def add_parser(subparsers):
p = subparsers.add_parser("apply", help="resolve a contract (+ local env synth)")
p.add_argument("--contract", default=".nova/contract.yml", help="contract YAML path")
p.add_argument("--out", default=None, help="output path (default: stdout)")
p.add_argument("--local", action="store_true", help="synthesize a local env (no AWS)")
p.add_argument("--environment", default=None, help="environment override")
p.add_argument("--sign-local-review", action="store_true", help="sign a local-review attestation (REQ-332)")
p.add_argument("--pat", default=None, help="PAT for --sign-local-review")
p.set_defaults(_run=run)
def run(args) -> int:
synth = env.synthesize_local_env(args.contract, environment=args.environment) if args.local else None
env_override = (synth["name"] if isinstance(synth, dict) else None) or args.environment
result = resolve(args.contract, environment_override=env_override)
blob = json.dumps(result, indent=2) + "\n"
pat = args.pat or env.get_env("PAT", "") or ""
attestation = sign_attestation({"contract": args.contract, "review": "local"}, pat) if (args.sign_local_review and pat) else None
blob = blob + (attestation + "\n" if attestation else "")
print(blob) if args.out is None else open(args.out, "w").write(blob)
return 0
if __name__ == "__main__":
import sys
print("use: nova apply --contract <contract.yml> [--local] [--sign-local-review]", file=sys.stderr)
+22
View File
@@ -0,0 +1,22 @@
"""nova attestation-matrix — run the 8-concern attestation matrix (REQ-109)."""
from __future__ import annotations
from core.attestation_matrix import cli_main
def add_parser(subparsers):
p = subparsers.add_parser("attestation-matrix", help="run the 8-concern attestation matrix")
p.add_argument("env", help="target environment (dev/qa/prod/dr)")
p.add_argument("evidence", nargs="?", default=None, help="evidence JSON path")
p.set_defaults(_run=run)
def run(args) -> int:
argv = ["nova-attestation-matrix", args.env] + ([args.evidence] if args.evidence else [])
return cli_main(argv)
if __name__ == "__main__":
import sys
print("use: nova attestation-matrix <env> [evidence.json]", file=sys.stderr)
+21
View File
@@ -0,0 +1,21 @@
"""nova auth — login / revoke / status subcommands (REQ-344, C-7.3).
Subpackage entry point: ``add_parser`` registers the ``auth`` subparser
with ``login``/``revoke``/``status`` sub-subcommands, each delegating to
its module's ``run``. Discovered by ``nova/cli.py`` via
``pkgutil.iter_modules`` (this package's ``add_parser`` is the hook).
"""
from __future__ import annotations
import argparse
def add_parser(subparsers):
p = subparsers.add_parser("auth", help="Nova IdP auth (login/revoke/status)")
sub = p.add_subparsers(dest="auth_command", required=True)
from nova.auth import login as _login, revoke as _revoke, status as _status
_login.add_parser(sub)
_revoke.add_parser(sub)
_status.add_parser(sub)
return p
+58
View File
@@ -0,0 +1,58 @@
"""nova auth login — session/PAT → OIDC token, store locally (REQ-344, C-7.3)."""
from __future__ import annotations
import json
import os
import sys
from core.auth_store import store_credential, credentials_path
def _vend(pat: str, env: str, endpoint: str) -> dict:
"""Call the token-vend Lambda (locally or via the function URL)."""
if endpoint and endpoint.startswith("http"):
import urllib.request
body = json.dumps({"token": pat, "environment": env}).encode()
req = urllib.request.Request(endpoint, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
# Local: invoke the Lambda in-process.
import importlib.util
from pathlib import Path
p = Path(__import__("core").__file__).parent / "lambda" / "nova_idp_token_vend.py"
spec = importlib.util.spec_from_file_location("nova_idp_token_vend", p)
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
os.environ.setdefault("NOVA_LAMBDA_LOCAL_BYPASS", "1")
resp = mod.lambda_handler({"body": json.dumps({"token": pat, "environment": env})}, None)
return json.loads(resp["body"])
def add_parser(subparsers):
p = subparsers.add_parser("login", help="exchange a PAT/session for an OIDC token")
p.add_argument("--pat", default=None, help="PAT JWT (prompted if absent)")
p.add_argument("--session", default=None, help="session token (alias for --pat)")
p.add_argument("--environment", default="dev", help="target environment")
p.add_argument("--endpoint", default=os.environ.get("NOVA_TOKEN_VEND_URL", ""),
help="token-vend function URL (empty = local)")
p.set_defaults(_run=run)
def run(args) -> int:
pat = args.pat or args.session or os.environ.get("NOVA_PAT")
if not pat:
pat = sys.stdin.readline().strip()
if not pat:
print("error: no PAT/session provided", file=sys.stderr); return 1
result = _vend(pat, args.environment, args.endpoint)
if "token" not in result:
print(f"error: {result.get('error', result)}", file=sys.stderr); return 2
import base64
payload = json.loads(base64.urlsafe_b64decode(result["token"].split(".")[1] + "=="))
store_credential(
jti=payload.get("jti", ""), cred_type=payload.get("typ", "nova_oidc_token"),
exp=payload.get("exp", 0), oidc_token=result["token"],
)
print(f"logged in: jti={payload.get('jti')} exp={payload.get('exp')} "
f"file={credentials_path()}")
return 0
+38
View File
@@ -0,0 +1,38 @@
"""nova auth revoke --pat <jti> — revoke a PAT (REQ-344, D-229)."""
from __future__ import annotations
import os
import sys
from core.auth_store import emit_revoke_audit
def add_parser(subparsers):
p = subparsers.add_parser("revoke", help="revoke a PAT by jti")
p.add_argument("--pat", required=True, help="PAT jti to revoke")
p.add_argument("--endpoint", default=os.environ.get("NOVA_TOKEN_VEND_URL", ""),
help="token-vend function URL (empty = local DDB)")
p.set_defaults(_run=run)
def _revoke_remote(jti: str, endpoint: str) -> dict:
import json, urllib.request
body = json.dumps({"action": "revoke_pat", "jti": jti}).encode()
req = urllib.request.Request(endpoint, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
def run(args) -> int:
try:
if args.endpoint and args.endpoint.startswith("http"):
_revoke_remote(args.pat, args.endpoint)
else:
from core.pat_lifecycle import revoke_pat
revoke_pat(args.pat)
emit_revoke_audit(args.pat)
print(f"revoked: jti={args.pat}")
return 0
except Exception as e:
print(f"error: {e}", file=sys.stderr); return 2
+34
View File
@@ -0,0 +1,34 @@
"""nova auth status — print active credential + mode (REQ-344)."""
from __future__ import annotations
import json
import os
import sys
from core.auth_store import active_credential, emit_status_audit
from core.mode_resolver import resolve_mode_from_env
def add_parser(subparsers):
p = subparsers.add_parser("status", help="show active credential + client mode")
p.set_defaults(_run=run)
def run(args) -> int:
cred = active_credential()
emit_status_audit()
mode, reason = resolve_mode_from_env(
credential_type=cred.get("type") if cred else None,
)
if cred is None:
print(f"no active credential (mode={mode}, reason={reason})")
return 0
print(json.dumps({
"active_credential_jti": cred.get("jti"),
"type": cred.get("type"),
"exp": cred.get("exp"),
"mode": mode,
"selection_reason": reason,
}, indent=2))
return 0
+60
View File
@@ -0,0 +1,60 @@
"""Nova CLI entry point — dispatch + audit (P1, REQ-324, INV-12).
Auto-discovers nova/<module>.py subcommands; each exports
add_parser(subparsers) + run(args) -> int. Resolves the client mode
via core.mode_resolver and emits a cli.invocation audit event (stderr
JSON line stub) before dispatching.
"""
from __future__ import annotations
import argparse
import importlib
import json
import pkgutil
import sys
from typing import Optional
from core.mode_resolver import resolve_mode_from_env
def _emit_invocation(mode, reason, cred_type, command, args):
"""INV-12: emit cli.invocation audit event to stderr (stub)."""
event = {
"event": "cli.invocation",
"mode": mode,
"selection_reason": reason,
"credential_type": cred_type,
"command": command,
"args": args,
}
sys.stderr.write(json.dumps(event, sort_keys=True) + "\n")
import nova
def _build_parser():
parser = argparse.ArgumentParser(prog="nova", description="Nova platform CLI")
parser.add_argument("--mode", choices=["agent", "interactive"], default=None)
sub = parser.add_subparsers(dest="command", required=True)
for mod_info in pkgutil.iter_modules(nova.__path__):
name = mod_info.name
if name == "cli":
continue
mod = importlib.import_module(f"nova.{name}")
mod.add_parser(sub)
return parser
def main(argv: Optional[list] = None) -> int:
parser = _build_parser()
args = parser.parse_args(argv)
mode, reason = resolve_mode_from_env()
arg_dict = {k: v for k, v in vars(args).items() if k != "_run"}
_emit_invocation(mode, reason, None, args.command, arg_dict)
return args._run(args)
if __name__ == "__main__":
sys.exit(main())
+21
View File
@@ -0,0 +1,21 @@
"""nova confidence — compute the confidence signal (REQ-19)."""
from __future__ import annotations
from core.confidence_signal import cli_main
def add_parser(subparsers):
p = subparsers.add_parser("confidence", help="compute the confidence signal")
p.add_argument("inputs_json", help="path to an inputs JSON file")
p.add_argument("environment", help="target environment")
p.set_defaults(_run=run)
def run(args) -> int:
return cli_main(["nova-confidence", args.inputs_json, args.environment])
if __name__ == "__main__":
import sys
print("use: nova confidence <inputs.json> <environment>", file=sys.stderr)
+28
View File
@@ -0,0 +1,28 @@
"""nova decommission — transform a resolved stack for decommission (REQ-92)."""
from __future__ import annotations
import json
from core.decommission_transform import decommission_transform
def add_parser(subparsers):
p = subparsers.add_parser("decommission", help="transform a stack JSON for decommission")
p.add_argument("stack_json", help="path to a resolved stack JSON")
p.add_argument("--out", default=None, help="output path (default: stdout)")
p.set_defaults(_run=run)
def run(args) -> int:
with open(args.stack_json) as fh:
stack = json.load(fh)
out = decommission_transform(stack)
blob = json.dumps(out, indent=2)
print(blob)
return 0
if __name__ == "__main__":
import sys
print("use: nova decommission <stack.json>", file=sys.stderr)
+25
View File
@@ -0,0 +1,25 @@
"""nova env-check — check that an environment is bound (REQ-181)."""
from __future__ import annotations
import sys
from core.environment_check import check
def add_parser(subparsers):
p = subparsers.add_parser("env-check", help="check that an environment is bound")
p.add_argument("contract", nargs="?", default=None, help="contract path")
p.add_argument("--env", default=None, help="environment name override")
p.set_defaults(_run=run)
def run(args) -> int:
ok, message = check(contract_path=args.contract, env_name=args.env)
print(message) if ok else sys.stderr.write(message + "\n")
return 0 if ok else 1
if __name__ == "__main__":
import sys
print("use: nova env-check <contract.yml> [--env name]", file=sys.stderr)
+37
View File
@@ -0,0 +1,37 @@
"""nova env-transition — detect/record the applied environment (REQ-183)."""
from __future__ import annotations
import json
from core.env_transition import detect_prior_env, record_applied_env
def add_parser(subparsers):
p = subparsers.add_parser("env-transition", help="detect/record the env for a contract")
sub = p.add_subparsers(dest="env_transition_command", required=True)
pd = sub.add_parser("detect")
pd.add_argument("--contract-id", required=True)
pd.add_argument("--consumer-repo", required=True)
pd.add_argument("--new-env", required=True)
pr = sub.add_parser("record")
pr.add_argument("--contract-id", required=True)
pr.add_argument("--consumer-repo", required=True)
pr.add_argument("--env", required=True)
p.set_defaults(_run=run)
def run(args) -> int:
cmd = args.env_transition_command
payload = _dispatch(cmd, args)
print(json.dumps(payload))
return 0 if cmd == "detect" else (0 if payload["recorded"] else 1)
def _dispatch(cmd, args) -> dict:
return {"prior_env": detect_prior_env(args.contract_id, args.consumer_repo, args.new_env)} if cmd == "detect" else {"recorded": record_applied_env(args.contract_id, args.consumer_repo, args.env)}
if __name__ == "__main__":
import sys
print("use: nova env-transition detect|record ...", file=sys.stderr)
+33
View File
@@ -0,0 +1,33 @@
"""nova hitl — attest a promotion gate (REQ-108)."""
from __future__ import annotations
import json
import sys
from core.hitl_gates import attest, approver_from_env
def add_parser(subparsers):
p = subparsers.add_parser("hitl", help="attest a promotion gate")
p.add_argument("--contract-id", required=True)
p.add_argument("--env", required=True, help="dev/qa/prod/dr")
p.add_argument("--evidence", default=None, help="evidence JSON path")
p.set_defaults(_run=run)
def run(args) -> int:
evidence = _load_evidence(args.evidence)
approver = approver_from_env() or ""
ok, reason = attest(args.contract_id, args.env, approver, evidence)
print(f"HITL PASS: {reason}") if ok else sys.stderr.write(f"HITL BLOCK: {reason}\n")
return 0 if ok else 1
def _load_evidence(path):
return {} if path is None else json.loads(open(path).read())
if __name__ == "__main__":
import sys
print("use: nova hitl --contract-id <id> --env <env> [--evidence f.json]", file=sys.stderr)
+13
View File
@@ -0,0 +1,13 @@
"""nova idp — IdP setup subcommands (REQ-340, C-2.1)."""
from __future__ import annotations
import argparse
def add_parser(subparsers):
p = subparsers.add_parser("idp", help="Nova IdP management (setup)")
sub = p.add_subparsers(dest="idp_command", required=True)
from nova.idp import setup as _setup
_setup.add_parser(sub)
return p

Some files were not shown because too many files have changed in this diff Show More