Gateway Policies
Allow or deny each agent request with gateway plugins, and see how global and project configurations combine.
The Agent Gateway enforces your rules on agent traffic through plugins. A plugin is a policy check the gateway runs on each request: it looks at who is calling, which project they belong to, and what they are sending, then allows the request, denies it, or records a finding. You choose which plugins run, where they run, and whether they block or only observe.
Gateway policies are separate from the compliance policies you apply to projects in Guardian. For those, see Policies.
Hook stages
Each plugin runs at a hook stage, a fixed point in the life of a request. Plugins at the same stage run in a set order. Guardian shows each stage by name; configuration files use the hook ID in parentheses.
| Stage | When it runs | What typically runs there |
|---|---|---|
Request Start (request.start) | As soon as a request arrives, before the gateway verifies the agent’s signature | Early checks on the raw request. Set in the global configuration only. |
Project Bootstrap (project.bootstrap) | After the gateway verifies the agent and identifies the project it is working in | Project trust and project membership checks |
Before Proxy (proxy.pre) | After verification, before the request goes to the model provider | Agent certification, authorization, verifiable credentials, OPA policy, and configuration validation. This is the main place to allow or deny. |
After Proxy (proxy.post) | When the provider’s response starts, before it reaches the agent | Checks on the response, usually custom plugins |
Request End (request.end) | After the response completes, in the background | VirusTotal scan and intent monitoring. These never slow down or block the agent. |
Observe and enforce
Every plugin entry runs in one of two modes. In a configuration file they are written observability and enforcement.
- Observe (
observability): the plugin records its decision, but the request always goes through. If the plugin itself fails, the request still goes through and the error is recorded. - Enforce (
enforcement): a deny blocks the request and the agent receives an error. If the plugin itself fails, the request is blocked (fail closed).
Roll out new policy observe-first:
- Add the plugin in observe mode.
- Let normal agent traffic run for a while and review what the plugin would have denied in the Audit Trail.
- Adjust the rules until the would-deny results match your intent.
- Switch the entry to enforce mode and apply the change.
Some plugins run in observe mode only because they cannot block by design (see the catalog below).
Project membership and project trust are the exception in the other direction. Their entries must always be written with mode = "enforcement"; the CLI rejects anything else. To roll those two out observe-first, set mode = "observe" inside the plugin’s own settings ([deployment.entries.config]), then switch it to "enforce" when you are ready.
Global and project configurations
A configuration is the list of plugins that run, with each plugin’s stage, order, mode, and settings. Project configurations only take effect once your administrator has enabled project context on the gateway; the global configuration applies either way. Configurations are layered:
- Global configuration. The organization-wide floor, set by the platform admin. It runs on every request through the gateway, whatever the project.
- Project configuration. An additive layer set by a project admin. It adds plugins for one project’s traffic.
The effective policy for a project is the union of both layers. At each stage the global entries run first, then the project entries. A project can add checks but can never remove, disable, or reorder the global floor. A project with no configuration of its own runs under the global floor only.
Project traffic is admitted only when the effective chain contains a project membership check that counts. A project’s own membership entry counts whatever its settings say. A membership entry in the global floor counts only when it can deny: its settings must leave mode at "enforce" and membership_required at true, which are the defaults when you omit them. A global entry set to mode = "observe" or membership_required = false does not count, and every project without its own entry is denied. So either put an enforcing membership entry in the global floor, or give each project its own entry (which can start in observe mode).
Every change creates a new, signed, versioned configuration, so you can see what changed and roll back. To see what is in effect for a project, open the project in Guardian and, in the project sidebar, select Plugin Configurations. The page shows the global and project layers, the plugins at each stage, and the configuration history. It is read-only; you change configurations with the guardian-plugin CLI, described in Policy as Code.
Built-in plugins
The gateway ships with these plugins. They do nothing until a configuration includes them, and a configuration can include one only after a platform admin has registered its definition (see Register a built-in plugin). Each plugin’s settings are listed under Built-in plugin settings.
| Plugin | What it does | Mode |
|---|---|---|
Project Membership (builtin.project_membership) | Checks that the agent holds a valid membership credential for the project: trusted issuer, status, expiry, revocation, and role. Only this check can establish which project a request belongs to. | Enforce, with an observe option in its own settings |
Project Trust (builtin.project_trust) | Checks that the project itself holds valid compliance evidence from a trusted issuer before membership is evaluated. Global configuration only. | Enforce, with an observe option in its own settings |
Agent Certification (builtin.agent_certification) | Checks that the agent holds a current certification from a trusted issuer under the program versions you require, that the instance sending the request runs the configuration it was certified with, and that the agent still holds the certified operating profile. It refuses a request that offers a tool or MCP server, or reaches a host, that the certified operating profile excludes. A request whose agent isn’t verified is always refused, even when certification isn’t required. Set max_status_age_seconds: by default a stored check is honored however old it is. | Enforce or observe |
Authorization (builtin.authorization) | Allows or denies requests with ordered rules that match on provider, operation, method, gateway or upstream path, and agent ID. | Enforce or observe |
Verifiable Credentials (builtin.verifiable_credential) | Verifies the agent’s credential: signature, trusted issuer, that it was issued to this agent, and validity dates. | Enforce or observe |
OPA Policy (builtin.opa_rego) | Evaluates your own Rego policy against the request. See OPA/Rego Policies. | Enforce or observe |
Configuration Validation (builtin.config_validation) | Checks the agent’s reported configuration files and permissions against rules, and flags violations on the exchange. See Agent Configuration. | Observe only. It never blocks a request. |
VirusTotal Scan (builtin.vtscan) | Hashes the agent’s skill files and looks each one up on VirusTotal for known malware. A file VirusTotal has never seen is uploaded to VirusTotal for analysis. | Observe only, runs after the request |
Intent Monitoring (builtin.intent_monitor) | Sends the user’s messages and the agent’s latest tool call to a model you choose, which judges whether the action matches what the user asked for, and flags drift. | Observe only, runs after the request |
Plugins that run after the request, such as the VirusTotal scan and the intent monitor, record their results as plugin events rather than in the exchange’s plugin activity. Read them through the Control Plane API with GET /v1/agents/{agentId}/plugin-events, and add plugin_id to see one plugin, for example ?plugin_id=builtin.intent_monitor. Each intent monitor evaluation records whether the agent’s action was aligned with the user’s request, with a confidence score. Organization Owners and the users bound to the agent can read these events.
Register a built-in plugin
A configuration that names a built-in plugin is rejected until the plugin’s definition is registered with the Control Plane. A platform admin registers each built-in once by running guardian-plugin publish (install it) against a guardian.plugin.toml that names it:
[package]
plugin_id = "builtin.authorization"
name = "authorization"
version = "v1.0.0"
[control_plane]
base_url = "https://control-plane.example.com/api" # your Control Plane URL
[definition]
runtime = "premade"
supported_hooks = ["proxy.pre"]For a built-in, publish skips the build and upload and only registers the definition, so it is safe to run again. guardian-plugin init --template opa-rego and --template vtscan write a ready manifest for those two plugins.
Custom plugins
When the built-in plugins and OPA/Rego cannot express a rule, you can write your own plugin in Rust with the Guardian plugin SDK and compile it to WebAssembly for the wasm32-wasip1 target. A minimal plugin that allows every request looks like this:
use guardian_plugin_sdk::{export_plugin, Decision, Request};
fn handle(_request: Request) -> Decision {
Decision::allow()
}
export_plugin!(handle);You publish the compiled plugin with guardian-plugin publish. The gateway only loads signed plugins, and guardian-plugin does not sign the artifact itself: the Control Plane signs it on upload with its publisher signer. Until your administrator configures one, publishing fails with a missing-signature error. After that, you enable a custom plugin through a configuration exactly like a built-in one, in observability or enforcement mode, at global or project scope. Publishing needs a platform admin. Policy as Code shows the manifest and build steps.
Beyond the request details the gateway passes in, a plugin can use these host capabilities:
| Capability | What it allows | Grant settings |
|---|---|---|
cid.resolve | Read content by CID from the gateway’s store, such as the agent’s configuration snapshot | max_bytes |
request.body.read | Read the full request body | max_bytes |
http.request | Make outbound HTTP or HTTPS requests | allowed_url_prefixes, allowed_methods, timeout_ms, max_request_bytes, max_response_bytes, max_inflight |
agent.badge.write | Publish checks on the calling agent, shown in the Agent Inventory | allowed_keys |
A capability must be both requested and granted. List it in the plugin definition’s required_capabilities, then grant it, with all of its settings, on each configuration entry that runs the plugin. A configuration that leaves out a required grant is rejected, and the gateway refuses any capability the entry was not granted.
Who manages what
| Task | Who |
|---|---|
| Set the global configuration (the organization floor), including project trust | Platform admin (in Guardian, an Organization Owner) |
| Register built-in plugins and publish custom plugins | Platform admin |
| Store plugin secrets, such as API keys | Platform admin |
| Add a project configuration on top of the floor | Project admin for that project (a Project Owner or Organization Owner) |
| View the plugins in effect for a project | Anyone who can view the project |
See Roles and Permissions for how roles are assigned.
Built-in plugin settings
Each entry’s settings go in its [deployment.entries.config] table. Unknown keys are rejected. Where a plugin needs a capability, the entry must also grant it (see Custom plugins for the grant settings).
A setting marked as a secret takes a reference rather than the value itself, such as vt_api_key = { kind = "secret_ref", name = "virustotal_api_key" }. A platform admin stores the value under the plugin ID and that name with POST /v1/plugins/secrets (see Policy as Code). Guardian never shows a stored secret’s value.
Project Membership
| Setting | Type | Default | Meaning |
|---|---|---|---|
trusted_issuers | List of did:key values, required | — | Issuers whose membership credentials are accepted |
membership_required | Boolean | true | Refuse a request without a valid membership |
accepted_statuses | List of valid, expired, not_yet_valid, revoked | ["valid"] | Credential statuses that pass |
enforce_expiration | Boolean | true | Check the credential’s expiry |
enforce_revocation | Boolean | true | Check the credential’s revocation status |
max_status_age_seconds | Integer | 0 (no limit) | Oldest stored status check accepted |
allowed_roles | List of strings | Any role | Every role the agent holds must be on this list |
required_roles | List of strings | None | Roles the agent must hold |
mode | observe or enforce | enforce | observe records the decision without denying |
Project Trust
| Setting | Type | Default | Meaning |
|---|---|---|---|
trusted_issuers | List of did:key values, required | — | Issuers whose project compliance credentials are accepted |
trust_required | Boolean | true | Refuse a project without valid compliance evidence |
required_policy_cids | List of urn:cid values | Any policy | Policies the evidence must cover |
policy_match | any or all | any | Whether one or every required policy must be covered |
accepted_statuses | List of valid, expired, not_yet_valid, revoked | ["valid"] | Credential statuses that pass |
enforce_expiration | Boolean | true | Check the credential’s expiry |
enforce_revocation | Boolean | true | Check the credential’s revocation status |
max_status_age_seconds | Integer | 0 (no limit) | Oldest stored status check accepted |
mode | observe or enforce | enforce | observe records the decision without denying |
Agent Certification
For what a certification covers, how agents earn one, and a rollout example, see Agent Certifications.
Needs the request.body.read grant with max_bytes of at least 33554432 (32 MiB, the gateway’s default request body limit), and an agent.badge.write grant whose allowed_keys include operating_profile. A configuration without them is rejected. If your gateway’s request body limit is raised, raise max_bytes to match: a request larger than the grant can’t be checked, and is refused.
| Setting | Type | Default | Meaning |
|---|---|---|---|
trusted_issuers | List of did:key values, required | — | Issuers whose certifications are accepted |
certification_required | Boolean | true | Refuse an agent with no certification under a required program. false admits it, and records rather than refuses a request its certification doesn’t cover |
required_program_cids | List of urn:cid values | Any program | Program versions the certification must be under |
program_match | any or all | any | Whether one or every required program must be covered |
accepted_statuses | List of valid, expired, not_yet_valid, revoked | ["valid"] | Certification statuses that pass |
enforce_expiration | Boolean | true | Check the certification’s expiry |
enforce_revocation | Boolean | true | Check the certification’s revocation status |
require_configuration_match | Boolean | true | Refuse a request from an instance that doesn’t run the certified configuration, or from an agent that no longer holds the certified operating profile |
max_status_age_seconds | Integer | 0 (no limit) | Oldest stored status and operating-profile check accepted |
enforce_profile_exclusions | Boolean | true | Refuse a request that offers what the certified operating profile excludes. false records it without refusing |
Each instance of an agent signs its requests with its own key, and the plugin judges every request by the configuration its own state gives. A request from an instance running a configuration its certification doesn’t name is refused with agent_certification_configuration_mismatch, and one whose state gives no configuration at all with agent_certification_configuration_unknown. The agent’s other instances keep working. The refused instance shows a Configuration not certified badge until an entry that requires a certification admits its next request, when the entry’s agent.badge.write grant allows the configuration badge. When the gateway can’t read the configuration just then, the request is refused with agent_certification_configuration_unavailable and status 503, and can be retried. The operating profile is the agent’s, declared for every instance, so it is checked for the whole agent, as the Control Plane last found it.
With certification_required = false, a certification that doesn’t cover a request counts as none on that entry. The request is admitted as one from an agent without a certification would be, held to no operating profile, so the tools, MCP servers and hosts the certification’s profile excludes aren’t refused there. To have them enforced, require a certification. The plugin records why the certification didn’t cover the request in its agent_certification.configuration_reason tag: a different configuration or operating profile, a state that gives none, a configuration it can’t read just then, or an operating-profile check older than max_status_age_seconds. Guardian shows it in the exchange detail’s Plugin Activity as Treated as Uncertified, with the reason. Such an entry never sets or clears the configuration badge, and an admission it holds to no profile leaves the operating-profile badge as it is. A certification that is revoked, expired, not yet valid or from an untrusted issuer is still refused.
Once it admits an agent, the plugin holds each request to the operating profiles named by every certification that admits it. With require_configuration_match = false, a certification of a configuration the agent no longer runs can still admit it, and its profile’s exclusions then apply as well. That only ever adds restrictions.
Each request is checked like this:
- A model request is checked by the tools it offers, in Anthropic Messages and Message Batches, OpenAI Chat Completions and Responses, and Codex on the ChatGPT backend. A request to any other endpoint whose body names tools is refused, because its tools can’t be read. So is a request offering a model provider’s MCP connector that lists no allowed tools, when the profile excludes tools: list the connector’s allowed tools.
- A relay request is checked by its destination host, and by the tools its body offers when the destination’s path is one of those formats. A relay body that names tools in another format is refused.
- A model request is also checked by its upstream host. A
host:rule without a port matches the host on any port. Rules match hosts as requests name them, so while a profile excludes hosts, a relay request that names its destination by an IP address the profile has no rule for is refused withagent_certification_host_unverifiable. Reach such hosts by name, or name the address in the profile. - An OpenAI batch is refused when the profile excludes tools: its requests are in an input file the gateway doesn’t read.
A request that offers a tool or MCP server, or reaches a host, that one of the profiles excludes is refused with agent_certification_profile_excluded, naming what matched and the profile rule. The agent then shows an Operating profile breach badge until a model request that an entry holds to the profile offers tools and none of them is excluded. A request that can’t be checked is refused too: with agent_certification_profile_unavailable when the profile isn’t one of the agent’s versions, agent_certification_profile_unreadable when it no longer parses or matches its CID, and agent_certification_tools_unreadable when the request’s tools can’t be read in full. With enforce_profile_exclusions = false, those requests are admitted and the plugin records what it would have refused in its tags. Tools the profile doesn’t declare are never refused; the agent’s Operating Profile page reports them.
Certify a configuration that doesn’t offer what the profile excludes. Claude Code offers its built-in tools on every request, so deny an excluded one under permissions in its settings, such as "deny": ["WebFetch"], and send the agent one request before the assessment pins the configuration, so the configuration it pins includes the rule. The settings files are part of the configuration, so a deny rule added after certification makes a new configuration that the certification doesn’t name.
Upgrading. Upgrade in this order: the gateway, whose migrations add the tables and columns the Control Plane reads; then the Control Plane; then publish every builtin.agent_certification entry again with both grants; then run guardian-plugin publish for the plugin, which registers its new setting; then the Governance Service and Guardian. Entries written before profile enforcement lack its grants. The upgraded gateway logs a configuration warning for each one, and admits the requests whose tools it can’t check against a profile, recording why, until the entry is published again with the grants. Host exclusions still apply to those entries. Set enforce_profile_exclusions only once every gateway is upgraded: an older gateway can’t read the setting, so the entry fails every request it checks. Remove the setting before rolling a gateway back, and roll the Control Plane back with it: an older gateway can’t read the checks the upgraded Control Plane stores, so it refuses every certified agent with agent_certification_configuration_unknown while require_configuration_match is on. To mark refused instances, publish the entry with allowed_keys = ["operating_profile", "configuration"]; an entry that allows only operating_profile publishes no configuration badge.
Authorization
| Setting | Type | Default | Meaning |
|---|---|---|---|
default_effect | allow or deny | allow | Decision when no rule matches |
default_status_code | Integer | 403 | Status the agent receives on a default deny |
default_code | String | policy_denied | Error code on a default deny |
default_message | String | request denied by authorization policy | Error message on a default deny |
rules | List of rules | None | Checked in order; the first rule that matches decides |
Each rule takes a required effect (allow or deny), an optional id recorded on the exchange, and optional status_code, code, and message for a deny. Its match table can set provider, operation, method, gateway_path, upstream_path, and agent_id, each as { operator = "exact" | "prefix", value = "..." }. Every field you set must match; a rule with no match matches every request. provider, operation, and method compare without regard to case.
Verifiable Credentials
Needs the cid.resolve grant.
| Setting | Type | Default | Meaning |
|---|---|---|---|
trusted_issuers | List of did:key values, required | — | Issuers whose credentials are accepted |
required | Boolean | true | Refuse a request that carries no credential |
header_name | String | X-Viper-Credential-CID | Request header that carries the credential CID |
subject_source | request_signer_did | request_signer_did | The credential must be issued to the request’s signer |
OPA Policy
| Setting | Type | Default | Meaning |
|---|---|---|---|
entrypoint | String, required | — | The data.* rule that returns the decision |
default_decision | Decision | Allow | Decision used when the entrypoint produces no result |
input_options | Table | Off | include_body, include_resolved_cid, and cid_header_names |
The CLI fills modules and data from the entry’s [deployment.entries.opa] files. See OPA/Rego Policies for the input, the decision, and the grants each option needs.
Configuration Validation
Needs the cid.resolve grant. The plugin checks against your organization’s configuration validation rules (see Agent Configuration); the entry’s own rules apply only when the gateway cannot read those.
| Setting | Type | Default | Meaning |
|---|---|---|---|
state_cid_header | String | X-Viper-State-CID | Request header that carries the agent’s state record CID |
rules | List of rules | None | Fallback rules, in the same shape as the organization’s |
VirusTotal Scan
Needs the cid.resolve grant and an http.request grant that allows GET and POST to https://www.virustotal.com. Files are sent to VirusTotal only when it has no result for their hash, but those uploads leave your environment, so enable this plugin only for skills you can share with VirusTotal. guardian-plugin init --template vtscan writes an entry with these grants.
| Setting | Type | Default | Meaning |
|---|---|---|---|
vt_api_key | Secret, required | — | Your VirusTotal API key |
state_cid_header | String | X-Viper-State-CID | Request header that carries the agent’s state record CID |
rate_limit_per_minute | Integer | 4 | VirusTotal API calls per minute |
scan_timeout_hours | Integer | 24 | Accepted, but not used by the gateway in this release |
Intent Monitoring
Needs the request.body.read grant and an http.request grant that allows POST to llm_url. Each evaluation sends the user’s messages from the request and the agent’s latest tool call, with its details, to llm_url, so choose a model endpoint that is approved for that content.
| Setting | Type | Default | Meaning |
|---|---|---|---|
llm_url | URL, required | — | An OpenAI-compatible chat completions endpoint |
llm_model | String | gpt-5.4-mini | Model name sent with each evaluation |
llm_api_key | Secret | None | Sent as a bearer token to llm_url |
Related
- Policy as Code — validate, plan, and apply configurations with
guardian-plugin - OPA/Rego Policies — write your own allow and deny rules
- Plugin Configurations — see the plugins in effect for a project
- Audit Trail — review what a plugin would have denied before you enforce it
- Associated Agents — link an agent to a project so project configurations apply to it
- Gateway Basics — where plugins run in one request’s path