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
forbidbeats anypermit, and with no matchingpermitthe answer is deny. A policy that errors is skipped.1 - OPA’s Rego computes any value you like. A
defaultapplies 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.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
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
forbiderrors on a malformed amount and is skipped, thepermitstill applies, and the answer is allow. Guard everyforbidwithhaschecks, and treat any diagnostic error as deny in your own code. - Rego: a rule whose body fails produces no value, and the
defaultapplies.2 Withdefault 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.