- Tools: matched against the names of tools the client declares.
- MCP servers: matched against MCP server names, URLs the client declares.
- URLs: heuristically matched against outbound URLs in tool-call arguments.
- Models: regex-matched against the resolved model id (distinct from the glob-based
models_allowedallowlist, see Models dimension vs models_allowed).
deny list and an optional allow list.
Enforcement point: policy-rule checks run immediately after authentication and rate limiting, before guardrails, budget reconciliation, or the upstream provider. The tools, MCP, and URL dimensions judge the request body as sent. The models dimension runs one step later, at model resolution, and judges the resolved model. A blocked request never incurs a provider token cost, a guardrail evaluator run, or a budget debit. This is the cheapest possible reject path in the dispatcher.
How it works
deny regex is blocked → 403.
Allow semantics: if allow is non-null, it becomes an allowlist, only values matching allow pass through. Anything else is blocked. allow: null means “no allowlist, deny-only.”
Regex flavour: patterns are compiled as Go RE2. This is a linear-time regex engine, lookbehind, backreferences, and unbounded lookahead are not supported (by design, to eliminate ReDoS risk). Escape dots literally and anchor your patterns.
What gets checked
Tools
OpenAI Chat Completions: matched againsttools[].function.name.
Anthropic Messages: matched against tools[].name.
Blocked → 403 tool_not_allowed with the offending name in error.message. (In v1, block attribution flows via the X-LangWatch-Gateway-Request-Id response header → trace correlation; a dedicated langwatch.policy.violation span attribute is a v1.1 observability follow-up.)
MCP servers
If the request declaresmcp_servers (on the Anthropic Messages API), each .name and .url is matched.
Blocked → 403 tool_not_allowed (we conflate MCP denials with tool denials for the error envelope; the policies_triggered field disambiguates).
URLs
Everyhttp:// and https:// URL appearing anywhere in the request body is extracted and evaluated against policy_rules.urls. Extraction is deliberately permissive: a URL present in the payload is a URL the model could be asked to fetch, regardless of which JSON key it was nested under, user messages, tool-call arguments, system prompts, all covered. Trailing markdown/JSON punctuation (.,;:)]} etc.) is stripped and duplicates are collapsed before matching.
First URL that hits a deny rule (or falls outside a non-null allow list) → 403 url_not_allowed before the request reaches any upstream provider. Attribution via request-id correlation (see §Tools above).
Enforcement vs egress control. This layer stops a request from reaching a provider when it contains a disallowed URL. It does not prevent a provider from returning a URL that a downstream tool-runner then fetches, that requires an egress proxy on the node running the tool call, which is out of scope for the gateway. Pair blocked URLs with egress controls for defence in depth.
Models
Themodels dimension is RE2-matched against the resolved model id, the model that serves the request and gets billed, not the string that the caller typed. Alias resolution runs first. Then the rules judge its result. This is policy-by-regex (e.g. “block anything matching ^gpt-4(-turbo)?$”), distinct from the static models_allowed glob allowlist on the VK.
Judging the resolved id has two effects:
- An alias that points to a denied model is blocked. An alias named
gpt-4does not go around a rule that denies^gpt-4(-turbo)?$. - A rule that denies a name that resolves to a different model does not block. If
gpt-4is an alias forazure/my-eastus-deployment, a deny on^gpt-4$never fires. The denied name never runs.
^gpt-4.* and ^openai/gpt-4.* both reach openai/gpt-4o. Write one form. You do not need both.
The other three dimensions read the request body as sent. The gateway judges tools, MCP servers, and URLs as the client wrote them.
Blocked → 403 model_not_allowed. Attribution via request-id correlation (see §Tools above).
Models dimension vs models_allowed
Both gate which models a VK can call, but they solve different problems:
They compose: a request passes only if both checks allow it. The gateway applies
models_allowed first, at model resolution. The regex rules then judge the resolved model. The two checks read the resolved model in the two spellings, so an alias cannot go around them. Use models_allowed unless you need regex semantics, then reach for policy_rules.models.
Common policies
Block shell execution
Only allow approved filesystem paths
Only allow official MCPs
Outbound URL allowlist
Configuring via the UI
On the VK edit screen (Gateway → Virtual Keys → Edit), the Policy Rules section has four rows, one per dimension, each with a pair of monospace textareas:
The section header links to the relevant 403 error codes (
tool_not_allowed, url_not_allowed, model_not_allowed) and calls out the fail-closed behaviour: if any regex is invalid, the VK fails with 503 service_unavailable, never silent-bypass. Fix the pattern, save the VK, and the next request picks it up (the bundle refresh invalidates the broken compiled cache).
Leave a row empty to skip that dimension entirely.
Common presets
The drawer exposes common starting points (click the helper links next to each textarea):- No shell execution:
^shell\..*,^bash$,^exec$,^run_command$in Tools → Deny. - Read-only filesystem:
^filesystem\.(write|delete|chmod|move)$in Tools → Deny. - Official MCPs only:
^@modelcontextprotocol/.*,^@anthropic-ai/.*in MCP → Allow. - Internal URLs only:
^https?://api\.internal\.com/.*in URLs → Allow. - Block legacy GPT-4:
^gpt-4(-turbo)?$in Models → Deny.
Performance
Pattern checks are synchronous RE2 regex evaluations; total latency is sub-microsecond even for VKs with dozens of patterns. Patterns are compiled lazily on the first request that touches a bundle and cached on the bundle; cache auto-invalidates on config refresh.Fail-closed behaviour
If the VK’spolicy_rules config is malformed (e.g. an invalid regex), the gateway does not silently bypass policy. Instead the request is rejected with 503 service_unavailable and the gateway WARN-logs policy_rules_compile_failed + policy_rules_broken with the dimension + error text. This is deliberate: a policy engine that stops enforcing on bad config is worse than one that errors loudly. (A dedicated langwatch.policy.broken span attribute is a v1.1 observability follow-up; v1 operators correlate via the request-id header.)
Fix the policy via langwatch virtual-keys update <id> (or the UI) and subsequent requests resume immediately, the bundle refresh invalidates the broken compiled cache.
Example: blocked tool response
param field pinpoints the offending argument so client SDKs can surface a targeted error to end users. See API: Errors for the full envelope.
Combining with guardrails
Policy rules are the first policy check on a request, before any guardrail call. If a pattern matches, the request fails fast without incurring a guardrail evaluator run. Use patterns for deterministic rules (tool names, URL allowlists) and guardrails for anything that needs semantic analysis (is this prompt an injection attempt?).Auditing
Every block emits:AuditLogrow (gateway shape) with actor (user or service principal, service-account VKs useactor=svc_<projectId>), VK id, policy kind, matched pattern, offending value (truncated to 64 chars), request id.X-LangWatch-Gateway-Request-Idresponse header for joining the 403 back to the trace stream.
langwatch.policy.violation=<kind>:<pattern> span attribute is tracked as a v1.1 observability follow-up. In v1, use the audit log + request-id correlation to identify repeat offenders and tune patterns.)