OPA/Rego Policies
Write your own allow and deny rules in Rego and run them on every agent request through the Agent Gateway.
The OPA/Rego plugin lets you write your own rules in Rego, the policy language of Open Policy Agent, and have the Agent Gateway evaluate them on every request before it reaches the model provider. The gateway runs the policy engine for you. You supply the policy and any static data, and the guardian-plugin CLI packages them into a configuration.
When to use OPA/Rego
Use OPA/Rego for deterministic rules the built-in plugins do not cover: allowing only approved models or providers, admitting a request only when an earlier plugin wrote a specific result, or requiring values in the agent’s verified state record. If a rule needs outside calls or changing state, write a custom plugin instead.
Policy project layout
A policy lives in its own folder, usually in a git repository:
model-allowlist/
guardian.plugin.toml manifest: scope, mode, and which files to load
policy/
main.rego one or more Rego files
data.json optional static data, exposed as data.*When you validate or apply, the CLI reads the files the manifest lists and embeds them in the configuration, so the gateway never reads files at request time.
A minimal manifest
[package]
plugin_id = "builtin.opa_rego"
name = "model-allowlist"
version = "v0.1.0"
abi_version = "v1"
[control_plane]
base_url = "https://control-plane.example.com/api" # your Control Plane URL
[definition]
runtime = "premade"
supported_hooks = ["proxy.pre"]
default_mode = "enforcement"
required_capabilities = []
[configuration]
scope = "global" # or "project" with project = "Your Project Name"
[[deployment.entries]]
plugin_id = "builtin.opa_rego"
hook = "proxy.pre"
order_index = 0
plugin_version = "v0.1.0"
mode = "observability" # start here, switch to "enforcement" later
timeout_ms = 50
[deployment.entries.granted_capabilities."request.body.read"]
max_bytes = 65536
[deployment.entries.config]
entrypoint = "data.guardian.model_allowlist.decision"
[deployment.entries.config.input_options]
include_body = true
[deployment.entries.opa]
module_files = ["policy/main.rego"]
data_files = ["policy/data.json"]If you keep configurations in a separate guardian.config.toml, put the [configuration] table and the deployment entry there instead. See Policy as Code.
A minimal policy
This policy denies every request unless the requested model is on the list.
package guardian.model_allowlist
import rego.v1
default decision := {
"allow": false,
"status_code": 403,
"code": "model_not_allowed",
"message": "this model is not approved for agents",
}
decision := {"allow": true} if {
input.body.json.model in data.guardian.allowed_models
}policy/data.json:
{ "guardian": { "allowed_models": ["gpt-4.1", "claude-sonnet-4-5"] } }Always define a default decision in the policy itself. If your entrypoint produces no result, the gateway falls back to the entry’s default_decision setting, and if that is not set either, it allows the request. For a fail-safe setup, set default_decision to deny in the manifest as well:
[deployment.entries.config.default_decision]
allow = false
status_code = 403
code = "policy_no_result"
message = "policy produced no decision"The input your policy sees
Your policy reads the request through input:
input.hook: the stagename, the request’scorrelation_id, andtimestamp_unix_ms.input.request:method,provider,operation,gateway_path,upstream_path,gateway_uri,upstream_url,query,headers,stream_requested, andstream_observed. Header names are lower-cased, and each header holds a list of values, so read one asinput.request.headers["content-type"][0]. Headers arrive unredacted, includingAuthorizationandX-Api-Key, so never copy header values intotags,context_writes, events, or deny messages, all of which end up in the audit trail.input.identity: the verifiedagent_idand thesigner_key_idthat signed the request.input.context: string values written by plugins that ran earlier in the chain, such as project membership results.input.body: the request body, withpresent,content_type,size_bytes,truncated, and eitherjson(when the body parses as JSON) ortext. Present only when you setinclude_body = trueand grantrequest.body.read.input.resolved_cids: the agent state record or credentials the request references, fetched and verified by the gateway. Each item hasheader_name,cid,found,verified_by_host,size_bytes,truncated, andjsonortext. Present only when you setinclude_resolved_cid = true, list the headers to resolve ininput_options.cid_header_names, and grantcid.resolve; withoutcid_header_namesthe list is empty. The supported headers arex-viper-state-cid,x-viper-credential-cid, andx-viper-verifiable-credential-cid.
{
"request": { "method": "POST", "provider": "openai", "operation": "chat_completions" },
"identity": { "agent_id": "urn:uuid:3f6c1a2e-8b4d-4e7a-9c11-2d5b7e9f0a44" },
"context": {},
"body": { "present": true, "truncated": false, "json": { "model": "gpt-4.1" } }
}The decision your policy returns
Your entrypoint returns an object with a required allow (true or false). On a deny, status_code, code, and message set what the agent receives and what the audit trail shows; any you leave out default to 403, plugin_denied, and request denied by plugin policy. A policy can also add tags to the exchange, write context_writes values that later plugins read, and emit events with emitted_events. tags and context_writes map names to string values. Each event needs a name and can carry string tags and an object body. Any other field makes the evaluation fail. terminate_stream is accepted but has no effect, because the policy runs before the provider responds.
Authoring loop
You need the OPA CLI v1.15.2 or later installed, because guardian-plugin uses it to compile your policy against the v1.15.2 capabilities the gateway runs. Set OPA_BIN if opa is not on your PATH.
-
Write or change the Rego and data files.
-
Test locally with a sample input file:
json{ "body": { "json": { "model": "gpt-4.1" } } }bashopa eval --data policy/ --input sample-input.json 'data.guardian.model_allowlist.decision' -
From the policy folder, validate with
guardian-plugin config validate, preview the change withguardian-plugin config plan, and apply it withguardian-plugin config apply. The first time, a platform admin runsguardian-plugin publishin the folder before applying, to register the plugin. Policy as Code covers authentication, scopes, and CI use.
Roll out observe-first
Apply the policy with mode = "observability" first. Requests keep flowing, and each would-be deny is recorded in the Audit Trail. When the results match your intent, change the entry to mode = "enforcement" and run config plan and config apply again. In enforce mode a deny blocks the request, and if the policy fails to evaluate, the request is blocked too.
Limits
- OPA/Rego runs only at Before Proxy (
proxy.pre). It cannot inspect the provider’s response. - The request body is not visible until you opt in with
include_bodyand therequest.body.readgrant. Keepmax_bytesas small as your rule allows; a body over the limit arrives withtruncated: true. - Policies cannot make network calls, read files, or change the request. Built-ins that reach the network or give different answers on each run, such as
http.sendandtime.now_ns, are rejected at validation. - Keep policies small and deterministic. They run on every request, and the entry’s
timeout_msbounds how long each evaluation may take:guardian-pluginaccepts 1 to 5000 ms, and uses 50 ms when you leave it out. An evaluation that runs over counts as a plugin failure, so it blocks the request in enforce mode.
Related
- Policy as Code — validate, plan, and apply the policy, including from CI
- Gateway Policies — built-in plugins, hook stages, and when a custom plugin fits better
- Audit Trail — review would-be denies during an observe-first rollout
- Agent Configuration — the agent state and validation findings a policy can act on