Handoff Tool Policy
Secure tool boundaries when one agent hands off to another
Tool policy limits which tools the target agent can use during a handoff — so a low-trust gatekeeper cannot escalate privileges by handing off to a powerful automation agent.
from praisonaiagents import Agent, handoff, tool
@tool
def search_tool(query: str) -> str:
"""Search the knowledge base."""
return f"Results for: {query}"
gatekeeper = Agent(name="Gatekeeper", tools=[search_tool])
automation = Agent(name="Automation", tools=[search_tool])
triage = Agent(
name="Triage",
handoffs=[handoff(automation)],
)
triage.start("Find policy docs and hand off to automation")The user asks the triage agent; tool policy limits what the target can run during handoff.
Quick Start
- Secure by default
No extra configuration needed —
handoff(target)already enforces intersect mode.python from praisonaiagents import Agent, handoff, tool @tool def search_tool(query: str) -> str: """Search the knowledge base.""" return f"Results for: {query}" @tool def execute_code(code: str) -> str: """Run code in a sandbox.""" return "executed" gatekeeper = Agent(name="Gatekeeper", tools=[search_tool]) automation = Agent(name="Automation", tools=[search_tool, execute_code]) triage = Agent( name="Triage", handoffs=[handoff(automation)], # automation only gets search_tool during handoff ) - Block dangerous tools
Always strip specific tool names regardless of mode.
python from praisonaiagents import Agent, handoff triage = Agent( name="Triage", handoffs=[handoff(automation, blocked_tools=["execute_code"])], ) - Opt into legacy passthrough
python from praisonaiagents import Agent, handoff triage = Agent( name="Triage", handoffs=[ handoff( automation, tool_policy_mode="passthrough", blocked_tools=["execute_code"], ), ], ) - Full HandoffConfig
python from praisonaiagents import Agent, handoff, HandoffConfig, HandoffToolPolicy config = HandoffConfig( tool_policy=HandoffToolPolicy( mode="intersect", blocked_tools=["execute_code", "shell_access"], ), ) triage = Agent( name="Triage", handoffs=[handoff(automation, config=config)], )HandoffConfigalso carriesmax_concurrent, enforced independently per handoff — see Per-Instance Concurrency.
How It Works
Error semantics
A failed handoff returns a dict, so hooks, telemetry, and retry logic treat it as a failed tool call.
{"error": "Handoff cycle detected - ...", "handoff_failed": True}
{"error": "Maximum handoff depth exceeded - ...", "handoff_failed": True}
{"error": "Error during handoff to Billing: ...", "handoff_failed": True}| Key | Meaning |
|---|---|
error | Human-readable reason (cycle, depth, or exception text) |
handoff_failed | Always True on failure |
Modes
| Mode | Default? | Target tool set | Use when |
|---|---|---|---|
intersect | ✅ Yes | Tools both agents have, minus blocked_tools | Multi-agent systems with mixed trust levels (recommended) |
passthrough | No (opt-in) | Target’s own tools minus blocked_tools | Legacy code, or intentional capability delegation |
Configuration Options
HandoffToolPolicy
| Option | Type | Default | Description |
|---|---|---|---|
mode | Literal["intersect", "passthrough"] | "intersect" | How tools are filtered during handoff |
blocked_tools | List[str] | [] | Tool names always stripped regardless of mode |
handoff() shorthand kwargs
| Parameter | Type | Default | Description |
|---|---|---|---|
tool_policy_mode | Optional[Literal["intersect","passthrough"]] | None | Shorthand for config.tool_policy.mode |
blocked_tools | Optional[List[str]] | None | Shorthand for config.tool_policy.blocked_tools |
Common Patterns
Gatekeeper → automation (secure default)
from praisonaiagents import Agent, handoff, tool
@tool
def search_tool(query: str) -> str:
return f"Results for: {query}"
@tool
def execute_code(code: str) -> str:
return "executed"
gatekeeper = Agent(name="Gatekeeper", tools=[search_tool])
automation = Agent(name="Automation", tools=[search_tool, execute_code])
router = Agent(name="Router", handoffs=[handoff(automation)])Always block destructive tools
router = Agent(
name="Router",
handoffs=[
handoff(
automation,
blocked_tools=["execute_code", "shell_access", "delete_file"],
),
],
)Passthrough with selective block list
router = Agent(
name="Router",
handoffs=[
handoff(
automation,
tool_policy_mode="passthrough",
blocked_tools=["execute_code"],
),
],
)Best Practices
Default to intersect for least-privilege
Leave tool_policy_mode unset unless you have a specific reason to use passthrough. Intersect mode prevents silent privilege escalation.
Always block known dangerous tools
Even in intersect mode, add blocked_tools for tools like execute_code or shell_access if they appear in the shared set.
Fix the source toolset, not the policy
If intersect mode leaves the target without a needed tool, add that tool to the source agent rather than switching to passthrough.
Test handoff boundaries in security tests
Verify that a gatekeeper handoff cannot invoke tools the source agent does not hold.