> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hexgate.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Policy decision

> The Decision object — allow, deny, approval-required — at the heart of every tool call.

Every tool call routes through a `PolicyEnforcer` that returns a typed `Decision`.
Deny-by-default; the [policy file](/policy/yaml-shape) 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](/concepts/approval-required)).

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](/concepts/user-scope) 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](/concepts/user-scope) (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.
