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