From 548583198ac52808e1ddd4550065f74f924e9e2d Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 11 Mar 2026 09:24:10 -0700 Subject: [PATCH] Allow bool web_search in ToolsToml (#14352) Summary - add a custom deserializer so `[tools].web_search` can be a bool (treated as disabled) or a config object - extend core and app-server tests to cover bool handling in TOML config Testing - Not run (not requested) --- .../app-server/tests/suite/v2/config_rpc.rs | 32 ++++++++++++++++ codex-rs/core/src/config/config_tests.rs | 38 +++++++++++++++++++ codex-rs/core/src/config/mod.rs | 31 ++++++++++++++- codex-rs/core/src/config/service.rs | 6 ++- 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 97f1e8dfd..23c9a6c6c 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -218,6 +218,38 @@ location = { country = "US", city = "New York", timezone = "America/New_York" } Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_read_ignores_bool_web_search_tool_config() -> Result<()> { + let codex_home = TempDir::new()?; + write_config( + &codex_home, + r#" +[tools] +web_search = true +"#, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_config_read_request(ConfigReadParams { + include_layers: false, + cwd: None, + }) + .await?; + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let ConfigReadResponse { config, .. } = to_response(resp)?; + + assert_eq!(config.tools.expect("tools present").web_search, None,); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn config_read_includes_apps() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 707566eb1..5549a3414 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -175,6 +175,44 @@ enabled = false ); } +#[test] +fn tools_web_search_true_deserializes_to_none() { + let cfg: ConfigToml = toml::from_str( + r#" +[tools] +web_search = true +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + view_image: None, + }) + ); +} + +#[test] +fn tools_web_search_false_deserializes_to_none() { + let cfg: ConfigToml = toml::from_str( + r#" +[tools] +web_search = false +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + view_image: None, + }) + ); +} + #[test] fn config_toml_deserializes_model_availability_nux() { let toml = r#" diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 41eaeabb9..d2b37000a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -82,6 +82,7 @@ use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use schemars::JsonSchema; use serde::Deserialize; +use serde::Deserializer; use serde::Serialize; use similar::DiffableStr; use std::collections::BTreeMap; @@ -1392,7 +1393,10 @@ pub struct RealtimeAudioToml { #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct ToolsToml { - #[serde(default)] + #[serde( + default, + deserialize_with = "deserialize_optional_web_search_tool_config" + )] pub web_search: Option, /// Enable the `view_image` tool that lets the agent attach local images. @@ -1400,6 +1404,31 @@ pub struct ToolsToml { pub view_image: Option, } +#[derive(Deserialize)] +#[serde(untagged)] +enum WebSearchToolConfigInput { + Enabled(bool), + Config(WebSearchToolConfig), +} + +fn deserialize_optional_web_search_tool_config<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + + Ok(match value { + None => None, + Some(WebSearchToolConfigInput::Enabled(enabled)) => { + let _ = enabled; + None + } + Some(WebSearchToolConfigInput::Config(config)) => Some(config), + }) +} + #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct AgentsToml { diff --git a/codex-rs/core/src/config/service.rs b/codex-rs/core/src/config/service.rs index 10e0679e5..df344afb4 100644 --- a/codex-rs/core/src/config/service.rs +++ b/codex-rs/core/src/config/service.rs @@ -168,10 +168,12 @@ impl ConfigService { }; let effective = layers.effective_config(); - validate_config(&effective) + + let effective_config_toml: ConfigToml = effective + .try_into() .map_err(|err| ConfigServiceError::toml("invalid configuration", err))?; - let json_value = serde_json::to_value(&effective) + let json_value = serde_json::to_value(&effective_config_toml) .map_err(|err| ConfigServiceError::json("failed to serialize configuration", err))?; let config: ApiConfig = serde_json::from_value(json_value) .map_err(|err| ConfigServiceError::json("failed to deserialize configuration", err))?;