Obol evaluates two policy layers on every request that touches a tool or a model, and both run inside the Rust gateway process. CEL decides visibility — whether a tool or model is even shown. Cedar decides permission — whether a specific call, with specific arguments, may execute. Both default to closed. The boundary rule is fixed by ADR-0003: CEL decides visibility, Cedar decides permission. Business rules never live in CEL, and no decision is ever a network hop or a database query.
Policy governs what can execute. It does not make the model choose well. Obol never guarantees that an agent picks the right tool — only that the wrong tool cannot execute.

The two stages

A tool that fails the CEL prefilter is never handed to Cedar. A tool that passes CEL still faces the full Cedar decision.

Default-deny in both directions

  • Cedar is default-deny with forbid overriding permit. No matching permit means deny.
  • Any Cedar evaluation error, context error, request-construction error, or entity error yields Deny. A forbid that errored is skipped by Cedar itself, so Obol fails closed instead.
  • Any CEL parse error, runtime error, undeclared variable, non-boolean result, missing tool or model, or empty allowlist means hidden.
  • If the gateway cannot compile the workspace’s policy bundle at all, the request stops as not-ready rather than executing.
Invariant 6 — tools/list never shows a tool the key cannot call — is enforced by both layers together. CEL hides destructive tools in prod unless the key names them verbatim; Cedar’s list decision must allow, and the tool must be call-reachable, before it appears in a listing.

Actions in the schema

The Cedar schema declares five actions, all in the Obol namespace: route deliberately carries the same cheap context as list: it answers “may this key reach this tool at all”, not “may this key make this specific call”. The selected provider still faces the full call decision with its argument conditions and approval gate intact.

How policy reaches the gateway

Policy is compiled data, distributed as snapshots. There is no network hop and no database query per decision.
1

Author in control

An operator stores a Cedar draft through the control plane. Storing a draft compiles nothing and publishes nothing.
2

Compile

Publishing compiles the stored draft plus a generated entity set into a PolicySnapshotworkspace_id, policy_id, revision_id, cedar_text, entities_json, hash, published_at.
3

Publish to Redis

Control writes the workspace and policy snapshots as one monotonic pair with a coherence fingerprint, then publishes an invalidation on obol:invalidate.
4

Load in the gateway

The gateway’s snapshot cache holds one immutable workspace/policy pair per workspace and pins it for the duration of a request. Cedar authorizers are compiled once per publication and cached.
See Policy publishing for the version, retry, and readback model.

What lands on the receipt

Every authorization decision is stamped on the invocation’s receipt: policy_id, revision_id, decision, the matched_policies that produced it (the @id(...) annotations from the policy source), and approval_id when an approval was involved. A call that Cedar denies or that is held for approval still carries the decision.
The decision block records which policy decided. It does not, by itself, describe the vendor outcome. What Obol proves after a call is tier-dependent — see Receipts.

Where to go next

Cedar

Schema, entity shape, argument conditions, example policies, fixtures.

CEL prefilter

The fixed context, the built-in expressions, and the CEL/Cedar boundary.

Approvals

How a destructive prod call is held and resumed after a human decision.

Publishing

Draft, compile, publish, and how the data plane picks up changes.
Related: Invariants, Gateway overview, MCP surface, Virtual keys, Policies API.