Skip to content

Policy-as-Code Governance (D5)

Next-Gen 40 · feature D5 · ADR 0029 · spec design/vision/specs/D5-policy-as-code.md

D5 puts a PolicyEngine seam in front of every governed decision — promotion, approvals, GPU budget, model-card completeness, tenancy, supply-chain — so those decisions consult one versioned, signed, per-tenant policy bundle instead of bespoke if statements scattered across the code. Decisions take structured input and return {allow, reasons}, are explainable via a dry-run, and are audited.

This layers on top of the existing declarative policy engine (examlops.policy, ADR 0079) — it does not replace it. With no OPA installed and no policy file, everything still works: the default engine is the YAML core and the default effect is allow.

The engine seam

from examlops.policy_engine import PolicyInput, evaluate

decision = evaluate("promotion", PolicyInput(
    action="promote", subject="alice", resource="JPCP/17", tenant="acme",
    context={"rmse_new": 4.1, "rmse_prod": 5.0},
))
decision.allow      # True/False
decision.reasons    # human-readable
decision.effect     # allow | deny | require_approval
Engine When Notes
YamlPolicyEngine default delegates to examlops.policy.decide (policy.yaml)
RegoPolicyEngine EXAMLOPS_POLICY_ENGINE=opa and opa on PATH shells to opa eval; degrades to YAML if OPA absent

Fail-open vs fail-closed (R4)

Security-critical decisions fail closed (deny) if the engine errors; the rest run in a monitor spirit (fail-open) so a policy bug can't take down bookkeeping:

fail-closed: supply_chain · deploy · budget · tenancy
fail-open  : promotion · approval · model_card · …

A policy.yaml that does not parse is not "no policy"

~/.config/examlops/policy.yaml (the declarative layer read by examlops.policy.decide) defaults to allow when it yields no rules. That is right for a file that is absent — nobody has asked for a gate — but it means an unparsable file silently removes every rule you wrote, including a human-approval gate on an autopilot promote. Fail-open here is deliberate (a broken file must not wedge a mutation path) but it is no longer silent:

  • exa policy list exits 1 and names the file and the parse error. It used to report a file that was right there as absent, which sends you looking in the wrong place entirely.
  • exa policy test <action> prints the same warning above the decision — otherwise it answers "allow — no matching policy", which is true of the rules that loaded and deeply misleading about the rules you actually wrote.
  • Every other caller goes through _load_policies, which logs a WARNING on the examlops.policy logger and carries on.

An empty file is not an error (it is a legitimate "no policies"), but a file whose top-level key is wrong — rules: instead of policies: — is: it parses cleanly and gates nothing.

Check it after every edit:

exa policy list && echo "policy file is live"

Domain gates — built-in default-deny a bundle can only tighten

from examlops.policy_engine import supply_chain_gate, budget_gate, card_gate

supply_chain_gate("JPCP", "17", signed=False)   # DENY — unsigned artifact (GWT-4)
budget_gate(gpu_hours_requested=100, budget_gpu_hours=50)   # DENY — over budget (GWT-3)
card_gate("JPCP", completeness=0.5, floor=0.8)  # DENY — model card too incomplete

These encode safety that a policy bundle can make stricter but never loosen — an unsigned artifact is always denied deployment regardless of what the YAML/Rego says.

Dry-run explainability

exa policy eval promotion --action promote --resource JPCP/17 \
    --set rmse_new=4.1 --set rmse_prod=5.0
# promotion: deny [yaml] — matched policy rule 'no-prod-regression' → deny

--dry-run (the default) explains without auditing; --enforce audits the decision.

Signed, versioned, per-tenant bundles

The effective policy for a tenant is the base ~/.config/examlops/policy.yaml plus an optional policy.<tenant>.yaml overlay. Sign it (D3 HMAC) so its content is tamper-evident and versioned:

exa policy bundle sign --tenant acme
# Signed policy bundle acme v1 (hash 3f9c…)

exa policy bundle verify --tenant acme
# Policy bundle acme is valid.

exa policy bundle list

verify recomputes the content hash and checks the signature, flagging a tampered or unsigned bundle (and exits 1 for CI).

Auditing (R7)

Every evaluate (unless --dry-run) and every bundle sign writes an audit_events row — scoped to the tenant — so the D4 tamper-evident trail records who decided what, and why.

  • examlops.policy (ADR 0079) — the YAML evaluation core this seam wraps.
  • D3 supply-chain — the HMAC key that signs bundles + the unsigned-deny gate.
  • D4 audit — every decision is a hash-chained audit event.
  • D6 tenancy — per-tenant overlays.
  • C3/C6/C8 — the promotion/SLO/fairness signals policies gate on.