Skip to main content
Each tool can carry a constraints: list of string expressions evaluated against the call’s arguments. Every constraint must pass for the call to authorize (implicit AND).
New here? Start with Writing conditions — a guided, example-first walkthrough. This page is the terse reference to look things up once you know the shape.
Types are compared strictly, matching Rego: a boolean is not a number, so true == 1 is false and true in [1] is false (unlike Python’s True == 1). Cross-type ordered comparisons ("x" > 10) never pass — they fail closed. Each side of a comparison is an operand — a literal, a field path, or count(...): A bare unquoted word on the right is read as a field reference, so a forgotten-quotes typo (args.x == USD) compares against the field USD (usually absent → deny) rather than the string "USD". Quote strings. Whole-line string functions take a field and a string literal: Quantifiers constrain the elements of a list-valued argument. . is the current element (.field for a sub-field), usable in any condition: Element sub-fields and nesting work too:
A non-list collection (or a missing one) fails closed. . is only valid inside a quantifier body. Besides args.*, constraints can reference two call-scope facts: role (the caller’s role) and tool (the tool being invoked) — e.g. role == "admin" or tool == "refund_order". These mirror Rego’s input.role / input.tool.

Caller attributes (ctx.*)

Constraints can also filter on the caller’s attributes — an open bag set on the request scope (HexgateContext(attributes={...})) — through the ctx.<key> namespace, alongside args.* / role / tool:
A missing ctx.<key> fails closed (deny), like any absent reference. ctx.* may gate allow/deny decisions — but it carries the same trust contract as role: populate HexgateContext.attributes from trusted server-side data (your auth/session/IdP), never from raw client input. An attribute is exactly as trustworthy as the code that set it.

Named constants

Define reusable values once in a consts: block and reference them as consts.<name> — a number, string, or list:
Change a value in one place and every tool/role that references it updates. Constants merge through inherits: like tools, so shared constants belong in a mixin. A consts.<name> that isn’t defined is rejected at build (and denies on the pydantic engine); across roles a name must map to a single value.

Boolean composition

Combine conditions with and, or, not, and parentheses:
Precedence is the usual or < and < not; use () to group. Multiple constraint lines are still AND-ed, so and is only needed to combine with an or. (Boolean ops inside a quantifier body — e.g. every(args.x, .a == 1 or .b == 2) — aren’t supported yet.)

End to end

With this billing role policy and async with HexgateContext(user_id="alice", user_roles=["billing"]):
  • refund_order(amount=200, currency="USD") → ✅ allowed
  • refund_order(amount=600, currency="USD") → ❌ denied — constraint args.amount <= 500
  • refund_order(amount=200, currency="EUR") → ❌ denied — constraint args.currency == "USD"
  • wire_transfer(amount=50000) → ✋ requires approval (mode = approval_required)
Switch to user_roles=["default"] and refund_order is missing from that policy — it falls through to default_policy.mode (deny). Constraints are Rego-compatible by design: the WASM engine compiles them to OPA Rego unchanged, and the pydantic engine evaluates the same strings in-process — both produce identical decisions (there’s a parity test suite that proves it).