ec53302014
PLAN.md: 4 execution phases (P1 engine-core, P2 contract+stack-IR policies, P3 plan-JSON+meta+pipeline wiring, P4 regression-gate+docs) + P5 final review/ship. Wave ordering with parallelization (3-2-2-3 concurrent personas). Each phase is a vertical slice (end-to-end: policies + Python wiring + tests + docs). Tags v1.24.0..v1.24.5. 19 requirements (REQ-291..309) mapped to phases and personas. ---ci--- project: acdl phase: 0 milestone: v1.25 status: plan ---/ci---
371 lines
18 KiB
Markdown
371 lines
18 KiB
Markdown
# PLAN — v1.25 (kyverno-json Unified Policy Engine)
|
|
|
|
> Feature milestone. Tags on the **v1.24.x** line: v1.24.0 (P0) →
|
|
> v1.24.1 (P1) → v1.24.2 (P2) → v1.24.3 (P3) → v1.24.4 (P4) → v1.24.5
|
|
> (P5 final = milestone release). 19 requirements (REQ-291..309),
|
|
> 4 execution phases + P0 pre-execution + P5 final review/ship.
|
|
|
|
## Wave model
|
|
|
|
Each phase is a **vertical slice** (end-to-end: policy files + Python
|
|
wiring + tests + docs). Phases are ordered by dependency: the engine
|
|
protocol (P1) must exist before policies (P2/P3) can be wired; the
|
|
pipeline wiring (P3) must exist before the meta-policies (P3) can
|
|
consume the merged PCR list; the regression-gate policies (P4) are
|
|
independent of the pipeline and can be authored in parallel with P3's
|
|
tests, but ship after P3 because they reference the engine registry
|
|
finalized in P1. Within each phase, the waves are the persona task
|
|
groups (parallelizable across personas when `parallelization.enabled:
|
|
true`, `max_concurrent_agents: 5`).
|
|
|
|
## Phase breakdown
|
|
|
|
### Phase P1 — engine-core (Wave 1, backend-engineer + policy-engineer + data-engineer)
|
|
|
|
**Type:** `feat` (engine protocol + registry + kyverno-json engine adapter + install + tests)
|
|
|
|
**Requirements:** REQ-291, REQ-292, REQ-293, REQ-294, REQ-308, REQ-309
|
|
|
|
**Must-haves:**
|
|
- `core/policy_engine.py` — `PolicyEngine` Protocol (PEP 544) +
|
|
`PolicyEngineRegistry` (selects from `config.json.policy.engine`) +
|
|
`NullEngine` fallback (emits `SKIPPED` when `policy` key absent)
|
|
(REQ-291)
|
|
- `.ciagent/config.json` gains `policy` object: `{"engine":
|
|
"kyverno-json", "policy_root":
|
|
"adapters/kyverno-json/policies"}` (REQ-292)
|
|
- `adapters/kyverno-json/kyverno_json_engine.py` — `KyvernoJsonEngine`
|
|
implementing the protocol: `is_configured()` guards on `which kj`;
|
|
`evaluate()` writes payload to temp JSON, invokes
|
|
`kj scan --policy <dir> --payload <json> --output json`, translates
|
|
native output → `list[dict]` PCR records (`engine: "kyverno"`,
|
|
`ruleId` prefixed `KJ_<policy_name>`, severity from
|
|
`nova.cloudinit.dev/severity` annotation); defensive parsing
|
|
(malformed → `error` PCR, never exception); `is_configured()==false`
|
|
→ single `SKIPPED` PCR (`KJ_ENGINE_NOT_CONFIGURED`) (REQ-293)
|
|
- `adapters/kyverno-json/__init__.py` exports `KyvernoJsonEngine`;
|
|
`adapters/kyverno-json/policies/_smoke.json` trivial
|
|
`require-contract-id` policy for round-trip validation;
|
|
`scripts/install-kyverno-json.sh` runs
|
|
`go install github.com/kyverno/kyverno-json/cmd/kj@latest`;
|
|
`.github/workflows/ci.yml` + `.gitea/workflows/ci.yml` install Go + kj
|
|
(cached) (REQ-294)
|
|
- `tests/test_policy_engine.py` — protocol conformance, registry
|
|
selection, unknown-engine `KeyError`, `NullEngine` fallback,
|
|
`is_configured()` false when `which kj` absent (mocked) (REQ-308)
|
|
- `tests/test_kyverno_json_engine.py` — `evaluate()` returns PCR dicts
|
|
validating against `schemas/policy_check_result.schema.json` (via
|
|
`jsonschema`); defensive parsing (malformed kyverno-json output →
|
|
`error` PCR); `is_configured()==false` → `SKIPPED` with
|
|
`KJ_ENGINE_NOT_CONFIGURED`; `pytest.skip("kj not installed")` when
|
|
`which kj` absent (REQ-309)
|
|
|
|
**Vertical slice:** The `PolicyEngineRegistry.get_engine()` returns a
|
|
configured `KyvernoJsonEngine` that can `evaluate()` a trivial payload
|
|
against `_smoke.json` and produce a valid PCR list. The confidence
|
|
signal is unchanged — it already consumes `list[PolicyCheckResult]`.
|
|
The platform runs with or without the `kj` binary (`is_configured()`
|
|
guard). All existing tests pass (NullEngine fallback when `policy` key
|
|
absent in test config — but the v1.25 config.json *sets* the key, so
|
|
existing tests that use the real config get `KyvernoJsonEngine` with
|
|
`is_configured()==false` → `SKIPPED`).
|
|
|
|
**Files touched:**
|
|
- `core/policy_engine.py` (NEW)
|
|
- `.ciagent/config.json` (MODIFIED — `policy` object)
|
|
- `adapters/kyverno-json/__init__.py` (NEW)
|
|
- `adapters/kyverno-json/kyverno_json_engine.py` (NEW)
|
|
- `adapters/kyverno-json/policies/_smoke.json` (NEW)
|
|
- `scripts/install-kyverno-json.sh` (NEW)
|
|
- `.github/workflows/ci.yml` (MODIFIED — Go + kj install step)
|
|
- `.gitea/workflows/ci.yml` (MODIFIED — Go + kj install step)
|
|
- `tests/test_policy_engine.py` (NEW)
|
|
- `tests/test_kyverno_json_engine.py` (NEW)
|
|
|
|
**Verification:** `pytest tests/test_policy_engine.py
|
|
tests/test_kyverno_json_engine.py tests/test_confidence_signal.py
|
|
tests/test_adapter.py tests/test_checkov_adapter.py
|
|
tests/test_kyverno_adapter.py -v` (new tests pass or skip-without-kj;
|
|
existing adapter/confidence tests unchanged). `python3 -m py_compile
|
|
core/policy_engine.py adapters/kyverno-json/kyverno_json_engine.py`.
|
|
|
|
---
|
|
|
|
### Phase P2 — contract + stack-IR policies (Wave 2, policy-engineer + backend-engineer)
|
|
|
|
**Type:** `feat` (policies + resolver wiring + tests)
|
|
|
|
**Requirements:** REQ-295, REQ-296, REQ-297, REQ-298, REQ-299
|
|
|
|
**Must-haves:**
|
|
- `adapters/kyverno-json/policies/contract/` — 4 policies over consumer
|
|
contract JSON: `require-id-pattern.json`,
|
|
`require-env-in-enum.json`, `require-infrastructure-min-1.json`,
|
|
`forbid-unknown-fields.json` — each a `ValidatingPolicy` with one
|
|
`validate.assert` rule using JMESPath against the payload root;
|
|
severity via `nova.cloudinit.dev/severity` annotation (REQ-295)
|
|
- `core/contract_resolver.py` invokes
|
|
`PolicyEngineRegistry.get_engine().evaluate(contract_dict,
|
|
policies/contract/, contract_id)` **before** resolving; failures
|
|
feed the `policy` input as `fail` PCRs (no resolver exit — confidence
|
|
signal decides the gate, `--soft-fail` pattern); emits
|
|
`nova.policy.evaluated` metrics event (REQ-296)
|
|
- `adapters/kyverno-json/policies/stack-ir/` — 3 policies over
|
|
resolved Stack IR: `require-tagging-standard.json` (ports
|
|
`nova_tagging.py` — `nova:owner` + `nova:environment` tags on every
|
|
`resources[]` entry), `forbid-public-ingress.json` (v1.0 demo rule),
|
|
`require-encryption-by-default.json` (v1.8 D-encryption-default);
|
|
`~` modifier iterates `resources[]` (REQ-297)
|
|
- `core/contract_resolver.py` invokes the engine with the resolved
|
|
Stack IR and `policies/stack-ir/` **after** resolving; resulting PCRs
|
|
appended to the contract-policy PCRs; resolver return values and
|
|
exceptions unchanged (additive) (REQ-298)
|
|
- `tests/test_stack_ir_policies.py` + `tests/fixtures/stack_ir/` —
|
|
passing IR (all tags + encryption) + failing IR (missing tags, public
|
|
ingress, plaintext bucket); each policy in isolation + full dir as
|
|
bundle; `pytest.skip("kj not installed")` when `which kj` absent
|
|
(REQ-299)
|
|
|
|
**Vertical slice:** A consumer contract passes through the resolver
|
|
and produces two PCR lists (contract policies pre-resolve, stack-IR
|
|
policies post-resolve) that feed the confidence signal. A contract
|
|
with a bad `id` or missing tags produces `fail` PCRs that lower the
|
|
confidence score. The resolver's existing tests pass unchanged (the
|
|
policy call is additive — it does not change resolver return values
|
|
or exceptions).
|
|
|
|
**Files touched:**
|
|
- `adapters/kyverno-json/policies/contract/require-id-pattern.json` (NEW)
|
|
- `adapters/kyverno-json/policies/contract/require-env-in-enum.json` (NEW)
|
|
- `adapters/kyverno-json/policies/contract/require-infrastructure-min-1.json` (NEW)
|
|
- `adapters/kyverno-json/policies/contract/forbid-unknown-fields.json` (NEW)
|
|
- `adapters/kyverno-json/policies/stack-ir/require-tagging-standard.json` (NEW)
|
|
- `adapters/kyverno-json/policies/stack-ir/forbid-public-ingress.json` (NEW)
|
|
- `adapters/kyverno-json/policies/stack-ir/require-encryption-by-default.json` (NEW)
|
|
- `core/contract_resolver.py` (MODIFIED — pre/post resolve engine calls)
|
|
- `tests/test_stack_ir_policies.py` (NEW)
|
|
- `tests/fixtures/stack_ir/passing.json` (NEW)
|
|
- `tests/fixtures/stack_ir/failing.json` (NEW)
|
|
|
|
**Verification:** `pytest tests/test_contract_resolver.py
|
|
tests/test_stack_ir_policies.py tests/test_policy_engine.py -v`
|
|
(existing resolver tests pass; new policy tests pass or skip-without-
|
|
kj). `python3 -m py_compile core/contract_resolver.py`.
|
|
|
|
---
|
|
|
|
### Phase P3 — plan-JSON policies + meta-orchestration + pipeline wiring (Wave 3, policy-engineer + backend-engineer)
|
|
|
|
**Type:** `feat` (plan-JSON policies + meta-policies + run_platform.sh wiring + tests)
|
|
|
|
**Requirements:** REQ-300, REQ-301, REQ-302, REQ-303
|
|
|
|
**Must-haves:**
|
|
- `adapters/kyverno-json/policies/plan-json/` — 3 policies over
|
|
`terraform show -json` output: `forbid-plaintext-secrets.json` (ports
|
|
CKV_AWS_41/45/46), `forbid-iam-wildcard.json` (ports CKV_AWS_1/40),
|
|
`require-kms-reference.json` (ports CKV_AWS_7/33); JMESPath over
|
|
`planned_values.root_module.resources[]` (REQ-300)
|
|
- `run_platform.sh` Step 5 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 concatenated and fed to the confidence signal;
|
|
`nova.policy.evaluated` event with both engine names; when
|
|
`which kj` is false, logs and proceeds with Checkov/Wiz list only
|
|
(no hard failure) (REQ-301)
|
|
- `tests/test_plan_json_policies.py` + `tests/fixtures/plan_json/` —
|
|
passing plan (no secrets, no wildcard, KMS alias) + failing plan
|
|
(plaintext password, `Action: "*"`, inline KMS key); policies in
|
|
isolation + bundle; `tests/test_run_platform_plan_json_policies.py`
|
|
asserts `run_platform.sh` has the kyverno-json Step 5 block +
|
|
concatenates PCR lists (script-substring assertion, pattern from
|
|
`tests/test_pipeline.py:79-95`) (REQ-302)
|
|
- `adapters/kyverno-json/policies/meta/` — `block-on-any-critical.json`
|
|
(asserts no PCR in merged list has `severity: critical` + `result:
|
|
fail`; if any does, emits `fail` PCR `KJ_META_BLOCK_CRITICAL`
|
|
severity `critical` — declarative source of truth; the
|
|
`confidence_signal.py` hard-override stays as defense-in-depth per
|
|
D-119) + `tagging-rules-agree.json` (cross-checks Checkov
|
|
`NOVA_TAG_NAMING` vs kj `KJ_REQUIRE_TAGGING_STANDARD` by
|
|
`resourceRef`; divergence emits `error` PCR per D-118);
|
|
`tests/test_meta_policies.py` (REQ-303)
|
|
|
|
**Vertical slice:** `run_platform.sh` Step 5 produces a merged PCR list
|
|
(Checkov/Wiz + kj plan-JSON policies + kj meta-policies over the
|
|
merged list) that feeds the confidence signal. A plan with a plaintext
|
|
secret produces two `fail` PCRs (one Checkov, one kj) for the same
|
|
resource — visible defense-in-depth. A critical finding anywhere
|
|
produces a `KJ_META_BLOCK_CRITICAL` meta-PCR that the confidence
|
|
signal's hard-override blocks. The pipeline runs with or without `kj`
|
|
(graceful skip).
|
|
|
|
**Files touched:**
|
|
- `adapters/kyverno-json/policies/plan-json/forbid-plaintext-secrets.json` (NEW)
|
|
- `adapters/kyverno-json/policies/plan-json/forbid-iam-wildcard.json` (NEW)
|
|
- `adapters/kyverno-json/policies/plan-json/require-kms-reference.json` (NEW)
|
|
- `adapters/kyverno-json/policies/meta/block-on-any-critical.json` (NEW)
|
|
- `adapters/kyverno-json/policies/meta/tagging-rules-agree.json` (NEW)
|
|
- `scripts/run_platform.sh` (MODIFIED — Step 5 kj parallel pass)
|
|
- `tests/test_plan_json_policies.py` (NEW)
|
|
- `tests/test_meta_policies.py` (NEW)
|
|
- `tests/test_run_platform_plan_json_policies.py` (NEW)
|
|
- `tests/fixtures/plan_json/passing.json` (NEW)
|
|
- `tests/fixtures/plan_json/failing.json` (NEW)
|
|
|
|
**Verification:** `pytest tests/test_plan_json_policies.py
|
|
tests/test_meta_policies.py tests/test_run_platform_plan_json_policies.py
|
|
tests/test_pipeline.py -v` (new tests pass or skip-without-kj; existing
|
|
pipeline tests pass). `python3 -m py_compile` on any modified Python.
|
|
Shellcheck on `run_platform.sh` if available.
|
|
|
|
---
|
|
|
|
### Phase P4 — regression-gate policies + docs (Wave 4, policy-engineer + data-engineer + lead-developer)
|
|
|
|
**Type:** `feat` (regression policies) + `docs` (adapter READMEs + ARCHITECTURE + STANDARDS + METRICS)
|
|
|
|
**Requirements:** REQ-304, REQ-305, REQ-306, REQ-307
|
|
|
|
**Must-haves:**
|
|
- `adapters/kyverno-json/policies/regression/` — 3 policies over
|
|
capability-inventory JSON frontmatter: `cap-013-adapter-dedup.json`,
|
|
`cap-023-metrics-collector.json`, `cap-024-deck-structure.json`;
|
|
emit `pass`/`fail` PCRs per capability; the existing
|
|
`core/regression_verify.py` is kept (drives the CI gate); the
|
|
policies are the declarative mirror (REQ-304)
|
|
- `tests/test_regression_policies.py` +
|
|
`tests/fixtures/capability_inventory/clean.json` +
|
|
`tests/fixtures/capability_inventory/drifted.json` — clean (all caps
|
|
pass) + drifted (duplicate adapter, missing metric status, broken
|
|
deck arc); regression gate still 287/287 baseline (new tests
|
|
additive, skip-without-kj) (REQ-305)
|
|
- `adapters/README.md` gains new kyverno-json adapter row + "Policy
|
|
Engine Protocol" section (Protocol, registry, swap boundary,
|
|
how-to-add-OpaEngine); `adapters/kyverno-json/README.md` documents
|
|
the engine, install path, policy directory layout, 4 policy
|
|
categories (REQ-306)
|
|
- `.ciagent/ARCHITECTURE.md` §12.7 (added in RESEARCH) is finalized;
|
|
`schemas/README.md` notes `engine: "kyverno"` shared by K8s adapter
|
|
+ kj (distinguished by `ruleId` prefix); `modules/STANDARDS.md`
|
|
gains "Policy authoring standard" section for module owners;
|
|
`docs/METRICS.md` notes the policy engine is swappable (Strategic
|
|
Objective #2 — provable trust via a replaceable substrate) (REQ-307)
|
|
|
|
**Vertical slice:** The regression gate's capability checks are now
|
|
declarative policies auditable as artifacts. A new module owner can
|
|
read `modules/STANDARDS.md` "Policy authoring standard" and write a
|
|
per-module kyverno-json policy. A new engineer can read
|
|
`adapters/README.md` "Policy Engine Protocol" and implement an
|
|
`OpaEngine`. The 287/287 baseline is unchanged.
|
|
|
|
**Files touched:**
|
|
- `adapters/kyverno-json/policies/regression/cap-013-adapter-dedup.json` (NEW)
|
|
- `adapters/kyverno-json/policies/regression/cap-023-metrics-collector.json` (NEW)
|
|
- `adapters/kyverno-json/policies/regression/cap-024-deck-structure.json` (NEW)
|
|
- `tests/test_regression_policies.py` (NEW)
|
|
- `tests/fixtures/capability_inventory/clean.json` (NEW)
|
|
- `tests/fixtures/capability_inventory/drifted.json` (NEW)
|
|
- `adapters/README.md` (MODIFIED — new row + PolicyEngine Protocol section)
|
|
- `adapters/kyverno-json/README.md` (NEW)
|
|
- `schemas/README.md` (MODIFIED — engine enum note)
|
|
- `modules/STANDARDS.md` (MODIFIED — Policy authoring standard section)
|
|
- `docs/METRICS.md` (MODIFIED — swappable engine narrative)
|
|
|
|
**Verification:** `pytest tests/test_regression_policies.py
|
|
tests/test_kyverno_json_engine.py -v` (new tests pass or skip-without-
|
|
kj). Full regression gate `pytest tests/` still at 287/287 baseline +
|
|
new tests (skip without kj). Manual read of `adapters/README.md` +
|
|
`adapters/kyverno-json/README.md` + `modules/STANDARDS.md` policy
|
|
section for clarity.
|
|
|
|
---
|
|
|
|
### Phase P5 — final review + audit + milestone ship (Final Phase)
|
|
|
|
**Type:** `docs` (review + audit + milestone completion)
|
|
|
|
**Requirements:** All REQ-291..309 (mark complete)
|
|
|
|
**Must-haves:**
|
|
- `ciagent-review` multi-persona code review across P1..P4
|
|
(lead-developer, backend-engineer, data-engineer, policy-engineer).
|
|
Auto-fix P0; flag P1+ for post-hoc review. If P1+ issues found, fix
|
|
them in this final phase (not loop back to EXECUTE).
|
|
- `ciagent-audit` — reconstruction test (git log ↔ `.ciagent/` files),
|
|
`.ciagent/` file discipline, branch hygiene, commit discipline.
|
|
Critical issues fixed in this phase.
|
|
- `ciagent-ship` (milestone) — merge `phase/05-final-review-ship` →
|
|
`milestone/v1.25-kyverno-json` → `main`; tag `v1.24.5` (= the v1.25
|
|
release per the prev-minor tagging rule); create Gitea release with
|
|
full milestone summary (all phases, all requirements); delete all
|
|
milestone branches (local + remote).
|
|
- Update `REQUIREMENTS.md` (mark REQ-291..309 complete),
|
|
`ROADMAP.md` (mark v1.25 complete), `CHECKPOINT.json`
|
|
(milestone_complete: true), `NORTH_STAR.md` (note Strategic
|
|
Objective #2 — provable trust via a replaceable policy-engine
|
|
substrate).
|
|
|
|
**Vertical slice:** The v1.25 milestone is complete: kyverno-json is
|
|
the primary policy tool, behind a swappable adapter, with policies
|
|
over all 4 Nova artifacts. Tags v1.24.0..v1.24.5 on the v1.24.x line.
|
|
The milestone branch merges to main.
|
|
|
|
**Verification:** `pytest tests/ -v` full suite passes (287 baseline +
|
|
new tests). `git log --oneline` shows the v1.25 phase commits.
|
|
`git tag` shows v1.24.0..v1.24.5. `git branch` shows no leftover
|
|
milestone/phase branches (all deleted post-ship).
|
|
|
|
---
|
|
|
|
## Wave ordering (parallelization)
|
|
|
|
With `parallelization.enabled: true`, `max_concurrent_agents: 5`,
|
|
`min_plans_for_parallel: 2`:
|
|
|
|
- **P1 Wave 1:** backend-engineer (protocol + registry + install) ‖
|
|
data-engineer (config.json policy object) ‖ policy-engineer (engine
|
|
adapter + smoke policy). 3 concurrent personas. Merge in order:
|
|
data-engineer → backend-engineer → policy-engineer.
|
|
- **P2 Wave 2:** policy-engineer (contract + stack-IR policies) ‖
|
|
backend-engineer (resolver wiring — depends on P1 registry). 2
|
|
concurrent. Merge: policy-engineer → backend-engineer (wiring
|
|
references the policy dirs).
|
|
- **P3 Wave 3:** policy-engineer (plan-JSON + meta policies) ‖
|
|
backend-engineer (run_platform.sh wiring — depends on P1 engine +
|
|
P2 resolver pattern). 2 concurrent. Merge: policy-engineer →
|
|
backend-engineer.
|
|
- **P4 Wave 4:** policy-engineer (regression policies) ‖ data-engineer
|
|
(capability-inventory fixtures) ‖ lead-developer (docs: READMEs,
|
|
STANDARDS, METRICS). 3 concurrent. Merge: data-engineer →
|
|
policy-engineer → lead-developer.
|
|
|
|
Territory enforcement: `warn` mode (per `config.json
|
|
personas.territory_enforcement: "warn"`). Cross-territory edits
|
|
(e.g., backend-engineer touching a policy file) emit a warning, not a
|
|
block.
|
|
|
|
## Requirement → phase → persona matrix
|
|
|
|
| REQ | Phase | Primary persona | Type |
|
|
|-----|-------|-----------------|------|
|
|
| REQ-291 | P1 | backend-engineer | feat |
|
|
| REQ-292 | P1 | data-engineer | feat (config) |
|
|
| REQ-293 | P1 | policy-engineer | feat |
|
|
| REQ-294 | P1 | backend-engineer | feat (install) |
|
|
| REQ-295 | P2 | policy-engineer | feat |
|
|
| REQ-296 | P2 | backend-engineer | feat (wiring) |
|
|
| REQ-297 | P2 | policy-engineer | feat |
|
|
| REQ-298 | P2 | backend-engineer | feat (wiring) |
|
|
| REQ-299 | P2 | policy-engineer | test |
|
|
| REQ-300 | P3 | policy-engineer | feat |
|
|
| REQ-301 | P3 | backend-engineer | feat (pipeline) |
|
|
| REQ-302 | P3 | policy-engineer + backend-engineer | test |
|
|
| REQ-303 | P3 | policy-engineer | feat (meta) |
|
|
| REQ-304 | P4 | policy-engineer | feat |
|
|
| REQ-305 | P4 | policy-engineer + data-engineer | test |
|
|
| REQ-306 | P4 | policy-engineer + lead-developer | docs |
|
|
| REQ-307 | P4 | lead-developer | docs |
|
|
| REQ-308 | P1 | backend-engineer | test |
|
|
| REQ-309 | P1 | policy-engineer | test | |