REGO policies (Governance 2.0)
Alongside the YAML constitutional rules that the governor evaluates on every ingested span, AuditTrail carries a second, REGO-style policy surface — "Governance 2.0". It lets you store Open-Policy-Agent-shaped policies per tenant and simulate them against an input document on demand.
Read the honesty section at the bottom first if you're deciding whether this fits your enforcement story: today this is a policy registry plus an on-demand simulator, not an always-on gate. The YAML governor is what runs automatically at ingest.
What it is
A policy is REGO source (or a pointer to an external OPA instance) stored
against your account. You then ask the simulator: "given this input
document, does the policy allow or deny?" The engine answers with a
verdict — allowed (true / false / null), a human-readable
reason, and the source that produced it (builtin, opa, or
error).
There are two evaluation paths (rego_engine.py):
- External OPA — set
external_opa_urlon the policy and the enginePOSTs{"input": ...}to your OPA instance and uses its verdict. This is the production-grade path for tenants who already run OPA. - Built-in subset evaluator — with no OPA URL, a small, deliberately limited evaluator runs a documented REGO subset in-process (no OPA binary required).
The subset never fakes acceptance: a policy that uses REGO features
outside the subset returns allowed = null with
reason = "unsupported REGO construct…" — an unknown verdict, not a
pass.
The built-in REGO subset
The in-process evaluator understands policies shaped like these (both parse cleanly):
package audittrail.simple
default allow = false
allow {
input.cost <= 0.10
input.tool != "send_email"
}package audittrail.simple
deny[msg] {
input.cost > 1.0
msg := "cost exceeded $1"
}Supported constructs:
default allow = true/default allow = false— the fallback verdict when no rule body matches.allow { … }blocks — every condition must hold for the block to flip the verdict to allow.deny[msg] { … }blocks — a matching block denies; themsg := "…"string becomes the reason.- Conditions of the form
input.<field> OP <scalar>, whereOPis one of== != < <= > >=and the scalar is a quoted string, number, or boolean.
Semantics match OPA's: the first matching deny wins, else a matching
allow wins, else the default allow value. Iteration, functions,
comprehensions, joins across documents and any other richer REGO are
not in the subset — use external_opa_url for those.
REST surface
routes/v20.py (policy_router) — prefix /api/v1/policies, auth
require_user_or_apikey (a sk-at-… API key or a session cookie).
Policies are user-scoped: you only ever see and simulate your own.
| Method | Path | Purpose |
|---|---|---|
GET | /policies | List the caller's policies |
POST | /policies | Create a policy (inline REGO or external OPA URL) → 201 |
POST | /policies/simulate | Evaluate a policy against an input doc |
DELETE | /policies/{policy_id} | Delete a policy → 204 |
There is intentionally no update/PATCH and no enable/disable endpoint — a policy is created, listed, simulated, and deleted.
Create a policy
curl -X POST https://your-audittrail-host/api/v1/policies \
-H "Authorization: Bearer sk-at-..." \
-H "Content-Type: application/json" \
-d '{
"name": "cost-ceiling",
"description": "Deny steps over $1",
"rego_source": "package audittrail.simple\ndeny[msg] {\n input.cost > 1.0\n msg := \"cost exceeded $1\"\n}",
"enabled": true
}'Simulate
Simulate a stored policy by policy_id, or pass rego_source inline to
try a draft without saving it. input is the document the policy reads
as input.*:
curl -X POST https://your-audittrail-host/api/v1/policies/simulate \
-H "Authorization: Bearer sk-at-..." \
-H "Content-Type: application/json" \
-d '{
"policy_id": "…",
"input": { "cost": 1.5, "tool": "send_email" }
}'{ "allowed": false, "reason": "cost exceeded $1", "source": "builtin" }The response shape is always { allowed, reason, source }:
allowed—true,false, ornull(could not evaluate).reason— the denymsg, an allow/default note, or the failure detail ("unsupported REGO construct…","OPA eval failed…").source—"builtin","opa", or"error".
What is and isn't enforced today
This is the honest boundary, because it's easy to assume "policy" means "gate":
- Stored policies do not run automatically.
evaluate_policyis called from exactly one place — thePOST /policies/simulateendpoint. No stored policy is evaluated at span ingest, inside the governor, or by the online-evaluation worker. Nothing in AuditTrail blocks, flags, or logs a violation off a stored REGO policy on its own. - The
enabledflag is metadata. You can set it on create and it's returned byGET /policies, but no code consults it —simulateignores it. It's a forward-looking field for an enforcement path that isn't wired yet, not a live on/off switch. - Automatic constitutional enforcement is the YAML governor. The
rules in
rules/*.yaml, evaluated on every ingested span with info / amber / red severities, are what actually govern agents in production. See EU AI Act compliance and the Settings → Rules editor. - The built-in evaluator is a subset. For full REGO you must point at
an external OPA via
external_opa_url; the subset returns an unknown verdict (allowed = null) rather than guessing on anything it doesn't recognise.
Use the simulator to author and unit-test policy logic against sample inputs (or to front your own OPA), and use the YAML governor for the always-on gate. If you want a stored REGO policy to fire automatically against live traces, that enforcement wiring is not in the product yet — track it before you rely on it.