Skip to content

Build1 publisher2 min readPublished

agentgateway ORs the two CEL rules a governance policy lists under matchExpressions

Written as separate entries, a country check and a model allowlist become alternatives, so a caller from a restricted jurisdiction gets through by naming an approved model, and the policy still reports Accepted and Attached.

The Engineer · Build desk

Illustration accompanying agentgateway ORs the two CEL rules a governance policy lists under matchExpressions

What happened

  • A two-entry CEL policy on an agentgateway route named governed-llm let through callers that either of its two rules, jurisdiction and approved model, should have refused on its own.
  • The test used two Keycloak users from the same realm and group, identical except for the country claim, maria set to US and pat set to IR.
  • Rewriting the country check and the model allowlist as a single matchExpressions entry joined with && produced the intended allow and deny results.
  • A version that read the model from llm.requestModel instead of the request body returned 403 to every caller on the route.
  • Both broken versions reported Accepted and Attached in the policy status and raised no error.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Accepted and Attached say the policy parsed and bound to the route, so a healthy status cannot be used as evidence that an LLM route is actually governed.
  • exposure With the rules OR'ed, the jurisdiction control holds only while the caller does not also name an approved model, which puts the compliance outcome in the hands of the request body.
  • decision An authorization test set built only from requests expected to fail will pass a route that refuses everything, so the suite has to include at least one request that must succeed.

Count the combinations. Two boolean conditions give four cases: country permitted or not, model approved or not. A list of entries is satisfied when any one entry evaluates true, so three of those four are allowed, while the rule the policy was written to express allows exactly one. The only request the two-entry version refuses is the one that breaks both rules at once.

The fail-open direction is the one worth writing down. One entry per rule is the natural way to write the policy, it looks correct in review, and it permits traffic. "Nothing surfaces until someone audits denials that never happened," the author wrote. The documentation does state that requests which do not match any of the conditions are denied, in one sentence; next to a YAML block with a list under it, a list of conditions looks like a list of requirements.

The second failure is about when a variable exists. agentgateway exposes an `llm` context carrying the model, the provider, token counts, and realized cost. Reading the model from there is cleaner than parsing the request body, and `llm.requestModel` is documented, but it belongs to the backend AI phase, which runs after routing has selected a backend. A `traffic.authorization` policy runs earlier, at the route level. The `llm` context is not populated yet, the expression cannot evaluate true, and a policy whose action is `Allow` denies everything. The 403s covered a request naming a model that was literally in the approved list.

Both results came from a live cluster on v2026.8.2, with the demo repo and every command in it run before publishing on 2026-09-18. For the OR behavior to reach another cluster, very little has to be true: an `Allow` policy with more than one entry under `matchExpressions`. The model names, the `country` claim and the Keycloak realm are incidental to it. The phase-ordering bug needs one more condition, that your build still populates the `llm` context only after backend selection, which is where the documented behavior puts it.

If the allowlist is per-credential, agentgateway v1.5.0 put an `allowedModels` list on the API key itself. The CEL route policy is for decisions that depend on something in the token instead of on which key was presented.

What to watch

  • Whether Solo moves the OR semantics out of one line of reference prose and into the YAML example itself.
  • Whether a later agentgateway release errors on a CEL variable that cannot resolve at the route level instead of evaluating it false.
  • Whether the allowedModels list on the API key gets a claim-based equivalent, so a token-driven allowlist does not have to be hand-joined CEL.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories