Documentation

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):

  1. External OPA — set external_opa_url on the policy and the engine POSTs {"input": ...} to your OPA instance and uses its verdict. This is the production-grade path for tenants who already run OPA.
  2. 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):

rego
package audittrail.simple
 
default allow = false
 
allow {
    input.cost <= 0.10
    input.tool != "send_email"
}
rego
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; the msg := "…" string becomes the reason.
  • Conditions of the form input.<field> OP <scalar>, where OP is 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.

MethodPathPurpose
GET/policiesList the caller's policies
POST/policiesCreate a policy (inline REGO or external OPA URL) → 201
POST/policies/simulateEvaluate 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

bash
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.*:

bash
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" }
  }'
json
{ "allowed": false, "reason": "cost exceeded $1", "source": "builtin" }

The response shape is always { allowed, reason, source }:

  • allowedtrue, false, or null (could not evaluate).
  • reason — the deny msg, 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_policy is called from exactly one place — the POST /policies/simulate endpoint. 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 enabled flag is metadata. You can set it on create and it's returned by GET /policies, but no code consults it — simulate ignores 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.