- Admission — may this caller, in this role, run this agent at all? Checked at the top of a run, before the model sees anything.
- Reach (delegation) — may this agent invoke or hand off to another agent? Checked at the moment it tries to.
PolicyEnforcer as tool calls,
so the decision type, the constraint language, the
audit trail, and the approval flow
all apply unchanged.
The two ways one agent reaches another
They are genuinely different, so a policy can treat them differently, via thevia modes:
- agent-as-tool (
via: tool) — the caller invokes a helper agent like a tool and folds the result back into its own answer. Control stays with the caller. - handoff (
via: handoff) — the caller transfers the whole conversation to another agent, which takes over and answers the user directly. Control does not return.
billing-bot is a bigger deal than asking it to compute one
number, so a policy can allow the tool mode freely and gate the handoff.
Write the policy
Agent rules sit next totools: in a role and read the same way — a mode plus
optional constraints:
admission to
agent.run, and each agents target to agent.tool:<name> / agent.handoff:<name> —
so the same decide() that gates every tool call runs on them, and the same
policy authoring (including modular boundaries, capabilities, and roles) composes
them. A deny written at a boundary is authoritative; grants union.
Closed-world, and opt-in
- Unlisted means denied. Once you declare agent rules, any agent you did not list is denied. You name what is in; everything else falls to deny. There is no default block to set.
- It is opt-in. An agent whose policy has no
admissionand noagentsblock is not gated at all — it runs exactly as before. The gates engage only once a block is present. - Declare
admissionon every role that should get in. Because admission is closed-world, once any role declares it the gate is on for every caller, and a role you did not grant admission is refused. Across several roles the check is permissive: any role that grants admission admits the caller.
Approvals and the depth cap
Anapproval_required agent rule uses the same human-approval
path a tool does. On a handoff the model is told the transfer was refused or is
pending and can re-plan; on admission the run waits for a yes before it starts.
A handoff transfers control forward, so a chain of handoffs can run away. Set a
per-run cap on the runner to bound it:
Framework support
Enforcement lands wherever the framework exposes the target agent at the delegation seam. Where it does not, Hexgate does not fail open silently — it logs a warning at setup, so you always know whether a rule is actually enforced.Agent-as-tool falls back to name-gating where the reach key isn’t engaged. A
sub-agent exposed as a tool is also an ordinary tool. When the policy declares a
via: tool target — an agent.tool: key — the delegation is decided under that
reach key (Google via AgentTool, OpenAI via the SDK’s tool-origin metadata),
closed-world with its via/constraints, and not also by the tool name. When the
policy declares no via: tool target — or on the native agent, pydantic_ai,
and LangChain, which expose no target handle — you gate agent-as-tool by writing a
tools: rule on the tool’s name instead. Both adapters engage on the same signal
(a declared via: tool target), so a handoff-only policy leaves as-tools
name-gated on both.Limits
- The handoff concept does not exist everywhere. pydantic_ai has no native
handoff primitive (multi-agent there is agent-as-tool, run inside a tool body),
and the native single-graph agent has no transfer seam. There is nothing to
intercept on those, by nature — a declared
agentshandoff rule warns rather than silently doing nothing. - Agent-as-tool reach on OpenAI is enforced only at the top level. The SDK
tags an
Agent.as_tool()with its target agent, so a top-level agent’sagent.tool:<name>reach rule is enforced. But a sub-agent reached as a tool runs through the SDK’s own runner on the unwrapped agent, so a reach edge one level deeper (B calls C as a tool) is not gated — only B’s tools were wrapped. Governing nested agents needs each nested agent wrapped through Hexgate; until then, deeper edges fall back to name-gating. (On an SDK too old to expose the tool origin, even the top level falls back to name-gating and avia: toolrule warns.) - Reach is governed by the source agent’s policy. When A hands off to B,
today the check is A’s
agents:rule. B re-checking its own admission when reached by a handoff is a planned addition. - Reach is gated only from a Hexgate-governed source. A transfer originating from an un-governed sub-agent is not gated.