---
title: "CEL visibility prefilter"
description: "The cheap in-process check that decides whether a tool or model is visible at all: its fixed context, its built-in expressions, and where the CEL/Cedar boundary sits."
---
The CEL prefilter answers one question: is this tool or this model visible to this key right now? It runs on every `tools/list` entry, every model check on `/v1/*`, and again immediately before every `tools/call`. It never decides permission.
The implementation is `apps/gateway/crates/obol-policy/src/cel/`. It uses the upstream `cel-interpreter` crate (0.10) with a fixed, eagerly built context — not a vendored fork and not a lazily resolved request context (ADR-0011).
## Built-in expressions
Three expressions compile once at startup and are combined in Rust for tools, and one for models:
```text
tool visible ⇔ key.allowed_tools.exists(p, glob(p, tool.name))
∧ tool.env == key.env
∧ !(method == "tools/list" ∧ tool.destructive ∧ env == "prod" ∧ !explicit)
model visible ⇔ key.allowed_models.exists(p, glob(p, model))
```
The destructive-hiding clause is what serves invariant 6. A glob grant such as `stripe.*` hides destructive tools from `tools/list` in prod; only a verbatim allowlist entry — `stripe.create_refund` — reveals one. The tool is still callable if the key names it and Cedar permits the call; hiding is about discovery.
## The context
The context is fixed. Nothing else is in scope, for built-in and customer expressions alike.
| Variable | Fields | Source |
|---|---|---|
| `key` | `allowed_tools`, `allowed_models`, `env`, `agent_id`, `tools_mode` | The authenticated `KeyContext` |
| `tool` | `name`, `destructive`, `connector`, `env` | The `ToolSnapshot`; `env` is the workspace env the tool lives in |
| `env` | — | Workspace environment as a string |
| `method` | — | `"tools/list"`, `"tools/call"`, or `"complete"` |
| `model` | — | The requested model name, or empty |
| `explicit` | — | True when the tool name appears verbatim in `key.allowed_tools` |
`explicit` is computed in Rust because CEL has no "matched exactly, not by glob" primitive. `tools_mode` is `"full"` or `"progressive"`.
One custom function is registered: `glob(pattern, value)`.
```text
* matches anything
prefix* matches by prefix
*suffix matches by suffix
exact matches only itself
```
The grammar is case-sensitive, an empty pattern matches nothing, and a `*` in the middle of a pattern is a literal asterisk.
## Failing closed
Every path that is not an unambiguous `true` results in **hidden**:
- a parse error, a runtime error, or an undeclared variable
- a non-boolean result
- a missing tool or a missing model
- an empty allowlist
`Prefilter::compile` additionally wraps the parser in `catch_unwind`, because the antlr-generated parser in `cel-interpreter` 0.10 panics on some malformed input rather than returning an error. A panic is mapped to a policy error, so a bad expression can never take a gateway replica down.
That guard depends on the default `panic = "unwind"` profile. Setting `panic = "abort"` anywhere in the gateway workspace reopens the crash vector.
Programs and the function registry are built once and shared; per-call cost is roughly 8 µs in a debug build, asserted by a test that runs 10,000 evaluations in under a second.
## CEL or Cedar?
Tool-name allowlists and globs. Environment matching. Hiding destructive tools from discovery. Model allowlists. Anything that is a cheap predicate over data already in memory.
Anything about permission. Argument conditions such as a refund cap. Approval gates. Per-agent grants. Anything a security reviewer needs to read as a rule, and anything that must appear on a receipt.
The rule is short: **CEL decides visibility, Cedar decides permission.** Tiny expressions only — never business rules. A CEL decision is not recorded as a matched policy on a receipt; only Cedar decisions are.
If you find yourself wanting request-body or header predicates in a visibility rule, that is a Cedar context question first.
## Customer expressions
`Prefilter::compile` and `Prefilter::eval` exist so a customer-authored visibility expression can run against exactly the context above and nothing more, validated at publish time by control. The same fail-closed rules apply.
{/* TODO: Confirm whether customer-authored CEL expressions are exposed through any published control-plane API surface today; the gateway seam exists, but no operator endpoint for authoring them was found. */}
Related: [Policy overview](/policy/overview), [Cedar authorization](/policy/cedar), [MCP surface](/gateway/mcp), [Virtual keys](/security/virtual-keys), [Invariants](/concepts/invariants).