Skip to main content

guardrails.tool_approvals

Capability-native async tool approval flow.

When an Agentspec marks tools as requiring human approval, this module provides:

  1. ToolApprovalConfig — configuration for the approval flow.
  2. ToolApprovalManager — manages approval records locally and waits for human decisions via asyncio.Event (in-process signaling over WebSocket).
  3. ToolsGuardrailCapability — a pydantic-ai AbstractCapability that intercepts tool calls needing approval via before_tool_execute.

ToolApprovalConfig Objects

@dataclass
class ToolApprovalConfig()

Configuration for the tool-approval flow.

from_spec

@classmethod
def from_spec(cls, spec_config: dict) -> ToolApprovalConfig

Build from Agentspec configuration.

Expected structure::

tool_approval:
tools:
- "deploy.*"
- "send_email"
- "write_file"
timeout: 300

ToolApprovalTimeoutError Objects

class ToolApprovalTimeoutError(GuardrailBlockedError)

Raised when the approval request times out.

ToolApprovalRejectedError Objects

class ToolApprovalRejectedError(GuardrailBlockedError)

Raised when the tool call is rejected by the human.

ToolApprovalExecutionReservationError Objects

class ToolApprovalExecutionReservationError(GuardrailBlockedError)

Raised when an approval cannot be reserved before tool execution.

ToolApprovalManager Objects

class ToolApprovalManager()

Manages tool approval requests using in-process asyncio.Event signaling.

When a tool call needs approval, an approval record is created in the local in-memory store (visible to WebSocket clients via snapshots). The manager then blocks on an asyncio.Event until a WebSocket tool_approval_decision message signals a decision. No HTTP polling is required — the event is signaled entirely in-process.

close

async def close() -> None

Compatibility no-op for adapter cleanup paths.

Some adapter flows call await approval_manager.close() after approval rounds. This manager currently owns no external resources, so shutdown is intentionally a no-op.

requires_approval

def requires_approval(tool_name: str) -> bool

Check whether a tool requires human approval.

request_and_wait

async def request_and_wait(tool_name: str,
tool_args: dict[str, Any],
tool_call_id: str | None = None) -> dict[str, Any]

Create a local approval record and block until a decision arrives.

The decision is delivered by signal_approval_event which is called from the WebSocket stream loop when a tool_approval_decision message is received from the frontend.

ToolsGuardrailCapability Objects

@dataclass
class ToolsGuardrailCapability(AbstractCapability[Any])

Capability that gates tool execution behind async human approval.

When a tool requires approval, an in-memory approval record is created and broadcast to WebSocket clients via snapshot. Execution is suspended on an asyncio.Event until the frontend sends a tool_approval_decision message, which signals the event and resumes (or rejects) the tool call.

handle_deferred_tool_calls

async def handle_deferred_tool_calls(
ctx: RunContext[Any], *,
requests: DeferredToolRequests) -> DeferredToolResults | None

Inline-handle deferred approvals that already have a local decision.

This lets pydantic-ai continue the run in a single call when the corresponding approval was already approved/rejected (e.g. from a recent turn or a mirrored WS decision), while unresolved approvals still bubble out as DeferredToolRequests for the stop-the-world flow.