From e590fad50b832bebc68e7f18067cad5fe13a7d85 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 25 Mar 2026 00:00:25 -0700 Subject: [PATCH] [plugins] Add a flag for tool search. (#15722) - [x] Add a flag for tool search. --- codex-rs/app-server/src/config_api.rs | 8 ++- .../suite/v2/experimental_feature_list.rs | 43 ++++++++++++++-- codex-rs/core/config.schema.json | 6 +++ codex-rs/core/src/tools/spec.rs | 3 +- codex-rs/core/src/tools/spec_tests.rs | 25 +++++++++- codex-rs/core/tests/suite/search_tool.rs | 49 ++++++++++++++++++- codex-rs/features/src/lib.rs | 8 +++ codex-rs/features/src/tests.rs | 6 +++ 8 files changed, 141 insertions(+), 7 deletions(-) diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 9ade74914..4aa2982a8 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -40,7 +40,13 @@ use std::sync::RwLock; use toml::Value as TomlValue; use tracing::warn; -const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &["apps", "plugins"]; +const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[ + "apps", + "plugins", + "tool_search", + "tool_suggest", + "tool_call_mcp_elicitation", +]; #[async_trait] pub(crate) trait UserConfigReloader: Send + Sync { diff --git a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs index 7bbcc36ee..3063830d5 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs @@ -163,14 +163,24 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re .await?; let actual = set_experimental_feature_enablement( &mut mcp, - BTreeMap::from([("plugins".to_string(), true)]), + BTreeMap::from([ + ("plugins".to_string(), true), + ("tool_search".to_string(), true), + ("tool_suggest".to_string(), true), + ("tool_call_mcp_elicitation".to_string(), false), + ]), ) .await?; assert_eq!( actual, ExperimentalFeatureEnablementSetResponse { - enablement: BTreeMap::from([("plugins".to_string(), true)]), + enablement: BTreeMap::from([ + ("plugins".to_string(), true), + ("tool_search".to_string(), true), + ("tool_suggest".to_string(), true), + ("tool_call_mcp_elicitation".to_string(), false), + ]), } ); @@ -190,6 +200,27 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re .and_then(|features| features.get("plugins")), Some(&json!(true)) ); + assert_eq!( + config + .additional + .get("features") + .and_then(|features| features.get("tool_search")), + Some(&json!(true)) + ); + assert_eq!( + config + .additional + .get("features") + .and_then(|features| features.get("tool_suggest")), + Some(&json!(true)) + ); + assert_eq!( + config + .additional + .get("features") + .and_then(|features| features.get("tool_call_mcp_elicitation")), + Some(&json!(false)) + ); Ok(()) } @@ -249,7 +280,13 @@ async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() - "{}", error.message ); - assert!(error.message.contains("apps, plugins"), "{}", error.message); + assert!( + error + .message + .contains("apps, plugins, tool_search, tool_suggest, tool_call_mcp_elicitation"), + "{}", + error.message + ); Ok(()) } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 0b7cf3ced..3e2d7ff68 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -485,6 +485,9 @@ "tool_call_mcp_elicitation": { "type": "boolean" }, + "tool_search": { + "type": "boolean" + }, "tool_suggest": { "type": "boolean" }, @@ -2099,6 +2102,9 @@ "tool_call_mcp_elicitation": { "type": "boolean" }, + "tool_search": { + "type": "boolean" + }, "tool_suggest": { "type": "boolean" }, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 77c295406..5686b4bb7 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -388,7 +388,8 @@ impl ToolsConfig { let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_)); let include_default_mode_request_user_input = include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput); - let include_search_tool = model_info.supports_search_tool; + let include_search_tool = + model_info.supports_search_tool && features.enabled(Feature::ToolSearch); let include_tool_suggest = include_search_tool && features.enabled(Feature::ToolSuggest) && features.enabled(Feature::Apps) diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index c11476176..36a099039 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -1858,6 +1858,7 @@ fn search_tool_description_lists_each_codex_apps_connector_once() { let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + features.enable(Feature::ToolSearch); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -1976,7 +1977,7 @@ fn search_tool_description_lists_each_codex_apps_connector_once() { } #[test] -fn search_tool_requires_model_capability_only() { +fn search_tool_requires_model_capability_and_feature_flag() { let model_info = search_capable_model_info(); let app_tools = Some(HashMap::from([( "mcp__codex_apps__calendar_create_event".to_string(), @@ -2012,6 +2013,22 @@ fn search_tool_requires_model_capability_only() { }); let (tools, _) = build_specs(&tools_config, None, app_tools.clone(), &[]).build(); assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs(&tools_config, None, app_tools.clone(), &[]).build(); + assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); + + let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -2030,6 +2047,7 @@ fn search_tool_requires_model_capability_only() { fn tool_suggest_is_not_registered_without_feature_flag() { let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); features.disable(Feature::ToolSuggest); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2073,6 +2091,7 @@ fn tool_suggest_requires_apps_and_plugins_features() { for disabled_feature in [Feature::Apps, Feature::Plugins] { let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); features.enable(Feature::ToolSuggest); features.enable(Feature::Apps); features.enable(Feature::Plugins); @@ -2110,6 +2129,7 @@ fn search_tool_description_handles_no_enabled_apps() { let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + features.enable(Feature::ToolSearch); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -2136,6 +2156,7 @@ fn search_tool_description_falls_back_to_connector_name_without_description() { let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + features.enable(Feature::ToolSearch); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -2184,6 +2205,7 @@ fn search_tool_registers_namespaced_app_tool_aliases() { let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + features.enable(Feature::ToolSearch); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -2250,6 +2272,7 @@ fn tool_suggest_description_lists_discoverable_tools() { let mut features = Features::with_defaults(); features.enable(Feature::Apps); features.enable(Feature::Plugins); + features.enable(Feature::ToolSearch); features.enable(Feature::ToolSuggest); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 0eee2f1d3..423c57a8c 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -86,7 +86,7 @@ fn tool_search_output_tools(request: &ResponsesRequest, call_id: &str) -> Vec TestCodexBuilder { test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) @@ -169,6 +177,45 @@ async fn search_tool_flag_adds_tool_search() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tool_search_disabled_by_default_exposes_apps_tools_directly() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let apps_server = AppsTestServer::mount_searchable(&server).await?; + let mock = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg-1", "done"), + ev_completed("resp-1"), + ]), + ) + .await; + + let mut builder = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + configure_apps_without_tool_search(config, apps_server.chatgpt_base_url.as_str()) + }); + let test = builder.build(&server).await?; + + test.submit_turn_with_policies( + "list tools", + AskForApproval::Never, + SandboxPolicy::DangerFullAccess, + ) + .await?; + + let body = mock.single_request().body_json(); + let tools = tool_names(&body); + assert!(!tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME)); + assert!(tools.iter().any(|name| name == CALENDAR_CREATE_TOOL)); + assert!(tools.iter().any(|name| name == CALENDAR_LIST_TOOL)); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index be98fac69..ccde6a5e5 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -142,6 +142,8 @@ pub enum Feature { SpawnCsv, /// Enable apps. Apps, + /// Enable the tool_search tool for apps. + ToolSearch, /// Enable discoverable tool suggestions for apps. ToolSuggest, /// Enable plugins. @@ -717,6 +719,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::ToolSearch, + key: "tool_search", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::ToolSuggest, key: "tool_suggest", diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 7c37ef617..ecfa87b41 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -120,6 +120,12 @@ fn tool_suggest_is_stable_and_enabled_by_default() { assert_eq!(Feature::ToolSuggest.default_enabled(), true); } +#[test] +fn tool_search_is_under_development_and_disabled_by_default() { + assert_eq!(Feature::ToolSearch.stage(), Stage::UnderDevelopment); + assert_eq!(Feature::ToolSearch.default_enabled(), false); +} + #[test] fn use_linux_sandbox_bwrap_is_a_removed_feature_key() { assert_eq!(