Cloud and BYOC for Orca Agent Engine are in Private Preview — request an invite
Docs

Policy guardrails

Enforce Agent Engine model, tool, token, and spend rules on model and MCP traffic in Orca AI Gateway.

A policy guardrail governs an action or usage boundary with an allow, ask, or deny decision. The gateway loads the rules from the Agent Engine registry, where they are defined as guardrails, or from a local file. They are separate from the payload guardrails configured under plugins.guardrails.

FamilyConfigured underGoverns
Policy guardrailsguardrail_source, guardrail_state, guardrails_policyModel choice, tool calls, token use, and spend.
Payload guardrailsplugins.guardrails[]Request and response content, including PII redaction.

Start with the Agent Engine integration. The session JWT mapping must supply verified org_id, workspace_id, and session_id scope values, because the registry scopes each bundle to all three; agent_id is optional. Agent Engine routes MCP calls through the Gateway; its model calls reach the Gateway in colocated mode or when separate mode selects gateway egress. A separate session calls its provider directly unless the deployment default or session metadata selects the gateway.

Connect the Agent Engine registry

Use the registry internal-listener URL and workload token when Agent Engine prepares the effective policy for each authenticated session. This example uses Redis for stateful policy evaluation:

gateway.yaml
guardrail_source:
  name: registry-policy
  kind: registry
  base_url: http://orca-registry:8081
  bearer_token_file: /var/run/secrets/orca/registry/token
  on_backend_error: deny

guardrail_state:
  name: policy-state
  kind: redis
  url: redis://redis:6379/1
  key_prefix: "orca:guardrails:"
  flush_interval_ms: 2000

guardrails_policy:
  mode: enforce
  phases: [llm_request, llm_response, tool_call, tool_result]
  failure_mode:
    llm_request: deny
  on_missing: deny
  on_expired: deny
  on_unsupported_guardrail: deny
  refresh_interval_secs: 30
  max_staleness_secs: 900

The source loads a session's bundle on the session's first authenticated request. base_url is the registry internal-listener root, and the gateway rereads the bearer-token file each time it fetches a bundle. Map the registry session claims into verified principal attributes before enabling policy. See Connect Agent Engine for a complete identity configuration.

The source calls /internal/v1/guardrails/effective, so a public Workspace API key cannot authenticate this connection. Agent Engine integration also needs a separate public Workspace credential for Registry model pricing and a workload token for the Registry usage sink.

If a refresh fails, the gateway keeps using the session's last valid bundle. For a session with no bundle yet, on_backend_error decides. With deny, and in practice with last_known, the session gets no bundle and on_missing applies: the source reuses a cached bundle only while it is younger than cache_ttl_secs (900 seconds by default), so last_known has nothing older to fall back to. With allow, the gateway substitutes an allow-all bundle that expires after 60 seconds, after which on_expired applies once max_staleness_secs has passed.

A changed rule reaches a running session when the gateway fetches a newer bundle. The registry source caches each session's bundle for up to cache_ttl_secs, so a change can take that long to apply; start a new session to validate a change immediately. The gateway does not need a restart.

Choose phases and mode

The gateway wires exactly four policy phases:

PhaseBoundaryEnforced result
llm_requestBefore a model request reaches a providerA deny returns 403 policy_guardrail_denied.
llm_responseAfter model usage is knownThe decision is recorded; the completed response is not withdrawn.
tool_callBefore an MCP tools/call reaches the serverA deny rejects the whole JSON-RPC batch that contains the call.
tool_resultBefore an MCP tool result reaches the caller, including each streamed eventA deny replaces that result with a JSON-RPC error.

Harness-only phases named request and response fail configuration validation when policy is active. Registry budget rules declared on request are evaluated at llm_request instead.

mode: enforce applies the boundary behavior in the table and writes state. mode: observe evaluates policy and records the would-be decision without blocking traffic. It skips the state updates that rules make, but it still seeds state and records usage counters in the state store. mode: off disables the runtime and does not require a source.

The gateway has no approval round trip. Any policy result of ask is therefore resolved to deny, and cost-budget ask_thresholds_usd never fire here.

Only failure_mode.llm_request is read from the failure-mode map; additional phase keys are accepted but have no effect. Keep harness enforcement on alongside gateway policy: the gateway cannot pause an MCP call for the Agent Engine ask confirmation exchange, and its phases do not include the harness's request phase.

Use supported rules

The 0.4.3 runtime executes five of the builtins that the Agent Engine registry can deliver:

BuiltinPurpose
block_toolsDeny tool names matching tools.
cost_budgetLimit session cost with max_cost_usd.
user_daily_cost_budgetLimit daily cost for a stable user or owner identity.
subagent_cost_budgetLimit cost attributed to one dispatched subagent.
token_budgetLimit session tokens with max_total_tokens.

Four more builtins run only from a local bundle, because the registry catalog does not offer them:

BuiltinPurpose
model_allowlistAllow model IDs matching allowed_models, compared with the model the client requested. An empty list denies every request. A rule that sets allowed_providers counts as unsupported.
tool_allowlistAllow tool names matching allowed_tools. An empty list allows every tool.
rpm / requests_per_minuteLimit requests per subject in each UTC minute.
tpm / tokens_per_minuteLimit tokens per subject in each UTC minute.

The gateway sees an MCP tool as mcp__<backend>__<tool>, where <backend> is the request's X-Orca-Backend value. Tool patterns accept * and ? globs.

A rule the gateway cannot run affects every event at its phase, not only the events it would match. Registry bundles can contain 17 other builtins and CEL expression rules, which the gateway does not execute. With the default on_unsupported_guardrail: deny, one such rule with a tool_call phase makes the gateway deny every MCP tool call in the sessions it applies to, and deny_pii_in_llm_request with its default llm_request phase makes it deny every model request. Unsupported tool_result rules pass. skip_with_alert skips unsupported rules instead, without a log entry or metric, and leaves them to the harness. Check the catalog before you add a Workspace or organization rule to a registry that feeds this gateway.

Cost guardrails need the spend-control price model to evaluate the next model request. In enforce mode, the gateway reserves an estimate at llm_request: the request's input tokens plus its maximum output tokens, priced at the most expensive destination of a fallback chain, or at the selected destination of other routes. It settles the difference when the response reports usage; a failed dispatch leaves the estimate counted. With the default failure_mode, a model with no price passes a cost cap, and the rule's own on_unpriced parameter is ignored; set failure_mode.llm_request: deny to block unpriced models instead.

user_daily_cost_budget needs a stable subject: a verified subject_id, user_id, or owner_id, or a principal that is neither an agent nor the session itself. Without one, the rule counts as unsupported, so under the default on_unsupported_guardrail: deny every model request it applies to is denied.

When a cost budget with expensive_models denies a request for a matching model, the gateway marks the request with budget.state=exhausted and routes it once more, so a route can match on that marker and choose a cheaper destination. If routing selects a different destination, the gateway authorizes and evaluates it again and records a policy.guardrail.budget_downgraded audit event; otherwise it returns the original 403.

Manage registry rules

Create, attach, and retire registry rules with ork guardrails, the API, or an SDK, as described in Guardrails. Rules with workspace or organization scope apply to every session in their scope; rules with explicit scope apply only to agents and sessions that list them. Registry rules use Agent Engine phase names: a budget authored on request is the gateway's llm_request.

Validate policy with an agent session

This minimal workflow creates three explicit guardrails and binds one to each test session. It checks a normal request, a cost-budget decision, and a token-budget decision without applying the rules to unrelated sessions in the Workspace. Configure the ork CLI as described in the CLI overview first.

Create the three guardrails and capture their ids:

Terminal
create_guardrail() {
  local scenario="$1"
  local builtin="$2"
  local params="$3"
  local response

  response=$(ork guardrails create \
    --config-json "$(jq -cn \
      --arg name "guardrail-demo-$scenario" \
      --arg builtin "$builtin" \
      --argjson params "$params" \
      '{name:$name,scope:"explicit",phases:["request"],rule:{kind:"builtin",builtin:$builtin,params:$params}}')" \
    --output json)
  jq -r '.id' <<< "$response"
}

ALLOW_GUARDRAIL_ID=$(create_guardrail allow cost_budget '{"max_cost_usd":10}')
COST_GUARDRAIL_ID=$(create_guardrail cost-deny cost_budget '{"max_cost_usd":0.000001}')
TOKEN_GUARDRAIL_ID=$(create_guardrail token-deny token_budget '{"max_total_tokens":1}')

Create one environment and agent for the test:

Terminal
environment=$(ork agent environments create \
  --name "guardrail-demo" \
  --config-type cloud \
  --output json)
ENVIRONMENT_ID=$(jq -r '.id' <<< "$environment")

agent=$(ork agent create \
  --name "guardrail-demo" \
  --model-json '{"provider":"anthropic","id":"claude-sonnet-4-6"}' \
  --system "Answer in one short sentence. Do not use tools." \
  --output json)
AGENT_ID=$(jq -r '.id' <<< "$agent")

Create a session for each rule. agent_with_overrides.guardrail_ids binds the explicit guardrail. The sessions do not set a per-session orca_llm_egress override, so a deployment whose default model egress is the gateway can use the resulting usage records to verify that default. See Route model calls through the gateway by default for the self-hosted Helm configuration.

Terminal
create_session() {
  local scenario="$1"
  local guardrail_id="$2"
  local agent_ref
  local session

  agent_ref=$(jq -cn \
    --arg id "$AGENT_ID" \
    --arg guardrail "$guardrail_id" \
    '{type:"agent_with_overrides",id:$id,guardrail_ids:[$guardrail]}')

  session=$(ork agent sessions create \
    --environment-id "$ENVIRONMENT_ID" \
    --agent-json "$agent_ref" \
    --title "guardrail-demo-$scenario" \
    --metadata "demo_scenario=$scenario" \
    --initial-event-json '{"type":"user.message","content":[{"type":"text","text":"Introduce yourself in one short sentence."}]}' \
    --output json)
  jq -r '.id' <<< "$session"
}

ALLOW_SESSION_ID=$(create_session allow "$ALLOW_GUARDRAIL_ID")
COST_SESSION_ID=$(create_session cost-deny "$COST_GUARDRAIL_ID")
TOKEN_SESSION_ID=$(create_session token-deny "$TOKEN_GUARDRAIL_ID")

Poll each session until it is no longer running, then inspect its events:

Terminal
for session_id in "$ALLOW_SESSION_ID" "$COST_SESSION_ID" "$TOKEN_SESSION_ID"; do
  while :; do
    session=$(ork agent sessions get "$session_id" --output json)
    status=$(jq -r '.status' <<< "$session")
    [[ "$status" != "running" ]] && break
    sleep 3
  done

  ork agent sessions events list \
    --session "$session_id" \
    --limit 100 \
    --output json
done

In observe mode, all three provider requests complete. The allow session has an allow request decision, while the cost and token sessions have deny request decisions without being blocked. In enforce mode, the two deny decisions stop before provider dispatch.

For the observe run, filter the configured usage and audit backends by the three session ids. Each session should have a usage record and a policy.guardrail.llm_request audit record. In enforce mode, a denied request never reaches a provider, so it has an audit record but no usage record. For Kafka sinks, see Audit and usage records for topic naming and decoding.

Archive the temporary resources after validation:

Terminal
for session_id in "$ALLOW_SESSION_ID" "$COST_SESSION_ID" "$TOKEN_SESSION_ID"; do
  ork agent sessions archive "$session_id"
done

ork agent archive "$AGENT_ID"
ork agent environments archive "$ENVIRONMENT_ID"

for guardrail_id in "$ALLOW_GUARDRAIL_ID" "$COST_GUARDRAIL_ID" "$TOKEN_GUARDRAIL_ID"; do
  ork guardrails archive "$guardrail_id" --output json
done

Store policy state

Stateful rules need guardrail_state. Without it, every MCP tool call that a stateful rule applies to is denied, and model requests follow failure_mode.llm_request. Use kind: memory for a single-replica development setup:

gateway.yaml
guardrail_state:
  name: local-policy-state
  kind: memory

Use kind: redis when replicas must share session, subject-day, and subject-minute counters. When the source is the registry, the Redis store also batches state updates back to the registry. The gateway logs a failed write-back and does not retry it, and drops new batches when the write-back queue is full; neither rejects the current request. Monitor the gateway logs for registry write-back failures.

Use a local bundle for development

A file source can load an unsigned bundle only when you opt in explicitly:

gateway.yaml
guardrail_source:
  name: local-policy
  kind: file
  path: /etc/orca-gateway/policy.json
  high_water_path: /var/lib/orca-gateway/policy.high-water
  verification:
    allow_unsigned: true

guardrail_state:
  name: local-policy-state
  kind: memory

guardrails_policy:
  mode: observe
  phases: [llm_request, llm_response, tool_call, tool_result]

The file source keeps rollback state in high_water_path, with lock and marker files beside it. When you omit the field, it writes <path>.high-water and those files beside the bundle, so the bundle's directory must be writable; a read-only ConfigMap mount fails. A file bundle must be unexpired and hold only workspace and organization rules. The gateway checks it at startup and keeps the last good bundle when a later reload fails. Increase the bundle's generation with every edit: the gateway ignores a reloaded bundle whose generation is not higher, and fails to start on one that is lower, or equal with different content.

Use unsigned files only in a trusted development environment. Signed file sources require an ES256 public key, a stable source_id, and persistent high_water_path rollback state. The current CLI cannot bootstrap a fresh signed high-water state, so a new or lost state volume fails closed.

Denial responses

A denied model request returns 403. denied_by names the guardrail, or ask_resolution when an ask was resolved to deny. It is absent when the denial comes from policy availability - a missing or expired bundle, a state store failure, or a rule the gateway cannot run - and the message then reads guardrail policy unavailable at ... boundary:

{
  "error": {
    "code": "policy_guardrail_denied",
    "message": "policy guardrail denied: This session has reached its $10.00 budget (spent $10.02). Switch to a less expensive model to continue.",
    "denied_by": "grd_01H8..."
  }
}

A denied MCP call returns HTTP 200 with a JSON-RPC error for each call in the batch:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32001,
    "message": "policy guardrail denied: Tool mcp__github__create_issue is blocked.",
    "data": { "code": "policy_guardrail_denied" }
  }
}

Audit and metrics

With an audit sink configured, each policy decision emits an audit event whose action is policy.guardrail.llm_request, policy.guardrail.llm_response, policy.guardrail.tool_call, or policy.guardrail.tool_result, with the phase, whether it was enforced, and the bundle generation. Usage records carry the most restrictive decision in guardrail_verdict, guardrail_denied_by, and guardrail_enforced, plus a guardrail_decisions list.

The gateway exports orca_gw_guardrail_decisions_total{phase,verdict,enforced} and orca_gw_guardrail_unavailable_total{phase}. See Telemetry for scraping.

Inspect effective policy

The admin plane exposes GET /admin/v1/guardrails/effective?session_id=..., which returns the bundle already cached for a verified session. It returns 404 when no single cached bundle matches the session, 400 without session_id, and 503 when mode is off. It does not fetch a bundle, and its guardrail_state holds the bundle's seed values rather than live counters. Keep the admin listener on a trusted network; see the HTTP API reference.

On this page