---ci--- project: acdl phase: 6 milestone: v1.29 status: complete ---/ci---
40 KiB
Nova — Architecture
Compressed. The full v1.0–v1.24 architecture history (v1.1 spike scope, v1.2 build-out, v1.8–v1.16 addenda) is preserved verbatim at
.ciagent/archive/ARCHITECTURE-v1.0-v1.24.md. This file retains the durable target architecture (§1–§12, the four layers + six cross-cutting concerns) + the three addenda that describe the current state: v1.11 (stateless adapter), v1.15 (Nova rebrand — current naming), and v1.17 (telemetry/observability layer + §12.7 Policy Engine Registry). Intermediate addenda (v1.1 spike scope, v1.2 build-out, v1.8/1.9/1.10/ 1.12/1.13/1.14/1.16) describe evolved or superseded states and are preserved in the archive snapshot.Source of truth for how:
docs/architecture.md(v0.2) is the upstream draft; this file is the Nova-repo operating copy, refined at phase boundaries. Where this file anddocs/vision.mdconflict, the vision wins.
Status
Architecture is at v0.2 upstream (docs/architecture.md). Milestone
v1.1 finalized it to v1.0 in Phase 07 by resolving the 11 open
decisions (see PROJECT.md open-decision resolutions table). The v1.11
addendum (stateless adapter) and the v1.17 addendum (telemetry layer +
§12.7 Policy Engine Registry) record the current-state refinements.
Overview
The platform is four layers + six cross-cutting concerns. The sixth concern — the engine abstraction (§12) — is first-class, not an implementation detail. The vision's "Two Consumer Surfaces, One Platform" tenet binds everything: L3A and L3B converge on the same contract schema, the same policy envelope, and the same evidence stream.
┌──────────── acdl-contracts ────────────┐
Developer ───▶ │ commit contract.yaml │ (L3A)
Citizen dev ──▶ │ Issue → agent → contract.yaml │ (L3B)
└────────────────┬───────────────────────┘
│ (push)
▼
┌──────────────────────┐
│ central pipeline │
│ (acdl repo, Gitea │
│ Actions / act_runner) │
└────────┬─────────────┘
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
contract→IR resolution policy (Checkov/Kyverno) confidence signal
│ │ │
▼ ▼ ▼
Terraform adapter ──▶ terraform plan ──▶ PolicyCheckResult ──▶ {score,band}
│ │
▼ ▼
dev (autonomous, ≥0.50) qa (HITL, ≥0.75) prod (HITL, ≥0.90) dr (HITL, ≥0.95)
│
▼
DynamoDB outbox ──▶ S3 Object Lock (7-yr, source of truth) ──▶ GitHub audit repo (hot index)
│
▼
acdl-evidence (timeline UI)
Layers
Layer 1 — Foundational Primitives
Single-purpose, engine-agnostic primitive modules. L1 modules do not compose with other L1s; L1 takes its environment as input. The L1 interface is defined against the Target Stack IR, not against Terraform directly (the IR is shaped to round-trip to Terraform in v1, per §12.1).
- No inter-L1 references. L1 may call Terraform data sources.
- Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (W3.D).
- Immutability on publication. 12-month deprecation window.
- AI refinement is a flag; the trigger is the W1.A joint condition.
Layer 2 — Composed Stacks
Combine L1 primitives into deployable shapes. Each codebase maps to one
canonical L2 stack (multiStack: true only per W1.B). Shape X
(parameterized module) or Shape Y (thin-composition layer). Hierarchical
composition, max depth 5, only registered L1s. The thin-composition tree's
wires field is defined against the IR's relationship type, not a
Terraform module block.
Pipeline quality checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, tag compliance, naming convention. Restricted from thin-composition: IAM principal creation, network boundary creation, key/secret creation, external data transfer. Auto-promote after 3 observed usages.
Layer 3A — Developer Consumer Surface
Tag-based reference to the central pipeline template. Developer-owned workflow file, no platform auto-sync. L3A and L3B are parallel paths, not a progression. W2.A (Path B): tag for dev/qa, SHA for prod; platform CLI resolves tag→SHA for prod-bound workflows.
Layer 3B — Agentic Consumer Surface
Hybrid runtime, skill as markdown, agent as executor. Trust model: trust
and always verify on the platform side. Skill envelope (4 dimensions).
Stateless agents, all state in the platform. profile: agentic marker
unlocks naturalLanguageIntent, confidenceAtSubmission, agentTrace.
Initial skill catalog (BA.A): web API, worker, scheduled job, static asset,
basic observability bootstrap.
Environment progression:
| Environment | Autonomy | Attester | Gate |
|---|---|---|---|
| dev | Full autonomy (no HITL) | — | Confidence ≥ 0.50, all six inputs present |
| qa | Held for attestation | QA | GitHub Deployment approval + full QA matrix (§10) |
| prod | Held for attestation | SRE | GitHub Deployment approval + full SRE matrix (§10) |
| dr | Held for attestation | SRE | GitHub Deployment approval + dr-drill evidence |
Staging is removed. Dev is the only autonomous environment.
Cross-cutting concerns
Central pipeline template (§6)
JSON Schema (draft 2020-12) with a thin domain wrapper. Central repo +
generated client libraries. Multi-stage validation: schema → policy → NFR →
confidence. Distributed enrichment. GitOps reconciler (K8s API; cdlc-gitops
state → CRDs) + Terraform execution layer (§12.5). The pipeline emits one
PolicyCheckResult per policy rule; the confidence signal consumes them as
one normalized input.
Contract schema (§7)
Central repo + generated client libraries. Strict fail-fast at schema stage, multi-stage validation with reason codes from a published vocabulary. W3.E: per-env mandatory inputs —
- dev:
stack,environment - qa adds:
validation.e2eSuite,validation.loadTest - prod adds:
runbook,dashboard,oncall - dr adds:
drDrillRef inputsalways optional;profile: agenticfields optional everywhere.
Confidence signal (§8)
Six canonical inputs, weighted sum with per-input breakdown. Per-env
thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95. Structured output
{ score, band, perInput, reasonCodes }. 1-year storage, no retraining in
v1. Halt with explicit reason on missing input.
Policy input = list of PolicyCheckResult records (engine-agnostic).
Severity → penalty: critical → hard override to mandatory block; high →
-0.2; medium → -0.05; low → -0.01; info → 0.0. One critical finding
hard-overrides the score regardless of all other inputs.
BA.B: thresholds frozen for v1; tuning begins v1.2 (quarterly FP/FN tracking; override = Infra & Ops + SRE joint sign-off, itself a confidence-event).
Audit and evidence stream (§9)
Tiered ledger: S3 with Object Lock in compliance mode (cold, source of
truth, 7-year retention) + GitHub audit repo (acdl-evidence, hot
query index, not part of the chain). Daily checkpoints. Event schema: JWS
detached signature, prev_event_hash chain, controlled-vocabulary
event_type. Outbox pattern: local durable outbox + async worker.
Outbox database = DynamoDB. RPO = 0 (synchronous write to local outbox before contract submission ack); RTO = async worker's dead-letter recovery. Single-region in v1. The outbox also stores per-contract QA and prod approver identities (the only durable record outside GitHub's audit log).
v1.17 update: the Decision Ledger (SQLite hash-chain, D-121) is the pilot's audit record. S3 Object Lock / JWS (D-083) is deferred — see the v1.17 addendum below.
Human-in-the-Loop mechanics (§10)
Pre-execution gates. qa, prod, dr are PR-based attestation gates backed by GitHub Environments with required reviewers. No partial deployment to roll back on rejection (qa, prod); dr is a separate GitHub Deployment against a separate cluster/region.
Reviewer routing: GitHub CODEOWNERS + Environment required reviewers (qa → QA; prod → SRE; dr → SRE). CODEOWNERS routes, does not enforce identity distinctness.
Separation of duties (platform-internal, not GitHub-native, not Kyverno
in v1): on dev→qa promotion the platform writes the QA approver's GitHub
identity to the DynamoDB outbox keyed by contractId; on qa→prod it reads
the stored QA approver and the new SRE approver; if equal, it blocks, emits
SEPARATION_OF_DUTIES_VIOLATION, and routes a halt artifact to SRE on-call.
Full 8-concern attestation matrix (functional, performance, security
posture, contract NFRs, operational readiness, incident response,
capacity/cost, resilience) — see docs/architecture.md §10.4.
Timeout: 1 business day = warn + escalate; 2 business days = auto-freeze +
re-submit (linked via supersedes). Rejection returns the contract to HELD;
the audit chain is extended, not torn up.
Agentic stack (§11)
Hybrid runtime: platform-managed control plane + consumer-owned agent. Versioned, signed skill catalog over MCP. Skill envelope enforced on invocation and result submission. Consumer-owned skill execution; the platform does not run the skill. Stateless agents, all state in the platform. Skills are reviewed for sensitive data before release (Infra & Ops owns the review; it is the mandatory release gate).
Engine execution (§12) — the binding constraint
Target Stack IR (locked): an engine-neutral description of resources (typed inputs/outputs/NFRs), relationships (single parent per child), composition (tree, max depth 5), and policy hooks. The L1 registry, L2 thin-composition tree, contract YML, and PolicyCheckResult schema are all defined against the IR — none against any specific engine.
Engine adapters are the only engine-specific code. An adapter compiles the IR into an engine execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add OpenTofu, Pulumi, K8s CRDs without architectural change.
v1 reality: the IR is shaped to round-trip cleanly to Terraform (nearly isomorphic). As more adapters appear, the IR gets more expressive and the adapters gain translation logic; the L1 content, the YML standard, and the thin-composition tree do not change.
v1.11 update: the Terraform adapter is now a stateless assembler (~80 lines, emits
module "x" { source }blocks) — see the v1.11 addendum below. The §12 "thin layer that translates IR → Terraform variable/output blocks" framing is superseded by the stateless-assembler model; the L1-owns-its-shape invariant is the new contract.
State storage: S3 (state) + DynamoDB (locking), cloud-managed, single-region in v1.
Policy toolchain: Checkov for Terraform plan policy (the L2 checks + tag/naming); Kyverno for K8s-native/platform-internal policy; OPA reserved for cross-resource cases, explicitly last resort.
v1.25 update: the policy toolchain is now unified under the swappable
PolicyEngineprotocol — see §12.7 below. Checkov and Wiz remain as raw-finding adapters feeding into kyverno-json meta-policies.
Policy result normalization (§12.6): the confidence signal consumes a
normalized PolicyCheckResult schema, not raw engine output.
{
"contractId": "uuid",
"evaluatedAt": "ISO-8601",
"engine": "checkov | kyverno | opa",
"ruleId": "CKV_AWS_24 | KYVERNO_NO_PRIVILEGED | ...",
"severity": "critical | high | medium | low | info",
"result": "pass | fail | skipped | error",
"message": "human-readable",
"evidence": { "...engine-specific, opaque to the signal..." },
"resourceRef": "IR-typed resource identifier"
}
Execution layer: GitHub/Gitea Actions in the central pipeline repo. State
locking via DynamoDB. AWS credentials via OIDC federation — long-lived
credentials are forbidden (§12.5). The platform does not run
terraform apply against a developer's workstation; all execution is in
the central pipeline.
Registry maintenance: L1 publication updates the L1 registry in the same PR. The registry is the IR-typed contract, not a Terraform-specific variable schema.
Contract→IR resolution: the contract declares intent in IR-typed terms; the pipeline resolves it to a target stack (list of L1 instances + inputs + relationships); the Terraform adapter compiles the target stack to a plan.
v1.11 Addendum — Stateless Adapter + Pipeline-Driven Lifecycle Testing (current state)
Stateless adapter (D-098). adapters/terraform/adapter.py rewritten
from a 918-line monolith (3 constant tables TYPE_MAP/INPUT_MAP/
OUTPUT_MAP, 39 type-specific branches) to a ~80-line stateless assembler.
Each L1 module ships a real terraform/ module dir
(versions.tf/variables.tf/locals.tf/main.tf/outputs.tf) owning
its resource shape, nested blocks, and defaults. The adapter reads the
registry, emits a root main.tf instantiating each L1 as
module "x" { source = "..." } with resolved inputs and wired refs.
Terraform owns lifecycle (D-101). scripts/run_platform.sh gains
--apply and --destroy modes. Python never runs terraform.
scripts/verify_deploy_microservice.py is deleted.
Pipeline-driven testing (D-102). A modules-lifecycle pipeline
(Gitea + GitHub, byte-identical) matrix-runs each L1 module's
examples/{simple,complex}.yml contracts through apply→modify→destroy
against live AWS. No per-module Python/pytest. The "test" = the pipeline
cell going green.
Single platform VPC (D-105). terraform/platform/main.tf owns ONE
VPC; the microservice composition references it via
terraform_remote_state (data source). State keys are deterministic and
env-aware (spike/{contract.id}/{contract.environment}/terraform.tfstate).
NOVA_LIFECYCLE_MODE (v1.12, REQ-134; renamed ACDL→NOVA in v1.15 P2).
The lifecycle pipeline defaults to plan-only (fast, no AWS mutation, no
cost). A CI variable NOVA_LIFECYCLE_MODE (default plan) overrides to
full for the real apply→modify→destroy. (P2–P4 dual-read fallback to
ACDL_LIFECYCLE_MODE; fallback removed in P5 per the v1.15 addendum.)
v1.15 Addendum — Nova Rebrand (current naming)
Milestone: v1.15-Nova. A full rebrand from ACDL / "Agentic Cloud
Delivery Platform" → Nova / "The New Dawn of DevSecOps — security as
a seamless enabler of fast deployments." This is a Major milestone
(breaking): consumer-facing path, env var prefixes, SSM path, AWS tag
keys, and AWS resource names all change. v1.15 tags run on the v1.15.x
minor line: v1.15.0 (P0) → v1.15.4 (P5 final = release). (G-104
binding.)
Naming conventions (rebranded — current)
| Convention | Before (v1.0–v1.14) | After (v1.15+) | Phase |
|---|---|---|---|
| Project name | ACDL / "Agentic Cloud Delivery Platform" |
Nova / "The New Dawn of DevSecOps" |
P1 |
| Tagline | "Consumers declare intent; the platform delivers safe production deployment through an agentic stack" | (retained) + "The New Dawn of DevSecOps — security as a seamless enabler of fast deployments" | P1 |
Schema $id URL |
https://acdl.cloudinit.dev/schemas/... |
https://nova.cloudinit.dev/schemas/... |
P1 |
| Gitea release title | ACDL vX.Y.Z |
Nova vX.Y.Z |
P1 (forward only) |
| Env var prefix | ACDL_* (21 vars) |
NOVA_* (dual-read fallback in P2–P4; removed P5) |
P2 |
| Env loader | scattered os.environ.get("ACDL_*") |
centralized core/env.py get_env() (D-108) |
P2 |
| Consumer contract path | .acdl/contract.yml |
.nova/contract.yml |
P2 |
| Checkov custom rule file | acdl_tagging.py |
nova_tagging.py |
P2 |
| Checkov tag-key enforcement | acdl:* (hard) |
nova:* (warn P2, hard P3) |
P2/P3 |
| SSM parameter path | /acdl/{env}/{contractId}/{output} |
/nova/{env}/{contractId}/{output} |
P3 |
| AWS tag keys | `acdl:owner | environment | contract |
| ABAC session policy match | acdl:* tags |
nova:* tags (parallel-tag period) |
P3 |
| DynamoDB tables | acdl-contracts, acdl-change-requests |
nova-contracts, nova-change-requests (scan+copy) |
P4 |
| Lambda (ingestor) | acdl-contract-ingestor (role/policy/function) |
nova-contract-ingestor |
P4 |
| Secrets Manager secret | acdl/github-token |
nova/github-token |
P4 |
| SNS topic | acdl-sod-halt |
nova-sod-halt |
P4 |
| Security group | acdl-ecs-sg |
nova-ecs-sg |
P4 |
| KMS alias | alias/acdl-platform |
alias/nova-platform |
P4 |
| ECS cluster/service/task | acdl-microservice |
nova-microservice |
P4 |
| ECR repo | acdl-microservice |
nova-microservice (re-push) |
P4 |
| IAM user/policy | acdl-spike-runner (+policy) |
nova-spike-runner (re-bootstrap) |
P4 |
| S3 state bucket | acdl-tfstate-581513795199-us-east-1 |
nova-tfstate-581513795199-us-east-1 (-migrate-state) |
P4 |
| ALB name prefix | acdl-alb |
nova-alb |
P4 |
| Lambda default table names | CONTRACTS_TABLE default acdl-contracts |
default nova-contracts (D-111) |
P4 |
Unchanged conventions (out of scope)
- S&P Global Energy visual theme (
sp-theme.json, deck CSS: #D6002A red, Akkurat Pro) — client branding, not the Nova product brand (D-107). - config.json
release.gitea.repo=acdl— real Gitea repo name unchanged (D-105). Doc URLs updated tonovafor prose only. - Git branch/tag naming —
milestone/v*,phase/*,v*semver; no brand name present (D-112: flat-branch convention preserved). - Past Gitea release titles — existing releases keep
ACDL vX.Y.Z.
The full migration ordering (P1–P5), capability gate, and rollback runbook are preserved in
.ciagent/archive/ARCHITECTURE-v1.0-v1.24.md§v1.15 Addendum.
v1.17 Addendum — Strategic Direction, Leadership Metrics & Unified Story (current telemetry layer)
The v1.17 milestone added a telemetry/observability layer, a Decision Ledger, a metrics export pipeline, a unified narrative deck, and a durable strategic-direction artifact. This addendum documents the architecture; the full research findings are in RESEARCH.md §v1.17.
New components
| Component | Path | Purpose |
|---|---|---|
| Event envelope | core/metrics/event_envelope.py |
CloudEvents 1.0 envelope + platform.* semantic conventions (P1, REQ-187) |
| Per-run manifest writer | core/metrics/run_manifest.py |
Emits nova.run.started/completed/failed events + writes metrics/runs/<run_id>.json (P1, REQ-187) |
| Decision Ledger (SQLite) | core/metrics/decision_ledger.py |
Extends outbox_writer.py → SQLite append-only hash-chain table; ai.decision.made + attestation.recorded events + outcome backfill (P1, REQ-188, D-121) |
| Infracost post-processor | core/metrics/infracost_adapter.py |
Runs Infracost on plan JSON; emits nova.cost.estimated{delta_usd} (P1, REQ-187, D-120) |
| Metrics collector | core/metrics/collector.py |
Reads all grounded signals (files + events) → SQLite cold store at metrics/nova_metrics.db (P2, REQ-189) |
| PowerBI export | core/metrics/powerbi_export.py |
Emits CSV/JSON views to metrics/powerbi/ (fact + dim + 8 deferred placeholder views) (P3, REQ-190) |
| Metrics schemas | schemas/metrics_*.schema.json |
Schemas for all event types + fact/dim tables (P1–P2, REQ-187/189) |
| Metrics catalog | docs/METRICS.md + docs/metrics/<kpi>.md |
Canonical catalog + per-KPI definition-of-success docs (P4, REQ-195, D-127) |
| Unified narrative deck | docs/presentations/nova-no-humans-platform.md |
Merged deck: Problem→Vision→How→Proof→Roadmap; x3 arc at deck+slide level (P5, REQ-196/197, D-130) |
| Strategic direction | .ciagent/NORTH_STAR.md |
PO-authored durable vision/objectives/anti-goals/targets; read by CIAgent in every future /ci-run (P0, REQ-185/186) |
Modified components
| Component | Change | Phase |
|---|---|---|
core/outbox_writer.py |
Extended to emit to SQLite append-only hash-chain table (Decision Ledger); ai.decision.made + attestation.recorded events added (P1, D-121) |
P1 |
scripts/run_platform.sh |
Per-run manifest writer invoked; $WORK/*.json persisted to metrics/runs/; Infracost post-processor invoked after plan (P1) |
P1 |
core/hitl_gates.py |
Emits attestation.recorded event to Decision Ledger on qa/prod/dr gate (P1, D-132) |
P1 |
core/confidence_signal.py |
Emits nova.confidence.computed + nova.ai.decision.made events (P1, D-122) |
P1 |
adapters/terraform/policy/checkov_adapter.py |
Emits nova.policy.evaluated event (P1) |
P1 |
core/regression_verify.py |
Emits nova.capability.verified event; CAP-023 (metrics collector) + CAP-024 (deck structure) added (P1, P6) |
P1, P6 |
pyproject.toml |
addopts gains --junitxml=metrics/test-results.xml + --json-report (P1, D-120) |
P1 |
docs/presentations/ |
Two old decks retired (deleted); unified deck added (P5, D-130) | P5 |
Telemetry/observability layer architecture (D-120)
┌─────────────────────────────────────────────────────────────────────┐
│ Nova platform components (existing) │
│ run_platform.sh · confidence_signal · checkov_adapter · │
│ hitl_gates · regression_verify · outbox_writer · contract_ingestor │
└────────────────────┬──────────────────────────────────────────────┘
│ CloudEvents 1.0 envelope (new emitters, P1)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ metrics/events.jsonl (append-only CloudEvents log) │
│ metrics/runs/<run_id>.json (per-run manifests) │
│ metrics/decision_ledger.db (SQLite hash-chain, D-121) │
│ metrics/test-results.xml (junit, P1) │
└────────────────────┬──────────────────────────────────────────────┘
│ collector reads (P2)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ metrics/nova_metrics.db (SQLite cold store, D-126) │
│ fact_run · fact_capability · fact_policy_check · fact_confidence │
│ fact_test · fact_decision · fact_cost_estimate │
│ dim_capability · dim_milestone │
│ + 8 empty placeholder views (deferred metrics) │
└────────────────────┬──────────────────────────────────────────────┘
│ powerbi_export (P3)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ metrics/powerbi/ (CSV/JSON views, folder connector, D-129) │
│ → PowerBI dashboards (external) │
└─────────────────────────────────────────────────────────────────────┘
Hot path: deferred (D-126). No live ops dashboard; SQLite is cold-only (batch/historical). The hot path activates when live AWS is re-provisioned (D-096 lift — the v1.26 milestone lifts this for the pilot estate).
NORTH_STAR integration point (REQ-186)
.ciagent/NORTH_STAR.md is read by CIAgent in context-loading for all
future milestones. The integration mechanism: a reference from
PROJECT.md + ARCHITECTURE.md (this section) + a config entry in
config.json (strategic_direction_file: ".ciagent/NORTH_STAR.md")
that the run workflow reads at SPECIFY. This ensures the strategic
direction survives across milestones without being overwritten by status
updates.
§12.7 — Policy Engine Registry (v1.25, REQ-291 — current)
The policy-engine abstraction is first-class: a swappable PolicyEngine
protocol so the engine may change without touching the confidence
signal, the pipeline, or the PolicyCheckResult schema. This is the
swap boundary that keeps the platform's compliance posture
replaceable (Strategic Objective #2 — provable trust via a replaceable
substrate, not a vendor lock-in).
contract.yml ─┐ ┌─→ list[PolicyCheckResult] ─┐
stack IR ─────┼─→ PolicyEngine.evaluate ├─→ list[PolicyCheckResult] ─┼─→ confidence_signal
plan JSON ────┤ (protocol) └─→ list[PolicyCheckResult] ─┘ (engine-agnostic,
PCR list ─────┘ unchanged)
│
▼
┌─ KyvernoJsonEngine (shells to `kj scan`; engine: "kyverno")
└─ OpaEngine (future — same protocol; engine: "opa")
checkov/wiz ──→ raw findings ──→ (merged PCR list is the meta-policy payload)
The protocol (core/policy_engine.py):
class PolicyEngine(Protocol):
@property
def name(self) -> str: ...
def is_configured(self) -> bool: ...
def evaluate(self, payload, policy_dir: Path, contract_id: str) -> list[dict]: ...
The registry reads config.json.policy.engine (default
"kyverno-json") and returns the active engine. A NullEngine is the
fallback when the policy key is absent (emits SKIPPED PCRs —
backward compatibility for tests that don't set the key). The
confidence signal is untouched — it already consumes
list[PolicyCheckResult] engine-agnostically (§12.6). v1.25 only
changes who produces the PCR list, not what the list is.
Engine enum reuse (D-116): kyverno-json PCR records carry
engine: "kyverno" (no new enum value). The engine field records the
policy-engine family, not the specific binary. The K8s Kyverno adapter
and the kyverno-json engine are distinguished by ruleId prefix
(KYVERNO_ vs KJ_) and evidence payload shape (namespace/kind
vs assertion/jmespath).
Defense-in-depth (D-119): the declarative meta-policy
block-on-any-critical (asserts no PCR has severity: critical +
result: fail) is the source of truth for "critical = block". The
confidence_signal.py PENALTY["critical"]: None hard-override stays
as the imperative safety net — the meta-policy runs before the
confidence signal (produces PCRs that flow in), the hard-override runs
inside it (the last gate). Removing the hard-override would make the
"critical = block" guarantee depend on a single policy file — a
regression in provable trust.
Graceful degradation (D-120): KyvernoJsonEngine.is_configured()
returns false when which kj is absent → evaluate() returns a single
SKIPPED PCR (ruleId: "KJ_ENGINE_NOT_CONFIGURED"). The platform
functions without the binary (the "platform functions without AI /
deterministic scripts" tenet holds — kyverno-json is deterministic, not
AI; the is_configured() guard ensures the platform runs even when the
binary is not installed).
§12.8 — Pilot Estate (v1.26, live)
The first real consumer estate is nova-blockchain-exchange — a
blockchain stock exchange on a homegrown Proof-of-Authority chain,
equities only, dev only (D-020/D-200/D-201). The live apply landed on
2026-08-19 against AWS account 581513795199. This is the estate that
activated the Post-Pilot metric denominators (see docs/METRICS.md).
The live apply (run id blkex-pilot-apply-v0.2):
- Target: account
581513795199, environmentdev, autonomous (no HITL — dev is the only autonomous environment, confidence ≥ 0.50). - The microservice L2 composition (ECS Fargate running nginx) + the
dynamodbL1 (thenova-blkex-ledger-devtable) + thes3L1 (thenova-blkex-blocks-dev-581513795199-us-east-1bucket). - The platform VPC prerequisite (
vpc-0d7c8867e6cc080f1+ 6 subnets + the ECS SG) is read viaterraform_remote_state— the L2 composition does not own the network boundary (the "restricted from thin-composition" rule from §Layer 2). - Confidence signal: score 0.800, band pass;
human_overridefalse;escalation_reasonabsent (clean apply).
The Gitea adapter (SPEC §10 Q1): Gitea Actions does not support
cross-repo uses:, so the consumer's deploy.yml is an inline
adapter — actions/checkout@v4 the consumer, actions/checkout@v4
acdl/acdl @ ref: v1.25 into platform/, then
bash platform/scripts/run_platform.sh .... The platform's own
.github/workflows/deploy.yml stays as the GitHub Actions reference
impl (the reusable workflow_call workflow). See adapters/README.md
§Consumers for the adapter note.
The Decision Ledger evidence stream (the apply produces these events in order):
nova.confidence.computed (score 0.800, band pass)
│
▼
nova.ai.decision.made (decision_id blkex-pilot-apply-v0.2,
chosen_action pass, human_override false)
│
▼
nova.attestation.recorded (dev = no HITL gate; the record exists,
the gate is a no-op in the autonomous env)
│
▼
nova.run.completed (apply succeeded)
│
▼
nova.outcome.backfilled (outcome pending → succeeded, REQ-317;
backfilled_at 2026-08-19T03:05:04Z)
The SQLite hash-chain is valid (0 breaks). S3 Object Lock / JWS (D-083) stays deferred — the SQLite Decision Ledger is the pilot's audit record (D-204).
Live outputs (account 581513795199):
- ALB DNS:
app-254671247.us-east-1.elb.amazonaws.com - ECS service:
arn:aws:ecs:us-east-1:581513795199:service/nova-cluster/nova-microservice - DynamoDB table:
nova-blkex-ledger-dev(PKblock_index, PAY_PER_REQUEST) - S3 bucket:
nova-blkex-blocks-dev-581513795199-us-east-1(versioning + SSE)
The full evidence (every ARN, the confidence JSON, the Decision Ledger
rows, the module-completeness gaps the live apply uncovered) is in
.ciagent/archive/P4-PILOT-RUN-EVIDENCE-v1.26.md (archived v1.27).
§12.9 — Secret Rotation (v1.26 P3 W7, SPEC §5.9 — current)
The platform-managed scheduled workflow workflows-src/rotate-aws-key.yml
rotates the NOVA_AWS_* static key daily (cron 0 0 * * *) and on
workflow_dispatch. v0.2 scope: the mechanism exists (SPEC §5.9 —
exists-not-ran); the v0.2 deploy uses the currently-active key. The
rotation is idempotent — scripts/rotate_spike_key.sh deactivates the old
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.
§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-authLambda — sign-up, sign-in, session creation. Argon2id password hashing (D-228: bundled abi3 wheel; fail-closed onImportError, no pure-Python fallback). DynamoDB:nova-users(PKuser_id, Argon2idpassword_hash),nova-sessions(PKsession_id, TTLexpires_at),nova-password-resets(PKreset_token, TTL 15m). Function URL with IAM auth.nova-idp-token-vendLambda — accepts a PAT (or session token), validates revocation (nova-pats.GetItem(jti, ConsistentRead=True)— D-229, 60s SLO), evaluates the kyverno-json ABAC policy atplatform/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. Thepolicy_version(git SHA, D-231) is recorded in everytoken.vend.allowed/deniedaudit event.nova-idp-jwksLambda (optional, separation of concerns) — function URL withAuthType: NONE(public key only),Cache-Control: max-age=3600.kms.get_public_key→ DER SPKI → JWK viacryptography. Custom domain + WAF via CloudFront is OPTIONAL (--public-jwks-domainflag onnova idp setup, D-230).nova-patsDynamoDB table — PKjti, GSI1sub(list PATs for user), GSI2pat_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.pyauto-discoversnova/<module>.pysubcommands viapkgutil.iter_modules, dispatches, emits thecli.invocationaudit event (INV-12) withmode,selection_reason,credential_type,command,args. - Each
nova/<module>.pyis ≤50 lines, delegates tocore/(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 withhypothesis(REQ-349).core/env.py:+synthesize_local_env()— synthesizes a local env dict from a contract +--localflag (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-actioncomposite 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:
- Sign-up →
nova-idp-auth→ Argon2id →nova-usersPutItem → session →nova-sessionsPutItem → return session token. - Token vend (hot path) →
nova-idp-token-vend→nova-patsstrong read (revocation) → kyverno-json ABAC eval → if allow → KMS sign → DER→raw → return OIDC JWT. Audit at every step. - JWKS fetch →
nova-idp-jwks→kms.get_public_key→ DER→JWK →{"keys":[...]}. Cached 1h at CloudFront (if custom domain) / client. - 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. Authorspublish.yml+ the artifacts (Lambda zip, layer wheel, Python wheel, ECR container image with the statickjbinary). Each tagv1.29.xproduces 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 in581513795199. 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.