# Nova Adapters ## Overview Adapters translate the engine-agnostic Target Stack IR to engine-specific formats. The Terraform adapter is the primary adapter (IR → HCL). Policy adapters translate security tool output into normalized `PolicyCheckResult` records that the confidence signal consumes in an engine-agnostic way. ## Existing Adapters | Adapter | Path | Input | Output | Purpose | | --- | --- | --- | --- | --- | | Terraform adapter | `adapters/terraform/adapter.py` | Stack instance JSON | Terraform HCL (`main.tf`, `terraform.tf`, `providers.tf`) | Compiles IR to Terraform | | 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//_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 ### Terraform Adapter Extension (stateless assembler — v1.11 rewrite) > The adapter owns **no module content**. There is no `TYPE_MAP`, no > `INPUT_MAP`, no `OUTPUT_MAP`, and no per-type branch logic (all deleted > in the v1.11 rewrite — the 918-line monolith collapsed to a ~80-line > assembler). Engine-specific shape lives in each L1 module's own > `terraform/` dir (`versions.tf`/`variables.tf`/`locals.tf`/`main.tf`/ > `outputs.tf`); the adapter only assembles them. To extend the Terraform adapter, **do not edit the adapter** — instead: 1. Add an L1 module with a real `terraform/` dir (owning its resource shape, nested HCL blocks, and defaults). 2. Register it in `modules/registry.json` under the module name with its `terraform_dir` path. The adapter reads `registry.json` to find each module's directory. 3. The adapter emits `module "" { source = "" }` blocks at the root, with resolved inputs + wired `ref:` refs between modules. No type-specific translation lives in the adapter. > If you find yourself reaching for a "TYPE_MAP"-style constant, the L1 > module is missing a piece — fix the module, not the adapter. ### Policy Adapter Pattern 1. Define `SEVERITY_MAP` and `RESULT_MAP` dicts that translate the engine's native severity/result vocabulary to the `PolicyCheckResult` enums. 2. Implement `_to_pcr(raw_record, contract_id)` → `PolicyCheckResult` dict. 3. Implement `adapt(input_path, contract_id)` → list of `PolicyCheckResult` dicts. 4. Implement `is_configured()` → bool (env var check) so the platform can skip the adapter when credentials are absent. ## How to Wire an Adapter - **Terraform adapter** — invoked by `scripts/run_platform.sh` Step 3 (`terraform-plan`). - **Checkov adapter** — invoked by `scripts/run_platform.sh` Step 5 (`checkov`). - **Wiz / Kyverno adapters** — optional Steps 5b/5c, run only when the relevant env vars are set. - All policy adapters output records that are validated against `schemas/policy_check_result.schema.json`. ## Dependencies - `jsonschema`, `pyyaml` — used by all adapters for loading and validating inputs. - `boto3` — used by the Wiz adapter for AWS API access. - `checkov` — used by the Checkov adapter to run policy scans. - No external deps for the Terraform adapter (pure Python). ## How to Test Adapters - `tests/test_adapter.py` — Terraform adapter (stateless assembly: registry read, `module "" { source }` emission, `ref:` wiring, outputs). No `TYPE_MAP`/`INPUT_MAP` tests — the adapter owns no type mappings. - `tests/test_checkov_adapter.py` — Checkov adapter. - `tests/test_wiz_adapter.py` — Wiz adapter. - `tests/test_kyverno_adapter.py` — Kyverno adapter. - All adapter tests load fixtures from `tests/fixtures/` and use `moto` for AWS mocking. ## Where to Write Tests - `tests/test_.py` paired with `tests/fixtures/_fixture.json`. ## Adding a New Adapter 1. Create `adapters//_adapter.py`. 2. Implement `adapt()` and (for policy adapters) `is_configured()`. 3. Add the adapter's engine name to the `engine` enum in `schemas/policy_check_result.schema.json` if it is a policy adapter. 4. Write a test (`tests/test__adapter.py`) plus a fixture (`tests/fixtures/_fixture.json`). 5. Add it to `scripts/run_platform.sh` if it is invoked at runtime. 6. Update this README. ## Consumers The Terraform adapter compiles contract IR for consumer estates. The first real consumer estate is now live: | Consumer | Version | Environment | Account | Forge / Adapter | Status | | --- | --- | --- | --- | --- | --- | | `nova-blockchain-exchange` | v0.2 | dev | `581513795199` | inline adapter (see note below) | **live** (pilot apply `blkex-pilot-apply-v0.2`, 2026-08-19) | ### Forge adapter note (SPEC §10 Q1) Forge Actions (the consumer's forge runtime) does **not** support cross-repo `uses:` references — the forge rejects `uses: //.github/workflows/@` with `expected format {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}`. The consumer (`nova-blockchain-exchange`) therefore uses an **inline adapter** in its `deploy.yml`: the workflow does `actions/checkout@v4` on the consumer, then `actions/checkout@v4` `acdl/acdl` @ `ref: v1.25` into `platform/`, and runs `bash platform/scripts/run_platform.sh ...` directly — no `uses:` indirection. The platform's own `.github/workflows/deploy.yml` (this repo) stays as the **GitHub Actions reference implementation** — the reusable `workflow_call` workflow used by GitHub-hosted consumers. The two files share the same contract shape; the only declared difference is the forge/runtime, not the stages or commands. See `.ciagent/ARCHITECTURE.md` §12.8 for the live pilot-estate wiring.