Cedar, OPA or CEL: which policy language for AI agent tool calls?

Rules for what an agent may do need a language. Cedar, OPA's Rego and CEL are the usual picks, and they differ in ways that matter for agents: defaults, how rules combine, and what an error does.

The short answer

All three can express “a refund over 100 needs a person.” The differences that matter for agents are elsewhere:

  • Cedar answers allow or deny. A satisfied forbid beats any permit, and with no matching permit the answer is deny. A policy that errors is skipped.1
  • OPA’s Rego computes any value you like. A default applies when every rule of that name is undefined, and several definitions of the same rule combine as an OR.2
  • CEL evaluates one expression your program supplies data to. It runs in linear time, is mutation free and isn’t Turing-complete, and it’s designed to be embedded in applications.3 Defaults and combining are up to your code.

If you need “allow, deny or ask a person,” none of them gives you three outcomes for free. You build the third one on top.

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

What an agent policy has to decide

A rule for a tool call sees the tool’s name and arguments, plus context such as who the agent acts for. It returns one of:

Outcome Example
Allow Read-only SQL on the reporting database
Deny DROP TABLE, a refund to an account not on file
Ask a person A refund over 100, an email to an outside domain

Plus one rule that’s easy to forget: when the policy can’t decide, the answer must be deny or ask, never allow. Agents send odd arguments: missing fields, strings where numbers should be, lists of ten thousand items. Your tool-call policy will meet all of them.

One rule, three languages

CEL. We ran this with cel-go v0.32.0, the version the Go module cache had, giving the program tool and args:

Code Program code, for developers. The text around it says what it does.

env, _ := cel.NewEnv(cel.Variable("tool", cel.StringType), cel.Variable("args", cel.MapType(cel.StringType, cel.DynType)))
rule := `tool == "stripe.refund" && double(args.amount) > 100.0`
ast, iss := env.Compile(rule)
prg, _ := env.Program(ast, cel.CostLimit(1000))

Code What the screen or file shows, copied as is.

amount=40 needs_approval=false err=<nil>
amount=250 needs_approval=true err=<nil>
bad rule: true

The last line is a rule with a syntax error, args.amount > , which failed at compile time. That’s useful: a broken rule can be refused when it’s loaded, not when an agent first hits it.

Rego (written to OPA’s documented syntax, not run here):

Code What the screen or file shows, copied as is.

package agent

default needs_approval := false

needs_approval if {
    input.tool == "stripe.refund"
    input.args.amount > 100
}

The default line supplies false when no needs_approval rule matches; without it, the result would be undefined.2

Cedar (written to Cedar’s documented model, not run here). Cedar decides allow or deny, so “needs approval” becomes a forbid that your code turns into an approval request:

Code What the screen or file shows, copied as is.

permit (principal, action == Action::"stripe.refund", resource);

forbid (principal, action == Action::"stripe.refund", resource)
when { context.amount > 100 };

A refund of 250 satisfies both policies; the forbid wins and the answer is deny.1 Your gateway reads that deny, sees which policy produced it, and routes the call to a person instead of dropping it.

How they differ

Cedar OPA Rego CEL
Answer Allow or deny1 Any JSON value Any value of the expression’s type
No rule matches Deny1 Undefined, or the default2 Not applicable: one expression, always evaluated
Several rules Forbid overrides permit1 Same-name rules combine as OR2 Your code combines expressions
Request shape Principal, action, resource, context1 Whatever JSON you send as input Variables you declare
Evaluation cost Linear time, not Turing-complete3
Runs as Library or service Library or service (OPA) Library inside your program3

Cedar’s request shape fits “who may do what to which thing” well: the agent is the principal, the tool is the action, the table or customer is the resource. Rego fits when the decision is more than yes or no, for example returning which approver a call needs. CEL fits when rules are short expressions in a system that already has its own structure, which is how Kubernetes and many gateways use it.

Errors are where agent rules fail

This is the row we’d look at first. Each language behaves differently when a rule can’t be evaluated, for example because context.amount is missing or is a string.

  • Cedar: a policy whose evaluation returns an error “does not factor into the authorization response; it is skipped,” and its ID appears in the diagnostics.1 Read that with the forbid example in mind. If the forbid errors on a malformed amount and is skipped, the permit still applies, and the answer is allow. Guard every forbid with has checks, and treat any diagnostic error as deny in your own code.
  • Rego: a rule whose body fails produces no value, and the default applies.2 With default needs_approval := false, a malformed amount means no approval needed. For agents, write the default the other way round: default allow := false, and have rules grant.
  • CEL: evaluation returns an error to your program. What happens next is your code. If it treats an error as “no match,” you have the same problem as above.

None of these is a flaw in the language. Each is a design choice that’s reasonable for its main use. Agents just happen to be very good at producing the inputs that hit the error path.

Which to pick

  • You already run OPA for Kubernetes or APIs: use Rego, and add tests for missing and mistyped arguments.
  • Your rules are about principals and resources, such as “this agent may refund this customer’s orders”: Cedar’s model maps cleanly, and its forbid-wins rule is easy to reason about.
  • You’re building a gateway or tool runner and want rules as short expressions with predictable cost: CEL, with a cost limit set.

Whichever you pick, the tool-call layer around it decides how a deny becomes an approval request. Human-in-the-loop approval and requiring approval for MCP tool calls cover that half.

A checklist for any of them

  • The final decision is deny unless a rule grants.
  • Any evaluation error, skipped policy or undefined result becomes deny or ask in your code.
  • Rules are checked when loaded; a rule that doesn’t compile is refused.
  • Tests cover missing fields, wrong types, huge lists and unexpected tools.
  • Evaluation has a time or cost limit.
  • Every decision is recorded with the rule that produced it.

To see what the agents configured on your machine could do with no rules at all, paste the config into the check page or run npx deedglass scan (how the grade works); to try rules on tool calls in your browser, open the playground.

Written with AI assistance and checked against our claims register.

The takeaway

Cedar skips a policy that errors, Rego falls back to its default, CEL hands the error to your code. For agent rules, make sure every error path ends in deny.

Read nextRequire approval for MCP tool calls: Claude Code, Cursor, then a gateway Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1Cedar docs: Authorization · docs.cedarpolicy.com · checked 26 October 2026
  2. 2Open Policy Agent docs: Policy language · openpolicyagent.org · checked 26 October 2026
  3. 3CEL specification README · github.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing