Skip to main content
The hexgate policy subcommands author, inspect, and compile policies without a running platform. See WASM bundles for what build produces and signing for keygen.

Prerequisite — opa

The WASM compile step shells out to the Open Policy Agent binary. Install it once:
Without opa on PATH, hexgate policy build --no-wasm still emits the yaml + rego (no .wasm), and the pydantic engine keeps working. Override the binary location with HEXGATE_OPA_BIN.

Reading a denied test

On a denied decision, test prints the reason; the wasm engine additionally lists each violated constraint string verbatim:

Multi-role dry-runs (--roles)

--role evaluates one role; --roles a,b evaluates a whole set exactly as the runtime does — every role is evaluated and the most permissive outcome wins, so the output also names the role that granted the call:
The two flags differ on undefined names, deliberately:
  • --role nope fails — a single undefined role is almost always a typo.
  • --roles support,nope warns and continues, evaluating nope against the default policy. That is what happens at runtime, and a dry-run that refused would disagree with production.
  • --roles "" fails — an empty set names no roles. Scripting this as --roles "$ROLES" with an unset variable would otherwise dry-run the default policy and exit 0, passing CI while asserting nothing. Pass --roles default if the fallback policy is what you meant to test.
At most 32 distinct roles are evaluated, matching the enforcer; past that the dry-run warns on stderr and evaluates the first 32, as production would. That fallback is also why default should stay least-privilege — see request context. hexgate policy validate warns when the default role grants something no named role grants, and when the document declares roles but never names one default (so one of them silently becomes the fallback):
validate also fails when roles set different guards: settings, since the runtime builds one guard pipeline per agent. It flags a constraint path no call ever sets (unknown-root): one that doesn’t start with args., ctx., run., agent_usage. or user_usage., such as user.department == "finance", or one that dots into role or tool, which are plain strings. The path is always missing, so the comparison is always false: a warning, or an error under an odd number of not, where it is always true and the fence never fires. It also flags an agent_usage.* or user_usage.* cap under an odd number of not (negated-usage-cap), such as not (agent_usage.denials_5m >= 5) or a boundary deny on agent_usage.denials_5m >= 5, which the linker wraps in not. Under usage: {on_unavailable: deny} a value the platform can’t supply is missing, so the cap is true and the call passes. It is an error when the policy sets on_unavailable: deny, and a warning on a module, which can’t know the agent’s fail mode. Write the cap as a plain comparison: agent_usage.denials_5m < 5. Pass the agent’s manifest to check the policy against its code too. At the default --max-severity error, these fail validate:
  • a guards: rule naming a guard the agent doesn’t declare (unknown-guard), or one two of its guards share (ambiguous-guard);
  • a rule on a tool or argument the agent doesn’t have that leaves the real tool less restricted than intended (unknown-tool / unknown-arg at error).
An admission, agents or skills rule’s args.* are checked against what its gate passes: agent for admission; agent, target and via for reach; skill, via, file_path and content_hash for a skill, plus script_args, short_options and positional_args for a script. The rest of the drift is printed but passes: a grant that never fires or an argument typo that fails closed is a warning, and a deny on a missing tool is info when the role’s default already denies it. A skills: rule on a skill the agent doesn’t declare (unknown-skill) is a warning, or info for a deny: an unlisted skill is denied either way. It is only checked when the manifest lists the agent’s skills; a manifest with no skills field skips it. Add --max-severity warning to fail on the warnings too:

Testing ctx.* attribute rules

Constraints can filter on caller attributes via ctx.* (see constraints). Pass them to test with --attributes, a JSON object so numbers and booleans keep their type:
A ctx.* key you don’t pass is treated as missing and fails closed, exactly as at runtime when the request scope omits it.

Testing run.* and usage rules

--run-facts feeds run.* constraints (this run’s counters), --agent-usage feeds agent_usage.* constraints (this agent across runs) and --user-usage feeds user_usage.* constraints (one end user across agents). Each takes a JSON object, and a path you don’t pass reads zero, as on a run’s first call or in a fresh process. That is how you dry-run a limit at its threshold:
See constraints for the paths each one exposes.

Policy modules: resolve and check

A project can split its policy into modules under policies/: boundaries in policies/boundaries/ set ceilings and denies, capabilities in policies/capabilities/ grant. The linker folds them into one effective policy (fences intersect, grants union, denies win), and that policy is what compiles to Rego and WASM. Two commands work on the tree.

resolve: see what will be enforced

resolve links the modules and prints the effective policy, with the layers it read and any grant a ceiling shadows. With this tree:
send_email is granted by support but the org ceiling never lists it, so it is shadowed and absent from the result.

check: lint the modules

check links the same tree and reports mistakes that don’t stop it from composing. On the tree above:
It reports a capability grant a ceiling makes dead, a duplicate grant, and a constraint path no call sets. With --manifest, it also reports rules on a tool, skill or argument the agent’s code doesn’t have, as validate does. Use check --max-severity warning in CI to keep dead and duplicate grants out of the tree.