---
title: "OPA/Rego Policies"
description: "Write your own allow and deny rules in Rego and run them on every agent request through the Agent Gateway."
source: "Guardian Platform"
---
The OPA/Rego plugin lets you write your own rules in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), 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](./gateway-policies-and-plugins.mdx#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](./gateway-policies-and-plugins.mdx#custom-plugins) instead.

## Policy project layout

A policy lives in its own folder, usually in a git repository:

```text
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

```toml
[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](./policy-as-code.mdx).

## A minimal policy

This policy denies every request unless the requested model is on the list.

```rego title="policy/main.rego"
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`:

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

```toml
[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 stage `name`, the request's `correlation_id`, and `timestamp_unix_ms`.
- `input.request`: `method`, `provider`, `operation`, `gateway_path`, `upstream_path`, `gateway_uri`, `upstream_url`, `query`, `headers`, `stream_requested`, and `stream_observed`. Header names are lower-cased, and each header holds a list of values, so read one as `input.request.headers["content-type"][0]`. Headers arrive unredacted, including `Authorization` and `X-Api-Key`, so never copy header values into `tags`, `context_writes`, events, or deny messages, all of which end up in the audit trail.
- `input.identity`: the verified `agent_id` and the `signer_key_id` that 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, with `present`, `content_type`, `size_bytes`, `truncated`, and either `json` (when the body parses as JSON) or `text`. Present only when you set `include_body = true` and grant `request.body.read`.
- `input.resolved_cids`: the agent state record or credentials the request references, fetched and verified by the gateway. Each item has `header_name`, `cid`, `found`, `verified_by_host`, `size_bytes`, `truncated`, and `json` or `text`. Present only when you set `include_resolved_cid = true`, list the headers to resolve in `input_options.cid_header_names`, and grant `cid.resolve`; without `cid_header_names` the list is empty. The supported headers are `x-viper-state-cid`, `x-viper-credential-cid`, and `x-viper-verifiable-credential-cid`.

```json
{
  "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](https://www.openpolicyagent.org/docs/latest/#running-opa) 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`.

1. Write or change the Rego and data files.
2. Test locally with a sample input file:

   ```json title="sample-input.json"
   { "body": { "json": { "model": "gpt-4.1" } } }
   ```

   ```bash
   opa eval --data policy/ --input sample-input.json 'data.guardian.model_allowlist.decision'
   ```

3. From the policy folder, validate with `guardian-plugin config validate`, preview the change with `guardian-plugin config plan`, and apply it with `guardian-plugin config apply`. The first time, a platform admin runs `guardian-plugin publish` in the folder before applying, to [register the plugin](./gateway-policies-and-plugins.mdx#register-a-built-in-plugin). [Policy as Code](./policy-as-code.mdx) 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](./gateway-audit-trail.mdx). 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_body` and the `request.body.read` grant. Keep `max_bytes` as small as your rule allows; a body over the limit arrives with `truncated: 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.send` and `time.now_ns`, are rejected at validation.
- Keep policies small and deterministic. They run on every request, and the entry's `timeout_ms` bounds how long each evaluation may take: `guardian-plugin` accepts 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](./policy-as-code.mdx) — validate, plan, and apply the policy, including from CI
- [Gateway Policies](./gateway-policies-and-plugins.mdx) — built-in plugins, hook stages, and when a custom plugin fits better
- [Audit Trail](./gateway-audit-trail.mdx) — review would-be denies during an observe-first rollout
- [Agent Configuration](./agent-configuration.mdx) — the agent state and validation findings a policy can act on