---
title: "Policy overview"
description: "How Obol decides what an agent may see and what it may execute: a CEL visibility prefilter and a Cedar authorization decision, both evaluated in-process in the gateway."
---
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
| | CEL prefilter | Cedar authorization |
|---|---|---|
| Question | Is this tool or model visible to this key? | May this principal take this action on this resource, with this context? |
| Runs on | Every `tools/list` entry, every `/v1/*` model check, and again before every `tools/call` | `tools/call`, `tools/list`, capability routing, model completion |
| Input | `KeyContext`, `ToolSnapshot`, workspace `Env`, method | The compiled policy bundle, the entity set, and the request context including arguments |
| Cost | Fixed, eagerly built context; programs compiled once | Entity hydration plus policy evaluation, cached per publication |
| Failure | Hidden | Deny |
| Code | `apps/gateway/crates/obol-policy/src/cel/` | `apps/gateway/crates/obol-policy/src/cedar/` |
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:
| Action | Principal → resource | Used for |
|---|---|---|
| `call` | `Agent` → `Tool` | The authorization of record for `tools/call`. Carries arguments-derived context. |
| `list` | `Agent` → `Tool` | Tool discovery. |
| `route` | `Agent` → `Tool` | Per-candidate filter before capability provider selection. Cheap on purpose. |
| `complete` | `Agent` → `Model` | Model access on `/v1/*`. |
| `read` | `User` or `Agent` → `Workspace`, `Connection`, `Tool` | Read-side checks. |
`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.
An operator stores a Cedar draft through the control plane. Storing a draft compiles nothing and publishes nothing.
Publishing compiles the stored draft plus a generated entity set into a `PolicySnapshot` — `workspace_id`, `policy_id`, `revision_id`, `cedar_text`, `entities_json`, `hash`, `published_at`.
Control writes the workspace and policy snapshots as one monotonic pair with a coherence fingerprint, then publishes an invalidation on `obol:invalidate`.
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](/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](/receipts/overview).
## Where to go next
Schema, entity shape, argument conditions, example policies, fixtures.
The fixed context, the built-in expressions, and the CEL/Cedar boundary.
How a destructive prod call is held and resumed after a human decision.
Draft, compile, publish, and how the data plane picks up changes.
Related: [Invariants](/concepts/invariants), [Gateway overview](/gateway/overview), [MCP surface](/gateway/mcp), [Virtual keys](/security/virtual-keys), [Policies API](/api-reference/policies).