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:
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:
--role nopefails — a single undefined role is almost always a typo.--roles support,nopewarns and continues, evaluatingnopeagainst thedefaultpolicy. 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 thedefaultpolicy and exit 0, passing CI while asserting nothing. Pass--roles defaultif the fallback policy is what you meant to test.
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-argaterror).
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:
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:
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:
--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.