diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d5a9ad77190..bc37e4d5e43 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -873,6 +873,7 @@ impl TurnContext { sandbox_policy: self.sandbox_policy.get(), windows_sandbox_level: self.windows_sandbox_level, }) + .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) .with_web_search_config(self.tools_config.web_search_config.clone()) .with_allow_login_shell(self.tools_config.allow_login_shell) .with_agent_roles(config.agent_roles.clone()); @@ -1261,6 +1262,9 @@ impl Session { session_telemetry: &SessionTelemetry, provider: ModelProviderInfo, session_configuration: &SessionConfiguration, + user_shell: &shell::Shell, + shell_zsh_path: Option<&PathBuf>, + main_execve_wrapper_exe: Option<&PathBuf>, per_turn_config: Config, model_info: ModelInfo, models_manager: &ModelsManager, @@ -1292,6 +1296,11 @@ impl Session { sandbox_policy: session_configuration.sandbox_policy.get(), windows_sandbox_level: session_configuration.windows_sandbox_level, }) + .with_unified_exec_shell_mode_for_session( + user_shell, + shell_zsh_path, + main_execve_wrapper_exe, + ) .with_web_search_config(per_turn_config.web_search_config.clone()) .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) .with_agent_roles(per_turn_config.agent_roles.clone()); @@ -2358,6 +2367,9 @@ impl Session { &self.services.session_telemetry, session_configuration.provider.clone(), &session_configuration, + self.services.user_shell.as_ref(), + self.services.shell_zsh_path.as_ref(), + self.services.main_execve_wrapper_exe.as_ref(), per_turn_config, model_info, &self.services.models_manager, @@ -5269,6 +5281,11 @@ async fn spawn_review_thread( sandbox_policy: parent_turn_context.sandbox_policy.get(), windows_sandbox_level: parent_turn_context.windows_sandbox_level, }) + .with_unified_exec_shell_mode_for_session( + sess.services.user_shell.as_ref(), + sess.services.shell_zsh_path.as_ref(), + sess.services.main_execve_wrapper_exe.as_ref(), + ) .with_web_search_config(None) .with_allow_login_shell(config.permissions.allow_login_shell) .with_agent_roles(config.agent_roles.clone()); diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index efb43b0e5b4..ed5d5790b5b 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2178,6 +2178,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { &session_telemetry, session_configuration.provider.clone(), &session_configuration, + services.user_shell.as_ref(), + services.shell_zsh_path.as_ref(), + services.main_execve_wrapper_exe.as_ref(), per_turn_config, model_info, &models_manager, @@ -2850,6 +2853,9 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( &session_telemetry, session_configuration.provider.clone(), &session_configuration, + services.user_shell.as_ref(), + services.shell_zsh_path.as_ref(), + services.main_execve_wrapper_exe.as_ref(), per_turn_config, model_info, &models_manager, diff --git a/codex-rs/core/src/memories/usage.rs b/codex-rs/core/src/memories/usage.rs index 8a86babb42a..40abb0da4ca 100644 --- a/codex-rs/core/src/memories/usage.rs +++ b/codex-rs/core/src/memories/usage.rs @@ -102,6 +102,7 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec command, @@ -154,6 +156,7 @@ impl ToolHandler for UnifiedExecHandler { let command = get_command( &args, session.user_shell(), + &turn.tools_config.unified_exec_shell_mode, turn.tools_config.allow_login_shell, ) .map_err(FunctionCallError::RespondToModel)?; @@ -323,15 +326,9 @@ impl ToolHandler for UnifiedExecHandler { pub(crate) fn get_command( args: &ExecCommandArgs, session_shell: Arc, + shell_mode: &UnifiedExecShellMode, allow_login_shell: bool, ) -> Result, String> { - let model_shell = args.shell.as_ref().map(|shell_str| { - let mut shell = get_shell_by_model_provided_path(&PathBuf::from(shell_str)); - shell.shell_snapshot = crate::shell::empty_shell_snapshot_receiver(); - shell - }); - - let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref()); let use_login_shell = match args.login { Some(true) if !allow_login_shell => { return Err( @@ -342,7 +339,22 @@ pub(crate) fn get_command( None => allow_login_shell, }; - Ok(shell.derive_exec_args(&args.cmd, use_login_shell)) + match shell_mode { + UnifiedExecShellMode::Direct => { + let model_shell = args.shell.as_ref().map(|shell_str| { + let mut shell = get_shell_by_model_provided_path(&PathBuf::from(shell_str)); + shell.shell_snapshot = crate::shell::empty_shell_snapshot_receiver(); + shell + }); + let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref()); + Ok(shell.derive_exec_args(&args.cmd, use_login_shell)) + } + UnifiedExecShellMode::ZshFork(zsh_fork_config) => Ok(vec![ + zsh_fork_config.shell_zsh_path.to_string_lossy().to_string(), + if use_login_shell { "-lc" } else { "-c" }.to_string(), + args.cmd.clone(), + ]), + } } #[cfg(test)] diff --git a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs index ee31aefc141..fbd2cb10810 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::shell::default_user_shell; use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::handlers::resolve_workdir_base_path; +use crate::tools::spec::ZshForkConfig; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; @@ -18,8 +19,13 @@ fn test_get_command_uses_default_shell_when_unspecified() -> anyhow::Result<()> assert!(args.shell.is_none()); - let command = - get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?; + let command = get_command( + &args, + Arc::new(default_user_shell()), + &UnifiedExecShellMode::Direct, + true, + ) + .map_err(anyhow::Error::msg)?; assert_eq!(command.len(), 3); assert_eq!(command[2], "echo hello"); @@ -34,8 +40,13 @@ fn test_get_command_respects_explicit_bash_shell() -> anyhow::Result<()> { assert_eq!(args.shell.as_deref(), Some("/bin/bash")); - let command = - get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?; + let command = get_command( + &args, + Arc::new(default_user_shell()), + &UnifiedExecShellMode::Direct, + true, + ) + .map_err(anyhow::Error::msg)?; assert_eq!(command.last(), Some(&"echo hello".to_string())); if command @@ -55,8 +66,13 @@ fn test_get_command_respects_explicit_powershell_shell() -> anyhow::Result<()> { assert_eq!(args.shell.as_deref(), Some("powershell")); - let command = - get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?; + let command = get_command( + &args, + Arc::new(default_user_shell()), + &UnifiedExecShellMode::Direct, + true, + ) + .map_err(anyhow::Error::msg)?; assert_eq!(command[2], "echo hello"); Ok(()) @@ -70,8 +86,13 @@ fn test_get_command_respects_explicit_cmd_shell() -> anyhow::Result<()> { assert_eq!(args.shell.as_deref(), Some("cmd")); - let command = - get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?; + let command = get_command( + &args, + Arc::new(default_user_shell()), + &UnifiedExecShellMode::Direct, + true, + ) + .map_err(anyhow::Error::msg)?; assert_eq!(command[2], "echo hello"); Ok(()) @@ -82,8 +103,13 @@ fn test_get_command_rejects_explicit_login_when_disallowed() -> anyhow::Result<( let json = r#"{"cmd": "echo hello", "login": true}"#; let args: ExecCommandArgs = parse_arguments(json)?; - let err = get_command(&args, Arc::new(default_user_shell()), false) - .expect_err("explicit login should be rejected"); + let err = get_command( + &args, + Arc::new(default_user_shell()), + &UnifiedExecShellMode::Direct, + false, + ) + .expect_err("explicit login should be rejected"); assert!( err.contains("login shell is disabled by config"), @@ -92,6 +118,38 @@ fn test_get_command_rejects_explicit_login_when_disallowed() -> anyhow::Result<( Ok(()) } +#[test] +fn test_get_command_ignores_explicit_shell_in_zsh_fork_mode() -> anyhow::Result<()> { + let json = r#"{"cmd": "echo hello", "shell": "/bin/bash"}"#; + let args: ExecCommandArgs = parse_arguments(json)?; + let shell_zsh_path = AbsolutePathBuf::from_absolute_path(if cfg!(windows) { + r"C:\opt\codex\zsh" + } else { + "/opt/codex/zsh" + })?; + let shell_mode = UnifiedExecShellMode::ZshFork(ZshForkConfig { + shell_zsh_path: shell_zsh_path.clone(), + main_execve_wrapper_exe: AbsolutePathBuf::from_absolute_path(if cfg!(windows) { + r"C:\opt\codex\codex-execve-wrapper" + } else { + "/opt/codex/codex-execve-wrapper" + })?, + }); + + let command = get_command(&args, Arc::new(default_user_shell()), &shell_mode, true) + .map_err(anyhow::Error::msg)?; + + assert_eq!( + command, + vec![ + shell_zsh_path.to_string_lossy().to_string(), + "-lc".to_string(), + "echo hello".to_string() + ] + ); + Ok(()) +} + #[test] fn exec_command_args_resolve_relative_additional_permissions_against_workdir() -> anyhow::Result<()> { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 5a1e1c91e9b..1ff4ef4fb65 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -226,20 +226,9 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( _attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, exec_request: ExecRequest, + shell_zsh_path: &std::path::Path, + main_execve_wrapper_exe: &std::path::Path, ) -> Result, ToolError> { - let Some(shell_zsh_path) = ctx.session.services.shell_zsh_path.as_ref() else { - tracing::warn!("ZshFork backend specified, but shell_zsh_path is not configured."); - return Ok(None); - }; - if !ctx.session.features().enabled(Feature::ShellZshFork) { - tracing::warn!("ZshFork backend specified, but ShellZshFork feature is not enabled."); - return Ok(None); - } - if !matches!(ctx.session.user_shell().shell_type, ShellType::Zsh) { - tracing::warn!("ZshFork backend specified, but user shell is not Zsh."); - return Ok(None); - } - let parsed = match extract_shell_script(&exec_request.command) { Ok(parsed) => parsed, Err(err) => { @@ -282,16 +271,6 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(), use_legacy_landlock: ctx.turn.features.use_legacy_landlock(), }; - let main_execve_wrapper_exe = ctx - .session - .services - .main_execve_wrapper_exe - .clone() - .ok_or_else(|| { - ToolError::Rejected( - "zsh fork feature enabled, but execve wrapper is not configured".to_string(), - ) - })?; let escalation_policy = CoreShellActionProvider { policy: Arc::clone(&exec_policy), session: Arc::clone(&ctx.session), @@ -312,8 +291,8 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( }; let escalate_server = EscalateServer::new( - shell_zsh_path.clone(), - main_execve_wrapper_exe, + shell_zsh_path.to_path_buf(), + main_execve_wrapper_exe.to_path_buf(), escalation_policy, ); let escalation_session = escalate_server diff --git a/codex-rs/core/src/tools/runtimes/shell/zsh_fork_backend.rs b/codex-rs/core/src/tools/runtimes/shell/zsh_fork_backend.rs index f864a48d6cd..0ac9e08e0a5 100644 --- a/codex-rs/core/src/tools/runtimes/shell/zsh_fork_backend.rs +++ b/codex-rs/core/src/tools/runtimes/shell/zsh_fork_backend.rs @@ -5,6 +5,7 @@ use crate::tools::runtimes::unified_exec::UnifiedExecRequest; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; +use crate::tools::spec::ZshForkConfig; use crate::unified_exec::SpawnLifecycleHandle; pub(crate) struct PreparedUnifiedExecSpawn { @@ -37,8 +38,9 @@ pub(crate) async fn maybe_prepare_unified_exec( attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, exec_request: ExecRequest, + zsh_fork_config: &ZshForkConfig, ) -> Result, ToolError> { - imp::maybe_prepare_unified_exec(req, attempt, ctx, exec_request).await + imp::maybe_prepare_unified_exec(req, attempt, ctx, exec_request, zsh_fork_config).await } #[cfg(unix)] @@ -83,9 +85,17 @@ mod imp { attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, exec_request: ExecRequest, + zsh_fork_config: &ZshForkConfig, ) -> Result, ToolError> { - let Some(prepared) = - unix_escalation::prepare_unified_exec_zsh_fork(req, attempt, ctx, exec_request).await? + let Some(prepared) = unix_escalation::prepare_unified_exec_zsh_fork( + req, + attempt, + ctx, + exec_request, + zsh_fork_config.shell_zsh_path.as_path(), + zsh_fork_config.main_execve_wrapper_exe.as_path(), + ) + .await? else { return Ok(None); }; @@ -118,8 +128,9 @@ mod imp { attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, exec_request: ExecRequest, + zsh_fork_config: &ZshForkConfig, ) -> Result, ToolError> { - let _ = (req, attempt, ctx, exec_request); + let _ = (req, attempt, ctx, exec_request, zsh_fork_config); Ok(None) } } diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 4c46510d088..7f0032c8d42 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -32,7 +32,7 @@ use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; use crate::tools::sandboxing::sandbox_override_for_first_attempt; use crate::tools::sandboxing::with_cached_approval; -use crate::tools::spec::UnifiedExecBackendConfig; +use crate::tools::spec::UnifiedExecShellMode; use crate::unified_exec::NoopSpawnLifecycle; use crate::unified_exec::UnifiedExecError; use crate::unified_exec::UnifiedExecProcess; @@ -71,12 +71,15 @@ pub struct UnifiedExecApprovalKey { pub struct UnifiedExecRuntime<'a> { manager: &'a UnifiedExecProcessManager, - backend: UnifiedExecBackendConfig, + shell_mode: UnifiedExecShellMode, } impl<'a> UnifiedExecRuntime<'a> { - pub fn new(manager: &'a UnifiedExecProcessManager, backend: UnifiedExecBackendConfig) -> Self { - Self { manager, backend } + pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self { + Self { + manager, + shell_mode, + } } } @@ -209,7 +212,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt if let Some(network) = req.network.as_ref() { network.apply_to_env(&mut env); } - if self.backend == UnifiedExecBackendConfig::ZshFork { + if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode { let spec = build_command_spec( &command, &req.cwd, @@ -223,7 +226,15 @@ impl<'a> ToolRuntime for UnifiedExecRunt let exec_env = attempt .env_for(spec, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; - match zsh_fork_backend::maybe_prepare_unified_exec(req, attempt, ctx, exec_env).await? { + match zsh_fork_backend::maybe_prepare_unified_exec( + req, + attempt, + ctx, + exec_env, + zsh_fork_config, + ) + .await? + { Some(prepared) => { return self .manager diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index f0623897975..2cf4e16d490 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -8,6 +8,8 @@ use crate::features::Features; use crate::mcp_connection_manager::ToolInfo; use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::original_image_detail::can_request_original_image_detail; +use crate::shell::Shell; +use crate::shell::ShellType; use crate::tools::code_mode::PUBLIC_TOOL_NAME; use crate::tools::code_mode::WAIT_TOOL_NAME; use crate::tools::code_mode::is_code_mode_nested_tool; @@ -46,12 +48,14 @@ use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use serde_json::json; use std::collections::BTreeMap; use std::collections::HashMap; +use std::path::PathBuf; const TOOL_SEARCH_DESCRIPTION_TEMPLATE: &str = include_str!("../../templates/search_tool/tool_description.md"); @@ -203,10 +207,46 @@ pub enum ShellCommandBackendConfig { ZshFork, } -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum UnifiedExecBackendConfig { +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum UnifiedExecShellMode { Direct, - ZshFork, + ZshFork(ZshForkConfig), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ZshForkConfig { + pub(crate) shell_zsh_path: AbsolutePathBuf, + pub(crate) main_execve_wrapper_exe: AbsolutePathBuf, +} + +impl UnifiedExecShellMode { + pub fn for_session( + shell_command_backend: ShellCommandBackendConfig, + user_shell: &Shell, + shell_zsh_path: Option<&PathBuf>, + main_execve_wrapper_exe: Option<&PathBuf>, + ) -> Self { + if cfg!(unix) + && shell_command_backend == ShellCommandBackendConfig::ZshFork + && matches!(user_shell.shell_type, ShellType::Zsh) + && let (Some(shell_zsh_path), Some(main_execve_wrapper_exe)) = + (shell_zsh_path, main_execve_wrapper_exe) + && let (Ok(shell_zsh_path), Ok(main_execve_wrapper_exe)) = ( + AbsolutePathBuf::try_from(shell_zsh_path.as_path()) + .inspect_err(|e| tracing::warn!("Failed to convert shell_zsh_path `{shell_zsh_path:?}`: {e:?}")), + AbsolutePathBuf::try_from(main_execve_wrapper_exe.as_path()).inspect_err(|e| { + tracing::warn!("Failed to convert main_execve_wrapper_exe `{main_execve_wrapper_exe:?}`: {e:?}") + }), + ) + { + Self::ZshFork(ZshForkConfig { + shell_zsh_path, + main_execve_wrapper_exe, + }) + } else { + Self::Direct + } + } } #[derive(Debug, Clone)] @@ -214,7 +254,7 @@ pub(crate) struct ToolsConfig { pub available_models: Vec, pub shell_type: ConfigShellToolType, shell_command_backend: ShellCommandBackendConfig, - pub unified_exec_backend: UnifiedExecBackendConfig, + pub unified_exec_shell_mode: UnifiedExecShellMode, pub allow_login_shell: bool, pub apply_patch_tool_type: Option, pub web_search_mode: Option, @@ -300,13 +340,6 @@ impl ToolsConfig { } else { ShellCommandBackendConfig::Classic }; - let unified_exec_backend = - if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) { - UnifiedExecBackendConfig::ZshFork - } else { - UnifiedExecBackendConfig::Direct - }; - let unified_exec_allowed = unified_exec_allowed_in_environment( cfg!(target_os = "windows"), sandbox_policy, @@ -353,7 +386,7 @@ impl ToolsConfig { available_models: available_models_ref.to_vec(), shell_type, shell_command_backend, - unified_exec_backend, + unified_exec_shell_mode: UnifiedExecShellMode::Direct, allow_login_shell: true, apply_patch_tool_type, web_search_mode: *web_search_mode, @@ -390,6 +423,29 @@ impl ToolsConfig { self } + pub fn with_unified_exec_shell_mode( + mut self, + unified_exec_shell_mode: UnifiedExecShellMode, + ) -> Self { + self.unified_exec_shell_mode = unified_exec_shell_mode; + self + } + + pub fn with_unified_exec_shell_mode_for_session( + mut self, + user_shell: &Shell, + shell_zsh_path: Option<&PathBuf>, + main_execve_wrapper_exe: Option<&PathBuf>, + ) -> Self { + self.unified_exec_shell_mode = UnifiedExecShellMode::for_session( + self.shell_command_backend, + user_shell, + shell_zsh_path, + main_execve_wrapper_exe, + ); + self + } + pub fn with_web_search_config(mut self, web_search_config: Option) -> Self { self.web_search_config = web_search_config; self diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index a88daecbd76..a5a904d2aae 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -2,6 +2,8 @@ use crate::client_common::tools::FreeformTool; use crate::config::test_config; use crate::models_manager::manager::ModelsManager; use crate::models_manager::model_info::with_config_overrides; +use crate::shell::Shell; +use crate::shell::ShellType; use crate::tools::ToolRouter; use crate::tools::registry::ConfiguredToolSpec; use crate::tools::router::ToolRouterParams; @@ -9,7 +11,9 @@ use codex_app_server_protocol::AppInfo; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; +use std::path::PathBuf; use super::*; @@ -1431,6 +1435,11 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { sandbox_policy: &SandboxPolicy::DangerFullAccess, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); + let user_shell = Shell { + shell_type: ShellType::Zsh, + shell_path: PathBuf::from("/bin/zsh"), + shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), + }; assert_eq!(tools_config.shell_type, ConfigShellToolType::ShellCommand); assert_eq!( @@ -1438,8 +1447,36 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { ShellCommandBackendConfig::ZshFork ); assert_eq!( - tools_config.unified_exec_backend, - UnifiedExecBackendConfig::ZshFork + tools_config.unified_exec_shell_mode, + UnifiedExecShellMode::Direct + ); + assert_eq!( + tools_config + .with_unified_exec_shell_mode_for_session( + &user_shell, + Some(&PathBuf::from(if cfg!(windows) { + r"C:\opt\codex\zsh" + } else { + "/opt/codex/zsh" + })), + Some(&PathBuf::from(if cfg!(windows) { + r"C:\opt\codex\codex-execve-wrapper" + } else { + "/opt/codex/codex-execve-wrapper" + })), + ) + .unified_exec_shell_mode, + if cfg!(unix) { + UnifiedExecShellMode::ZshFork(ZshForkConfig { + shell_zsh_path: AbsolutePathBuf::from_absolute_path("/opt/codex/zsh").unwrap(), + main_execve_wrapper_exe: AbsolutePathBuf::from_absolute_path( + "/opt/codex/codex-execve-wrapper", + ) + .unwrap(), + }) + } else { + UnifiedExecShellMode::Direct + } ); } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 08f654d129e..95b328a8273 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -578,8 +578,10 @@ impl UnifiedExecProcessManager { Some(context.session.conversation_id), )); let mut orchestrator = ToolOrchestrator::new(); - let mut runtime = - UnifiedExecRuntime::new(self, context.turn.tools_config.unified_exec_backend); + let mut runtime = UnifiedExecRuntime::new( + self, + context.turn.tools_config.unified_exec_shell_mode.clone(), + ); let exec_approval_requirement = context .session .services