# 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/<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

### 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 "<rid>" { source = "<path>" }` 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 "<rid>" { 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_<adapter_name>.py` paired with `tests/fixtures/<adapter>_fixture.json`.

## Adding a New Adapter

1. Create `adapters/<name>/<name>_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_<name>_adapter.py`) plus a fixture (`tests/fixtures/<name>_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: <owner>/<repo>/.github/workflows/<file>@<ref>` 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.