fix: sanitize tool names in hook decorator filters

The tools parameter in before_tool_call/after_tool_call decorators now
auto-sanitizes names via sanitize_tool_name(), so users can pass
BaseTool.name directly without knowing the internal normalization.

Also fixes tool_utils.py to pass the sanitized name to
ToolCallHookContext for consistency with crew_agent_executor.py.

Closes #5335
This commit is contained in:
Greyson LaLonde
2026-04-08 20:00:42 +08:00
parent fc9280ccf6
commit 41635472f6
3 changed files with 39 additions and 2 deletions

View File

@@ -5,6 +5,8 @@ from functools import wraps
import inspect
from typing import TYPE_CHECKING, Any, TypeVar, overload
from crewai.utilities.string_utils import sanitize_tool_name
if TYPE_CHECKING:
from crewai.hooks.llm_hooks import LLMCallHookContext
@@ -37,6 +39,9 @@ def _create_hook_decorator(
tools: list[str] | None = None,
agents: list[str] | None = None,
) -> Callable[..., Any]:
if tools:
tools = [sanitize_tool_name(t) for t in tools]
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
setattr(f, marker_attribute, True)

View File

@@ -96,7 +96,7 @@ async def aexecute_tool_and_check_finality(
if tool:
tool_input = tool_calling.arguments if tool_calling.arguments else {}
hook_context = ToolCallHookContext(
tool_name=tool_calling.tool_name,
tool_name=sanitized_tool_name,
tool_input=tool_input,
tool=tool,
agent=agent,
@@ -120,7 +120,7 @@ async def aexecute_tool_and_check_finality(
tool_result = await tool_usage.ause(tool_calling, agent_action.text)
after_hook_context = ToolCallHookContext(
tool_name=tool_calling.tool_name,
tool_name=sanitized_tool_name,
tool_input=tool_input,
tool=tool,
agent=agent,

View File

@@ -192,6 +192,38 @@ class TestToolHookDecorators:
# Should still be 1 (hook didn't execute for read_file)
assert len(execution_log) == 1
def test_before_tool_call_tool_filter_sanitizes_names(self):
"""Tool filter should auto-sanitize names so users can pass BaseTool.name directly."""
execution_log = []
# User passes the human-readable tool name (e.g. BaseTool.name)
@before_tool_call(tools=["Delete File", "Execute Code"])
def filtered_hook(context):
execution_log.append(context.tool_name)
return None
hooks = get_before_tool_call_hooks()
assert len(hooks) == 1
mock_tool = Mock()
# Context uses the sanitized name (as set by the executor)
context = ToolCallHookContext(
tool_name="delete_file",
tool_input={},
tool=mock_tool,
)
hooks[0](context)
assert execution_log == ["delete_file"]
# Non-matching tool still filtered out
context2 = ToolCallHookContext(
tool_name="read_file",
tool_input={},
tool=mock_tool,
)
hooks[0](context2)
assert execution_log == ["delete_file"]
def test_before_tool_call_with_combined_filters(self):
"""Test that combined tool and agent filters work."""
execution_log = []