OPA policies reference
This page is the lookup reference for writing OPA (Rego) policies on the Agent Gateway. It covers:
- The Policy element: which canvas slots accept a policy and how to configure the element on a surface.
- Policy definitions: versions, dry-run testing, and deletion: the fields on the policy editor and list, the dry-run Test panel, and what the delete confirmation checks.
- Appliance-wide (global) policy enforcement: enforcing a policy definition on every gateway or every surface at once.
- Policy input reference: every field the gateway populates in the
inputobject that Rego rules read at evaluation time.
If you are new to OPA policies on the Agent Gateway, read the OPA policies concept overview first.
The Policy element
The Policy element runs an OPA (Rego) policy against the request or response and denies it when the policy returns false. Policies run in-process. No external OPA daemon is required. Multiple Policy elements can coexist on a surface, one per slot.
The slot the element fills is determined by the edge you drop it on.
| Drop position | Slot | When it evaluates |
|---|---|---|
| Access Point → Managed Agent (request) | Target policy | Before the request is forwarded to the Managed Agent. One Policy element per slot. |
| Access Point → Managed Agent (response) | Response policy | On the response before it is forwarded back to the caller. |
| Managed Agent → Transit Point (request) | Transit Point policy | Per hop, before dispatch to that destination. |
| Managed Agent → Transit Point (response) | Transit Point response policy | On the response from that destination, before it returns to the Managed Agent. |
A surface saved with an inbound policy shows it as a second Policy element on the Access Point → Managed Agent edge, evaluated before the target policy. New surfaces can’t add one in the dashboard.
Gateway-level policies (configured outside the surface) always run before any surface-level policy element. A gateway-level deny cannot be overridden by this element.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| Policy Definition | select | Yes | — | ID of the OPA policy definition in the policy store. Evaluated against the PolicyInput struct at runtime. |
| Enrich policy input with agent context | bool | No | false | When on, the pipeline fetches the target’s agent card and queries the trust registry before evaluation, populating input.agent in the OPA input. Adds latency. Enable only when the policy uses input.agent. Not available for response policies. |
Policy definitions: versions, dry-run testing, and deletion
Every policy definition is stored as an append-only sequence of versions, not a single mutable record. Saving a change to the Rego body appends a new version and keeps every earlier one. Saving only the name, description, or enabled state updates the current version in place without creating a new one.
Content hash and version fields
The policy editor and list both show read-only fields that identify the exact Rego enforced, so a decision or an audit entry can be traced back to specific bytes even after the policy has since changed.
| Field | What it does | Default |
|---|---|---|
| SHA | The content hash (sha256:<hex>) of the Rego currently enforced. Shown on the policy editor, in the policy list, and against every row of the version history. Binds the policy’s scope to its body, so identical Rego under Gateway and Agent Surface scope hashes differently. | — |
| Version history | An append-only, newest-first list of every revision the policy has had: version number, content hash, creation time, author, and the name/description as they stood at the time. Selecting an older version compares it line-by-line against the current one. | — |
| Author | The username of whoever saved the revision, resolved from the authenticated account. Falls back to the raw account identifier when the account can no longer be resolved, for example a deleted user. | — |
Dry-run testing (Test panel)
The Test (dry-run) panel appears below the Rego editor once the policy definition has been saved at least once.
| Field | What it does | Default |
|---|---|---|
| Sample input JSON | The input document the dry run evaluates the policy against. Saved with the policy definition, so the last input you tested with is still there next time you open it. | A minimal example with a JWT subject, an HTTP method and path, and an inbound direction. |
| Run against the draft above | Evaluates the Rego currently in the editor, including unsaved changes, against the sample input. Disabled while the policy does not compile or the sample input is not valid JSON. Nothing here reaches the live policy engine or in-flight traffic. | — |
| Auto-run | Re-runs the dry run automatically, a short pause after every change to the policy or the sample input. | Off |
The result shows an ALLOW or DENY badge, a reason when the policy defines one, and the query, version, and content hash the dry run evaluated (shown as draft instead of a version number when testing unsaved Rego).
The draft Rego and sample input are each size-capped, and a run is time-bounded, so an oversized or pathological input is rejected rather than left to run indefinitely: an oversized draft returns “Draft policy is too large to simulate”, and an oversized sample input returns “Sample input is too large to simulate”.
Delete confirmation and blast radius
Selecting the delete icon on a policy row opens a confirmation instead of deleting immediately.
- The confirmation states how many gateways (for a Gateway policy) or surfaces (for an Agent Surface policy) currently reference the policy, so you can judge the impact before committing to a delete that cannot be undone.
- This count only covers direct references within the policy’s own scope: gateways with it selected on their Global Policy tab, or surfaces with a Policy element bound to it. It does not include appliance-wide (global) enforcement. Check the policy’s Global column before deleting; a globally-enforced policy’s removal fails its entire plane closed, not only the objects the count includes. See Appliance-wide (global) policy enforcement.
- A referencing gateway or surface does not fall back to allowing traffic once its policy disappears. It fails closed: every request that policy used to evaluate is denied until a replacement is attached or the reference is removed.
Appliance-wide (global) policy enforcement
Any Gateway-type or Agent Surface-type policy definition can also be enforced across every gateway or every surface on the appliance, from the Policies list, in addition to or instead of being attached to individual objects. This is a reach setting on an existing definition, not a separate Rego scope: an appliance-wide Gateway policy still uses package gateway.policy and query path data.gateway.policy.allow, and an appliance-wide Agent Surface policy still uses package surface.policy and query path data.surface.policy.allow, exactly as described in Policy scopes and input structs.
| Field | What it does | Default |
|---|---|---|
| Global (toggle, per policy row on the Gateway or Agent Surfaces tab) | Enforces this policy definition on every gateway or every surface on the appliance, on top of that object’s own policy. Switching it on shows a confirmation stating how many gateways or surfaces will enforce it. | Off |
| Monitor (checkbox, shown once Global is on) | Evaluates and records the decision without blocking a request the policy would have denied. | Off (enforcing) |
- Appliance-wide policies evaluate deny-overrides, ahead of that plane’s own per-object policy. On an inbound direct request the order is: appliance-wide gateway policy, the gateway’s own Global Policy tab policy, appliance-wide surface policy, then the surface’s own Policy element. A deny at any stage is final.
- A Monitor assignment is evaluated and its would-be decision recorded, but it never blocks a request, so it is a way to observe a new appliance-wide rule before enforcing it.
A Global assignment that is switched on (not Monitor) and can no longer be resolved, because the underlying policy definition was disabled, deleted, or no longer compiles, fails its entire plane closed: every gateway or every surface, depending on the plane, denies matching traffic until the assignment is fixed or removed. A Monitor-only assignment does not have this effect; it is skipped instead.
Policy input reference
The gateway serialises request context into a structured input object before evaluating any Rego policy. Policy rules read fields from input to make access decisions. Use this reference when writing Rego rules to understand which fields are available, how authentication data appears in input.source_auth, and which fields are populated at each evaluation point.
Policy scopes and input structs
Two distinct input shapes exist. Use the correct package declaration and field paths for the scope you are writing.
| Scope | Package declaration | Query path | Input struct |
|---|---|---|---|
| Gateway | package gateway.policy | data.gateway.policy.allow | PolicyInput |
| Surface (inbound, outbound, response) | package surface.policy | data.surface.policy.allow | PolicyInput |
| MCP tool | package surface.policy | data.surface.policy.allow | McpPolicyContext |
Gateway and surface policies share the same PolicyInput struct and field namespace. MCP tool policies use a different struct (McpPolicyContext) with a different field layout; see MCP tool policy input.
The legacy package channel.policy declaration is rejected by the API on create and update. Any definition stored before the migration was automatically rewritten to package surface.policy at startup. Always use package surface.policy in new policies.
Gateway and surface policy input (PolicyInput)
Full example
A fully-populated PolicyInput for an inbound MCP request with JWT authentication and trust registry data:
{
"http": {
"method": "POST",
"path": "/v1/surfaces/my-surface/mcp",
"headers": {
"content-type": "application/json",
"x-request-id": "req-abc123"
}
},
"gateway": {
"direction": "inbound"
},
"channel": {
"config_id": "surface-abc123",
"name": "my-surface"
},
"source_auth": {
"method": "jwt_bearer",
"subject": "user@example.com",
"claims": {
"role": "admin",
"org": "acme-corp",
"aud": "https://gateway.example.com"
}
},
"mcp": {
"method": "tools/call",
"tool_name": "search",
"params": { "query": "hello" }
},
"agent": {
"did": "did:web:example.com:agent-1",
"trust_verification": true,
"source_trust_verification": true,
"agent_dna": {
"uai": "urn:uai:example.com:agent-1:v1",
"birthEvent": { "scid": "...", "timestamp": "..." },
"genesis": { "codeHash": "...", "genesisHash": "..." },
"behavioral": { "behavioralHash": "..." },
"operational": {},
"attestations": {}
}
}
}Not all fields are present on every request. See Input availability by evaluation path for when each field is populated.
input.http
Always present. Sensitive headers are stripped before policy evaluation, on inbound and outbound requests alike. Policies cannot read credential values from headers.
| Field | Type | Description |
|---|---|---|
method | string | HTTP method: "POST", "GET", etc. |
path | string | Request path, for example /v1/surfaces/my-surface/mcp. |
headers | object | Non-sensitive request headers as a flat string-to-string map. On an outbound Transit Point request, these are the headers from the original inbound request. |
input.gateway
Always present. Describes the traffic direction and the identity participants.
| Field | Type | Description |
|---|---|---|
direction | string | "inbound" when traffic arrives at this gateway. "outbound" when the managed agent sends a request through a transit point. |
source_id | string or null | On outbound requests, the resolved DID of the managed agent acting as the protected caller. null on inbound requests. |
target_id | string or null | On outbound requests: the target endpoint URL or remote gateway DID. null on inbound requests. |
input.channel
Always present. Identifies the surface handling the request.
| Field | Type | Description |
|---|---|---|
config_id | string or null | Internal surface ID. Stable identifier across renames. |
name | string or null | Human-readable surface name as configured in the dashboard. |
variant_alias | string or null | Variant alias from the request URL (/route$alias/...). null when the request targets the default variant. |
input.source_auth
On inbound evaluations, present whenever the surface has a Caller Context element and the request reaches policy. A caller whose credential failed reaches policy only on some Access Points; see Failed. On outbound evaluations, present when the Transit Point’s own Caller authentication verified the caller, with the same shape as on the inbound side.
The method field names the Caller Context method that verified the caller and determines the shape of the object. When a Caller Context element holds several methods, the gateway verifies each request with the method whose credential it carries, so method tells a policy which kind of credential the caller used. See How the gateway selects a method.
This rule admits every JWT caller, and only those API key callers verified against one secret:
package surface.policy
default allow := false
allow if input.source_auth.method == "jwt_bearer"
allow if {
input.source_auth.method == "api_key"
input.source_auth.key_name == "partner-keys"
}JWT bearer
Callers authenticated with a validated JWT.
{
"method": "jwt_bearer",
"subject": "user@example.com",
"claims": {
"role": "admin",
"org": "acme-corp"
}
}| Field | Description |
|---|---|
subject | The value of the JWT verification strategy’s Caller identity claim, sub by default. |
claims | All claims from the token payload as a JSON object. Access individual claims as input.source_auth.claims.<claim_name>. |
API key
Callers verified by an API Key (secret store) or API Key (provider) method. Both report method as api_key, so a policy tells them apart by key_name.
{
"method": "api_key",
"key_name": "my-client-id"
}| Field | Description |
|---|---|
key_name | For API Key (secret store), the method’s Secret ID. For API Key (provider), the Key ID of the key the caller presented, as shown on the API keys page. |
DID auth
Callers authenticated with a DID-based credential.
{
"method": "did_auth",
"did": "did:web:example.com:caller"
}| Field | Description |
|---|---|
did | The caller’s DID. |
mTLS
Callers authenticated with a mutual TLS client certificate.
{
"method": "mtls",
"principal": "CN=my-service,O=Example Corp",
"fingerprint": "sha256:abc123...",
"subject_dn": "CN=my-service,O=Example Corp",
"issuer_dn": "CN=Example CA,O=Example Corp",
"sans": {
"dns": ["my-service.example.com"],
"uri": ["spiffe://example.com/my-service"]
}
}| Field | Description |
|---|---|
principal | The matched principal string (fingerprint, subject CN, DNS SAN, URI SAN, or RDN), depending on the configured identity binding. |
fingerprint | SHA-256 fingerprint (hash) of the client certificate in hex. |
subject_dn | Full subject Distinguished Name. |
issuer_dn | Full issuer Distinguished Name. |
sans.dns | DNS Subject Alternative Names. Absent when none. |
sans.uri | URI Subject Alternative Names (for example SPIFFE IDs). Absent when none. |
sans.email | Email Subject Alternative Names. Absent when none. |
sans.ip | IP address Subject Alternative Names. Absent when none. |
Failed
Callers whose credential was missing or invalid, or who sent credentials for more than one Caller Context method, on an Access Point that hands the failure to policy. Whether a failed caller reaches policy depends on the Access Point’s state:
- Closed (the default for surfaces created from version 1.0.4, and a legacy Access Point after Close Access Point): the gateway refuses the caller with
401andCaller credential missing or invalidbefore any policy runs, so policy sees nofailedresult. - Public access on: the request reaches policy with
methodset tofailed, and the surface forwards it with no asserted caller identity unless a policy denies it. - Legacy (stored before version 1.0.4 and not yet closed): the request reaches policy with
methodset tofailed, as on a public Access Point.
On a surface with authorization server discovery for MCP clients, a failed caller gets a 401 instead in every state. See Access Point for Public access and Close Access Point.
{
"method": "failed",
"attempted_method": "jwt_bearer",
"reason": "Invalid credential: Token has expired"
}| Field | Description |
|---|---|
attempted_method | The Caller Context method selected for the request: the method whose credential the request carried, or the primary (first) method when it carried none or credentials for more than one. One of jwt_bearer, api_key, api_key_provider, did_auth, or mtls. |
reason | A human-readable description of why authentication failed. A request carrying credentials for more than one method gives “Ambiguous credentials: the request carries credentials for more than one source authentication method”. |
On a public or legacy Access Point, a failed credential does not block the request by itself. Add a policy rule that checks input.source_auth.method to deny an unauthenticated caller there. On a closed Access Point, the gateway refuses that caller with 401 before policy runs.
input.mcp
Present on inbound MCP protocol requests only. Absent on outbound requests and non-MCP protocols.
| Field | Type | Description |
|---|---|---|
method | string | The MCP JSON-RPC method, for example "tools/call", "tools/list", "resources/read". |
tool_name | string or null | Tool name from a tools/call request. null for other MCP methods. |
resource_uri | string or null | Resource URI from a resources/read request. null for other methods. |
prompt_name | string or null | Prompt name from a prompts/get request. null for other methods. |
params | object or null | Full parsed params from the JSON-RPC request body. |
input.a2a
Present on A2A protocol requests. It is built from the original request body before identity injection and reflects what the caller actually sent.
| Field | Type | Description |
|---|---|---|
method | string or null | The A2A JSON-RPC method, for example "message/send". |
message | object or null | The A2A message object (params.message or top-level message). Contains role, parts, metadata, and messageId when present. |
input.agent
Present when Extract Trust Registry Data is enabled on the surface and data was retrieved. Also present with did populated after the caller DID is resolved. Absent when neither condition applies.
| Field | Type | Description |
|---|---|---|
did | string or null | The agent’s effective DID. |
trust_verification | boolean or null | true when all configured trust registry recognition queries pass. false when any query returns untrusted. null when no queries ran. |
source_trust_verification | boolean or null | Caller-leg trust verification result when the surface is configured in Both mode. |
target_trust_verification | boolean or null | Target-leg trust verification result when the surface is configured in Both mode. |
agent_dna | object or null | Agent DNA fingerprint from the agent card. Contains uai and nested fingerprint objects. Absent when the agent card does not include an agentDNA field. |
trust_registry_did | string or null | The trust registry DID from the agent’s trust-registry extension. |
provider_did | string or null | The department or provider DID from the agent’s trust-registry extension. |
authority_did | string or null | The authority DID from the trust-registry extension. |
If input.agent.trust_verification is absent, a condition that checks input.agent.trust_verification == true evaluates to undefined in Rego and the rule denies. Ensure Trust Registry data extraction is enabled on the surface when writing policies that check input.agent.
input.extension_identity
Present when a verified Verifiable Presentation (VP) was included in the request body. Absent otherwise.
| Field | Type | Description |
|---|---|---|
did | string or null | The agent DID from the VP holder/subject. |
identity_hash | string or null | A hash of the identity payload for correlation. |
input.payment
Present on inbound requests where an x402 payment was verified. Absent otherwise.
| Field | Type | Description |
|---|---|---|
verified | boolean | true when the payment was cryptographically verified. |
response_header | string or null | The raw payment receipt header value. |
input.identity_binding
Present when a verified VP from an upstream gateway was processed. Absent otherwise.
| Field | Type | Description |
|---|---|---|
verified | boolean | Whether the VP signature was cryptographically verified. |
agent.did | string | The managed agent’s DID on the sending gateway. |
agent.identity_fields | object | Agent identity fields from the VP credential subject. |
caller.fields | object | Allowlisted caller claims, keyed by field name. Absent when the sending gateway bound no caller context. |
caller.user_hash | string | One-way hash of the caller’s identity on the sending gateway. It depends on how that gateway identified the caller: the authentication method, the credential’s issuer and the principal. |
caller.assurance | string | "gateway_attested" or "caller_credential_chained". |
caller.identity_source | string | How the sending gateway sourced caller context: "transit_token" or "jwt_bearer". |
delegated | boolean | true when the sending gateway was acting for a caller. |
issuer_gateway | string | The DID of the gateway that issued the VP. |
target | string | The endpoint the request was forwarded to. Absent when not recorded. |
intent | object | Structured intent object from the VP. Absent when not present. |
input.trust_check_results
Present when Trust Check elements are configured on the surface and at least one ran. Absent when no Trust Check elements are configured for the leg.
{
"trust_check_results": {
"caller": [
{
"id": "tc-caller-1",
"trust_registry_id": "tr-main",
"query_type": "recognition",
"ok": true,
"error": null,
"name": "Verify caller department",
"authority_id": "did:web:authority.example.com",
"entity_id": "did:web:caller.example.com",
"action": "is",
"resource": "ownedAgent",
"query_resolved": true
}
],
"target": []
}
}| Field | Type | Description |
|---|---|---|
caller | array | Results for the caller leg. Empty array [] when no caller-leg checks ran. Never null. |
target | array | Results for the target leg. On a request through a Transit Point, only that Transit Point’s results. A Transit Point without Trust Checks leaves input.trust_check_results absent, so a policy that requires Trust Checks also checks that count(input.trust_check_results.target) > 0. |
Each result in the array shares the same shape as the Trust Check result fields. For the complete list of error.code values and their categories, see Trust Check error codes.
input.transit
Present on every evaluation of a Transit Point call: the gateway, surface, per-Transit-Point and response policies, and MCP Tool Gating conditions on a Transit Point. Absent on inbound evaluations. It names two principals, each in its own field, because one call can carry both:
input.transit.caller: the end user the transit token names, as the Access Point authenticated them. Without a valid transit token it isanonymous.input.transit.client: the caller the Transit Point’s own Caller authentication verified on this request. Absent when the Transit Point authenticated none.
{
"transit": {
"caller": {
"verified": true,
"verification": "transit_token",
"identity_source": "gateway-computed",
"user_identity_hash": "42628a381c35947c51d560b24315c5590c6f3c217701fcabd95f9fd9697bcd3f",
"did": "did:web:caller.example.com"
},
"client": {
"verification": "source_auth",
"method": "jwt_bearer",
"user_identity_hash": "2155543194a07922605d5396881518ffe9bc6a2dfdc434e250ea1363324f297f",
"subject": "crm-agent"
}
}
}| Field | Type | Description |
|---|---|---|
caller.verified | boolean | true when verification is transit_token, or fabric_peer with a caller the receiving surface verified. It covers user_identity_hash and fields, not did. Test it before reading any other caller field. |
caller.verification | string | transit_token: the call carries a valid transit token whose caller the Access Point authenticated. unverified: a valid token names a caller DID that nothing verified. fabric_peer: the token was issued for a request a paired gateway sent over Fabric. anonymous: the call carries no valid transit token, as on a Transit Point that doesn’t require one and receives none, or on a discovery read. |
caller.identity_source | string | How the caller DID was established. Absent when the token names no DID. |
caller.user_identity_hash | string | The caller’s identity hash. It depends on how the caller authenticated, who issued the credential and the principal, so the same name authenticated another way has another hash. Present only when verified. |
caller.did | string | The caller DID the transit token carries. Always asserted, even when verified is true. Absent for a fabric_peer caller and when the token names none. |
caller.fields | object | Caller claims this Transit Point’s Workload Binding allowlists, from the caller’s verified token. Absent unless verified. |
caller.fabric | object | Present for a fabric_peer caller: peer_gateway_id, peer_issuer_did, hops, and caller_did, the DID the peer named. The peer gateway is verified, and caller_did is the peer’s word. |
client.verification | string | source_auth when the Transit Point’s Caller authentication verified the caller. A Transit Point given a resource access token through the API reports resource_token instead. |
client.method | string | jwt_bearer, api_key (for both API key methods), or mtls. |
client.user_identity_hash | string | The client’s identity hash. Absent for a credential that names no subject, such as a JWT without its strategy’s caller identity claim. |
client.subject | string | The verified principal: the value of the JWT’s caller identity claim (sub by default), the Key ID for API key (provider), or the Secret ID for API key (secret store). Absent with the hash. |
On a Transit Point that requires the transit token, caller is the end user. On a Transit Point that authenticates its own callers, client is the agent or service that called it. A Transit Point that does both fills both. A rule about the end user reads caller, and a rule about the calling agent reads client.
To allow only calls whose end user the Access Point verified, test verified and verification:
package surface.policy
default allow := false
allow if {
input.transit.caller.verified
input.transit.caller.verification == "transit_token"
}To allow only named callers on a Transit Point that authenticates its own callers, check that the Transit Point verified the caller before you match its name:
package surface.policy
default allow := false
named_callers := {"crm-agent", "billing-agent"}
allow if {
input.transit.client.verification == "source_auth"
named_callers[input.transit.client.subject]
}A transit token issued for a partner’s request over Fabric reaches the agent with caller.fabric set. Check caller.fabric only as an extra rule on Transit Points that require the transit token: on one that doesn’t, an agent that sends no token leaves the rule unmatched. Which partners reach a surface is set on the Publishing tab of each remote gateway record. See Gateways.
input.metadata
Present when Metadata Injection rules have added metadata to the request. A flat key-value map with string keys and arbitrary JSON values.
Input availability by evaluation path
Use this table when a policy condition behaves unexpectedly. The field you are checking may simply not be populated on that evaluation path.
The four columns correspond to these evaluation paths:
- Inbound direct: a caller connects to an Access Point on this gateway.
- Inbound G2G (fabric send): the call arrives through a gateway-to-gateway Fabric tunnel, with this gateway acting as the receiving end.
- Inbound GW2 (connection point): this gateway acts as a connection point receiving a call forwarded from an upstream gateway.
- Outbound: the Managed Agent sends a request through a Transit Point to an upstream target.
Condition shorthand used in this table:
- If Caller Context configured: the surface has a Caller Context element, whether or not the request’s credentials validated.
- If TR enabled: Extract Trust Registry Data is enabled on the surface.
- If TC elements configured: at least one Trust Check element is configured for this leg.
- If VP present: a verified Verifiable Presentation was included in the request body.
- If binding VP present: a verified VP from an upstream gateway was processed at the connection point.
- If x402 verified: an x402 payment was cryptographically verified on the request.
✓ = present when the stated condition is met. — = never present on this path.
| Field | Inbound direct | Inbound G2G (fabric send) | Inbound GW2 (connection point) | Outbound |
|---|---|---|---|---|
input.http | ✓ | ✓ | ✓ | ✓ |
input.http.headers | Filtered | Filtered | Filtered | Filtered, from the original inbound request |
input.gateway | ✓ | ✓ | ✓ | ✓ |
input.gateway.direction | "inbound" | "outbound" | "inbound" | "outbound" |
input.gateway.source_id | null | null | Caller DID | Managed agent DID |
input.gateway.target_id | null | Remote gateway DID | null | Target endpoint URL |
input.channel | ✓ | ✓ | ✓ | ✓ |
input.source_auth | If Caller Context configured | If Caller Context configured | If Caller Context configured | If the Transit Point authenticated the caller |
input.mcp | If MCP protocol | — | — | — |
input.a2a | If A2A | If A2A | If A2A | — |
input.agent | If TR enabled | If TR enabled | If TR enabled | If TR enabled |
input.extension_identity | If VP present | — | If VP present | — |
input.payment | If x402 verified | — | If x402 verified | — |
input.identity_binding | — | — | If binding VP present | — |
input.trust_check_results | If TC elements configured | If TC elements configured | If TC elements configured | If TC elements configured |
input.transit | — | — | — | ✓ |
MCP tool policy input
MCP tool policies share the same package declaration (package surface.policy) and query path (data.surface.policy.allow) as surface policies, but use a different input struct (McpPolicyContext) with different field paths.
Do not use input.source_auth.* in an MCP tool policy. That field is absent from this input. Use input.jwt for JWT claims. A rule referencing a field that does not exist evaluates to undefined and silently denies the request.
Full example
{
"mcp": {
"method": "search",
"params": { "query": "hello" },
"protocol": "json-rpc-2.0"
},
"jwt": {
"role": "admin",
"org": "acme-corp",
"sub": "user@example.com"
},
"channel": {
"name": "my-mcp-surface",
"id": "surface-abc123",
"protocol": "mcp"
},
"request": {
"source_ip": "203.0.113.42",
"method": "POST",
"path": "/v1/surfaces/my-mcp-surface/mcp"
}
}Fields
| Field | Type | Description |
|---|---|---|
input.mcp.method | string | The tool name from the tools/call request: the value the policy should gate on. |
input.mcp.params | object or null | Full parsed params from the tools/call body. |
input.mcp.protocol | string | Always "json-rpc-2.0". |
input.jwt | object | Flat JWT claims map when the caller used JWT bearer auth. Absent from the input entirely when the caller used a different authentication method. Referencing input.jwt.* without JWT auth evaluates to undefined and silently denies. Access claims directly: input.jwt.role, input.jwt.sub. |
input.channel.name | string | Surface name. |
input.channel.id | string or null | Surface ID. |
input.channel.protocol | string | Always "mcp" for MCP tool policies. |
input.request.method | string | HTTP method of the request. |
input.request.path | string | HTTP request path. |
input.request.source_ip | string | Source IP address of the caller. |
Field name mapping from PolicyInput to McpPolicyContext
| In gateway / surface policy | In MCP tool policy | Notes |
|---|---|---|
input.source_auth.claims.* | input.jwt.* | JWT claims are a flat map in McpPolicyContext. |
input.mcp.tool_name | input.mcp.method | Tool name is in method in McpPolicyContext. |
input.http.method | input.request.method | |
input.http.path | input.request.path | |
input.channel.config_id | input.channel.id | |
| Not available | input.request.source_ip | Source IP only available in MCP tool policies. |
Absent fields and Rego behaviour
Fields that are not populated are omitted entirely from the JSON input. They are not set to null unless explicitly nullable in the schema above. In Rego, referencing an absent field evaluates to undefined.
A rule block where any condition is undefined does not contribute to the allow decision. When default allow = false, a rule that silently becomes undefined causes a deny.
# This rule silently denies when input.mcp is absent (non-MCP request)
allow if {
input.mcp.tool_name == "search"
}To write rules that handle both present and absent fields safely, use object.get or check for field existence first:
allow if {
input.source_auth.method == "jwt_bearer"
object.get(input.source_auth, "claims", {}).role == "admin"
}Stripped headers
input.http.headers never contains a header named exactly authorization, proxy-authorization, cookie, or set-cookie, or any header whose name contains token, secret, credential, apikey, or api-key, matched case-insensitively. These are stripped at policy input construction time, on both inbound and outbound requests. Policies cannot read bearer tokens, API key header values, session cookies, or similarly named credential headers from input.http.headers. Use input.source_auth for authenticated identity information instead.
On a Transit Point’s outbound leg, input.http.headers carries the non-sensitive headers from the original inbound request, so a policy on that leg can check a custom header the caller sent (for example a threat-intelligence or classification header) without needing to read it from input.source_auth.
Related
- OPA policies: conceptual overview of how the gateway evaluates policies, evaluation order, and policy scopes.
- Apply OPA policies to your gateway and surfaces: create policy definitions and attach them to a gateway or surface.
- Control MCP tool access with per-tool policies: per-tool policy configuration using
McpPolicyContext. - Trust registries: how
input.agent.trust_verificationandinput.trust_check_resultsare populated. - Audit log: how policy version and content-hash evidence appears on Audit Log rows and in the event detail panel.
Glad to hear it. Thanks for letting us know.
Sorry to hear that. Thanks for letting us know.
Still stuck? Ask the community on Discord or contact us.