Skip to content

RFC: Builtin OPA/Rego Policy Plugin for Guardian Hook System

Status: Draft proposal Last updated: 2026-04-14 Applies to: llm-gateway/service (hook runtime), control-plane/service (plugin definitions + snapshots), and crates/guardian-plugin (premade definition bootstrap) Related RFC: rfc-hook-plugin-system.md

This RFC defines a first-party builtin OPA/Rego policy plugin for the Guardian hook system.

The plugin is identified as builtin.opa_rego and runs as a native Go premade plugin within the existing snapshot-driven hook runtime. It evaluates snapshot-managed Rego modules and static JSON data against a normalized hook input and returns the canonical Guardian hook decision shape.

Customer customization is policy-driven and data-driven. Customers author Rego policy and static data, but do not author or deploy a separate policy-engine plugin binary.

V1 scope is proxy.pre policy evaluation. The design keeps Guardian’s existing execution, capability, audit, and failure semantics unchanged.

For V1, local Rego files are an authoring and tooling convenience, not a runtime dependency. The runtime source of truth remains the active signed snapshot.

  1. Provide a general-purpose policy engine for hook-based enforcement without requiring gateway rebuilds.
  2. Keep policy evaluation inside the existing signed snapshot and deterministic hook execution model.
  3. Support gradual rollout through observability mode before enforcement mode.
  4. Allow policy to evaluate verified request metadata, prior plugin context, and optional host-mediated request/body/CID inputs.
  5. Preserve the existing deny envelope and current hook runtime failure semantics.
  6. Keep policy activation-time validation strict enough to catch syntax and schema issues before request traffic is affected.
  1. Customer-authored policy-engine binaries in V1.
  2. Remote sidecar or RPC-based OPA execution in V1.
  3. Direct outbound network access from Rego policy evaluation.
  4. Request or response mutation.
  5. Full response body policy evaluation in V1.
  6. Per-request policy fetches outside the active snapshot.
  7. Replacing the general hook runtime contract with an OPA-specific contract.

The hook system RFC already establishes:

  • native Go premade plugins as a first-class runtime option
  • deterministic sequential execution with first-deny short-circuit
  • signed snapshots as the sole runtime policy source
  • host-mediated capabilities instead of raw runtime privileges

This RFC applies those rules to a dedicated OPA/Rego builtin plugin rather than treating OPA as a custom WASM guest.

The resulting design is:

  • engine implementation: first-party native Go plugin
  • policy source: snapshot-managed configuration
  • evaluation model: compile policy once per active snapshot, evaluate once per matching request
  • operator control surface: plugin definition, snapshot entry configuration, mode, timeout, and granted capabilities

builtin.opa_rego is specified as a premade plugin rather than a customer-authored WASM plugin for the following reasons:

  1. It matches the architecture already described in the hook system RFC for follow-on first-party plugins.
  2. It keeps the policy engine implementation under the same release, audit, and support model as builtin.authorization.
  3. It avoids embedding an OPA evaluator separately in each marketplace plugin artifact.
  4. It lets customers customize behavior through policy and data while the execution contract stays stable.
  5. It avoids introducing an availability dependency on a separate sidecar or remote policy service.
  6. It keeps policy activation, rollback, and audit aligned with the existing snapshot lifecycle.
  • plugin ID: builtin.opa_rego
  • runtime: premade
  • supported hooks in V1: proxy.pre
  • canonical use: verified authorization and admission policy after core verification and route resolution

The canonical definition should default to enforcement, because the plugin’s purpose is authorization policy. Operators should still roll out new policies in observability mode first and promote to enforcement after review.

The plugin runs in the existing per-hook sequential pipeline and participates in the same order_index ordering as all other plugins. It may consume context written by earlier plugins and may itself short-circuit the hook when it denies in enforcement mode.

V1 policy and data are embedded directly in the snapshot entry configuration JSON. This keeps the active snapshot self-contained and avoids adding new artifact-loading behavior for premade plugins.

V1 configuration includes:

  • one or more Rego modules as strings
  • optional static JSON data
  • one configured decision entrypoint
  • plugin-specific input/options settings
  • a default decision used when the policy decision is undefined

The gateway runtime does not read Rego files from a local directory at request time, and it does not fetch arbitrary policy from a remote policy service during request handling.

V1 should support a simple local authoring workflow through existing Guardian tooling.

Recommended operator workflow:

  1. Author Rego modules and optional JSON data as local files in a repo or working directory.
  2. Point Guardian tooling at those local files.
  3. Have the tooling read the local files and materialize their contents into the plugin configuration JSON for builtin.opa_rego.
  4. Submit that fully materialized configuration through the control-plane definition and snapshot workflow.
  5. Activate the signed snapshot.

Under this model:

  • local files are used during authoring and packaging
  • snapshot config is the runtime payload
  • the gateway only needs the active snapshot in order to compile and evaluate policy

This keeps V1 easy to reason about while still fitting the snapshot-driven runtime model.

More robust packaging is intentionally deferred until after the simple V1 flow is proven.

Likely follow-on packaging options include:

  • signed OPA bundle artifacts referenced from snapshot config
  • remote bundle download or polling
  • dynamic data sources outside the active snapshot

The plugin definition should publish a strict JSON schema, similar to builtin.authorization.

An illustrative configuration shape is:

{
"entrypoint": "data.guardian.authz.decision",
"modules": [
{
"name": "main.rego",
"source": "package guardian.authz\n\n default decision := {\"allow\": true }\n"
}
],
"data": {
"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"]
}
}

Recommended config fields:

  • entrypoint: fully qualified OPA data path to the decision rule
  • modules: ordered list of named Rego modules
  • data: static JSON document exposed as policy data
  • default_decision: fallback Guardian decision when the entrypoint is undefined
  • input_options.include_body: whether to read full request body when capability is granted
  • input_options.include_resolved_cid: whether to resolve configured CID header values when capability is granted
  • input_options.cid_header_names: header names inspected for CID values to resolve

Configuration must be decoded strictly with unknown fields rejected.

The plugin should evaluate Rego against a normalized JSON input document constructed by the host.

The input document should include:

  • request metadata derived from the hook event envelope
  • verified identity fields already established by the host
  • prior plugin context from the envelope
  • optional body data when request.body.read is granted and enabled in config
  • optional CID resolution results when cid.resolve is granted and enabled in config

Illustrative shape:

{
"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"
},
"body": {
"present": true,
"truncated": false,
"content_type": "application/json",
"json": {
"model": "gpt-4.1",
"messages": [
{
"role": "user",
"content": "hello"
}
]
}
},
"resolved_cids": [
{
"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"
}
}
]
}

The host should normalize input as follows:

  1. Header names are lower-cased.
  2. Header values remain arrays.
  3. Request body and resolved CID content are exposed as parsed JSON when possible.
  4. When parsed JSON is unavailable but data is valid UTF-8, the host may expose text instead.
  5. Large raw binary payloads should not be copied directly into the policy input in V1.
  6. Truncation and size metadata must be surfaced explicitly so policy can distinguish complete from partial inputs.

The configured Rego entrypoint must evaluate to either:

  • a single object that maps to the Guardian hook decision contract, or
  • undefined, in which case the host uses default_decision

The decision object maps to the existing hook decision fields:

  • allow
  • status_code
  • code
  • message
  • tags
  • context_writes
  • emitted_events

Illustrative decision object:

{
"allow": false,
"status_code": 403,
"code": "policy_denied",
"message": "request denied by rego policy",
"tags": {
"policy.package": "guardian.authz",
"policy.rule": "deny_unverified_state"
},
"context_writes": {
"policy.result": "deny"
},
"emitted_events": [
{
"name": "policy.evaluated",
"tags": {
"result": "deny"
},
"body": {
"rule": "deny_unverified_state"
}
}
]
}

Decision validation rules:

  1. allow is required when a decision object is returned.
  2. Returned fields must match the Guardian decision schema.
  3. Invalid decision objects are plugin execution errors.
  4. Undefined entrypoint results are not execution errors when default_decision is configured.

This plugin uses the same host capability broker as other plugins.

V1 should support:

  • request.body.read
  • cid.resolve

V1 should not use http.request directly from the OPA builtin plugin.

Remote lookups and non-deterministic enrichment are better handled by earlier plugins that write summary data into hook context. The OPA/Rego plugin can then evaluate those derived facts from env.Context without making its own outbound calls.

  1. Capability use remains bounded by snapshot grants and host-enforced limits.
  2. Plugin config enables logical use of a capability, but grants still determine whether the capability call is permitted.
  3. CID resolution is grant-gated, bounded by host-enforced limits, and backed by the gateway blob store. request_referenced is returned as metadata, not as an allowlist.
  4. Body access remains bounded by the configured max bytes in the snapshot entry.
  5. Policy must not treat request-derived CID content as authenticated unless verified_by_host is true.

OPA compilation should happen when a snapshot is compiled and activated, not on every request.

Activation-time work should include:

  • strict config decoding
  • Rego parse and compile
  • entrypoint validation
  • prepared query creation
  • baseline validation of decision/default-decision shape

Per-request work should include:

  • input normalization
  • optional capability reads
  • prepared query evaluation
  • decision validation and mapping to HookDecision

This implies one runtime enhancement for premade plugins: the hook runtime should support snapshot-scoped premade compilation or preparation so activation-time state can be retained in the compiled snapshot.

This plugin inherits the general hook runtime behavior:

  1. Explicit deny in enforcement mode denies the request.
  2. Evaluation/runtime/capability errors in enforcement mode fail closed.
  3. Evaluation/runtime/capability errors in observability mode fail open.
  4. Invalid policy configuration should prevent snapshot activation rather than surfacing first at request time.

Expected enforcement error behavior remains the standard hook runtime behavior:

  • HTTP status defaults to 503
  • code defaults to plugin_execution_error
  • message defaults to a generic plugin execution failure

The plugin should emit the same runtime summaries and hook event records as other plugins.

Recommended tags and event fields include:

  • policy package or entrypoint
  • decision outcome
  • matched rule identifier when policy exposes one
  • whether body input was present or truncated
  • whether CID input was present, found, verified, or truncated

The plugin should not emit entire request bodies or resolved CID payloads by default in tags or events.

The following integration points are required:

  1. A builtin definition helper in shared/servicekit/plugins, parallel to builtin.authorization.
  2. A registered premade plugin implementation in llm-gateway/service/internal/hooks.
  3. A guardian-plugin CLI bootstrap case for builtin.opa_rego.
  4. Strict config schema validation during definition and snapshot workflows.
  5. Snapshot entry support for granted capabilities exactly as with other plugins.

V1 developer tooling should optimize the local-authoring-to-materialized-snapshot workflow without making local files part of the runtime contract.

Recommended V1 tooling behavior:

  1. guardian-plugin should allow operators to reference local Rego module files, optional globs, and optional local JSON data files.
  2. Tooling should expand those local sources into inline configuration JSON before the snapshot is created or updated.
  3. Tooling should provide a render or materialize style command so operators can inspect the final inline config before deploy.
  4. validate should compile Rego, validate JSON data, and report source-file-aware errors before submitting the resulting config.
  5. init should be able to scaffold a minimal OPA policy layout and manifest wiring for a new builtin policy plugin.
  6. The control plane should persist the resulting materialized config, not the original local file paths.

Recommended implementation slices:

  1. Add builtin plugin constants, schema helpers, and default config helpers in shared/servicekit/plugins.
  2. Add a native Go OPA/Rego plugin implementation in llm-gateway/service/internal/hooks.
  3. Extend the hook runtime with premade activation-time compilation support.
  4. Register the plugin in gateway startup configuration.
  5. Add builtin.opa_rego support to crates/guardian-plugin, including local-file materialization, render/materialize output, and OPA-oriented init scaffolding.
  6. Add control-plane tests for config schema and snapshot validation.
  7. Add gateway tests for allow, deny, undefined decision, invalid decision, capability denial, and fail-closed behavior.

Testing should cover:

  1. strict config decoding and schema validation
  2. Rego compile failures at activation time
  3. successful allow and deny decisions
  4. undefined decision fallback behavior
  5. invalid decision object handling
  6. body-read and CID-resolution input shaping
  7. local-file authoring flow producing the expected materialized inline config and render/materialize output
  8. enforcement fail-closed and observability fail-open behavior
  9. multi-plugin sequencing where earlier plugins write context consumed by OPA policy

This keeps OPA in the custom plugin lane, but it duplicates policy-engine packaging, complicates the supported contract, and weakens the distinction between engine implementation and customer policy content.

This reduces in-process engine work, but it introduces an external availability dependency, weakens snapshot self-containment, and makes policy evaluation depend on network dispatch during the request path.

This is simpler than OPA, but it recreates a less expressive policy system and duplicates functionality already available in Rego.

The following are intentionally deferred:

  • signed bundle artifacts for policy/data
  • snapshot references to packaged policy bundles instead of inline modules
  • support for hooks beyond proxy.pre
  • partial evaluation and policy trace output
  • optional explanation payloads for policy debugging
  • additional normalized inputs for response-stage policy
  1. builtin.opa_rego can be registered as a premade plugin definition and activated in a snapshot.
  2. Snapshot activation rejects invalid Rego modules, invalid entrypoints, and invalid plugin config.
  3. The plugin evaluates in proxy.pre using the standard deterministic hook pipeline.
  4. The plugin returns the existing Guardian decision envelope without introducing a parallel deny format.
  5. The plugin can consume prior plugin context plus optional host-mediated body and CID inputs.
  6. Enforcement mode remains fail-closed and observability mode remains fail-open.
  7. V1 authoring may begin from local Rego and JSON files, but the active runtime uses snapshot-materialized inline policy/data rather than local file paths.
  8. Customer customization is policy-driven and data-driven rather than customer-authored plugin engine code.