diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index 4f0c36f51..e6d843fb1 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -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 = { diff --git a/lib/crewai/tests/hooks/test_interception_conformance.py b/lib/crewai/tests/hooks/test_interception_conformance.py index 0ac33e6ea..0c5bfab41 100644 --- a/lib/crewai/tests/hooks/test_interception_conformance.py +++ b/lib/crewai/tests/hooks/test_interception_conformance.py @@ -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)