Files
acdl/.ciagent/ARCHITECTURE.md
T
Jon Chery f753353ad4 docs(P00): research findings — v1.25 kyverno-json engine surface, 4 policy targets, PolicyEngine swap boundary
RESEARCH.md: kyverno-json CLI (kj scan), ValidatingPolicy structure,
assertion trees + ~ modifier + JMESPath, output shape, severity-via-
annotation convention, 4 policy targets (contract/stack-IR/plan-JSON/
meta), PolicyEngine protocol + OPA-equivalent swap surface, latency
<1s (parallel with checkov), deterministic-not-AI tenet, ECS catalog
prior art, 5 logged assumptions (A1..A5).

PERSONAS.md: 4 active personas (lead-developer, backend-engineer,
new policy-engineer, data-engineer); frontend-engineer deactivated.
policy-engineer owns kyverno-json policies + engine translation +
STANDARDS.md policy-authoring section.

ARCHITECTURE.md §12.7: Policy Engine Registry — protocol, registry,
NullEngine fallback, engine enum reuse (D-116), defense-in-depth
critical-override (D-119), graceful degradation (D-120).

---ci---
project: acdl
phase: 0
milestone: v1.25
status: research
---/ci---
2026-08-12 18:09:12 +00:00

53 KiB
Raw Permalink Blame History

Nova — Architecture (v1.1 target)

Target architecture for the real Agentic Cloud Delivery Platform (rebranded Nova in v1.15). 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 and docs/vision.md conflict, the vision wins.

Status

Architecture is at v0.2 upstream (docs/architecture.md). Milestone v1.1 finalizes it to v1.0 in Phase 07 by resolving the 11 open decisions (see PROJECT.md open-decision resolutions table). This file records the locked commitments and the v1.1 spike scope.

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
  • inputs always optional; profile: agentic fields 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).

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).

Angine execution (§12) — the binding constraint

Target Stack IR (locked): a 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.

Angine adapters are the only engine-specific code. An adapter compiles the IR into a 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.

Terraform adapter (v1): translates IR-typed L1 interface → Terraform variable/output blocks; IR-typed L2 thin-composition tree → Terraform root module; IR-typed relationships → module references; emits a terraform plan from the IR. The adapter is a thin layer; it does not own L1/L2 content.

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.

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.1 spike scope

The spike (Phases 0810) materializes the minimum that proves the IR commitments hold (no polyglot mess):

  • One L1: l1-s3 (IR-typed interface; the only AWS resource in the spike).
  • One L2 thin-composition: l2-static-assets (references l1-s3 only).
  • Terraform adapter: IR → terraform plan against AWS via OIDC.
  • One contract submission → contract→IR → terraform plan → Checkov PolicyCheckResult → confidence signal → evidence event to the DynamoDB outbox.
  • State: S3 + DynamoDB (real AWS, single-region).

Out of spike scope: full HITL matrix wiring, Kyverno, OPA, MCP skill catalog, GitOps reconciler, multi-region, prod/dr environments, the 5-skill L3B catalog. Those are post-spike (v1.2+) platform build-out.

Gitea API surface (carried from v1.0, refined)

Capability Gitea support ACDL approach (v1.1)
Org-scoped repo create POST /api/v1/orgs/{org}/repos Used for any new repos
Native Pages None Serve acdl-evidence via raw file URLs (unchanged from v1.0)
Environments API None; act_runner ignores environment: Model HITL gates via workflow_dispatch approval inputs (v1.0 D-013 pattern) — refined in Phase 07 for the real pre-execution gate model
repository_dispatch Not supported Cross-repo trigger via workflow_dispatch API (unchanged)
Reusable workflows Supported acdl/.gitea/workflows/pipeline.yml via uses: ...@<ref>
id-token: write / OIDC Not supported (RESEARCH TARGET 1, conf 0.95). Gitea docs list id-token as an unsupported GitHub-only scope; open proposal go-gitea/gitea#33681; draft PR go-gitea/gitea#36988 unmerged. Even Gitea's own CI uses long-lived AWS keys (issue #37980). Spike waiver D-039: per-run-rotated long-lived key (rotated after each run by scripts/rotate_spike_key.sh). Real OIDC deferred to v1.2, blocked on PR #36988.
actions/configure-aws-credentials Unusable without OIDC Spike uses static AWS creds from a (rotated) Gitea Actions secret via the aws-actions/configure-aws-credentials@v4 access-key-id/secret-access-key inputs, or plain AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars. v1.2 switches to role-to-assume when OIDC lands.

Branch pinning rule (refined for W2.A)

  • Dev/qa contracts reference the reusable workflow by tag (@v1.1-spike).
  • Prod-bound workflows reference by SHA; the platform CLI (platform/cli/resolve-tag.ts, Phase 07) resolves the current tag to its SHA. (Spike scope: the CLI is a stub; the real CLI lands in v1.2.)

Verification toolchain

ACDL has no package.json. The verification gate substitutes:

  • typecheck: terraform validate, python3 -m py_compile, JSON Schema validation (ajv or python -m jsonschema) against schemas/.
  • test: per-phase scripts/verify_phaseNN.sh (Phase 06: archive integrity; Phase 07: schema validation + decision-resolution completeness; Phase 08: OIDC assume-role + state backend; Phase 09: IR + L1 + adapter terraform plan; Phase 10: end-to-end contract submission).
  • build: terraform init (real build for the spike).
  • See PERSONAS.md verification_toolchain.

Build order (v1.1)

  1. Phase 06 — archive demo, reorient repo.
  2. Phase 07 — finalize architecture v1.0; author schemas + designs.
  3. Phase 08 — AWS OIDC bootstrap (use temp key once, rotate).
  4. Phase 09 — IR + l1-s3 + Terraform adapter → terraform plan.
  5. Phase 10 — l2-static-assets + contract→IR → end-to-end spike.
  6. COMPLETE gate — review → ship v1.2.0 → audit. DONE.

v1.2 build-out scope

v1.2 takes the v1.1 spike (dev-only, plan-only, single S3 L1) to a real, simpler, better-documented platform that delivers a microservice to AWS ECS Fargate end-to-end. The locked architecture (§1–§12) is unchanged — v1.2 extends the implementation, not the design.

In scope (five axes, user-directed 2026-07-21)

  1. Re-evaluate the current state. go-gitea/gitea#36988 (OIDC for Gitea Actions) re-checked 2026-07-21: still open (last updated 2026-05-27, not merged). Real OIDC remains deferred to v1.3+; v1.2 extends the D-039 per-run-rotated-key waiver as D-047. The waiver continues to satisfy §12.5's intent (no persistently long-lived key): the spike key is rotated after each run by scripts/rotate_spike_key.sh, and Phase 12 tightens the IAM scoping + rotation hygiene.
  2. NFR improvements on the existing spike. Least-privilege IAM audit of spike_runner_policy.json; idempotent create_state_backend.py / create_iam_user.py; proper exit codes / error handling; P1-1 redaction (two AWS access key IDs in .ciagent/VERIFY.md Phase 09 narrative).
  3. Streamline / simplify the current setup. Consolidate run_spike_plan.sh + run_spike_e2e.sh into one scripts/run_platform.sh; remove dead code and stale platform/ paths.
  4. README.md fully up to date on how the platform works. Reflect v1.1 complete; document the actual spike flow, scripts/run_platform.sh, the real repo layout, and the v1.2 objective.
  5. Bootstrap a consumer repo with a basic microservice deployed to ECS end-to-end. New Gitea repo acdl-consumer-microservice (org continuous-intelligence); new IR-typed L1s (l1-vpc, l1-ecs-cluster, l1-ecs-service, l1-iam-role, l1-alb, l1-ecr); new l2-microservice thin-composition; one contract submission → terraform apply (dev, autonomous per §10, confidence ≥ 0.50) → a live ECS Fargate service serving HTTP 200 → evidence event to the DynamoDB outbox → acdl-evidence timeline.

Angine extension (ECS Fargate)

The Terraform adapter (§12) remains the only engine-specific code. v1.2 expands the adapter TYPE_MAP to cover the six new ECS-shaped IR resource types. The L1 interface shape (IR-typed inputs/outputs/NFRs, registered in modules-ir/registry.json) is unchanged — only the set of registered L1s grows. The IR commitments (REQ-28) continue to hold: modules-ir/, schemas/, contracts/, core/confidence_signal.py, core/contract_resolver.py, core/outbox_writer.py remain engine-agnostic.

terraform apply (dev only)

v1.2 lifts the engine execution from plan to apply for the dev environment only. Dev is autonomous per §10 (confidence ≥ 0.50, no HITL). apply for qa/prod/dr remains HITL-gated and out of scope for v1.2. The apply result (resources created, plan diff) is captured in the evidence stream as a terraform.apply event.

Out of scope for v1.2 (deferred to v1.3+)

Feature Reason
Real OIDC federation go-gitea/gitea#36988 still open. v1.2 extends D-039 waiver (D-047); real OIDC is v1.3+.
Full HITL matrix wiring (qa/prod/dr) v1.2 is dev-only autonomous apply; HITL wiring is v1.3.
Kyverno + OPA policy engines v1.2 keeps Checkov only; Kyverno/OPA are v1.3.
MCP skill catalog + real L3B agent v1.2 keeps the L3B stub; the 5-skill catalog is v1.3.
Audit ledger build-out (S3 Object Lock + JWS + async worker + DLQ + daily checkpoints) v1.2 keeps the v1.1 outbox; the regulatory ledger is v1.3.
Multi-region state / outbox Single-region in v1 (§9, §12.3); multi-region is v1.3+.
Prod/dr environments v1.2 is dev-only; prod/dr are v1.3.
GitOps reconciler (ArgoCD/Flux) v1.3+.

Build order (v1.2)

  1. Phase 11 — re-eval #36988 + NFR audit + simplification findings + README rewrite.
  2. Phase 12 — NFR harden + simplify (idempotent bootstrap, one run_platform.sh, IAM audit, redactions).
  3. Phase 13 — six ECS L1s + adapter TYPE_MAP expansion.
  4. Phase 14 — l2-microservice + contract schema extension.
  5. Phase 15 — consumer repo + terraform apply (dev) → live ECS service.
  6. Phase 16 — capstone e2e: consumer commit → live HTTP 200 → evidence → timeline.
  7. COMPLETE gate — review → ship v1.3.0 → audit.

v1.8 Architecture Addendum

Milestone v1.8 (complete, tag v1.8.0). Adds encryption-by-default, deletion-protection-by-default, uptime monitoring, decommission alias, engineering standards, and path documentation.

New Primitives

  • kms-key (aws:kms:key) — Per-stack customer-managed KMS key with enable_key_rotation = true. One key per L2 deployment (no shared keys). Wired into both L2 compositions as a child, with its kms_key_arn output connected to all children's kms_key_arn input. Adapter emits aws_kms_key + enable_key_rotation.
  • uptime (aws:ecs:uptime-service) — Uptime-kuma on ECS Fargate with a feature flag (feature_flag_enabled), monitored endpoints (HTTP/DNS/TCP), alert channels (Teams/email/SMS/GitHub issues). Deployed by default after any L2 module with a separate terraform state. When the feature flag is false, the adapter emits no resources.

Encryption by Default

All 12 L1 primitives have encryption_enabled NFR (default true). Primitives with at-rest data (s3, rds, ecr, ecs-service, ecs-cluster) have an optional kms_key_arn input. The adapter emits encryption blocks (SSE-KMS for S3, storage_encrypted for RDS, encryption_configuration for ECR) referencing the per-stack CMK when provided. Managed KMS fallback with stderr warning for standalone L1 deployments.

Deletion Protection by Default

All 12 L1 primitives have deletion_protection NFR (default true). The adapter emits lifecycle { prevent_destroy = true } when true. L2 modules expose a features.deletion_protection flag (default true) propagated to all children via the resolver. Setting inputs.deletion_protection: false in the contract disables it for the whole stack.

Decommission Alias

A mode: decommission on the deploy pipeline implements a 2-step destroy:

  1. Disable deletion protection (resolve with deletion_protection: false, terraform plan/apply, HITL SRE gate via GitHub environment).
  2. Zero counts + destroy (decommission_transform zeroes all scalable counts, terraform plan/apply, second HITL SRE gate).

CMDB validation via DynamoDB acdl-change-requests table. The Lambda validate_change_request action queries the table and asserts status == "approved" + consumerRepo match.

Adapter Expansion

TYPE_MAP grew from 16 to 19 entries (+ aws:kms:key, aws:kms:alias, aws:ecs:uptime-service). Specialized emission branches added for KMS key rotation, S3 SSE-KMS configuration, uptime ECS Fargate task, and prevent_destroy lifecycle on all resources.

Pipeline Stages

The deploy pipeline grew from 8 to 9 stages (+ deploy-uptime after publish-outputs). The deploy-uptime stage constructs a synthetic uptime contract from the L2 stack outputs, resolves + adapts it to a separate terraform state directory, and publishes the uptime URL via PR comment.

Forge-Agnostic API URLs

The platform Lambda (contract_ingestor.py) reads GITHUB_API_BASE env for forge-agnostic API URLs. GitHub uses /search/issues; Gitea uses /repos/{owner}/{repo}/issues. Detection via /api/v1 in the base URL.

v1.9 Addendum (2026-07-23)

New Components

  • core/contract_resolver.py interpolation (D-081): the resolver now expands ${env.<field>} + ${contract.<field>} tokens post-schema-validation, pre-IR-resolution. The env context is the loaded environment onboarding JSON (core/environments/<name>.json, schema schemas/environment.schema.json). The resolver's child_input_map routes L2 wires to the sub-resource that declares the input (P1-1 — desired_countaws:ecs:service, familyaws:ecs:task_definition).
  • core/environment_check.py load() (REQ-104): loads + returns the parsed environment JSON; emits a stderr warning for placeholder account_id when env != dev.
  • core/hitl_gates.py (REQ-108, D-084): the HITL pre-execution attestation gate. Records the approver identity to the DynamoDB outbox (approver_qa/approver_prod/approver_dr), runs the separation-of- duties check on prod, invokes the attestation matrix, returns (ok, reason). Dev skips (autonomous). run_platform.sh calls attest before apply for qa/prod/dr.
  • core/attestation_matrix.py (REQ-109, D-084): the 8-concern attestation matrix from hitl_matrix_design.md §10.4. Offline-testable concerns (contract NFRs, schema validity, policy pass) run for real; operator-supplied concerns accept signed evidence artifacts validated for freshness + schema. Signature verification skips when ACDL_ATTESTATION_SIGNING_KEY_ID is unset (D-089).
  • core/separation_of_duties.py route_halt_artifact (REQ-107): real SNS publish (acdl-sod-halt topic, ARN from ACDL_SOD_HALT_TOPIC_ARN) + outbox fallback (SEPARATION_OF_DUTIES_VIOLATION event). The SNS topic is defined in terraform/platform/main.tf.
  • adapters/wiz/wiz_adapter.py WizClient (REQ-110): real GraphQL API client (<WIZ_API_URL>/graphql, Bearer auth, pagination via pageInfo.hasNextPage). fetch_and_adapt translates issues → PolicyCheckResult. Graceful degrade when unconfigured.
  • adapters/kyverno/kyverno_adapter.py (REQ-111): fleshed-out PolicyReportPolicyCheckResult mapping (pass/fail/skip/warn + severity + skip-with-reason + resource construction). Inactive-for-TF guard preserved.

Per-Environment Promotion (D-082)

The deploy workflow (.github/workflows/deploy.yml + .gitea/workflows/deploy.yml, byte-identical) declares an environment workflow_call input. When non-empty, run_platform.sh --environment <name> overrides the contract's environment field before schema validation (D-088). One CI job per environment; promotion = running the matching job, no environment: field editing. Per-env contract files (contracts/<module>.<env>.yaml) use interpolation for env-specific values.

Adapter Parameterization (P1-1, D-085)

The adapter (adapters/terraform/adapter.py) reads ECS/ALB/VPC defaults from L1 interface.json inputs (desired_count, launch_type, family, target_type, load_balancer_type, name). The adapter is a thin translator; the child_input_map routes wires to the declaring sub-resource.

Deferred (D-083)

S3 Object Lock + JWS detached signatures + async worker + DLQ + daily checkpoints (audit ledger build-out) — deferred to a future milestone. The hash-chain + DynamoDB-outbox path remains the v1.9 production audit record.

v1.10 Addendum — Regression VERIFY + Local Emulators + Capability Re-Verification

Regression-Class VERIFY (D-091, core/regression_verify.py)

The standard VERIFY stage was diff-scoped (it checked the phase diff only, never re-ran underlying capability). This let 8 NFR-patch phases (v1.9.1v1.9.8) pass while the platform decayed. The regression-class VERIFY (core/regression_verify.py) re-runs capability checks against the current codebase and tags each Verified/Decayed/Broken. It fails closed on any non-Verified capability, blocking milestone completion.

The registry (CAPABILITY_REGISTRY) holds 16 capability checks (CAP-001..CAP-016): 12 local-tier + 4 live-AWS. Adding a capability is a single function + one registry entry. The gate runs via scripts/run_regression.sh and writes .ciagent/REGRESSION_REPORT.md

  • .json.

Local Emulating Adapters (D-092, core/local_emulators.py)

Four local adapters let the platform run the full headline E2E without cloud credentials:

  • FlatFileOutbox — flat-file DynamoDB outbox emulator (hash-chained JSONL; resumable across instances; chain verification).
  • LocalEcsEmulator — local ECS Fargate HTTP 200 emulator (binds port 0 on 127.0.0.1; daemon thread; clean destroy).
  • LocalS3StateBackend — rewrites the terraform S3 backend to a local backend (per-stack tfstate in a temp folder).
  • LocalLambdaStub — invokes the contract_ingestor handler in-process (patches _get_dynamodb/_get_secrets_client/urllib.urlopen; DynamoDB writes redirected to the FlatFileOutbox).

run_local_e2e() runs the full pipeline: contract → resolver → adapter → local S3 backend → local ECS (HTTP 200) → flat-file outbox (chain verified) → local Lambda (200). Gated on ACDL_LOCAL_TIER=1.

Capability Re-Verification Sweep (D-093)

.ciagent/CAPABILITY_INVENTORY.md enumerates 16 auto-verified capabilities + 6 IAM-gated escalated resources. The sweep found and fixed 7 adapter defects in adapters/terraform/adapter.py (duplicate outputs, duplicate args, missing required args, deprecated AWS provider v5 arg names). The headline E2E now passes at both tiers: local emulator + live-AWS terraform init/validate/plan.

Adapter Defect Fixes (P54)

7 defects fixed in adapters/terraform/adapter.py:

  1. Duplicate output definitions (per-resource + stack-level both emitted).
  2. Duplicate desired_count/launch_type on ECS service.
  3. Duplicate target_type/family/load_balancer_type.
  4. Missing assume_role_policy/role_name on IAM role (L2 composition gap).
  5. Missing cidr_block/vpc_id/name defaults on VPC/subnet/route_table/ ECS cluster/ECR repository.
  6. ECR kms_key_arn unsupported arg → encryption_configuration block.
  7. CloudFront OAC + WAF deprecated arg names (AWS provider v5): signing_behavior, signing_protocol, origin_access_control_id, s3_origin_config.origin_access_identity, origin_id, rule (singular), scope=CLOUDFRONT (uppercase).

v1.11 Addendum — Stateless Adapter + Pipeline-Driven Lifecycle Testing

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. (P2P4 dual-read fallback to ACDL_LIFECYCLE_MODE; fallback removed in P5 per the v1.15 addendum.)

v1.12 Addendum — Presentation Refinement + CAP-013 Fix

CAP-013 adapter dedup fix (REQ-129). Multi-resource L1s (ecs-service, alb) with stack outputs + cross-module refs now dedup to ONE module block named by the composition child id, with expanded sub-ids rewritten via id_remap. terraform validate succeeds for the microservice stack.

CAP-017/018 probe fixes (REQ-130). CAP-017's probe no longer requires locals.tf for modules that legitimately omit it. CAP-018's probe instantiates LocalLambdaStub with the required outbox arg.

v1.13 Addendum — Presentation Polish + Config Schema Migration

Config.json schema migration (v1.13.1). Regenerated .ciagent/config.json to the updated CIAgent v2 config structure (drop removed fields, migrate gitearelease.gitea, add secrets/ship/backend/ideation/personas/logging/telemetry sections).

Presentation polish (v1.13.0, v1.13.2). Action headlines, story-arc restructure, larger fonts, 6 new mermaid diagrams, badge cleanup, platform-architecture diagram. Docs-only NFR patches.

v1.14 Addendum — NFR Refinement (bug fixes, security, stubs, tests, docs)

Bug fixes (Wave 1, P1-P6). Adapter dedup rejects unregistered modules with ValueError (P1). Static-assets composition wires cloudfront inputs (P2). L2 lifecycle scripts document remote-state design (P3). Regression gate adds terraform fmt -check syntax probe (P4). Adapter dedup-merge + remote-state-key unit tests (P5). ALB target group name_prefix derives from var.name (P6).

Security (Wave 2, P7-P12). 6 swallowed-error sites narrowed to specific exceptions (P7). Account ID externalized to ACDL_AWS_ACCOUNT_ID env (P8). IAM policy scoped to acdl-* ARNs (P9). Contract ingestor validates contractId/environment/error (P10). Environment schema adds additionalProperties: false + format validation (P11). .gitignore credential-pattern catch-all (P12).

Stub/test/CI/hygiene (Wave 3, P13-P17). Kyverno --kube-version flag removed (P13, G-103). Orphan artifacts + dead config cleaned (P14). 7 untested scripts gain test coverage (P15). Gitea workflow parity documented + script set flags fixed (P16). Config.json persona + branching strategy + ollama-cloud aligned (P17).

Standards/docs/VPC (Wave 4, P18-P20). STANDARDS.md reconciled (P18). Documentation synced: ARCHITECTURE.md addenda, stale @v1.6-1.9@v1.13, GRILL G-005/G-008 resolved, COST.md window extended, D-083 deferral recorded (P19). Platform VPC CIDR parameterized + data-driven subnet count (P20).

D-083 deferral (explicit). The audit ledger build-out (S3 Object Lock

  • JWS detached signatures + SQS DLQ + async worker + daily checkpoints) remains deferred (D-096, v1.14). The hash-chain + DynamoDB outbox is the v1.14 audit record. JWS per-event authenticity is not implemented; a forged event is only detectable by re-reading the whole chain. The deferral is documented here explicitly per the v1.14 grill (E-001).

v1.15 Addendum — Nova Rebrand (Major/breaking, 2026-07-30)

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. Per the branch-strategy precedent (breaking/feature milestones tag on their OWN minor line), 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)

Convention Before (v1.0v1.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 P2P4; 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 to nova for prose only.
  • Git branch/tag namingmilestone/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.

Migration ordering (binding)

  1. P1 docs/decks/prose — no runtime impact; ships consumer migration guide announcing the 5 breaking changes.
  2. P2 code + env vars (dual-read) + consumer path — deployments don't break during the transition window (dual-read fallback).
  3. P3 SSM path (copy → read → delete) + tag keys (parallel-tag → policy swap → remove old).
  4. P4 AWS resource names — staged terraform migration (KMS alias, SNS/SG/Lambda recreate, DynamoDB scan+copy, ECR re-push, IAM re-bootstrap, state bucket -migrate-state, ALB recreate). Maintenance window + rollback runbook (docs/NOVA_AWS_MIGRATION.md).
  5. P5 final review + audit + remove dual-read fallback + milestone ship.

Capability gate (binding)

The regression gate (CAP-001..CAP-016, scripts/run_regression.sh) must stay 16/16 Verified throughout the rebrand. P2/P3/P4 update test fixtures that reference ACDL/acdl so the gate stays green. No capability is added, removed, or reclassified in v1.15 — the rebrand is nomenclature + identifiers, not behavior.


v1.16 Addendum — Nova Simplification (NFR, 2026-07-30)

The v1.16 NFR milestone added 6 new code components + 1 new Terraform module + 1 new schema, all documented here for the architecture record.

New components

Component Path Purpose
Onboarding request handler core/onboarding.py generate_env_file(request, template_env) — produces a <env>.json from a consumer onboarding request (P19, REQ-183). CLI entry point for self-service env-file generation.
Decommission transform core/decommission_transform.py decommission_transform(stack) — zero counts + disable deletion protection (REQ-92). Extracted from contract_resolver (P12, REQ-176).
Contract resolver CLI core/contract_resolver_cli.py main() CLI entry point — resolves a contract YAML to a Target Stack JSON. Extracted from contract_resolver (P12, REQ-176).
Regression verify CLI core/regression_verify_cli.py main() CLI entry point — runs the regression gate + writes the report. Extracted from regression_verify (P13, REQ-177).
Workflow sync generator scripts/sync_workflows.py --check/--write — generates the 3 byte-identical Gitea+GitHub workflow pairs from workflows-src/ (P8, REQ-172).
Onboarding Terraform terraform/onboarding/ aws_iam_role.consumer_deploy + aws_iam_role_policy.consumer_invoke (ABAC nova:owner tag). Offline-proven only (P20, REQ-184, D-114).

Modified components

Component Change Phase
core/contract_resolver.py _load_env delegates to environment_check.load() (dedup); is_l2 uses registry kind field; _load_schema caches schemas; decommission_transform + CLI re-export shim (P12). P7, P12, P14
core/regression_verify.py Dedup helpers (_check_resolver, _check_live_terraform_plan, _assert_contracts_resolve); CAP-013..016 Skipped on post-teardown (G-111); passed accepts Skipped; CLI re-export shim (P13). P5, P9, P13
core/lambda/contract_ingestor.py Fail closed on missing IAM identity (P10); env enum from core/environments/ (P10); payload size cap + schema validation (P11); onboard_consumer action (P18); [NOVA-ALERT] rebrand (P2). P2, P10, P11, P18
core/output_publisher.py SAFE_OUTPUT_NAMES schema-driven from interface.json; narrowed excepts; urllib.error import (P4, P14). P4, P14
core/environment_check.py Onboarding message rebranded Nova + self-service request path (P2, P19). P2, P19
core/local_emulators.py LocalLambdaStub sets NOVA_LAMBDA_LOCAL_BYPASS; stale dual-read comments + acdl_* prefixes removed (P3, P10). P3, P10
scripts/run_platform.sh --help flag; run_hitl_gate() fn; NOVA_CONTRACT_ID/NOVA_WORK_DIR config; decommission + uptime blocks extracted to sourced helpers (P6, P9, P15). P6, P9, P15
adapters/terraform/adapter.py State bucket nova-tfstate-* (P1); module docstring Nova (P2). P1, P2
adapters/kyverno/policies/require-resource-labels.yml nova:* labels (not acdl:*) (P1). P1
modules/registry.json kind field (l1/l2) on all 14 entries (P7). P7

New schema

  • schemas/onboarding.schema.json — the self-service onboarding request (consumerRepo, requestedEnvironment, ownerId, billingTag). P18, REQ-182.

Onboarding request-path architecture (D-113)

The no-humans onboarding flow is a 3-step request path (real AWS provisioning deferred):

Consumer → POST Lambda (onboard_consumer) → pending CMDB row (P18)
         → core/onboarding.py → <env>.json binding file (P19)
         → terraform/onboarding/ → cross-account role + ABAC tag (P20, offline)

The Lambda Function URL (IAM auth) + consumer_invoke_policy.json (ABAC nova:owner) are the transport; the request is accepted + a binding generated + the role Terraform proven offline. No AWS resources are created by the request path (D-113/D-114).

Regression gate (G-111 binding)

The regression gate (D-091) now treats Skipped as acceptable for the post-v1.11-teardown steady state (D-096): CAP-013..016 (live-AWS tier) return Skipped when the resources are absent (NoSuchBucket/ ResourceNotFoundException). RegressionReport.passed is all(r.status in ("Verified", "Skipped")). The gate passes at 18 Verified + 4 Skipped (0 Decayed/Broken).

v1.17 Addendum — Strategic Direction, Leadership Metrics & Unified Story (2026-08-04)

The v1.17 milestone adds 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 (P1P2, 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).

NORTH_STAR integration point (REQ-186)

.ciagent/NORTH_STAR.md is read by CIAgent in context-loading for all future milestones. The integration mechanism (to be finalized in P4): 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)

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).