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.

Diagram

Handoff Tool Policy

📋 User Request

⚙️ Handoff Tool Policy

✅ Result

python
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.

Diagram

blocked

Gatekeeper Agent
search_tool

HandoffToolPolicy
mode: intersect

Automation Agent
search_tool only

execute_code ❌

Quick Start

  1. 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
    )
  2. 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"])],
    )
  3. 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"],
            ),
        ],
    )
  4. 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)],
    )

    HandoffConfig also carries max_concurrent, enforced independently per handoff — see Per-Instance Concurrency.

How It Works

Diagram
Target AgentHandoffToolPolicySource AgentTarget AgentHandoffToolPolicySource Agenthandoff invokedintersect source ∩ target toolsstrip blocked_toolseffective tool setinvoke only allowed tools

Error semantics

A failed handoff returns a dict, so hooks, telemetry, and retry logic treat it as a failed tool call.

python
{"error": "Handoff cycle detected - ...", "handoff_failed": True}
{"error": "Maximum handoff depth exceeded - ...", "handoff_failed": True}
{"error": "Error during handoff to Billing: ...", "handoff_failed": True}
KeyMeaning
errorHuman-readable reason (cycle, depth, or exception text)
handoff_failedAlways True on failure

Modes

ModeDefault?Target tool setUse when
intersect✅ YesTools both agents have, minus blocked_toolsMulti-agent systems with mixed trust levels (recommended)
passthroughNo (opt-in)Target’s own tools minus blocked_toolsLegacy code, or intentional capability delegation
Diagram

No

Yes, intentional delegation

Yes

No

Need handoff tool policy?

Source and target
same trust level?

Use intersect (default)

Migrating legacy code?

Use passthrough + blocked_tools

Add blocked_tools for dangerous tools

Configuration Options

HandoffToolPolicy

OptionTypeDefaultDescription
modeLiteral["intersect", "passthrough"]"intersect"How tools are filtered during handoff
blocked_toolsList[str][]Tool names always stripped regardless of mode

handoff() shorthand kwargs

ParameterTypeDefaultDescription
tool_policy_modeOptional[Literal["intersect","passthrough"]]NoneShorthand for config.tool_policy.mode
blocked_toolsOptional[List[str]]NoneShorthand for config.tool_policy.blocked_tools

Common Patterns

Gatekeeper → automation (secure default)

python
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

python
router = Agent(
    name="Router",
    handoffs=[
        handoff(
            automation,
            blocked_tools=["execute_code", "shell_access", "delete_file"],
        ),
    ],
)

Passthrough with selective block list

python
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.

Migration Note

Related