--- 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).