Skip to content

OPA/Rego Operator Workflow

This guide explains how to author, validate, and deploy builtin.opa_rego policies for the Guardian hook runtime.

For design background, see:

builtin.opa_rego is a first-party premade plugin. Operators provide Rego policy and static JSON data; the gateway provides the policy engine.

V1 behavior:

  • Plugin ID: builtin.opa_rego
  • Runtime: premade
  • Supported hook: proxy.pre
  • Runtime source of truth: the active plugin snapshot
  • Local Rego and JSON files: authoring inputs only

The gateway does not read policy files from disk at request time and does not fetch policy from a remote OPA service. guardian-plugin materializes local policy files into inline snapshot configuration before deploy.

Snapshot config for builtin.opa_rego is strict JSON with this shape:

{
"entrypoint": "data.guardian.authz.decision",
"modules": [
{
"name": "policy/main.rego",
"source": "package guardian.authz\n\nimport rego.v1\n\n..."
}
],
"data": {
"guardian": {
"allowed_models": ["gpt-4.1", "gpt-4.1-mini"]
}
},
"default_decision": {
"allow": true
},
"input_options": {
"include_body": false,
"include_resolved_cid": false,
"cid_header_names": ["x-viper-state-cid"]
}
}

Required fields:

  • entrypoint: OPA data reference evaluated for the Guardian decision.
  • modules: non-empty list of Rego modules with unique name values.

Optional fields:

  • data: static JSON object exposed to Rego as data.
  • default_decision: Guardian decision used when the entrypoint is undefined.
  • input_options.include_body: include request body input when request.body.read is granted.
  • input_options.include_resolved_cid: include host-mediated blob-store CID resolution input when cid.resolve is granted.
  • input_options.cid_header_names: CID-bearing request headers to resolve. Supported names are x-viper-state-cid and x-viper-credential-cid (or the deprecated x-viper-verifiable-credential-cid during migration).

If default_decision is omitted, the runtime default is {"allow": true}.

Install or run the CLI from the repository root:

Terminal window
cargo install --path crates/guardian-plugin --force
# or
cargo run -p guardian-plugin -- --help

guardian-plugin validate requires a local OPA CLI. It uses OPA_BIN when set, otherwise opa on PATH.

Terminal window
opa version
# or
OPA_BIN=/path/to/opa cargo run -p guardian-plugin -- validate

Create a starter layout:

Terminal window
cargo run -p guardian-plugin -- init --template opa-rego guardian.plugin.toml

The OPA authoring section in guardian.plugin.toml points at local files:

[deployment.entries.opa]
module_files = ["policy/main.rego"]
module_globs = ["policy/*.rego"]
data_files = ["policy/data.json"]

These paths are resolved relative to the manifest directory and must stay inside that directory. During materialize, validate, and deploy, the CLI reads those files and writes inline modules and data into the snapshot config. Local file paths are not persisted as runtime dependencies.

Inspect the final deployment entries before deploy:

Terminal window
cargo run -p guardian-plugin -- \
--manifest docs/examples/plugins/premade/opa-rego/model-allowlist/guardian.plugin.toml \
materialize

Validate locally:

Terminal window
cargo run -p guardian-plugin -- \
--manifest docs/examples/plugins/premade/opa-rego/model-allowlist/guardian.plugin.toml \
validate

Deploy and activate:

Terminal window
cargo run -p guardian-plugin -- \
--manifest docs/examples/plugins/premade/opa-rego/model-allowlist/guardian.plugin.toml \
deploy --activate

New policies should usually start in observability mode. Review audit and plugin decision summaries, then change the deployment entry to enforcement when the policy is ready to block traffic.

OPA evaluates a host-normalized input document. Common fields include:

{
"hook": {
"name": "proxy.pre",
"correlation_id": "req_123",
"timestamp_unix_ms": 1776123456789
},
"request": {
"method": "POST",
"provider": "openai",
"operation": "chat_completions",
"gateway_path": "/v1/openai_api/v1/chat/completions",
"upstream_path": "/v1/chat/completions",
"gateway_uri": "/v1/openai_api/v1/chat/completions",
"upstream_url": "https://api.openai.com/v1/chat/completions",
"query": "",
"headers": {
"content-type": ["application/json"],
"x-viper-state-cid": ["bafybei-example"]
},
"stream_requested": false,
"stream_observed": false
},
"identity": {
"agent_id": "agent:demo",
"signer_key_id": "did:key:z6Mk..."
},
"context": {
"scanner.result": "clean"
}
}

When request body input is enabled and granted, input.body may contain:

{
"present": true,
"truncated": false,
"size_bytes": 42,
"content_type": "application/json",
"json": {
"model": "gpt-4.1"
}
}

When CID resolution is enabled and granted, input.resolved_cids may contain:

[
{
"header_name": "x-viper-state-cid",
"cid": "bafybei-example",
"found": true,
"request_referenced": true,
"verified_by_host": true,
"truncated": false,
"size_bytes": 35812,
"json": {
"agent_version": "1.2.3"
}
}
]

Header names are lower-cased. CID resolution is grant-gated by cid.resolve and reads from the gateway blob store; it is not direct plugin network access. request_referenced reports whether the CID was part of the request context, but it is metadata rather than a resolution allowlist. Body and CID payloads expose parsed json when possible, text when valid UTF-8, and explicit truncated and size_bytes metadata.

The configured entrypoint must evaluate to either a Guardian decision object or undefined. Undefined results use default_decision.

Decision object fields:

  • allow: required boolean when a decision object is returned.
  • status_code: optional non-negative integer.
  • code: optional machine-readable string.
  • message: optional operator/user-readable string.
  • tags: optional object of string values.
  • context_writes: optional object of string values for later plugins.
  • emitted_events: optional event list with required name.
  • terminate_stream: optional boolean.

Example deny decision:

{
"allow": false,
"status_code": 403,
"code": "policy_denied",
"message": "request denied by OPA/Rego policy",
"tags": {
"policy": "model_allowlist"
}
}

The OPA/Rego plugin can use host-mediated capabilities only when both config and snapshot grants allow them.

Supported V1 capability inputs:

  • request.body.read: required for input.body.
  • cid.resolve: required for input.resolved_cids.

Not supported by builtin.opa_rego in V1:

  • http.request
  • Raw network access
  • Runtime file reads
  • Request or response mutation

Use an earlier plugin for remote enrichment, write a compact result into hook context, and let OPA/Rego make the final admission decision from input.context.

builtin.opa_rego participates in the standard hook runtime.

  • Explicit deny in enforcement mode blocks the request.
  • Runtime, capability, or invalid decision errors in enforcement mode fail closed with the standard plugin execution error response.
  • Runtime, capability, or invalid decision errors in observability mode fail open and record the plugin error.
  • Invalid snapshot configuration or invalid Rego should be rejected during validation or snapshot activation, before traffic is affected.

Checked-in starter examples:

Validate each example with:

Terminal window
cargo run -p guardian-plugin -- \
--manifest docs/examples/plugins/premade/opa-rego/model-allowlist/guardian.plugin.toml \
validate

Then inspect the materialized payload:

Terminal window
cargo run -p guardian-plugin -- \
--manifest docs/examples/plugins/premade/opa-rego/model-allowlist/guardian.plugin.toml \
materialize

V1 intentionally does not support:

  • Remote OPA or OPA sidecar execution.
  • Remote policy bundles or per-request policy fetches.
  • Runtime reads from local policy files.
  • Rego outbound network calls such as http.send.
  • Non-deterministic Rego builtins.
  • Request or response mutation.
  • Hooks other than proxy.pre.
  • Full response-body policy evaluation.

Future versions may add signed bundle artifacts, more hooks, richer debugging, or additional normalized inputs after the snapshot-managed V1 workflow is proven.