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
1. Summary
Section titled “1. Summary”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.
2. Goals
Section titled “2. Goals”- Provide a general-purpose policy engine for hook-based enforcement without requiring gateway rebuilds.
- Keep policy evaluation inside the existing signed snapshot and deterministic hook execution model.
- Support gradual rollout through observability mode before enforcement mode.
- Allow policy to evaluate verified request metadata, prior plugin context, and optional host-mediated request/body/CID inputs.
- Preserve the existing deny envelope and current hook runtime failure semantics.
- Keep policy activation-time validation strict enough to catch syntax and schema issues before request traffic is affected.
3. Non-Goals
Section titled “3. Non-Goals”- Customer-authored policy-engine binaries in V1.
- Remote sidecar or RPC-based OPA execution in V1.
- Direct outbound network access from Rego policy evaluation.
- Request or response mutation.
- Full response body policy evaluation in V1.
- Per-request policy fetches outside the active snapshot.
- Replacing the general hook runtime contract with an OPA-specific contract.
4. Design Position
Section titled “4. Design Position”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
5. Why This Is a Builtin Plugin
Section titled “5. Why This Is a Builtin Plugin”builtin.opa_rego is specified as a premade plugin rather than a customer-authored WASM plugin for the following reasons:
- It matches the architecture already described in the hook system RFC for follow-on first-party plugins.
- It keeps the policy engine implementation under the same release, audit, and support model as
builtin.authorization. - It avoids embedding an OPA evaluator separately in each marketplace plugin artifact.
- It lets customers customize behavior through policy and data while the execution contract stays stable.
- It avoids introducing an availability dependency on a separate sidecar or remote policy service.
- It keeps policy activation, rollback, and audit aligned with the existing snapshot lifecycle.
6. Runtime Placement
Section titled “6. Runtime Placement”6.1 Plugin identity
Section titled “6.1 Plugin identity”- 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
6.2 Mode
Section titled “6.2 Mode”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.
6.3 Ordering
Section titled “6.3 Ordering”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.
7. Policy and Data Source Model
Section titled “7. Policy and Data Source Model”7.1 Runtime source of truth
Section titled “7.1 Runtime source of truth”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.
7.2 V1 authoring flow
Section titled “7.2 V1 authoring flow”V1 should support a simple local authoring workflow through existing Guardian tooling.
Recommended operator workflow:
- Author Rego modules and optional JSON data as local files in a repo or working directory.
- Point Guardian tooling at those local files.
- Have the tooling read the local files and materialize their contents into the plugin configuration JSON for
builtin.opa_rego. - Submit that fully materialized configuration through the control-plane definition and snapshot workflow.
- 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.
7.3 Future packaging model
Section titled “7.3 Future packaging 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
8. Configuration Model
Section titled “8. Configuration Model”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 rulemodules: ordered list of named Rego modulesdata: static JSON document exposed as policy datadefault_decision: fallback Guardian decision when the entrypoint is undefinedinput_options.include_body: whether to read full request body when capability is grantedinput_options.include_resolved_cid: whether to resolve configured CID header values when capability is grantedinput_options.cid_header_names: header names inspected for CID values to resolve
Configuration must be decoded strictly with unknown fields rejected.
9. Input Model
Section titled “9. Input Model”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.readis granted and enabled in config - optional CID resolution results when
cid.resolveis 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" } } ]}9.1 Normalization rules
Section titled “9.1 Normalization rules”The host should normalize input as follows:
- Header names are lower-cased.
- Header values remain arrays.
- Request body and resolved CID content are exposed as parsed JSON when possible.
- When parsed JSON is unavailable but data is valid UTF-8, the host may expose text instead.
- Large raw binary payloads should not be copied directly into the policy input in V1.
- Truncation and size metadata must be surfaced explicitly so policy can distinguish complete from partial inputs.
10. Decision Contract
Section titled “10. Decision Contract”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:
allowstatus_codecodemessagetagscontext_writesemitted_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:
allowis required when a decision object is returned.- Returned fields must match the Guardian decision schema.
- Invalid decision objects are plugin execution errors.
- Undefined entrypoint results are not execution errors when
default_decisionis configured.
11. Capabilities and Trust Boundaries
Section titled “11. Capabilities and Trust Boundaries”This plugin uses the same host capability broker as other plugins.
11.1 V1 capability posture
Section titled “11.1 V1 capability posture”V1 should support:
request.body.readcid.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.
11.2 Capability rules
Section titled “11.2 Capability rules”- Capability use remains bounded by snapshot grants and host-enforced limits.
- Plugin config enables logical use of a capability, but grants still determine whether the capability call is permitted.
- CID resolution is grant-gated, bounded by host-enforced limits, and backed by the gateway blob store.
request_referencedis returned as metadata, not as an allowlist. - Body access remains bounded by the configured max bytes in the snapshot entry.
- Policy must not treat request-derived CID content as authenticated unless
verified_by_hostis true.
12. Activation and Performance Model
Section titled “12. Activation and Performance Model”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.
13. Failure Semantics
Section titled “13. Failure Semantics”This plugin inherits the general hook runtime behavior:
- Explicit deny in enforcement mode denies the request.
- Evaluation/runtime/capability errors in enforcement mode fail closed.
- Evaluation/runtime/capability errors in observability mode fail open.
- 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
14. Observability and Audit
Section titled “14. Observability and Audit”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.
15. Control-Plane and CLI Integration
Section titled “15. Control-Plane and CLI Integration”The following integration points are required:
- A builtin definition helper in
shared/servicekit/plugins, parallel tobuiltin.authorization. - A registered premade plugin implementation in
llm-gateway/service/internal/hooks. - A
guardian-pluginCLI bootstrap case forbuiltin.opa_rego. - Strict config schema validation during definition and snapshot workflows.
- Snapshot entry support for granted capabilities exactly as with other plugins.
15.1 Developer Tooling (V1)
Section titled “15.1 Developer Tooling (V1)”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:
guardian-pluginshould allow operators to reference local Rego module files, optional globs, and optional local JSON data files.- Tooling should expand those local sources into inline configuration JSON before the snapshot is created or updated.
- Tooling should provide a
renderormaterializestyle command so operators can inspect the final inline config before deploy. validateshould compile Rego, validate JSON data, and report source-file-aware errors before submitting the resulting config.initshould be able to scaffold a minimal OPA policy layout and manifest wiring for a new builtin policy plugin.- The control plane should persist the resulting materialized config, not the original local file paths.
16. Implementation Plan
Section titled “16. Implementation Plan”Recommended implementation slices:
- Add builtin plugin constants, schema helpers, and default config helpers in
shared/servicekit/plugins. - Add a native Go OPA/Rego plugin implementation in
llm-gateway/service/internal/hooks. - Extend the hook runtime with premade activation-time compilation support.
- Register the plugin in gateway startup configuration.
- Add
builtin.opa_regosupport tocrates/guardian-plugin, including local-file materialization, render/materialize output, and OPA-oriented init scaffolding. - Add control-plane tests for config schema and snapshot validation.
- Add gateway tests for allow, deny, undefined decision, invalid decision, capability denial, and fail-closed behavior.
17. Testing Plan
Section titled “17. Testing Plan”Testing should cover:
- strict config decoding and schema validation
- Rego compile failures at activation time
- successful allow and deny decisions
- undefined decision fallback behavior
- invalid decision object handling
- body-read and CID-resolution input shaping
- local-file authoring flow producing the expected materialized inline config and render/materialize output
- enforcement fail-closed and observability fail-open behavior
- multi-plugin sequencing where earlier plugins write context consumed by OPA policy
18. Alternatives Considered
Section titled “18. Alternatives Considered”18.1 Customer-authored WASM OPA plugin
Section titled “18.1 Customer-authored WASM OPA plugin”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.
18.2 External OPA sidecar over HTTP
Section titled “18.2 External OPA sidecar over HTTP”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.
18.3 Ad hoc policy language in Go config
Section titled “18.3 Ad hoc policy language in Go config”This is simpler than OPA, but it recreates a less expressive policy system and duplicates functionality already available in Rego.
19. Deferred Work
Section titled “19. Deferred Work”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
20. Acceptance Criteria
Section titled “20. Acceptance Criteria”builtin.opa_regocan be registered as a premade plugin definition and activated in a snapshot.- Snapshot activation rejects invalid Rego modules, invalid entrypoints, and invalid plugin config.
- The plugin evaluates in
proxy.preusing the standard deterministic hook pipeline. - The plugin returns the existing Guardian decision envelope without introducing a parallel deny format.
- The plugin can consume prior plugin context plus optional host-mediated body and CID inputs.
- Enforcement mode remains fail-closed and observability mode remains fail-open.
- 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.
- Customer customization is policy-driven and data-driven rather than customer-authored plugin engine code.