Policy, approval and retention commands

Part of the NovaFabric CLI reference. Both nova and novafabric run the same binary.

Policy and governance (v0.8)

nova policy test

Run the Rego unit test suite on the built-in policy bundle (requires opa installed).

nova policy test
nova policy test --bundle /path/to/custom-bundle

Options:

Returns exit code 0 if all tests pass, 1 if any test fails or opa is not found.

The built-in bundle ships reference gates including promote_gate.rego, regression_gate.rego (v0.9), and budget_gate.rego — the cost/energy budget gate (ADR-0136, experimental).

Budget gate (budget_gate.rego, experimental). Denies promotion when the capsule's already-recorded cost/energy/token rollup exceeds declared ceilings — a promotion gate over sealed evidence, never a live spend alert. The rollup is assembled with novafabric.policy.budget_block_from_capsule(capsule_dir) (reads nova.cost from model-calls.jsonl, measured_joules from energy-receipts.jsonl, and gen_ai.usage.* token counts) and passed as input.resource.budget; ceilings go in input.context.budget_ceilings (total_cost / cost_per_run / energy_kwh / tokens). Missing evidence is never treated as zero: absent ceilings or absent data pass with an explicit "no data" reason (skip_unmeasured, the default), while input.context.missing_evidence: "require_measured" fail-closes on a declared ceiling with no recorded evidence. Spec: design/spec/budget-gate-v0.md (private). The nova policy budget set|list|show authoring commands and the budget-gate verdict record are future design (ADR-0136 P2/P3) — not yet implemented.

Environment-conditioned gates (experimental, ADR-0126 P3). The evidence-export gate's input carries the capsule's recorded deployment environment as input.resource.deployment_environment (the value of nova capture --environment / NOVAFABRIC_ENVIRONMENT, verbatim; null when none was recorded — never inferred), so a custom policy can apply production-only requirements:

allow if {
    input.resource.deployment_environment == "production"
    input.resource.redaction_proof_present == true
    input.resource.unsafe_skips == 0
}

Asset-scoped gates (promotion, protected labels) carry null. The mutating-replay gate does not yet populate the field (planned); no built-in gate conditions on it.


nova policy explain <decision-id>

Replay a past policy decision from the audit log, showing the event type, actor, resource, and details.

nova policy explain 3f8a1b2c-...

The dashboard Policy Explain panel auto-completes decision IDs via GET /api/policy/recent-decisions?limit=50 (serve REST API), which returns up to 50 recent IDs from ~/.novafabric/dashboard-audit.jsonl in most-recent-first order.


nova policy list

List Rego policy files in the built-in bundle and any signed promotion policies stored in the PolicyStore.

nova policy list
nova policy list --namespace staging
nova policy list --bundle /path/to/custom-bundle
nova policy list --db ~/.local/share/novafabric/merkle.db

Options:

Output has two sections:

  1. Rego Bundle — table of .rego files with file name (relative to bundle root) and size.
  2. Signed Promotion Policies — table of stored policy versions with version number, namespace, and creation timestamp. If no DB exists yet, a note is printed and the command exits cleanly.

nova approve <name@version>

Approve an asset that is in the pending_approval lifecycle state, recording the approver and note in the audit log.

nova approve my-model@v2
nova approve my-model@v2 --approver alice --note "Reviewed eval results"

Options:

Exits with code 1 if the asset is not found or not in pending_approval state.


nova hold create <registry>

Place a legal hold on a registry, suspending all capsule deletion (ADR-0031).

nova hold create my-registry --reason "SEC examination 2026-Q2"
nova hold create my-registry --reason "SEC examination 2026-Q2" --duration-days 365

Options:

Holds are stored in .novafabric/registries/<registry>/holds.jsonl (append-only).


nova hold list <registry>

List active (unreleased) legal holds on a registry.

nova hold list my-registry

nova hold release <hold-id>

Release a legal hold by ID. Records the release in the audit log.

nova hold release hold-a1b2c3d4

Exits with code 1 if the hold ID is not found or already released.


nova capsule delete <capsule-id>

Delete a capsule record, subject to retention policy and legal holds.

nova capsule delete cap-01JXXXXX --registry my-registry
nova capsule delete cap-01JXXXXX --registry my-registry --age-days 1826
nova capsule delete cap-01JXXXXX --registry my-registry --force

Options:

Deletion is blocked when:

On success, a signed capsule.delete entry is appended to the audit log.


Retention scheduler (ADR-0134)

Applies the ADR-0031 retention windows over time: a WORM-aware, crypto-shred-integrated, audited sweep. Bindings live in a new optional bindings: block inside the existing .novafabric/registries/<registry>/retention-policy.yaml (schema: schemas/retention-binding.schema.json); a registry with no bindings is swept for nothing. NovaFabric embeds no daemon — run the sweep manually or wire nova retention apply --yes into cron/systemd.

# retention-policy.yaml (ADR-0031 fields unchanged; bindings are additive)
bindings:
  - id: mifid-trade-5y
    match: {tag: trade-record, deployment_environment: production}
    window: P1825D          # ISO-8601 duration from capsule created_at, or YYYY-MM-DD
    action: purge           # expire-metadata | purge | crypto-shred
  - id: gdpr-subject-pii
    match: {tag: contains-pii}
    window: P180D
    action: crypto-shred    # dispatches to the ADR-0069 DEK destruction

Matching reads capsule manifests under the capsule directory: metadata.tags (comma-separated), metadata.deployment_environment, metadata.pii_subject_id (crypto-shred subject), and alias (matched by the asset predicate, glob allowed).

Safety invariants (fixed, not configurable): a WORM/legal hold always wins — a WORM-retained capsule is never purged or shredded before its locked_until (recorded skipped: worm_hold); purge is refused under deletion_mode: prohibited; a crypto-shred inside the Art.17(3)(b) window is deferred. Every decision — including every skip — appends one hash-chained retention.action audit entry (schemas/retention-action-record.schema.json): deletion is itself evidence.

nova retention plan

Dry-run (the default posture): list what WOULD be affected. Touches nothing, writes no audit records, and uses the identical due-computation code path as apply.

nova retention plan --registry my-registry
nova retention plan --registry my-registry --json

nova retention apply

Apply due retention actions. Confirm-gated: prompts unless --yes (or --dry-run). Idempotent and fail-safe — re-runs are no-ops; a per-item failure is recorded error and the sweep continues.

nova retention apply --registry my-registry --dry-run          # preview
nova retention apply --registry my-registry --yes              # cron/CI
nova retention apply -r my-registry --action crypto-shred --limit 100 --yes

Options:

nova retention status

Read-only: per binding, how many items are due now, how many are held (WORM/legal), and the next due date.

nova retention status --registry my-registry --json

nova retention explain <capsule-id>

Read-only: which bindings match this capsule, its computed due date, and its current hold state.

nova retention explain cap-01JXXXXX --registry my-registry