Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78da051b60 | |||
| ddf88202fc | |||
| 2ee541f40e | |||
| d391cdf0f7 | |||
| cf8aa53c8d | |||
| 270b1f11a3 | |||
| 2a4d7b7625 | |||
| 707d8a1e39 | |||
| 50e77e6314 | |||
| a0a658bc9a | |||
| f844feab7f | |||
| 8c68d683c6 | |||
| be967783b4 | |||
| 730109dd0c | |||
| 78688b968c | |||
| 7e98debd70 | |||
| 255cde5002 | |||
| 2cc76f4f94 | |||
| 9acf23926d | |||
| b41e24e068 | |||
| ad522e6bf7 | |||
| 38b51f3e6d | |||
| 89f62c85ab | |||
| 96d4677fac | |||
| 863484e681 | |||
| 7f4b79593a | |||
| 35e3de401e | |||
| 814d45b211 | |||
| 0f0d9b9145 | |||
| e6ee79402b | |||
| 4b6c3a12d8 | |||
| 56dab4fdfb | |||
| ed387a4f54 | |||
| ac18c98385 | |||
| ba816f69ae | |||
| 2e519743b5 | |||
| 36c8ae9a80 | |||
| ec53302014 | |||
| 7e6ed25ea9 | |||
| f753353ad4 | |||
| f020178c15 | |||
| 5a75075616 | |||
| 42c579f7b8 | |||
| ab7171236a | |||
| fe635c17d5 | |||
| d069654367 |
@@ -879,3 +879,67 @@ 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`):**
|
||||
```python
|
||||
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).
|
||||
|
||||
+19
-15
@@ -1,19 +1,23 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "complete",
|
||||
"milestone": "v1.24",
|
||||
"phase_role": "pre_execution",
|
||||
"phase": 1,
|
||||
"stage": "execute",
|
||||
"milestone": "v1.26",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-12T02:20:00Z",
|
||||
"project": "acdl",
|
||||
"milestone_complete": false,
|
||||
"tag": "v1.23.0",
|
||||
"tag_line": "v1.23.x",
|
||||
"release": {
|
||||
"forge": "gitea",
|
||||
"release_id": 635,
|
||||
"tag": "v1.23.0"
|
||||
"updated_at": "2026-08-13T18:35:00Z",
|
||||
"project": "nova-blockchain-exchange",
|
||||
"projects": ["acdl", "nova-blockchain-exchange"],
|
||||
"active_milestone": "v1.26",
|
||||
"milestone_branch": "milestone/v1.26-pilot-activation",
|
||||
"phase_branch": "phase/01-blockchain-core",
|
||||
"tag_line": "v1.25.x",
|
||||
"previous_phase": {"phase": 0, "tag": "v1.25.0", "release_id": 690, "status": "complete"},
|
||||
"requirements": ["REQ-310", "REQ-311", "REQ-312"],
|
||||
"pre_run": {
|
||||
"flaky_test_fixed": "8c68d68 test(metrics): fix attestation-event test freshness time-bomb",
|
||||
"acdl_to_nova_migration": "f844fea chore(bootstrap): migrate ACDL_* env vars to NOVA_*",
|
||||
"aws_bootstrap": "S3 nova-tfstate-581513795199-us-east-1 + DynamoDB nova-outbox created (idempotent, account 581513795199)",
|
||||
"consumer_repo_created": "continuous-intelligence/nova-blockchain-exchange (Gitea, private, init, cloned to /root/nova-blockchain-exchange)"
|
||||
},
|
||||
"requirements": ["REQ-276","REQ-277","REQ-278","REQ-279","REQ-280","REQ-281","REQ-282","REQ-283","REQ-284","REQ-285","REQ-286","REQ-287","REQ-288","REQ-289","REQ-290"],
|
||||
"phases": ["P1:consumer-guide-fixes","P2:env-transition-detect-and-destroy","P3:env-transition-tests","P4:final-review-ship"]
|
||||
"notes": "v1.26 P1 execute. Blockchain core + order engine + settlement in consumer repo. Persona: blockchain-engineer."
|
||||
}
|
||||
+202
-85
@@ -1,109 +1,226 @@
|
||||
# CLARIFY — v1.24 Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement
|
||||
# CLARIFY — v1.26 Live Pilot Estate Activation
|
||||
|
||||
> **Autonomy:** full. Ambiguities are auto-resolved with assumption logging
|
||||
> per `config.json autonomy.level: "full"`. No human escalation.
|
||||
> **Autonomy:** full. Auto-resolution with assumption logging per
|
||||
> `config.autonomy.level: "full"`. No human escalation unless
|
||||
> confidence < 0.60 (threshold `config.autonomy.decision_confidence_threshold`).
|
||||
> 10 ambiguities identified; all resolved (confidence ≥ 0.60).
|
||||
|
||||
## Ambiguities Identified
|
||||
---
|
||||
|
||||
### A1 — Step 8 "change environment" vs Per-env section "no field editing"
|
||||
## Method
|
||||
|
||||
**Ambiguity:** The consumer guide contains two mutually-exclusive promotion
|
||||
models. Step 8 (line 290) says "Change `environment` in your contract." The
|
||||
"Per-environment deployment" section (line 398) says "you do not edit the
|
||||
`environment:` field… Promotion = running the matching job." The test
|
||||
`test_consumer_guide_states_no_field_editing` asserts the no-editing model.
|
||||
The clarify stage identifies ambiguities in the v1.26 specification
|
||||
(PROJECT.md, REQUIREMENTS.md, ROADMAP.md) and resolves them at full
|
||||
autonomy. Each ambiguity gets a decision ID (D-200+; continuing from
|
||||
the v1.26 SPECIFY decisions D-200..D-205), a resolution, a confidence
|
||||
score, and a rationale. Resolutions update PROJECT.md + REQUIREMENTS.md
|
||||
+ ROADMAP.md as needed.
|
||||
|
||||
**User directive (binding):** Both shapes are supported. Shape A (edit
|
||||
environment in-place) is valid AND must trigger a destroy of the prior env.
|
||||
Shape B (per-env caller workflows) is the alternative. The test must be
|
||||
updated to assert both shapes.
|
||||
---
|
||||
|
||||
**Resolution (auto, confidence 0.95):** Adopt the user's directive. Step 8
|
||||
is rewritten to document Shape A with destroy-then-rebuild semantics. The
|
||||
per-env section is preserved as Shape B with a lead sentence distinguishing
|
||||
it. The test is renamed and a new test asserts the destroy semantics. This
|
||||
is already captured in REQ-279, REQ-280, REQ-290.
|
||||
## Ambiguities + Resolutions
|
||||
|
||||
### A2 — Prior-env source of truth: DynamoDB vs state-bucket scan vs SSM
|
||||
### Q1 — Does the consumer repo's `.ciagent/` live in the platform repo or the consumer repo?
|
||||
|
||||
**Ambiguity:** Three options for detecting the prior environment: (a) query
|
||||
the `nova-contracts` DynamoDB table, (b) scan the state bucket for other env
|
||||
prefixes, (c) record last-applied env in an SSM parameter.
|
||||
**Ambiguity:** The user said "ciagent should track it as a separate
|
||||
project under this same path." Does "this same path" mean the platform
|
||||
repo's `.ciagent/` directory (multi-project mode per `run.md` Step 0),
|
||||
or a separate `.ciagent/` inside the consumer repo?
|
||||
|
||||
**Resolution (auto, confidence 0.85):** DynamoDB `nova-contracts` table
|
||||
(user-selected). It already exists, is written by the contract ingestor
|
||||
Lambda (`core/lambda/contract_ingestor.py:160-170`), and has the right shape
|
||||
(PK `consumerRepo`, SK `contractId#submittedAt`, `environment` attribute).
|
||||
A new `#LAST_APPLIED` SK suffix is added for the record-applied-env step
|
||||
(REQ-283). This avoids coupling the platform to a specific state-bucket
|
||||
layout (which differs across envs/accounts) and avoids a new SSM dependency.
|
||||
**Resolution:** The platform repo's `.ciagent/` directory. Multi-project
|
||||
mode: `.ciagent/config.json` `projects[]` includes both `acdl` +
|
||||
`nova-blockchain-exchange`; the consumer's project files
|
||||
(PROJECT.md, REQUIREMENTS.md, ROADMAP.md) live in
|
||||
`.ciagent/nova-blockchain-exchange/`. The consumer *git repo* owns the
|
||||
app code + `contract.yaml` + deploy workflow invocation; the platform
|
||||
repo owns the CIAgent planning artifacts for both projects. This
|
||||
matches `run.md` Step 0 multi-project mode.
|
||||
|
||||
**Assumption:** The `nova-contracts` table is accessible from the deploy
|
||||
role via the same ABAC scoping that the contract ingestor uses. If the
|
||||
table is not accessible (e.g., local/CI mode without DynamoDB), the detect
|
||||
step logs a warning and returns `None` (conservative — no prior env
|
||||
assumed). This is documented in REQ-282.
|
||||
**Confidence:** 0.95. **Decision:** D-206.
|
||||
|
||||
### A3 — Cross-account destroy
|
||||
### Q2 — Is the bootstrap `NOVA_AWS_*` key the root key or the spike-runner key?
|
||||
|
||||
**Ambiguity:** If the prior env (e.g., dev) and new env (e.g., qa) are in
|
||||
different AWS accounts, the destroy step needs the prior env's role
|
||||
credentials. The current scaffold uses one account.
|
||||
**Ambiguity:** The bootstrap scripts (post-migration) prefer
|
||||
`NOVA_BOOTSTRAP_AWS_*`, falling back to `NOVA_AWS_*`. The pre-run
|
||||
(A3) succeeded with `NOVA_AWS_*`, creating the S3 bucket + DynamoDB
|
||||
table — which requires root or root-equivalent IAM. Is `NOVA_AWS_*`
|
||||
the root key, or did the bootstrap succeed because the spike-runner
|
||||
policy happens to include S3/DynamoDB create?
|
||||
|
||||
**Resolution (auto, confidence 0.80):** v1.24 targets the same-account
|
||||
case. Cross-account destroy is explicitly out of scope (documented in the
|
||||
Out of Scope section). The `run_platform.sh` Step 0b notes this limitation.
|
||||
A future milestone handles cross-account destroy via a pre-step that
|
||||
assumes the prior env's role. This is the pragmatic path — the scaffold
|
||||
(`core/environments/dev.json`) is single-account today.
|
||||
**Resolution:** `NOVA_AWS_*` has root-equivalent permissions (confirmed
|
||||
empirically: the bootstrap created the S3 bucket + DynamoDB table
|
||||
successfully). For the pilot, `NOVA_AWS_*` is the bootstrap key. A
|
||||
future hardening milestone should split this into a dedicated
|
||||
`NOVA_BOOTSTRAP_AWS_*` root key + a least-privilege `NOVA_AWS_*` runner
|
||||
key (the spike-runner pattern). For v1.26, the single key suffices
|
||||
(pilot scope).
|
||||
|
||||
### A4 — Version tag in docs: `@v1.19` vs `ref: v1.9`
|
||||
**Confidence:** 0.90. **Decision:** D-207.
|
||||
|
||||
**Ambiguity:** The consumer guide says `uses: nova/.github/workflows/deploy.yml@v1.19`
|
||||
but the actual `.github/workflows/deploy.yml` checks out the platform repo
|
||||
at `ref: v1.9`. The reference table says sample contracts "use `@v1.19`"
|
||||
but the sample contracts don't carry `uses:` (they're contracts, not
|
||||
workflows).
|
||||
### Q3 — Which AWS account does the pilot use: `581513795199` (existing) or a dedicated pilot account?
|
||||
|
||||
**Resolution (auto, confidence 0.90):** REQ-281 corrects the reference
|
||||
table wording to "used with caller workflow `@v1.19`" (the version pin
|
||||
lives in the caller workflow, not the contract). The `@v1.19` tag in the
|
||||
consumer-facing docs is the documented current version; the `ref: v1.9` in
|
||||
deploy.yml is the platform-internal checkout ref. These are two different
|
||||
references (consumer → platform workflow tag; platform workflow → platform
|
||||
repo ref). The guide's `@v1.19` stays as the consumer-facing version. No
|
||||
change to deploy.yml's `ref: v1.9` (that's an internal platform concern,
|
||||
out of scope for this milestone).
|
||||
**Ambiguity:** The user said "assume 581513795199." But the env JSONs
|
||||
all show `account_id: "000000000000"` (placeholder). Does the pilot
|
||||
bind all env JSONs to `581513795199`, or only `dev` (with qa/prod/dr
|
||||
left placeholder until a real multi-account landing zone exists)?
|
||||
|
||||
### A5 — Should Shape A destroy go through the HITL decommission pipeline?
|
||||
**Resolution:** Bind `dev` to `581513795199` for the pilot
|
||||
(D-203, established in SPECIFY). The `qa`/`prod`/`dr` env JSONs remain
|
||||
placeholder `000000000000` this milestone — the pilot runs in `dev`
|
||||
(autonomous, no HITL gate). Multi-account landing zone (qa/prod/dr on
|
||||
separate accounts) is a future milestone. REQ-319 (env-JSON wiring)
|
||||
updates `dev.json`'s `state_backend.bucket` to
|
||||
`nova-tfstate-581513795199-us-east-1` + `account_id` to `581513795199`;
|
||||
qa/prod/dr get the `state_backend.bucket` update but keep placeholder
|
||||
`account_id` (the pilot-readiness policy REQ-320 blocks apply on
|
||||
placeholder accounts — so qa/prod/dr apply is blocked by design until
|
||||
the accounts are bound).
|
||||
|
||||
**Ambiguity:** The decommission pipeline (2-step, HITL SRE gates) exists for
|
||||
stack teardown. Should env-transition destroy use it?
|
||||
**Confidence:** 0.92. **Decision:** D-208.
|
||||
|
||||
**Resolution (auto, confidence 0.85):** No. Env-transition is an automated
|
||||
lifecycle step, not an explicit decommission. The destroy runs as a direct
|
||||
`terraform destroy -auto-approve` against the prior env's state (REQ-284).
|
||||
The decommission pipeline remains for explicit stack teardown with SRE
|
||||
gates. This is documented in the Out of Scope section. Rationale: the
|
||||
consumer already has HITL attestation on the *new* env (qa/prod/dr gates);
|
||||
requiring a second SRE gate for the prior env's destroy would block
|
||||
autonomous dev→qa promotion, contradicting the "lower environments are
|
||||
autonomous" tenet.
|
||||
### Q4 — Does "all types of securities" mean all types in v1.26, or equities-only pilot with others deferred?
|
||||
|
||||
### A6 — Phase count and ordering
|
||||
**Ambiguity:** The user said "stock market built on homegrown blockchain
|
||||
offering all types of securities." This could mean equities + bonds +
|
||||
derivatives + options all in v1.26, or equities-only pilot with others
|
||||
deferred (the recommended scope from the plan).
|
||||
|
||||
**Ambiguity:** The requirements traceability table shows 3 phases (P1:
|
||||
docs, P2: feat, P3: test) but the roadmap entry says "4 phases."
|
||||
**Resolution:** Equities-only pilot (D-200, established in SPECIFY).
|
||||
Bonds/derivatives/options have very different settlement models (T+1
|
||||
for equities; T+2 for bonds; derivatives vary; options exercise
|
||||
models). A pilot should demonstrate the Nova platform's policy gates
|
||||
over a real estate — equities (T+1) is the simplest. "All types of
|
||||
securities" is the *product vision*; v1.26 is the *pilot* (equities
|
||||
first). The roadmap documents the deferral.
|
||||
|
||||
**Resolution (auto, confidence 0.90):** 4 phases = P0 (pre-execution) + P1
|
||||
(docs fixes) + P2 (env-transition feat) + P3 (tests) + P4 (final
|
||||
review/ship). The "4 phases" in the roadmap counts execution phases (P1-P3)
|
||||
+ final (P4). This matches the run.md phase model (P0 pre-execution, P1..PN
|
||||
execution, P N+1 final). The traceability table lists P1-P3 (execution);
|
||||
P4 is the final phase (review + audit + ship, no new requirements).
|
||||
**Confidence:** 0.85. **Decision:** D-200 (reaffirmed).
|
||||
|
||||
## Clarification Commit
|
||||
### Q5 — Is the homegrown blockchain a real consensus protocol or a minimal PoA ledger?
|
||||
|
||||
No changes to REQUIREMENTS.md or PROJECT.md from clarify — the ambiguities
|
||||
are resolved and already captured in the requirements (REQ-276..290) and
|
||||
the Out of Scope section. The resolutions above are logged for traceability.
|
||||
**Ambiguity:** "Homegrown blockchain" could mean a full consensus
|
||||
protocol (multi-validator BFT) or a minimal PoA ledger (single
|
||||
validator, append-only).
|
||||
|
||||
**Resolution:** Minimal PoA ledger (D-201, established in SPECIFY).
|
||||
Single validator (config-driven), append-only blocks, SHA-256 hash
|
||||
chain, deterministic block production. Settlement finality = block
|
||||
commit. Multi-validator BFT is a future milestone. The pilot's purpose
|
||||
is to exercise the Nova platform's deploy/policy/attestation gates over
|
||||
a real consumer — the chain needs to be real enough to record
|
||||
transactions, not to solve Byzantine consensus.
|
||||
|
||||
**Confidence:** 0.88. **Decision:** D-201 (reaffirmed).
|
||||
|
||||
### Q6 — Does the pilot's `terraform apply` actually run, or is it `--plan-only`?
|
||||
|
||||
**Ambiguity:** The platform's `run_platform.sh` defaults to
|
||||
plan-only (no apply). The `deploy.yml` workflow's `mode` input can be
|
||||
`full` (apply) or `plan-only`. Does the pilot actually `terraform apply`
|
||||
(creating real AWS resources for the blockchain exchange), or does it
|
||||
stop at plan?
|
||||
|
||||
**Resolution:** The pilot runs `mode: full` (apply) for `dev` only.
|
||||
The apply creates real AWS resources (ECS for the matching engine,
|
||||
DynamoDB for the ledger, S3 for block storage) in account
|
||||
`581513795199`. `qa`/`prod`/`dr` are blocked by the pilot-readiness
|
||||
policy (REQ-320) until their accounts are bound (D-208). The apply is
|
||||
autonomous for `dev` (no HITL gate; confidence threshold 0.50). The
|
||||
`ai.decision.made` + `attestation.recorded` events land in the Decision
|
||||
Ledger — but `dev` attestation is autonomous (no human approver), so
|
||||
only `ai.decision.made` fires for `dev`.
|
||||
|
||||
**Confidence:** 0.90. **Decision:** D-209.
|
||||
|
||||
### Q7 — What AWS resources does the blockchain exchange contract declare?
|
||||
|
||||
**Ambiguity:** The `contract.yaml` declares the exchange's
|
||||
infrastructure. What specific AWS resources? The platform's adapter
|
||||
maps contract infrastructure blocks to Terraform. What stack types
|
||||
does the blockchain exchange use?
|
||||
|
||||
**Resolution:** The pilot contract declares 3 infrastructure blocks:
|
||||
(1) `ecs` (Fargate service for the matching engine + settlement
|
||||
service — the platform's existing `microservice` module pattern), (2)
|
||||
`dynamodb` (the ledger table — single-table, PK `block_index`), (3)
|
||||
`s3` (block storage — one object per block, key `blocks/{index}.json`).
|
||||
The adapter's `TYPE_MAP` already covers `aws_ecs_service`,
|
||||
`aws_dynamodb_table`, `aws_s3_bucket` (existing L1 primitives). No new
|
||||
adapter stack types needed for the pilot. The contract's
|
||||
`infrastructure` block references these by module name (`microservice`
|
||||
for ECS, `dynamodb` for the table, `s3` for the bucket).
|
||||
|
||||
**Confidence:** 0.82. **Decision:** D-210.
|
||||
|
||||
### Q8 — Does the outcome-backfill emitter (REQ-317) change the PCR schema?
|
||||
|
||||
**Ambiguity:** REQ-317 wires `apply.completed`/`apply.failed` →
|
||||
`fact_decision.outcome`. Does this touch the `PolicyCheckResult` schema
|
||||
(PCR) — the v1.25 moat that must not change?
|
||||
|
||||
**Resolution:** No. The outcome backfill touches the *metrics cold
|
||||
store* (`fact_decision` table in `metrics/nova_metrics.db`), not the
|
||||
PCR schema. The PCR schema (`schemas/policy_check_result.schema.json`)
|
||||
is unchanged. The backfill reads run-manifest events (not PCRs) and
|
||||
updates the decision's outcome column. This respects the v1.25 hard
|
||||
constraint: "DO NOT change `schemas/policy_check_result.schema.json`."
|
||||
|
||||
**Confidence:** 0.95. **Decision:** D-211.
|
||||
|
||||
### Q9 — Does the consumer repo need its own test suite + CI, or does the platform's CI cover it?
|
||||
|
||||
**Ambiguity:** The consumer repo (`nova-blockchain-exchange`) has app
|
||||
code (blockchain, engine, settlement). Does it run its own tests in
|
||||
its own CI, or does the platform's `platform-test.yml` cover it?
|
||||
|
||||
**Resolution:** The consumer repo runs its own tests in its own CI
|
||||
(`nova-blockchain-exchange/.github/workflows/ci.yml` — lint + pytest on
|
||||
the blockchain/engine/settlement code). The platform's
|
||||
`platform-test.yml` covers the *platform* repo only (it validates
|
||||
contracts against the schema, runs adapter tests, etc.). The consumer
|
||||
repo's `deploy.yml` invocation triggers the platform's deploy workflow
|
||||
(which runs `run_platform.sh`); the platform's policy + attestation
|
||||
gates apply over the consumer's apply. The consumer's unit tests
|
||||
(chain integrity, order matching, settlement) are the consumer's
|
||||
responsibility. REQ-310..312 include consumer-side tests
|
||||
(`test_block.py`, `test_order_book.py`, `test_settlement.py`).
|
||||
|
||||
**Confidence:** 0.88. **Decision:** D-212.
|
||||
|
||||
### Q10 — Is the milestone a feature milestone (tags on v1.25.x) or a major milestone (breaking schema changes)?
|
||||
|
||||
**Ambiguity:** v1.26 introduces a 2nd project (multi-project mode) +
|
||||
new requirements. Does this break any schema (→ major milestone, tags
|
||||
on v1.26.x), or is it a feature milestone (tags on v1.25.x)?
|
||||
|
||||
**Resolution:** Feature milestone. No schema breaks: the PCR schema is
|
||||
unchanged (D-211); the contract schema is unchanged (the consumer
|
||||
contract validates against the existing
|
||||
`schemas/contract.schema.json`); the env JSON gains a real
|
||||
`account_id` (data, not schema). Multi-project mode is a config
|
||||
change (not a schema break). Tags run on the **v1.25.x** patch line:
|
||||
`v1.25.0` (P0) → `v1.25.5` (P5 = milestone release). Per `run.md`
|
||||
versioning logic: "Feature milestone (at least one feat phase):
|
||||
progressive patches per phase. The final phase's patch IS the milestone
|
||||
release. No separate minor tag."
|
||||
|
||||
**Confidence:** 0.92. **Decision:** D-213.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
10 ambiguities identified; all auto-resolved at full autonomy
|
||||
(confidence ≥ 0.60). 8 new decisions (D-206..D-213) + 3 reaffirmed
|
||||
from SPECIFY (D-200, D-201, D-203). 0 escalations (all ≥ 0.60). The
|
||||
resolutions are recorded in this file + reflected in PROJECT.md /
|
||||
REQUIREMENTS.md / ROADMAP.md updates.
|
||||
|
||||
**Key decisions:**
|
||||
- D-206: `.ciagent/` for both projects in the platform repo (multi-project mode).
|
||||
- D-207: `NOVA_AWS_*` has root-equivalent perms; single key for pilot.
|
||||
- D-208: `dev` bound to `581513795199`; qa/prod/dr stay placeholder (pilot-readiness policy blocks apply on placeholder).
|
||||
- D-209: Pilot runs `mode: full` (apply) for `dev` only; autonomous (no HITL gate).
|
||||
- D-210: Contract declares ecs + dynamodb + s3 (existing adapter stack types; no new TYPE_MAP entries).
|
||||
- D-211: Outcome backfill touches metrics cold store, NOT the PCR schema (v1.25 moat preserved).
|
||||
- D-212: Consumer repo has its own CI + unit tests; platform CI covers platform only.
|
||||
- D-213: Feature milestone; tags on v1.25.x (no schema breaks).
|
||||
+186
-161
@@ -1,200 +1,225 @@
|
||||
# CIAgent Grill Report
|
||||
# GRILL — v1.26 Live Pilot Estate Activation
|
||||
|
||||
## Run: 2026-08-12 (mode: self-grill, focus: all axes) — v1.24 Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement
|
||||
> Adversarial review of the v1.26 SPECIFY + CLARIFY + RESEARCH + IDEATE +
|
||||
> PLAN. The grill red-teams the proposal across feasibility, scope,
|
||||
> budget, and the domain claims (homegrown blockchain, pilot estate,
|
||||
> metric grounding). Each challenge gets a binding verdict
|
||||
> (PROCEED / REVISE / ESCALATE). Autonomy: full — escalations auto-
|
||||
> resolve with assumption logging unless confidence < 0.60.
|
||||
|
||||
### Overall Verdict: PROCEED (confidence: 0.82)
|
||||
## Verdict: PROCEED (0.84) — 0 escalations, 2 revisions
|
||||
|
||||
The plan is sound. The user directive is clear and binding. The code
|
||||
integration points are confirmed by inspection. Two binding revisions
|
||||
applied (both low-risk doc clarifications). No escalations.
|
||||
The milestone is feasible, scoped, and the domain claims hold. Two
|
||||
plan revisions are binding (G-Q4, G-Q8) and are already captured in
|
||||
PLAN.md. No work is blocked.
|
||||
|
||||
---
|
||||
|
||||
## Axis 1: Feasibility
|
||||
## Challenges
|
||||
|
||||
**Challenge:** Can `run_platform.sh` Step 0b actually run `terraform
|
||||
destroy` against the prior env's state without the prior env's AWS
|
||||
credentials?
|
||||
### G-Q1 — Is a homegrown PoA blockchain viable for a pilot, or is it reckless?
|
||||
|
||||
**Response:** In the same-account case (the scaffold today, per
|
||||
`core/environments/dev.json`), yes — the deploy role has access to the
|
||||
shared state bucket and the resources are in the same account. The
|
||||
`terraform init -reconfigure` re-points to the prior env's state key
|
||||
within the same bucket. Cross-account is explicitly out of scope
|
||||
(D-205). **Confidence: 0.85.**
|
||||
**Challenge:** Authoring a blockchain (even a minimal PoA ledger) is a
|
||||
non-trivial domain. A homegrown chain could have correctness bugs (hash
|
||||
chain breaks, non-deterministic blocks, settlement-finality race
|
||||
conditions). Why not use a proven chain (Ethereum L2, Solana, Hyperledger
|
||||
Fabric)?
|
||||
|
||||
**Challenge:** Does `deletion_protection: false` injection work the same
|
||||
way as decommission Step 2?
|
||||
**Verdict:** PROCEED (confidence 0.88). The pilot's purpose is to
|
||||
exercise the Nova platform's deploy/policy/attestation gates over a
|
||||
real consumer estate — not to build a production blockchain. A
|
||||
homegrown PoA ledger is the minimal viable chain: append-only blocks,
|
||||
single validator, SHA-256 hash chain, deterministic block production.
|
||||
This is ~200 lines of Python (block + ledger + validator). The chain
|
||||
needs to be real enough to record transactions + produce a settlement-
|
||||
finality signal for the kyverno-json policy (REQ-315) — not to solve
|
||||
Byzantine consensus. A proven chain (Ethereum/Solana/Hyperledger) would
|
||||
be the *consumer app's* choice, not the platform's; the platform is
|
||||
chain-agnostic. For the pilot, the homegrown chain avoids a heavyweight
|
||||
external dependency (a full node, smart contracts, gas models) that
|
||||
would obscure the platform-gates demonstration. REQ-310 tests cover
|
||||
chain integrity, hash determinism, genesis, append/verify — the
|
||||
correctness surface is bounded. Multi-validator BFT is a future
|
||||
milestone (D-201). No revision needed.
|
||||
|
||||
**Response:** Yes. `scripts/run_decommission.sh:34-37` sets
|
||||
`res['nfrs']['deletion_protection'] = False` on every resource. The
|
||||
contract resolver propagates `inputs.deletion_protection` to children's
|
||||
NFRs (`core/contract_resolver.py:360-372`). The env-transition destroy
|
||||
step must resolve with `environment_override=prior_env` AND inject
|
||||
`deletion_protection=false` into the contract inputs before resolving.
|
||||
This is a confirmed pattern. **Confidence: 0.90.**
|
||||
### G-Q2 — Does "all types of securities" scope-explode the milestone?
|
||||
|
||||
**Verdict:** FEASIBLE.
|
||||
**Challenge:** The user said "offering all types of securities." Equities
|
||||
(D-200, pilot scope) is one type. Bonds (T+2), derivatives (varying),
|
||||
options (exercise models) have very different settlement models. Does
|
||||
the equities-only deferral betray the user's intent?
|
||||
|
||||
## Axis 2: Scope
|
||||
**Verdict:** PROCEED (confidence 0.85). The user *chose* equities-only
|
||||
pilot (Q4 in the plan discussion, answer "A to all 3 questions" — the
|
||||
recommended scope). "All types of securities" is the *product vision*;
|
||||
v1.26 is the *pilot* (equities first). The roadmap documents the
|
||||
deferral. The pilot demonstrates the Nova platform's gates over the
|
||||
simplest settlement model (T+1); expanding to other security types is
|
||||
a straightforward extension (new settlement-service branches + new
|
||||
kyverno-json policies) once the platform-gates pattern is proven. No
|
||||
revision needed — the scope decision is the user's, not the grill's.
|
||||
|
||||
**Challenge:** Is 4 phases (P1-P3 + P4) the right size, or is this
|
||||
over-scoped?
|
||||
### G-Q3 — Does the consumer-repo-as-2nd-project break single-project tooling?
|
||||
|
||||
**Response:** 15 requirements across 3 execution phases is
|
||||
well-scoped. P1 (7 REQs, all docs/test) is the largest by count but the
|
||||
smallest by effort (text edits + test assertions). P2 (6 REQs, feat) is
|
||||
the core implementation. P3 (2 REQs, test) is coverage. P4 is final
|
||||
review. This is a tight, coherent milestone. **Confidence: 0.88.**
|
||||
**Challenge:** CIAgent has been single-project since v1.0. v1.26
|
||||
activates multi-project mode (2 projects: `acdl` +
|
||||
`nova-blockchain-exchange`). Does this break assumptions in the
|
||||
CIAgent tooling (branch naming, `.ciagent/` paths, commit `---ci---`
|
||||
blocks)?
|
||||
|
||||
**Challenge:** Should the cross-account destroy be in scope?
|
||||
**Verdict:** PROCEED (confidence 0.90). `run.md` Step 0 explicitly
|
||||
specifies multi-project mode: `projects[]` with length > 0,
|
||||
`active_projects` array, `.ciagent/<slug>/` subdirectory paths, branch
|
||||
prefixes `<slug>/`. The `---ci---` block gains a `project: <slug>`
|
||||
field (already in the v1.26 commits). The consumer's project files
|
||||
live in `.ciagent/nova-blockchain-exchange/`. The platform's existing
|
||||
flat `.ciagent/` files remain the primary set (the platform is the
|
||||
default project). Branch naming: the consumer's phases use
|
||||
`nova-blockchain-exchange/phase/01-...`; the platform's phases use
|
||||
`acdl/phase/03-...` (or flat `phase/03-...` for platform-level work).
|
||||
No tooling change needed — the multi-project spec is already in
|
||||
`run.md`. D-206 records this. No revision needed.
|
||||
|
||||
**Response:** No. The scaffold is single-account. Adding cross-account
|
||||
would require assuming the prior env's role, which needs a trust policy
|
||||
the scaffold doesn't have yet. Deferring is pragmatic. The Out of Scope
|
||||
section documents this. **Confidence: 0.85.**
|
||||
### G-Q4 — Does the P2 contract reference a `dynamodb` module that doesn't exist until P3?
|
||||
|
||||
**Verdict:** PROPERLY-SCOPED.
|
||||
**Challenge:** The original plan had REQ-322 (DynamoDB primitive) in
|
||||
P3, but the P2 contract (REQ-313) references `dynamodb` in its
|
||||
`infrastructure` block. If the primitive doesn't exist until P3, the
|
||||
P2 contract's `dynamodb` block can't resolve at registry time — only
|
||||
at schema time (the schema is open). Is this a vertical-slice
|
||||
violation (P2 ships a contract that can't fully resolve)?
|
||||
|
||||
## Axis 3: Cost / ROI
|
||||
**Verdict:** REVISE (confidence 0.92). This is a real vertical-slice
|
||||
violation. PLAN.md already revised: REQ-322 moves to P2 W0 (before the
|
||||
contract). The revised mapping (PLAN.md "Revised: REQ-322 → P2 W0")
|
||||
makes P2 self-contained: the primitive + the contract + the deploy
|
||||
invocation all land in P2. This is a binding revision — the original
|
||||
P3 placement is superseded. ROADMAP.md is already updated (REQ-322 in
|
||||
P2). No further revision needed — the plan self-corrected.
|
||||
|
||||
**Challenge:** Is the env-transition feature worth the complexity?
|
||||
### G-Q5 — Does live-AWS pilot break the MTTR < 60s target?
|
||||
|
||||
**Response:** Yes. The user identified a real orphaned-resources risk
|
||||
that violates the platform's full-lifecycle-management mission. The
|
||||
fix is a ~80-line Python module + a shell block. The alternative
|
||||
(blocking env edits, forcing Shape B) contradicts the user's directive.
|
||||
The ROI is high: closes a real lifecycle gap with minimal code.
|
||||
**Confidence: 0.90.**
|
||||
**Challenge:** NORTH_STAR.md MTTR target: < 60s p95. The pilot runs
|
||||
`terraform apply` (creating real AWS resources: ECS + DynamoDB + S3).
|
||||
Apply latency for a 3-resource stack is typically 2-5 minutes (ECS
|
||||
service creation is the slow step). Does this break the MTTR target?
|
||||
|
||||
**Verdict:** JUSTIFIED.
|
||||
**Verdict:** PROCEED (confidence 0.86). The MTTR target is for
|
||||
*platform-detected + platform-remediated incidents* (apply.failed →
|
||||
successful retry), not for first-time apply latency. The pilot's
|
||||
first apply is a deployment, not an incident-remediation. The MTTR
|
||||
metric measures the retry path: if the apply fails (e.g. IAM
|
||||
permission), the platform retries — the retry MTTR is the time from
|
||||
`apply.failed` to `apply.succeeded`, which is < 60s for a retry (the
|
||||
resources are already partially created; the retry completes the
|
||||
remaining steps). The pilot's apply latency is a deployment metric
|
||||
(lead time), not an MTTR metric. RESEARCH §1.2 (v1.25 grill G-Q3)
|
||||
analyzed this same question for the kyverno-json pass — the same
|
||||
reasoning applies. No revision needed.
|
||||
|
||||
## Axis 4: Correctness
|
||||
### G-Q6 — Is the settlement-finality policy (REQ-315) over-engineering for a pilot?
|
||||
|
||||
**Challenge:** The `detect_prior_env` query — is querying by
|
||||
`contractId#submittedAt` SK prefix correct for finding the last-applied
|
||||
env?
|
||||
**Challenge:** A kyverno-json policy asserting settlement finality
|
||||
(`all_committed: true`) before promotion is a securities-specific
|
||||
extension of v1.25's policy engine. Is this over-engineering for a
|
||||
pilot that only runs in `dev` (autonomous, no promotion to qa/prod/dr
|
||||
in v1.26 per D-208)?
|
||||
|
||||
**Response:** The `nova-contracts` table has PK `consumerRepo` and SK
|
||||
`contractId#submittedAt`. To find the last record for a given
|
||||
contractId, we query by PK `consumerRepo` + SK `begins_with
|
||||
"contractId#"` + FilterExpression `status = "submitted"` (or
|
||||
`#LAST_APPLIED`), sort by `submittedAt` desc, take the first. This is
|
||||
correct DynamoDB pattern. The `record_applied_env` step writes a new
|
||||
item with SK `contractId#LAST_APPLIED#<timestamp>` so the detect step
|
||||
can filter by `begins_with "contractId#LAST_APPLIED#"`. **Confidence:
|
||||
0.85.**
|
||||
**Verdict:** PROCEED (confidence 0.80). The policy is *authored* in
|
||||
v1.26 (P3) but its *enforcement* activates when a promotion to qa/prod
|
||||
happens — which is a *future* milestone (D-208: qa/prod/dr stay
|
||||
placeholder this milestone). The policy is tested (passing + failing
|
||||
fixtures; skip when `kj` absent) in P3, but it doesn't gate a `dev`
|
||||
apply (the pilot-readiness policy REQ-320 gates `dev`; the settlement-
|
||||
finality policy gates promotions). Authoring + testing the policy in
|
||||
v1.26 is the right thing: it (a) proves the kyverno-json engine can
|
||||
assert a domain invariant, (b) ships the policy artifact so a future
|
||||
milestone that binds qa/prod/dr can enable it without re-architecting,
|
||||
(c) extends v1.25's moat (the policy engine is swappable + extensible
|
||||
to new domains). The cost is ~1 policy file + 1 test file. No revision
|
||||
needed — but the POLICY IS NOT ENFORCED in v1.26 (it's authored +
|
||||
tested, enforcement is future). PLAN.md should note this. **Minor
|
||||
revision: PLAN.md P3 W4 Task 4.1 should note "policy authored + tested;
|
||||
enforcement deferred to the milestone that binds qa/prod/dr."** Already
|
||||
implicit in the plan (the policy gates promotions, not dev applies);
|
||||
making it explicit is a documentation refinement, not a scope change.
|
||||
|
||||
**Challenge:** What if the DynamoDB table doesn't exist in local/CI
|
||||
mode?
|
||||
### G-Q7 — Is D-083 deferral defensible for a pilot with real money-like flows?
|
||||
|
||||
**Response:** The detect step catches `ClientError` / `EndpointNotFound`,
|
||||
logs a warning, and returns `None` (no prior env). The pipeline proceeds
|
||||
normally. This is the conservative path — no false-positive destroys.
|
||||
**Confidence: 0.90.**
|
||||
**Challenge:** The pilot is a stock exchange — securities trading. D-083
|
||||
(S3 Object Lock / JWS tamper-evident ledger) is deferred (D-204). The
|
||||
SQLite hash-chain + DynamoDB outbox is the audit record. Is this
|
||||
defensible for a domain where audit integrity is legally mandated?
|
||||
|
||||
**Verdict:** CORRECT.
|
||||
**Verdict:** PROCEED (confidence 0.82). The pilot is a *technical
|
||||
demonstration*, not a production trading system. No real money, no real
|
||||
securities, no real investors — the "securities" are test tokens on a
|
||||
homegrown chain. The audit integrity requirement (SEC Rule 17a-4, FINRA
|
||||
retention) applies to *production* trading systems, not to a pilot
|
||||
exercising a platform's deploy/policy/attestation gates. The SQLite
|
||||
hash-chain + DynamoDB outbox is a tamper-*evident* record (any tampering
|
||||
breaks the hash chain) — it's just not tamper-*resistant* (S3 Object
|
||||
Lock + JWS would make it tamper-resistant). For a pilot, tamper-evident
|
||||
suffices. D-083 lift is a future milestone (when the pilot becomes a
|
||||
production system). D-204 records this. No revision needed.
|
||||
|
||||
## Axis 5: Testing
|
||||
### G-Q8 — Does the outcome-backfill emitter (REQ-317) touch the PCR schema?
|
||||
|
||||
**Challenge:** Can the env-transition behavior be tested without live
|
||||
AWS?
|
||||
**Challenge:** REQ-317 wires `apply.completed`/`apply.failed` →
|
||||
`fact_decision.outcome`. The v1.25 hard constraint says "DO NOT change
|
||||
`schemas/policy_check_result.schema.json`." Does the backfill touch the
|
||||
PCR schema?
|
||||
|
||||
**Response:** Yes. `test_env_transition.py` uses moto for DynamoDB
|
||||
(mock_aws pattern from `test_contract_ingestor.py:74-110`).
|
||||
`test_run_platform_env_transition.py` uses shell-text assertions
|
||||
(pattern from `test_pipeline.py:79-95`). No live AWS needed.
|
||||
**Confidence: 0.92.**
|
||||
**Verdict:** PROCEED (confidence 0.95). D-211 (CLARIFY) already
|
||||
resolved this: the outcome backfill touches the *metrics cold store*
|
||||
(`fact_decision` table in `metrics/nova_metrics.db`), not the PCR
|
||||
schema. The backfill reads run-manifest events (not PCRs) and updates
|
||||
the decision's outcome column. The PCR schema is unchanged. This
|
||||
respects the v1.25 hard constraint. No revision needed.
|
||||
|
||||
**Verdict:** TESTABLE.
|
||||
### G-Q9 — Does the `NOVA_AWS_*` root-equivalent key create a security risk?
|
||||
|
||||
## Axis 6: Security
|
||||
**Challenge:** D-207 says `NOVA_AWS_*` has root-equivalent permissions
|
||||
(confirmed empirically: the bootstrap created the S3 bucket + DynamoDB
|
||||
table). Using a root key for the pilot's `terraform apply` is a
|
||||
security risk — a key compromise gives full account access. Should the
|
||||
pilot use a least-privilege key?
|
||||
|
||||
**Challenge:** Does the destroy step introduce a risk of destroying the
|
||||
wrong resources?
|
||||
|
||||
**Response:** The destroy targets the prior env's state key
|
||||
(`spike/{id}/{prior_env}/terraform.tfstate`). The state key is
|
||||
deterministic and env-scoped. The destroy can only affect resources in
|
||||
that state file. The `deletion_protection=false` injection is scoped to
|
||||
the destroy step only — the new env's apply runs with the default
|
||||
`deletion_protection=true`. **Confidence: 0.88.**
|
||||
|
||||
**Challenge:** Could a malicious consumer trigger a destroy of another
|
||||
consumer's resources?
|
||||
|
||||
**Response:** No. The DynamoDB query is scoped by PK `consumerRepo`
|
||||
(the consumer's own repo identity). The destroy runs under the
|
||||
consumer's ABAC-scoped deploy role, which can only touch resources
|
||||
tagged `nova:owner=<consumer-repo>`. A consumer cannot query or destroy
|
||||
another consumer's stack. **Confidence: 0.90.**
|
||||
|
||||
**Verdict:** SECURE.
|
||||
|
||||
## Axis 7: Maintainability
|
||||
|
||||
**Challenge:** Is the `core/env_transition.py` module a clean
|
||||
abstraction or a one-off?
|
||||
|
||||
**Response:** It's a reusable module with two functions
|
||||
(`detect_prior_env`, `record_applied_env`) that encapsulate the
|
||||
DynamoDB query logic. It can be extended for cross-account destroy in a
|
||||
future milestone. The shell Step 0b is a thin orchestrator. This is
|
||||
maintainable. **Confidence: 0.85.**
|
||||
|
||||
**Verdict:** MAINTAINABLE.
|
||||
|
||||
## Axis 8: Docs consistency
|
||||
|
||||
**Challenge:** Will the consumer guide be internally consistent after
|
||||
P1?
|
||||
|
||||
**Response:** The 5 fixes address all known inconsistencies: field
|
||||
table ↔ schema, Step 2 ↔ Step 4, Step 5 ↔ environments doc, Step 8 ↔
|
||||
per-env section, reference table ↔ sample contracts. The test updates
|
||||
assert both shapes are documented. A manual end-to-end read in P1's
|
||||
verification step catches any remaining inconsistency. **Confidence:
|
||||
0.88.**
|
||||
|
||||
**Verdict:** CONSISTENT.
|
||||
|
||||
## Axis 9: Adversarial
|
||||
|
||||
**Challenge:** What if the consumer edits `environment:` AND changes
|
||||
other inputs simultaneously? Does the destroy-then-apply still work?
|
||||
|
||||
**Response:** Yes. The destroy step re-resolves the contract with
|
||||
`environment_override=prior_env` — the other input changes are
|
||||
irrelevant to the destroy (it destroys whatever is in the prior env's
|
||||
state). The new apply resolves with the new env + new inputs. The two
|
||||
operations are independent. **Confidence: 0.85.**
|
||||
|
||||
**Challenge:** What if the prior env's state was already manually
|
||||
destroyed (e.g., via decommission)?
|
||||
|
||||
**Response:** `terraform destroy` against an empty state is a no-op
|
||||
(exits 0). The detect step still detects the prior env from DynamoDB,
|
||||
but the destroy is a no-op. The apply proceeds. This is correct
|
||||
behavior — no false failure. **Confidence: 0.88.**
|
||||
|
||||
**Verdict:** ROBUST.
|
||||
**Verdict:** PROCEED (confidence 0.78). The risk is real but bounded:
|
||||
(a) the pilot runs in a single account (`581513795199`) with no
|
||||
production workloads (the v1.11 teardown left it empty; the pilot is
|
||||
the only workload), (b) the key is in `.env.secrets` (gitignored, never
|
||||
committed), (c) the deploy workflow uses OIDC by default (the static
|
||||
key is the override, not the primary path). A future hardening
|
||||
milestone should split `NOVA_AWS_*` into a root `NOVA_BOOTSTRAP_AWS_*`
|
||||
+ a least-privilege `NOVA_AWS_*` runner key (the spike-runner pattern).
|
||||
For v1.26, the single key suffices (pilot scope). D-207 records this.
|
||||
**Minor revision: PLAN.md should note the key-split as a future
|
||||
hardening item.** Already implicit in D-207; making it explicit in the
|
||||
plan is a documentation refinement.
|
||||
|
||||
---
|
||||
|
||||
## Binding revisions applied
|
||||
## Summary
|
||||
|
||||
1. **R1 (docs):** Add to RESEARCH.md pitfalls: the destroy step must
|
||||
inject `deletion_protection=false` into the contract inputs before
|
||||
re-resolving with `environment_override=prior_env`. Without this,
|
||||
`prevent_destroy` lifecycle blocks (REQ-86) block the destroy. This
|
||||
is already noted in RESEARCH §5 pitfall 3 and PLAN P2 implementation
|
||||
note 3. No change needed — already captured.
|
||||
9 challenges; 0 escalations; 2 binding revisions (G-Q4, G-Q6/G-Q9
|
||||
minor). Overall verdict: PROCEED (confidence 0.84).
|
||||
|
||||
2. **R2 (docs):** Clarify in PLAN P2 that the `record_applied_env` SK
|
||||
format is `contractId#LAST_APPLIED#<timestamp>` so the detect step
|
||||
can query `begins_with "contractId#LAST_APPLIED#"`. This is already
|
||||
in RESEARCH §2 and GRILL Axis 4. No change needed — already captured.
|
||||
**Binding revisions:**
|
||||
- **G-Q4:** REQ-322 moves to P2 W0 (already revised in PLAN.md + ROADMAP.md).
|
||||
- **G-Q6:** PLAN.md P3 W4 Task 4.1 should note the settlement-finality
|
||||
policy is authored + tested in v1.26 but *enforcement* is deferred to
|
||||
the milestone that binds qa/prod/dr (documentation refinement).
|
||||
- **G-Q9:** PLAN.md should note the `NOVA_AWS_*` key-split as a future
|
||||
hardening item (documentation refinement).
|
||||
|
||||
## Escalations
|
||||
|
||||
None. All challenges resolved at full autonomy.
|
||||
**No work is blocked.** The milestone is feasible, scoped, and the
|
||||
domain claims hold. The homegrown PoA blockchain is a minimal viable
|
||||
chain (~200 lines), not a production consensus protocol. The equities-
|
||||
only scope is the user's choice. The multi-project mode is specified in
|
||||
`run.md`. The P2→P3 dependency is resolved (REQ-322 → P2 W0). The
|
||||
MTTR target is for incident-remediation, not first-time apply. The
|
||||
settlement-finality policy is authored + tested, enforcement is future.
|
||||
D-083 deferral is defensible for a technical pilot. The PCR schema is
|
||||
unchanged. The root-equivalent key is a bounded risk with a documented
|
||||
future hardening path.
|
||||
@@ -0,0 +1,194 @@
|
||||
# IDEATE — v1.26 Live Pilot Estate Activation
|
||||
|
||||
> **Autonomy:** full. 3-tier ideation per `config.json ideation.enabled:
|
||||
> true`. `cross_project.enabled: false` → cross-project tier scoped to
|
||||
> multi-project (deferred ideas only, no cross-project candidates
|
||||
> accepted). `confidence_threshold: 0.6`, `max_ideas: 20`.
|
||||
> Categories: security, quality, architecture, coverage, improvement.
|
||||
|
||||
## Tier 1 — Mechanical (pattern-driven, codebase-grounded)
|
||||
|
||||
### I1 — Outcome-backfill emitter ✅ ACCEPTED (REQ-317)
|
||||
|
||||
**Category:** quality, coverage
|
||||
**Confidence:** 0.92
|
||||
**Pattern:** stuck `pending` status → backfilled from a later event
|
||||
(the most direct metric-grounding pattern).
|
||||
**Source:** `core/metrics/decision_ledger.py:210-211` documents the
|
||||
event chain `confidence.computed → ai.decision.made →
|
||||
attestation.recorded → run.completed/failed`. `collector.py:262`
|
||||
inserts `fact_decision.outcome` as `"pending"` — no backfill step
|
||||
wires `run.completed/failed` back into the decision's outcome. The AI
|
||||
Decision Accuracy metric (`trust_snapshot.py:70-85`) reads
|
||||
`decisions WHERE outcome='succeeded' ÷ total` → 0% today (all pending).
|
||||
**Idea:** `core/metrics/outcome_backfill.py` reads run-manifest
|
||||
`completed`/`failed` events and updates `fact_decision.outcome` +
|
||||
`fact_decision.backfilled_at`. The collector invokes backfill after run
|
||||
completion. Grounds AI Decision Accuracy (Post-Pilot target).
|
||||
**Accepted into:** REQ-317. Phase P3.
|
||||
|
||||
### I2 — `reason='confidence'` escalation tag ✅ ACCEPTED (REQ-318)
|
||||
|
||||
**Category:** quality, coverage
|
||||
**Confidence:** 0.90
|
||||
**Pattern:** boolean field → discriminated field (the metric-numerator
|
||||
precision pattern).
|
||||
**Source:** `core/confidence_signal.py:184` — a `block` band sets
|
||||
`human_override=True`. The Human Escalation Frequency metric
|
||||
(`docs/metrics/human_escalation_frequency.md:11-12`) is defined as
|
||||
`count(runs WHERE hitl_block=1 AND reason='confidence') ÷ total runs`.
|
||||
The `reason='confidence'` discriminator is not stored today.
|
||||
**Idea:** `ai.decision.made` gains `escalation_reason: 'confidence'`
|
||||
when `band == 'block'`. The collector persists it into `fact_run`.
|
||||
Grounds Human Escalation Frequency numerator.
|
||||
**Accepted into:** REQ-318. Phase P3.
|
||||
|
||||
### I3 — Env-JSON `state_backend` wiring reconciliation ✅ ACCEPTED (REQ-319)
|
||||
|
||||
**Category:** architecture, improvement
|
||||
**Confidence:** 0.88
|
||||
**Pattern:** unused config field → wired config field (the
|
||||
single-source-of-truth pattern).
|
||||
**Source:** `adapters/terraform/adapter.py:116-117` computes the state
|
||||
bucket as `nova-tfstate-<AWS_ACCOUNT_ID>-us-east-1` from the
|
||||
`AWS_ACCOUNT_ID` env var — **not** from the env JSON's
|
||||
`state_backend.bucket`. The env JSON's `state_backend` field is
|
||||
currently unused by the live apply path.
|
||||
**Idea:** The adapter reads `env.state_backend.bucket` when present
|
||||
(falling back to the computed name for backwards compat). `dev.json`
|
||||
gets the real bucket name. Closes the wiring gap so the pilot's env
|
||||
JSON is the single source of truth.
|
||||
**Accepted into:** REQ-319. Phase P3.
|
||||
|
||||
### I4 — Pilot-readiness kyverno-json policy ✅ ACCEPTED (REQ-320)
|
||||
|
||||
**Category:** security, architecture
|
||||
**Confidence:** 0.85
|
||||
**Pattern:** runtime guard → declarative policy (the v1.25 thesis
|
||||
applied to pilot onboarding).
|
||||
**Source:** `core/environment_check.py:48-53` emits a stderr warning
|
||||
(non-fatal) when `account_id == "000000000000"` and env != dev. A
|
||||
warning is not a gate. The pilot should fail-closed if someone tries
|
||||
to apply against a placeholder account.
|
||||
**Idea:** A kyverno-json policy over the env JSON asserting
|
||||
`account_id != "000000000000"` before any apply. Declarative
|
||||
fail-closed gate. Extends v1.25's policy engine to the pilot-onboarding
|
||||
domain.
|
||||
**Accepted into:** REQ-320. Phase P3.
|
||||
|
||||
## Tier 2 — Backend-enriched (signal-driven)
|
||||
|
||||
### I5 — Settlement-finality kyverno-json policy ✅ ACCEPTED (REQ-315)
|
||||
|
||||
**Category:** security, coverage
|
||||
**Confidence:** 0.82
|
||||
**Pattern:** domain invariant → declarative policy (the v1.25 thesis
|
||||
applied to the securities domain — the most novel use of kyverno-json
|
||||
in v1.26).
|
||||
**Source:** The pilot's settlement service records matches as
|
||||
transactions on the chain; settlement finality = block commit. The
|
||||
NORTH_STAR Objective #2 (provable trust) says trust should be a policy
|
||||
artifact, not a promise. Today settlement finality is a runtime
|
||||
property of the chain; making it a declarative policy turns it into an
|
||||
auditable gate.
|
||||
**Idea:** A kyverno-json policy over the settlement-service status JSON
|
||||
asserting `all_committed: true` before any promotion (qa→prod). The
|
||||
securities-specific extension of v1.25's policy engine. The policy is
|
||||
skip-when-kj-absent (graceful).
|
||||
**Accepted into:** REQ-315. Phase P3.
|
||||
|
||||
### I6 — Pilot-estate regression capability (CAP-025) ✅ ACCEPTED (REQ-316)
|
||||
|
||||
**Category:** quality, coverage
|
||||
**Confidence:** 0.88
|
||||
**Pattern:** manual e2e → regression-gated capability (the v1.0 CAP
|
||||
pattern applied to the pilot).
|
||||
**Source:** `core/regression_verify.py` has CAP-013..024 (live-AWS +
|
||||
local tiers). The pilot estate is a new live-AWS capability —
|
||||
"contract resolve → adapter compile → terraform plan → policy scan →
|
||||
confidence signal → attestation → outbox record" against
|
||||
`581513795199`. Without a regression CAP, the pilot could silently
|
||||
decay.
|
||||
**Idea:** CAP-025 (live-pilot-apply) in the regression gate. The
|
||||
round-trip assertion. Grounds the pilot as a maintained capability,
|
||||
not a one-shot demo.
|
||||
**Accepted into:** REQ-316. Phase P3.
|
||||
|
||||
### I7 — DynamoDB L1 primitive ✅ ACCEPTED (REQ-322)
|
||||
|
||||
**Category:** architecture, coverage
|
||||
**Confidence:** 0.95
|
||||
**Pattern:** missing primitive → authored module (the v1.7 + v1.8
|
||||
module-build-out pattern).
|
||||
**Source:** RESEARCH §3.4 — no `modules/l1/dynamodb/` exists. The
|
||||
blockchain exchange's ledger table needs it. The adapter is
|
||||
stateless/registry-driven (no `TYPE_MAP`); a new stack type requires a
|
||||
new L1 module, not an adapter change.
|
||||
**Idea:** Author `modules/l1/dynamodb/` (interface.json +
|
||||
terraform/main.tf + README.md + instance.json + registry.json entry).
|
||||
The single platform-side module build-out for the milestone. Follows
|
||||
the `s3`/`rds` primitive template. Encryption + PITR enabled per v1.8
|
||||
NFR defaults.
|
||||
**Accepted into:** REQ-322. Phase P3.
|
||||
|
||||
### I8 — Stale `adapters/README.md` TYPE_MAP references ❌ DEFERRED (scope)
|
||||
|
||||
**Category:** improvement
|
||||
**Confidence:** 0.70 (above threshold, but scoped into REQ-321)
|
||||
**Pattern:** stale doc → corrected doc.
|
||||
**Source:** `adapters/README.md:49-54` references the deleted
|
||||
`TYPE_MAP`/`INPUT_MAP`/`OUTPUT_MAP` — contradicts `adapter.py:1-11` +
|
||||
`modules/STANDARDS.md:212-214`.
|
||||
**Idea:** Fix the stale references as part of the docs phase.
|
||||
**Reason deferred as a standalone idea:** Already captured in REQ-321
|
||||
(docs + adapter README). No new requirement needed — the fix lands in
|
||||
P4 docs.
|
||||
|
||||
## Tier 3 — Cross-project (deferred — multi-project, but cross-project sharing disabled)
|
||||
|
||||
### I9 — Cross-project policy sharing ❌ DEFERRED (config)
|
||||
|
||||
**Category:** improvement
|
||||
**Confidence:** N/A
|
||||
**Pattern:** policies shared across projects in a multi-project org.
|
||||
**Source:** `config.json ideation.cross_project.enabled: false`.
|
||||
**Idea:** In a multi-project org, kyverno-json policies could be shared
|
||||
across projects (a tagging standard policy applies to all projects).
|
||||
**Reason deferred:** `cross_project.enabled: false`. Even though
|
||||
v1.26 is multi-project (acdl + nova-blockchain-exchange),
|
||||
cross-project *ideation* is disabled in config. Recorded for when the
|
||||
org grows + the flag is enabled.
|
||||
|
||||
### I10 — Consumer-repo CI scaffolding as a reusable template ❌ DEFERRED
|
||||
|
||||
**Category:** improvement
|
||||
**Confidence:** 0.55 (below threshold — deferred, not rejected)
|
||||
**Pattern:** one-off CI → reusable template.
|
||||
**Source:** The consumer repo (`nova-blockchain-exchange`) needs its
|
||||
own CI (`ci.yml` — lint + pytest). If Nova expects many consumers, a
|
||||
reusable consumer-CI template would reduce onboarding friction.
|
||||
**Idea:** A `nova-consumer-template` repo (or a
|
||||
`.github/workflow-templates/` dir) that new consumers instantiate.
|
||||
**Reason deferred:** Nova has 1 consumer today (the pilot). A template
|
||||
is premature abstraction until the 2nd consumer arrives. The pilot's
|
||||
CI is authored directly (REQ-310..312 tests). Recorded for when the
|
||||
3rd consumer onboards.
|
||||
|
||||
## Summary
|
||||
|
||||
- 7 ideas accepted (I1..I7) → already captured as REQ-315, REQ-316,
|
||||
REQ-317, REQ-318, REQ-319, REQ-320, REQ-322.
|
||||
- 3 ideas deferred (I8 scoped into REQ-321; I9 config-disabled; I10
|
||||
below threshold) with documented blocking reasons.
|
||||
- 0 ideas rejected (below-threshold ideas are deferred, not rejected —
|
||||
they may activate when their blockers lift).
|
||||
- The accepted ideas are the **quality improvement** the `--ideate` flag
|
||||
drives: I1 + I2 ground the Post-Pilot metrics (outcome backfill +
|
||||
escalation reason); I3 closes the env-JSON wiring gap; I4 + I5 extend
|
||||
v1.25's policy engine to the pilot domain (pilot-readiness +
|
||||
settlement-finality); I6 gates the pilot as a maintained capability;
|
||||
I7 is the single platform-side module build-out.
|
||||
- No new requirements added beyond REQ-310..322 (the accepted ideas are
|
||||
already scoped into the existing requirements). The IDEATE pass
|
||||
validated the requirement set rather than expanding it — the ideas
|
||||
were anticipated in the SPECIFY + RESEARCH stages.
|
||||
+13
-1
@@ -229,4 +229,16 @@ their AI engineering teams reach for first when an agent needs to deploy.
|
||||
RESEARCH.md/ARCHITECTURE.md. It is the *how*; this file is the *why*.
|
||||
- **Pillar C (story):** the unified narrative deck proves Pillars A+B to
|
||||
leadership. The deck's Proof section cites grounded metrics; its
|
||||
Roadmap section cites deferred targets honestly.
|
||||
Roadmap section cites deferred targets honestly.
|
||||
|
||||
## v1.25 update — swappable policy-engine substrate
|
||||
|
||||
Strategic Objective #2 (provable trust) gained a concrete substrate in
|
||||
v1.25: the policy engine that produces the `PolicyCheckResult` records
|
||||
feeding the confidence signal is now **swappable** via the
|
||||
`PolicyEngine` protocol (`core/policy_engine.py`). `kyverno-json` is
|
||||
the v1.25 default; `OPA` (or any other engine) can replace it by
|
||||
implementing the same 3-method protocol — without touching the
|
||||
confidence signal, the PCR schema, or the pipeline. See
|
||||
ARCHITECTURE.md §12.7. The trust moat is a *replaceable* engine, not a
|
||||
vendor lock-in.
|
||||
+152
-66
@@ -1,83 +1,169 @@
|
||||
---
|
||||
project: acdl
|
||||
milestone: v1.24
|
||||
milestone: v1.26
|
||||
generated_at: 2026-08-12
|
||||
generator: lead-developer
|
||||
verification_toolchain:
|
||||
typecheck: "python3 -m py_compile core/env_transition.py tests/test_env_transition.py tests/test_run_platform_env_transition.py"
|
||||
test: "pytest tests/test_env_transition.py tests/test_run_platform_env_transition.py tests/test_consumer_guide_per_env_section.py tests/test_adapter.py tests/test_contract_resolver.py tests/test_deploy_workflow_env_input.py tests/test_pipeline.py -v"
|
||||
lint: "ruff check core/env_transition.py tests/test_env_transition.py tests/test_run_platform_env_transition.py 2>/dev/null || python3 -m py_compile core/env_transition.py"
|
||||
typecheck: "python3 -m py_compile core/confidence_signal.py core/metrics/outcome_backfill.py adapters/terraform/adapter.py modules/l1/dynamodb/terraform/main.tf"
|
||||
test: "pytest tests/test_adapter.py tests/test_contract_resolver.py tests/test_confidence_signal.py tests/test_outcome_backfill.py tests/test_settlement_finality_policy.py tests/test_pilot_readiness_policy.py tests/test_block.py tests/test_order_book.py tests/test_settlement.py -v"
|
||||
lint: "ruff check core/metrics/outcome_backfill.py adapters/kyverno-json/policies/pilot-readiness/ adapters/kyverno-json/policies/settlement-finality/ 2>/dev/null || python3 -m py_compile core/metrics/outcome_backfill.py"
|
||||
note: |
|
||||
v1.24 is the Consumer Guide Accuracy & Env-Promotion Lifecycle
|
||||
Enforcement milestone — a mixed docs+feat+test milestone. Two active
|
||||
personas: lead-developer (consumer-guide.md edits + guide test
|
||||
updates), backend-engineer (core/env_transition.py + run_platform.sh
|
||||
Step 0b + deploy.yml + adapter doc comment + env_transition tests +
|
||||
pipeline tests). frontend-engineer stays deactivated (no UI). No
|
||||
data-engineer (no schema changes — the nova-contracts table already
|
||||
exists). No new personas.
|
||||
v1.26 is the Live Pilot Estate Activation milestone — a feat
|
||||
milestone. Four active personas: lead-developer (coordination +
|
||||
docs + ARCHITECTURE.md §12.8), backend-engineer (confidence_signal.py
|
||||
escalation reason + outcome_backfill.py + run_platform.sh wiring +
|
||||
env-JSON state_backend reconciliation), data-engineer (DynamoDB L1
|
||||
primitive + metrics cold store outcome backfill), policy-engineer
|
||||
(kyverno-json pilot-readiness + settlement-finality policies), +
|
||||
blockchain-engineer (custom, phase-specific — chain core + order
|
||||
engine + settlement). frontend-engineer is deactivated (no UI).
|
||||
Territory enforcement: warn (the pilot is cross-territory by
|
||||
nature — the consumer repo + the platform repo share the milestone).
|
||||
---
|
||||
|
||||
# ACDL — Persona Roster (v1.24 Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement)
|
||||
# PERSONAS — v1.26 Live Pilot Estate Activation
|
||||
|
||||
> v1.24 roster. Two active personas + two deactivated. This is a mixed
|
||||
> docs+feat+test milestone: the work is consumer-guide accuracy fixes
|
||||
> (lead-developer), a new env-transition detect-and-destroy platform
|
||||
> feature (backend-engineer), and test coverage for both (split).
|
||||
> Generated by the lead-developer at the end of RESEARCH. Assesses the
|
||||
> project domains, activates/deactivates personas, creates custom
|
||||
> personas for domains beyond the default four, aligns frameworks +
|
||||
> territory + constraints to the actual project structure.
|
||||
|
||||
## Active personas
|
||||
## Active Roster (5)
|
||||
|
||||
### lead-developer
|
||||
- **Domain:** coordination + docs
|
||||
- **Frameworks:** []
|
||||
- **Constraints:** ["pragmatic", "battle-tested defaults", "docs match code"]
|
||||
- **Territory:**
|
||||
- `docs/consumer-guide.md`
|
||||
- `tests/test_consumer_guide_per_env_section.py`
|
||||
- `.ciagent/*.md` (PLAN.md, RESEARCH.md, etc.)
|
||||
- **Reason:** Owns the consumer guide narrative — the 5 accuracy fixes
|
||||
(stale fields table, inconsistent caller, "dev only" phrasing, Step 8
|
||||
rewrite with destroy semantics, reference table wording) and the
|
||||
consumer-guide test updates (rename + new destroy-on-env-change test).
|
||||
No UI work (frontend-engineer deactivated). No Python/bash platform
|
||||
code (backend-engineer territory).
|
||||
### 1. lead-developer (active)
|
||||
- **active:** true
|
||||
- **phase_specific:** false
|
||||
- **reason:** Coordinates task decomposition + resolves conflicts between
|
||||
engineering personas. Owns the milestone narrative (PROJECT.md,
|
||||
ROADMAP.md, ARCHITECTURE.md §12.8). Final architectural decisions when
|
||||
personas disagree (e.g. where the outcome-backfill emitter lives).
|
||||
- **domain:** project coordination, milestone narrative, cross-persona
|
||||
conflict resolution.
|
||||
- **frameworks:** none (coordination role).
|
||||
- **territory:** `.ciagent/`, `docs/METRICS.md`, `adapters/README.md`,
|
||||
`modules/README.md`, `modules/STANDARDS.md`.
|
||||
- **constraints:** does not write Python/Terraform (delegates to
|
||||
backend/data-engineer); does not author policies (delegates to
|
||||
policy-engineer); does not author chain code (delegates to
|
||||
blockchain-engineer).
|
||||
|
||||
### backend-engineer
|
||||
- **Domain:** backend (Python + bash + YAML)
|
||||
- **Frameworks:** ["boto3", "terraform"]
|
||||
- **Constraints:** ["api-first", "fail-closed", "no orphan paths", "state-key determinism"]
|
||||
- **Territory:**
|
||||
- `core/env_transition.py` (NEW)
|
||||
- `scripts/run_platform.sh` (Step 0b insert + record-applied-env)
|
||||
- `.github/workflows/deploy.yml` (NOVA_CONSUMER_REPO env)
|
||||
- `adapters/terraform/adapter.py` (doc comment only)
|
||||
- `tests/test_env_transition.py` (NEW)
|
||||
- `tests/test_run_platform_env_transition.py` (NEW)
|
||||
- **Reason:** Owns the env-transition detect-and-destroy feature: the new
|
||||
`core/env_transition.py` module (DynamoDB query + record), the
|
||||
`run_platform.sh` Step 0b orchestrator (re-resolve + terraform destroy +
|
||||
evidence event + fail-closed), the deploy.yml env var passthrough, and
|
||||
the two new test files. Uses boto3 (DynamoDB) + terraform (destroy) +
|
||||
bash (pipeline orchestration).
|
||||
### 2. backend-engineer (active)
|
||||
- **active:** true
|
||||
- **phase_specific:** false
|
||||
- **reason:** Owns the platform-side Python changes: confidence signal
|
||||
escalation reason (REQ-318), outcome-backfill emitter (REQ-317),
|
||||
env-JSON state_backend wiring (REQ-319), adapter test updates for
|
||||
DynamoDB (REQ-322), regression CAP-025 (REQ-316).
|
||||
- **domain:** core Python (confidence_signal.py, metrics/, adapter.py,
|
||||
regression_verify.py, contract_resolver.py), run_platform.sh wiring.
|
||||
- **frameworks:** Python 3.12, pytest, boto3, SQLite, DynamoDB.
|
||||
- **territory:** `core/confidence_signal.py`, `core/metrics/`,
|
||||
`adapters/terraform/adapter.py`, `core/regression_verify.py`,
|
||||
`core/environments/`, `scripts/run_platform.sh`, `tests/test_adapter.py`,
|
||||
`tests/test_confidence_signal.py`, `tests/test_outcome_backfill.py`,
|
||||
`tests/test_regression_pilot.py`.
|
||||
- **constraints:** does not change `schemas/policy_check_result.schema.json`
|
||||
(v1.25 moat, D-211); does not change `schemas/contract.schema.json`
|
||||
(no schema breaks, D-213); does not author Terraform modules
|
||||
(delegates to data-engineer for DynamoDB); does not author policies
|
||||
(delegates to policy-engineer); does not author chain code (delegates
|
||||
to blockchain-engineer).
|
||||
|
||||
## Deactivated personas
|
||||
### 3. data-engineer (active)
|
||||
- **active:** true
|
||||
- **phase_specific:** false
|
||||
- **reason:** Owns the DynamoDB L1 primitive (REQ-322) — the single
|
||||
platform-side module build-out. Owns the metrics cold store
|
||||
outcome-backfill integration (REQ-317, the `fact_decision.outcome`
|
||||
column + `backfilled_at` timestamp). Owns the env-JSON data updates
|
||||
(REQ-319, `core/environments/*.json` account_id + state_backend.bucket).
|
||||
- **domain:** Terraform modules (`modules/l1/`), schema definitions
|
||||
(`interface.json`), registry (`modules/registry.json`), metrics cold
|
||||
store (`metrics/nova_metrics.db`, `core/metrics/collector.py`).
|
||||
- **frameworks:** Terraform, JSON, SQLite, DynamoDB, boto3.
|
||||
- **territory:** `modules/l1/dynamodb/`, `modules/registry.json`,
|
||||
`modules/README.md`, `core/environments/*.json`,
|
||||
`core/metrics/collector.py`, `tests/test_adapter.py` (DynamoDB
|
||||
emission test).
|
||||
- **constraints:** does not change the adapter (stateless, v1.11);
|
||||
follows the v1.8 NFR defaults (encryption + deletion protection +
|
||||
PITR); follows the module standards (`modules/STANDARDS.md`).
|
||||
|
||||
### frontend-engineer
|
||||
- **Active:** false
|
||||
- **Reason:** No UI work in v1.24. The consumer guide is markdown docs
|
||||
(lead-developer territory). Deactivated per v1.17/v1.18/v1.22/v1.23
|
||||
precedent.
|
||||
### 4. policy-engineer (active, custom — added in v1.25)
|
||||
- **active:** true
|
||||
- **phase_specific:** false
|
||||
- **reason:** Owns the kyverno-json policy authoring for the pilot:
|
||||
settlement-finality (REQ-315), pilot-readiness (REQ-320). Extends
|
||||
v1.25's policy engine to the securities domain.
|
||||
- **domain:** declarative policies (kyverno-json ValidatingPolicy YAML),
|
||||
JMESPath assertions, policy tests.
|
||||
- **frameworks:** kyverno-json, JMESPath, JSON, pytest.
|
||||
- **territory:** `adapters/kyverno-json/policies/pilot-readiness/`,
|
||||
`adapters/kyverno-json/policies/settlement-finality/`,
|
||||
`tests/test_settlement_finality_policy.py`,
|
||||
`tests/test_pilot_readiness_policy.py`.
|
||||
- **constraints:** policies are declarative (no imperative Python);
|
||||
`is_configured()` guard skips gracefully when `kj` absent; follows
|
||||
the v1.25 policy-authoring standard (`modules/STANDARDS.md` policy
|
||||
section + `adapters/kyverno-json/README.md`).
|
||||
|
||||
### data-engineer
|
||||
- **Active:** false
|
||||
- **Reason:** No schema changes in v1.24. The `nova-contracts` DynamoDB
|
||||
table already exists with the right shape (PK `consumerRepo`, SK
|
||||
`contractId#submittedAt`). The env-transition module only adds a new
|
||||
`#LAST_APPLIED` SK suffix — no schema migration, no ORM, no new tables.
|
||||
The backend-engineer handles the boto3 queries.
|
||||
### 5. blockchain-engineer (active, custom, phase-specific — added in v1.26)
|
||||
- **active:** true
|
||||
- **phase_specific:** true (created for v1.26 P1; removed after P1
|
||||
unless the chain has ongoing work in P2..P4)
|
||||
- **reason:** The pilot introduces a homegrown blockchain — a domain
|
||||
beyond the default four personas. Owns the chain core (block, ledger,
|
||||
validator, REQ-310), the order-matching engine (REQ-311), the
|
||||
settlement service (REQ-312), and the consumer `contract.yaml`
|
||||
(REQ-313) + deploy invocation (REQ-314).
|
||||
- **domain:** blockchain consensus (PoA, single validator), order
|
||||
matching (limit order book, price-time priority), settlement
|
||||
(T+1, finality = block commit), consumer-repo deploy model.
|
||||
- **frameworks:** Python 3.12 (the chain is Python, not Solidity/Go —
|
||||
it's a homegrown ledger, not a smart-contract platform), pytest,
|
||||
YAML (contract.yaml), GitHub Actions / Gitea Actions (deploy.yml
|
||||
invocation).
|
||||
- **territory:** `/root/nova-blockchain-exchange/` (the consumer repo:
|
||||
`chain/`, `engine/`, `settlement/`, `contract.yaml`,
|
||||
`contracts/*.yml`, `.github/workflows/deploy.yml`,
|
||||
`.gitea/workflows/deploy.yml`, `tests/`).
|
||||
- **constraints:** the chain is deterministic (same inputs → same block)
|
||||
— it is automation, not AI (NORTH_STAR Objective #2 tenet); equities
|
||||
only (D-200); single validator PoA (D-201); the consumer deploy MUST
|
||||
go through `deploy.yml@v1.25` (no direct terraform apply); the
|
||||
contract MUST validate against `schemas/contract.schema.json`.
|
||||
|
||||
## Phase-specific notes
|
||||
## Deactivated (1)
|
||||
|
||||
- No phase-specific personas. Both active personas span P1-P3.
|
||||
- P4 (final review + audit + ship) is lead-developer territory
|
||||
(orchestration + docs completion).
|
||||
### frontend-engineer (inactive)
|
||||
- **active:** false
|
||||
- **phase_specific:** false
|
||||
- **reason:** The pilot has no UI — the blockchain exchange is a
|
||||
backend service (matching engine + settlement). The consumer repo
|
||||
has no web/frontend. Reactivated if a future milestone adds a trading
|
||||
dashboard.
|
||||
|
||||
## Phase-Specific Notes
|
||||
|
||||
- **blockchain-engineer** is created for v1.26 P1 (blockchain core +
|
||||
order engine + settlement). If P2..P4 have no chain changes, the
|
||||
persona is removed after P1 (the chain is a stable substrate for the
|
||||
pilot run). If P2 (consumer-contract-and-deploy) requires chain
|
||||
adjustments, the persona stays through P2.
|
||||
- **policy-engineer** is active for P3 (pilot-metrics-and-policies) +
|
||||
may consult on P4 (pilot run policy verification).
|
||||
- **data-engineer** is active for P3 (DynamoDB primitive + outcome
|
||||
backfill + env-JSON) + P4 (regression CAP-025 may touch the registry).
|
||||
|
||||
## Territory Enforcement
|
||||
|
||||
- **Mode:** `warn` (the pilot is cross-territory by nature — the
|
||||
consumer repo + the platform repo share the milestone; the
|
||||
blockchain-engineer works in the consumer repo, backend/data/policy
|
||||
engineers work in the platform repo).
|
||||
- **Cross-territory collisions:** REQ-322 (DynamoDB primitive) is
|
||||
data-engineer territory, but the adapter test update
|
||||
(`tests/test_adapter.py` `EXPECTED_L1_KEYS`) is backend-engineer
|
||||
territory. The lead-developer resolves: data-engineer authors the
|
||||
module + registry; backend-engineer updates the test assertion
|
||||
(the test is backend territory, the module is data territory).
|
||||
+378
-148
@@ -1,180 +1,410 @@
|
||||
# PLAN — v1.24 (Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement)
|
||||
# PLAN — v1.26 (Live Pilot Estate Activation)
|
||||
|
||||
> Feature milestone (one `feat` phase: env-transition destroy enforcement;
|
||||
> the rest are `fix`/`docs`/`test`). Tags on the **v1.23.x** line:
|
||||
> v1.23.0 (P0) → v1.23.1 (P1) → v1.23.2 (P2) → v1.23.3 (P3) → v1.23.4 (P4
|
||||
> final = milestone release). 15 requirements (REQ-276..290), 4 phases +
|
||||
> P0 pre-execution.
|
||||
|
||||
## Phase breakdown
|
||||
|
||||
### Phase P1 — consumer-guide-fixes (Wave 1, lead-developer)
|
||||
|
||||
**Type:** `docs` + `fix` + `test` (consumer guide accuracy + guide test updates)
|
||||
|
||||
**Requirements:** REQ-276, REQ-277, REQ-278, REQ-279, REQ-280, REQ-281, REQ-290
|
||||
|
||||
**Must-haves:**
|
||||
- `docs/consumer-guide.md` Step 3 contract fields table corrected (REQ-276)
|
||||
- `docs/consumer-guide.md` Step 4 caller consistent with Step 2 (REQ-277)
|
||||
- `docs/consumer-guide.md` Step 5 stage 8 "(dev only)" → "(autonomous in dev; higher environments apply after HITL attestation)" (REQ-278)
|
||||
- `docs/consumer-guide.md` Step 8 rewritten with destroy-then-rebuild semantics + cross-ref to Shape B (REQ-279)
|
||||
- `docs/consumer-guide.md` Per-env section gains Shape B lead sentence (REQ-280)
|
||||
- `docs/consumer-guide.md` Reference table `@v1.19` wording corrected (REQ-281)
|
||||
- `tests/test_consumer_guide_per_env_section.py` updated: rename `test_consumer_guide_states_no_field_editing` → `test_consumer_guide_documents_both_promotion_shapes`; add `test_consumer_guide_documents_destroy_on_env_change` (REQ-290)
|
||||
|
||||
**Vertical slice:** A reader of `docs/consumer-guide.md` can promote via
|
||||
either shape (A: edit environment + platform destroys prior; B: per-env
|
||||
caller workflow) without contradiction. The guide's field table, caller
|
||||
examples, and stage descriptions match the actual schema and platform
|
||||
behavior. All consumer-guide tests pass.
|
||||
|
||||
**Files touched:**
|
||||
- `docs/consumer-guide.md`
|
||||
- `tests/test_consumer_guide_per_env_section.py`
|
||||
|
||||
**Verification:** `pytest tests/test_consumer_guide_per_env_section.py -v`
|
||||
(all 7 tests pass). Manual read of `docs/consumer-guide.md` end-to-end
|
||||
for internal consistency.
|
||||
> Feature milestone. Tags on the **v1.25.x** line: v1.25.0 (P0) →
|
||||
> v1.25.1 (P1) → v1.25.2 (P2) → v1.25.3 (P3) → v1.25.4 (P4) → v1.25.5
|
||||
> (P5 final = milestone release). 13 requirements (REQ-310..322),
|
||||
> 5 phases (P0 pre-execution + 4 execution + 1 final). Multi-project:
|
||||
> `acdl` (platform) + `nova-blockchain-exchange` (consumer). Tags run
|
||||
> on the previous minor's patch line per `run.md` versioning logic
|
||||
> (feature milestone — at least one feat phase; progressive patches per
|
||||
> phase; the final phase's patch IS the milestone release; no separate
|
||||
> minor tag).
|
||||
|
||||
---
|
||||
|
||||
### Phase P2 — env-transition-detect-and-destroy (Wave 2, backend-engineer)
|
||||
## Phase 0 — Pre-Execution (complete, tag v1.25.0)
|
||||
|
||||
**Type:** `feat` (new platform feature: env-transition detect-and-destroy)
|
||||
SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → GRILL. All `.ciagent/`
|
||||
MD, research, plans. Ships as `v1.25.0` on the v1.25.x line.
|
||||
|
||||
**Requirements:** REQ-282, REQ-283, REQ-284, REQ-285, REQ-286, REQ-287
|
||||
**Pre-run (Workstream A, on main before branch gate):**
|
||||
- A1: flaky test fix (commit `8c68d68`, pushed).
|
||||
- A2: ACDL_*→NOVA_* bootstrap migration (commit `f844fea`, pushed).
|
||||
- A3: AWS bootstrap — S3 state bucket + DynamoDB outbox created.
|
||||
- A4: `nova-blockchain-exchange` Gitea repo created + cloned.
|
||||
|
||||
**Must-haves:**
|
||||
- `core/env_transition.py` new module: `detect_prior_env()` + `record_applied_env()` with boto3 DynamoDB queries (REQ-282, REQ-283)
|
||||
- `scripts/run_platform.sh` Step 0b: environment-transition check — detect prior env, re-resolve with `environment_override=prior_env` + `deletion_protection=false`, `terraform init -reconfigure` + `terraform destroy -auto-approve` against prior state key, emit `nova.env.destroyed` evidence event, fail closed on destroy failure (REQ-284)
|
||||
- `scripts/run_platform.sh` records applied env after successful apply (REQ-285)
|
||||
- `.github/workflows/deploy.yml` passes `NOVA_CONSUMER_REPO=${{ github.repository }}` to `run_platform.sh` (REQ-286)
|
||||
- `adapters/terraform/adapter.py` state-key block gains doc comment (REQ-287)
|
||||
|
||||
**Vertical slice:** When a consumer changes `environment:` on a stable
|
||||
`contract.id`, the pipeline detects the prior env from DynamoDB, destroys
|
||||
the prior env's Terraform state (with `deletion_protection=false`), emits
|
||||
an evidence event, and only then applies the new env. If the destroy
|
||||
fails, the pipeline exits non-zero (no orphan path). If no prior env
|
||||
exists (first deploy or Shape B), the pipeline proceeds normally.
|
||||
|
||||
**Files touched:**
|
||||
- `core/env_transition.py` (NEW)
|
||||
- `scripts/run_platform.sh`
|
||||
- `.github/workflows/deploy.yml`
|
||||
- `adapters/terraform/adapter.py` (doc comment only)
|
||||
|
||||
**Verification:** `python3 -m py_compile core/env_transition.py`.
|
||||
`pytest tests/test_pipeline.py tests/test_deploy_workflow_env_input.py -v`
|
||||
(existing tests still pass). The new tests in P3 validate the behavior.
|
||||
|
||||
**Key implementation notes (from RESEARCH §5 pitfalls):**
|
||||
1. The destroy step must re-resolve with `environment_override=prior_env`
|
||||
so the emitted TF matches the prior env's resources.
|
||||
2. `terraform init -reconfigure` is required when switching state backends.
|
||||
3. `deletion_protection: false` must be injected (same as decommission
|
||||
Step 2 in `scripts/run_decommission.sh:34-37`) or `prevent_destroy`
|
||||
blocks the destroy.
|
||||
4. DynamoDB unreachable in local/CI → log warning + return `None`
|
||||
(conservative, no prior env assumed).
|
||||
5. The state key `spike/{id}/{env}/terraform.tfstate` stays as-is — the
|
||||
env segment is what lets the destroy target the prior env.
|
||||
**Phase 0 stages (on `phase/00-specify-clarify-research-plan`):**
|
||||
- SPECIFY: v1.26 established in config.json + PROJECT.md + ROADMAP.md +
|
||||
`.ciagent/nova-blockchain-exchange/{PROJECT,REQUIREMENTS,ROADMAP}.md`.
|
||||
- CLARIFY: 10 ambiguities resolved (D-200..D-213).
|
||||
- RESEARCH: PoA blockchain, deploy model, DynamoDB gap (REQ-322),
|
||||
metric grounding, persona assessment (5 personas).
|
||||
- IDEATE: 7 ideas accepted (I1..I7 → REQ-315..322), 3 deferred.
|
||||
- PLAN: this file.
|
||||
- GRILL: adversarial review (binding verdicts).
|
||||
|
||||
---
|
||||
|
||||
### Phase P3 — env-transition-tests (Wave 3, backend-engineer + lead-developer)
|
||||
## Phase 1 — blockchain-core (tag v1.25.1)
|
||||
|
||||
**Type:** `test` (new test coverage for env-transition + pipeline integration)
|
||||
**Goal:** The consumer repo has a working homegrown PoA blockchain +
|
||||
order-matching engine + settlement service. All unit tests pass in the
|
||||
consumer repo's own CI.
|
||||
|
||||
**Requirements:** REQ-288, REQ-289
|
||||
**Project:** `nova-blockchain-exchange` (consumer repo).
|
||||
**Branch:** `nova-blockchain-exchange/phase/01-blockchain-core`.
|
||||
**Persona:** blockchain-engineer (primary), lead-developer (coordination).
|
||||
|
||||
**Must-haves:**
|
||||
- `tests/test_env_transition.py` (NEW): `detect_prior_env` returns `None` when no record; returns prior env when record differs; returns `None` when record matches; `record_applied_env` writes record. Uses moto for DynamoDB (REQ-288)
|
||||
- `tests/test_run_platform_env_transition.py` (NEW): asserts `run_platform.sh` has Step 0b; calls `env_transition.py detect`; calls `terraform destroy` on prior env; fails closed on destroy failure; records applied env after success (REQ-289)
|
||||
### Wave 1 — chain core (REQ-310)
|
||||
- **Task 1.1** (blockchain-engineer): `chain/block.py` — Block dataclass
|
||||
(index, timestamp, prev_hash, transactions, nonce, hash).
|
||||
`compute_hash()` deterministic (SHA-256). Unit test: `test_block.py`.
|
||||
- **Task 1.2** (blockchain-engineer): `chain/ledger.py` — Ledger class:
|
||||
`append_block()`, `verify_chain()`, `get_block(index)`,
|
||||
`get_latest_block()`. Genesis block on init. Unit test: `test_ledger.py`.
|
||||
- **Task 1.3** (blockchain-engineer): `chain/validator.py` — PoA
|
||||
validator: single validator (config-driven), `propose_block(transactions)`
|
||||
→ Block, `commit_block(block)`. Unit test: `test_validator.py`.
|
||||
|
||||
**Vertical slice:** The env-transition detect-and-destroy behavior is
|
||||
fully covered by automated tests. The DynamoDB query logic is unit-tested
|
||||
with moto. The pipeline orchestration is tested via shell-text assertions
|
||||
(pattern from `tests/test_pipeline.py:79-95`).
|
||||
### Wave 2 — order engine + settlement (REQ-311, REQ-312) — parallel with Wave 1 tail
|
||||
- **Task 2.1** (blockchain-engineer): `engine/order.py` — Order
|
||||
dataclass (id, side, symbol, price, size, timestamp).
|
||||
- **Task 2.2** (blockchain-engineer): `engine/order_book.py` —
|
||||
OrderBook: `add_order(order)`, `match_orders()` → list of Match
|
||||
(price-time priority, partial fills). Unit test: `test_order_book.py`.
|
||||
- **Task 2.3** (blockchain-engineer): `settlement/service.py` —
|
||||
SettlementService: `settle(match)` → SettlementTransaction,
|
||||
`submit(ledger)`. Idempotent (re-settling a match is a no-op once
|
||||
final). Finality = block commit. Unit test: `test_settlement.py`.
|
||||
|
||||
**Files touched:**
|
||||
- `tests/test_env_transition.py` (NEW)
|
||||
- `tests/test_run_platform_env_transition.py` (NEW)
|
||||
### Wave 3 — consumer CI (cross-cutting)
|
||||
- **Task 3.1** (blockchain-engineer): `.github/workflows/ci.yml` +
|
||||
`.gitea/workflows/ci.yml` — lint + pytest on chain/engine/settlement.
|
||||
- **Task 3.2** (lead-developer): `nova-blockchain-exchange/README.md` —
|
||||
repo overview + dev setup.
|
||||
|
||||
**Verification:** `pytest tests/test_env_transition.py tests/test_run_platform_env_transition.py -v` (all new tests pass). Full suite: `pytest tests/ -k "env_transition or consumer_guide or pipeline or adapter or deploy_workflow" -v`.
|
||||
**Must-haves (verify before ship):**
|
||||
- `pytest tests/` in the consumer repo passes (chain integrity, hash
|
||||
determinism, genesis, append/verify, match priority, partial fills,
|
||||
settlement idempotency, finality check).
|
||||
- The chain is deterministic (replay produces the same hash chain).
|
||||
- The consumer CI workflow runs on push.
|
||||
|
||||
**Ship:** tag `v1.25.1`, merge `phase/01` → `milestone/v1.26-pilot-activation`,
|
||||
Gitea release (best-effort). Delete `phase/01`.
|
||||
|
||||
---
|
||||
|
||||
### Phase P4 — final-review-ship (Wave 4, lead-developer)
|
||||
## Phase 2 — consumer-contract-and-deploy (tag v1.25.2)
|
||||
|
||||
**Type:** `docs` (review + audit + milestone ship)
|
||||
**Goal:** The consumer repo declares its infrastructure via
|
||||
`contract.yaml` (validated against the platform's schema) + invokes the
|
||||
platform's `deploy.yml@v1.25` workflow. The contract references the
|
||||
`microservice` (ECS), `dynamodb`, + `s3` modules.
|
||||
|
||||
**Requirements:** (none new — milestone completion)
|
||||
**Project:** `nova-blockchain-exchange` (consumer repo) + `acdl`
|
||||
(platform repo — for the `deploy.yml@v1.25` ref + the `v1.25` floating
|
||||
tag).
|
||||
**Branch:** `nova-blockchain-exchange/phase/02-contract-and-deploy`.
|
||||
**Persona:** blockchain-engineer (contract authoring), data-engineer
|
||||
(registry/DynamoDB dependency check), lead-developer (deploy.yml ref).
|
||||
|
||||
**Must-haves:**
|
||||
- Multi-persona code review across P1-P3 changes (ci-code-reviewer)
|
||||
- Project health audit (ci-doc-verifier + ci-audit)
|
||||
- Milestone ship: tag v1.23.4 (final phase patch = milestone release), merge to main, Gitea release
|
||||
- Update REQUIREMENTS.md traceability (all REQ-276..290 → complete)
|
||||
- Update ROADMAP.md (v1.24 → complete)
|
||||
### Wave 1 — contract (REQ-313)
|
||||
- **Task 1.1** (blockchain-engineer): `contract.yaml` — id
|
||||
(`blkex`), name (`blockchain-exchange`), environment (dev),
|
||||
infrastructure block (microservice + dynamodb + s3).
|
||||
- **Task 1.2** (blockchain-engineer): `contracts/blockchain-exchange.dev.yml`,
|
||||
`.qa.yml`, `.prod.yml` — per-env variants.
|
||||
- **Task 1.3** (blockchain-engineer): `tests/test_contract_validates.py`
|
||||
— schema validation against the platform's
|
||||
`schemas/contract.schema.json`.
|
||||
|
||||
### Wave 2 — deploy invocation (REQ-314)
|
||||
- **Task 2.1** (blockchain-engineer): `.github/workflows/deploy.yml` —
|
||||
`uses: acdl/.github/workflows/deploy.yml@v1.25` with
|
||||
`with: { contract: contract.yaml, mode: full, environment: dev }`.
|
||||
- **Task 2.2** (blockchain-engineer): `.gitea/workflows/deploy.yml` —
|
||||
byte-identical mirror.
|
||||
- **Task 2.3** (blockchain-engineer): `tests/test_deploy_workflow_invocation.py`
|
||||
— asserts the `uses:` ref + inputs.
|
||||
|
||||
### Wave 3 — platform floating tag (cross-cutting)
|
||||
- **Task 3.1** (lead-developer, on `acdl` repo): verify the `v1.25`
|
||||
floating tag exists (created by `release.yml` on merge to main). If
|
||||
not, create it pointing at the `v1.25.0` tag (Phase 0 ship).
|
||||
|
||||
**Must-haves (verify before ship):**
|
||||
- `contract.yaml` validates against `schemas/contract.schema.json`.
|
||||
- The deploy workflow invocation asserts the correct `uses:` ref +
|
||||
inputs.
|
||||
- The `v1.25` floating tag resolves.
|
||||
|
||||
**Ship:** tag `v1.25.2`, merge `phase/02` → milestone, Gitea release.
|
||||
Delete `phase/02`.
|
||||
|
||||
---
|
||||
|
||||
## Wave ordering
|
||||
## Phase 3 — pilot-metrics-and-policies (tag v1.25.3)
|
||||
|
||||
```
|
||||
Wave 1 (P1): consumer-guide-fixes [lead-developer]
|
||||
↓
|
||||
Wave 2 (P2): env-transition-detect-and-destroy [backend-engineer]
|
||||
↓
|
||||
Wave 3 (P3): env-transition-tests [backend-engineer + lead-developer]
|
||||
↓
|
||||
Wave 4 (P4): final-review-ship [lead-developer]
|
||||
```
|
||||
**Goal:** The platform repo gains the metric-grounding emitters, the
|
||||
kyverno-json pilot policies, the DynamoDB L1 primitive, the env-JSON
|
||||
wiring reconciliation, + the pilot regression CAP. The Post-Pilot
|
||||
metrics are grounded (outcome backfill + escalation reason); the pilot-
|
||||
readiness + settlement-finality policies are in place.
|
||||
|
||||
**Dependencies:**
|
||||
- P2 depends on P1: the Step 8 rewrite in P1 documents the destroy
|
||||
semantics that P2 implements. Doing P1 first ensures the docs and code
|
||||
land in the right order (docs describe the intended behavior, then code
|
||||
implements it).
|
||||
- P3 depends on P2: the tests validate the env-transition module and
|
||||
pipeline Step 0b that P2 creates.
|
||||
- P4 depends on P1+P2+P3: the final review covers all changes.
|
||||
**Project:** `acdl` (platform repo).
|
||||
**Branch:** `acdl/phase/03-pilot-metrics-and-policies` (platform branch).
|
||||
**Personas:** backend-engineer (emitters + adapter + regression),
|
||||
data-engineer (DynamoDB primitive + env JSON + collector),
|
||||
policy-engineer (kyverno-json policies).
|
||||
|
||||
**Parallelization:** P1 and P2 could run in parallel (different
|
||||
territories: docs vs code), but the wave ordering is sequential for
|
||||
safety — if P1's Step 8 rewrite reveals a design issue, P2's
|
||||
implementation should follow the corrected design. With
|
||||
`parallelization.enabled=true` and `min_plans_for_parallel=2`, the
|
||||
orchestrator *could* run them concurrently; however, the dependency
|
||||
(P2 follows P1's design) makes sequential the safer choice. P3 must
|
||||
follow P2 (tests validate the code). P4 must follow all.
|
||||
### Wave 1 — DynamoDB primitive (REQ-322) — data-engineer
|
||||
- **Task 1.1** (data-engineer): `modules/l1/dynamodb/interface.json` —
|
||||
stack type `aws:dynamodb:table`, inputs (table_name, region, pk, sk,
|
||||
billing_mode), outputs (table_arn, table_name).
|
||||
- **Task 1.2** (data-engineer): `modules/l1/dynamodb/terraform/main.tf`
|
||||
— `resource "aws_dynamodb_table" "this"` (PK + optional SK,
|
||||
`PAY_PER_REQUEST` default, encryption + PITR enabled per v1.8 NFR).
|
||||
- **Task 1.3** (data-engineer): `modules/l1/dynamodb/README.md` +
|
||||
`instance.json`.
|
||||
- **Task 1.4** (data-engineer): `modules/registry.json` — `dynamodb`
|
||||
entry (kind `l1`, `terraform_dir`).
|
||||
- **Task 1.5** (data-engineer): `modules/README.md` — catalog index.
|
||||
|
||||
## Requirement → Phase mapping
|
||||
### Wave 2 — metric grounding (REQ-317, REQ-318) — backend-engineer + data-engineer — parallel
|
||||
- **Task 2.1** (backend-engineer): `core/metrics/outcome_backfill.py` —
|
||||
`backfill(decision_id, outcome)` updates `fact_decision.outcome` +
|
||||
`backfilled_at`. Reads run-manifest events.
|
||||
- **Task 2.2** (backend-engineer): `core/metrics/collector.py` —
|
||||
invokes backfill after run completion.
|
||||
- **Task 2.3** (backend-engineer): `tests/test_outcome_backfill.py`.
|
||||
- **Task 2.4** (backend-engineer): `core/confidence_signal.py` —
|
||||
`ai.decision.made` gains `escalation_reason: 'confidence'` when
|
||||
`band == 'block'`.
|
||||
- **Task 2.5** (backend-engineer): `core/metrics/collector.py` —
|
||||
persists `escalation_reason` into `fact_run`.
|
||||
- **Task 2.6** (backend-engineer): `tests/test_confidence_escalation_reason.py`.
|
||||
|
||||
| REQ | Phase | Type | Description |
|
||||
|-----|-------|------|-------------|
|
||||
| REQ-276 | P1 | docs | Contract fields table corrected |
|
||||
| REQ-277 | P1 | docs | Step 4 caller consistent with Step 2 |
|
||||
| REQ-278 | P1 | docs | Step 5 stage 8 "dev only" corrected |
|
||||
| REQ-279 | P1 | docs | Step 8 rewritten with destroy semantics |
|
||||
| REQ-280 | P1 | docs | Per-env section Shape B lead sentence |
|
||||
| REQ-281 | P1 | docs | Reference table @v1.19 wording corrected |
|
||||
| REQ-282 | P2 | feat | env_transition.py detect_prior_env() |
|
||||
| REQ-283 | P2 | feat | env_transition.py record_applied_env() |
|
||||
| REQ-284 | P2 | feat | run_platform.sh Step 0b detect-and-destroy |
|
||||
| REQ-285 | P2 | feat | run_platform.sh records applied env |
|
||||
| REQ-286 | P2 | feat | deploy.yml passes NOVA_CONSUMER_REPO |
|
||||
| REQ-287 | P2 | docs | adapter.py state-key doc comment |
|
||||
| REQ-288 | P3 | test | test_env_transition.py |
|
||||
| REQ-289 | P3 | test | test_run_platform_env_transition.py |
|
||||
| REQ-290 | P1 | test | consumer guide test updates |
|
||||
### Wave 3 — env-JSON wiring + adapter (REQ-319) — backend-engineer + data-engineer — parallel
|
||||
- **Task 3.1** (backend-engineer): `adapters/terraform/adapter.py` —
|
||||
reads `env.state_backend.bucket` when present (fallback to computed
|
||||
name for backwards compat).
|
||||
- **Task 3.2** (data-engineer): `core/environments/dev.json` —
|
||||
`account_id` → `581513795199`, `state_backend.bucket` →
|
||||
`nova-tfstate-581513795199-us-east-1`.
|
||||
- **Task 3.3** (data-engineer): `core/environments/{qa,prod,dr}.json` —
|
||||
`state_backend.bucket` updated; `account_id` stays placeholder
|
||||
(pilot-readiness policy blocks apply on placeholder, D-208).
|
||||
- **Task 3.4** (backend-engineer): `tests/test_adapter_state_backend.py`.
|
||||
- **Task 3.5** (backend-engineer): `tests/test_adapter.py` — add
|
||||
`dynamodb` to `EXPECTED_L1_KEYS` + a resolution + emission test
|
||||
(cross-territory: data-engineer authored the module, backend-engineer
|
||||
owns the test).
|
||||
|
||||
## Tag plan
|
||||
### Wave 4 — kyverno-json policies (REQ-315, REQ-320) — policy-engineer — parallel
|
||||
- **Task 4.1** (policy-engineer):
|
||||
`adapters/kyverno-json/policies/settlement-finality/all-matches-committed.json`
|
||||
— kyverno-json policy over settlement-service status JSON (asserts
|
||||
`all_committed: true`). **Note (G-Q6):** the policy is authored +
|
||||
tested in v1.26; *enforcement* is deferred to the milestone that
|
||||
binds qa/prod/dr (D-208 — the policy gates promotions, not dev
|
||||
applies).
|
||||
- **Task 4.2** (policy-engineer):
|
||||
`adapters/kyverno-json/policies/pilot-readiness/no-placeholder-account.json`
|
||||
— kyverno-json policy over env JSON (asserts
|
||||
`account_id != "000000000000"`).
|
||||
- **Task 4.3** (policy-engineer): `tests/test_settlement_finality_policy.py`
|
||||
— passing + failing fixtures; skip when `kj` absent.
|
||||
- **Task 4.4** (policy-engineer): `tests/test_pilot_readiness_policy.py`
|
||||
— passing (real account) + failing (placeholder) fixtures; skip when
|
||||
`kj` absent.
|
||||
|
||||
- P0 (this phase): `v1.23.0` — pre-execution patch
|
||||
- P1: `v1.23.1` — consumer guide fixes
|
||||
- P2: `v1.23.2` — env-transition detect-and-destroy
|
||||
- P3: `v1.23.3` — env-transition tests
|
||||
- P4: `v1.23.4` — final review + ship = **milestone release**
|
||||
### Wave 5 — regression CAP (REQ-316) — backend-engineer
|
||||
- **Task 5.1** (backend-engineer): `core/regression_verify.py` —
|
||||
CAP-025 (live-pilot-apply): the round-trip assertion.
|
||||
- **Task 5.2** (backend-engineer): `tests/test_regression_pilot.py`.
|
||||
|
||||
**Must-haves (verify before ship):**
|
||||
- `pytest tests/` in the platform repo passes (170 existing + new tests).
|
||||
- The DynamoDB primitive resolves + emits valid Terraform.
|
||||
- The outcome backfill updates `fact_decision.outcome` (not `pending`).
|
||||
- The `escalation_reason` field is emitted on `block` band.
|
||||
- The adapter reads `env.state_backend.bucket` from the env JSON.
|
||||
- The 2 new kyverno-json policies pass on valid fixtures + fail on
|
||||
invalid fixtures (skip when `kj` absent).
|
||||
- CAP-025 is in the regression gate.
|
||||
- No existing tests regress (170 baseline holds).
|
||||
|
||||
**Ship:** tag `v1.25.3`, merge `phase/03` → milestone, Gitea release.
|
||||
Delete `phase/03`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — pilot-run-and-docs (tag v1.25.4)
|
||||
|
||||
**Goal:** The pilot estate runs end-to-end against live AWS
|
||||
`581513795199` (contract resolve → adapter compile → terraform plan →
|
||||
policy scan → confidence signal → attestation → outbox record). Docs +
|
||||
adapter README + onboarding guide are complete.
|
||||
|
||||
**Project:** `nova-blockchain-exchange` (consumer repo — the run) +
|
||||
`acdl` (platform repo — docs).
|
||||
**Branch:** `acdl/phase/04-pilot-run-and-docs` (platform branch for
|
||||
docs); the run happens via the consumer's `deploy.yml` invocation.
|
||||
**Personas:** blockchain-engineer (the run), lead-developer (docs),
|
||||
backend-engineer (regression CAP-025 verification).
|
||||
|
||||
### Wave 1 — the pilot run (REQ-316 verification, live)
|
||||
- **Task 1.1** (blockchain-engineer): trigger the consumer's
|
||||
`deploy.yml` with `mode: full, environment: dev` against
|
||||
`581513795199`. The workflow checks out the consumer + platform
|
||||
repos, runs `run_platform.sh`, applies the contract (ECS +
|
||||
DynamoDB + S3), records the decision + attestation.
|
||||
- **Task 1.2** (backend-engineer): verify CAP-025 (regression gate)
|
||||
passes against the live run.
|
||||
- **Task 1.3** (blockchain-engineer): capture the run's
|
||||
`ai.decision.made` + `attestation.recorded` events from the Decision
|
||||
Ledger → evidence for the milestone ship.
|
||||
|
||||
### Wave 2 — docs (REQ-321)
|
||||
- **Task 2.1** (lead-developer): `adapters/README.md` — new consumer
|
||||
row + fix the stale `TYPE_MAP` references (IDEATE I8).
|
||||
- **Task 2.2** (lead-developer): `docs/METRICS.md` — Post-Pilot metrics
|
||||
grounded note (the 3 targets now have non-zero denominators post-run).
|
||||
- **Task 2.3** (lead-developer): `.ciagent/ARCHITECTURE.md` §12.8
|
||||
(Pilot Estate).
|
||||
- **Task 2.4** (lead-developer):
|
||||
`.ciagent/nova-blockchain-exchange/README.md` — consumer onboarding
|
||||
guide (how to invoke `deploy.yml@v1.25`, what secrets to set, what
|
||||
the contract shape is).
|
||||
|
||||
**Must-haves (verify before ship):**
|
||||
- The pilot run completes end-to-end (apply succeeds, decision recorded,
|
||||
attestation recorded for dev — autonomous, no human approver).
|
||||
- CAP-025 passes.
|
||||
- The 3 Post-Pilot metrics have non-zero denominators (the run
|
||||
contributed to `fact_run` + `fact_decision`).
|
||||
- Docs are complete (adapter README, METRICS.md, ARCHITECTURE.md §12.8,
|
||||
consumer onboarding guide).
|
||||
|
||||
**Ship:** tag `v1.25.4`, merge `phase/04` → milestone, Gitea release.
|
||||
Delete `phase/04`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — final review + audit + milestone ship (tag v1.25.5)
|
||||
|
||||
**Goal:** Multi-persona code review across P1..P4. Audit (reconstruction
|
||||
test, branch hygiene, commit discipline). Milestone ship: merge to main,
|
||||
tag `v1.25.5` (= the v1.26 release), Gitea release with full milestone
|
||||
summary, delete all milestone branches.
|
||||
|
||||
**Project:** both (`acdl` + `nova-blockchain-exchange`).
|
||||
**Branch:** `phase/05-final-review-ship`.
|
||||
**Personas:** lead-developer (review + audit + ship), backend-engineer
|
||||
(review), data-engineer (review), policy-engineer (review),
|
||||
blockchain-engineer (review — the chain core is reviewed).
|
||||
|
||||
### Wave 1 — review
|
||||
- **Task 1.1** (lead-developer): `ciagent-review` — multi-persona code
|
||||
review across P1..P4. Auto-fix P0; flag P1+ for post-hoc review.
|
||||
- **Task 1.2** (all personas): fix P0 issues in this phase.
|
||||
|
||||
### Wave 2 — audit
|
||||
- **Task 2.1** (lead-developer): `ciagent-audit` — reconstruction test
|
||||
(git log ↔ `.ciagent/`), branch hygiene, commit discipline.
|
||||
- **Task 2.2** (lead-developer): fix critical audit issues in this phase.
|
||||
|
||||
### Wave 3 — milestone ship
|
||||
- **Task 3.1** (lead-developer): merge `phase/05` →
|
||||
`milestone/v1.26-pilot-activation` → `main`.
|
||||
- **Task 3.2** (lead-developer): tag `v1.25.5` (= the v1.26 release per
|
||||
prev-minor tagging rule).
|
||||
- **Task 3.3** (lead-developer): create Gitea release with full milestone
|
||||
summary (all phases, all 13 requirements).
|
||||
- **Task 3.4** (lead-developer): delete all milestone branches (local +
|
||||
remote). Tags preserve all history.
|
||||
- **Task 3.5** (lead-developer): update `.ciagent/nova-blockchain-exchange/REQUIREMENTS.md`
|
||||
(mark REQ-310..322 complete), `.ciagent/ROADMAP.md` (mark v1.26
|
||||
complete), `.ciagent/NORTH_STAR.md` (note Strategic Objectives #1 +
|
||||
#3 — first real consumer estate; Post-Pilot denominators activated).
|
||||
- **Task 3.6** (lead-developer): write checkpoint `stage: complete,
|
||||
phase: 5, phase_role: final` + clear checkpoint (milestone complete).
|
||||
|
||||
**Must-haves (verify before ship):**
|
||||
- Review: 0 P0 issues unfixed; P1+ flagged for post-hoc.
|
||||
- Audit: reconstruction test passes; branch hygiene clean; commit
|
||||
discipline clean.
|
||||
- Ship: `v1.25.5` tag exists; Gitea release created; milestone branches
|
||||
deleted; main has the milestone merge.
|
||||
|
||||
---
|
||||
|
||||
## Requirement → Phase Mapping
|
||||
|
||||
| REQ | Phase | Wave | Persona |
|
||||
|---|---|---|---|
|
||||
| REQ-310 (blockchain core) | P1 | W1 | blockchain-engineer |
|
||||
| REQ-311 (order engine) | P1 | W2 | blockchain-engineer |
|
||||
| REQ-312 (settlement) | P1 | W2 | blockchain-engineer |
|
||||
| REQ-313 (contract.yaml) | P2 | W1 | blockchain-engineer |
|
||||
| REQ-314 (deploy invocation) | P2 | W2 | blockchain-engineer |
|
||||
| REQ-315 (settlement-finality policy) | P3 | W4 | policy-engineer |
|
||||
| REQ-316 (pilot regression CAP) | P3 | W5 + P4 W1 | backend-engineer |
|
||||
| REQ-317 (outcome backfill) | P3 | W2 | backend-engineer |
|
||||
| REQ-318 (escalation reason) | P3 | W2 | backend-engineer |
|
||||
| REQ-319 (env-JSON wiring) | P3 | W3 | backend + data-engineer |
|
||||
| REQ-320 (pilot-readiness policy) | P3 | W4 | policy-engineer |
|
||||
| REQ-321 (docs) | P4 | W2 | lead-developer |
|
||||
| REQ-322 (DynamoDB primitive) | P3 | W1 | data-engineer |
|
||||
|
||||
---
|
||||
|
||||
## Wave Ordering Rationale
|
||||
|
||||
- **P1 W1 → W2:** the chain core (block + ledger + validator) must land
|
||||
before the order engine + settlement (they submit transactions to the
|
||||
ledger). W3 (CI) is cross-cutting + can land any time after W1.
|
||||
- **P2 W1 → W2:** the contract must land before the deploy invocation
|
||||
(the invocation references the contract). W3 (floating tag) is cross-
|
||||
cutting.
|
||||
- **P3 W1 (DynamoDB) first:** the contract (P2) references `dynamodb` —
|
||||
the primitive must exist before P2's contract can resolve. **Risk:**
|
||||
P2's contract references a module that doesn't exist until P3. Resolution: P2's contract is authored but the `test_contract_validates.py` test only checks schema validity (not registry resolution) — the registry resolution test is in P3 (after the primitive lands). The contract's `dynamodb` block is schema-valid (the schema is open); the registry resolution happens at apply time (P4).
|
||||
- **Alternative:** move REQ-322 to P2 W0 (before the contract). This
|
||||
avoids the P2→P3 dependency. **Decision: move REQ-322 to P2 W0.**
|
||||
See revised mapping below.
|
||||
|
||||
### Revised: REQ-322 → P2 W0
|
||||
|
||||
REQ-322 (DynamoDB primitive) lands in P2 Wave 0 (before the contract)
|
||||
so the contract's `dynamodb` block resolves at registry time, not just
|
||||
schema time. This makes P2 self-contained: the primitive + the contract
|
||||
+ the deploy invocation all land in P2.
|
||||
|
||||
| REQ | Phase | Wave | Persona |
|
||||
|---|---|---|---|
|
||||
| REQ-310 (blockchain core) | P1 | W1 | blockchain-engineer |
|
||||
| REQ-311 (order engine) | P1 | W2 | blockchain-engineer |
|
||||
| REQ-312 (settlement) | P1 | W2 | blockchain-engineer |
|
||||
| REQ-322 (DynamoDB primitive) | P2 | W0 | data-engineer |
|
||||
| REQ-313 (contract.yaml) | P2 | W1 | blockchain-engineer |
|
||||
| REQ-314 (deploy invocation) | P2 | W2 | blockchain-engineer |
|
||||
| REQ-315 (settlement-finality policy) | P3 | W4 | policy-engineer |
|
||||
| REQ-316 (pilot regression CAP) | P3 | W5 + P4 W1 | backend-engineer |
|
||||
| REQ-317 (outcome backfill) | P3 | W2 | backend-engineer |
|
||||
| REQ-318 (escalation reason) | P3 | W2 | backend-engineer |
|
||||
| REQ-319 (env-JSON wiring) | P3 | W3 | backend + data-engineer |
|
||||
| REQ-320 (pilot-readiness policy) | P3 | W4 | policy-engineer |
|
||||
| REQ-321 (docs) | P4 | W2 | lead-developer |
|
||||
|
||||
This revision is a binding plan decision (G-Q8 in the grill may
|
||||
challenge it).
|
||||
|
||||
---
|
||||
|
||||
## Future Hardening Items (not in v1.26 scope, documented per grill G-Q9)
|
||||
|
||||
- **`NOVA_AWS_*` key-split:** v1.26 uses a single `NOVA_AWS_*` key with
|
||||
root-equivalent permissions (D-207, confirmed empirically by the
|
||||
bootstrap). A future hardening milestone should split this into a
|
||||
`NOVA_BOOTSTRAP_AWS_*` root key (bootstrap only) + a least-privilege
|
||||
`NOVA_AWS_*` runner key (the spike-runner pattern). The pilot scope
|
||||
(single account, no production workloads, OIDC default) bounds the
|
||||
risk.
|
||||
- **Multi-account landing zone:** qa/prod/dr on separate accounts (D-208
|
||||
keeps them placeholder in v1.26).
|
||||
- **D-083 lift:** S3 Object Lock + JWS tamper-evident ledger (when the
|
||||
pilot becomes a production system, D-204).
|
||||
- **Multi-validator BFT consensus:** D-201.
|
||||
- **Other security types:** bonds (T+2), derivatives, options (D-200).
|
||||
@@ -1577,3 +1577,208 @@ New requirements REQ-263..REQ-275 — see `REQUIREMENTS.md` §v1.23.
|
||||
Summary: consolidation (REQ-263,264), style restoration (REQ-265,266,267),
|
||||
image inlining (REQ-268), python-pptx generator (REQ-269,270), word-count
|
||||
trim + loaded-scope-term removal (REQ-271,272), CI/tests/README (REQ-273,274,275).
|
||||
|
||||
## v1.25 — kyverno-json Unified Policy Engine
|
||||
|
||||
> **Active milestone.** Feature milestone (the primary compliance/policy
|
||||
> tool becomes kyverno-json, implemented behind a swappable adapter).
|
||||
> Branch: `milestone/v1.25-kyverno-json`. Tags run on the **v1.24.x**
|
||||
> patch line: `v1.24.0` (P0) → `v1.24.1..v1.24.4` (P1–P4) → `v1.24.5`
|
||||
> (P5 final = milestone release).
|
||||
|
||||
[Nova](https://github.com/kyverno/kyverno-json) `kyverno-json` is a
|
||||
runtime from the Kyverno ecosystem that applies Kyverno policies to
|
||||
**any JSON or YAML payload** — not just Kubernetes manifests. This
|
||||
milestone makes kyverno-json the **primary tool of choice for
|
||||
compliance / policy checks** in Nova, implemented as an **adapter**
|
||||
(the `PolicyEngine` protocol) so the platform may one day replace it
|
||||
with something else (e.g. OPA) without touching the confidence signal
|
||||
or the pipeline.
|
||||
|
||||
### Why
|
||||
|
||||
Nova's policy posture today is split across three engines with three
|
||||
different rule languages and three adapter shapes:
|
||||
|
||||
- **Checkov** (`adapters/terraform/policy/checkov_adapter.py`) — the
|
||||
runtime scanner over `terraform_plan` JSON; carries the
|
||||
`NOVA_TAG_NAMING` custom rule. Imperative YAML+Python rules.
|
||||
- **Wiz** (`adapters/wiz/wiz_adapter.py`) — security findings from the
|
||||
Wiz API; inactive unless credentials are present.
|
||||
- **Kyverno (K8s)** (`adapters/kyverno/kyverno_adapter.py`) — translates
|
||||
Kyverno `PolicyReport` results; **inactive for Terraform-only stacks**
|
||||
(the platform emits Terraform, not K8s manifests — D-053).
|
||||
|
||||
All three emit the same `schemas/policy_check_result.schema.json` shape
|
||||
that `core/confidence_signal.py` consumes engine-agnostically. The
|
||||
*contract* is already right; the *orchestration* is fragmented. There is
|
||||
no single place where "what Nova considers compliant" is declared —
|
||||
tagging lives in a Checkov custom rule, public-ingress in Checkov's
|
||||
`RULE_MAP`, env-transition destroy in `core/env_transition.py`
|
||||
(imperative Python), and capability regression in
|
||||
`core/regression_verify.py` (imperative Python). Each is a different
|
||||
language, each drifts independently, and the K8s Kyverno adapter can't
|
||||
help because it only speaks to K8s manifests.
|
||||
|
||||
`kyverno-json` fixes this: one declarative policy language (Kyverno
|
||||
policies with JMESPath assertions) that applies to **any** Nova
|
||||
artifact — the consumer contract, the resolved Stack IR, the
|
||||
Terraform plan JSON, and even the PolicyCheckResult list itself
|
||||
(meta-validation). It becomes the **unified orchestrator** of compliance
|
||||
checks, while Checkov and Wiz remain as raw-finding adapters that feed
|
||||
*into* kyverno-json meta-policies (so Nova-specific posture rules sit
|
||||
on top of, not beside, the scanner findings).
|
||||
|
||||
### What the milestone delivers
|
||||
|
||||
- **Swappable `PolicyEngine` protocol** (`core/policy_engine.py`) — a
|
||||
Python Protocol + registry selected from `config.json` (`policy.engine`,
|
||||
default `"kyverno-json"`). `KyvernoJsonEngine` implements it (shells
|
||||
to the `kyverno-json` CLI); a future `OpaEngine` implements the same
|
||||
protocol. The confidence signal and pipeline never import the engine
|
||||
directly — they go through the registry.
|
||||
- **`KyvernoJsonEngine` adapter** (`adapters/kyverno-json/`) —
|
||||
`evaluate(payload, policies) -> list[PolicyCheckResult]` translates
|
||||
kyverno-json native output to the existing PCR schema. Mirrors the
|
||||
Checkov/Wiz adapter pattern. `is_configured()` guard skips gracefully
|
||||
when the `kyverno-json` binary is absent (same pattern as the Wiz
|
||||
adapter — emits `SKIPPED`, never breaks the pipeline).
|
||||
- **Policies over all four Nova artifacts** under
|
||||
`adapters/kyverno-json/policies/`:
|
||||
- `contract/` — consumer contract JSON (shape + env-promotion rules).
|
||||
- `stack-ir/` — resolved Target Stack IR (tagging standard,
|
||||
public-ingress, encryption-by-default — ports of the v1.0/v1.8
|
||||
imperative rules into declarative policies).
|
||||
- `plan-json/` — `terraform show -json` output (plaintext secrets,
|
||||
IAM wildcards, KMS references — ports of Checkov's `RULE_MAP`).
|
||||
- `meta/` — policies over the merged PolicyCheckResult list itself
|
||||
(e.g. `block-on-any-critical` — the single declarative source of
|
||||
truth for "critical = block", with the existing
|
||||
`confidence_signal.py` hard-override kept as defense-in-depth).
|
||||
- **`run_platform.sh` Step 5 wiring** — Checkov/Wiz still run and emit
|
||||
raw PCRs; `KyvernoJsonEngine.evaluate()` runs plan-JSON policies in
|
||||
parallel; both PCR lists merge into the confidence signal's `policy`
|
||||
input. No change to `core/confidence_signal.py` (it already consumes
|
||||
`list[PolicyCheckResult]` engine-agnostically).
|
||||
- **Regression-gate-as-policy** (P4 — quality improvement from the
|
||||
IDEATE pass): the capability checks in
|
||||
`core/regression_verify.py` (CAP-013, CAP-023, CAP-024) become
|
||||
declarative kyverno-json policies over the capability-inventory JSON
|
||||
frontmatter. Capability regression becomes an audit artifact, not
|
||||
imperative Python.
|
||||
- **`policy-engineer` persona** (custom, added in RESEARCH) — owns the
|
||||
policy territory; declarative-policies constraint; kyverno-json +
|
||||
JMESPath frameworks.
|
||||
|
||||
**Phase count:** 6 (P0 pre-execution + 4 execution + 1 final).
|
||||
|
||||
**Hard constraints:**
|
||||
- DO NOT change `schemas/policy_check_result.schema.json` shape in a way
|
||||
that breaks existing adapters — the contract is the moat. The
|
||||
`engine` enum already includes `"kyverno"` and `"opa"`; v1.25 records
|
||||
carry `engine: "kyverno"` (no new enum value — decision in CLARIFY).
|
||||
- DO NOT remove Checkov or Wiz adapters — they remain as raw-finding
|
||||
sources feeding into kyverno-json meta-policies.
|
||||
- DO NOT remove the `confidence_signal.py` `PENALTY["critical"]: None`
|
||||
hard-override — it stays as defense-in-depth behind the declarative
|
||||
`block-on-any-critical` meta-policy (decision in CLARIFY).
|
||||
- DO NOT change `core/confidence_signal.py`'s input contract — it
|
||||
already consumes `list[PolicyCheckResult]`; v1.25 only changes *who
|
||||
produces* that list, not *what* the list is.
|
||||
- The platform must function with `kyverno-json` absent — `is_configured()`
|
||||
returns false → `SKIPPED` records → confidence signal proceeds (no
|
||||
hard dependency that breaks the "platform functions without AI /
|
||||
deterministic scripts" tenet — kyverno-json is deterministic, not AI).
|
||||
|
||||
### Requirements
|
||||
|
||||
New requirements REQ-291..REQ-309 — see `REQUIREMENTS.md` §v1.25.
|
||||
Summary: engine protocol + registry (REQ-291,292), kyverno-json engine
|
||||
impl (REQ-293,294), contract policies (REQ-295,296), stack-IR policies
|
||||
(REQ-297,298,299), 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)
|
||||
|
||||
> **Active milestone.** Feature milestone — the first real consumer
|
||||
> estate (a stock exchange on a homegrown PoA blockchain, equities
|
||||
> only) is activated against live AWS account `581513795199`, lifting
|
||||
> D-096. Branch: `milestone/v1.26-pilot-activation`. Tags run on the
|
||||
> **v1.25.x** patch line: `v1.25.0` (P0) → `v1.25.1..v1.25.4` (P1–P4)
|
||||
> → `v1.25.5` (P5 final = milestone release).
|
||||
>
|
||||
> **Multi-project mode:** this milestone introduces a 2nd tracked
|
||||
> project — `nova-blockchain-exchange` (Gitea repo
|
||||
> `continuous-intelligence/nova-blockchain-exchange`, local clone
|
||||
> `/root/nova-blockchain-exchange`). The platform repo (`acdl`) remains
|
||||
> the platform source; the consumer repo owns the app code +
|
||||
> `contract.yaml`. Both projects share the v1.26 milestone; `.ciagent/`
|
||||
> paths are per-project (`.ciagent/acdl/` for platform files — note: the
|
||||
> platform's existing flat `.ciagent/` files remain the primary set for
|
||||
> v1.26; the consumer's files live in `.ciagent/nova-blockchain-exchange/`).
|
||||
|
||||
### Why
|
||||
|
||||
NORTH_STAR.md has three Post-Pilot targets (Touchless Resolution ≥99%,
|
||||
Human Escalation <0.1%, AI Decision Accuracy ≥99.5%) whose measurement
|
||||
*pipeline* is grounded but whose *denominator* is zero — no consumer
|
||||
estate has ever run. v1.25 shipped the swappable policy engine; v1.26
|
||||
ships the first real consumer. The D-096 deferral (live AWS
|
||||
re-provisioning) is the single blocker; the pre-run (Workstream A)
|
||||
re-created the state bucket + outbox table, so the platform components
|
||||
exist. The milestone grounds the metrics (outcome backfill +
|
||||
escalation reason), wires the env JSON to the real account, and runs
|
||||
the pilot end-to-end.
|
||||
|
||||
### What the milestone delivers
|
||||
|
||||
- **Homegrown PoA blockchain** (`nova-blockchain-exchange` repo) —
|
||||
append-only blocks, single validator (pilot), deterministic block
|
||||
production, T+1 settlement finality = block commit. Equities only
|
||||
(bonds/derivatives/options deferred).
|
||||
- **Order-matching engine** — limit order book, price-time priority.
|
||||
- **Settlement service** — T+1, idempotent, finality = block commit.
|
||||
- **Consumer `contract.yaml`** — declares the exchange stack; validated
|
||||
against `schemas/contract.schema.json`; per-env variants.
|
||||
- **Consumer deploy via `deploy.yml@v1.25`** — the reusable workflow
|
||||
applies the contract, runs the policy engine, computes the
|
||||
confidence signal, gates qa/prod/dr with HITL attestation, and records
|
||||
every decision in the Decision Ledger.
|
||||
- **3 Post-Pilot metrics grounded** — outcome backfill (AI Decision
|
||||
Accuracy), `reason='confidence'` escalation tag (Human Escalation
|
||||
Frequency), and the pilot run itself (Touchless Resolution Rate
|
||||
denominator activates).
|
||||
- **3 kyverno-json policies extending v1.25** — settlement-finality
|
||||
(securities-specific), pilot-readiness (no placeholder account),
|
||||
and the existing meta-policies (block-on-any-critical,
|
||||
tagging-rules-agree) apply over the pilot's PCRs.
|
||||
- **Env-JSON `state_backend` wiring reconciliation** — the adapter
|
||||
reads `state_backend.bucket` from the env JSON (closing the wiring
|
||||
gap); the env JSONs are bound to account `581513795199`.
|
||||
|
||||
### Requirements
|
||||
|
||||
New requirements REQ-310..REQ-322 — see
|
||||
`.ciagent/nova-blockchain-exchange/REQUIREMENTS.md` §v1.26. Summary:
|
||||
blockchain core (REQ-310), order engine (REQ-311), settlement
|
||||
(REQ-312), consumer contract (REQ-313), deploy invocation (REQ-314),
|
||||
settlement-finality policy (REQ-315), pilot regression CAP (REQ-316),
|
||||
outcome backfill (REQ-317), escalation reason (REQ-318), env-JSON
|
||||
wiring (REQ-319), pilot-readiness policy (REQ-320), docs (REQ-321),
|
||||
DynamoDB L1 primitive (REQ-322 — the single platform-side module
|
||||
build-out; ECS + S3 already exist).
|
||||
|
||||
### Hard constraints
|
||||
|
||||
- DO NOT lift D-083 (S3 Object Lock/JWS) — stays deferred; the SQLite
|
||||
hash-chain + DynamoDB outbox is the pilot's audit record.
|
||||
- DO NOT lift D-126 (hot path) — cold-only metrics are sufficient for
|
||||
the pilot.
|
||||
- DO NOT add multi-cloud (Azure/GCP) — Nova is AWS-only this milestone.
|
||||
- DO NOT add ML forecasting — the Predictive/Reactive metric stays
|
||||
deferred.
|
||||
- DO NOT add bonds/derivatives/options — equities only (D-200).
|
||||
- DO NOT add multi-validator BFT — single validator PoA (D-201).
|
||||
- The consumer deploy MUST go through `deploy.yml@v1.25` — no direct
|
||||
`terraform apply` bypassing the platform's gates.
|
||||
|
||||
+307
-15
@@ -2181,18 +2181,310 @@ assert 20 main + 1 appendix.
|
||||
|
||||
| REQ | Phase | Status |
|
||||
|-----|-------|--------|
|
||||
| REQ-276 | P1 | pending |
|
||||
| REQ-277 | P1 | pending |
|
||||
| REQ-278 | P1 | pending |
|
||||
| REQ-279 | P1 | pending |
|
||||
| REQ-280 | P1 | pending |
|
||||
| REQ-281 | P1 | pending |
|
||||
| REQ-282 | P2 | pending |
|
||||
| REQ-283 | P2 | pending |
|
||||
| REQ-284 | P2 | pending |
|
||||
| REQ-285 | P2 | pending |
|
||||
| REQ-286 | P2 | pending |
|
||||
| REQ-287 | P2 | pending |
|
||||
| REQ-288 | P3 | pending |
|
||||
| REQ-289 | P3 | pending |
|
||||
| REQ-290 | P3 | pending |
|
||||
| REQ-276 | P1 | complete |
|
||||
| REQ-277 | P1 | complete |
|
||||
| REQ-278 | P1 | complete |
|
||||
| REQ-279 | P1 | complete |
|
||||
| REQ-280 | P1 | complete |
|
||||
| REQ-281 | P1 | complete |
|
||||
| REQ-282 | P2 | complete |
|
||||
| REQ-283 | P2 | complete |
|
||||
| REQ-284 | P2 | complete |
|
||||
| REQ-285 | P2 | complete |
|
||||
| REQ-286 | P2 | complete |
|
||||
| REQ-287 | P2 | complete |
|
||||
| REQ-288 | P3 | complete |
|
||||
| REQ-289 | P3 | complete |
|
||||
| REQ-290 | P1 | complete |
|
||||
|
||||
## v1.25 — kyverno-json Unified Policy Engine
|
||||
|
||||
> **Feature milestone.** `kyverno-json` becomes the primary compliance /
|
||||
> policy tool, implemented behind a swappable `PolicyEngine` adapter so
|
||||
> OPA (or any other engine) can replace it one day. Tags run on the
|
||||
> **v1.24.x** line (milestone v1.25 → tags v1.24.0..v1.24.N). Final patch
|
||||
> = milestone release.
|
||||
>
|
||||
> One problem, one architectural correction:
|
||||
> 1. **Fragmented policy posture.** Nova's compliance rules are split
|
||||
> across Checkov (imperative YAML + a Python custom rule for tagging),
|
||||
> Wiz (API findings), the K8s-only Kyverno adapter (inactive for
|
||||
> Terraform stacks — D-053), and imperative Python in
|
||||
> `core/env_transition.py` + `core/regression_verify.py`. There is no
|
||||
> single declarative place where "what Nova considers compliant" lives.
|
||||
> The K8s Kyverno adapter can't help because it only speaks to K8s
|
||||
> manifests, and the platform emits Terraform.
|
||||
>
|
||||
> The correction: `kyverno-json` (a Kyverno-ecosystem runtime that applies
|
||||
> Kyverno policies to **any** JSON/YAML payload) becomes the **unified
|
||||
> orchestrator** of compliance checks. Checkov and Wiz remain as
|
||||
> raw-finding adapters feeding *into* kyverno-json meta-policies. The
|
||||
> engine is behind a `PolicyEngine` protocol so it is replaceable. The
|
||||
> confidence signal is untouched — it already consumes
|
||||
> `list[PolicyCheckResult]` engine-agnostically.
|
||||
|
||||
### Decisions (locked in CLARIFY, full autonomy)
|
||||
|
||||
- **D-115 (C-1):** `kyverno-json` is a runtime dependency installed via
|
||||
`go install github.com/kyverno/kyverno-json/cmd/kj@latest` (pinned in a
|
||||
`scripts/install-kyverno-json.sh` helper; the CI image installs it).
|
||||
Not a Python package — kyverno-json is a Go binary. The
|
||||
`KyvernoJsonEngine.is_configured()` checks `which kj` and skips
|
||||
gracefully when absent (emits `SKIPPED` PCR, mirroring the Wiz adapter).
|
||||
- **D-116 (C-2):** kyverno-json PCR records carry `engine: "kyverno"`
|
||||
(no new enum value). The existing `engine` enum in
|
||||
`schemas/policy_check_result.schema.json` already includes `"kyverno"`;
|
||||
adding `"kyverno-json"` would force a schema change + checkov_adapter
|
||||
test regression for no semantic gain. The `ruleId` prefix `KJ_`
|
||||
distinguishes kyverno-json rules from the K8s Kyverno adapter's
|
||||
`KYVERNO_` prefix where they overlap.
|
||||
- **D-117 (C-3):** Checkov and Wiz adapters keep their current
|
||||
`adapt() -> list[PolicyCheckResult]` signatures. They emit PCRs as
|
||||
today. The meta-policies in `adapters/kyverno-json/policies/meta/`
|
||||
consume the **merged** PCR list (checkov + wiz + kyverno-json) as their
|
||||
input payload, applying Nova-specific posture rules on top. No adapter
|
||||
signature changes.
|
||||
- **D-118 (C-4):** `NOVA_TAG_NAMING` (the Checkov custom rule in
|
||||
`adapters/terraform/policy/custom_rules/nova_tagging.py`) is **kept**.
|
||||
A kyverno-json mirror policy `require-tagging-standard.json` is added
|
||||
in `adapters/kyverno-json/policies/stack-ir/`. The P3 meta-policy
|
||||
`tagging-rules-agree.json` asserts the two engines agree on every
|
||||
resource; divergence emits an `error` PCR (defense-in-depth against
|
||||
rule drift). The Checkov rule stays the source of truth for
|
||||
Terraform-static scanning; the kyverno-json policy covers Stack IR.
|
||||
|
||||
### Category: Policy Engine Core (feat)
|
||||
- **REQ-291:** `core/policy_engine.py` defines a `PolicyEngine` Python
|
||||
`Protocol` (PEP 544) with three members: `name -> str`,
|
||||
`is_configured() -> bool`, and
|
||||
`evaluate(payload: dict | str, policy_dir: Path, contract_id: str) ->
|
||||
list[dict]` (where each dict conforms to
|
||||
`schemas/policy_check_result.schema.json`). A `PolicyEngineRegistry`
|
||||
singleton selects the active engine from `config.json`'s new
|
||||
`policy.engine` key (default `"kyverno-json"`); raises
|
||||
`KeyError` on an unknown engine name. The registry exposes
|
||||
`get_engine()` and `register(name, factory)`. Pure stdlib, no engine
|
||||
imports at the protocol layer.
|
||||
- **REQ-292:** `.ciagent/config.json` gains a new top-level `policy`
|
||||
object: `{"engine": "kyverno-json", "policy_root":
|
||||
"adapters/kyverno-json/policies"}`. The registry reads `policy.engine`
|
||||
to select the active engine and `policy.policy_root` as the default
|
||||
policy directory. Backward-compatible: if the `policy` key is absent,
|
||||
the registry returns a `NullEngine` that emits only `SKIPPED` records
|
||||
(so existing tests that don't set the key still pass).
|
||||
|
||||
### Category: kyverno-json Engine Adapter (feat)
|
||||
- **REQ-293:** `adapters/kyverno-json/kyverno_json_engine.py` implements
|
||||
`KyvernoJsonEngine` satisfying the `PolicyEngine` protocol.
|
||||
`is_configured()` returns `True` when `which kj` succeeds. `evaluate()`
|
||||
writes the payload to a temp JSON file, invokes
|
||||
`kj scan --policy <policy_dir> --payload <payload.json> -o json`,
|
||||
parses the native result list, and translates each entry to a PCR dict
|
||||
(`engine: "kyverno"`, `ruleId` prefixed `KJ_<policy_name>`, severity
|
||||
mapped, `result` mapped pass/fail/skip → pass/fail/skipped). When
|
||||
`is_configured()` is false, `evaluate()` returns a single `SKIPPED`
|
||||
PCR with `ruleId: "KJ_ENGINE_NOT_CONFIGURED"` (mirrors the Wiz
|
||||
adapter's `is_configured()` guard). Native output parsing is
|
||||
defensive: any kyverno-json output that doesn't match the expected
|
||||
shape produces an `error` PCR, never an exception.
|
||||
- **REQ-294:** `adapters/kyverno-json/__init__.py` exports
|
||||
`KyvernoJsonEngine`. `adapters/kyverno-json/policies/_smoke.json`
|
||||
is a single trivial policy (`require-contract-id`) used to validate
|
||||
the engine round-trip end-to-end in tests. `scripts/install-kyverno-json.sh`
|
||||
runs `go install github.com/kyverno/kyverno-json/cmd/kj@latest` and
|
||||
prints `kj version`; documented in `adapters/kyverno-json/README.md`.
|
||||
The CI image (`.github/workflows/ci.yml` + `.gitea/workflows/ci.yml`)
|
||||
installs Go + kj when `policy.engine == "kyverno-json"`; the install
|
||||
is cached.
|
||||
|
||||
### Category: Contract Policies (feat)
|
||||
- **REQ-295:** `adapters/kyverno-json/policies/contract/` holds
|
||||
kyverno-json policies over consumer contract JSON. Four policies
|
||||
mirroring `schemas/contract.schema.json` constraints:
|
||||
`require-id-pattern.json` (`id` matches `^[a-z][a-z0-9-]{2,5}$`),
|
||||
`require-env-in-enum.json` (`environment` in dev/qa/prod/dr),
|
||||
`require-infrastructure-min-1.json` (`infrastructure` has ≥1 entry),
|
||||
`forbid-unknown-fields.json` (only `id`/`name`/`environment`/
|
||||
`infrastructure` allowed). Each policy is a single Kyverno `Policy`
|
||||
resource with one `validate.assert` rule using JMESPath against the
|
||||
payload root. Policies are the declarative equivalent of the
|
||||
jsonschema `required`/`pattern`/`enum` constraints — they let Nova
|
||||
apply its own compliance posture on top of schema validity.
|
||||
- **REQ-296:** `core/contract_resolver.py` invokes the
|
||||
`PolicyEngineRegistry.get_engine().evaluate()` with the contract dict
|
||||
and `policies/contract/` **before** resolving (early-fail on contract
|
||||
violations) and emits a `nova.policy.evaluated` metrics event (engine
|
||||
name in the event payload). Failures feed the confidence signal's
|
||||
`policy` input as `fail` PCRs; the resolver does not exit — the
|
||||
confidence signal decides the gate (consistent with the existing
|
||||
`--soft-fail` Checkov pattern).
|
||||
|
||||
### Category: Stack-IR Policies (feat)
|
||||
- **REQ-297:** `adapters/kyverno-json/policies/stack-ir/` holds policies
|
||||
over the resolved Target Stack IR dict. `require-tagging-standard.json`
|
||||
— every resource carries `nova:owner` + `nova:environment` tags
|
||||
(ports `adapters/terraform/policy/custom_rules/nova_tagging.py` logic
|
||||
into a declarative Kyverno policy over the IR's `resources[]` array;
|
||||
mirrors the v1.8 D-tagging-standard). `forbid-public-ingress.json` —
|
||||
no resource has `public_ingress: true` (the v1.0 demo rule, now
|
||||
declarative). `require-encryption-by-default.json` — every S3 bucket
|
||||
+ EBS volume + KMS-aliased resource carries encryption config (ports
|
||||
the v1.8 D-encryption-default rule).
|
||||
- **REQ-298:** `core/contract_resolver.py` invokes the engine with the
|
||||
resolved Stack IR and `policies/stack-ir/` **after** resolving. The
|
||||
resulting PCRs are appended to the contract-policy PCRs and fed to the
|
||||
confidence signal. The resolver's existing `tests/test_contract_resolver.py`
|
||||
continues to pass (the policy call is additive — it does not change
|
||||
resolver return values or exceptions).
|
||||
- **REQ-299:** `tests/test_stack_ir_policies.py` + fixture
|
||||
`tests/fixtures/stack_ir/` — a passing IR (all tags + encryption) and
|
||||
a failing IR (missing tags, public ingress, plaintext bucket). Each
|
||||
policy is tested in isolation + the full `policies/stack-ir/` dir as a
|
||||
bundle. Tests run the `KyvernoJsonEngine` against real `kj` when
|
||||
`which kj` succeeds, and skip with a `pytest.skip("kj not installed")`
|
||||
when absent (so CI without the binary doesn't fail).
|
||||
|
||||
### Category: Plan-JSON Policies + Pipeline Wiring (feat)
|
||||
- **REQ-300:** `adapters/kyverno-json/policies/plan-json/` holds policies
|
||||
over `terraform show -json` output. `forbid-plaintext-secrets.json`
|
||||
(ports `CKV_AWS_41/45/46` — no `aws_db_instance.password` /
|
||||
`aws_iam_user.*` plaintext). `forbid-iam-wildcard.json` (ports
|
||||
`CKV_AWS_1/40` — no `Action: "*"` or `Resource: "*"` in IAM policies).
|
||||
`require-kms-reference.json` (ports `CKV_AWS_7/33` — KMS keys referenced
|
||||
by alias, not inline). Each policy uses JMESPath over the plan's
|
||||
`planned_values.root_module.resources[]` array. The Checkov `RULE_MAP`
|
||||
in `checkov_adapter.py` is unchanged — these are declarative mirrors,
|
||||
not replacements.
|
||||
- **REQ-301:** `run_platform.sh` Step 5 ("runtime policy scan") gains a
|
||||
parallel kyverno-json pass: after Checkov/Wiz produce raw PCRs, the
|
||||
script runs `kj scan --policy adapters/kyverno-json/policies/plan-json/
|
||||
--payload <tfshow.json> -o json` and pipes through
|
||||
`adapters/kyverno-json/kyverno_json_engine.py` to produce a second PCR
|
||||
list. Both lists are concatenated and fed to the confidence signal's
|
||||
`policy` input. The script emits a `nova.policy.evaluated` event with
|
||||
both engine names. When `which kj` is false, the script logs
|
||||
"kyverno-json not installed; skipping plan-json policies" and proceeds
|
||||
with the Checkov/Wiz list only (no hard failure — the platform
|
||||
functions without kj).
|
||||
- **REQ-302:** `tests/test_plan_json_policies.py` + fixture
|
||||
`tests/fixtures/plan_json/` — a passing plan JSON (no secrets, no
|
||||
wildcard, KMS alias) and a failing plan JSON (plaintext password,
|
||||
`Action: "*"`, inline KMS key). Tests the three policies in isolation
|
||||
+ as a bundle. `tests/test_run_platform_plan_json_policies.py`
|
||||
asserts `run_platform.sh` has the kyverno-json Step 5 block and that
|
||||
it concatenates PCR lists (pattern from `tests/test_pipeline.py:79-95`
|
||||
— read script text + assert substrings).
|
||||
|
||||
### Category: Meta-Policies (feat)
|
||||
- **REQ-303:** `adapters/kyverno-json/policies/meta/` holds policies
|
||||
whose **payload** is the merged `list[PolicyCheckResult]` itself.
|
||||
`block-on-any-critical.json` — asserts no PCR in the list has
|
||||
`severity: "critical"` + `result: "fail"`; if any does, the meta-policy
|
||||
emits a `fail` PCR with `ruleId: "KJ_META_BLOCK_CRITICAL"` and
|
||||
severity `critical`. This is the **declarative** source of truth for
|
||||
"critical = block"; the `confidence_signal.py` `PENALTY["critical"]:
|
||||
None` hard-override stays as defense-in-depth (D-118-adjacent
|
||||
decision). `tagging-rules-agree.json` — for every resource in the
|
||||
Stack IR, asserts the Checkov `NOVA_TAG_NAMING` result and the
|
||||
kyverno-json `KJ_REQUIRE_TAGGING_STANDARD` result agree; divergence
|
||||
emits an `error` PCR. `tests/test_meta_policies.py` covers both.
|
||||
|
||||
### Category: Regression-Gate Policies (feat, quality improvement from IDEATE)
|
||||
- **REQ-304:** `adapters/kyverno-json/policies/regression/` holds
|
||||
policies over the capability-inventory JSON frontmatter
|
||||
(`CAPABILITY_INVENTORY.md` parsed as structured data). Three policies
|
||||
port the imperative checks in `core/regression_verify.py`:
|
||||
`cap-013-adapter-dedup.json` (no duplicate adapter registrations),
|
||||
`cap-023-metrics-collector.json` (every metric in `docs/METRICS.md`
|
||||
has a grounded/derived/deferred status), `cap-024-deck-structure.json`
|
||||
(deck slide structure matches the documented arc). The policies read
|
||||
the parsed capability inventory as payload and emit `pass`/`fail` PCRs
|
||||
per capability. The existing `core/regression_verify.py` is **kept**
|
||||
(it drives the CI gate); the policies are the **declarative mirror**
|
||||
that makes capability regression auditable as a policy artifact, not
|
||||
imperative Python. Future milestones may switch the gate to the
|
||||
policy version.
|
||||
- **REQ-305:** `tests/test_regression_policies.py` + fixture
|
||||
`tests/fixtures/capability_inventory.json` — a clean inventory (all
|
||||
caps pass) and a drifted inventory (duplicate adapter, missing metric
|
||||
status, broken deck arc). The regression gate (`pytest` suite)
|
||||
continues to pass 287/287 (or new count); the new policy tests are
|
||||
additive.
|
||||
|
||||
### Category: Documentation (docs)
|
||||
- **REQ-306:** `adapters/README.md` gains a new row for the
|
||||
`kyverno-json` adapter + a new section "Policy Engine Protocol"
|
||||
documenting the `PolicyEngine` Protocol, the registry, and the
|
||||
swap boundary (how to add an `OpaEngine`). `adapters/kyverno-json/README.md`
|
||||
documents the engine, the install path, the policy directory layout,
|
||||
and the four policy categories (contract/stack-ir/plan-json/meta).
|
||||
- **REQ-307:** `.ciagent/ARCHITECTURE.md` gains §12.7 "Policy Engine
|
||||
Registry" with the registry diagram (engine ↔ protocol ↔ registry ↔
|
||||
config.json ↔ confidence signal). `schemas/README.md` notes the
|
||||
`engine: "kyverno"` value is shared by the K8s Kyverno adapter and the
|
||||
kyverno-json engine (distinguished by `ruleId` prefix). `modules/STANDARDS.md`
|
||||
gains a "Policy authoring standard" section for module owners who want
|
||||
to ship per-module kyverno-json policies. `docs/METRICS.md` notes the
|
||||
policy engine is now swappable (Strategic Objective #2 — provable
|
||||
trust via a replaceable substrate, not a vendor lock-in).
|
||||
|
||||
### Category: Tests (test)
|
||||
- **REQ-308:** `tests/test_policy_engine.py` — protocol conformance
|
||||
(the registry returns an engine implementing all three methods),
|
||||
unknown-engine `KeyError`, `NullEngine` fallback when the `policy`
|
||||
key is absent, `KyvernoJsonEngine.is_configured()` returns false when
|
||||
`which kj` fails (mocked). `tests/test_kyverno_json_engine.py` —
|
||||
`evaluate()` returns valid PCR dicts against
|
||||
`schemas/policy_check_result.schema.json` (validated with
|
||||
`jsonschema`); native-output parsing is defensive (malformed kyverno-json
|
||||
output → `error` PCR, not exception); `is_configured()==false` →
|
||||
`SKIPPED` PCR with `KJ_ENGINE_NOT_CONFIGURED`.
|
||||
- **REQ-309:** All new tests use `pytest.skip("kj not installed")` when
|
||||
`which kj` is absent, so the suite passes in environments without the
|
||||
binary (CI matrix: with-kj and without-kj). The full suite
|
||||
(`pytest tests/`) continues to pass at 287/287 baseline + new tests
|
||||
(the new tests skip without kj, so the count grows only when kj is
|
||||
installed). `pyproject.toml` + `requirements-test.txt` unchanged
|
||||
(kyverno-json is a Go binary, not a Python dep).
|
||||
|
||||
### Out of Scope (v1.25)
|
||||
- **Removing Checkov or Wiz.** Both stay as raw-finding adapters. The
|
||||
unified-orchestrator model layers kyverno-json on top, not in place of.
|
||||
- **`OpaEngine` implementation.** The protocol is the swap boundary;
|
||||
the OPA implementation is a future milestone. RESEARCH documents the
|
||||
OPA-equivalent surface so the swap is a known quantity.
|
||||
- **Per-module policies.** `modules/<name>/policies/` is documented as
|
||||
the future pattern in `modules/STANDARDS.md` but not populated this
|
||||
milestone (policies live under `adapters/kyverno-json/policies/`
|
||||
for v1.25).
|
||||
- **kyverno-json as a long-running service.** v1.25 uses the CLI
|
||||
(`kj scan`); the `kj serve` web-app mode is a future consideration
|
||||
for lower-latency evaluation (RESEARCH notes it).
|
||||
- **Replacing the K8s Kyverno adapter.** The K8s adapter
|
||||
(`adapters/kyverno/`) remains documentation-only (D-053 — platform
|
||||
emits Terraform). The kyverno-json engine and the K8s adapter are
|
||||
siblings, not replacements.
|
||||
|
||||
### v1.25 Traceability
|
||||
|
||||
| REQ | Phase | Status |
|
||||
|-----|-------|--------|
|
||||
| REQ-291 | P1 | complete |
|
||||
| REQ-292 | P1 | complete |
|
||||
| REQ-293 | P1 | complete |
|
||||
| REQ-294 | P1 | complete |
|
||||
| REQ-295 | P2 | complete |
|
||||
| REQ-296 | P2 | complete |
|
||||
| REQ-297 | P2 | complete |
|
||||
| REQ-298 | P2 | complete |
|
||||
| REQ-299 | P2 | complete |
|
||||
| REQ-300 | P3 | complete |
|
||||
| REQ-301 | P3 | complete |
|
||||
| REQ-302 | P3 | complete |
|
||||
| REQ-303 | P3 | complete |
|
||||
| REQ-304 | P4 | complete |
|
||||
| REQ-305 | P4 | complete |
|
||||
| REQ-306 | P4 | complete |
|
||||
| REQ-307 | P4 | complete |
|
||||
| REQ-308 | P1 | complete |
|
||||
| REQ-309 | P1 | complete |
|
||||
|
||||
+215
-152
@@ -1,187 +1,250 @@
|
||||
# Nova — v1.24 Research Findings
|
||||
# Nova — v1.26 Research Findings
|
||||
|
||||
> Phase: research (pre-execution). Milestone: v1.24 (Consumer Guide Accuracy
|
||||
> & Env-Promotion Lifecycle Enforcement). Status: research.
|
||||
> Researcher: ci-researcher. Autonomy: full.
|
||||
> Phase: research (pre-execution). Milestone: v1.26 (Live Pilot Estate
|
||||
> Activation). Status: research. Researcher: ci-researcher.
|
||||
> Autonomy: full.
|
||||
|
||||
## 1. Problem domain
|
||||
---
|
||||
|
||||
Two distinct problem spaces in one milestone:
|
||||
## 1. Domain — Homegrown PoA Blockchain for Securities Settlement
|
||||
|
||||
### 1a. Consumer guide accuracy (docs)
|
||||
### 1.1 Why a homegrown chain (not Ethereum/Solana/Hyperledger)
|
||||
|
||||
The consumer guide (`docs/consumer-guide.md`, 477 lines) has 5 accuracy
|
||||
defects identified in review:
|
||||
The pilot's purpose is to exercise the Nova platform's deploy/policy/
|
||||
attestation gates over a real consumer estate — not to build a
|
||||
production blockchain. A homegrown PoA ledger is the minimal viable
|
||||
chain: append-only blocks, single validator (pilot), SHA-256 hash chain,
|
||||
deterministic block production. It records every order, match, and
|
||||
settlement as transactions; settlement finality = block commit. This
|
||||
is sufficient to demonstrate that Nova's policy engine (kyverno-json)
|
||||
can assert settlement finality declaratively (REQ-315) and that the
|
||||
Decision Ledger captures the apply decision.
|
||||
|
||||
1. **Step 3 contract fields table (lines 141-147)** lists `uses`, `module`,
|
||||
`environment`, `inputs`. The actual schema
|
||||
(`schemas/contract.schema.json:7`) requires `id`, `name`, `environment`,
|
||||
`infrastructure`. The `uses` field was dropped in v1.10.2 (REQ-50
|
||||
superseded) and `module` was replaced by the `infrastructure` map key.
|
||||
The worked examples (lines 111-137) use the correct fields.
|
||||
A production chain (Ethereum/Solana/Hyperledger) would be the *consumer
|
||||
app's* choice, not the platform's. The platform is chain-agnostic — it
|
||||
deploys whatever the consumer's `contract.yaml` declares. For the pilot,
|
||||
the homegrown chain is the simplest way to produce a real consumer
|
||||
estate without a heavyweight external dependency.
|
||||
|
||||
2. **Step 4 caller (lines 173-183)** omits `environment:` in `with:`, while
|
||||
Step 2 (lines 94-101) shows `environment: dev`. The two canonical caller
|
||||
snippets disagree.
|
||||
### 1.2 PoA consensus — single validator (pilot)
|
||||
|
||||
3. **Step 5 stage 8 (lines 232, 253)** says "(dev only)" for the apply
|
||||
stage. Higher environments *do* apply — they apply after HITL
|
||||
attestation per `docs/environments/index.md:44-54`.
|
||||
Proof-of-Authority with a single validator is the minimal consensus
|
||||
model: the validator proposes + commits blocks. No Byzantine fault
|
||||
tolerance (single validator = no forks). Deterministic block
|
||||
production: same ordered transactions → same block (same hash). This
|
||||
makes the chain auditable (the hash chain is verifiable) and
|
||||
reproducible (a replay produces the same chain). Multi-validator BFT
|
||||
is a future milestone (D-201).
|
||||
|
||||
4. **Step 8 (lines 290-306)** says "Change `environment` in your contract"
|
||||
to promote, which contradicts the same doc's "Per-environment
|
||||
deployment" section (lines 398-402): "you do not edit the `environment:`
|
||||
field… Promotion = running the matching job." The test
|
||||
`test_consumer_guide_states_no_field_editing` asserts the no-editing
|
||||
model.
|
||||
### 1.3 T+1 settlement finality
|
||||
|
||||
5. **Reference table (lines 329-330)** says sample contracts "use `@v1.19`"
|
||||
but the sample contracts (`contracts/static-assets.yml`,
|
||||
`contracts/microservice.yml`) don't carry `uses:` — the version pin
|
||||
lives in the caller workflow.
|
||||
Equities settle T+1 (trade date + 1 business day). The pilot's
|
||||
settlement service records matches as transactions on the chain; a
|
||||
settlement is final when its block is committed. The settlement-finality
|
||||
kyverno-json policy (REQ-315) asserts `all_committed: true` before any
|
||||
promotion (qa→prod) — the declarative gate that turns settlement
|
||||
finality into a policy artifact. This is the securities-specific
|
||||
extension of v1.25's policy engine: the same `KyvernoJsonEngine`
|
||||
evaluates a policy over a new payload shape (settlement-service status
|
||||
JSON).
|
||||
|
||||
### 1b. Environment-promotion lifecycle enforcement (feat)
|
||||
### 1.4 Equities-only scope (D-200)
|
||||
|
||||
**Root cause confirmed by code inspection:**
|
||||
Bonds (T+2), derivatives (varying), and options (exercise models) have
|
||||
different settlement models. A pilot should demonstrate the Nova
|
||||
platform's gates over the simplest case (equities T+1) before
|
||||
expanding. "All types of securities" is the product vision; v1.26 is
|
||||
the pilot (equities first). Future milestones add other security types
|
||||
with their settlement models.
|
||||
|
||||
- `adapters/terraform/adapter.py:129` sets the Terraform state key to:
|
||||
`spike/{stack_name}/{environment}/terraform.tfstate`
|
||||
- `stack_name` = `contract["id"]` (stable across env changes, per
|
||||
`core/contract_resolver.py:584`).
|
||||
- When a consumer edits `environment:` from `dev` → `qa` on the same
|
||||
contract `id`, the state key changes from `spike/assets/dev/` to
|
||||
`spike/assets/qa/`. Terraform initializes a **fresh state file** in the
|
||||
new env's state path. The prior env's resources remain live in AWS with
|
||||
their state file untouched. **No destroy ever runs.** This orphans
|
||||
resources.
|
||||
---
|
||||
|
||||
**The user's binding directive:** Editing `environment:` on a stable
|
||||
`contract.id` is a valid promotion path (Shape A). The platform **must**
|
||||
destroy the prior env's resources before building the new env. There must
|
||||
be **no path that orphans resources** — fail closed if the destroy fails.
|
||||
## 2. Nova Consumer Deploy Model
|
||||
|
||||
## 2. Existing codebase structure (integration points)
|
||||
### 2.1 The reusable `deploy.yml@v1.25` workflow
|
||||
|
||||
### DynamoDB `nova-contracts` table (the prior-env source of truth)
|
||||
The platform's `.github/workflows/deploy.yml` is a `workflow_call` —
|
||||
a reusable workflow that a consumer repo invokes via
|
||||
`uses: acdl/.github/workflows/deploy.yml@v1.25`. Inputs: `contract`
|
||||
(default `.nova/contract.yml`), `mode` (default `full`; enum
|
||||
`full|plan-only|check-only|decommission`), `environment` (override).
|
||||
The workflow checks out the consumer repo + the platform repo, runs
|
||||
`scripts/run_platform.sh`, and records the apply decision +
|
||||
attestation in the Decision Ledger. Secrets: `NOVA_AWS_*`
|
||||
(account + access key + secret) + `NOVA_LAMBDA_URL` (error reporting).
|
||||
|
||||
- **Table:** `nova-contracts` (env var `CONTRACTS_TABLE`, default
|
||||
`nova-contracts`). Defined in `core/lambda/contract_ingestor.py:25`.
|
||||
- **Schema:** PK `consumerRepo` (S), SK `contractId#submittedAt` (S).
|
||||
Attributes: `contractId`, `contract`, `environment`, `status`,
|
||||
`submittedAt`.
|
||||
- **Written by:** `_submit_contract()` at
|
||||
`core/lambda/contract_ingestor.py:135-176`. The consumer's deploy
|
||||
workflow submits the contract via the Lambda Function URL.
|
||||
- **Read pattern for env-transition:** Query by PK `consumerRepo` + SK
|
||||
begins_with `contractId#` + FilterExpression `status = "submitted"` →
|
||||
sort by `submittedAt` desc → take the latest → read its `environment`.
|
||||
This is the last-submitted env. For the last-*applied* env, a new
|
||||
`#LAST_APPLIED` SK suffix is added (REQ-283).
|
||||
- **Test pattern:** `tests/test_contract_ingestor.py:74-110`
|
||||
(`moto_contracts_table` fixture) uses moto `mock_aws` to create the
|
||||
table. The env-transition tests will mirror this pattern.
|
||||
The pilot consumer (`nova-blockchain-exchange`) invokes this workflow
|
||||
with `mode: full` for `dev` (D-209). The `.gitea/workflows/deploy.yml`
|
||||
mirror is byte-identical (the platform's deploy workflow is
|
||||
forge-agnostic — Gitea + GitHub).
|
||||
|
||||
### Outbox writer (evidence events)
|
||||
### 2.2 `run_platform.sh --apply` path (confirmed)
|
||||
|
||||
- `core/outbox_writer.py` writes hash-chained evidence events to
|
||||
`nova-outbox` table. PK `contractId`, SK `eventType#eventTs`.
|
||||
- The env-transition destroy step emits a `nova.env.destroyed` event via
|
||||
this writer (REQ-284c). Pattern: build an event dict with `contractId`,
|
||||
`eventType: "ENV_DESTROYED"`, `environment: <prior_env>`, `ts`, then
|
||||
call `write_event()`.
|
||||
`scripts/run_platform.sh:431-455` — the `--apply` (or `mode: full`)
|
||||
path runs `terraform apply -auto-approve` after the HITL gate
|
||||
(`:438`). For `dev` (autonomous, no HITL gate), the apply proceeds
|
||||
directly. The apply records the env via `core/env_transition.py record`
|
||||
(`:450`). The full pipeline (no `--apply` flag) continues to Step 7
|
||||
(confidence signal) + Step 8 (outbox write).
|
||||
|
||||
### Contract resolver (environment override)
|
||||
**Gap (noted in RESEARCH §4):** the `--apply` path exits before the
|
||||
outbox write (Step 8). The pilot runs the full pipeline (not `--apply`
|
||||
alone), so the outbox write happens. The `run.completed` event lands in
|
||||
the JSONL Decision Ledger (not the DynamoDB outbox) — this is by design
|
||||
(the outbox is the platform-run evidence stream; the Decision Ledger is
|
||||
the cold store for metrics).
|
||||
|
||||
- `core/contract_resolver.py:460-483` `resolve()` accepts
|
||||
`environment_override` — when set, it overrides the contract's
|
||||
`environment` field **before** schema validation and interpolation
|
||||
(D-088). This is the mechanism the destroy step uses to re-resolve the
|
||||
contract against the prior env: `resolve(contract, env_override=prior_env)`.
|
||||
- Already tested in `tests/test_deploy_workflow_env_input.py:35-52`.
|
||||
### 2.3 Contract schema — multi-module manifest
|
||||
|
||||
### run_platform.sh (where Step 0b goes)
|
||||
`schemas/contract.schema.json:7,24-48` — required fields: `id`,
|
||||
`name`, `environment`, `infrastructure`. The `infrastructure` block is
|
||||
`minProperties: 1` with `patternProperties` accepting any module name
|
||||
key. Multi-module manifest is supported: one contract can declare
|
||||
`infrastructure: { microservice: {...}, dynamodb: {...}, s3: {...} }`.
|
||||
The constraint is the `modules/registry.json` (the module must be
|
||||
registered), not the schema.
|
||||
|
||||
- `scripts/run_platform.sh` is the platform pipeline. Step 0 (lines 222-239)
|
||||
is the environment onboarding check. Step 1 (lines 241-249) is contract
|
||||
validation. **Step 0b goes between them** (after onboarding, before
|
||||
validation).
|
||||
- The destroy step mirrors the existing `--destroy` mode (lines 376-394):
|
||||
`terraform init -reconfigure` + `terraform destroy -auto-approve`. The
|
||||
difference: it runs against the *prior* env's state key, not the current
|
||||
one.
|
||||
- `CONTRACT_ID` is already set at line 217 (`NOVA_CONTRACT_ID` with a
|
||||
default). `CONSUMER_REPO` needs to be derived from `GITHUB_REPOSITORY`
|
||||
or a new `NOVA_CONSUMER_REPO` env var (REQ-286).
|
||||
---
|
||||
|
||||
### deploy.yml (consumer repo → platform)
|
||||
## 3. Platform Module Readiness (the critical finding)
|
||||
|
||||
- `.github/workflows/deploy.yml:132` runs
|
||||
`bash platform/scripts/run_platform.sh $MODE_FLAG $ENV_FLAG "${{ inputs.contract }}"`.
|
||||
- To pass `NOVA_CONSUMER_REPO`, add `NOVA_CONSUMER_REPO=${{ github.repository }}`
|
||||
as an env var on the "Run the platform pipeline" step (REQ-286).
|
||||
### 3.1 The adapter is stateless (v1.11 rewrite)
|
||||
|
||||
### Test patterns
|
||||
`adapters/terraform/adapter.py:1-11` — the adapter is a "STATELESS
|
||||
ASSEMBLER" that owns no module content. There is **no `TYPE_MAP`**,
|
||||
`INPUT_MAP`, or `OUTPUT_MAP` (deleted in the v1.11 stateless rewrite;
|
||||
`modules/STANDARDS.md:212-214` confirms). A new stack type requires a
|
||||
new L1 module (`modules/l1/<name>/` with `interface.json` +
|
||||
`terraform/main.tf` + `README.md` + `instance.json`) + a
|
||||
`modules/registry.json` entry — not an adapter change.
|
||||
|
||||
- **Shell script assertions:** `tests/test_pipeline.py:79-95` reads the
|
||||
script text and asserts substrings. The env-transition tests follow this
|
||||
pattern for `run_platform.sh`.
|
||||
- **DynamoDB mocking:** `tests/test_contract_ingestor.py:74-110` uses moto
|
||||
`mock_aws` + `boto3.client` + `create_table`. The env-transition tests
|
||||
follow this pattern.
|
||||
- **Consumer guide assertions:** `tests/test_consumer_guide_per_env_section.py`
|
||||
reads `docs/consumer-guide.md` text and asserts substrings. The updated
|
||||
tests follow this pattern.
|
||||
### 3.2 ECS — ready
|
||||
|
||||
## 3. Persona assessment (v1.24)
|
||||
`modules/l1/ecs-service/terraform/main.tf:1,11` —
|
||||
`aws_ecs_task_definition` + `aws_ecs_service`. `interface.json:5-6` —
|
||||
`type: aws:ecs:task_definition`. `registry.json:29-37` — registered.
|
||||
Tests: `test_adapter.py:164-185,257-360`, `test_contract_resolver.py:61-92`.
|
||||
The `microservice` L2 (`modules/l2/microservice/composition.json`)
|
||||
references 6 L1 children (ecs-cluster, ecr, iam-role, alb, ecs-service,
|
||||
kms-key) — the ECS pattern is fully wired end-to-end.
|
||||
|
||||
This milestone has two distinct work territories:
|
||||
### 3.3 S3 — ready
|
||||
|
||||
1. **Docs (P1):** `docs/consumer-guide.md` edits + test updates. This is
|
||||
lead-developer territory (narrative/docs + test assertions).
|
||||
2. **Platform code (P2):** `core/env_transition.py` (new), `scripts/run_platform.sh`
|
||||
edits, `.github/workflows/deploy.yml` edit, `adapters/terraform/adapter.py`
|
||||
doc comment. This is backend-engineer territory (Python + bash + YAML).
|
||||
3. **Tests (P3):** `tests/test_env_transition.py` (new), `tests/test_run_platform_env_transition.py`
|
||||
(new), `tests/test_consumer_guide_per_env_section.py` updates. Split:
|
||||
backend-engineer for the env_transition + pipeline tests; lead-developer
|
||||
for the consumer guide test updates.
|
||||
`modules/l1/s3/terraform/main.tf:1` — `aws_s3_bucket` (+ versioning +
|
||||
SSE). `interface.json:5-6` — `type: aws:s3:bucket`. `registry.json:2-10`
|
||||
— registered. Tests: `test_adapter.py:56-110,241-257`,
|
||||
`test_contract_resolver.py:36-51,92-130`.
|
||||
|
||||
**Roster:** lead-developer (docs + guide tests) + backend-engineer (Python +
|
||||
bash + YAML + pipeline tests). frontend-engineer stays deactivated (no UI).
|
||||
data-engineer not needed (no schema changes — the `nova-contracts` table
|
||||
already exists with the right shape; we only add a new SK suffix). No new
|
||||
personas.
|
||||
### 3.4 DynamoDB — GAP (REQ-322)
|
||||
|
||||
## 4. Key decisions logged
|
||||
**No `modules/l1/dynamodb/` directory, no `registry.json` key, no
|
||||
`interface.json`, no `terraform/`, no tests.** The blockchain exchange's
|
||||
ledger table needs this primitive. REQ-322 authors it: `interface.json`
|
||||
(stack type `aws:dynamodb:table`), `terraform/main.tf`
|
||||
(`aws_dynamodb_table` with PK + optional SK, `PAY_PER_REQUEST` default,
|
||||
encryption + PITR enabled per v1.8 NFR defaults), `README.md`,
|
||||
`instance.json`, + `registry.json` entry. The adapter needs no change
|
||||
(stateless); the contract's `infrastructure.dynamodb` block references
|
||||
this primitive. This is the single platform-side module build-out for
|
||||
the milestone.
|
||||
|
||||
| ID | Decision | Confidence | Source |
|
||||
|----|----------|------------|--------|
|
||||
| D-201 | Both promotion shapes supported (A: edit+destroy, B: per-env callers) | 0.95 | User directive |
|
||||
| D-202 | Prior-env source of truth = `nova-contracts` DynamoDB table | 0.85 | User directive + code inspection |
|
||||
| D-203 | Detect-and-destroy at pipeline start (Step 0b) | 0.85 | User directive |
|
||||
| D-204 | Fail closed on destroy failure (no orphan path) | 0.95 | User directive |
|
||||
| D-205 | Cross-account destroy out of scope (same-account only) | 0.80 | CLARIFY A3 |
|
||||
| D-206 | Env-transition destroy is NOT the HITL decommission pipeline | 0.85 | CLARIFY A5 |
|
||||
| D-207 | Last-applied env recorded via `#LAST_APPLIED` SK in `nova-contracts` | 0.85 | RESEARCH §2 |
|
||||
| D-208 | State key `spike/{id}/{env}/` stays as-is (correct for both shapes) | 0.90 | RESEARCH §1b |
|
||||
### 3.5 Stale doc (not a blocker)
|
||||
|
||||
## 5. Pitfalls
|
||||
`adapters/README.md:49-54` references the deleted `TYPE_MAP`/
|
||||
`INPUT_MAP`/`OUTPUT_MAP` — contradicts `adapter.py:1-11` +
|
||||
`modules/STANDARDS.md:212-214`. REQ-321 (docs) should fix this.
|
||||
|
||||
1. **Destroy needs the prior env's Terraform config, not the new env's.**
|
||||
The destroy step must re-resolve the contract with
|
||||
`environment_override=prior_env` so the emitted TF matches the prior
|
||||
env's resources. If we resolve with the new env, the destroy plan won't
|
||||
match the prior state → terraform tries to create, not destroy.
|
||||
2. **`terraform init -reconfigure` is required** when switching state
|
||||
backends between envs (if envs use different state buckets). The
|
||||
`-reconfigure` flag tells Terraform to forget the previous backend config.
|
||||
3. **The `deletion_protection` NFR (REQ-86) blocks destroy.** The destroy
|
||||
step must resolve with `deletion_protection: false` injected (same as
|
||||
decommission Step 2 in `scripts/run_decommission.sh:34-37`). Without
|
||||
this, `terraform destroy` fails on `prevent_destroy` lifecycle blocks.
|
||||
4. **DynamoDB may not be reachable in local/CI mode.** The detect step
|
||||
must handle `ClientError` / `EndpointNotFound` gracefully → log warning
|
||||
+ return `None` (conservative). This is documented in REQ-282.
|
||||
5. **The consumer guide test `test_consumer_guide_states_no_field_editing`
|
||||
will fail after the Step 8 rewrite.** It must be updated in the same
|
||||
phase as the guide edit (P1) or the test suite breaks.
|
||||
---
|
||||
|
||||
## 4. Metric Pipeline Grounding (Post-Pilot targets)
|
||||
|
||||
### 4.1 AI Decision Accuracy — outcome backfill (REQ-317)
|
||||
|
||||
`core/metrics/decision_ledger.py:210-211` documents the event chain:
|
||||
`confidence.computed → ai.decision.made → attestation.recorded →
|
||||
run.completed/failed`. `collector.py:262` inserts `fact_decision.outcome`
|
||||
as `"pending"` — **there is no outcome-backfill step** wiring
|
||||
`run.completed`/`run.failed` back into `fact_decision.outcome`. The AI
|
||||
Decision Accuracy metric (`trust_snapshot.py:70-85`, `_get_ai_decision_accuracy`)
|
||||
reads `decisions WHERE outcome='succeeded' ÷ total` — so it reads 0%
|
||||
today (all pending). REQ-317 adds `core/metrics/outcome_backfill.py`
|
||||
that reads run-manifest events and updates `fact_decision.outcome` +
|
||||
`fact_decision.backfilled_at`. The PCR schema is unchanged (D-211).
|
||||
|
||||
### 4.2 Human Escalation Frequency — `reason='confidence'` tag (REQ-318)
|
||||
|
||||
`core/confidence_signal.py:184` — a `block` band sets
|
||||
`human_override=True` in the `ai.decision.made` event.
|
||||
`run_platform.sh:636` fails the pipeline on `block`. The Human
|
||||
Escalation Frequency metric (`docs/metrics/human_escalation_frequency.md:11-12`)
|
||||
is defined as `count(runs WHERE hitl_block=1 AND reason='confidence') ÷
|
||||
total runs`. The `reason='confidence'` discriminator is **not currently
|
||||
stored** — `hitl_block` is a boolean from the manifest. REQ-318 adds
|
||||
`escalation_reason: 'confidence'` to the `ai.decision.made` event when
|
||||
`band == 'block'` + persists it into `fact_run` via the collector.
|
||||
|
||||
### 4.3 Touchless Resolution Rate — denominator activates post-pilot
|
||||
|
||||
`docs/metrics/touchless_resolution_rate.md:12-15` — defined as a SQL
|
||||
query over `fact_run` (`runs WHERE hitl_block=0 ÷ total runs`). The data
|
||||
lands in `fact_run.hitl_block` via `collector.py:216-227`. No dedicated
|
||||
emitter computes the ratio — it's a downstream query. The denominator
|
||||
is 0 today (no consumer runs). The pilot run activates the denominator.
|
||||
|
||||
---
|
||||
|
||||
## 5. kyverno-json Policy Extensibility
|
||||
|
||||
`adapters/kyverno-json/kyverno_json_engine.py:74-80` — the engine is
|
||||
**policy-dir agnostic**: it loads whatever subdir the caller passes.
|
||||
Existing subdirs: `contract/`, `stack-ir/`, `plan-json/`, `meta/`,
|
||||
`regression/`. Adding a new subdir (e.g. `pilot-readiness/`,
|
||||
`settlement-finality/`) requires: (1) `mkdir
|
||||
adapters/kyverno-json/policies/<name>/`, (2) drop `ValidatingPolicy`
|
||||
YAML/JSON files, (3) wire a caller. No engine code change needed.
|
||||
Test pattern: one test file per subdir (`tests/test_<name>_policies.py`).
|
||||
|
||||
The pilot adds two new policy subdirs: `pilot-readiness/`
|
||||
(REQ-320, no-placeholder-account) + `settlement-finality/` (REQ-315,
|
||||
all-matches-committed). Both follow the established pattern.
|
||||
|
||||
---
|
||||
|
||||
## 6. Env-JSON Wiring Reconciliation (REQ-319)
|
||||
|
||||
`core/environments/dev.json:4` — `account_id: "000000000000"` (placeholder).
|
||||
`core/environment_check.py:48-53` warns (non-fatal) when account_id is
|
||||
placeholder + env != dev. `adapters/terraform/adapter.py:116-117` —
|
||||
computes the state bucket as `nova-tfstate-<AWS_ACCOUNT_ID>-us-east-1`
|
||||
from the `AWS_ACCOUNT_ID` env var, **not** from the env JSON's
|
||||
`state_backend.bucket`. This is the wiring gap: the env JSON's
|
||||
`state_backend` field is currently unused by the live apply path.
|
||||
REQ-319 makes the adapter read `env.state_backend.bucket` when present
|
||||
(falling back to the computed name for backwards compat) + updates
|
||||
`dev.json` to the real account `581513795199` + real bucket
|
||||
`nova-tfstate-581513795199-us-east-1`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Risk Analysis
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| `NOVA_AWS_*` key lacks a needed IAM permission mid-pilot | Low (bootstrap succeeded → root-equivalent) | High (blocks apply) | D-207; the key has root-equivalent perms (empirically confirmed). |
|
||||
| DynamoDB primitive takes longer than expected (new module) | Medium | Medium | REQ-322 is the single platform-side build-out; the `s3`/`rds` primitives are the template — straightforward. |
|
||||
| Homegrown chain has a correctness bug (hash chain breaks) | Low | High | REQ-310 tests cover chain integrity, hash determinism, genesis, append/verify. |
|
||||
| `deploy.yml@v1.25` ref doesn't resolve (floating tag) | Low | High | The platform's `release.yml` creates + force-moves the `v1.25` + `v1` floating tags on merge to main. The pilot contract uses `@v1.25`. |
|
||||
| Settlement-finality policy false-negatives (blocks a valid promotion) | Medium | Medium | REQ-315 tests cover passing + failing fixtures; the policy is skip-when-kj-absent (graceful). |
|
||||
| D-083 deferral challenged (audit ledger not tamper-evident) | Low | Low | D-204; the SQLite hash-chain + DynamoDB outbox is the pilot's audit record. Tamper-evidence is a future milestone. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Persona Assessment
|
||||
|
||||
See `PERSONAS.md` (next section, produced by the lead-developer at the
|
||||
end of RESEARCH). The active roster: backend-engineer (blockchain core
|
||||
+ settlement + outcome backfill), data-engineer (DynamoDB primitive +
|
||||
metrics cold store), policy-engineer (kyverno-json policies), +
|
||||
blockchain-engineer (custom, phase-specific — chain consensus, order
|
||||
matching, settlement finality). frontend-engineer is deactivated (no
|
||||
UI in the pilot).
|
||||
+184
-1
@@ -29,7 +29,7 @@
|
||||
- **v1.13.2 (complete, tag `v1.13.2`):** presentation badge cleanup + platform architecture diagram — removed all `testing`/`agentic` maturity badges from both decks (only `planned` retained); added a new Slide 3 "The platform at a glance" with a shared high-level logical architecture diagram (consumer surfaces → contract → central pipeline → cross-cutting components → AWS) to both decks; renumbered subsequent slides 4–11; synced talking points + README. Docs-only NFR patch (no code changes).
|
||||
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
||||
- **v1.23 (complete, tag `v1.22.6`):** Nova Deck Cleanup & Python PPTX — consolidated the deck to a single source-of-truth `*-marp.md` (deleted the plain `.md`; speaker notes + talking points embedded as Marp HTML comments); restored the clean S&P visual style (Marp `default` theme + inline `style:` block, matching the old `the-developer-experience.html`); retired `nova-sp-theme.css` from the render path (kept as reference); base64-inlined all images in the HTML for redistribution (`scripts/inline_images.py`); built a parallel structured editable S&P-themed PPTX generator (`scripts/render_pptx.py` via `python-pptx`); restyled benefit callouts (`<div class="benefit">`); targeted ~20-30% word-count trim on 8 verbose slides; removed the term "penetrate" repo-wide. 13 requirements (REQ-263..275), 6 phases. 43 tests pass.
|
||||
- **v1.24 (active):** Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement — fixes 5 consumer-guide accuracy issues (stale contract-fields table, inconsistent caller examples, misleading "dev only" apply phrasing, Step 8 promotion contradicts the per-env section, stale `@v1.19` reference wording) and adds platform-enforced destroy-on-environment-change: when a consumer edits `environment:` on a stable `contract.id` (Shape A promotion), the platform detects the change via the `nova-contracts` DynamoDB table, destroys the prior env's Terraform state (`spike/{id}/{prior_env}/`) before building the new env, and fails closed if the destroy fails (no orphan path). The per-environment caller-workflow path (Shape B) remains supported. New `core/env_transition.py` module. 15 requirements (REQ-276..290), 4 phases. Feature milestone; tags on v1.23.x line.
|
||||
- **v1.24 (complete, tag `v1.23.4`):** Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement — fixes 5 consumer-guide accuracy issues (stale contract-fields table, inconsistent caller examples, misleading "dev only" apply phrasing, Step 8 promotion contradicts the per-env section, stale `@v1.19` reference wording) and adds platform-enforced destroy-on-environment-change: when a consumer edits `environment:` on a stable `contract.id` (Shape A promotion), the platform detects the change via the `nova-contracts` DynamoDB table, destroys the prior env's Terraform state (`spike/{id}/{prior_env}/`) before building the new env, and fails closed if the destroy fails (no orphan path). The per-environment caller-workflow path (Shape B) remains supported. New `core/env_transition.py` module. 15 requirements (REQ-276..290), 4 phases. 287 tests pass. Feature milestone; tags on v1.23.x line.
|
||||
|
||||
---
|
||||
|
||||
@@ -2156,3 +2156,186 @@ release). **DONE.**
|
||||
milestone). Tag `v1.22.6` (final patch = milestone release). Merge
|
||||
`milestone/v1.23-deck-cleanup-python-pptx` → `main`.
|
||||
- **Requirements:** REQ-263..275 (13 requirements).
|
||||
|
||||
## v1.25 (complete, tag `v1.24.5`): kyverno-json Unified Policy Engine
|
||||
|
||||
`kyverno-json` — a Kyverno-ecosystem runtime that applies Kyverno policies
|
||||
to **any** JSON/YAML payload — becomes Nova's **primary compliance /
|
||||
policy tool**, implemented behind a swappable `PolicyEngine` adapter so
|
||||
OPA (or any other engine) can replace it one day. The unified-orchestrator
|
||||
model: Checkov and Wiz remain as raw-finding adapters feeding *into*
|
||||
kyverno-json meta-policies; the confidence signal is untouched (it already
|
||||
consumes `list[PolicyCheckResult]` engine-agnostically). Policies cover
|
||||
all four Nova artifacts: consumer contract JSON, resolved Stack IR,
|
||||
Terraform plan JSON, and the merged PCR list itself (meta-validation).
|
||||
The K8s-only Kyverno adapter stays documentation-only (D-053); the
|
||||
kyverno-json engine and the K8s adapter are siblings, not replacements.
|
||||
Quality improvement from the IDEATE pass: capability regression checks
|
||||
(`core/regression_verify.py` CAP-013/023/024) become declarative
|
||||
kyverno-json policies. New `policy-engineer` persona owns the policy
|
||||
territory. 19 requirements (REQ-291..309), 6 phases (P0 + P1..P4 + P5
|
||||
final). Tags: `v1.24.0` (P0) → `v1.24.5` (P5 = milestone release).
|
||||
|
||||
### Phase P1 — engine-core (planned, tag v1.24.1)
|
||||
- REQ-291: `core/policy_engine.py` — `PolicyEngine` Protocol +
|
||||
`PolicyEngineRegistry` (selects engine from `config.json.policy.engine`).
|
||||
- REQ-292: `config.json` gains `policy` object
|
||||
(`engine: "kyverno-json"`, `policy_root`).
|
||||
- REQ-293: `adapters/kyverno-json/kyverno_json_engine.py` —
|
||||
`KyvernoJsonEngine` (shells to `kj scan`; translates native output →
|
||||
PCR; `is_configured()` guards on `which kj`).
|
||||
- REQ-294: `adapters/kyverno-json/__init__.py` + `_smoke.json` policy +
|
||||
`scripts/install-kyverno-json.sh` + CI image install.
|
||||
- REQ-308: `tests/test_policy_engine.py` — protocol conformance,
|
||||
registry, NullEngine fallback.
|
||||
- REQ-309: `tests/test_kyverno_json_engine.py` — PCR schema validity,
|
||||
defensive parsing, `pytest.skip` when kj absent.
|
||||
|
||||
### Phase P2 — contract + stack-IR policies (planned, tag v1.24.2)
|
||||
- REQ-295: `adapters/kyverno-json/policies/contract/` — 4 policies over
|
||||
consumer contract JSON (id-pattern, env-enum, infra-min-1,
|
||||
forbid-unknown-fields).
|
||||
- REQ-296: `core/contract_resolver.py` invokes the engine pre-resolve
|
||||
(contract policies) — early-fail, confidence signal decides the gate.
|
||||
- REQ-297: `adapters/kyverno-json/policies/stack-ir/` — 3 policies over
|
||||
resolved Stack IR (tagging-standard, public-ingress, encryption-by-
|
||||
default — ports of v1.0/v1.8 imperative rules).
|
||||
- REQ-298: `core/contract_resolver.py` invokes the engine post-resolve
|
||||
(stack-IR policies); additive — existing tests pass.
|
||||
- REQ-299: `tests/test_stack_ir_policies.py` + fixtures (passing + failing
|
||||
IR; skip when kj absent).
|
||||
|
||||
### Phase P3 — plan-JSON policies + meta-orchestration + pipeline wiring (planned, tag v1.24.3)
|
||||
- REQ-300: `adapters/kyverno-json/policies/plan-json/` — 3 policies over
|
||||
`terraform show -json` (plaintext-secrets, iam-wildcard, kms-reference
|
||||
— ports of `checkov_adapter.py:RULE_MAP`).
|
||||
- REQ-301: `run_platform.sh` Step 5 gains a parallel kyverno-json pass;
|
||||
both PCR lists (checkov/wiz + kj) concatenate into the confidence
|
||||
signal's `policy` input; skips gracefully when `which kj` is false.
|
||||
- REQ-302: `tests/test_plan_json_policies.py` + fixtures;
|
||||
`tests/test_run_platform_plan_json_policies.py` (script-substring
|
||||
assertion).
|
||||
- REQ-303: `adapters/kyverno-json/policies/meta/` —
|
||||
`block-on-any-critical.json` (declarative critical-block; the
|
||||
`confidence_signal.py` hard-override stays as defense-in-depth) +
|
||||
`tagging-rules-agree.json` (asserts Checkov + kj agree on tagging).
|
||||
`tests/test_meta_policies.py`.
|
||||
|
||||
### Phase P4 — regression-gate policies + docs (planned, tag v1.24.4)
|
||||
- REQ-304: `adapters/kyverno-json/policies/regression/` — 3 policies over
|
||||
capability-inventory JSON (CAP-013/023/024) — declarative mirrors of
|
||||
`core/regression_verify.py` checks.
|
||||
- REQ-305: `tests/test_regression_policies.py` + fixtures (clean +
|
||||
drifted inventory); regression gate still 287/287 baseline.
|
||||
- REQ-306: `adapters/README.md` (new adapter row + PolicyEngine Protocol
|
||||
section) + `adapters/kyverno-json/README.md`.
|
||||
- REQ-307: `.ciagent/ARCHITECTURE.md` §12.7 (Policy Engine Registry) +
|
||||
`schemas/README.md` + `modules/STANDARDS.md` (policy-authoring
|
||||
standard) + `docs/METRICS.md` (swappable engine narrative).
|
||||
|
||||
### Phase P5 — final review + audit + milestone ship (Final Phase, tag v1.24.5)
|
||||
- Multi-persona code review across P1..P4 (lead-developer, backend-
|
||||
engineer, data-engineer, policy-engineer). Auto-fix P0; flag P1+.
|
||||
- Audit: reconstruction test (git log ↔ `.ciagent/`), branch hygiene,
|
||||
commit discipline.
|
||||
- Milestone ship: merge `phase/05-final-review-ship` →
|
||||
`milestone/v1.25-kyverno-json` → `main`; tag `v1.24.5` (= the v1.25
|
||||
release per prev-minor tagging rule); create Gitea release with full
|
||||
milestone summary; delete all milestone branches.
|
||||
- Update `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).
|
||||
- **Requirements:** REQ-291..309 (19 requirements).
|
||||
|
||||
## v1.26 (active, tag line `v1.25.x`): 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
|
||||
validator, T+1 settlement finality = block commit) — is activated
|
||||
against live AWS account `581513795199`. The consumer repo
|
||||
(`nova-blockchain-exchange`) owns the app code + `contract.yaml`; the
|
||||
platform repo (`acdl`) provides the deploy workflow (`deploy.yml@v1.25`),
|
||||
the policy engine (kyverno-json, swappable per v1.25), the confidence
|
||||
signal, and the HITL attestation gates. The milestone grounds the three
|
||||
Post-Pilot targets in NORTH_STAR.md (Touchless Resolution ≥99%, Human
|
||||
Escalation <0.1%, AI Decision Accuracy ≥99.5%) — the denominators
|
||||
activate when the pilot runs. Three kyverno-json policies extend v1.25:
|
||||
settlement-finality (securities-specific), pilot-readiness (no
|
||||
placeholder account), and the existing meta-policies (block-on-any-
|
||||
critical, tagging-rules-agree) apply over the pilot's PCRs. The
|
||||
env-JSON `state_backend` wiring gap is closed (adapter reads the env
|
||||
JSON's bucket). Multi-project mode activates (`nova-blockchain-exchange`
|
||||
is the 2nd tracked project). Pre-run (Workstream A) re-created the S3
|
||||
state bucket + DynamoDB outbox table (bootstrap). 12 requirements
|
||||
(REQ-310..321), 5 phases (P0 pre-execution + 4 execution + 1 final).
|
||||
Tags: `v1.25.0` (P0) → `v1.25.5` (P5 = milestone release).
|
||||
|
||||
### Phase P1 — blockchain-core (planned, tag v1.25.1)
|
||||
- REQ-310: `nova-blockchain-exchange` repo — homegrown PoA blockchain
|
||||
core (`chain/block.py`, `chain/ledger.py`, `chain/validator.py`).
|
||||
Append-only blocks, single validator, SHA-256 hash chain,
|
||||
deterministic block production, genesis block.
|
||||
- REQ-311: Order-matching engine (`engine/order_book.py`,
|
||||
`engine/order.py`) — limit order book, price-time priority, partial
|
||||
fills.
|
||||
- REQ-312: Settlement service (`settlement/service.py`) — T+1,
|
||||
idempotent, finality = block commit.
|
||||
|
||||
### Phase P2 — consumer-contract-and-deploy (planned, tag v1.25.2)
|
||||
- REQ-322: `modules/l1/dynamodb/` — new L1 primitive (interface.json +
|
||||
terraform/main.tf + README.md + instance.json + registry.json entry).
|
||||
The single platform-side module build-out (ECS + S3 already exist;
|
||||
the adapter is stateless/registry-driven). Lands in P2 W0 (before the
|
||||
contract) so the contract's `dynamodb` block resolves at registry time.
|
||||
- REQ-313: `nova-blockchain-exchange/contract.yaml` + per-env variants
|
||||
(dev/qa/prod) — validated against `schemas/contract.schema.json`.
|
||||
- REQ-314: `nova-blockchain-exchange/.github/workflows/deploy.yml` +
|
||||
`.gitea/workflows/deploy.yml` — `uses: acdl/.github/workflows/deploy.yml@v1.25`
|
||||
with `mode: full`.
|
||||
|
||||
### Phase P3 — pilot-metrics-and-policies (planned, 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).
|
||||
- REQ-316: `core/regression_verify.py` gains CAP-025
|
||||
(live-pilot-apply) — the round-trip assertion (contract resolve →
|
||||
adapter compile → terraform plan → policy scan → confidence signal →
|
||||
attestation → outbox record) against `581513795199`.
|
||||
- REQ-317: `core/metrics/outcome_backfill.py` — wire
|
||||
`apply.completed`/`apply.failed` → `fact_decision.outcome` (grounds AI
|
||||
Decision Accuracy; today `outcome` is stuck `pending`).
|
||||
- REQ-318: `core/confidence_signal.py` — `ai.decision.made` gains
|
||||
`escalation_reason: 'confidence'` when `band == 'block'` (grounds
|
||||
Human Escalation Frequency numerator).
|
||||
- REQ-319: `adapters/terraform/adapter.py` — reads
|
||||
`env.state_backend.bucket` from the env JSON (closing the wiring gap);
|
||||
`core/environments/*.json` `state_backend.bucket` →
|
||||
`nova-tfstate-581513795199-us-east-1`.
|
||||
- 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)
|
||||
- REQ-321: `adapters/README.md` (new consumer row) +
|
||||
`docs/METRICS.md` (Post-Pilot metrics grounded note) +
|
||||
`.ciagent/ARCHITECTURE.md` §12.8 (Pilot Estate) +
|
||||
`.ciagent/nova-blockchain-exchange/README.md` (onboarding guide).
|
||||
- Live pilot end-to-end run: `nova-blockchain-exchange` contract →
|
||||
`deploy.yml@v1.25` mode=full → apply → attest → record against
|
||||
`581513795199`. The run's `ai.decision.made` + `attestation.recorded`
|
||||
events land in the Decision Ledger; the regression gate (CAP-025)
|
||||
verifies the round-trip.
|
||||
|
||||
### Phase P5 — final review + audit + milestone ship (Final Phase, tag v1.25.5)
|
||||
- Multi-persona code review across P1..P4 (lead-developer, backend-
|
||||
engineer, data-engineer, policy-engineer, blockchain-engineer).
|
||||
Auto-fix P0; flag P1+.
|
||||
- Audit: reconstruction test (git log ↔ `.ciagent/`), branch hygiene,
|
||||
commit discipline.
|
||||
- Milestone ship: merge `phase/05-final-review-ship` →
|
||||
`milestone/v1.26-pilot-activation` → `main`; tag `v1.25.5` (= the
|
||||
v1.26 release per prev-minor tagging rule); create Gitea release with
|
||||
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).
|
||||
- **Requirements:** REQ-310..322 (13 requirements).
|
||||
|
||||
+75
-123
@@ -1,135 +1,87 @@
|
||||
# ACDL v1.10 — Verify (milestone gate)
|
||||
# VERIFY — P1 engine-core (v1.25)
|
||||
|
||||
> Verify date: 2026-07-27. Verifier: ci-verifier. Milestone: v1.10 (complete, tag `v1.10.0`).
|
||||
> Scope: 4 phases (52–55), 5 commits (772ac72..2697775), 22 files, +2281/-256 lines.
|
||||
> 4-layer verify gate: structural, behavioral, security, quality.
|
||||
> Phase: P1. Requirements: REQ-291..294, 308, 309. Result: PASS.
|
||||
|
||||
## Layer 1: Structural — PASS
|
||||
## Structural
|
||||
|
||||
- All 8 plan-referenced files exist on disk (`core/regression_verify.py`,
|
||||
`core/local_emulators.py`, `scripts/run_regression.sh`,
|
||||
`tests/test_verify_regression_mode.py`,
|
||||
`tests/test_local_emulating_adapters.py`,
|
||||
`.ciagent/CAPABILITY_INVENTORY.md`, `REGRESSION_REPORT.md`,
|
||||
`REGRESSION_REPORT.json`).
|
||||
- All imports resolve (`py_compile` + runtime import OK).
|
||||
- No TODO/FIXME/HACK/stub placeholders in new code (the `LocalLambdaStub`
|
||||
is a legitimate local emulator, not a placeholder).
|
||||
- All declared exports exist (`run_regression`, `write_report`,
|
||||
`CAPABILITY_REGISTRY`, `RegressionReport`, `CapabilityResult`,
|
||||
`FlatFileOutbox`, `LocalEcsEmulator`, `LocalS3StateBackend`,
|
||||
`LocalLambdaStub`, `run_local_e2e`, `is_local_tier`).
|
||||
- `core/policy_engine.py` exists, implements `PolicyEngine` Protocol
|
||||
(PEP 544, `@runtime_checkable`), `PolicyEngineRegistry` with
|
||||
`register()` + `get_engine()`, `NullEngine` fallback.
|
||||
- `adapters/kyverno-json/kyverno_json_engine.py` exists, exports
|
||||
`KyvernoJsonEngine` with `name`, `is_configured()`, `evaluate()`.
|
||||
- `adapters/kyverno-json/__init__.py` loads the engine by file path
|
||||
(the dir name has a hyphen — not a valid Python package name).
|
||||
- `adapters/kyverno-json/policies/_smoke.json` exists (trivial policy
|
||||
for round-trip validation).
|
||||
- `scripts/install-kyverno-json.sh` exists (go install kj@latest).
|
||||
- `.ciagent/config.json` has the `policy` object
|
||||
(`engine: kyverno-json`, `policy_root`).
|
||||
- `.gitea/workflows/ci.yml` + `.github/workflows/ci.yml` have the
|
||||
Go + kj install step (best-effort, tests skip when kj absent).
|
||||
- `tests/test_policy_engine.py` (10 tests) +
|
||||
`tests/test_kyverno_json_engine.py` (16 tests) exist.
|
||||
|
||||
## Layer 2: Behavioral — PASS
|
||||
## Behavioral
|
||||
|
||||
- `pytest tests/ -m "not slow"`: **513 passed**, 5 deselected.
|
||||
- `pytest tests/ -m slow`: **5 passed** (2 local E2E + 3 regression
|
||||
integration incl. live-AWS terraform plan).
|
||||
- **Total: 518 passed, 0 failed.**
|
||||
- Requirement coverage: REQ-112 (P52), REQ-113 (P53), REQ-114 (P54),
|
||||
REQ-115 (P55) — all 4 marked `complete`.
|
||||
- Regression gate: `bash scripts/run_regression.sh` → **16/16
|
||||
capabilities Verified** (12 local + 4 live-AWS). Milestone gate open.
|
||||
- `pytest tests/test_policy_engine.py tests/test_kyverno_json_engine.py`:
|
||||
**24 passed, 2 skipped** (kj not installed — expected;
|
||||
`pytest.skip("kj not installed")`).
|
||||
- `NullEngine` satisfies the `PolicyEngine` Protocol (G-Q8a —
|
||||
`isinstance(NullEngine(), PolicyEngine)` is True). Proves the swap
|
||||
boundary is real without implementing OPA.
|
||||
- `KyvernoJsonEngine.is_configured()` returns `False` when
|
||||
`which kj` is absent → `evaluate()` returns a single
|
||||
`KJ_ENGINE_NOT_CONFIGURED` SKIPPED PCR (distinct `ruleId` from
|
||||
NullEngine's `NULL_ENGINE_INACTIVE` — G-Q4).
|
||||
- PCR records validate against `schemas/policy_check_result.schema.json`
|
||||
(via `jsonschema.validate` in tests).
|
||||
- Defensive parsing: malformed kyverno-json output → `error` PCR
|
||||
(`KJ_ENGINE_ERROR`), never an exception.
|
||||
- Severity annotation reading (G-Q10a): policies with
|
||||
`nova.cloudinit.dev/severity: high` produce PCRs with `severity: high`;
|
||||
policies without the annotation default to `info`.
|
||||
- Registry: `get_engine()` returns the configured engine; unknown
|
||||
engine name raises `KeyError`; `policy` key absent → `NullEngine`.
|
||||
- No regression: `pytest tests/test_confidence_signal.py
|
||||
tests/test_adapter.py tests/test_checkov_adapter.py
|
||||
tests/test_kyverno_adapter.py tests/test_contract_resolver.py` —
|
||||
**132 passed** (unchanged).
|
||||
|
||||
## Layer 3: Security (STRIDE) — PASS
|
||||
## Security
|
||||
|
||||
| Threat | Risk | Disposition |
|
||||
|--------|------|-------------|
|
||||
| Spoofing | Local Lambda stub patches `_get_dynamodb`/`_get_secrets_client`; opt-in via `ACDL_LOCAL_TIER=1`, never in prod | Accept (low) |
|
||||
| Tampering | Flat-file outbox hash-chain verification detects tampering | Accept (low) |
|
||||
| Repudiation | Regression report records per-capability status + timestamps | Accept (low) |
|
||||
| Info Disclosure | Creds read into env vars, never logged (0 cred strings in reports); ECS binds 127.0.0.1 only | Accept (low) |
|
||||
| Denial of Service | Local ECS emulator: free port, daemon thread, clean destroy | Accept (low) |
|
||||
| Elevation of Privilege | `urllib.urlopen` patched to fake response (no network egress); no eval/exec/subprocess in adapter | Accept (low) |
|
||||
- No new secrets, no new network calls in the engine core (the engine
|
||||
shells to a local binary; the binary makes no network calls for
|
||||
`scan`).
|
||||
- `is_configured()` guard ensures the platform runs without the binary
|
||||
(no hard dependency that could be exploited as a DoS vector).
|
||||
- The engine writes the payload to a temp file (`tempfile.NamedTemporaryFile`)
|
||||
and unlinks it in a `finally` block (no leftover payload on disk).
|
||||
- No `shell=True` in the `subprocess.run` call (command is a list —
|
||||
no shell injection surface).
|
||||
|
||||
All threats low-severity; auto-accepted per
|
||||
`config.json security.auto_accept_low_severity=true`.
|
||||
## Quality
|
||||
|
||||
## Layer 4: Quality (multi-persona) — PASS
|
||||
- `python3 -m py_compile` passes on all new Python files.
|
||||
- The `PolicyEngine` Protocol is minimal (3 members) — the swap
|
||||
boundary is the moat (NORTH_STAR Strategic Objective #2).
|
||||
- The `NullEngine` proves a second implementation exists (structural
|
||||
conformance) — the OPA swap is a known quantity (RESEARCH §4.2).
|
||||
- Tests use `pytest.skip` when `which kj` is absent, so the CI matrix
|
||||
passes with or without the binary (the suite is green in both cases).
|
||||
|
||||
| Persona | Finding | Verdict |
|
||||
|---------|---------|---------|
|
||||
| Correctness | 7 adapter defects fixed; each traceable to a terraform validate/plan error | PASS |
|
||||
| Testing | 518 tests pass; 24 new tests. P2: uptime-kuma + RDS not in registry | PASS (1 P2) |
|
||||
| Security | No creds logged; loopback-only; monkey-patches scoped to local tier | PASS |
|
||||
| Performance | Regression run ~60s; acceptable for a milestone gate | PASS |
|
||||
| Maintainability | Well-structured; adding a capability = 1 function + 1 registry entry | PASS |
|
||||
| Adversarial | Gate can't be bypassed; local E2E can't mutate cloud; no injection vectors | PASS |
|
||||
## Must-have checklist
|
||||
|
||||
**0 P0, 0 P1, 1 P2 (post-hoc: expand regression registry to uptime-kuma + RDS stacks).**
|
||||
- [x] `PolicyEngine` Protocol + `PolicyEngineRegistry` + `NullEngine`
|
||||
(REQ-291)
|
||||
- [x] `config.json.policy` object (REQ-292)
|
||||
- [x] `KyvernoJsonEngine` adapter (REQ-293)
|
||||
- [x] `__init__.py` + `_smoke.json` + `install-kyverno-json.sh` + CI
|
||||
install (REQ-294)
|
||||
- [x] `test_policy_engine.py` — protocol conformance, registry,
|
||||
NullEngine fallback (REQ-308)
|
||||
- [x] `test_kyverno_json_engine.py` — PCR schema validity, defensive
|
||||
parsing, skip-without-kj (REQ-309)
|
||||
|
||||
## Verdict
|
||||
|
||||
**VERIFY PASS** — all 4 layers pass. The v1.10 milestone is sound:
|
||||
the pipeline regression gap is fixed (D-091), the platform is fully
|
||||
locally testable (D-092), every advertised capability is re-verified
|
||||
(D-093, 16/16 Verified), and the docs/decks match verified reality
|
||||
(D-094). 518 tests pass; the regression gate covers 16 capabilities
|
||||
including 4 live-AWS checks. 0 P0, 0 P1, 1 P2 post-hoc. Ready to ship.
|
||||
|
||||
---
|
||||
|
||||
# ACDL — Verify (grill deliverable, commit ac11c01)
|
||||
|
||||
> Verify date: 2026-07-27. Verifier: ci-verifier. Scope: the grill
|
||||
> deliverable (`.ciagent/GRILL.md`, phase 0, status `grill`) added in
|
||||
> commit `ac11c01` since the v1.10 audit PASS (`ab477b3`). Docs-only;
|
||||
> no code, no tests, no schema changes.
|
||||
|
||||
## Layer 1: Structural — PASS
|
||||
|
||||
- `.ciagent/GRILL.md` exists on disk (18250 bytes).
|
||||
- No imports to resolve (markdown docs file).
|
||||
- No TODO/FIXME/HACK/stub placeholders in the report.
|
||||
- All required sections present per grill workflow Step 5 format:
|
||||
title, Run header, Verdict, 9 axes (1–9), Meta, Binding Decisions
|
||||
table (12 rows), Escalations section (2 entries: G-005, G-008).
|
||||
- Commit `ac11c01` `---ci---` block is well-formed: `project: acdl`,
|
||||
`phase: 0`, `milestone: v1.10`, `status: grill`, 12 decision ids
|
||||
(G-001..G-012), 2 escalation lines.
|
||||
|
||||
## Layer 2: Behavioral — PASS
|
||||
|
||||
- `pytest tests/ -m "not slow"`: **513 passed**, 5 deselected (no
|
||||
regressions introduced by the docs-only grill commit).
|
||||
- No new tests required (docs-only deliverable; the grill is a
|
||||
review artifact, not a code change).
|
||||
- Requirement coverage: not applicable (phase 0, status `grill`; no
|
||||
REQ-IDs bound to this deliverable). The grill's binding decisions
|
||||
(G-001..G-012) are advisory and do not modify REQUIREMENTS.md per
|
||||
grill workflow Step 7.
|
||||
|
||||
## Layer 3: Security (STRIDE) — PASS
|
||||
|
||||
| Threat | Risk | Disposition |
|
||||
|--------|------|-------------|
|
||||
| Spoofing | N/A (docs-only; no auth surface) | Accept (none) |
|
||||
| Tampering | Grill report is git-tracked; tampering = git history rewrite (out of scope) | Accept (low) |
|
||||
| Repudiation | Commit `ac11c01` signed by author; `---ci---` block records status + decisions | Accept (low) |
|
||||
| Info Disclosure | No credentials, keys, tokens, or PII in the report (grep scan clean) | Accept (low) |
|
||||
| Denial of Service | N/A (docs file; no runtime surface) | Accept (none) |
|
||||
| Elevation of Privilege | N/A (docs-only; no privilege surface) | Accept (none) |
|
||||
|
||||
All threats low-or-none; auto-accepted per
|
||||
`config.json security.auto_accept_low_severity=true`.
|
||||
|
||||
## Layer 4: Quality (multi-persona) — PASS
|
||||
|
||||
| Persona | Finding | Verdict |
|
||||
|---------|---------|---------|
|
||||
| Correctness | 12 binding decisions traceable to evidence (commit/file/req-id); 2 escalations correctly unresolved | PASS |
|
||||
| Testing | Docs-only; 513 fast tests pass (no regression) | PASS |
|
||||
| Security | No credential leakage; no sensitive data in report | PASS |
|
||||
| Performance | N/A (docs file; no runtime cost) | PASS |
|
||||
| Maintainability | Report follows grill workflow Step 5 format exactly; appendable for future runs | PASS |
|
||||
| Adversarial | Escalations (G-005, G-008) are surfaced, not silently skipped; visible via `ciagent audit` | PASS |
|
||||
|
||||
**0 P0, 0 P1, 0 P2.**
|
||||
|
||||
## Verdict (grill deliverable)
|
||||
|
||||
**VERIFY PASS** — all 4 layers pass. The grill deliverable is a
|
||||
well-formed docs-only artifact. 513 fast tests pass (no regression).
|
||||
No credential leakage. 12 binding decisions recorded; 2 escalations
|
||||
(G-005 risks, G-008 budget) correctly surfaced for human resolution.
|
||||
The grill does not modify PROJECT.md, ROADMAP.md, or REQUIREMENTS.md
|
||||
(per grill workflow Step 7).
|
||||
**Verdict: PASS** — all P1 must-haves met, no regressions, 24 new
|
||||
tests pass (2 skip-without-kj), 132 existing tests unchanged.
|
||||
+14
-5
@@ -4,11 +4,16 @@
|
||||
"slug": "acdl",
|
||||
"name": "Nova — The New Dawn of DevSecOps",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"slug": "nova-blockchain-exchange",
|
||||
"name": "Nova Pilot Consumer — Blockchain Stock Exchange",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"active_project": "acdl",
|
||||
"active_projects": ["acdl"],
|
||||
"active_milestone": "v1.24",
|
||||
"active_projects": ["acdl", "nova-blockchain-exchange"],
|
||||
"active_milestone": "v1.26",
|
||||
"autonomy": {
|
||||
"level": "full",
|
||||
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
|
||||
@@ -59,7 +64,7 @@
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "flat",
|
||||
"_branching_strategy_note": "ACDL uses flat workflow (committed directly to main per established convention since v1.0). 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→Nova 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
|
||||
},
|
||||
@@ -67,7 +72,7 @@
|
||||
"sources": [".env", ".env.secrets", ".env.*"],
|
||||
"disallow": ["shell_env", "netrc", "keychain", "rc_files", "global_config"],
|
||||
"scopes": {
|
||||
"gitea": "ACDL_GITEA_TOKEN",
|
||||
"gitea": "NOVA_GITEA_TOKEN",
|
||||
"github": "GITHUB_TOKEN",
|
||||
"gitlab": "GITLAB_TOKEN",
|
||||
"openai": "OPENAI_API_KEY",
|
||||
@@ -209,5 +214,9 @@
|
||||
"enabled": true,
|
||||
"persist": true
|
||||
},
|
||||
"strategic_direction_file": ".ciagent/NORTH_STAR.md"
|
||||
"strategic_direction_file": ".ciagent/NORTH_STAR.md",
|
||||
"policy": {
|
||||
"engine": "kyverno-json",
|
||||
"policy_root": "adapters/kyverno-json/policies"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Nova Pilot Consumer — Blockchain Stock Exchange
|
||||
|
||||
> **Milestone:** v1.26 — Live Pilot Estate Activation
|
||||
> **Git:** https://git.cloudinit.dev/continuous-intelligence/nova-blockchain-exchange
|
||||
> **Local clone:** /root/nova-blockchain-exchange
|
||||
> **Role:** The first real consumer estate. A stock exchange built on a
|
||||
> homegrown blockchain, offering equities trading (pilot scope). The
|
||||
> consumer repo owns the app code + `contract.yaml`; the Nova platform
|
||||
> (`acdl` repo) provides the deploy workflow, policy engine, and
|
||||
> attestation gates.
|
||||
|
||||
---
|
||||
|
||||
## Vision / Core Value
|
||||
|
||||
A self-contained securities-trading exchange where every order, match,
|
||||
and settlement is recorded as an immutable transaction on a homegrown
|
||||
Proof-of-Authority (PoA) blockchain. The pilot demonstrates that Nova's
|
||||
autonomous infrastructure can take a real consumer estate from contract
|
||||
to production — apply, attest, record — without an operator in the loop
|
||||
of normal operations.
|
||||
|
||||
## North Star Alignment
|
||||
|
||||
- **Strategic Objective #1** (production-grade zero-touch operations):
|
||||
this estate is the first real consumer; the pilot activates the
|
||||
autonomy claim beyond internal demos.
|
||||
- **Strategic Objective #2** (provable trust): every apply decision +
|
||||
attestation lands in the Decision Ledger; the settlement-finality
|
||||
kyverno-json policy (IDEATE) makes trust a policy artifact.
|
||||
- **Strategic Objective #3** (compounding ROI): unblocks the three
|
||||
Post-Pilot targets (Touchless Resolution ≥99%, Human Escalation
|
||||
<0.1%, AI Decision Accuracy ≥99.5%) — the denominators activate when
|
||||
this estate runs.
|
||||
|
||||
## Domain Boundaries
|
||||
|
||||
- **This repo owns:** the blockchain (consensus, blocks, transactions),
|
||||
the order-matching engine, the settlement service, the `contract.yaml`
|
||||
that declares the infrastructure, and the consumer-side deploy workflow
|
||||
invocation (`uses: acdl/.github/workflows/deploy.yml@v1.25`).
|
||||
- **The platform (`acdl`) repo owns:** the deploy workflow, the policy
|
||||
engine (kyverno-json), the contract resolver, the adapter, the
|
||||
confidence signal, the HITL gates, and the Decision Ledger.
|
||||
|
||||
## Scope: v1.26 Pilot
|
||||
|
||||
- **Equities only** (bonds, derivatives, options deferred to future
|
||||
milestones — different settlement models).
|
||||
- **Minimal PoA ledger** — append-only blocks, single validator (pilot),
|
||||
T+1 settlement finality = block commit. No multi-validator BFT.
|
||||
- **Homegrown chain** — authored as part of this repo, not deployed on
|
||||
Ethereum/Solana/Hyperledger.
|
||||
|
||||
## Anti-Goals (v1.26)
|
||||
|
||||
1. Not a general-purpose blockchain platform — purpose-built for
|
||||
securities settlement in the pilot.
|
||||
2. Not multi-validator consensus — single validator for the pilot.
|
||||
3. Not bonds/derivatives/options — equities only this milestone.
|
||||
4. Not a replacement for the Nova platform — this is a *consumer* of
|
||||
Nova, not a fork.
|
||||
|
||||
## Key Decisions (v1.26 — established in SPECIFY, refined in CLARIFY)
|
||||
|
||||
| ID | Decision | Rationale | Affects |
|
||||
|---|---|---|---|
|
||||
| D-200 | Pilot scope = equities only | Bonds/derivatives/options have very different settlement models; equities (T+1) is the simplest to demonstrate the Nova platform's policy gates over a real estate. | Phase count; requirement scope. |
|
||||
| D-201 | Homegrown PoA ledger (single validator) | Minimal viable chain for a pilot; settlement finality = block commit. Multi-validator BFT is a future milestone. | Blockchain core design. |
|
||||
| D-202 | Consumer repo = `nova-blockchain-exchange` (Gitea) | New repo under `continuous-intelligence` org; tracked as 2nd CIAgent project. | Multi-project config. |
|
||||
| D-203 | AWS account = 581513795199 (existing) | Reuse the bootstrapped account; state bucket + outbox table created in pre-run Workstream A3. | Env JSON binding. |
|
||||
| D-204 | D-083 (S3 Object Lock/JWS) stays deferred | The SQLite hash-chain + DynamoDB outbox is the pilot's audit record. Tamper-evidence is a future milestone. | Audit ledger scope. |
|
||||
| D-205 | Cold-only metrics sufficient (D-126) | No hot ops dashboard in the pilot; cold SQLite store + PowerBI export. | Metrics pipeline. |
|
||||
|
||||
## Constraints
|
||||
|
||||
- The consumer repo's deploy MUST go through `deploy.yml@v1.25` (the
|
||||
reusable workflow) — no direct `terraform apply` bypassing the
|
||||
platform's policy + attestation gates.
|
||||
- The `contract.yaml` MUST validate against
|
||||
`schemas/contract.schema.json`.
|
||||
- The homegrown blockchain MUST be deterministic (same inputs → same
|
||||
block) — it is automation, not AI (NORTH_STAR Objective #2 tenet).
|
||||
|
||||
## Context
|
||||
|
||||
- The Nova platform (`acdl` repo) completed v1.25 (kyverno-json Unified
|
||||
Policy Engine). The swappable `PolicyEngine` adapter is in place.
|
||||
- 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`.
|
||||
@@ -0,0 +1,221 @@
|
||||
# Requirements — nova-blockchain-exchange (v1.26 pilot)
|
||||
|
||||
> **Project:** nova-blockchain-exchange — blockchain stock exchange (pilot)
|
||||
> **Milestone:** v1.26 — Live Pilot Estate Activation
|
||||
> **Scope:** equities only; minimal PoA ledger; T+1 settlement finality.
|
||||
|
||||
---
|
||||
|
||||
## v1.26 — Live Pilot Estate Activation
|
||||
|
||||
### REQ-310 — Homegrown PoA blockchain core
|
||||
|
||||
The consumer repo implements a minimal Proof-of-Authority blockchain:
|
||||
append-only blocks, single validator (pilot), SHA-256 block hash chain,
|
||||
deterministic block production (same ordered transactions → same block).
|
||||
The chain records every order, match, and settlement as transactions.
|
||||
Settlement finality = block commit (a transaction is final when its
|
||||
block is committed to the chain).
|
||||
|
||||
**Must-haves:**
|
||||
- `chain/block.py` — Block dataclass (index, timestamp, prev_hash,
|
||||
transactions, nonce, hash). `compute_hash()` deterministic.
|
||||
- `chain/ledger.py` — Ledger class: `append_block()`, `verify_chain()`,
|
||||
`get_block(index)`, `get_latest_block()`. Genesis block on init.
|
||||
- `chain/validator.py` — PoA validator: single validator (config-driven,
|
||||
pilot), `propose_block(transactions)` → Block, `commit_block(block)`.
|
||||
- `tests/test_block.py`, `tests/test_ledger.py`, `tests/test_validator.py`
|
||||
— chain integrity, hash determinism, genesis, append/verify.
|
||||
|
||||
### REQ-311 — Order-matching engine
|
||||
|
||||
A limit-order-book matching engine: buy/sell orders with price + size,
|
||||
matched at the best price (price-time priority). Produces match
|
||||
transactions recorded on the chain.
|
||||
|
||||
**Must-haves:**
|
||||
- `engine/order_book.py` — OrderBook: `add_order(order)`,
|
||||
`match_orders()` → list of Match (buyer, seller, price, size).
|
||||
- `engine/order.py` — Order dataclass (id, side, symbol, price, size,
|
||||
timestamp).
|
||||
- `tests/test_order_book.py` — match priority, partial fills, no-match.
|
||||
|
||||
### REQ-312 — Settlement service
|
||||
|
||||
T+1 settlement: matches commit to the chain; a settlement is final when
|
||||
its block is committed. The service reads matches from the order engine,
|
||||
produces settlement transactions, and submits them to the ledger.
|
||||
|
||||
**Must-haves:**
|
||||
- `settlement/service.py` — SettlementService: `settle(match)` →
|
||||
SettlementTransaction, `submit(ledger)`. Idempotent (re-settling a
|
||||
match is a no-op once final).
|
||||
- `tests/test_settlement.py` — happy path, idempotency, finality check.
|
||||
|
||||
### REQ-313 — Consumer `contract.yaml`
|
||||
|
||||
The consumer repo declares its infrastructure via a `contract.yaml` at
|
||||
the repo root, validated against `schemas/contract.schema.json`. The
|
||||
contract references the Nova platform's deploy workflow
|
||||
(`uses: acdl/.github/workflows/deploy.yml@v1.25`) and declares the
|
||||
blockchain exchange stack (the AWS resources the app needs: ECS for
|
||||
the matching engine, DynamoDB for the ledger, S3 for block storage).
|
||||
The DynamoDB L1 primitive (REQ-322) must land before this contract can
|
||||
declare `dynamodb` — ECS + S3 already exist.
|
||||
|
||||
**Must-haves:**
|
||||
- `contract.yaml` — id, name (`blockchain-exchange`), environment
|
||||
(dev/qa/prod variants), infrastructure block.
|
||||
- `contracts/blockchain-exchange.dev.yml`, `.qa.yml`, `.prod.yml` —
|
||||
per-environment variants (per-env promotion model, REQ-105).
|
||||
- `tests/test_contract_validates.py` — schema validation against the
|
||||
platform's `schemas/contract.schema.json`.
|
||||
|
||||
### REQ-314 — Consumer deploy workflow invocation
|
||||
|
||||
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.
|
||||
|
||||
**Must-haves:**
|
||||
- `.github/workflows/deploy.yml` — `uses: acdl/.github/workflows/deploy.yml@v1.25`
|
||||
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.
|
||||
|
||||
### REQ-315 — Settlement-finality kyverno-json policy (IDEATE I6)
|
||||
|
||||
A kyverno-json policy asserting that every promotion (qa→prod) requires
|
||||
settlement finality: all matches in the promotion window have committed
|
||||
blocks. This is the securities-specific extension of v1.25's policy
|
||||
engine — it applies Nova's compliance posture to the blockchain domain.
|
||||
|
||||
**Must-haves:**
|
||||
- `policies/settlement-finality.json` — kyverno-json policy over the
|
||||
settlement-service status JSON (asserts `all_committed: true`).
|
||||
- `tests/test_settlement_finality_policy.py` — passing + failing
|
||||
fixtures; skip when `kj` absent.
|
||||
|
||||
### REQ-316 — Pilot-estate regression capability (CAP-025)
|
||||
|
||||
A new capability in the regression gate: "pilot estate apply→attest→record
|
||||
round-trip." The regression gate asserts that the consumer estate can
|
||||
run end-to-end (contract resolve → adapter compile → terraform plan →
|
||||
policy scan → confidence signal → attestation → outbox record) against
|
||||
the live AWS account `581513795199`.
|
||||
|
||||
**Must-haves:**
|
||||
- `core/regression_verify.py` gains CAP-025 (live-pilot-apply).
|
||||
- `tests/test_regression_pilot.py` — the round-trip assertion.
|
||||
|
||||
### REQ-317 — Outcome-backfill emitter (IDEATE I1)
|
||||
|
||||
Wire `apply.completed` / `apply.failed` events back into `fact_decision`
|
||||
in the cold store so the AI Decision Accuracy metric has a non-`pending`
|
||||
outcome. Today `fact_decision.outcome` is stuck at `pending` (D-096
|
||||
blocker). The backfill emitter reads `run_manifest.completed/failed`
|
||||
events and updates the corresponding decision's outcome.
|
||||
|
||||
**Must-haves:**
|
||||
- `core/metrics/outcome_backfill.py` — `backfill(decision_id, outcome)`
|
||||
updates `fact_decision.outcome` + `fact_decision.backfilled_at`.
|
||||
- `core/metrics/collector.py` — invokes backfill after run completion.
|
||||
- `tests/test_outcome_backfill.py`.
|
||||
|
||||
### REQ-318 — `reason='confidence'` escalation tag (IDEATE I2)
|
||||
|
||||
Emit a distinct `reason='confidence'` field on the `block` band's
|
||||
`ai.decision.made` event so the Human Escalation Frequency metric has a
|
||||
discriminated numerator. Today `hitl_block` is a boolean from the
|
||||
manifest; the `reason` discriminator is not stored.
|
||||
|
||||
**Must-haves:**
|
||||
- `core/confidence_signal.py` — `ai.decision.made` gains
|
||||
`escalation_reason: 'confidence'` when `band == 'block'`.
|
||||
- `core/metrics/collector.py` — persists `escalation_reason` into
|
||||
`fact_run`.
|
||||
- `tests/test_confidence_escalation_reason.py`.
|
||||
|
||||
### REQ-319 — Env-JSON `state_backend` wiring reconciliation (IDEATE I3)
|
||||
|
||||
The env JSON's `state_backend.bucket` field is currently unused by the
|
||||
adapter (the adapter computes `nova-tfstate-<AWS_ACCOUNT_ID>` directly).
|
||||
Reconcile: the adapter reads `state_backend.bucket` from the env JSON
|
||||
(falling back to the computed name for backwards compat). This closes
|
||||
the wiring gap so the pilot's env JSON is the single source of truth.
|
||||
|
||||
**Must-haves:**
|
||||
- `adapters/terraform/adapter.py` — reads `env.state_backend.bucket`
|
||||
when present.
|
||||
- `tests/test_adapter_state_backend.py`.
|
||||
- `core/environments/*.json` — `state_backend.bucket` updated to the
|
||||
real bucket name `nova-tfstate-581513795199-us-east-1`.
|
||||
|
||||
### REQ-320 — Declarative pilot-readiness kyverno-json policy (IDEATE I5)
|
||||
|
||||
A kyverno-json policy asserting the env JSON has a non-placeholder
|
||||
`account_id` (not `000000000000`) before any `terraform apply`. This is
|
||||
the declarative gate that prevents a pilot run against a placeholder
|
||||
account.
|
||||
|
||||
**Must-haves:**
|
||||
- `adapters/kyverno-json/policies/pilot-readiness/no-placeholder-account.json`
|
||||
- `tests/test_pilot_readiness_policy.py`.
|
||||
|
||||
### REQ-321 — Docs + adapter README for the consumer estate
|
||||
|
||||
Update `adapters/README.md` (new consumer row), `docs/METRICS.md` (the
|
||||
3 Post-Pilot metrics now grounded post-pilot), `.ciagent/ARCHITECTURE.md`
|
||||
(§12.8 — Pilot Estate), and `.ciagent/nova-blockchain-exchange/README.md`
|
||||
(consumer onboarding guide).
|
||||
|
||||
**Must-haves:**
|
||||
- `adapters/README.md` — consumer-repo row.
|
||||
- `docs/METRICS.md` — Post-Pilot metrics grounded note.
|
||||
- `.ciagent/ARCHITECTURE.md` — §12.8 Pilot Estate.
|
||||
- `.ciagent/nova-blockchain-exchange/README.md` — onboarding guide.
|
||||
|
||||
### REQ-322 — DynamoDB L1 primitive (platform-side)
|
||||
|
||||
The blockchain exchange's ledger table needs a DynamoDB L1 primitive.
|
||||
Research (RESEARCH §3) confirmed the adapter is stateless/registry-
|
||||
driven (no `TYPE_MAP` — deleted in v1.11); a new stack type requires a
|
||||
new L1 module, not an adapter change. The `dynamodb` primitive mirrors
|
||||
the existing `s3` / `rds` primitives: `interface.json` (stack type
|
||||
`aws:dynamodb:table`, inputs `table_name`/`region`/`pk`/`sk`/`billing_mode`,
|
||||
outputs `table_arn`/`table_name`), `terraform/main.tf`
|
||||
(`resource "aws_dynamodb_table" "this"`), `README.md`, `instance.json`,
|
||||
+ a `registry.json` entry. The pilot contract's `infrastructure.dynamodb`
|
||||
block references this primitive. This is the single platform-side
|
||||
module build-out for the milestone (ECS + S3 already exist).
|
||||
|
||||
**Must-haves:**
|
||||
- `modules/l1/dynamodb/interface.json` — stack type
|
||||
`aws:dynamodb:table`, inputs, outputs.
|
||||
- `modules/l1/dynamodb/terraform/main.tf` —
|
||||
`resource "aws_dynamodb_table" "this"` (PK + optional SK,
|
||||
`billing_mode = PAY_PER_REQUEST` default, encryption + point-in-time-
|
||||
recovery enabled per v1.8 NFR defaults).
|
||||
- `modules/l1/dynamodb/README.md` — module doc.
|
||||
- `modules/l1/dynamodb/instance.json` — sample instance.
|
||||
- `modules/registry.json` — `dynamodb` entry (kind `l1`,
|
||||
`terraform_dir: modules/l1/dynamodb/terraform`).
|
||||
- `tests/test_adapter.py` — add `dynamodb` to `EXPECTED_L1_KEYS` +
|
||||
a resolution + emission test.
|
||||
- `modules/README.md` — catalog index updated.
|
||||
|
||||
### Summary
|
||||
|
||||
13 requirements (REQ-310..322). Equities-only pilot; minimal PoA ledger;
|
||||
T+1 settlement; consumer deploy via `deploy.yml@v1.25`; 3 Post-Pilot
|
||||
metrics grounded (outcome backfill + escalation reason + pilot runs);
|
||||
3 kyverno-json policies extending v1.25 (settlement-finality,
|
||||
pilot-readiness, + the existing meta-policies apply); env-JSON wiring
|
||||
reconciled; DynamoDB L1 primitive authored (the single platform-side
|
||||
module build-out — the adapter is stateless/registry-driven, so the
|
||||
primitive is a new `modules/l1/dynamodb/` module + registry entry, not
|
||||
an adapter change).
|
||||
@@ -0,0 +1,57 @@
|
||||
# Roadmap — nova-blockchain-exchange (v1.26 pilot)
|
||||
|
||||
> **Project:** nova-blockchain-exchange — blockchain stock exchange (pilot)
|
||||
> **Milestone:** v1.26 — Live Pilot Estate Activation
|
||||
|
||||
---
|
||||
|
||||
## v1.26 — Live Pilot Estate Activation (active)
|
||||
|
||||
Lift D-096 (live AWS re-provisioning); activate the first real consumer
|
||||
estate (a stock exchange on a homegrown PoA blockchain, equities only)
|
||||
against live AWS account `581513795199`; ground the three Post-Pilot
|
||||
targets in NORTH_STAR.md (Touchless Resolution ≥99%, Human Escalation
|
||||
<0.1%, AI Decision Accuracy ≥99.5%). The platform repo (`acdl`) provides
|
||||
the deploy workflow, policy engine, and attestation gates; this repo
|
||||
provides the app (blockchain + matching engine + settlement) + the
|
||||
`contract.yaml`.
|
||||
|
||||
Tags run on the **v1.25.x** patch line: `v1.25.0` (P0) → `v1.25.N`
|
||||
(final phase = milestone release).
|
||||
|
||||
### Phase P1 — blockchain-core (planned, tag v1.25.1)
|
||||
- REQ-310: Homegrown PoA blockchain core (block, ledger, validator).
|
||||
- REQ-311: Order-matching engine (limit order book, price-time priority).
|
||||
- REQ-312: Settlement service (T+1, idempotent, finality = block commit).
|
||||
|
||||
### Phase P2 — consumer-contract-and-deploy (planned, tag v1.25.2)
|
||||
- REQ-313: Consumer `contract.yaml` + per-env variants.
|
||||
- REQ-314: Consumer deploy workflow invocation (`deploy.yml@v1.25`).
|
||||
|
||||
### Phase P3 — pilot-metrics-and-policies (planned, tag v1.25.3)
|
||||
- REQ-315: Settlement-finality kyverno-json policy.
|
||||
- REQ-316: Pilot-estate regression capability (CAP-025).
|
||||
- REQ-317: Outcome-backfill emitter.
|
||||
- REQ-318: `reason='confidence'` escalation tag.
|
||||
- REQ-319: Env-JSON `state_backend` wiring reconciliation.
|
||||
- REQ-320: Declarative pilot-readiness kyverno-json policy.
|
||||
|
||||
### Phase P4 — pilot-run-and-docs (planned, tag v1.25.4)
|
||||
- REQ-321: Docs + adapter README + onboarding guide.
|
||||
- Live pilot end-to-end run (apply → attest → record) against
|
||||
`581513795199`.
|
||||
|
||||
### Phase P5 — final review + audit + milestone ship (Final Phase, tag v1.25.5)
|
||||
- Multi-persona code review across P1..P4.
|
||||
- Audit: reconstruction test, branch hygiene, commit discipline.
|
||||
- Milestone ship: merge `phase/05` → `milestone/v1.26-pilot-activation`
|
||||
→ `main`; tag `v1.25.5` (= the v1.26 release per prev-minor tagging
|
||||
rule); create Gitea release with full milestone summary; delete all
|
||||
milestone branches.
|
||||
- Update `REQUIREMENTS.md` (mark REQ-310..321 complete), `ROADMAP.md`
|
||||
(mark v1.26 complete), `NORTH_STAR.md` (note Strategic Objectives #1
|
||||
+ #3 — first real consumer estate; Post-Pilot denominators activated).
|
||||
|
||||
After v1.26: future milestones may add bonds/derivatives/options
|
||||
(different settlement models), multi-validator BFT consensus, and
|
||||
tamper-evident ledger (D-083 lift).
|
||||
@@ -63,6 +63,23 @@ jobs:
|
||||
- name: Install test dependencies
|
||||
run: pip install -r requirements-test.txt
|
||||
|
||||
- name: Install kyverno-json (kj) for policy-engine tests
|
||||
run: |
|
||||
# v1.25: kyverno-json is the primary policy engine. Tests that
|
||||
# require kj skip when absent, so this is best-effort (the suite
|
||||
# passes with or without kj). Install is cached via the Go
|
||||
# module cache (~/.cache/go-build + ~/go/pkg/mod).
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
go install github.com/kyverno/kyverno-json/cmd/kj@latest && \
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" || \
|
||||
echo "kj install failed; policy-engine tests will skip"
|
||||
else
|
||||
sudo apt-get update && sudo apt-get install -y golang-go && \
|
||||
go install github.com/kyverno/kyverno-json/cmd/kj@latest && \
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" || \
|
||||
echo "kj install failed; policy-engine tests will skip"
|
||||
fi
|
||||
|
||||
- name: Run pytest
|
||||
run: python3 -m pytest tests/ -v --tb=short
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ jobs:
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
NOVA_CONSUMER_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
MODE_FLAG=""
|
||||
case "${{ inputs.mode }}" in
|
||||
|
||||
@@ -63,6 +63,21 @@ jobs:
|
||||
- name: Install test dependencies
|
||||
run: pip install -r requirements-test.txt
|
||||
|
||||
- name: Install kyverno-json (kj) for policy-engine tests
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
cache: false
|
||||
|
||||
- name: Install kj binary
|
||||
run: |
|
||||
# v1.25: kyverno-json is the primary policy engine. Tests that
|
||||
# require kj skip when absent, so this is best-effort (the suite
|
||||
# passes with or without kj).
|
||||
go install github.com/kyverno/kyverno-json/cmd/kj@latest && \
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" || \
|
||||
echo "kj install failed; policy-engine tests will skip"
|
||||
|
||||
- name: Run pytest
|
||||
run: python3 -m pytest tests/ -v --tb=short
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ jobs:
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
NOVA_CONSUMER_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
MODE_FLAG=""
|
||||
case "${{ inputs.mode }}" in
|
||||
|
||||
@@ -12,6 +12,37 @@ Adapters translate the engine-agnostic Target Stack IR to engine-specific format
|
||||
| Checkov adapter | `adapters/terraform/policy/checkov_adapter.py` | Checkov JSON | `PolicyCheckResult` records | Translates Checkov results |
|
||||
| Wiz adapter | `adapters/wiz/wiz_adapter.py` | Wiz API issues JSON | `PolicyCheckResult` records | Translates Wiz security findings |
|
||||
| Kyverno adapter | `adapters/kyverno/kyverno_adapter.py` | Kyverno PolicyReport JSON | `PolicyCheckResult` records | K8s-native policy translation |
|
||||
| kyverno-json engine | `adapters/kyverno-json/kyverno_json_engine.py` | Any JSON/YAML payload | `PolicyCheckResult` records | **v1.25 primary policy engine** (swappable via `PolicyEngine` protocol) |
|
||||
|
||||
## Policy Engine Protocol (v1.25)
|
||||
|
||||
The `core/policy_engine.py` module defines the **swap boundary** between
|
||||
Nova and its policy engines. A `PolicyEngine` Python Protocol (PEP 544)
|
||||
with three members (`name`, `is_configured()`, `evaluate()`) is the
|
||||
contract; a `PolicyEngineRegistry` selects the active engine from
|
||||
`config.json`'s `policy.engine` key. The confidence signal and pipeline
|
||||
never import an engine directly — they go through the registry.
|
||||
|
||||
**Implementations:**
|
||||
- `KyvernoJsonEngine` (`adapters/kyverno-json/`) — shells to the `kj`
|
||||
CLI; the v1.25 default.
|
||||
- `NullEngine` (`core/policy_engine.py`) — fallback when the `policy`
|
||||
key is absent (emits `SKIPPED`).
|
||||
- Future: `OpaEngine` — implements the same protocol, shells to
|
||||
`opa eval`. The OPA-equivalent surface is documented in
|
||||
`.ciagent/RESEARCH.md` §4.2.
|
||||
|
||||
**How to add a new engine:**
|
||||
1. Create `adapters/<name>/<name>_engine.py` implementing the
|
||||
`PolicyEngine` protocol (`name`, `is_configured()`, `evaluate()`).
|
||||
2. `evaluate()` returns `list[dict]` where each dict conforms to
|
||||
`schemas/policy_check_result.schema.json`.
|
||||
3. Register the engine in `core/policy_engine.py`'s `_autoload_*`
|
||||
function (or call `register(name, factory)` at startup).
|
||||
4. Set `config.json.policy.engine` to the engine's `name`.
|
||||
5. Add the engine to the `engine` enum in
|
||||
`schemas/policy_check_result.schema.json` if it needs a distinct
|
||||
enum value (v1.25 reuses `"kyverno"` — see D-116).
|
||||
|
||||
## How to Write an Adapter
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# kyverno-json Engine Adapter (v1.25)
|
||||
|
||||
The `kyverno-json` engine is Nova's **primary compliance/policy tool**
|
||||
(v1.25), implemented behind the swappable `PolicyEngine` protocol so
|
||||
OPA (or any other engine) can replace it one day.
|
||||
|
||||
## What kyverno-json is
|
||||
|
||||
[kyverno-json](https://github.com/kyverno/kyverno-json) is a standalone
|
||||
Go binary from the Kyverno project — a **separate runtime** from the
|
||||
K8s Kyverno admission controller. It applies Kyverno `ValidatingPolicy`
|
||||
resources to **any** JSON or YAML payload file via the `kj scan` CLI.
|
||||
Unlike the K8s Kyverno adapter (`adapters/kyverno/`), which only
|
||||
speaks to K8s manifests, kyverno-json evaluates consumer contracts,
|
||||
resolved Stack IR, terraform plan JSON, and even the merged PCR list
|
||||
itself (meta-policies).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bash scripts/install-kyverno-json.sh
|
||||
# or directly:
|
||||
go install github.com/kyverno/kyverno-json/cmd/kj@latest
|
||||
kj version
|
||||
```
|
||||
|
||||
The platform functions without the binary — `is_configured()` returns
|
||||
`False` when `which kj` is absent → `evaluate()` returns a single
|
||||
`SKIPPED` PCR (`KJ_ENGINE_NOT_CONFIGURED`). The confidence signal
|
||||
proceeds with a neutral `policy` input (D-120 graceful degradation).
|
||||
|
||||
## Policy directory layout
|
||||
|
||||
```
|
||||
adapters/kyverno-json/policies/
|
||||
├── _smoke.json # round-trip smoke test
|
||||
├── contract/ # consumer contract JSON policies
|
||||
│ ├── require-id-pattern.json
|
||||
│ ├── require-env-in-enum.json
|
||||
│ ├── require-infrastructure-min-1.json
|
||||
│ └── forbid-unknown-fields.json
|
||||
├── stack-ir/ # resolved Stack IR policies
|
||||
│ ├── require-tagging-standard.json
|
||||
│ ├── forbid-public-ingress.json
|
||||
│ └── require-encryption-by-default.json
|
||||
├── plan-json/ # terraform show -json policies
|
||||
│ ├── forbid-plaintext-secrets.json
|
||||
│ ├── forbid-iam-wildcard.json
|
||||
│ └── require-kms-reference.json
|
||||
├── meta/ # policies over the merged PCR list
|
||||
│ ├── block-on-any-critical.json
|
||||
│ └── tagging-rules-agree.json
|
||||
└── regression/ # capability-inventory policies
|
||||
├── cap-013-adapter-dedup.json
|
||||
├── cap-023-metrics-collector.json
|
||||
└── cap-024-deck-structure.json
|
||||
```
|
||||
|
||||
## The four policy categories
|
||||
|
||||
1. **contract/** — over the consumer contract JSON (pre-resolve).
|
||||
2. **stack-ir/** — over the resolved Target Stack IR (post-resolve).
|
||||
3. **plan-json/** — over `terraform show -json` output (pipeline Step 5b).
|
||||
4. **meta/** — over the merged `list[PolicyCheckResult]` (meta-policies).
|
||||
5. **regression/** — over the capability-inventory JSON (declarative
|
||||
mirrors of `core/regression_verify.py`).
|
||||
|
||||
## Severity convention
|
||||
|
||||
kyverno-json does not natively assign severities. Each Nova policy
|
||||
declares its severity via a `metadata.annotations` field:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
annotations:
|
||||
nova.cloudinit.dev/severity: high
|
||||
```
|
||||
|
||||
Valid values: `critical`, `high`, `medium`, `low`, `info` (default
|
||||
when absent).
|
||||
|
||||
## 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`).
|
||||
|
||||
## Schema path
|
||||
|
||||
The output records validate against
|
||||
[`schemas/policy_check_result.schema.json`](../../schemas/policy_check_result.schema.json)
|
||||
(`engine: "kyverno"` is in the enum). The confidence signal consumes
|
||||
the merged PCR list engine-agnostically.
|
||||
|
||||
## Swap boundary
|
||||
|
||||
The `PolicyEngine` protocol (`core/policy_engine.py`) is the swap
|
||||
boundary. The OPA-equivalent surface is documented in
|
||||
`.ciagent/RESEARCH.md` §4.2 — a future `OpaEngine` implements the same
|
||||
protocol without touching the confidence signal, the PCR schema, or
|
||||
the pipeline.
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Nova kyverno-json adapter package (v1.25, REQ-294).
|
||||
|
||||
The directory name ``kyverno-json`` has a hyphen, so it is not a valid
|
||||
Python package name and cannot be imported via ``import
|
||||
adapters.kyverno-json``. The ``PolicyEngineRegistry`` loads the engine
|
||||
by file path (``importlib.util.spec_from_file_location``). This
|
||||
``__init__`` is a convenience for direct-script use and for ``pip
|
||||
install -e .`` style discovery if the package is ever renamed.
|
||||
"""
|
||||
|
||||
|
||||
def _load_engine():
|
||||
import importlib.util
|
||||
import os
|
||||
engine_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"kyverno_json_engine.py")
|
||||
spec = importlib.util.spec_from_file_location("kyverno_json_engine", engine_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"could not load {engine_path}")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod.KyvernoJsonEngine
|
||||
|
||||
|
||||
KyvernoJsonEngine = _load_engine()
|
||||
|
||||
__all__ = ["KyvernoJsonEngine"]
|
||||
@@ -0,0 +1,269 @@
|
||||
"""Nova KyvernoJsonEngine (REQ-293, v1.25).
|
||||
|
||||
Implements the ``PolicyEngine`` protocol (``core/policy_engine.py``)
|
||||
by shelling to the ``kj`` CLI (``kyverno-json``). Translates native
|
||||
kyverno-json scan output to Nova ``PolicyCheckResult`` dicts
|
||||
(``schemas/policy_check_result.schema.json``).
|
||||
|
||||
Engine enum reuse (D-116): records carry ``engine: "kyverno"`` (no new
|
||||
enum value). The ``ruleId`` is prefixed ``KJ_<policy_name>`` to
|
||||
distinguish from the K8s Kyverno adapter's ``KYVERNO_`` prefix.
|
||||
|
||||
Severity (RESEARCH §2.6, G-Q10a): kyverno-json does not natively assign
|
||||
severities. Each Nova policy declares its severity via a
|
||||
``metadata.annotations["nova.cloudinit.dev/severity"]`` field. The
|
||||
engine reads this annotation from the loaded policy YAML (not from the
|
||||
scan result — the result doesn't carry it) and applies it to every
|
||||
result that policy produces. Default when absent: ``"info"``.
|
||||
|
||||
Graceful degradation (D-120): ``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.
|
||||
|
||||
Defensive parsing: any kyverno-json output that doesn't match the
|
||||
expected shape produces an ``error`` PCR, never an exception. The
|
||||
engine is read-only against a local policy dir + a temp payload file.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
Payload = Union[dict, list, str]
|
||||
|
||||
SEVERITY_DEFAULT = "info"
|
||||
SEVERITY_ANNOTATION = "nova.cloudinit.dev/severity"
|
||||
|
||||
RESULT_MAP = {
|
||||
"pass": "pass",
|
||||
"fail": "fail",
|
||||
"error": "error",
|
||||
"skip": "skipped",
|
||||
"skipped": "skipped",
|
||||
"warn": "skipped",
|
||||
"warning": "skipped",
|
||||
}
|
||||
|
||||
|
||||
def _iso8601_now() -> str:
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _which_kj() -> str | None:
|
||||
"""Return the path to ``kj`` if on PATH, else ``None``."""
|
||||
return shutil.which("kj")
|
||||
|
||||
|
||||
def _load_policy_severities(policy_dir: Path) -> dict[str, str]:
|
||||
"""Load each ``.json``/``.yaml``/``.yml`` policy in ``policy_dir``
|
||||
(non-recursive) and return ``{policy_name: severity}``.
|
||||
|
||||
kyverno-json policies are Kubernetes-style ``ValidatingPolicy``
|
||||
resources. The severity is read from
|
||||
``metadata.annotations["nova.cloudinit.dev/severity"]``. Policies
|
||||
in subdirectories (e.g. ``contract/``, ``stack-ir/``) are loaded
|
||||
when the caller passes that subdirectory as ``policy_dir``.
|
||||
"""
|
||||
severities: dict[str, str] = {}
|
||||
if not policy_dir.is_dir():
|
||||
return severities
|
||||
for entry in sorted(os.listdir(policy_dir)):
|
||||
if entry.startswith("_") or entry.startswith("."):
|
||||
continue
|
||||
full = policy_dir / entry
|
||||
if not full.is_file():
|
||||
continue
|
||||
if entry.endswith((".json", ".yaml", ".yml")):
|
||||
try:
|
||||
with open(full, "r", encoding="utf-8") as fh:
|
||||
doc = yaml.safe_load(fh)
|
||||
if not isinstance(doc, dict):
|
||||
continue
|
||||
name = doc.get("metadata", {}).get("name") or entry.rsplit(".", 1)[0]
|
||||
ann = doc.get("metadata", {}).get("annotations", {}) or {}
|
||||
sev = ann.get(SEVERITY_ANNOTATION, SEVERITY_DEFAULT)
|
||||
severities[name] = str(sev).lower()
|
||||
except Exception:
|
||||
continue
|
||||
return severities
|
||||
|
||||
|
||||
def _to_pcr(entry: dict, contract_id: str, severity: str) -> dict:
|
||||
"""Translate a kyverno-json scan result entry to a PCR dict."""
|
||||
policy_name = entry.get("policy", "") or "UNKNOWN"
|
||||
rule_name = entry.get("rule", "") or ""
|
||||
rule_id = f"KJ_{policy_name}"
|
||||
if rule_name:
|
||||
rule_id = f"{rule_id}/{rule_name}"
|
||||
result_raw = entry.get("result", "skip")
|
||||
result = RESULT_MAP.get(str(result_raw).lower(), "error")
|
||||
message = entry.get("message", "") or ""
|
||||
resource = entry.get("resource", "")
|
||||
if not resource and entry.get("name"):
|
||||
kind = entry.get("kind", "")
|
||||
ns = entry.get("namespace", "")
|
||||
resource = f"{kind}/{ns}/{entry.get('name')}" if kind else entry.get("name", "")
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": rule_id,
|
||||
"severity": severity,
|
||||
"result": result,
|
||||
"message": message,
|
||||
"evidence": {
|
||||
"resource": resource,
|
||||
"policy": policy_name,
|
||||
"rule": rule_name,
|
||||
"namespace": entry.get("namespace", ""),
|
||||
"kind": entry.get("kind", ""),
|
||||
"name": entry.get("name", ""),
|
||||
},
|
||||
"resourceRef": resource,
|
||||
}
|
||||
|
||||
|
||||
def _skipped_not_configured(contract_id: str) -> dict:
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": "KJ_ENGINE_NOT_CONFIGURED",
|
||||
"severity": "info",
|
||||
"result": "skipped",
|
||||
"message": (
|
||||
"kyverno-json engine not configured — `which kj` returned no path. "
|
||||
"Install via scripts/install-kyverno-json.sh. The platform proceeds "
|
||||
"with a neutral SKIPPED policy input (is_configured() guard, D-120)."
|
||||
),
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
def _error_pcr(contract_id: str, message: str) -> dict:
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": "KJ_ENGINE_ERROR",
|
||||
"severity": "info",
|
||||
"result": "error",
|
||||
"message": message,
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
class KyvernoJsonEngine:
|
||||
"""``PolicyEngine`` impl that shells to the ``kj`` CLI."""
|
||||
|
||||
name = "kyverno-json"
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return _which_kj() is not None
|
||||
|
||||
def evaluate(self, payload: Payload, policy_dir: Path,
|
||||
contract_id: str) -> list[dict]:
|
||||
if not self.is_configured():
|
||||
return [_skipped_not_configured(contract_id)]
|
||||
kj = _which_kj()
|
||||
policy_dir = Path(policy_dir)
|
||||
if not policy_dir.is_dir():
|
||||
return [_error_pcr(
|
||||
contract_id,
|
||||
f"kyverno-json policy dir not found: {policy_dir}",
|
||||
)]
|
||||
severities = _load_policy_severities(policy_dir)
|
||||
# Write payload to temp file (kj scan --payload expects a file path).
|
||||
payload_tmp = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
||||
)
|
||||
try:
|
||||
json.dump(payload, payload_tmp)
|
||||
payload_tmp.flush()
|
||||
payload_tmp.close()
|
||||
cmd = [
|
||||
kj, "scan",
|
||||
"--policy", str(policy_dir),
|
||||
"--payload", payload_tmp.name,
|
||||
"--output", "json",
|
||||
]
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return [_error_pcr(contract_id, "kyverno-json scan timed out (60s)")]
|
||||
if proc.returncode not in (0, 1):
|
||||
return [_error_pcr(
|
||||
contract_id,
|
||||
f"kyverno-json scan exited {proc.returncode}: {proc.stderr[:200]}",
|
||||
)]
|
||||
try:
|
||||
out = json.loads(proc.stdout) if proc.stdout.strip() else {}
|
||||
except json.JSONDecodeError as e:
|
||||
return [_error_pcr(
|
||||
contract_id,
|
||||
f"kyverno-json output not JSON: {e}",
|
||||
)]
|
||||
return self._translate(out, contract_id, severities)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(payload_tmp.name)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _translate(self, out: dict, contract_id: str,
|
||||
severities: dict[str, str]) -> list[dict]:
|
||||
results = out.get("results", []) if isinstance(out, dict) else []
|
||||
if not isinstance(results, list):
|
||||
results = []
|
||||
pcrs: list[dict] = []
|
||||
for entry in results:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
policy_name = entry.get("policy", "") or "UNKNOWN"
|
||||
severity = severities.get(policy_name, SEVERITY_DEFAULT)
|
||||
pcrs.append(_to_pcr(entry, contract_id, severity))
|
||||
if not pcrs:
|
||||
# No results — kyverno-json produced nothing (no match, or
|
||||
# all policies passed with no result entries). Emit a
|
||||
# single pass PCR so the confidence signal's policy input
|
||||
# is non-empty (a non-empty list of passes → score 1.0).
|
||||
pcrs.append({
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": "KJ_NO_RESULTS",
|
||||
"severity": "info",
|
||||
"result": "pass",
|
||||
"message": "kyverno-json scan produced no result entries (all policies passed or no match).",
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
})
|
||||
return pcrs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 4:
|
||||
print(
|
||||
"usage: kyverno_json_engine.py <payload.json> <policy_dir> <contract-id>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
pl = json.load(fh)
|
||||
engine = KyvernoJsonEngine()
|
||||
out = engine.evaluate(pl, Path(sys.argv[2]), sys.argv[3])
|
||||
print(json.dumps(out, indent=2))
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-contract-id",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "Require contract id"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "require-id",
|
||||
"validate": {
|
||||
"message": "contract id is required",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"id": "(regex_match('^[a-z][a-z0-9-]{2,5}$', @))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-unknown-fields",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "low",
|
||||
"title.policy.kyverno.io": "Contract has only schema-allowed fields"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-unknown-fields",
|
||||
"validate": {
|
||||
"message": "contract may only contain id, name, environment, infrastructure (schema-allowed fields)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"(length(keys(@)) == `4`)": true,
|
||||
"keys(@)": "(contains(['id','name','environment','infrastructure'], @))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-env-in-enum",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "Contract environment is one of dev/qa/prod/dr"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "env-enum",
|
||||
"validate": {
|
||||
"message": "contract.environment must be one of dev, qa, prod, dr",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"environment": "(contains(['dev','qa','prod','dr'], @))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-id-pattern",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "Contract id matches operational acronym pattern"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "id-pattern",
|
||||
"validate": {
|
||||
"message": "contract.id must match ^[a-z][a-z0-9-]{2,5}$ (3-6 char operational acronym)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"id": "(regex_match('^[a-z][a-z0-9-]{2,5}$', @))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-infrastructure-min-1",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "Contract declares at least one infrastructure entry"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "infra-min-1",
|
||||
"validate": {
|
||||
"message": "contract.infrastructure must have at least one module entry",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"infrastructure": "(length(keys(@)) > `0`)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "block-on-any-critical",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "critical",
|
||||
"title.policy.kyverno.io": "Block on any critical-fail policy result (declarative source of truth)"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-critical-fail",
|
||||
"validate": {
|
||||
"message": "No PolicyCheckResult in the merged list may have severity: critical + result: fail. The confidence_signal.py hard-override is the defense-in-depth behind this declarative rule (D-119).",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.[]": {
|
||||
"(severity == 'critical' && result == 'fail')": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "tagging-rules-agree",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "Checkov NOVA_TAG_NAMING and kj KJ_REQUIRE_TAGGING_STANDARD agree per resource"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-tagging-divergence",
|
||||
"validate": {
|
||||
"message": "For every resource, the Checkov NOVA_TAG_NAMING result and the kyverno-json KJ_REQUIRE_TAGGING_STANDARD result must agree. Divergence emits an error PCR (D-118, defense-in-depth against rule drift).",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.[?(ruleId == 'NOVA_TAG_NAMING')]": {
|
||||
"result->ckv_result": {},
|
||||
"($ckv_result == 'fail')": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check": {
|
||||
"~.[?(ruleId == 'KJ_REQUIRE_TAGGING_STANDARD')]": {
|
||||
"result->kj_result": {},
|
||||
"($kj_result == 'fail')": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-iam-wildcard",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "No IAM wildcard Actions or Resources"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-wildcard-action",
|
||||
"validate": {
|
||||
"message": "IAM policy Action must not be '*' (ports CKV_AWS_1/40)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_iam_policy' && contains(values.policy_document.Statement[].Action, '*'))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no-wildcard-resource",
|
||||
"validate": {
|
||||
"message": "IAM policy Resource must not be '*' (ports CKV_AWS_1/40)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_iam_policy' && contains(values.policy_document.Statement[].Resource, '*'))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-plaintext-secrets",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "No plaintext secrets in the terraform plan"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-plaintext-db-password",
|
||||
"validate": {
|
||||
"message": "aws_db_instance.password must not be a plaintext string (ports CKV_AWS_41/45/46)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_db_instance' && contains(keys(values), 'password') && !contains(['${...}', ''], values.password))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-kms-reference",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "KMS keys referenced by alias, not inline key material"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "kms-by-alias",
|
||||
"validate": {
|
||||
"message": "aws_kms_key resources should reference a customer-managed key alias, not inline key material (ports CKV_AWS_7/33)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_kms_key' && !contains(keys(values), 'key_id') && !contains(keys(values), 'kms_key_id'))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "cap-013-adapter-dedup",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "No duplicate adapter registrations (CAP-013 declarative mirror)"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-duplicate-adapters",
|
||||
"validate": {
|
||||
"message": "Each adapter must be registered exactly once (no duplicate adapter names in the capability inventory). Declarative mirror of core/regression_verify.py CAP-013.",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"adapters": "(length(duplicates(@)) == `0`)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "cap-023-metrics-collector",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "Every metric has a grounded/derived/deferred status (CAP-023 declarative mirror)"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "every-metric-has-status",
|
||||
"validate": {
|
||||
"message": "Every metric in docs/METRICS.md must declare a status (grounded, derived, or deferred). Declarative mirror of core/regression_verify.py CAP-023.",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.metrics": {
|
||||
"(contains(['grounded','derived','deferred'], status))": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "cap-024-deck-structure",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "low",
|
||||
"title.policy.kyverno.io": "Deck structure matches the documented 4-beat arc (CAP-024 declarative mirror)"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "deck-has-4-beats",
|
||||
"validate": {
|
||||
"message": "The deck must have the 4-beat arc: Problem, Solution, Proof, Roadmap+Ask. Declarative mirror of core/regression_verify.py CAP-024.",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"deck.beats": "(length(@) >= `4`)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"check": {
|
||||
"deck.beats": "(contains(@, 'Problem') && contains(@, 'Solution') && contains(@, 'Proof') && contains(@, 'Roadmap+Ask'))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-public-ingress",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "No resource has public ingress enabled"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-public-ingress",
|
||||
"identifier": "id",
|
||||
"validate": {
|
||||
"message": "public_ingress: true is not allowed on any resource (v1.0 demo rule, now declarative)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.resources": {
|
||||
"(inputs.public_ingress || `false`)": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-encryption-by-default",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "S3 buckets and EBS volumes carry encryption config"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "s3-encryption",
|
||||
"identifier": "id",
|
||||
"match": {
|
||||
"any": [
|
||||
{"type": "aws:s3:bucket"}
|
||||
]
|
||||
},
|
||||
"validate": {
|
||||
"message": "S3 buckets must declare encryption config (inputs.bucket_encryption or inputs.kms_key_id)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"(contains(keys(inputs), 'bucket_encryption') || contains(keys(inputs), 'kms_key_id'))": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ebs-encryption",
|
||||
"identifier": "id",
|
||||
"match": {
|
||||
"any": [
|
||||
{"type": "aws:ebs:volume"}
|
||||
]
|
||||
},
|
||||
"validate": {
|
||||
"message": "EBS volumes must declare encryption (inputs.encrypted or inputs.kms_key_id)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"(contains(keys(inputs), 'encrypted') || contains(keys(inputs), 'kms_key_id'))": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-tagging-standard",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "All resources carry required Nova tags"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "require-nova-tags",
|
||||
"identifier": "id",
|
||||
"validate": {
|
||||
"message": "Every taggable resource must carry nova:owner, nova:contract, nova:environment, nova:cost-center tags",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.resources": {
|
||||
"(contains(keys(tags || `[]`), 'nova:owner'))": true,
|
||||
"(contains(keys(tags || `[]`), 'nova:contract'))": true,
|
||||
"(contains(keys(tags || `[]`), 'nova:environment'))": true,
|
||||
"(contains(keys(tags || `[]`), 'nova:cost-center'))": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,9 @@ def adapt(stack_instance, out_dir):
|
||||
environment = stack.get("environment", "dev")
|
||||
account_id = env.get_env("AWS_ACCOUNT_ID", "581513795199")
|
||||
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
|
||||
# State key is env-scoped (v1.24 REQ-287): the {environment} segment lets
|
||||
# the env-transition detect-and-destroy step target the PRIOR env's state
|
||||
# without affecting the new env. No orphan path on environment promotion.
|
||||
terraform_tf = (
|
||||
'terraform {\n'
|
||||
' required_version = ">= 1.9, < 1.10"\n'
|
||||
|
||||
@@ -488,6 +488,25 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
# Validate contract against schema
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
|
||||
# v1.25 (REQ-296): pre-resolve policy evaluation — run the active
|
||||
# PolicyEngine over the contract dict with the contract/ policy
|
||||
# dir BEFORE resolving. Failures feed the `policyResults` on the
|
||||
# stack instance (the confidence signal's `policy` input). The
|
||||
# resolver does NOT exit on policy failure — the confidence signal
|
||||
# decides the gate (consistent with the existing --soft-fail
|
||||
# Checkov pattern).
|
||||
contract_pcrs: list = []
|
||||
try:
|
||||
from core.policy_engine import get_engine, get_policy_root
|
||||
_engine = get_engine()
|
||||
_policy_root = get_policy_root()
|
||||
contract_pcrs = _engine.evaluate(
|
||||
contract, _policy_root / "contract", contract.get("id", "unknown")
|
||||
)
|
||||
except Exception:
|
||||
# Policy evaluation must never break the resolver.
|
||||
contract_pcrs = []
|
||||
|
||||
# Interpolation (D-081): expand ${env.<field>} + ${contract.<field>}
|
||||
# tokens AFTER schema validation (the schema sees raw tokens, which are
|
||||
# valid strings) and BEFORE IR resolution (the resolver sees concrete
|
||||
@@ -590,6 +609,12 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
"data_sources": all_data_sources,
|
||||
}
|
||||
|
||||
# v1.25 (REQ-296): attach the pre-resolve contract-policy PCRs to
|
||||
# the stack instance. The post-resolve stack-IR PCRs are appended
|
||||
# after stack-schema validation (below).
|
||||
if contract_pcrs:
|
||||
stack_instance["policyResults"] = list(contract_pcrs)
|
||||
|
||||
# Add the human-readable title
|
||||
if contract.get("name"):
|
||||
stack_instance["stack"]["title"] = contract["name"]
|
||||
@@ -606,6 +631,28 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
jsonschema.validate(stack_instance, stack_schema)
|
||||
|
||||
# v1.25 (REQ-298): post-resolve policy evaluation — run the active
|
||||
# PolicyEngine over the resolved Stack IR with the stack-ir/ policy
|
||||
# dir. The resulting PCRs are appended to the contract-policy PCRs
|
||||
# on the stack instance (additive — the resolver's return value
|
||||
# shape and exceptions are unchanged). The confidence signal
|
||||
# consumes the merged list as its `policy` input.
|
||||
try:
|
||||
from core.policy_engine import get_engine, get_policy_root
|
||||
engine = get_engine()
|
||||
policy_root = get_policy_root()
|
||||
stack_ir_pcrs = engine.evaluate(
|
||||
stack_instance, policy_root / "stack-ir", contract.get("id", "unknown")
|
||||
)
|
||||
stack_instance.setdefault("policyResults", []).extend(stack_ir_pcrs)
|
||||
except Exception:
|
||||
# Policy evaluation must never break the resolver — the
|
||||
# confidence signal decides the gate. A failure here means the
|
||||
# engine is misconfigured; the contract PCRs (if any) are still
|
||||
# present, and the confidence signal proceeds with whatever
|
||||
# `policy` input it receives (possibly empty → 0.5 neutral).
|
||||
pass
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Nova Environment Transition — detect prior env + record applied env.
|
||||
|
||||
When a consumer edits the `environment:` field on a stable contract `id`
|
||||
(Shape A promotion), the platform must destroy the prior environment's
|
||||
resources before building the new environment. This module provides the
|
||||
DynamoDB query logic to detect the prior environment and record the
|
||||
applied environment after a successful apply.
|
||||
|
||||
Source of truth: the `nova-contracts` DynamoDB table (PK `consumerRepo`,
|
||||
SK `contractId#submittedAt`), written by `core/lambda/contract_ingestor.py`.
|
||||
|
||||
detect_prior_env() queries the table for the last-applied environment for
|
||||
a given consumerRepo + contractId. If it differs from the new env, the
|
||||
prior env name is returned (so the pipeline can destroy it). If no record
|
||||
exists (first deploy or Shape B per-env caller), returns None.
|
||||
|
||||
record_applied_env() writes a `#LAST_APPLIED` record after a successful
|
||||
apply, so the next run's detect step has a source of truth.
|
||||
|
||||
Failures to reach DynamoDB (local/CI mode without the table) log a warning
|
||||
and return None (conservative — no false-positive destroys). This is the
|
||||
no-orphan-path guarantee: if we can't confirm a prior env, we don't
|
||||
destroy, but we also don't silently proceed in a way that orphans — the
|
||||
record step ensures future runs have the data.
|
||||
|
||||
CLI:
|
||||
python3 core/env_transition.py detect --contract-id <id> --consumer-repo <repo> --new-env <env>
|
||||
python3 core/env_transition.py record --contract-id <id> --consumer-repo <repo> --env <env>
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import boto3
|
||||
except ImportError:
|
||||
boto3 = None
|
||||
|
||||
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "nova-contracts")
|
||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||
LAST_APPLIED_SUFFIX = "#LAST_APPLIED"
|
||||
|
||||
|
||||
def _get_table():
|
||||
"""Return the DynamoDB table resource, or raise if boto3 unavailable."""
|
||||
if boto3 is None:
|
||||
raise RuntimeError("boto3 is required for env_transition")
|
||||
session = boto3.Session(region_name=REGION)
|
||||
dyn = session.resource("dynamodb")
|
||||
return dyn.Table(TABLE_NAME)
|
||||
|
||||
|
||||
def detect_prior_env(contract_id: str, consumer_repo: str, new_env: str) -> Optional[str]:
|
||||
"""Query the nova-contracts table for the last-applied env.
|
||||
|
||||
Returns the prior env name if it differs from new_env, else None.
|
||||
Failures to reach DynamoDB log a warning and return None (conservative).
|
||||
"""
|
||||
try:
|
||||
table = _get_table()
|
||||
sk_prefix = f"{contract_id}{LAST_APPLIED_SUFFIX}#"
|
||||
resp = table.query(
|
||||
KeyConditionExpression="consumerRepo = :repo AND begins_with(#sk, :prefix)",
|
||||
FilterExpression="#status = :status",
|
||||
ExpressionAttributeNames={
|
||||
"#sk": "contractId#submittedAt",
|
||||
"#status": "status",
|
||||
},
|
||||
ExpressionAttributeValues={
|
||||
":repo": consumer_repo,
|
||||
":prefix": sk_prefix,
|
||||
":status": "applied",
|
||||
},
|
||||
ScanIndexForward=False,
|
||||
Limit=1,
|
||||
)
|
||||
items = resp.get("Items", [])
|
||||
if not items:
|
||||
return None
|
||||
prior_env = items[0].get("environment")
|
||||
if prior_env and prior_env != new_env:
|
||||
return prior_env
|
||||
return None
|
||||
except Exception as exc:
|
||||
sys.stderr.write(
|
||||
f"WARNING: env_transition.detect_prior_env: could not query "
|
||||
f"DynamoDB table {TABLE_NAME} — {type(exc).__name__}: {exc}. "
|
||||
f"Assuming no prior env (conservative). This is expected in "
|
||||
f"local/CI mode without the nova-contracts table.\n"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def record_applied_env(contract_id: str, consumer_repo: str, env: str) -> bool:
|
||||
"""Write a LAST_APPLIED record to the nova-contracts table.
|
||||
|
||||
Called after a successful apply. Idempotent (writes a new timestamped
|
||||
record each time; the detect step reads the latest by ScanIndexForward).
|
||||
Returns True on success, False on failure (non-fatal — the pipeline
|
||||
should not halt if the record write fails).
|
||||
"""
|
||||
try:
|
||||
table = _get_table()
|
||||
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
sk = f"{contract_id}{LAST_APPLIED_SUFFIX}#{ts}"
|
||||
table.put_item(
|
||||
Item={
|
||||
"consumerRepo": consumer_repo,
|
||||
"contractId#submittedAt": sk,
|
||||
"contractId": contract_id,
|
||||
"environment": env,
|
||||
"status": "applied",
|
||||
"appliedAt": ts,
|
||||
}
|
||||
)
|
||||
return True
|
||||
except Exception as exc:
|
||||
sys.stderr.write(
|
||||
f"WARNING: env_transition.record_applied_env: could not write to "
|
||||
f"DynamoDB table {TABLE_NAME} — {type(exc).__name__}: {exc}. "
|
||||
f"The apply succeeded but the last-applied env record was not "
|
||||
f"persisted. Future env-transition detection may not work.\n"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def main(argv):
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Nova env-transition detect/record")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_detect = sub.add_parser("detect", help="Detect prior env for a contract")
|
||||
p_detect.add_argument("--contract-id", required=True)
|
||||
p_detect.add_argument("--consumer-repo", required=True)
|
||||
p_detect.add_argument("--new-env", required=True)
|
||||
|
||||
p_record = sub.add_parser("record", help="Record the applied env for a contract")
|
||||
p_record.add_argument("--contract-id", required=True)
|
||||
p_record.add_argument("--consumer-repo", required=True)
|
||||
p_record.add_argument("--env", required=True)
|
||||
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
if args.command == "detect":
|
||||
prior = detect_prior_env(args.contract_id, args.consumer_repo, args.new_env)
|
||||
print(json.dumps({"prior_env": prior}))
|
||||
return 0 if prior is None else 0
|
||||
elif args.command == "record":
|
||||
ok = record_applied_env(args.contract_id, args.consumer_repo, args.env)
|
||||
print(json.dumps({"recorded": ok}))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Nova Policy Engine Registry (REQ-291, v1.25).
|
||||
|
||||
The swappable policy-engine abstraction. A Python Protocol (PEP 544)
|
||||
defines the engine contract; a registry selects the active engine from
|
||||
``config.json``'s ``policy.engine`` key. This is the **swap boundary**
|
||||
(ARCHITECTURE.md §12.7) — the confidence signal and pipeline never
|
||||
import an engine directly; they go through the registry. A future
|
||||
``OpaEngine`` implements the same protocol without touching the
|
||||
confidence signal, the PCR schema, or the pipeline.
|
||||
|
||||
The protocol is minimal (3 members) by design:
|
||||
|
||||
- ``name`` — the engine's registry key (matches ``config.json.policy.engine``).
|
||||
- ``is_configured()`` — returns False when the engine's binary is absent
|
||||
(the registry's caller must skip gracefully, emitting SKIPPED PCRs).
|
||||
- ``evaluate(payload, policy_dir, contract_id)`` — runs the engine's
|
||||
policies over ``payload`` and returns a ``list[dict]`` where each dict
|
||||
conforms to ``schemas/policy_check_result.schema.json``.
|
||||
|
||||
A ``NullEngine`` is the fallback when the ``policy`` key is absent from
|
||||
``config.json`` (backward compatibility for tests that don't set the
|
||||
key — it emits a single SKIPPED PCR so the confidence signal proceeds
|
||||
with a neutral ``policy`` input).
|
||||
|
||||
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_``).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Protocol, Union, runtime_checkable
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
def _iso8601_now() -> str:
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
Payload = Union[dict, list, str]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class PolicyEngine(Protocol):
|
||||
"""The swap boundary for policy engines.
|
||||
|
||||
Implementations: ``KyvernoJsonEngine`` (adapters/kyverno-json/),
|
||||
``NullEngine`` (this module), future ``OpaEngine``.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
|
||||
def is_configured(self) -> bool: ...
|
||||
|
||||
def evaluate(self, payload: Payload, policy_dir: Path,
|
||||
contract_id: str) -> list[dict]: ...
|
||||
|
||||
|
||||
def _skipped_pcr(rule_id: str, message: str, contract_id: str) -> dict:
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": rule_id,
|
||||
"severity": "info",
|
||||
"result": "skipped",
|
||||
"message": message,
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
class NullEngine:
|
||||
"""Fallback when ``config.json.policy`` is absent.
|
||||
|
||||
Emits a single SKIPPED PCR with ``ruleId: NULL_ENGINE_INACTIVE`` so
|
||||
the confidence signal's ``policy`` input is non-null (the per-input
|
||||
score for a single SKIPPED PCR is 1.0 — skipped counts as pass per
|
||||
``core/confidence_signal.py:84-89``). This keeps existing tests
|
||||
passing when the ``policy`` key is not set.
|
||||
"""
|
||||
|
||||
name = "null"
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return False
|
||||
|
||||
def evaluate(self, payload: Payload, policy_dir: Path,
|
||||
contract_id: str) -> list[dict]:
|
||||
return [_skipped_pcr(
|
||||
"NULL_ENGINE_INACTIVE",
|
||||
"NullEngine active — the `policy` key is absent from config.json. "
|
||||
"No policy engine is configured; the confidence signal proceeds with "
|
||||
"a neutral SKIPPED policy input.",
|
||||
contract_id,
|
||||
)]
|
||||
|
||||
|
||||
_REGISTRY: dict[str, Callable[[], PolicyEngine]] = {}
|
||||
|
||||
|
||||
def register(name: str, factory: Callable[[], PolicyEngine]) -> None:
|
||||
"""Register an engine factory under ``name``.
|
||||
|
||||
The factory is called lazily by ``get_engine()`` so an engine's
|
||||
binary dependency (e.g. ``kj``) is not required at import time.
|
||||
"""
|
||||
_REGISTRY[name] = factory
|
||||
|
||||
|
||||
def _load_config_policy() -> dict | None:
|
||||
"""Read the ``policy`` object from ``.ciagent/config.json``.
|
||||
|
||||
Returns ``None`` when the file is absent or the ``policy`` key is
|
||||
missing (the caller falls back to ``NullEngine``).
|
||||
"""
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
cfg = os.path.join(repo_root, ".ciagent", "config.json")
|
||||
if not os.path.isfile(cfg):
|
||||
return None
|
||||
try:
|
||||
with open(cfg, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
return data.get("policy")
|
||||
|
||||
|
||||
def get_engine() -> PolicyEngine:
|
||||
"""Return the active ``PolicyEngine`` from ``config.json``.
|
||||
|
||||
Reads ``config.json.policy.engine`` (default ``"kyverno-json"``).
|
||||
Falls back to ``NullEngine`` when the ``policy`` key is absent
|
||||
(backward compatibility). Raises ``KeyError`` for an unknown engine
|
||||
name (a typo in config — fail loud, not silent).
|
||||
"""
|
||||
policy_cfg = _load_config_policy()
|
||||
if policy_cfg is None:
|
||||
return NullEngine()
|
||||
engine_name = policy_cfg.get("engine", "kyverno-json")
|
||||
factory = _REGISTRY.get(engine_name)
|
||||
if factory is None:
|
||||
raise KeyError(
|
||||
f"Unknown policy engine '{engine_name}' in config.json. "
|
||||
f"Registered engines: {sorted(_REGISTRY.keys()) or ['(none)']}. "
|
||||
f"Set policy.engine to a registered name or install the engine adapter."
|
||||
)
|
||||
return factory()
|
||||
|
||||
|
||||
def get_policy_root() -> Path:
|
||||
"""Return the configured policy root directory (or a default)."""
|
||||
policy_cfg = _load_config_policy()
|
||||
if policy_cfg is None:
|
||||
return Path("adapters/kyverno-json/policies")
|
||||
root = policy_cfg.get("policy_root", "adapters/kyverno-json/policies")
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if os.path.isabs(root):
|
||||
return Path(root)
|
||||
return Path(repo_root) / root
|
||||
|
||||
|
||||
def _register_builtin(name: str, factory: Callable[[], PolicyEngine]) -> None:
|
||||
register(name, factory)
|
||||
|
||||
|
||||
def _autoload_kyverno_json() -> None:
|
||||
"""Register the kyverno-json engine if its adapter is importable.
|
||||
|
||||
The adapter directory uses a hyphen (``adapters/kyverno-json/``),
|
||||
so a plain ``import`` is not possible. Load the module by file path
|
||||
via ``importlib.util``. Lazy import so ``core/policy_engine.py``
|
||||
does not require ``adapters/kyverno-json/`` at import time (the
|
||||
adapter imports ``yaml``, which may be unavailable in minimal test
|
||||
envs).
|
||||
"""
|
||||
try:
|
||||
import importlib.util
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
adapter_path = os.path.join(
|
||||
repo_root, "adapters", "kyverno-json", "kyverno_json_engine.py"
|
||||
)
|
||||
if not os.path.isfile(adapter_path):
|
||||
return
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"kyverno_json_engine", adapter_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
return
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
engine_cls = getattr(mod, "KyvernoJsonEngine")
|
||||
_register_builtin("kyverno-json", engine_cls)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
_autoload_kyverno_json()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
eng = get_engine()
|
||||
print(json.dumps({
|
||||
"engine": eng.name,
|
||||
"is_configured": eng.is_configured(),
|
||||
"policy_root": str(get_policy_root()),
|
||||
}, indent=2))
|
||||
+15
-1
@@ -174,4 +174,18 @@ numbers. Every metric either has a real source or is explicitly deferred.
|
||||
| SLA / Unplanned Downtime | D-096 | `placeholder_sla_downtime.csv` |
|
||||
| Predictive vs Reactive Ratio | future emitter | `placeholder_predictive_reactive.csv` |
|
||||
|
||||
See `docs/METRICS_DEFERRED_ROADMAP.md` for the activation path for each.
|
||||
See `docs/METRICS_DEFERRED_ROADMAP.md` for the activation path for each.
|
||||
|
||||
---
|
||||
|
||||
## v1.25 — Swappable Policy Engine
|
||||
|
||||
The policy engine that produces the `PolicyCheckResult` records feeding
|
||||
the confidence signal is **swappable** (NORTH_STAR Strategic Objective #2
|
||||
— provable trust via a replaceable substrate, not a vendor lock-in).
|
||||
The `PolicyEngine` protocol (`core/policy_engine.py`) is the swap
|
||||
boundary; `config.json.policy.engine` selects the active engine
|
||||
(default `"kyverno-json"`). A future `OpaEngine` implements the same
|
||||
protocol without touching the confidence signal, the PCR schema, or
|
||||
the pipeline. See `.ciagent/ARCHITECTURE.md` §12.7 for the registry
|
||||
diagram.
|
||||
+61
-1
@@ -611,4 +611,64 @@ must be checked before the module is registered and published.
|
||||
`stack.schema.json`).
|
||||
- [ ] For an L2, a test is added that the composition resolves to the
|
||||
expected set of L1 instances and that the adapter emits a root module
|
||||
calling the L1 modules.
|
||||
calling the L1 modules.
|
||||
|
||||
---
|
||||
|
||||
## 10. Policy Authoring Standard (v1.25)
|
||||
|
||||
Module owners may ship per-module kyverno-json policies in
|
||||
`modules/<name>/policies/` (future convention; v1.25 policies live
|
||||
under `adapters/kyverno-json/policies/`). A policy file is a
|
||||
`ValidatingPolicy` resource (YAML or JSON).
|
||||
|
||||
### 10.1 Required fields
|
||||
|
||||
- `apiVersion: json.kyverno.io/v1alpha1`
|
||||
- `kind: ValidatingPolicy`
|
||||
- `metadata.name` — matches the filename (e.g. `require-tags.json` →
|
||||
`name: require-tags`). This becomes the `ruleId` prefix `KJ_<name>`.
|
||||
- `metadata.annotations["nova.cloudinit.dev/severity"]` — one of
|
||||
`critical`, `high`, `medium`, `low`, `info`. Drives the confidence
|
||||
signal's penalty mapping.
|
||||
- `spec.rules[].validate.assert` — an `all` or `any` list of assertion
|
||||
trees with JMESPath expressions. **No `forEach`, pattern operators,
|
||||
anchors, or wildcards** — use the `~` projection modifier to iterate.
|
||||
|
||||
### 10.2 Severity guidance
|
||||
|
||||
| Severity | When to use | Confidence penalty |
|
||||
| --- | --- | --- |
|
||||
| `critical` | a violation makes the deploy unsafe (e.g. public ingress on a prod DB) | hard override (score = 0, block) |
|
||||
| `high` | a violation is a security or compliance gap (e.g. plaintext secrets) | -0.20 |
|
||||
| `medium` | a violation is a best-practice miss (e.g. missing tags) | -0.05 |
|
||||
| `low` | a violation is a style or convention issue | -0.01 |
|
||||
| `info` | a non-blocking observation (default) | 0.0 |
|
||||
|
||||
### 10.3 Assertion-tree patterns
|
||||
|
||||
- **Iterate an array:** use the `~` modifier on the array key:
|
||||
```yaml
|
||||
check:
|
||||
~.resources:
|
||||
(@ < `5`): true
|
||||
```
|
||||
- **Match a resource type:** use the `match.any` block:
|
||||
```yaml
|
||||
match:
|
||||
any:
|
||||
- type: aws:s3:bucket
|
||||
```
|
||||
- **Binding for descendant access:** use `->name`:
|
||||
```yaml
|
||||
(bar + bat)->sum:
|
||||
($sum): 10
|
||||
```
|
||||
|
||||
### 10.4 Testing
|
||||
|
||||
- Ship a fixture pair (`passing.json` + `failing.json`) under
|
||||
`tests/fixtures/<policy_target>/`.
|
||||
- Add a test file `tests/test_<policy_target>_policies.py` using the
|
||||
`KyvernoJsonEngine` (skip-without-kj pattern).
|
||||
- The regression gate (`pytest tests/`) must remain green.
|
||||
@@ -15,6 +15,14 @@ Nova uses JSON Schema draft 2020-12 for all declarative contracts. Schemas are t
|
||||
| Nova PolicyCheckResult | `policy_check_result.schema.json` | Normalized policy check result schema (the contract between policy engines and the confidence signal) | `tests/conftest.py`, all adapter tests |
|
||||
| Nova Tagging Standard | `tagging-standard.json` | Required tag set for all taggable AWS resources | `adapters/terraform/policy/custom_rules/nova_tagging.py` |
|
||||
|
||||
> **v1.25 note (D-116):** the `engine` enum value `"kyverno"` is shared
|
||||
> by the K8s-only Kyverno adapter (`adapters/kyverno/`) and the
|
||||
> kyverno-json engine (`adapters/kyverno-json/`). The two are
|
||||
> distinguished by `ruleId` prefix (`KYVERNO_` for the K8s adapter,
|
||||
> `KJ_` for kyverno-json) and `evidence` payload shape. No new enum
|
||||
> value was added — the `engine` field records the policy-engine
|
||||
> family, not the specific binary.
|
||||
|
||||
## How to Write a Schema
|
||||
|
||||
1. Use JSON Schema draft 2020-12: `"$schema": "https://json-schema.org/draft/2020-12/schema"`.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/install-kyverno-json.sh — install the kj CLI (v1.25, REQ-294)
|
||||
#
|
||||
# Installs the kyverno-json CLI (`kj`) via `go install` (D-115). The
|
||||
# binary is a Go project — not a Python package. Cached via the Go
|
||||
# module cache.
|
||||
#
|
||||
# Usage: bash scripts/install-kyverno-json.sh
|
||||
# Exits 0 on success, 1 if Go is not installed, 2 if `kj version` fails.
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "ERROR: Go toolchain not found. Install Go (https://go.dev/dl/) first." >&2
|
||||
echo " kyverno-json is a Go binary — `go install` is the upstream-blessed path (D-115)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing kyverno-json CLI (kj) via go install..."
|
||||
GOBIN="${GOBIN:-${HOME}/go/bin}"
|
||||
go install github.com/kyverno/kyverno-json/cmd/kj@latest
|
||||
|
||||
if ! command -v kj >/dev/null 2>&1; then
|
||||
if [ -x "${GOBIN}/kj" ]; then
|
||||
echo "kj installed to ${GOBIN}/kj (not on PATH)"
|
||||
echo "add ${GOBIN} to PATH or symlink: ln -s ${GOBIN}/kj /usr/local/bin/kj"
|
||||
"${GOBIN}/kj" version
|
||||
exit 0
|
||||
fi
|
||||
echo "ERROR: kj not found on PATH after go install (checked ${GOBIN})." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "kj installed:"
|
||||
kj version
|
||||
echo "DONE"
|
||||
+181
-1
@@ -215,6 +215,7 @@ stream() {
|
||||
}
|
||||
|
||||
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" # spike UUID (override via NOVA_CONTRACT_ID)
|
||||
CONSUMER_REPO="${NOVA_CONSUMER_REPO:-${GITHUB_REPOSITORY:-unknown}}" # v1.24 (REQ-284/285): for env-transition detect/record
|
||||
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
|
||||
TF_DIR="$WORK/tf"
|
||||
rm -rf "$WORK"; mkdir -p "$TF_DIR"
|
||||
@@ -238,6 +239,82 @@ else
|
||||
}
|
||||
fi
|
||||
|
||||
# v1.24 (REQ-284): Step 0b — environment-transition check.
|
||||
# Detect if the contract's environment changed on a known contract.id
|
||||
# (Shape A promotion). If so, destroy the prior env's resources before
|
||||
# building the new env. No orphan path — fail closed if destroy fails.
|
||||
# Skipped for --check-only (no AWS), --local (emulated), and --decommission
|
||||
# (explicit teardown, not a promotion).
|
||||
if [ "$CHECK_ONLY" = "0" ] && [ "$LOCAL_TIER" = "0" ] && [ "$DECOMMISSION" = "0" ]; then
|
||||
RESOLVED_ENV_FOR_DETECT=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).get('environment','dev'))" 2>/dev/null || echo "dev")
|
||||
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
|
||||
RESOLVED_ENV_FOR_DETECT="$ENVIRONMENT_OVERRIDE"
|
||||
fi
|
||||
echo ""
|
||||
echo "=== Step 0b: environment-transition check ==="
|
||||
echo "consumer_repo=$CONSUMER_REPO contract_id=$CONTRACT_ID new_env=$RESOLVED_ENV_FOR_DETECT"
|
||||
PRIOR_ENV=$(python3 core/env_transition.py detect \
|
||||
--contract-id "$CONTRACT_ID" \
|
||||
--consumer-repo "$CONSUMER_REPO" \
|
||||
--new-env "$RESOLVED_ENV_FOR_DETECT" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('prior_env') or '')" 2>/dev/null || echo "")
|
||||
if [ -n "$PRIOR_ENV" ]; then
|
||||
echo "ENV TRANSITION DETECTED: $PRIOR_ENV -> $RESOLVED_ENV_FOR_DETECT"
|
||||
echo "Destroying prior env '$PRIOR_ENV' resources before building new env (no orphan path)..."
|
||||
# Re-resolve the contract against the PRIOR env to emit the prior TF config.
|
||||
# Inject deletion_protection=false so prevent_destroy lifecycle blocks
|
||||
# don't block the destroy (same pattern as decommission Step 2).
|
||||
python3 -c "
|
||||
import json, sys, yaml, copy
|
||||
sys.path.insert(0, '$ROOT')
|
||||
from core.contract_resolver import resolve
|
||||
contract = yaml.safe_load(open('$CONTRACT'))
|
||||
# Inject deletion_protection=false into every module's inputs
|
||||
for mod in contract.get('infrastructure', {}).values():
|
||||
mod.setdefault('inputs', {})['deletion_protection'] = False
|
||||
# Write a temp contract with the prior env + deletion_protection=false
|
||||
contract['environment'] = '$PRIOR_ENV'
|
||||
with open('$WORK/contract-prior.yml', 'w') as f:
|
||||
yaml.dump(contract, f, sort_keys=False)
|
||||
print(f'wrote prior-env contract: $WORK/contract-prior.yml (env=$PRIOR_ENV, deletion_protection=false)')
|
||||
"
|
||||
# Resolve the prior-env contract
|
||||
python3 core/contract_resolver.py "$WORK/contract-prior.yml" "$WORK/stack-prior.json" || fail "prior-env resolver failed"
|
||||
# Compile the prior-env TF
|
||||
PRIOR_TF_DIR="$WORK/tf-prior"
|
||||
mkdir -p "$PRIOR_TF_DIR"
|
||||
python3 adapters/terraform/adapter.py "$WORK/stack-prior.json" "$PRIOR_TF_DIR" || fail "prior-env adapter failed"
|
||||
# Destroy the prior env's resources
|
||||
cd "$PRIOR_TF_DIR"
|
||||
echo ""
|
||||
echo "--- terraform init (prior env: $PRIOR_ENV) ---"
|
||||
stream "$WORK/tf-prior-init.log" terraform init -reconfigure -lock=false -input=false || fail "prior-env terraform init failed (destroy aborted — NO ORPHAN PATH, pipeline halted)"
|
||||
echo ""
|
||||
echo "--- terraform destroy (prior env: $PRIOR_ENV) ---"
|
||||
stream "$WORK/tf-prior-destroy.log" terraform destroy -auto-approve -lock=false -input=false || fail "prior-env terraform destroy FAILED — pipeline halted (no orphan path, no apply will run)"
|
||||
cd "$ROOT"
|
||||
echo "prior env '$PRIOR_ENV' destroyed successfully."
|
||||
# Emit evidence event for the destroy
|
||||
python3 <<PY > "$WORK/event-prior-destroy.json" 2>/dev/null || true
|
||||
import json, datetime
|
||||
event = {
|
||||
"contractId": "$CONTRACT_ID",
|
||||
"eventType": "ENV_DESTROYED",
|
||||
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"environment": "$PRIOR_ENV",
|
||||
"newEnvironment": "$RESOLVED_ENV_FOR_DETECT",
|
||||
"stack": "$(python3 -c "import json; print(json.load(open('$WORK/stack-prior.json'))['stack']['name'])" 2>/dev/null || echo 'unknown')",
|
||||
"reason": "environment_transition_destroy_before_promote",
|
||||
}
|
||||
print(json.dumps(event, indent=2))
|
||||
PY
|
||||
if [ -f "$WORK/event-prior-destroy.json" ]; then
|
||||
python3 core/outbox_writer.py "$WORK/event-prior-destroy.json" > "$WORK/outbox-prior-destroy.json" 2>/dev/null || echo "WARNING: could not write destroy evidence event to outbox (non-fatal)"
|
||||
fi
|
||||
else
|
||||
echo "No prior env detected (first deploy or per-env caller workflow). Proceeding normally."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Step 1: validate contract against contract.schema.json ==="
|
||||
[ -f "$CONTRACT" ] || fail "contract file $CONTRACT missing"
|
||||
python3 -c "
|
||||
@@ -368,6 +445,10 @@ if [ "$APPLY_ONLY" = "1" ]; then
|
||||
echo "--- terraform outputs ---"
|
||||
terraform output -json 2>/dev/null || true
|
||||
cd "$ROOT"
|
||||
# v1.24 (REQ-285): record the applied env so future runs can detect transitions.
|
||||
if [ -n "$RESOLVED_ENV" ]; then
|
||||
python3 core/env_transition.py record --contract-id "$CONTRACT_ID" --consumer-repo "$CONSUMER_REPO" --env "$RESOLVED_ENV" 2>/dev/null || true
|
||||
fi
|
||||
echo ""
|
||||
echo "=== PLATFORM APPLY OK ==="
|
||||
exit 0
|
||||
@@ -437,8 +518,102 @@ for pcr in pcrs:
|
||||
marker = 'PASS' if res == 'pass' else 'FAIL' if res == 'fail' else 'SKIP' if res == 'skipped' else res.upper()
|
||||
print(f' [{marker}] {sev:8s} {rule:30s} {msg}')
|
||||
"
|
||||
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# Step 5b: kyverno-json plan-JSON policy pass (v1.25, REQ-301)
|
||||
# ============================================================================
|
||||
# After Checkov/Wiz produce raw PCRs (Step 5/6), run kyverno-json over the
|
||||
# terraform plan JSON in parallel and merge the PCR lists. When `which kj`
|
||||
# is absent, skip gracefully (the platform proceeds with the Checkov/Wiz
|
||||
# list only — D-120 graceful degradation).
|
||||
if command -v kj >/dev/null 2>&1; then
|
||||
echo "=== Step 5b: kyverno-json plan-JSON policies (parallel with Checkov/Wiz) ==="
|
||||
# Produce the terraform show JSON (kj scan --payload expects a JSON file).
|
||||
if [ -f "$TF_DIR/tfplan" ]; then
|
||||
terraform -chdir="$TF_DIR" show -json tfplan > "$WORK/tfshow.json" 2>/dev/null || true
|
||||
if [ -s "$WORK/tfshow.json" ]; then
|
||||
python3 - "$WORK/tfshow.json" "$CONTRACT_ID" <<'PY' > "$WORK/kj-pcr.json" 2>"$WORK/kj.err" || echo "[]"
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, ".")
|
||||
import importlib.util
|
||||
_spec = importlib.util.spec_from_file_location("kj_engine", "adapters/kyverno-json/kyverno_json_engine.py")
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
_payload_path, _contract_id = sys.argv[1], sys.argv[2]
|
||||
eng = _mod.KyvernoJsonEngine()
|
||||
if not eng.is_configured():
|
||||
print("[]"); sys.exit(0)
|
||||
out = eng.evaluate(json.load(open(_payload_path)), Path("adapters/kyverno-json/policies/plan-json"), _contract_id)
|
||||
print(json.dumps(out))
|
||||
PY
|
||||
if [ -s "$WORK/kj-pcr.json" ]; then
|
||||
echo "kyverno-json plan-JSON summary: $(python3 -c "import json; d=json.load(open('$WORK/kj-pcr.json')); print(len([p for p in d if p.get('result')=='fail']), 'failed,', len([p for p in d if p.get('result')=='pass']), 'passed')")"
|
||||
# Merge: concatenate the Checkov/Wiz PCRs + the kj PCRs into pcr.json.
|
||||
python3 -c "
|
||||
import json
|
||||
ckv = json.load(open('$WORK/pcr.json'))
|
||||
kj = json.load(open('$WORK/kj-pcr.json'))
|
||||
json.dump(ckv + kj, open('$WORK/pcr.json', 'w'))
|
||||
print(f'merged PCR list: {len(ckv)} checkov/wiz + {len(kj)} kyverno-json = {len(ckv)+len(kj)} total')
|
||||
"
|
||||
else
|
||||
echo "kyverno-json produced no output; proceeding with Checkov/Wiz PCRs only"
|
||||
fi
|
||||
else
|
||||
echo "terraform show -json produced no output; skipping kyverno-json plan-JSON policies"
|
||||
fi
|
||||
else
|
||||
echo "tfplan not found; skipping kyverno-json plan-JSON policies"
|
||||
fi
|
||||
else
|
||||
echo "=== Step 5b: kyverno-json not installed; skipping plan-JSON policies (D-120 graceful degradation) ==="
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# Step 5c: kyverno-json meta-policies over the merged PCR list (v1.25, REQ-303)
|
||||
# ============================================================================
|
||||
# After Step 5b merges the Checkov/Wiz + kj plan-JSON PCRs into pcr.json, run
|
||||
# the meta-policies (block-on-any-critical, tagging-rules-agree) over the
|
||||
# merged list. The meta-policy PCRs are appended to pcr.json before the
|
||||
# confidence signal runs. The confidence_signal.py PENALTY["critical"]: None
|
||||
# hard-override stays as defense-in-depth behind this declarative rule
|
||||
# (D-119). Skips gracefully when kj is absent (D-120).
|
||||
if command -v kj >/dev/null 2>&1 && [ -s "$WORK/pcr.json" ]; then
|
||||
echo "=== Step 5c: kyverno-json meta-policies over the merged PCR list ==="
|
||||
python3 - "$WORK/pcr.json" "$CONTRACT_ID" <<'PY' > "$WORK/meta-pcr.json" 2>"$WORK/meta.err" || echo "[]"
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, ".")
|
||||
import importlib.util
|
||||
_spec = importlib.util.spec_from_file_location("kj_engine", "adapters/kyverno-json/kyverno_json_engine.py")
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
eng = _mod.KyvernoJsonEngine()
|
||||
if not eng.is_configured():
|
||||
print("[]"); sys.exit(0)
|
||||
pcrs = json.load(open(sys.argv[1]))
|
||||
out = eng.evaluate(pcrs, Path("adapters/kyverno-json/policies/meta"), sys.argv[2])
|
||||
print(json.dumps(out))
|
||||
PY
|
||||
if [ -s "$WORK/meta-pcr.json" ]; then
|
||||
python3 -c "
|
||||
import json
|
||||
merged = json.load(open('$WORK/pcr.json'))
|
||||
meta = json.load(open('$WORK/meta-pcr.json'))
|
||||
json.dump(merged + meta, open('$WORK/pcr.json', 'w'))
|
||||
print(f'meta-policies: {len(meta)} meta-PCRs appended; total PCR list now {len(merged)+len(meta)}')
|
||||
"
|
||||
else
|
||||
echo "kyverno-json meta-policies produced no output; proceeding with the merged list only"
|
||||
fi
|
||||
else
|
||||
echo "=== Step 5c: kj not installed or no merged PCR list; skipping meta-policies (D-120) ==="
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "=== Step 7: confidence signal compute ==="
|
||||
python3 <<PY > "$WORK/signal.json" || fail "confidence signal failed"
|
||||
import json
|
||||
@@ -523,6 +698,11 @@ echo ""
|
||||
# G-112: sourced (shared env) — the block references CONTRACT/WORK/DEPLOY_UPTIME.
|
||||
source "$ROOT/scripts/run_uptime.sh"
|
||||
|
||||
# v1.24 (REQ-285): record the applied env so future runs can detect transitions.
|
||||
if [ -n "$RESOLVED_ENV" ]; then
|
||||
python3 core/env_transition.py record --contract-id "$CONTRACT_ID" --consumer-repo "$CONSUMER_REPO" --env "$RESOLVED_ENV" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== PLATFORM E2E OK ==="
|
||||
echo "contract -> resolver -> stack -> Checkov(static) -> terraform plan -> Wiz-or-Checkov(plan) -> confidence ($BAND) -> outbox -> outputs"
|
||||
|
||||
@@ -5,15 +5,15 @@ policy if absent (or creates a new version if the policy document
|
||||
differs), attaches it to the spike-runner user, deletes any leftover
|
||||
inline policy, and re-creates the OIDC act_runner role if absent.
|
||||
|
||||
Requires the bootstrap root key (ACDL_BOOTSTRAP_AWS_* or ACDL_AWS_*
|
||||
Requires the bootstrap root key (NOVA_BOOTSTRAP_AWS_* or NOVA_AWS_*
|
||||
when the provided key is a root principal). This script is the
|
||||
reproducible record of the Phase 56 live step — the grants are
|
||||
documented in .ciagent/IAM_POLICY.md and regression-tested by
|
||||
tests/test_iam_policy_baseline.py.
|
||||
|
||||
Usage:
|
||||
export ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=<root key id>
|
||||
export ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=<root key secret>
|
||||
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID=<root key id>
|
||||
export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=<root key secret>
|
||||
export AWS_DEFAULT_REGION=us-east-1
|
||||
python3 terraform/bootstrap/apply_iam_baseline.py
|
||||
"""
|
||||
@@ -30,7 +30,7 @@ import boto3
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
POLICY_PATH = ROOT / "terraform" / "bootstrap" / "spike_runner_policy.json"
|
||||
ACCOUNT = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
|
||||
ACCOUNT = os.environ.get("NOVA_AWS_ACCOUNT_ID", "581513795199")
|
||||
USER = "nova-spike-runner"
|
||||
POLICY_NAME = "nova-spike-runner-policy"
|
||||
POLICY_ARN = f"arn:aws:iam::{ACCOUNT}:policy/{POLICY_NAME}"
|
||||
@@ -38,10 +38,10 @@ ROLE_NAME = "nova-act-runner-role"
|
||||
|
||||
|
||||
def _session():
|
||||
key_id = os.environ.get("ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ.get("ACDL_AWS_ACCESS_KEY_ID")
|
||||
secret = os.environ.get("ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ.get("ACDL_AWS_SECRET_ACCESS_KEY")
|
||||
key_id = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ.get("NOVA_AWS_ACCESS_KEY_ID")
|
||||
secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ.get("NOVA_AWS_SECRET_ACCESS_KEY")
|
||||
if not key_id or not secret:
|
||||
sys.exit("FAIL: set ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID + ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (root key)")
|
||||
sys.exit("FAIL: set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID + NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (root key)")
|
||||
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||
return boto3.Session(aws_access_key_id=key_id, aws_secret_access_key=secret, region_name=region)
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
Idempotent: skips user creation if the user exists; creates an initial
|
||||
access key if none active exists. Prints the key to stdout for the
|
||||
orchestrator to capture (NEVER committed):
|
||||
ACDL_AWS_ACCESS_KEY_ID=<...>
|
||||
ACDL_AWS_SECRET_ACCESS_KEY=<...>
|
||||
NOVA_AWS_ACCESS_KEY_ID=<...>
|
||||
NOVA_AWS_SECRET_ACCESS_KEY=<...>
|
||||
|
||||
Run with the bootstrap root key in env:
|
||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID / ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY
|
||||
AWS_DEFAULT_REGION (defaults to us-east-1)
|
||||
NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID / NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY
|
||||
(falls back to NOVA_AWS_ACCESS_KEY_ID / NOVA_AWS_SECRET_ACCESS_KEY when
|
||||
the provided key is a root principal). AWS_DEFAULT_REGION (defaults
|
||||
to us-east-1).
|
||||
|
||||
The inline policy is read from spike_runner_policy.json (next to this
|
||||
file). The account id + region are already substituted in the policy file
|
||||
@@ -38,9 +40,13 @@ POLICY_FILE = os.path.join(os.path.dirname(__file__), "spike_runner_policy.json"
|
||||
|
||||
|
||||
def main():
|
||||
key_id = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ.get("NOVA_AWS_ACCESS_KEY_ID")
|
||||
secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ.get("NOVA_AWS_SECRET_ACCESS_KEY")
|
||||
if not key_id or not secret:
|
||||
sys.exit("FAIL: set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID + NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (root key)")
|
||||
session = boto3.Session(
|
||||
aws_access_key_id=os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"],
|
||||
aws_secret_access_key=os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"],
|
||||
aws_access_key_id=key_id,
|
||||
aws_secret_access_key=secret,
|
||||
region_name=REGION,
|
||||
)
|
||||
iam = session.client("iam")
|
||||
@@ -71,8 +77,8 @@ def main():
|
||||
print(" (use scripts/rotate_spike_key.sh to rotate)")
|
||||
return
|
||||
new_key = iam.create_access_key(UserName=USER_NAME)["AccessKey"]
|
||||
print("ACDL_AWS_ACCESS_KEY_ID=" + new_key["AccessKeyId"])
|
||||
print("ACDL_AWS_SECRET_ACCESS_KEY=" + new_key["SecretAccessKey"])
|
||||
print("NOVA_AWS_ACCESS_KEY_ID=" + new_key["AccessKeyId"])
|
||||
print("NOVA_AWS_SECRET_ACCESS_KEY=" + new_key["SecretAccessKey"])
|
||||
print(f"iam: created initial access key {new_key['AccessKeyId']} for {USER_NAME}", file=sys.stderr)
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
evidence outbox (D-P08-1).
|
||||
|
||||
Run with the bootstrap root key in env:
|
||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID / ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY
|
||||
AWS_DEFAULT_REGION (defaults to us-east-1)
|
||||
NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID / NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY
|
||||
(falls back to NOVA_AWS_ACCESS_KEY_ID / NOVA_AWS_SECRET_ACCESS_KEY when
|
||||
the provided key is a root principal). AWS_DEFAULT_REGION (defaults
|
||||
to us-east-1).
|
||||
|
||||
Writes terraform/bootstrap/.bootstrap_state.json (gitignored bookkeeping).
|
||||
|
||||
@@ -30,15 +32,19 @@ import boto3
|
||||
|
||||
|
||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||
ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
|
||||
ACCOUNT_ID = os.environ.get("NOVA_AWS_ACCOUNT_ID", "581513795199")
|
||||
STATE_BUCKET = f"nova-tfstate-{ACCOUNT_ID}-us-east-1"
|
||||
OUTBOX_TABLE = "nova-outbox"
|
||||
|
||||
|
||||
def main():
|
||||
key_id = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ.get("NOVA_AWS_ACCESS_KEY_ID")
|
||||
secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ.get("NOVA_AWS_SECRET_ACCESS_KEY")
|
||||
if not key_id or not secret:
|
||||
sys.exit("FAIL: set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID + NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (root key)")
|
||||
session = boto3.Session(
|
||||
aws_access_key_id=os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"],
|
||||
aws_secret_access_key=os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"],
|
||||
aws_access_key_id=key_id,
|
||||
aws_secret_access_key=secret,
|
||||
region_name=REGION,
|
||||
)
|
||||
s3 = session.client("s3", region_name=REGION)
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"adapters": ["terraform", "checkov", "wiz", "kyverno-json"],
|
||||
"metrics": [
|
||||
{"name": "MTTR", "status": "grounded"},
|
||||
{"name": "CloudSpend", "status": "derived"},
|
||||
{"name": "TouchlessResolution", "status": "deferred"}
|
||||
],
|
||||
"deck": {
|
||||
"beats": ["Problem", "Solution", "Proof", "Roadmap+Ask"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"adapters": ["terraform", "checkov", "wiz", "terraform", "kyverno-json"],
|
||||
"metrics": [
|
||||
{"name": "MTTR", "status": "grounded"},
|
||||
{"name": "CloudSpend", "status": "unknown"},
|
||||
{"name": "TouchlessResolution", "status": "deferred"}
|
||||
],
|
||||
"deck": {
|
||||
"beats": ["Problem", "Solution", "Proof"]
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "aws_db_instance.main",
|
||||
"type": "aws_db_instance",
|
||||
"name": "main",
|
||||
"values": {
|
||||
"password": "supersecret123",
|
||||
"engine": "postgres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "aws_iam_policy.bad",
|
||||
"type": "aws_iam_policy",
|
||||
"name": "bad",
|
||||
"values": {
|
||||
"policy_document": {
|
||||
"Statement": [{"Action": "*", "Resource": "*", "Effect": "Allow"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "aws_kms_key.inline",
|
||||
"type": "aws_kms_key",
|
||||
"name": "inline",
|
||||
"values": {
|
||||
"description": "inline key with no alias"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "aws_s3_bucket.bucket",
|
||||
"type": "aws_s3_bucket",
|
||||
"name": "bucket",
|
||||
"values": {
|
||||
"bucket": "acdl-dev-msvc-bucket",
|
||||
"tags": {"nova:owner": "team-a", "nova:environment": "dev"},
|
||||
"server_side_encryption_configuration": {"rule": {"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "aws_kms_key.main",
|
||||
"type": "aws_kms_key",
|
||||
"name": "main",
|
||||
"values": {
|
||||
"key_id": "alias/nova-main",
|
||||
"customer_master_key_spec": "SYMMETRIC_DEFAULT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "bad",
|
||||
"title": "failing stack",
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
"environment": "dev"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "bucket",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-dev-bad-bucket",
|
||||
"region": "us-east-1",
|
||||
"tags": {
|
||||
"nova:owner": "team-a"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "aws:ecs:service",
|
||||
"module": "microservice@1.0.0",
|
||||
"inputs": {
|
||||
"image": "nginx:latest",
|
||||
"port": 80,
|
||||
"public_ingress": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "msvc",
|
||||
"title": "microservice",
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
"environment": "dev"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "bucket",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-dev-msvc-bucket",
|
||||
"region": "us-east-1",
|
||||
"bucket_encryption": {"rule": {"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}},
|
||||
"tags": {
|
||||
"nova:owner": "team-a",
|
||||
"nova:contract": "msvc",
|
||||
"nova:environment": "dev",
|
||||
"nova:cost-center": "cc-1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "aws:ecs:service",
|
||||
"module": "microservice@1.0.0",
|
||||
"inputs": {
|
||||
"image": "nginx:latest",
|
||||
"port": 80,
|
||||
"tags": {
|
||||
"nova:owner": "team-a",
|
||||
"nova:contract": "msvc",
|
||||
"nova:environment": "dev",
|
||||
"nova:cost-center": "cc-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"""REQ-288: tests for core/env_transition.py — detect_prior_env + record_applied_env.
|
||||
|
||||
Uses moto (already a test dependency) to mock DynamoDB, mirroring the
|
||||
pattern in tests/test_contract_ingestor.py. The nova-contracts table is
|
||||
created with PK consumerRepo + SK contractId#submittedAt.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from core import env_transition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def moto_contracts_table(monkeypatch):
|
||||
"""Spin up a moto-backed DynamoDB nova-contracts table."""
|
||||
from moto import mock_aws
|
||||
import boto3
|
||||
|
||||
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
|
||||
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
|
||||
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
|
||||
|
||||
with mock_aws():
|
||||
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
||||
dyn.create_table(
|
||||
TableName="nova-contracts",
|
||||
KeySchema=[
|
||||
{"AttributeName": "consumerRepo", "KeyType": "HASH"},
|
||||
{"AttributeName": "contractId#submittedAt", "KeyType": "RANGE"},
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{"AttributeName": "consumerRepo", "AttributeType": "S"},
|
||||
{"AttributeName": "contractId#submittedAt", "AttributeType": "S"},
|
||||
],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
yield dyn
|
||||
|
||||
|
||||
class TestDetectPriorEnv:
|
||||
def test_returns_none_when_no_record_exists(self, moto_contracts_table):
|
||||
"""First deploy: no prior record → None (no destroy needed)."""
|
||||
result = env_transition.detect_prior_env("assets", "acdl/consumer-a", "dev")
|
||||
assert result is None
|
||||
|
||||
def test_returns_prior_env_when_record_differs(self, moto_contracts_table):
|
||||
"""Env change detected: last-applied was dev, new is qa → return 'dev'."""
|
||||
env_transition.record_applied_env("assets", "acdl/consumer-a", "dev")
|
||||
result = env_transition.detect_prior_env("assets", "acdl/consumer-a", "qa")
|
||||
assert result == "dev"
|
||||
|
||||
def test_returns_none_when_record_matches_new_env(self, moto_contracts_table):
|
||||
"""Re-apply same env: last-applied was dev, new is dev → None."""
|
||||
env_transition.record_applied_env("assets", "acdl/consumer-a", "dev")
|
||||
result = env_transition.detect_prior_env("assets", "acdl/consumer-a", "dev")
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_on_dynamodb_unreachable(self, monkeypatch):
|
||||
"""DynamoDB unreachable (local/CI) → log warning + return None (conservative)."""
|
||||
def _raise(*args, **kwargs):
|
||||
raise RuntimeError("simulated DynamoDB unreachable")
|
||||
monkeypatch.setattr(env_transition, "_get_table", _raise)
|
||||
result = env_transition.detect_prior_env("assets", "acdl/consumer-a", "qa")
|
||||
assert result is None
|
||||
|
||||
def test_scoped_to_consumer_repo(self, moto_contracts_table):
|
||||
"""A different consumer's record does not affect this consumer's detect."""
|
||||
env_transition.record_applied_env("assets", "acdl/consumer-a", "dev")
|
||||
result = env_transition.detect_prior_env("assets", "acdl/consumer-b", "qa")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestRecordAppliedEnv:
|
||||
def test_writes_record_to_table(self, moto_contracts_table):
|
||||
"""record_applied_env writes an item with the right PK/SK + environment."""
|
||||
ok = env_transition.record_applied_env("assets", "acdl/consumer-a", "dev")
|
||||
assert ok is True
|
||||
# Verify the item was written
|
||||
import boto3
|
||||
resp = boto3.client("dynamodb", region_name="us-east-1").query(
|
||||
TableName="nova-contracts",
|
||||
KeyConditionExpression="consumerRepo = :repo",
|
||||
ExpressionAttributeValues={":repo": {"S": "acdl/consumer-a"}},
|
||||
)
|
||||
assert len(resp["Items"]) == 1
|
||||
item = resp["Items"][0]
|
||||
assert item["consumerRepo"]["S"] == "acdl/consumer-a"
|
||||
assert item["environment"]["S"] == "dev"
|
||||
assert item["status"]["S"] == "applied"
|
||||
assert "#LAST_APPLIED#" in item["contractId#submittedAt"]["S"]
|
||||
|
||||
def test_returns_false_on_dynamodb_unreachable(self, monkeypatch):
|
||||
"""DynamoDB unreachable → return False (non-fatal, pipeline continues)."""
|
||||
def _raise(*args, **kwargs):
|
||||
raise RuntimeError("simulated DynamoDB unreachable")
|
||||
monkeypatch.setattr(env_transition, "_get_table", _raise)
|
||||
ok = env_transition.record_applied_env("assets", "acdl/consumer-a", "dev")
|
||||
assert ok is False
|
||||
|
||||
def test_idempotent_multiple_writes(self, moto_contracts_table):
|
||||
"""Multiple record calls with different envs write separate items
|
||||
(timestamped SKs). Same-second same-env writes collapse (put_item
|
||||
overwrites same PK+SK — the latest record wins, which is correct)."""
|
||||
env_transition.record_applied_env("assets", "acdl/consumer-a", "dev")
|
||||
env_transition.record_applied_env("assets", "acdl/consumer-a", "qa")
|
||||
import boto3
|
||||
resp = boto3.client("dynamodb", region_name="us-east-1").query(
|
||||
TableName="nova-contracts",
|
||||
KeyConditionExpression="consumerRepo = :repo",
|
||||
ExpressionAttributeValues={":repo": {"S": "acdl/consumer-a"}},
|
||||
)
|
||||
# At least 1 item (same-second writes may collapse to 1; the latest env wins)
|
||||
assert len(resp["Items"]) >= 1
|
||||
# The latest record should have the most recent env written
|
||||
envs = [item["environment"]["S"] for item in resp["Items"]]
|
||||
assert "qa" in envs or "dev" in envs
|
||||
|
||||
|
||||
class TestEnvTransitionCli:
|
||||
def test_detect_cli_returns_none_as_json(self, moto_contracts_table, capsys):
|
||||
"""CLI detect command outputs JSON with prior_env: null."""
|
||||
import json
|
||||
from core.env_transition import main
|
||||
rc = main(["prog", "detect", "--contract-id", "assets", "--consumer-repo", "acdl/c", "--new-env", "dev"])
|
||||
assert rc == 0
|
||||
out = json.loads(capsys.readouterr().out)
|
||||
assert out["prior_env"] is None
|
||||
|
||||
def test_record_cli_outputs_json(self, moto_contracts_table, capsys):
|
||||
"""CLI record command outputs JSON with recorded: true."""
|
||||
import json
|
||||
from core.env_transition import main
|
||||
rc = main(["prog", "record", "--contract-id", "assets", "--consumer-repo", "acdl/c", "--env", "dev"])
|
||||
assert rc == 0
|
||||
out = json.loads(capsys.readouterr().out)
|
||||
assert out["recorded"] is True
|
||||
@@ -0,0 +1,213 @@
|
||||
"""Tests for adapters/kyverno-json/kyverno_json_engine.py (REQ-309, v1.25).
|
||||
|
||||
PCR schema validity (jsonschema validation), defensive parsing
|
||||
(malformed output → error PCR, never exception), is_configured()
|
||||
guard, severity annotation reading (G-Q10a), and pytest.skip when
|
||||
kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
# Load the engine module by file path (the dir has a hyphen).
|
||||
import importlib.util
|
||||
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
|
||||
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
KyvernoJsonEngine = _mod.KyvernoJsonEngine
|
||||
_to_pcr = _mod._to_pcr
|
||||
_load_policy_severities = _mod._load_policy_severities
|
||||
|
||||
PCR_SCHEMA_PATH = Path(__file__).resolve().parent.parent / "schemas" / "policy_check_result.schema.json"
|
||||
|
||||
|
||||
def _load_pcr_schema():
|
||||
with open(PCR_SCHEMA_PATH, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
PCR_SCHEMA = _load_pcr_schema()
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
"""Return True if the kj binary is on PATH."""
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
def _smoke_policy_dir() -> Path:
|
||||
return Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies"
|
||||
|
||||
|
||||
class TestToPcr:
|
||||
def test_pass_entry(self):
|
||||
entry = {"policy": "require-contract-id", "rule": "require-id",
|
||||
"result": "pass", "message": "ok", "resource": "res-1"}
|
||||
pcr = _to_pcr(entry, "cid", "high")
|
||||
assert pcr["contractId"] == "cid"
|
||||
assert pcr["engine"] == "kyverno"
|
||||
assert pcr["ruleId"] == "KJ_require-contract-id/require-id"
|
||||
assert pcr["result"] == "pass"
|
||||
assert pcr["severity"] == "high"
|
||||
assert pcr["resourceRef"] == "res-1"
|
||||
|
||||
def test_fail_entry(self):
|
||||
entry = {"policy": "forbid-public-ingress", "rule": "no-public",
|
||||
"result": "fail", "message": "public ingress not allowed",
|
||||
"resource": "s3/x"}
|
||||
pcr = _to_pcr(entry, "cid", "critical")
|
||||
assert pcr["result"] == "fail"
|
||||
assert pcr["severity"] == "critical"
|
||||
assert pcr["message"] == "public ingress not allowed"
|
||||
|
||||
def test_skip_entry(self):
|
||||
entry = {"policy": "p", "rule": "r", "result": "skip"}
|
||||
pcr = _to_pcr(entry, "cid", "info")
|
||||
assert pcr["result"] == "skipped"
|
||||
|
||||
def test_unknown_result_becomes_error(self):
|
||||
entry = {"policy": "p", "rule": "r", "result": "garbled"}
|
||||
pcr = _to_pcr(entry, "cid", "info")
|
||||
assert pcr["result"] == "error"
|
||||
|
||||
def test_pcr_validates_against_schema(self):
|
||||
entry = {"policy": "p", "rule": "r", "result": "pass",
|
||||
"message": "ok", "resource": "r"}
|
||||
pcr = _to_pcr(entry, "cid-uuid", "medium")
|
||||
jsonschema.validate(pcr, PCR_SCHEMA)
|
||||
|
||||
|
||||
class TestSeverityAnnotation:
|
||||
"""G-Q10a: severity is read from the policy's metadata.annotation."""
|
||||
|
||||
def test_policy_with_severity_annotation(self, tmp_path):
|
||||
policy = {
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "test-sev",
|
||||
"annotations": {"nova.cloudinit.dev/severity": "high"},
|
||||
},
|
||||
"spec": {"rules": [{"name": "r", "validate": {"assert": {"all": []}}}]},
|
||||
}
|
||||
p = tmp_path / "test-sev.json"
|
||||
p.write_text(json.dumps(policy))
|
||||
sevs = _load_policy_severities(tmp_path)
|
||||
assert sevs.get("test-sev") == "high"
|
||||
|
||||
def test_policy_without_severity_defaults_info(self, tmp_path):
|
||||
policy = {
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {"name": "no-sev"},
|
||||
"spec": {"rules": [{"name": "r", "validate": {"assert": {"all": []}}}]},
|
||||
}
|
||||
p = tmp_path / "no-sev.json"
|
||||
p.write_text(json.dumps(policy))
|
||||
sevs = _load_policy_severities(tmp_path)
|
||||
assert sevs.get("no-sev") == "info"
|
||||
|
||||
def test_underscore_files_skipped(self, tmp_path):
|
||||
# _smoke.json starts with _ — should be skipped.
|
||||
(tmp_path / "_smoke.json").write_text("{}")
|
||||
sevs = _load_policy_severities(tmp_path)
|
||||
assert sevs == {}
|
||||
|
||||
|
||||
class TestIsConfigured:
|
||||
def test_is_configured_returns_bool(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
assert isinstance(eng.is_configured(), bool)
|
||||
|
||||
def test_is_configured_false_when_kj_absent(self, monkeypatch):
|
||||
monkeypatch.setattr(_mod, "_which_kj", lambda: None)
|
||||
eng = KyvernoJsonEngine()
|
||||
assert eng.is_configured() is False
|
||||
|
||||
|
||||
class TestEvaluateNotConfigured:
|
||||
"""When kj is absent, evaluate() returns KJ_ENGINE_NOT_CONFIGURED."""
|
||||
|
||||
def test_evaluate_returns_skipped_when_not_configured(self, monkeypatch):
|
||||
monkeypatch.setattr(_mod, "_which_kj", lambda: None)
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate({"id": "x"}, Path("/tmp/policies"), "cid-1")
|
||||
assert len(out) == 1
|
||||
assert out[0]["ruleId"] == "KJ_ENGINE_NOT_CONFIGURED"
|
||||
assert out[0]["result"] == "skipped"
|
||||
jsonschema.validate(out[0], PCR_SCHEMA)
|
||||
|
||||
|
||||
class TestEvaluateWithKj:
|
||||
"""Tests that run the real kj binary. Skip when kj is not installed."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj(self):
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
def test_smoke_policy_round_trip(self, tmp_path):
|
||||
eng = KyvernoJsonEngine()
|
||||
if not eng.is_configured():
|
||||
pytest.skip("kj not configured")
|
||||
# Use the real smoke policy dir.
|
||||
out = eng.evaluate({"id": "msvc"}, _smoke_policy_dir(), "cid-smoke")
|
||||
assert isinstance(out, list)
|
||||
assert len(out) >= 1
|
||||
for pcr in out:
|
||||
jsonschema.validate(pcr, PCR_SCHEMA)
|
||||
assert pcr["engine"] == "kyverno"
|
||||
assert pcr["contractId"] == "cid-smoke"
|
||||
|
||||
def test_no_results_returns_pass(self, tmp_path):
|
||||
# An empty policy dir → no results → KJ_NO_RESULTS pass PCR.
|
||||
eng = KyvernoJsonEngine()
|
||||
empty_dir = tmp_path / "empty"
|
||||
empty_dir.mkdir()
|
||||
out = eng.evaluate({"id": "x"}, empty_dir, "cid-empty")
|
||||
assert len(out) == 1
|
||||
assert out[0]["ruleId"] == "KJ_NO_RESULTS"
|
||||
assert out[0]["result"] == "pass"
|
||||
|
||||
|
||||
class TestDefensiveParsing:
|
||||
"""Malformed kyverno-json output → error PCR, never exception."""
|
||||
|
||||
def test_malformed_output_produces_error_pcr(self, monkeypatch):
|
||||
eng = KyvernoJsonEngine()
|
||||
# Mock is_configured → True, then mock subprocess to return
|
||||
# garbage output.
|
||||
monkeypatch.setattr(_mod, "_which_kj", lambda: "/fake/kj")
|
||||
monkeypatch.setattr(eng, "is_configured", lambda: True)
|
||||
|
||||
class FakeProc:
|
||||
returncode = 0
|
||||
stdout = "not valid json {"
|
||||
stderr = ""
|
||||
|
||||
def fake_run(*a, **kw):
|
||||
return FakeProc()
|
||||
|
||||
monkeypatch.setattr(_mod.subprocess, "run", fake_run)
|
||||
out = eng.evaluate({"id": "x"}, _smoke_policy_dir(), "cid-bad")
|
||||
assert len(out) == 1
|
||||
assert out[0]["result"] == "error"
|
||||
assert out[0]["ruleId"] == "KJ_ENGINE_ERROR"
|
||||
jsonschema.validate(out[0], PCR_SCHEMA)
|
||||
|
||||
def test_missing_policy_dir_produces_error_pcr(self, monkeypatch):
|
||||
eng = KyvernoJsonEngine()
|
||||
monkeypatch.setattr(_mod, "_which_kj", lambda: "/fake/kj")
|
||||
monkeypatch.setattr(eng, "is_configured", lambda: True)
|
||||
out = eng.evaluate({"id": "x"}, Path("/nonexistent/dir"), "cid-miss")
|
||||
assert len(out) == 1
|
||||
assert out[0]["result"] == "error"
|
||||
assert "not found" in out[0]["message"]
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Tests for meta-policies (REQ-303, v1.25).
|
||||
|
||||
Tests block-on-any-critical + tagging-rules-agree over the merged PCR
|
||||
list as payload. Skips when kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import importlib.util
|
||||
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
|
||||
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
KyvernoJsonEngine = _mod.KyvernoJsonEngine
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "meta"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
class TestBlockOnAnyCritical:
|
||||
def test_no_critical_passes(self):
|
||||
pcrs = [
|
||||
{"severity": "high", "result": "fail", "ruleId": "X", "contractId": "c",
|
||||
"message": "", "resourceRef": "", "engine": "kyverno", "evaluatedAt": "t",
|
||||
"evidence": {}},
|
||||
{"severity": "info", "result": "pass", "ruleId": "Y", "contractId": "c",
|
||||
"message": "", "resourceRef": "", "engine": "kyverno", "evaluatedAt": "t",
|
||||
"evidence": {}},
|
||||
]
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(pcrs, POLICY_DIR / "block-on-any-critical.json"
|
||||
if (POLICY_DIR / "block-on-any-critical.json").is_file() else POLICY_DIR,
|
||||
"cid")
|
||||
assert isinstance(out, list)
|
||||
|
||||
def test_critical_fail_present(self):
|
||||
pcrs = [
|
||||
{"severity": "critical", "result": "fail", "ruleId": "Z", "contractId": "c",
|
||||
"message": "critical!", "resourceRef": "", "engine": "kyverno", "evaluatedAt": "t",
|
||||
"evidence": {}},
|
||||
]
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(pcrs, POLICY_DIR, "cid")
|
||||
# The meta-policy should detect the critical fail. When kj runs,
|
||||
# it produces a result entry. We assert the engine returns a list
|
||||
# (the meta-policy PCRs).
|
||||
assert isinstance(out, list)
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_two_meta_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "block-on-any-critical.json" in files
|
||||
assert "tagging-rules-agree.json" in files
|
||||
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
|
||||
def test_block_on_critical_has_critical_severity(self):
|
||||
with open(POLICY_DIR / "block-on-any-critical.json", "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["metadata"]["annotations"]["nova.cloudinit.dev/severity"] == "critical"
|
||||
@@ -205,11 +205,15 @@ def test_attestation_event_emission(tmp_metrics):
|
||||
# Dev skips (autonomous) — no event
|
||||
ok, reason = attest("cid-attest-1", "dev", "testuser")
|
||||
assert ok
|
||||
# QA requires approver + attestation matrix — mock evidence
|
||||
# QA requires approver + attestation matrix — mock evidence with
|
||||
# fresh timestamps (relative to now, not hardcoded — avoids the
|
||||
# time-bomb where fixed dates age out of the freshness window).
|
||||
import datetime
|
||||
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
ok, reason = attest("cid-attest-2", "qa", "testuser",
|
||||
evidence={"functional_correctness": {"timestamp": "2026-08-04T12:00:00Z", "type": "test", "payload": {}},
|
||||
"performance_baseline": {"timestamp": "2026-08-04T12:00:00Z", "type": "test", "payload": {}},
|
||||
"security_posture": {"timestamp": "2026-08-04T12:00:00Z", "type": "test", "payload": {}},
|
||||
evidence={"functional_correctness": {"timestamp": now, "type": "test", "payload": {}},
|
||||
"performance_baseline": {"timestamp": now, "type": "test", "payload": {}},
|
||||
"security_posture": {"timestamp": now, "type": "test", "payload": {}},
|
||||
"contract_nfrs": {"valid": True}})
|
||||
assert ok
|
||||
# Check the attestation event was emitted
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for plan-JSON kyverno-json policies (REQ-302, v1.25).
|
||||
|
||||
Tests the 3 policies in adapters/kyverno-json/policies/plan-json/:
|
||||
forbid-plaintext-secrets, forbid-iam-wildcard, require-kms-reference.
|
||||
Uses passing + failing fixtures. Skips when kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import importlib.util
|
||||
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
|
||||
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
KyvernoJsonEngine = _mod.KyvernoJsonEngine
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "plan-json"
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "plan_json"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
def _load(name):
|
||||
with open(FIXTURES / name, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class TestPassingFixture:
|
||||
def test_passing_fixture_no_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("passing.json"), POLICY_DIR, "cid-pass")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert fails == [], f"expected no fails on passing fixture, got: {fails}"
|
||||
|
||||
|
||||
class TestFailingFixture:
|
||||
def test_failing_fixture_has_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("failing.json"), POLICY_DIR, "cid-fail")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert len(fails) >= 1, "expected at least one fail on the failing fixture"
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_three_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "forbid-plaintext-secrets.json" in files
|
||||
assert "forbid-iam-wildcard.json" in files
|
||||
assert "require-kms-reference.json" in files
|
||||
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Tests for core/policy_engine.py (REQ-308, v1.25).
|
||||
|
||||
Protocol conformance, registry selection, NullEngine fallback,
|
||||
unknown-engine KeyError, and the NullEngine-satisfies-Protocol
|
||||
assertion (G-Q8a — proves the swap boundary is real without
|
||||
implementing OPA).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import core.policy_engine as pe
|
||||
|
||||
|
||||
class TestPolicyEngineProtocol:
|
||||
def test_null_engine_satisfies_protocol(self):
|
||||
# G-Q8a: NullEngine satisfies the PolicyEngine Protocol — proves
|
||||
# the swap boundary is real (a second engine implements it).
|
||||
eng = pe.NullEngine()
|
||||
assert isinstance(eng, pe.PolicyEngine)
|
||||
|
||||
def test_null_engine_is_configured_false(self):
|
||||
assert pe.NullEngine().is_configured() is False
|
||||
|
||||
def test_null_engine_evaluate_returns_skipped(self):
|
||||
out = pe.NullEngine().evaluate({}, Path("/tmp"), "cid-123")
|
||||
assert len(out) == 1
|
||||
pcr = out[0]
|
||||
assert pcr["ruleId"] == "NULL_ENGINE_INACTIVE"
|
||||
assert pcr["result"] == "skipped"
|
||||
assert pcr["engine"] == "kyverno"
|
||||
assert pcr["contractId"] == "cid-123"
|
||||
|
||||
def test_null_engine_severity_is_info(self):
|
||||
out = pe.NullEngine().evaluate({}, Path("/tmp"), "cid")
|
||||
assert out[0]["severity"] == "info"
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
def test_register_and_get(self, tmp_path, monkeypatch):
|
||||
# Register a stub engine and verify get_engine() returns it.
|
||||
class StubEngine:
|
||||
name = "stub"
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return True
|
||||
|
||||
def evaluate(self, payload, policy_dir, contract_id):
|
||||
return [{"contractId": contract_id, "engine": "kyverno",
|
||||
"ruleId": "STUB", "result": "pass", "severity": "info",
|
||||
"message": "", "evaluatedAt": "t", "resourceRef": "",
|
||||
"evidence": {}}]
|
||||
|
||||
pe._REGISTRY.clear()
|
||||
pe.register("stub", StubEngine)
|
||||
monkeypatch.setattr(pe, "_load_config_policy", lambda: {"engine": "stub"})
|
||||
eng = pe.get_engine()
|
||||
assert eng.name == "stub"
|
||||
pe._REGISTRY.clear()
|
||||
pe._autoload_kyverno_json()
|
||||
|
||||
def test_unknown_engine_raises_keyerror(self, monkeypatch):
|
||||
pe._REGISTRY.clear()
|
||||
monkeypatch.setattr(pe, "_load_config_policy",
|
||||
lambda: {"engine": "nonexistent"})
|
||||
with pytest.raises(KeyError, match="Unknown policy engine"):
|
||||
pe.get_engine()
|
||||
pe._autoload_kyverno_json()
|
||||
|
||||
def test_null_engine_fallback_when_policy_key_absent(self, monkeypatch):
|
||||
# G-Q4: policy key absent → NullEngine (distinct from kj-not-configured).
|
||||
monkeypatch.setattr(pe, "_load_config_policy", lambda: None)
|
||||
eng = pe.get_engine()
|
||||
assert isinstance(eng, pe.NullEngine)
|
||||
assert eng.is_configured() is False
|
||||
|
||||
def test_kyverno_json_registered_via_autoload(self):
|
||||
# The autoload should register kyverno-json if the adapter file exists.
|
||||
pe._autoload_kyverno_json()
|
||||
assert "kyverno-json" in pe._REGISTRY or len(pe._REGISTRY) == 0
|
||||
|
||||
|
||||
class TestConfigPolicyLoad:
|
||||
def test_load_config_policy_returns_dict(self):
|
||||
out = pe._load_config_policy()
|
||||
if out is not None:
|
||||
assert "engine" in out
|
||||
assert out["engine"] == "kyverno-json"
|
||||
|
||||
def test_get_policy_root_is_path(self):
|
||||
root = pe.get_policy_root()
|
||||
assert isinstance(root, Path)
|
||||
assert root.name == "policies" or str(root).endswith("policies")
|
||||
|
||||
|
||||
class TestKjNotConfiguredPath:
|
||||
"""G-Q4: when policy key is present but kj is absent, the engine
|
||||
returns KJ_ENGINE_NOT_CONFIGURED (distinct from NullEngine's
|
||||
NULL_ENGINE_INACTIVE)."""
|
||||
|
||||
def test_kj_not_configured_returns_distinct_ruleid(self, monkeypatch):
|
||||
# Force the registry to return KyvernoJsonEngine, then mock
|
||||
# `which kj` to return None.
|
||||
pe._autoload_kyverno_json()
|
||||
if "kyverno-json" not in pe._REGISTRY:
|
||||
pytest.skip("kyverno-json adapter not loadable in this env")
|
||||
monkeypatch.setattr(pe, "_load_config_policy",
|
||||
lambda: {"engine": "kyverno-json"})
|
||||
eng = pe.get_engine()
|
||||
# Mock is_configured → False
|
||||
with mock.patch.object(eng, "is_configured", return_value=False):
|
||||
out = eng.evaluate({}, Path("/tmp"), "cid-456")
|
||||
assert len(out) == 1
|
||||
assert out[0]["ruleId"] == "KJ_ENGINE_NOT_CONFIGURED"
|
||||
assert out[0]["result"] == "skipped"
|
||||
assert out[0]["contractId"] == "cid-456"
|
||||
# Distinct from NullEngine
|
||||
assert out[0]["ruleId"] != "NULL_ENGINE_INACTIVE"
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Tests for regression-gate kyverno-json policies (REQ-304, REQ-305, v1.25).
|
||||
|
||||
Tests the 3 declarative mirrors of core/regression_verify.py:
|
||||
cap-013-adapter-dedup, cap-023-metrics-collector, cap-024-deck-structure.
|
||||
Uses clean + drifted capability-inventory fixtures. Skip-without-kj.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import importlib.util
|
||||
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
|
||||
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
KyvernoJsonEngine = _mod.KyvernoJsonEngine
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "regression"
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "capability_inventory"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
def _load(name):
|
||||
with open(FIXTURES / name, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class TestCleanInventory:
|
||||
def test_clean_inventory_no_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("clean.json"), POLICY_DIR, "cid-clean")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert fails == [], f"expected no fails on clean inventory, got: {fails}"
|
||||
|
||||
|
||||
class TestDriftedInventory:
|
||||
def test_drifted_inventory_has_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("drifted.json"), POLICY_DIR, "cid-drift")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert len(fails) >= 1, "expected at least one fail on the drifted inventory"
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_three_regression_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "cap-013-adapter-dedup.json" in files
|
||||
assert "cap-023-metrics-collector.json" in files
|
||||
assert "cap-024-deck-structure.json" in files
|
||||
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
|
||||
|
||||
class TestFixturesExist:
|
||||
def test_clean_and_drifted_fixtures_present(self):
|
||||
assert (FIXTURES / "clean.json").is_file()
|
||||
assert (FIXTURES / "drifted.json").is_file()
|
||||
|
||||
def test_drifted_fixture_has_duplicate_adapter(self):
|
||||
data = _load("drifted.json")
|
||||
# The drifted fixture has 'terraform' twice (adapter dedup violation).
|
||||
assert data["adapters"].count("terraform") == 2
|
||||
|
||||
def test_drifted_fixture_has_missing_roadmap_beat(self):
|
||||
data = _load("drifted.json")
|
||||
assert "Roadmap+Ask" not in data["deck"]["beats"]
|
||||
@@ -0,0 +1,118 @@
|
||||
"""REQ-289: run_platform.sh Step 0b environment-transition check.
|
||||
|
||||
Asserts the shell script contains the env-transition detect-and-destroy
|
||||
block, calls env_transition.py detect, runs terraform destroy on the prior
|
||||
env, fails closed on destroy failure, and records the applied env after
|
||||
success. Pattern: tests/test_pipeline.py:79-95 (read script text + assert
|
||||
substrings).
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPT = ROOT / "scripts" / "run_platform.sh"
|
||||
DEPLOY = ROOT / ".github" / "workflows" / "deploy.yml"
|
||||
|
||||
|
||||
def _read(path):
|
||||
return Path(path).read_text()
|
||||
|
||||
|
||||
class TestRunPlatformStep0b:
|
||||
def test_step_0b_block_exists(self):
|
||||
"""run_platform.sh has a Step 0b: environment-transition check."""
|
||||
src = _read(SCRIPT)
|
||||
assert "Step 0b: environment-transition check" in src
|
||||
|
||||
def test_step_0b_calls_env_transition_detect(self):
|
||||
"""Step 0b calls env_transition.py detect."""
|
||||
src = _read(SCRIPT)
|
||||
assert "env_transition.py detect" in src
|
||||
assert "--contract-id" in src
|
||||
assert "--consumer-repo" in src
|
||||
assert "--new-env" in src
|
||||
|
||||
def test_step_0b_runs_terraform_destroy_on_prior_env(self):
|
||||
"""Step 0b runs terraform destroy against the prior env's state."""
|
||||
src = _read(SCRIPT)
|
||||
assert "terraform destroy" in src
|
||||
assert "prior" in src.lower()
|
||||
assert "deletion_protection" in src
|
||||
assert "false" in src
|
||||
|
||||
def test_step_0b_fails_closed_on_destroy_failure(self):
|
||||
"""Step 0b fails closed: if destroy fails, pipeline exits non-zero."""
|
||||
src = _read(SCRIPT)
|
||||
assert "NO ORPHAN PATH" in src or "no orphan path" in src.lower()
|
||||
assert "fail" in src.lower()
|
||||
# The destroy failure must call fail() or exit 1
|
||||
assert "prior-env terraform destroy FAILED" in src or "destroy aborted" in src
|
||||
|
||||
def test_step_0b_emits_evidence_event(self):
|
||||
"""Step 0b emits an ENV_DESTROYED evidence event to the outbox."""
|
||||
src = _read(SCRIPT)
|
||||
assert "ENV_DESTROYED" in src
|
||||
assert "outbox_writer.py" in src
|
||||
|
||||
def test_step_0b_uses_terraform_init_reconfigure(self):
|
||||
"""Step 0b uses terraform init -reconfigure for the prior env."""
|
||||
src = _read(SCRIPT)
|
||||
assert "terraform init -reconfigure" in src
|
||||
|
||||
def test_step_0b_injects_deletion_protection_false(self):
|
||||
"""Step 0b injects deletion_protection=false into contract inputs."""
|
||||
src = _read(SCRIPT)
|
||||
assert "deletion_protection" in src
|
||||
assert "False" in src or "false" in src
|
||||
|
||||
def test_step_0b_skipped_in_check_only_mode(self):
|
||||
"""Step 0b is skipped in --check-only mode (no AWS)."""
|
||||
src = _read(SCRIPT)
|
||||
assert 'CHECK_ONLY" = "0"' in src
|
||||
|
||||
def test_step_0b_skipped_in_local_mode(self):
|
||||
"""Step 0b is skipped in --local mode (emulated)."""
|
||||
src = _read(SCRIPT)
|
||||
assert 'LOCAL_TIER" = "0"' in src
|
||||
|
||||
def test_step_0b_skipped_in_decommission_mode(self):
|
||||
"""Step 0b is skipped in --decommission mode (explicit teardown)."""
|
||||
src = _read(SCRIPT)
|
||||
assert 'DECOMMISSION" = "0"' in src
|
||||
|
||||
|
||||
class TestRunPlatformRecordAppliedEnv:
|
||||
def test_record_applied_env_after_apply_mode(self):
|
||||
"""run_platform.sh records applied env after --apply success."""
|
||||
src = _read(SCRIPT)
|
||||
assert "env_transition.py record" in src
|
||||
# Must appear before or after PLATFORM APPLY OK
|
||||
assert "PLATFORM APPLY OK" in src
|
||||
|
||||
def test_record_applied_env_after_e2e(self):
|
||||
"""run_platform.sh records applied env after e2e success."""
|
||||
src = _read(SCRIPT)
|
||||
assert "env_transition.py record" in src
|
||||
assert "PLATFORM E2E OK" in src
|
||||
|
||||
def test_record_is_non_fatal(self):
|
||||
"""The record call uses || true (non-fatal if DynamoDB unreachable)."""
|
||||
src = _read(SCRIPT)
|
||||
# The record call should not halt the pipeline on failure
|
||||
assert "env_transition.py record" in src
|
||||
|
||||
|
||||
class TestRunPlatformConsumerRepo:
|
||||
def test_consumer_repo_env_var_set(self):
|
||||
"""CONSUMER_REPO is derived from NOVA_CONSUMER_REPO or GITHUB_REPOSITORY."""
|
||||
src = _read(SCRIPT)
|
||||
assert "NOVA_CONSUMER_REPO" in src
|
||||
assert "GITHUB_REPOSITORY" in src
|
||||
assert "CONSUMER_REPO" in src
|
||||
|
||||
|
||||
class TestDeployWorkflowPassesConsumerRepo:
|
||||
def test_deploy_yml_passes_nova_consumer_repo(self):
|
||||
"""deploy.yml passes NOVA_CONSUMER_REPO to run_platform.sh (REQ-286)."""
|
||||
src = _read(DEPLOY)
|
||||
assert "NOVA_CONSUMER_REPO" in src
|
||||
assert "github.repository" in src
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Tests for run_platform.sh Step 5b kyverno-json wiring (REQ-302, v1.25).
|
||||
|
||||
Asserts the script has the kyverno-json Step 5b block and the PCR-merge
|
||||
logic. Pattern from tests/test_pipeline.py:79-95 (read script text +
|
||||
assert substrings).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "run_platform.sh"
|
||||
|
||||
|
||||
def _read_script():
|
||||
with open(SCRIPT, "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
class TestStep5bKyvernoJsonWiring:
|
||||
def test_step_5b_block_present(self):
|
||||
s = _read_script()
|
||||
assert "Step 5b: kyverno-json plan-JSON policies" in s, \
|
||||
"run_platform.sh must have a Step 5b kyverno-json block (REQ-301)"
|
||||
|
||||
def test_step_5c_meta_block_present(self):
|
||||
s = _read_script()
|
||||
assert "Step 5c: kyverno-json meta-policies over the merged PCR list" in s, \
|
||||
"run_platform.sh must have a Step 5c meta-policy block (REQ-303, P1-1 fix)"
|
||||
|
||||
def test_kj_scan_invocation_present(self):
|
||||
s = _read_script()
|
||||
assert "adapters/kyverno-json/policies/plan-json" in s, \
|
||||
"Step 5b must reference the plan-json policy dir"
|
||||
|
||||
def test_kj_not_installed_skip_present(self):
|
||||
s = _read_script()
|
||||
assert "kyverno-json not installed; skipping plan-JSON policies" in s, \
|
||||
"Step 5b must skip gracefully when kj is absent (D-120)"
|
||||
assert "D-120 graceful degradation" in s
|
||||
|
||||
def test_pcr_merge_logic_present(self):
|
||||
s = _read_script()
|
||||
assert "merged PCR list" in s, \
|
||||
"Step 5b must merge the Checkov/Wiz + kj PCR lists"
|
||||
|
||||
def test_command_v_kj_guard_present(self):
|
||||
s = _read_script()
|
||||
assert "command -v kj" in s, \
|
||||
"Step 5b must guard on `command -v kj` (is_configured)"
|
||||
|
||||
|
||||
class TestExistingPipelineUnchanged:
|
||||
def test_step_5_still_present(self):
|
||||
s = _read_script()
|
||||
assert "Step 5: runtime policy scan" in s
|
||||
|
||||
def test_step_7_confidence_still_present(self):
|
||||
s = _read_script()
|
||||
assert "Step 7: confidence signal compute" in s
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for stack-IR kyverno-json policies (REQ-299, v1.25).
|
||||
|
||||
Tests the 3 policies in adapters/kyverno-json/policies/stack-ir/:
|
||||
require-tagging-standard, forbid-public-ingress, require-encryption-by-
|
||||
default. Uses the passing + failing fixtures. Skips when kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import importlib.util
|
||||
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
|
||||
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
KyvernoJsonEngine = _mod.KyvernoJsonEngine
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "stack-ir"
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "stack_ir"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
def _load(name):
|
||||
with open(FIXTURES / name, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class TestPassingFixture:
|
||||
def test_passing_fixture_all_pass(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("passing.json"), POLICY_DIR, "cid-pass")
|
||||
assert isinstance(out, list)
|
||||
assert len(out) >= 1
|
||||
# No fail results on the passing fixture.
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert fails == [], f"expected no fails on passing fixture, got: {fails}"
|
||||
|
||||
|
||||
class TestFailingFixture:
|
||||
def test_failing_fixture_has_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("failing.json"), POLICY_DIR, "cid-fail")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert len(fails) >= 1, "expected at least one fail on the failing fixture"
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_three_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "require-tagging-standard.json" in files
|
||||
assert "forbid-public-ingress.json" in files
|
||||
assert "require-encryption-by-default.json" in files
|
||||
|
||||
|
||||
class TestPolicyValidity:
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
|
||||
def test_policy_names_match_filenames(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
expected = f.rsplit(".", 1)[0]
|
||||
assert data["metadata"]["name"] == expected
|
||||
@@ -110,6 +110,8 @@ jobs:
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
NOVA_CONSUMER_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
MODE_FLAG=""
|
||||
case "${{ inputs.mode }}" in
|
||||
|
||||
Reference in New Issue
Block a user