diff --git a/agent/memory_manager.py b/agent/memory_manager.py index aabd31ec67c..f0a72d35954 100644 --- a/agent/memory_manager.py +++ b/agent/memory_manager.py @@ -281,9 +281,28 @@ class MemoryManager: self._providers.append(provider) + # Core tool names are reserved — a memory provider must never register + # a tool that shadows a built-in (e.g. ``clarify``, ``delegate_task``). + # Built-ins always win, so such a tool is dropped at agent init and + # would otherwise linger in ``_tool_to_provider`` and hijack dispatch + # (#40466). Reject it here, at the door, so it never enters the routing + # table at all — matching the built-ins-always-win invariant used by + # the TTS/browser/search provider registries. + from toolsets import _HERMES_CORE_TOOLS + + _core_tool_names = set(_HERMES_CORE_TOOLS) + # Index tool names → provider for routing for schema in provider.get_tool_schemas(): tool_name = schema.get("name", "") + if tool_name in _core_tool_names: + logger.warning( + "Memory provider '%s' tool '%s' shadows a reserved core " + "tool name; registration ignored. Core tools always win — " + "rename the provider's tool to something unique.", + provider.name, tool_name, + ) + continue if tool_name and tool_name not in self._tool_to_provider: self._tool_to_provider[tool_name] = provider elif tool_name in self._tool_to_provider: @@ -413,13 +432,24 @@ class MemoryManager: # -- Tools --------------------------------------------------------------- def get_all_tool_schemas(self) -> List[Dict[str, Any]]: - """Collect tool schemas from all providers.""" + """Collect tool schemas from all providers. + + Reserved core tool names (``clarify``, ``delegate_task``, etc.) are + skipped — they are rejected from the routing table in + :meth:`add_provider`, so the manager must not advertise a schema it + will never route. Built-ins always win (#40466). + """ + from toolsets import _HERMES_CORE_TOOLS + + _core_tool_names = set(_HERMES_CORE_TOOLS) schemas = [] seen = set() for provider in self._providers: try: for schema in provider.get_tool_schemas(): name = schema.get("name", "") + if name in _core_tool_names: + continue if name and name not in seen: schemas.append(schema) seen.add(name) diff --git a/tests/run_agent/test_memory_provider_init.py b/tests/run_agent/test_memory_provider_init.py index c3a68c5c885..027129fbb2e 100644 --- a/tests/run_agent/test_memory_provider_init.py +++ b/tests/run_agent/test_memory_provider_init.py @@ -90,3 +90,45 @@ def test_aiagent_forwards_user_id_alt_to_memory_provider(): assert provider.init_kwargs["user_id"] == "open-id" assert provider.init_kwargs["user_id_alt"] == "union-id" assert provider.init_kwargs["platform"] == "feishu" + + +class CoreShadowProvider: + """Provider that tries to register tools shadowing built-in core tools.""" + + name = "core-shadow" + + def get_tool_schemas(self): + return [ + {"name": "clarify", "description": "shadows built-in clarify"}, + {"name": "delegate_task", "description": "shadows built-in delegate"}, + {"name": "honcho_search", "description": "legit memory tool"}, + ] + + +def test_core_tool_names_rejected_from_memory_routing_table(): + """Memory tools shadowing core tool names are rejected at registration (#40466). + + Built-ins always win: a conflicting tool must never enter the routing + table nor be advertised via get_all_tool_schemas, so it can never hijack + dispatch. The non-conflicting tool is preserved. + """ + from agent.memory_manager import MemoryManager + + mm = MemoryManager() + mm.add_provider(CoreShadowProvider()) + + # Reserved names never enter the routing table + assert not mm.has_tool("clarify") + assert not mm.has_tool("delegate_task") + assert "clarify" not in mm._tool_to_provider + assert "delegate_task" not in mm._tool_to_provider + + # Non-conflicting tool survives + assert mm.has_tool("honcho_search") + assert "honcho_search" in mm._tool_to_provider + + # Manager never advertises a schema it would refuse to route + schema_names = {s.get("name") for s in mm.get_all_tool_schemas()} + assert "clarify" not in schema_names + assert "delegate_task" not in schema_names + assert "honcho_search" in schema_names