Skip to main content
A policy doesn’t have to live in YAML. You can construct the same validated model in Python, which is handy for tests, generated policies, and keeping policy next to the code it governs.

Three ways to build a policy

From a dict — the model validates it (including every constraint’s grammar):
from hexgate import AgentPolicy, enforce_policy

policy = AgentPolicy.model_validate({
    "version": 1,
    "default_policy": {"mode": "deny"},
    "tools": {
        "web_search": {"mode": "allow"},
        "refund_order": {"mode": "allow", "constraints": ["args.amount <= 500"]},
    },
})
agent = enforce_policy(agent, policy)
With PolicyBuilder — fluent, and C(...) gives you type-checked constraints instead of hand-written strings:
from hexgate import PolicyBuilder, C, enforce_policy

policy = (
    PolicyBuilder(default="deny")
    .allow("web_search")
    .allow("refund_order", when=[C("args.amount") <= 500, C("args.currency") == "USD"])
    .approve("edit_file")
    .build()
)
agent = enforce_policy(agent, policy)
C covers every operator: <= < > >= == !=, .is_in([...]), .not_in([...]), .count(), and cross-field comparisons (C("args.max") >= C("args.min")). Each constraint is validated the moment you write it, so a typo raises at that line — not later at enforcement. Role-aware, with RolePolicyBuilder — produces a PolicySet:
from hexgate import RolePolicyBuilder, PolicyBuilder, C

policies = (
    RolePolicyBuilder()
    .role("read_only", PolicyBuilder().allow("web_search"), mixin=True)
    .role("default", PolicyBuilder(), inherits=["read_only"])
    .role("billing",
          PolicyBuilder().allow("refund_order", when=[C("args.amount") <= 500]),
          inherits=["read_only"])
    .build()
)

Unit-test a policy

The assert_* helpers evaluate a policy the same way the runtime enforces it:
from hexgate import PolicyBuilder, C, assert_allows, assert_denies, assert_needs_approval

policy = (
    PolicyBuilder()
    .allow("refund", when=[C("args.amount") <= 500])
    .approve("edit_file")
    .build()
)

assert_allows(policy, "refund", {"amount": 100})
assert_denies(policy, "refund", {"amount": 999})
assert_denies(policy, "some_unlisted_tool")          # deny-by-default
assert_needs_approval(policy, "edit_file")
For a PolicySet, pass role=:
assert_allows(policies, "refund_order", {"amount": 100}, role="billing")
assert_denies(policies, "refund_order", role="default")
The builder and C emit the exact constraint strings the YAML parser accepts, so a policy built in code enforces identically to the same policy in YAML — there is only one grammar.