diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 6392b3e70..f415c3604 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -655,11 +655,13 @@ fn build_responses_headers( let mut headers = experimental_feature_headers(config); headers.insert( WEB_SEARCH_ELIGIBLE_HEADER, - HeaderValue::from_static(if config.web_search_mode == WebSearchMode::Disabled { - "false" - } else { - "true" - }), + HeaderValue::from_static( + if matches!(config.web_search_mode, Some(WebSearchMode::Disabled)) { + "false" + } else { + "true" + }, + ), ); if let Some(turn_state) = turn_state && let Some(state) = turn_state.get() diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 4dcc5d445..c2c25967d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -100,6 +100,7 @@ use crate::config::Config; use crate::config::Constrained; use crate::config::ConstraintResult; use crate::config::GhostSnapshotConfig; +use crate::config::resolve_web_search_mode_for_turn; use crate::config::types::McpServerConfig; use crate::config::types::ShellEnvironmentPolicy; use crate::context_manager::ContextManager; @@ -581,6 +582,10 @@ impl Session { session_configuration.collaboration_mode.reasoning_effort(); per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; per_turn_config.model_personality = session_configuration.personality; + per_turn_config.web_search_mode = Some(resolve_web_search_mode_for_turn( + per_turn_config.web_search_mode, + session_configuration.sandbox_policy.get(), + )); per_turn_config.features = config.features.clone(); per_turn_config } @@ -2888,7 +2893,7 @@ async fn spawn_review_thread( let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &review_model_info, features: &review_features, - web_search_mode: review_web_search_mode, + web_search_mode: Some(review_web_search_mode), }); let review_prompt = resolved.prompt.clone(); @@ -2900,7 +2905,7 @@ async fn spawn_review_thread( let mut per_turn_config = (*config).clone(); per_turn_config.model = Some(model.clone()); per_turn_config.features = review_features.clone(); - per_turn_config.web_search_mode = review_web_search_mode; + per_turn_config.web_search_mode = Some(review_web_search_mode); let otel_manager = parent_turn_context .client diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 4e49efabd..5d84d577e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -306,8 +306,8 @@ pub struct Config { /// model info's default preference. pub include_apply_patch_tool: bool, - /// Explicit or feature-derived web search mode. Defaults to cached. - pub web_search_mode: WebSearchMode, + /// Explicit or feature-derived web search mode. + pub web_search_mode: Option, /// If set to `true`, used only the experimental unified exec tool. pub use_experimental_unified_exec_tool: bool, @@ -1203,27 +1203,36 @@ pub fn resolve_oss_provider( } } -/// Resolve the web search mode from explicit config, feature flags, and sandbox policy. -/// Live search is auto-enabled when sandbox policy is `DangerFullAccess` +/// Resolve the web search mode from explicit config and feature flags. fn resolve_web_search_mode( config_toml: &ConfigToml, config_profile: &ConfigProfile, features: &Features, - sandbox_policy: &SandboxPolicy, -) -> WebSearchMode { +) -> Option { if let Some(mode) = config_profile.web_search.or(config_toml.web_search) { - return mode; + return Some(mode); } if features.enabled(Feature::WebSearchCached) { - return WebSearchMode::Cached; + return Some(WebSearchMode::Cached); } if features.enabled(Feature::WebSearchRequest) { - return WebSearchMode::Live; + return Some(WebSearchMode::Live); + } + None +} + +pub(crate) fn resolve_web_search_mode_for_turn( + explicit_mode: Option, + sandbox_policy: &SandboxPolicy, +) -> WebSearchMode { + if let Some(mode) = explicit_mode { + return mode; } if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) { - return WebSearchMode::Live; + WebSearchMode::Live + } else { + WebSearchMode::Cached } - WebSearchMode::Cached } impl Config { @@ -1347,8 +1356,7 @@ impl Config { AskForApproval::default() } }); - let web_search_mode = - resolve_web_search_mode(&cfg, &config_profile, &features, &sandbox_policy); + let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features); // TODO(dylan): We should be able to leverage ConfigLayerStack so that // we can reliably check this at every config level. let did_user_set_custom_approval_policy_or_sandbox_mode = approval_policy_override @@ -2271,15 +2279,12 @@ trust_level = "trusted" } #[test] - fn web_search_mode_defaults_to_cached_if_unset() { + fn web_search_mode_defaults_to_none_if_unset() { let cfg = ConfigToml::default(); let profile = ConfigProfile::default(); let features = Features::with_defaults(); - assert_eq!( - resolve_web_search_mode(&cfg, &profile, &features, &SandboxPolicy::ReadOnly), - WebSearchMode::Cached - ); + assert_eq!(resolve_web_search_mode(&cfg, &profile, &features), None); } #[test] @@ -2293,8 +2298,8 @@ trust_level = "trusted" features.enable(Feature::WebSearchCached); assert_eq!( - resolve_web_search_mode(&cfg, &profile, &features, &SandboxPolicy::ReadOnly), - WebSearchMode::Live + resolve_web_search_mode(&cfg, &profile, &features), + Some(WebSearchMode::Live) ); } @@ -2309,48 +2314,33 @@ trust_level = "trusted" features.enable(Feature::WebSearchRequest); assert_eq!( - resolve_web_search_mode(&cfg, &profile, &features, &SandboxPolicy::ReadOnly), - WebSearchMode::Disabled + resolve_web_search_mode(&cfg, &profile, &features), + Some(WebSearchMode::Disabled) ); } #[test] - fn danger_full_access_defaults_web_search_live_when_unset() -> std::io::Result<()> { - let codex_home = TempDir::new()?; - let cfg = ConfigToml { - sandbox_mode: Some(SandboxMode::DangerFullAccess), - ..Default::default() - }; + fn web_search_mode_for_turn_defaults_to_cached_when_unset() { + let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::ReadOnly); - let config = Config::load_from_base_config_with_overrides( - cfg, - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - )?; - - assert_eq!(config.web_search_mode, WebSearchMode::Live); - - Ok(()) + assert_eq!(mode, WebSearchMode::Cached); } #[test] - fn explicit_web_search_mode_wins_in_danger_full_access() -> std::io::Result<()> { - let codex_home = TempDir::new()?; - let cfg = ConfigToml { - sandbox_mode: Some(SandboxMode::DangerFullAccess), - web_search: Some(WebSearchMode::Cached), - ..Default::default() - }; + fn web_search_mode_for_turn_defaults_to_live_for_danger_full_access() { + let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::DangerFullAccess); - let config = Config::load_from_base_config_with_overrides( - cfg, - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - )?; + assert_eq!(mode, WebSearchMode::Live); + } - assert_eq!(config.web_search_mode, WebSearchMode::Cached); + #[test] + fn web_search_mode_for_turn_prefers_explicit_value() { + let mode = resolve_web_search_mode_for_turn( + Some(WebSearchMode::Cached), + &SandboxPolicy::DangerFullAccess, + ); - Ok(()) + assert_eq!(mode, WebSearchMode::Cached); } #[test] @@ -3786,7 +3776,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), @@ -3869,7 +3859,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), @@ -3967,7 +3957,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), @@ -4051,7 +4041,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 89dbaf90f..d156d3e0d 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -86,7 +86,7 @@ async fn start_review_conversation( let mut sub_agent_config = config.as_ref().clone(); // Carry over review-only feature restrictions so the delegate cannot // re-enable blocked tools (web search, view image). - sub_agent_config.web_search_mode = WebSearchMode::Disabled; + sub_agent_config.web_search_mode = Some(WebSearchMode::Disabled); // Set explicit review rubric for the sub-agent sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string()); diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index d7fd1d018..a128ceeaf 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -27,7 +27,7 @@ use std::collections::HashMap; pub(crate) struct ToolsConfig { pub shell_type: ConfigShellToolType, pub apply_patch_tool_type: Option, - pub web_search_mode: WebSearchMode, + pub web_search_mode: Option, pub collab_tools: bool, pub collaboration_modes_tools: bool, pub experimental_supported_tools: Vec, @@ -36,7 +36,7 @@ pub(crate) struct ToolsConfig { pub(crate) struct ToolsConfigParams<'a> { pub(crate) model_info: &'a ModelInfo, pub(crate) features: &'a Features, - pub(crate) web_search_mode: WebSearchMode, + pub(crate) web_search_mode: Option, } impl ToolsConfig { @@ -1384,17 +1384,17 @@ pub(crate) fn build_specs( } match config.web_search_mode { - WebSearchMode::Cached => { + Some(WebSearchMode::Cached) => { builder.push_spec(ToolSpec::WebSearch { external_web_access: Some(false), }); } - WebSearchMode::Live => { + Some(WebSearchMode::Live) => { builder.push_spec(ToolSpec::WebSearch { external_web_access: Some(true), }); } - WebSearchMode::Disabled => {} + Some(WebSearchMode::Disabled) | None => {} } builder.push_spec_with_parallel_support(create_view_image_tool(), true); @@ -1556,7 +1556,7 @@ mod tests { let config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs(&config, None, &[]).build(); @@ -1620,7 +1620,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); assert_contains_tool_names( @@ -1638,7 +1638,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); assert!( @@ -1650,7 +1650,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); assert_contains_tool_names(&tools, &["request_user_input"]); @@ -1659,7 +1659,7 @@ mod tests { fn assert_model_tools( model_slug: &str, features: &Features, - web_search_mode: WebSearchMode, + web_search_mode: Option, expected_tools: &[&str], ) { let config = test_config(); @@ -1683,7 +1683,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -1705,7 +1705,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -1725,7 +1725,7 @@ mod tests { assert_model_tools( "gpt-5-codex", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1747,7 +1747,7 @@ mod tests { assert_model_tools( "gpt-5.1-codex", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1770,7 +1770,7 @@ mod tests { assert_model_tools( "gpt-5-codex", &features, - WebSearchMode::Live, + Some(WebSearchMode::Live), &[ "exec_command", "write_stdin", @@ -1794,7 +1794,7 @@ mod tests { assert_model_tools( "gpt-5.1-codex", &features, - WebSearchMode::Live, + Some(WebSearchMode::Live), &[ "exec_command", "write_stdin", @@ -1817,7 +1817,7 @@ mod tests { assert_model_tools( "codex-mini-latest", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "local_shell", "list_mcp_resources", @@ -1838,7 +1838,7 @@ mod tests { assert_model_tools( "gpt-5.1-codex-mini", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1860,7 +1860,7 @@ mod tests { assert_model_tools( "gpt-5", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell", "list_mcp_resources", @@ -1881,7 +1881,7 @@ mod tests { assert_model_tools( "gpt-5.1", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1903,7 +1903,7 @@ mod tests { assert_model_tools( "exp-5.1", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "exec_command", "write_stdin", @@ -1927,7 +1927,7 @@ mod tests { assert_model_tools( "codex-mini-latest", &features, - WebSearchMode::Live, + Some(WebSearchMode::Live), &[ "exec_command", "write_stdin", @@ -1951,7 +1951,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs(&tools_config, Some(HashMap::new()), &[]).build(); @@ -1973,7 +1973,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -1992,7 +1992,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -2023,7 +2023,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs( &tools_config, @@ -2119,7 +2119,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); // Intentionally construct a map with keys that would sort alphabetically. @@ -2196,7 +2196,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2254,7 +2254,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2309,7 +2309,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2366,7 +2366,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2479,7 +2479,7 @@ Examples of valid command strings: let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( &tools_config, diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 9e268a2c5..22d9fa8b7 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -264,7 +264,7 @@ async fn responses_stream_includes_web_search_eligible_header_false_when_disable let test = test_codex() .with_config(|config| { - config.web_search_mode = WebSearchMode::Disabled; + config.web_search_mode = Some(WebSearchMode::Disabled); }) .build(&server) .await diff --git a/codex-rs/core/tests/suite/model_tools.rs b/codex-rs/core/tests/suite/model_tools.rs index 0c8277e91..b08b5281c 100644 --- a/codex-rs/core/tests/suite/model_tools.rs +++ b/codex-rs/core/tests/suite/model_tools.rs @@ -38,7 +38,7 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec { .with_model(model) // Keep tool expectations stable when the default web_search mode changes. .with_config(|config| { - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); config.features.enable(Feature::CollaborationModes); }); let test = builder diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 5831c6235..579a1856d 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -92,7 +92,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { config.user_instructions = Some("be consistent and helpful".to_string()); config.model = Some("gpt-5.1-codex-max".to_string()); // Keep tool expectations stable when the default web_search mode changes. - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); config.features.enable(Feature::CollaborationModes); }) .build(&server) diff --git a/codex-rs/core/tests/suite/web_search_cached.rs b/codex-rs/core/tests/suite/web_search_cached.rs index 261efaf94..df8b7dbb5 100644 --- a/codex-rs/core/tests/suite/web_search_cached.rs +++ b/codex-rs/core/tests/suite/web_search_cached.rs @@ -1,6 +1,7 @@ #![allow(clippy::unwrap_used)] use codex_core::features::Feature; +use codex_core::protocol::SandboxPolicy; use codex_protocol::config_types::WebSearchMode; use core_test_support::load_sse_fixture_with_id; use core_test_support::responses; @@ -25,7 +26,7 @@ fn find_web_search_tool(body: &Value) -> &Value { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn web_search_mode_cached_sets_external_web_access_false_in_request_body() { +async fn web_search_mode_cached_sets_external_web_access_false() { skip_if_no_network!(); let server = start_mock_server().await; @@ -35,7 +36,7 @@ async fn web_search_mode_cached_sets_external_web_access_false_in_request_body() let mut builder = test_codex() .with_model("gpt-5-codex") .with_config(|config| { - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); }); let test = builder .build(&server) @@ -56,7 +57,7 @@ async fn web_search_mode_cached_sets_external_web_access_false_in_request_body() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn web_search_mode_takes_precedence_over_legacy_flags_in_request_body() { +async fn web_search_mode_takes_precedence_over_legacy_flags() { skip_if_no_network!(); let server = start_mock_server().await; @@ -67,7 +68,7 @@ async fn web_search_mode_takes_precedence_over_legacy_flags_in_request_body() { .with_model("gpt-5-codex") .with_config(|config| { config.features.enable(Feature::WebSearchRequest); - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); }); let test = builder .build(&server) @@ -86,3 +87,90 @@ async fn web_search_mode_takes_precedence_over_legacy_flags_in_request_body() { "web_search mode should win over legacy web_search_request" ); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn web_search_mode_defaults_to_cached_when_unset() { + skip_if_no_network!(); + + let server = start_mock_server().await; + let sse = sse_completed("resp-1"); + let resp_mock = responses::mount_sse_once(&server, sse).await; + + let mut builder = test_codex() + .with_model("gpt-5-codex") + .with_config(|config| { + config.web_search_mode = None; + config.features.disable(Feature::WebSearchCached); + config.features.disable(Feature::WebSearchRequest); + }); + let test = builder + .build(&server) + .await + .expect("create test Codex conversation"); + + test.submit_turn_with_policy("hello default cached web search", SandboxPolicy::ReadOnly) + .await + .expect("submit turn"); + + let body = resp_mock.single_request().body_json(); + let tool = find_web_search_tool(&body); + assert_eq!( + tool.get("external_web_access").and_then(Value::as_bool), + Some(false), + "default web_search should be cached when unset" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn web_search_mode_updates_between_turns_with_sandbox_policy() { + skip_if_no_network!(); + + let server = start_mock_server().await; + let resp_mock = responses::mount_sse_sequence( + &server, + vec![sse_completed("resp-1"), sse_completed("resp-2")], + ) + .await; + + let mut builder = test_codex() + .with_model("gpt-5-codex") + .with_config(|config| { + config.web_search_mode = None; + config.features.disable(Feature::WebSearchCached); + config.features.disable(Feature::WebSearchRequest); + }); + let test = builder + .build(&server) + .await + .expect("create test Codex conversation"); + + test.submit_turn_with_policy("hello cached", SandboxPolicy::ReadOnly) + .await + .expect("submit first turn"); + test.submit_turn_with_policy("hello live", SandboxPolicy::DangerFullAccess) + .await + .expect("submit second turn"); + + let requests = resp_mock.requests(); + assert_eq!(requests.len(), 2, "expected two response requests"); + + let first_body = requests[0].body_json(); + let first_tool = find_web_search_tool(&first_body); + assert_eq!( + first_tool + .get("external_web_access") + .and_then(Value::as_bool), + Some(false), + "read-only policy should default web_search to cached" + ); + + let second_body = requests[1].body_json(); + let second_tool = find_web_search_tool(&second_body); + assert_eq!( + second_tool + .get("external_web_access") + .and_then(Value::as_bool), + Some(true), + "danger-full-access policy should default web_search to live" + ); +}