diff --git a/codex-rs/core/src/tools/handlers/shell/shell_command.rs b/codex-rs/core/src/tools/handlers/shell/shell_command.rs index 4c98c988c5..c8e8698a7f 100644 --- a/codex-rs/core/src/tools/handlers/shell/shell_command.rs +++ b/codex-rs/core/src/tools/handlers/shell/shell_command.rs @@ -9,6 +9,7 @@ use crate::exec_env::create_env; use crate::function_tool::FunctionCallError; use crate::maybe_emit_implicit_skill_invocation; use crate::session::turn_context::TurnContext; +use crate::session::turn_context::TurnEnvironment; use crate::shell::Shell; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; @@ -50,6 +51,19 @@ pub(crate) struct ShellCommandHandlerOptions { } impl ShellCommandHandler { + fn compatible_execution_cwd( + turn_context: &TurnContext, + ) -> codex_utils_absolute_path::AbsolutePathBuf { + turn_context + .environments + .primary() + .and_then(TurnEnvironment::compatible_cwd) + .unwrap_or_else(|| { + #[allow(deprecated)] + turn_context.cwd.clone() + }) + } + pub(crate) fn new(options: ShellCommandHandlerOptions) -> Self { let backend = match options.backend_config { ShellCommandBackendConfig::Classic => ShellCommandBackend::Classic, @@ -92,8 +106,11 @@ impl ShellCommandHandler { let shell = session.user_shell(); let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?; let command = Self::base_command(shell.as_ref(), ¶ms.command, use_login_shell); - #[allow(deprecated)] - let cwd = turn_context.resolve_path(params.workdir.clone()); + let base_cwd = Self::compatible_execution_cwd(turn_context); + let cwd = params + .workdir + .as_ref() + .map_or(base_cwd.clone(), |workdir| base_cwd.join(workdir)); Ok(ExecParams { command, @@ -176,16 +193,14 @@ impl ShellCommandHandler { )); } - #[allow(deprecated)] - let cwd = resolve_workdir_base_path(&arguments, &turn.cwd)?; + let base_cwd = Self::compatible_execution_cwd(turn.as_ref()); + let cwd = resolve_workdir_base_path(&arguments, &base_cwd)?; let params: ShellCommandToolCallParams = parse_arguments_with_base_path(&arguments, &cwd)?; - #[allow(deprecated)] - let workdir = turn.resolve_path(params.workdir.clone()); maybe_emit_implicit_skill_invocation( session.as_ref(), turn.as_ref(), ¶ms.command, - &workdir, + &cwd, ) .await; let prefix_rule = params.prefix_rule.clone(); diff --git a/codex-rs/core/src/tools/handlers/shell_tests.rs b/codex-rs/core/src/tools/handlers/shell_tests.rs index e42e0739ec..27c3f4866f 100644 --- a/codex-rs/core/src/tools/handlers/shell_tests.rs +++ b/codex-rs/core/src/tools/handlers/shell_tests.rs @@ -130,6 +130,41 @@ async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_contex assert_eq!(exec_params.arg0, None); } +#[tokio::test] +async fn shell_command_handler_uses_selected_local_environment_cwd() { + let (session, mut turn_context) = make_session_and_context().await; + let selected = turn_context.environments.turn_environments[0].clone(); + let selected_dir = tempfile::tempdir().expect("selected cwd"); + let selected_cwd = selected_dir.path().to_path_buf().abs(); + turn_context.environments.turn_environments[0] = TurnEnvironment::new( + selected.environment_id, + selected.environment, + selected_cwd.clone(), + selected.shell, + ); + let params = ShellCommandToolCallParams { + command: "echo hello".to_string(), + workdir: Some("subdir".to_string()), + login: None, + timeout_ms: None, + sandbox_permissions: None, + additional_permissions: None, + prefix_rule: None, + justification: None, + }; + + let exec_params = ShellCommandHandler::to_exec_params( + ¶ms, + &session, + &turn_context, + session.thread_id, + /*allow_login_shell*/ false, + ) + .expect("selected local environment should be executable"); + + assert_eq!(exec_params.cwd, selected_cwd.join("subdir")); +} + #[test] fn shell_command_handler_respects_explicit_login_flag() { let (_tx, shell_snapshot) = watch::channel(Some(Arc::new(ShellSnapshot {