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: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.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).
* in the middle of a pattern is a literal asterisk.
Failing closed
Every path that is not an unambiguoustrue 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.
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?
Belongs in CEL
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.
Belongs in Cedar
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.
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.
Related: Policy overview, Cedar authorization, MCP surface, Virtual keys, Invariants.