Skip to main content
Allowing a tool is often too blunt. You don’t want the agent to make any refund — you want refunds under $500. You don’t want it to write any file — only files under /srv/. That “only when…” is a condition. A condition is a yes/no test on the arguments of the call. Attach conditions to a tool and it runs only when all of them say yes:
That’s the whole idea. The rest of this page shows how to say more.

Three things to keep in mind

  • Conditions read the call’s arguments under args.args.amount, args.currency, args.file_path.
  • Every line must pass. Listing two conditions means both have to hold.
  • When in doubt, it’s a no. If an argument is missing or the wrong type, the condition fails and the call is denied. Safe by default — you never have to write “…and the value exists.”

Your first condition

Compare an argument to a value:
The operators are what you’d expect: ==, !=, <, <=, >, >=. Comparing to text? Quote it — double quotes:
Quotes matter. args.currency == USD (no quotes) doesn’t compare to the word “USD” — it compares args.currency to another field called USD, which almost never exists, so the call is denied. If you mean text, quote it.

One of a set

Use in (and not in) to check membership:

Combining conditions

Separate lines are already and — all must hold. When you need or, or to group things, write it inline with and / or / not and parentheses:
or binds loosest, then and, then not — the usual precedence. Reach for parentheses whenever it makes the intent clearer.

Checking the shape of text

For strings, four helpers go beyond exact match:
matches uses a regular expression and is unanchored — it looks for the pattern anywhere in the string. Add ^ and $ when you mean the whole value, like the ticket-ID example above.

Rules over a list

When an argument is a list, every and any let you constrain its elements. Inside, . is “the current element”:
  • every(list, …) — the condition must hold for all elements.
  • any(list, …) — it must hold for at least one.
  • count(list) … — compares the number of elements.
If the elements are objects, reach into them with .field:

Comparing two arguments

The right-hand side can be another argument, not just a fixed value:

Reusing values with consts

When the same value shows up in several rules, name it once in a consts: block and reference it as consts.<name>:
Change the number in one place and every rule that uses it follows. It also lets someone tune the allowlist without touching the logic. Put shared constants in a mixin (inherits:) so several roles can reuse them.

Who’s calling, and what they’re calling

Two facts about the call itself are always available:
They’re most useful with or — “under the limit, or an admin”:

A worked example

A support agent that can email customers and issue refunds, with a stricter default role and a looser billing role:

Good to know

  • Missing or wrong-typed arguments deny. A numeric rule like args.amount <= 500 won’t be fooled by a string arriving in args.amount — it fails closed.
  • Dev and production agree. The same conditions are enforced identically in local development and in a compiled, signed policy bundle — there’s one grammar, checked both ways.
  • Not yet supported: and/or/not inside a quantifier body (e.g. every(args.x, .a == 1 or .b == 2)), and in against another field.

Where to next