diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index c9da5c688b..2ddb447a35 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -269,6 +269,9 @@ "shell_tool": { "type": "boolean" }, + "shell_zsh_fork": { + "type": "boolean" + }, "skill_env_var_dependency_prompt": { "type": "boolean" }, @@ -287,6 +290,9 @@ "unified_exec": { "type": "boolean" }, + "unified_exec_zsh_fork": { + "type": "boolean" + }, "use_linux_sandbox_bwrap": { "type": "boolean" }, @@ -357,6 +363,14 @@ } ], "default": null + }, + "zsh_path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Optional absolute path to patched zsh used by sidecar-backed shell execution." } }, "type": "object" @@ -1402,6 +1416,9 @@ "shell_tool": { "type": "boolean" }, + "shell_zsh_fork": { + "type": "boolean" + }, "skill_env_var_dependency_prompt": { "type": "boolean" }, @@ -1420,6 +1437,9 @@ "unified_exec": { "type": "boolean" }, + "unified_exec_zsh_fork": { + "type": "boolean" + }, "use_linux_sandbox_bwrap": { "type": "boolean" }, @@ -1758,6 +1778,14 @@ "windows_wsl_setup_acknowledged": { "description": "Tracks whether the Windows onboarding screen has been acknowledged.", "type": "boolean" + }, + "zsh_path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Optional absolute path to patched zsh used by sidecar-backed shell execution." } }, "title": "ConfigToml", diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b83a17592c..222539e9d7 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -328,6 +328,8 @@ pub struct Config { /// Optional absolute path to the Node runtime used by `js_repl`. pub js_repl_node_path: Option, + /// Optional absolute path to patched zsh used by sidecar-backed shell execution. + pub zsh_path: Option, /// Value to use for `reasoning.effort` when making a request using the /// Responses API. @@ -962,6 +964,8 @@ pub struct ConfigToml { /// Optional absolute path to the Node runtime used by `js_repl`. pub js_repl_node_path: Option, + /// Optional absolute path to patched zsh used by sidecar-backed shell execution. + pub zsh_path: Option, /// Profile to use from the `profiles` map. pub profile: Option, @@ -1311,6 +1315,7 @@ pub struct ConfigOverrides { pub config_profile: Option, pub codex_linux_sandbox_exe: Option, pub js_repl_node_path: Option, + pub zsh_path: Option, pub base_instructions: Option, pub developer_instructions: Option, pub personality: Option, @@ -1438,6 +1443,7 @@ impl Config { config_profile: config_profile_key, codex_linux_sandbox_exe, js_repl_node_path: js_repl_node_path_override, + zsh_path: zsh_path_override, base_instructions, developer_instructions, personality, @@ -1674,6 +1680,9 @@ impl Config { let js_repl_node_path = js_repl_node_path_override .or(config_profile.js_repl_node_path.map(Into::into)) .or(cfg.js_repl_node_path.map(Into::into)); + let zsh_path = zsh_path_override + .or(config_profile.zsh_path.map(Into::into)) + .or(cfg.zsh_path.map(Into::into)); let review_model = override_review_model.or(cfg.review_model); @@ -1796,6 +1805,7 @@ impl Config { file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode), codex_linux_sandbox_exe, js_repl_node_path, + zsh_path, hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), show_raw_agent_reasoning: cfg @@ -4125,6 +4135,7 @@ model_verbosity = "high" file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, js_repl_node_path: None, + zsh_path: None, hide_agent_reasoning: false, show_raw_agent_reasoning: false, model_reasoning_effort: Some(ReasoningEffort::High), @@ -4236,6 +4247,7 @@ model_verbosity = "high" file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, js_repl_node_path: None, + zsh_path: None, hide_agent_reasoning: false, show_raw_agent_reasoning: false, model_reasoning_effort: None, @@ -4345,6 +4357,7 @@ model_verbosity = "high" file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, js_repl_node_path: None, + zsh_path: None, hide_agent_reasoning: false, show_raw_agent_reasoning: false, model_reasoning_effort: None, @@ -4440,6 +4453,7 @@ model_verbosity = "high" file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, js_repl_node_path: None, + zsh_path: None, hide_agent_reasoning: false, show_raw_agent_reasoning: false, model_reasoning_effort: Some(ReasoningEffort::High), diff --git a/codex-rs/core/src/config/profile.rs b/codex-rs/core/src/config/profile.rs index ff343cbf53..5ed4403479 100644 --- a/codex-rs/core/src/config/profile.rs +++ b/codex-rs/core/src/config/profile.rs @@ -30,6 +30,8 @@ pub struct ConfigProfile { pub chatgpt_base_url: Option, /// Optional path to a file containing model instructions. pub model_instructions_file: Option, + /// Optional absolute path to patched zsh used by sidecar-backed shell execution. + pub zsh_path: Option, pub js_repl_node_path: Option, /// Deprecated: ignored. Use `model_instructions_file`. #[schemars(skip)] diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 3cab14d1fa..2255dfc090 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -84,6 +84,10 @@ pub enum Feature { JsReplToolsOnly, /// Use the single unified PTY-backed exec tool. UnifiedExec, + /// Route shell tool execution through the zsh sidecar. + ShellZshFork, + /// Route unified exec execution through the zsh sidecar. + UnifiedExecZshFork, /// Include the freeform apply_patch tool. ApplyPatchFreeform, /// Allow the model to request web searches that fetch live content. @@ -428,6 +432,18 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: !cfg!(windows), }, + FeatureSpec { + id: Feature::ShellZshFork, + key: "shell_zsh_fork", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::UnifiedExecZshFork, + key: "unified_exec_zsh_fork", + stage: Stage::UnderDevelopment, + 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 a897e5a8f6..8697475774 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -67,6 +67,8 @@ impl ToolsConfig { let shell_type = if !features.enabled(Feature::ShellTool) { ConfigShellToolType::Disabled + } else if features.enabled(Feature::ShellZshFork) { + ConfigShellToolType::ShellCommand } else if features.enabled(Feature::UnifiedExec) { // If ConPTY not supported (for old Windows versions), fallback on ShellCommand. if codex_utils_pty::conpty_supported() { @@ -2336,6 +2338,23 @@ mod tests { assert_contains_tool_names(&tools, &subset); } + #[test] + fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { + let config = test_config(); + let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); + let mut features = Features::with_defaults(); + features.enable(Feature::UnifiedExec); + features.enable(Feature::ShellZshFork); + + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Live), + }); + + assert_eq!(tools_config.shell_type, ConfigShellToolType::ShellCommand); + } + #[test] #[ignore] fn test_parallel_support_flags() {