Cedar is the authorization of record. It is embedded in the gateway through the cedar-policy crate and evaluated synchronously, in-process, with no I/O. The schema, the example policy pack, and the fixtures live in packages/cedar/ and are the cross-language contract between the Rust gateway and the Python control plane (ADR-0017).

Schema

packages/cedar/schema.cedarschema declares the whole Obol namespace:
The hierarchy is Tool in Connection in Workspace, and Agent in Workspace. Policies are strictly validated against this schema at compile time — a policy that does not validate is never published.

Entity shape

Control publishes entities as JSON in exactly the shape Entities::from_json_value accepts. Each entry is {uid: {type, id}, attrs, parents}. Decimal attributes use Cedar’s extension encoding with one to four fractional digits.
packages/cedar/fixtures/entities.json is the reference set: one workspace, two agents, two connections, four tools, and two models.

Request-state binding

Several virtual keys can share one agent identity. Publishing one key’s attributes onto that shared agent would let another key inherit its refund cap, so for live requests the gateway projects the principal rather than reading it from the published set (ADR-0040):
  • The Obol::Agent entity — env, key_prefix, tools_mode, max_refund_usd from the key’s attributes, plus workspace membership — is rebuilt from the authenticated KeyContext on every evaluation. It replaces, rather than merges with, any published agent of the same id, so an omitted constraint cannot survive from another or a revoked key.
  • A Tool entity missing from the published set is synthesized from its ToolSnapshot.
  • Published entities still supply every other identity and resource. Raw offline fixtures keep their explicit entity inputs untouched.
A key attribute that is not a decimal where the schema expects one is an entity error, and an entity error is a deny.

Example policies

packages/cedar/policies/support-agent.cedar is the shipped example pack. Every policy carries an @id annotation, because those ids are what land on receipts.

Argument conditions

The refund cap is the canonical argument condition: the amount comes from the call context, the cap from the principal.
context.amount_usd is derived in the gateway by reading the tool’s amount_path out of the call arguments and dividing by its amount_divisor. The gateway rejects a negative amount before authorization, because a lessThanOrEqual cap on its own would allow one.

Resource sets

Cedar 4.x does not accept a set of entities in the policy scope. A set must move into the when clause.

Forbids

A forbid overrides every permit. The approval gate and the absolute prohibition are both forbids, and only their @id tells them apart.
A forbid whose @id ends in needs-approval is the approval gate. When it is the only reason a destructive prod call was denied and the call is not yet approved, the gateway returns approval_required instead of deny. Any other forbid stays a plain deny, and so does a deny with no matched policy at all — no approval could ever flip a missing permit. A workspace-authored approval forbid must be named with that suffix to become a ticket. See Approvals.

Authoring notes

  • decimal has no <, <=, > or >= operators. Use lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual. Decimals carry one to four fractional digits.
  • The policy scope accepts == A, in A for a single entity, or is T. Sets belong in when.
  • Annotate every policy with @id("…"). Without it the receipt falls back to Cedar’s generated policyN.
  • A workspace with no authored policy starts from a single agents-list-and-complete permit. Everything else is authored.

Evaluation and caching

Authorizer::compile parses the PolicySet, strictly validates it against the schema, and loads the entities. The compiled authorizer is cached by (workspace_id, policy_id, revision_id, hash) — the publication identity, not just the bundle content — so an identical bundle republished under a new revision still produces a verdict that names the current revision. On a compile error the cache is untouched and the gateway keeps serving with the previous authorizer. A Verdict carries the decision, the matched_policies (resolved @id annotations), any errors, and the policy_id and revision_id of the snapshot that decided.

Discovery and reachability

Listing a tool requires both the list permission and call reachability. Reachability is a partial evaluation of the call action with the principal, resource, environment, and destructive classification known, and arguments and approval left unknown:
  • A definite denial or an evaluation error hides the tool.
  • An argument-dependent residual stays discoverable and undergoes full authorization at invocation.
  • A virtual capability additionally needs at least one eligible, authorized concrete binding.
  • The same filter runs before provider selection, so a permanently forbidden provider cannot outrank a callable alternative.
This is a conservative approximation, not a satisfiability proof and not permission to execute.

Fixtures

packages/cedar/fixtures/*.json cases are the executable contract. Each is {policy, entities, request{principal, action, resource, context}, expect{decision, policy_ids}}, run by obol-policy tests, by the obol-policy-check CLI, and by control’s Python tests. packages/fixtures/cedar/ is a byte-for-byte mirror, diff-checked in CI. Run the suite, or validate a pack by hand:
A fixture expectation matches when the decision is equal and every expected policy id appears in matched_policies. Related: Policy overview, CEL prefilter, Publishing, Receipts, Virtual keys.