diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d4547db19..e22cfe65a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3862,7 +3862,7 @@ model_verbosity = "high" forced_login_method: None, include_apply_patch_tool: false, web_search_mode: None, - use_experimental_unified_exec_tool: false, + use_experimental_unified_exec_tool: !cfg!(windows), ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), suppress_unstable_features_warning: false, @@ -3947,7 +3947,7 @@ model_verbosity = "high" forced_login_method: None, include_apply_patch_tool: false, web_search_mode: None, - use_experimental_unified_exec_tool: false, + use_experimental_unified_exec_tool: !cfg!(windows), ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), suppress_unstable_features_warning: false, @@ -4047,7 +4047,7 @@ model_verbosity = "high" forced_login_method: None, include_apply_patch_tool: false, web_search_mode: None, - use_experimental_unified_exec_tool: false, + use_experimental_unified_exec_tool: !cfg!(windows), ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), suppress_unstable_features_warning: false, @@ -4133,7 +4133,7 @@ model_verbosity = "high" forced_login_method: None, include_apply_patch_tool: false, web_search_mode: None, - use_experimental_unified_exec_tool: false, + use_experimental_unified_exec_tool: !cfg!(windows), ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), suppress_unstable_features_warning: false, diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 1eb2a2dea..a44477905 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -406,6 +406,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::UnifiedExec, + key: "unified_exec", + stage: Stage::Stable, + default_enabled: !cfg!(windows), + }, FeatureSpec { id: Feature::WebSearchRequest, key: "web_search_request", @@ -419,16 +425,6 @@ pub const FEATURES: &[FeatureSpec] = &[ default_enabled: false, }, // Experimental program. Rendered in the `/experimental` menu for users. - FeatureSpec { - id: Feature::UnifiedExec, - key: "unified_exec", - stage: Stage::Experimental { - name: "Background terminal", - menu_description: "Run long-running terminal commands in the background.", - announcement: "NEW! Try Background terminals for long-running commands. Enable in /experimental!", - }, - default_enabled: false, - }, FeatureSpec { id: Feature::ShellSnapshot, key: "shell_snapshot", diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 8851a157a..ddac191a9 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -1687,6 +1687,22 @@ mod tests { assert_eq!(&tool_names, &expected_tools,); } + fn assert_default_model_tools( + model_slug: &str, + features: &Features, + web_search_mode: Option, + shell_tool: &'static str, + expected_tail: &[&str], + ) { + let mut expected = if features.enabled(Feature::UnifiedExec) { + vec!["exec_command", "write_stdin"] + } else { + vec![shell_tool] + }; + expected.extend(expected_tail); + assert_model_tools(model_slug, features, web_search_mode, &expected); + } + #[test] fn web_search_mode_cached_sets_external_web_access_false() { let config = test_config(); @@ -1735,12 +1751,12 @@ mod tests { fn test_build_specs_gpt5_codex_default() { let mut features = Features::with_defaults(); features.enable(Feature::CollaborationModes); - assert_model_tools( + assert_default_model_tools( "gpt-5-codex", &features, Some(WebSearchMode::Cached), + "shell_command", &[ - "shell_command", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", @@ -1757,12 +1773,12 @@ mod tests { fn test_build_specs_gpt51_codex_default() { let mut features = Features::with_defaults(); features.enable(Feature::CollaborationModes); - assert_model_tools( + assert_default_model_tools( "gpt-5.1-codex", &features, Some(WebSearchMode::Cached), + "shell_command", &[ - "shell_command", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", @@ -1827,12 +1843,12 @@ mod tests { fn test_codex_mini_defaults() { let mut features = Features::with_defaults(); features.enable(Feature::CollaborationModes); - assert_model_tools( + assert_default_model_tools( "codex-mini-latest", &features, Some(WebSearchMode::Cached), + "local_shell", &[ - "local_shell", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", @@ -1848,12 +1864,12 @@ mod tests { fn test_codex_5_1_mini_defaults() { let mut features = Features::with_defaults(); features.enable(Feature::CollaborationModes); - assert_model_tools( + assert_default_model_tools( "gpt-5.1-codex-mini", &features, Some(WebSearchMode::Cached), + "shell_command", &[ - "shell_command", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", @@ -1870,12 +1886,12 @@ mod tests { fn test_gpt_5_defaults() { let mut features = Features::with_defaults(); features.enable(Feature::CollaborationModes); - assert_model_tools( + assert_default_model_tools( "gpt-5", &features, Some(WebSearchMode::Cached), + "shell", &[ - "shell", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", @@ -1891,12 +1907,12 @@ mod tests { fn test_gpt_5_1_defaults() { let mut features = Features::with_defaults(); features.enable(Feature::CollaborationModes); - assert_model_tools( + assert_default_model_tools( "gpt-5.1", &features, Some(WebSearchMode::Cached), + "shell_command", &[ - "shell_command", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", diff --git a/codex-rs/core/tests/suite/model_tools.rs b/codex-rs/core/tests/suite/model_tools.rs index c687545f2..d74ab0d7b 100644 --- a/codex-rs/core/tests/suite/model_tools.rs +++ b/codex-rs/core/tests/suite/model_tools.rs @@ -50,6 +50,16 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec { tool_identifiers(&body) } +fn expected_default_tools(shell_tool: &str, tail: &[&str]) -> Vec { + let mut tools = if cfg!(windows) { + vec![shell_tool.to_string()] + } else { + vec!["exec_command".to_string(), "write_stdin".to_string()] + }; + tools.extend(tail.iter().map(|tool| (*tool).to_string())); + tools +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn model_selects_expected_tools() { skip_if_no_network!(); @@ -58,83 +68,93 @@ async fn model_selects_expected_tools() { let codex_tools = collect_tool_identifiers_for_model("codex-mini-latest").await; assert_eq!( codex_tools, - vec![ - "local_shell".to_string(), - "list_mcp_resources".to_string(), - "list_mcp_resource_templates".to_string(), - "read_mcp_resource".to_string(), - "update_plan".to_string(), - "request_user_input".to_string(), - "web_search".to_string(), - "view_image".to_string() - ], + expected_default_tools( + "local_shell", + &[ + "list_mcp_resources", + "list_mcp_resource_templates", + "read_mcp_resource", + "update_plan", + "request_user_input", + "web_search", + "view_image", + ], + ), "codex-mini-latest should expose the local shell tool", ); let gpt5_codex_tools = collect_tool_identifiers_for_model("gpt-5-codex").await; assert_eq!( gpt5_codex_tools, - vec![ - "shell_command".to_string(), - "list_mcp_resources".to_string(), - "list_mcp_resource_templates".to_string(), - "read_mcp_resource".to_string(), - "update_plan".to_string(), - "request_user_input".to_string(), - "apply_patch".to_string(), - "web_search".to_string(), - "view_image".to_string() - ], + expected_default_tools( + "shell_command", + &[ + "list_mcp_resources", + "list_mcp_resource_templates", + "read_mcp_resource", + "update_plan", + "request_user_input", + "apply_patch", + "web_search", + "view_image", + ], + ), "gpt-5-codex should expose the apply_patch tool", ); let gpt51_codex_tools = collect_tool_identifiers_for_model("gpt-5.1-codex").await; assert_eq!( gpt51_codex_tools, - vec![ - "shell_command".to_string(), - "list_mcp_resources".to_string(), - "list_mcp_resource_templates".to_string(), - "read_mcp_resource".to_string(), - "update_plan".to_string(), - "request_user_input".to_string(), - "apply_patch".to_string(), - "web_search".to_string(), - "view_image".to_string() - ], + expected_default_tools( + "shell_command", + &[ + "list_mcp_resources", + "list_mcp_resource_templates", + "read_mcp_resource", + "update_plan", + "request_user_input", + "apply_patch", + "web_search", + "view_image", + ], + ), "gpt-5.1-codex should expose the apply_patch tool", ); let gpt5_tools = collect_tool_identifiers_for_model("gpt-5").await; assert_eq!( gpt5_tools, - vec![ - "shell".to_string(), - "list_mcp_resources".to_string(), - "list_mcp_resource_templates".to_string(), - "read_mcp_resource".to_string(), - "update_plan".to_string(), - "request_user_input".to_string(), - "web_search".to_string(), - "view_image".to_string() - ], + expected_default_tools( + "shell", + &[ + "list_mcp_resources", + "list_mcp_resource_templates", + "read_mcp_resource", + "update_plan", + "request_user_input", + "web_search", + "view_image", + ], + ), "gpt-5 should expose the apply_patch tool", ); let gpt51_tools = collect_tool_identifiers_for_model("gpt-5.1").await; assert_eq!( gpt51_tools, - vec![ - "shell_command".to_string(), - "list_mcp_resources".to_string(), - "list_mcp_resource_templates".to_string(), - "read_mcp_resource".to_string(), - "update_plan".to_string(), - "request_user_input".to_string(), - "apply_patch".to_string(), - "web_search".to_string(), - "view_image".to_string() - ], + expected_default_tools( + "shell_command", + &[ + "list_mcp_resources", + "list_mcp_resource_templates", + "read_mcp_resource", + "update_plan", + "request_user_input", + "apply_patch", + "web_search", + "view_image", + ], + ), "gpt-5.1 should expose the apply_patch tool", ); let exp_tools = collect_tool_identifiers_for_model("exp-5.1").await; diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 810cd5249..acff97381 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -136,8 +136,12 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - let expected_tools_names = vec![ - "shell_command", + let mut expected_tools_names = if cfg!(windows) { + vec!["shell_command"] + } else { + vec!["exec_command", "write_stdin"] + }; + expected_tools_names.extend([ "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", @@ -146,7 +150,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { "apply_patch", "web_search", "view_image", - ]; + ]); let body0 = req1.single_request().body_json(); let expected_instructions = if expected_tools_names.contains(&"apply_patch") {