Skip to content

Handling allow/deny decisions

When the gateway denies a tool call, the SDK raises an exception at the point where the tool would have run. This guide shows how to catch those decisions, which exception to catch, and how to test a policy without actually blocking anything.

The decision flow

Every governed tool call goes through the gate before it executes:

flowchart TD
    Call["Agent invokes a tool"]
    Gate["Governance gate<br/>asks the gateway"]
    Allow["allow → tool runs normally"]
    Redact["redact → tool runs, secrets stripped"]
    Deny["deny → SDK raises<br/>ToolExecutionBlockedError"]

    Call --> Gate
    Gate -->|allow| Allow
    Gate -->|redact| Redact
    Gate -->|deny| Deny

A deny surfaces in your code as a raised exception; allow and redact let the call proceed (with redaction applied for the latter). So "handling a deny" is ordinary Python exception handling.

The exception hierarchy

Every error the SDK raises inherits from agent_assembly.AssemblyError, so a single except AssemblyError: is a safe backstop. The tree:

AssemblyError                       (base — catches everything from the SDK)
├── AgentError                      (agent registration / lifecycle)
├── PolicyError                     (policy evaluation problems)
├── GatewayError                    (network / HTTP transport to the gateway)
├── ConfigurationError              (bad init_assembly() arguments)
├── AdapterValidationError          (an adapter violated the FrameworkAdapter contract)
├── OpTerminatedError               (the gateway terminated an in-flight op)
└── ToolExecutionBlockedError       (a tool call was blocked by governance)
    ├── MCPToolBlockedError         (an MCP tool call was blocked)
    └── PolicyViolationError        (policy denied a tool call)

The key node for decision handling is ToolExecutionBlockedError. Both MCPToolBlockedError and PolicyViolationError derive from it, so except ToolExecutionBlockedError: catches every policy-blocked tool call regardless of which framework or path raised it.

MCPToolBlockedError additionally carries tool_name and server attributes so you can tell which MCP tool on which server was blocked.

A block is not a bug

ToolExecutionBlockedError and its subtypes are not SDK failures — they mean the policy engine did its job and denied a call. Treat them as expected control flow, not as errors to suppress.

Catching a denial

Catch the most specific exception you care about:

from agent_assembly import init_assembly, ToolExecutionBlockedError

with init_assembly(
    gateway_url="http://localhost:7391",
    api_key="dev-key",
    agent_id="my-agent",
    mode="sdk-only",
):
    try:
        result = run_my_agent()  # somewhere inside, a governed tool call happens
    except ToolExecutionBlockedError as blocked:
        # The gateway denied a tool call. Decide what to do:
        #   - log it and continue with a fallback,
        #   - surface a friendly message to the user,
        #   - re-raise to fail the run.
        log.warning("tool call blocked by policy: %s", blocked)
        result = fallback_response()

To distinguish an MCP block specifically:

from agent_assembly import MCPToolBlockedError, ToolExecutionBlockedError

try:
    run_my_agent()
except MCPToolBlockedError as blocked:
    log.warning("MCP tool %r on server %r blocked", blocked.tool_name, blocked.server)
except ToolExecutionBlockedError as blocked:
    log.warning("tool call blocked: %s", blocked)

Seeing what would be blocked, without blocking

To roll out a policy safely, register the agent in observe (dry-run) mode. Every action proceeds, but the gateway records would-be violations as shadow audit events — so nothing in your agent raises, and you can review what the policy would have denied:

with init_assembly(
    gateway_url="http://localhost:7391",
    api_key="dev-key",
    agent_id="my-agent",
    enforcement_mode="observe",  # dry-run: record, don't block
):
    run_my_agent()

Switch back to enforcement_mode="enforce" (or omit it — the gateway defaults to enforce) when you're confident the policy is correct. See Core Concepts → Enforcement modes.

Other exceptions you may see

Exception When What to do
ConfigurationError Bad init_assembly() arguments (unknown mode, missing gateway). Fix the call; it's raised before any side effect.
GatewayError The SDK reached the network but the gateway didn't answer. Check the gateway is up and the URL/key are right.
OpTerminatedError The gateway terminated an in-flight op; carries the originating op_id. Correlate via op_id; the awaited operation was cancelled server-side.

For message-by-message remedies, see Troubleshooting.