From 98da2c4499dcbf9c419fde36507967423cfdca25 Mon Sep 17 00:00:00 2001 From: tongzhou wang Date: Thu, 6 Aug 2026 00:12:06 +0000 Subject: [PATCH] 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 --- codex-rs/core/src/tools/spec_plan.rs | 18 +++++- codex-rs/core/src/tools/spec_plan_tests.rs | 75 ++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 0fcce89487..4c13de5692 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -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::>(); + 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); } diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 000e09f12c..bdb2cb8b21 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -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 = [