From f8fe96d548a57f0fdf5ca1aa063c0e3026939bef Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Tue, 28 Apr 2026 17:51:30 -0700 Subject: [PATCH] feat: disable capabilities by model provider (#19442) ## Why Unsupported features must fail closed and Codex must not expose OpenAI-hosted fallback paths when the active provider cannot support them. In practice, Bedrock should not surface app connectors, MCP servers, tool search/suggestions, image generation, web search, or JS REPL until those paths are explicitly supported for that provider. This PR moves that decision into provider-owned capability metadata instead of scattering Bedrock-specific checks across callers. ## What changed - Adds `ProviderCapabilities` to `codex-model-provider`, with default support for existing providers and a Bedrock override that disables unsupported launch surfaces. - Adds `ToolCapabilityBounds` to `codex-tools` so provider capability limits can clamp otherwise-enabled tool config. - Applies capability bounds when building session and review-thread tool config. - Routes MCP/app connector configuration through `McpManager::mcp_config`, which filters configured MCP servers and app connectors based on the active provider. - Updates app-server MCP list/read paths to use the filtered MCP config. - Adds coverage for default provider capabilities, Bedrock disabled capabilities, and optional tool-surface clamping. ## Testing built locally and verified that bedrock responses api now return without errors calling unsupported tools. --- codex-rs/core/src/session/review.rs | 4 + codex-rs/core/src/session/turn_context.rs | 8 + codex-rs/core/src/tools/spec.rs | 7 +- codex-rs/core/src/tools/spec_tests.rs | 58 ++++++ codex-rs/core/src/tools/tool_search_entry.rs | 19 ++ .../model-provider/src/amazon_bedrock/mod.rs | 25 +++ codex-rs/model-provider/src/lib.rs | 1 + codex-rs/model-provider/src/provider.rs | 37 ++++ codex-rs/tools/src/tool_config.rs | 23 +++ codex-rs/tools/src/tool_config_tests.rs | 32 ++++ codex-rs/tools/src/tool_registry_plan.rs | 19 +- .../tools/src/tool_registry_plan_tests.rs | 165 ++++++++++++++++++ 12 files changed, 390 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index c22a9faaf..73671d306 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -25,6 +25,7 @@ pub(super) async fn spawn_review_thread( let _ = review_features.disable(Feature::WebSearchCached); let review_web_search_mode = WebSearchMode::Disabled; let goal_tools_supported = !config.ephemeral && parent_turn_context.tools_config.goal_tools; + let provider_capabilities = parent_turn_context.provider.capabilities(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &review_model_info, available_models: &sess @@ -41,6 +42,9 @@ pub(super) async fn spawn_review_thread( permission_profile: &parent_turn_context.permission_profile, windows_sandbox_level: parent_turn_context.windows_sandbox_level, }) + .with_namespace_tools_capability(provider_capabilities.namespace_tools) + .with_image_generation_capability(provider_capabilities.image_generation) + .with_web_search_capability(provider_capabilities.web_search) .with_unified_exec_shell_mode_for_session( crate::tools::spec::tool_user_shell_type(sess.services.user_shell.as_ref()), sess.services.shell_zsh_path.as_ref(), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 35b517b72..09690f93d 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -173,6 +173,7 @@ impl TurnContext { /*developer_instructions*/ None, ); let features = self.features.clone(); + let provider_capabilities = self.provider.capabilities(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, available_models: &models_manager @@ -187,6 +188,9 @@ impl TurnContext { permission_profile: &self.permission_profile, windows_sandbox_level: self.windows_sandbox_level, }) + .with_namespace_tools_capability(provider_capabilities.namespace_tools) + .with_image_generation_capability(provider_capabilities.image_generation) + .with_web_search_capability(provider_capabilities.web_search) .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) .with_web_search_config(self.tools_config.web_search_config.clone()) .with_allow_login_shell(self.tools_config.allow_login_shell) @@ -448,6 +452,7 @@ impl Session { image_generation_tool_auth_allowed(auth_manager.as_deref()); let auth_manager_for_context = auth_manager.clone(); let provider_for_context = create_model_provider(provider, auth_manager); + let provider_capabilities = provider_for_context.capabilities(); let session_telemetry_for_context = session_telemetry; let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -459,6 +464,9 @@ impl Session { permission_profile: &session_configuration.permission_profile(), windows_sandbox_level: session_configuration.windows_sandbox_level, }) + .with_namespace_tools_capability(provider_capabilities.namespace_tools) + .with_image_generation_capability(provider_capabilities.image_generation) + .with_web_search_capability(provider_capabilities.web_search) .with_unified_exec_shell_mode_for_session( crate::tools::spec::tool_user_shell_type(user_shell), shell_zsh_path, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 91d38f25b..c167fdbc3 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -107,7 +107,7 @@ pub(crate) fn build_specs_with_discoverable_tools( use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2; use crate::tools::handlers::unavailable_tool_message; - use crate::tools::tool_search_entry::build_tool_search_entries; + use crate::tools::tool_search_entry::build_tool_search_entries_for_config; let mut builder = ToolRegistryBuilder::new(); let mcp_tool_plan_inputs = mcp_tools.as_ref().map(map_mcp_tools_for_plan); @@ -170,7 +170,7 @@ pub(crate) fn build_specs_with_discoverable_tools( }); let deferred_dynamic_tools = dynamic_tools .iter() - .filter(|tool| tool.defer_loading) + .filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none())) .cloned() .collect::>(); let mut tool_search_handler = None; @@ -270,7 +270,8 @@ pub(crate) fn build_specs_with_discoverable_tools( } ToolHandlerKind::ToolSearch => { if tool_search_handler.is_none() { - let entries = build_tool_search_entries( + let entries = build_tool_search_entries_for_config( + config, deferred_mcp_tools.as_ref(), &deferred_dynamic_tools, ); diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 5942bf806..f3ba9cc48 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -20,6 +20,7 @@ use codex_tools::AdditionalProperties; use codex_tools::ConfiguredToolSpec; use codex_tools::DiscoverableTool; use codex_tools::JsonSchema; +use codex_tools::LoadableToolSpec; use codex_tools::ResponsesApiNamespaceTool; use codex_tools::ResponsesApiTool; use codex_tools::ShellCommandBackendConfig; @@ -40,6 +41,7 @@ use std::collections::BTreeMap; use std::path::PathBuf; use super::*; +use crate::tools::tool_search_entry::build_tool_search_entries_for_config; fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool { rmcp::model::Tool { @@ -1030,6 +1032,62 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() { assert!(registry.has_handler(&mcp_alias)); } +#[tokio::test] +async fn tool_search_entries_skip_namespace_outputs_when_namespace_tools_are_disabled() { + let model_info = search_capable_model_info().await; + let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); + let available_models = Vec::new(); + let mut tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + tools_config.namespace_tools = false; + let mcp_tools = HashMap::from([( + "mcp__test_server__echo".to_string(), + mcp_tool_info(mcp_tool( + "echo", + "Echo", + serde_json::json!({"type": "object"}), + )), + )]); + let dynamic_tools = vec![ + DynamicToolSpec { + namespace: Some("codex_app".to_string()), + name: "automation_update".to_string(), + description: "Create or update automations.".to_string(), + input_schema: serde_json::json!({"type": "object", "properties": {}}), + defer_loading: true, + }, + DynamicToolSpec { + namespace: None, + name: "plain_dynamic".to_string(), + description: "Plain dynamic tool.".to_string(), + input_schema: serde_json::json!({"type": "object", "properties": {}}), + defer_loading: true, + }, + ]; + + let entries = + build_tool_search_entries_for_config(&tools_config, Some(&mcp_tools), &dynamic_tools); + let outputs = entries + .into_iter() + .map(|entry| entry.output) + .collect::>(); + + assert_eq!(outputs.len(), 1); + match &outputs[0] { + LoadableToolSpec::Function(tool) => assert_eq!(tool.name, "plain_dynamic"), + LoadableToolSpec::Namespace(_) => panic!("namespace tool_search output should be hidden"), + } +} + #[tokio::test] async fn direct_mcp_tools_register_namespaced_handlers() { let config = test_config().await; diff --git a/codex-rs/core/src/tools/tool_search_entry.rs b/codex-rs/core/src/tools/tool_search_entry.rs index 23535439f..5d65d8146 100644 --- a/codex-rs/core/src/tools/tool_search_entry.rs +++ b/codex-rs/core/src/tools/tool_search_entry.rs @@ -2,6 +2,7 @@ use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_tools::LoadableToolSpec; use codex_tools::ToolSearchResultSource; +use codex_tools::ToolsConfig; use codex_tools::dynamic_tool_to_loadable_tool_spec; use codex_tools::tool_search_result_source_to_loadable_tool_spec; use std::collections::HashMap; @@ -52,6 +53,24 @@ pub(crate) fn build_tool_search_entries( entries } +pub(crate) fn build_tool_search_entries_for_config( + config: &ToolsConfig, + mcp_tools: Option<&HashMap>, + dynamic_tools: &[DynamicToolSpec], +) -> Vec { + let mcp_tools = if config.namespace_tools { + mcp_tools + } else { + None + }; + let dynamic_tools = dynamic_tools + .iter() + .filter(|tool| config.namespace_tools || tool.namespace.is_none()) + .cloned() + .collect::>(); + build_tool_search_entries(mcp_tools, &dynamic_tools) +} + fn mcp_tool_search_entry(info: &ToolInfo) -> Result { Ok(ToolSearchEntry { search_text: build_mcp_search_text(info), diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 2c47b2f25..6c593d566 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -21,6 +21,7 @@ use codex_protocol::openai_models::ModelsResponse; use crate::provider::ModelProvider; use crate::provider::ProviderAccountResult; use crate::provider::ProviderAccountState; +use crate::provider::ProviderCapabilities; use auth::resolve_provider_auth; use auth::resolve_region; pub(crate) use catalog::static_model_catalog; @@ -55,6 +56,14 @@ impl ModelProvider for AmazonBedrockModelProvider { &self.info } + fn capabilities(&self) -> ProviderCapabilities { + ProviderCapabilities { + namespace_tools: false, + image_generation: false, + web_search: false, + } + } + fn auth_manager(&self) -> Option> { None } @@ -116,4 +125,20 @@ mod tests { "https://bedrock-mantle.eu-central-1.api.aws/v1" ); } + + #[test] + fn capabilities_disable_unsupported_launch_features() { + let provider = AmazonBedrockModelProvider::new( + ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None), + ); + + assert_eq!( + provider.capabilities(), + ProviderCapabilities { + namespace_tools: false, + image_generation: false, + web_search: false, + } + ); + } } diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index ac51968ac..4e4660812 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -13,5 +13,6 @@ pub use provider::ModelProvider; pub use provider::ProviderAccountError; pub use provider::ProviderAccountResult; pub use provider::ProviderAccountState; +pub use provider::ProviderCapabilities; pub use provider::SharedModelProvider; pub use provider::create_model_provider; diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 9027fa3cb..aedd36f37 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -19,6 +19,28 @@ use crate::auth::auth_manager_for_provider; use crate::auth::resolve_provider_auth; use crate::models_endpoint::OpenAiModelsEndpoint; +/// Optional provider-backed features that Codex may expose at runtime. +/// +/// These capabilities are a provider-owned upper bound. Callers can disable +/// more functionality through normal config, but should not expose a feature +/// that the active provider marks unsupported here. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProviderCapabilities { + pub namespace_tools: bool, + pub image_generation: bool, + pub web_search: bool, +} + +impl Default for ProviderCapabilities { + fn default() -> Self { + Self { + namespace_tools: true, + image_generation: true, + web_search: true, + } + } +} + /// Current app-visible account state for a model provider. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderAccountState { @@ -59,6 +81,11 @@ pub trait ModelProvider: fmt::Debug + Send + Sync { /// Returns the configured provider metadata. fn info(&self) -> &ModelProviderInfo; + /// Returns the provider-owned capability upper bounds. + fn capabilities(&self) -> ProviderCapabilities { + ProviderCapabilities::default() + } + /// Returns the provider-scoped auth manager, when this provider uses one. /// /// TODO(celia-oai): Make auth manager access internal to this crate so callers @@ -301,6 +328,16 @@ mod tests { .expect("valid model") } + #[test] + fn configured_provider_uses_default_capabilities() { + let provider = create_model_provider( + ModelProviderInfo::create_openai_provider(/*base_url*/ None), + /*auth_manager*/ None, + ); + + assert_eq!(provider.capabilities(), ProviderCapabilities::default()); + } + #[test] fn create_model_provider_builds_command_auth_manager_without_base_manager() { let provider = create_model_provider( diff --git a/codex-rs/tools/src/tool_config.rs b/codex-rs/tools/src/tool_config.rs index 208efead9..a218a5530 100644 --- a/codex-rs/tools/src/tool_config.rs +++ b/codex-rs/tools/src/tool_config.rs @@ -94,6 +94,7 @@ pub struct ToolsConfig { pub web_search_tool_type: WebSearchToolType, pub image_gen_tool: bool, pub search_tool: bool, + pub namespace_tools: bool, pub tool_suggest: bool, pub exec_permission_approvals_enabled: bool, pub request_permissions_tool_enabled: bool, @@ -214,6 +215,7 @@ impl ToolsConfig { web_search_tool_type: model_info.web_search_tool_type, image_gen_tool: include_image_gen_tool, search_tool: include_search_tool, + namespace_tools: true, tool_suggest: include_tool_suggest, exec_permission_approvals_enabled, request_permissions_tool_enabled, @@ -241,6 +243,27 @@ impl ToolsConfig { self } + pub fn with_namespace_tools_capability(mut self, namespace_tools: bool) -> Self { + if !namespace_tools { + self.namespace_tools = false; + } + self + } + + pub fn with_image_generation_capability(mut self, image_generation: bool) -> Self { + if !image_generation { + self.image_gen_tool = false; + } + self + } + + pub fn with_web_search_capability(mut self, web_search: bool) -> Self { + if !web_search { + self.web_search_mode = None; + } + self + } + pub fn with_spawn_agent_usage_hint(mut self, spawn_agent_usage_hint: bool) -> Self { self.spawn_agent_usage_hint = spawn_agent_usage_hint; self diff --git a/codex-rs/tools/src/tool_config_tests.rs b/codex-rs/tools/src/tool_config_tests.rs index ab82b0bdf..79431d58b 100644 --- a/codex-rs/tools/src/tool_config_tests.rs +++ b/codex-rs/tools/src/tool_config_tests.rs @@ -231,3 +231,35 @@ fn image_generation_requires_feature_and_supported_model() { assert!(!auth_disallowed_tools_config.image_gen_tool); assert!(!unsupported_tools_config.image_gen_tool); } + +#[test] +fn provider_capability_methods_disable_provider_bound_tool_surfaces() { + let model_info = model_info(); + let features = Features::with_defaults(); + let available_models = Vec::new(); + let mut tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + tools_config.search_tool = true; + tools_config.tool_suggest = true; + tools_config.image_gen_tool = true; + tools_config.namespace_tools = true; + + let tools_config = tools_config + .with_namespace_tools_capability(/*namespace_tools*/ false) + .with_image_generation_capability(/*image_generation*/ false) + .with_web_search_capability(/*web_search*/ false); + + assert!(tools_config.search_tool); + assert!(tools_config.tool_suggest); + assert!(!tools_config.image_gen_tool); + assert!(!tools_config.namespace_tools); + assert_eq!(tools_config.web_search_mode, None); +} diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index f7c10dc2c..eff3750ed 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -263,14 +263,18 @@ pub fn build_tool_registry_plan( let deferred_dynamic_tools = params .dynamic_tools .iter() - .filter(|tool| tool.defer_loading) + .filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none())) .collect::>(); + let deferred_mcp_tools_for_search = if config.namespace_tools { + params.deferred_mcp_tools + } else { + None + }; if config.search_tool - && (params.deferred_mcp_tools.is_some() || !deferred_dynamic_tools.is_empty()) + && (deferred_mcp_tools_for_search.is_some() || !deferred_dynamic_tools.is_empty()) { - let mut search_source_infos = params - .deferred_mcp_tools + let mut search_source_infos = deferred_mcp_tools_for_search .map(|deferred_mcp_tools| { collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| { ToolSearchSource { @@ -296,7 +300,7 @@ pub fn build_tool_registry_plan( ); plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch); - if let Some(deferred_mcp_tools) = params.deferred_mcp_tools { + if let Some(deferred_mcp_tools) = deferred_mcp_tools_for_search { for tool in deferred_mcp_tools { plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp); } @@ -589,6 +593,11 @@ pub fn build_tool_registry_plan( ); } + if !config.namespace_tools { + plan.specs + .retain(|configured_tool| !matches!(&configured_tool.spec, ToolSpec::Namespace(_))); + } + plan } diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 8f64349c2..457003423 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1143,6 +1143,84 @@ fn test_build_specs_mcp_tools_converted() { ); } +#[test] +fn namespace_specs_are_hidden_when_namespace_tools_are_disabled() { + let model_info = model_info(); + let features = Features::with_defaults(); + let available_models = Vec::new(); + let mut tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + tools_config.namespace_tools = false; + + let (tools, handlers) = build_specs( + &tools_config, + Some(HashMap::from([( + ToolName::namespaced("mcp__sample__", "echo"), + mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})), + )])), + /*deferred_mcp_tools*/ None, + &[], + ); + + assert_lacks_tool_name(&tools, "mcp__sample__"); + assert!(handlers.contains(&ToolHandlerSpec { + name: ToolName::namespaced("mcp__sample__", "echo"), + kind: ToolHandlerKind::Mcp, + })); +} + +#[test] +fn namespaced_dynamic_specs_are_hidden_when_namespace_tools_are_disabled() { + let model_info = model_info(); + let features = Features::with_defaults(); + let available_models = Vec::new(); + let mut tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + tools_config.namespace_tools = false; + let dynamic_tools = vec![ + DynamicToolSpec { + namespace: Some("codex_app".to_string()), + name: "automation_update".to_string(), + description: "Create or update automations.".to_string(), + input_schema: json!({"type": "object", "properties": {}}), + defer_loading: false, + }, + DynamicToolSpec { + namespace: None, + name: "plain_dynamic".to_string(), + description: "Plain dynamic tool.".to_string(), + input_schema: json!({"type": "object", "properties": {}}), + defer_loading: false, + }, + ]; + + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*deferred_mcp_tools*/ None, + &dynamic_tools, + ); + + assert_lacks_tool_name(&tools, "codex_app"); + assert_contains_tool_names(&tools, &["plain_dynamic"]); +} + #[test] fn test_build_specs_mcp_namespace_description_falls_back_when_missing() { let model_info = model_info(); @@ -1398,6 +1476,44 @@ fn search_tool_requires_model_capability_and_enabled_feature() { assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]); } +#[test] +fn search_tool_is_hidden_when_only_deferred_namespace_tools_are_available() { + let model_info = search_capable_model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); + let available_models = Vec::new(); + let mut tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + tools_config.namespace_tools = false; + + let (tools, handlers) = build_specs( + &tools_config, + /*mcp_tools*/ None, + Some(vec![deferred_mcp_tool( + "_create_event", + "mcp__codex_apps__calendar", + CODEX_APPS_MCP_SERVER_NAME, + Some("Calendar"), + Some("Plan events and manage your calendar."), + )]), + &[], + ); + + assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); + assert!(!handlers.contains(&ToolHandlerSpec { + name: ToolName::plain(TOOL_SEARCH_TOOL_NAME), + kind: ToolHandlerKind::ToolSearch, + })); +} + #[test] fn search_tool_registers_for_deferred_dynamic_tools() { let model_info = search_capable_model_info(); @@ -1484,6 +1600,55 @@ fn search_tool_registers_for_deferred_dynamic_tools() { })); } +#[test] +fn search_tool_keeps_plain_deferred_dynamic_tools_when_namespace_tools_are_disabled() { + let model_info = search_capable_model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); + let available_models = Vec::new(); + let mut tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + tools_config.namespace_tools = false; + let dynamic_tools = vec![ + DynamicToolSpec { + namespace: Some("codex_app".to_string()), + name: "automation_update".to_string(), + description: "Create or update automations.".to_string(), + input_schema: json!({"type": "object", "properties": {}}), + defer_loading: true, + }, + DynamicToolSpec { + namespace: None, + name: "plain_dynamic".to_string(), + description: "Plain dynamic tool.".to_string(), + input_schema: json!({"type": "object", "properties": {}}), + defer_loading: true, + }, + ]; + + let (tools, handlers) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*deferred_mcp_tools*/ None, + &dynamic_tools, + ); + + assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME, "plain_dynamic"]); + assert_lacks_tool_name(&tools, "codex_app"); + assert!(handlers.contains(&ToolHandlerSpec { + name: ToolName::plain(TOOL_SEARCH_TOOL_NAME), + kind: ToolHandlerKind::ToolSearch, + })); +} + #[test] fn tool_suggest_is_not_registered_without_feature_flag() { let model_info = search_capable_model_info();