Skip to main content
Every tool call routes through a PolicyEnforcer that returns a typed Decision. Deny-by-default; the policy file lists what’s allowed.
  • allow — the original tool runs unchanged.
  • deny — the framework sees a [policy_denied] marker as the tool result; the model can recover.
  • approval_required — calls a dev-supplied handler, or returns an [approval_required] marker (see approval-required tool calls).
The decision is built from the tool name and its arguments against the active policy bundle. The caller’s role is a third input, but it isn’t passed at the call site — decide resolves it internally from the active request context via a contextvar.

Where enforcement happens

Enforcement sits at the tool boundary. When you wrap or load an agent, each tool is replaced with a guarded version that runs the enforcer before the real tool:
  1. The model emits a tool call; the framework dispatches it.
  2. The guarded tool asks the enforcer to decide, passing the tool name and its arguments. The caller’s role isn’t a parameter — it’s resolved from the active request context (a contextvar), so the call site only supplies the tool name and arguments.
  3. The enforcer evaluates the call against the active policy. Two engines back this — an in-process pydantic engine (the default) and a WASM engine (what production runs) — and both return the same Decision.
  4. allow → the original tool runs. deny → the model sees a [policy_denied] marker as the tool result. approval_required → the dev-supplied handler is called, or an [approval_required] marker is returned.
  5. At the start of every turn — each agent run (one ainvoke / astream_events call, typically handling a single user message) — the policy is refreshed once, up front. If it changed, the new version takes effect on the tool calls in that run — no restart, no re-wrapping.
The refresh source is chosen automatically from your environment:
  • HEXGATE_LOCAL_POLICY set → a local YAML file or bundle directory, refreshed on file change
  • HEXGATE_API_KEY set, no local override → the platform, refreshed via ETag / 304 Not Modified
  • Neither → no refresh; enforcement uses whatever was loaded once

What the per-turn refresh covers

Only the policy. The system_prompt, the manifest’s tool list, and the model id are read once at agent construction and stay fixed for the life of the process. Edit those on the dashboard and the change lands at the next hexgate serve restart — not at the next turn. That split is intentional: policy is the operator’s live, per-decision-auditable lever, while the manifest (tools, model, prompt) is owned by your code.

approval_required without a handler

  • No handler attached — behaves like a graceful block: the tool returns a structured ok: False result with error_type: "approval_required" so the agent can try a fallback instead of crashing.
  • Handler attached (via enforce_policy(..., approval_handler=...)) — the host decides whether to allow the action at runtime.