mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 07:15:10 +00:00
fix: dispatch tool_selection on the standalone agent kickoff path
`agent.kickoff()` builds its executor in `_prepare_kickoff` without going through `create_agent_executor`, so `tool_selection` hooks never saw the tools it assembled (apps, MCP, memory tools included) and the point could not be trusted as a policy gate. Dispatch the same seam there after the tool list is final, and add conformance tests for both paths.
This commit is contained in:
@@ -1433,6 +1433,22 @@ class Agent(BaseAgent):
|
||||
if sanitize_tool_name(mt.name) not in existing_names
|
||||
)
|
||||
|
||||
from crewai.hooks.contexts import ToolSelectionContext
|
||||
from crewai.hooks.dispatch import InterceptionPoint, dispatch
|
||||
|
||||
# Same policy seam as create_agent_executor: standalone kickoff must
|
||||
# also let tool_selection hooks see (and filter) the final tool list,
|
||||
# or the point can't be trusted to gate what an agent can use.
|
||||
selection_ctx = ToolSelectionContext(
|
||||
agent=self,
|
||||
agent_role=getattr(self, "role", None),
|
||||
crew=self.crew,
|
||||
tools=raw_tools,
|
||||
payload=raw_tools,
|
||||
)
|
||||
dispatch(InterceptionPoint.TOOL_SELECTION, selection_ctx)
|
||||
raw_tools = selection_ctx.payload
|
||||
|
||||
parsed_tools = parse_tools(raw_tools)
|
||||
|
||||
agent_info = {
|
||||
|
||||
@@ -117,3 +117,60 @@ class TestFlowStepPoints:
|
||||
return None
|
||||
|
||||
assert _SimpleFlow().kickoff() == "rewritten"
|
||||
|
||||
|
||||
class TestToolSelection:
|
||||
"""tool_selection must gate every path that hands tools to an executor."""
|
||||
|
||||
@staticmethod
|
||||
def _make_agent():
|
||||
from crewai import Agent
|
||||
|
||||
return Agent(
|
||||
role="Selector",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _make_tools():
|
||||
from crewai.tools import tool
|
||||
|
||||
@tool("allowed_tool")
|
||||
def allowed_tool() -> str:
|
||||
"""A tool the policy permits."""
|
||||
return "ok"
|
||||
|
||||
@tool("blocked_tool")
|
||||
def blocked_tool() -> str:
|
||||
"""A tool the policy strips."""
|
||||
return "nope"
|
||||
|
||||
return [allowed_tool, blocked_tool]
|
||||
|
||||
def test_hook_filters_tools_on_executor_creation(self):
|
||||
@on(InterceptionPoint.TOOL_SELECTION)
|
||||
def drop_blocked(ctx):
|
||||
return [t for t in ctx.payload if t.name != "blocked_tool"]
|
||||
|
||||
agent = self._make_agent()
|
||||
agent.create_agent_executor(tools=self._make_tools())
|
||||
|
||||
names = [t.name for t in agent.agent_executor.original_tools]
|
||||
assert "allowed_tool" in names
|
||||
assert "blocked_tool" not in names
|
||||
|
||||
def test_hook_filters_tools_on_standalone_kickoff_prepare(self):
|
||||
@on(InterceptionPoint.TOOL_SELECTION)
|
||||
def drop_blocked(ctx):
|
||||
return [t for t in ctx.payload if t.name != "blocked_tool"]
|
||||
|
||||
agent = self._make_agent()
|
||||
agent.tools = self._make_tools()
|
||||
_executor, _inputs, agent_info, parsed_tools = agent._prepare_kickoff("hi")
|
||||
|
||||
names = [t.name for t in agent_info["tools"]]
|
||||
assert "allowed_tool" in names
|
||||
assert "blocked_tool" not in names
|
||||
assert all(t.name != "blocked_tool" for t in parsed_tools)
|
||||
|
||||
Reference in New Issue
Block a user