Reserve the tool_search namespace for the search tool (#37188)

## What changed

- Remove namespace tools named `tool_search` before registering the built-in
  search tool, preventing them from sharing its model-visible surface.
- Record removed namespace tools as collisions so strict collision handling
  reports their qualified tool names.

## Testing

- Cover direct and deferred MCP tools that use the reserved namespace.
- Cover strict collision errors for both direct and deferred conflicts.

GitOrigin-RevId: 28fff918b4358a67b167467d92728fa545b951d7
This commit is contained in:
tongzhou wang
2026-08-06 00:16:57 +00:00
committed by copyberry
parent 928bda82cf
commit 98da2c4499
2 changed files with 92 additions and 1 deletions
+17 -1
View File
@@ -342,7 +342,23 @@ pub(crate) fn finalize_tool_router(
})
{
if registry.remove(&tool_search_name).is_some() {
registry.record_collision(tool_search_name);
registry.record_collision(tool_search_name.clone());
}
// Special model tools own the namespace matching their wire identity, so
// regular namespace tools cannot advertise that same model-visible surface.
let conflicting_tool_names = registry
.entries()
.filter_map(|tool| {
let ToolSpec::Namespace(namespace) = tool.runtime.spec() else {
return None;
};
(namespace.name == tool_search_name.name).then(|| tool.runtime.tool_name())
})
.collect::<Vec<_>>();
for tool_name in conflicting_tool_names {
if registry.remove(&tool_name).is_some() {
registry.record_collision(tool_name);
}
}
append_tool_search_executor(turn_context, &mut registry, tool_search_handler_cache);
}
@@ -1161,6 +1161,50 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
"tool_search",
&ToolName::namespaced("mcp__searchable", "lookup").to_string(),
]);
let reserved_namespace = probe_with(
|turn| {
set_feature(turn, Feature::CodeMode, /*enabled*/ true);
turn.model_info.supports_search_tool = true;
},
ToolPlanInputs {
tool_runtimes: vec![
mcp_runtime(
"reserved_direct",
"tool_search",
"inspect",
ToolExposure::Direct,
),
mcp_runtime(
"reserved_deferred",
"tool_search",
"tool_search_tool",
ToolExposure::Deferred,
),
mcp_runtime(
"searchable",
"mcp__searchable",
"lookup",
ToolExposure::Deferred,
),
],
..ToolPlanInputs::default()
},
)
.await;
reserved_namespace.assert_visible_contains(&["tool_search"]);
reserved_namespace.assert_registered_contains(&[
"tool_search",
&ToolName::namespaced("mcp__searchable", "lookup").to_string(),
]);
reserved_namespace.assert_registered_lacks(&[
&ToolName::namespaced("tool_search", "inspect").to_string(),
&ToolName::namespaced("tool_search", "tool_search_tool").to_string(),
]);
assert!(matches!(
reserved_namespace.visible_spec("tool_search"),
ToolSpec::ToolSearch { .. }
));
}
#[tokio::test]
@@ -1301,6 +1345,37 @@ async fn strict_tool_collisions_reject_external_and_synthetic_duplicates() {
false,
true,
),
(
"tool_search.tool_search_tool",
ToolPlanInputs {
tool_runtimes: vec![mcp_runtime(
"reserved",
"tool_search",
"tool_search_tool",
ToolExposure::Deferred,
)],
..ToolPlanInputs::default()
},
false,
true,
),
(
"tool_search.inspect",
ToolPlanInputs {
tool_runtimes: vec![
mcp_runtime("reserved", "tool_search", "inspect", ToolExposure::Direct),
mcp_runtime(
"searchable",
"mcp__searchable",
"lookup",
ToolExposure::Deferred,
),
],
..ToolPlanInputs::default()
},
false,
true,
),
];
let namespace_cases = [