diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index b15827997..dafaffca3 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1829,7 +1829,7 @@ impl CodexMessageProcessor { return; } - let cwd = cwd.unwrap_or_else(|| self.config.cwd.to_path_buf()); + let cwd = cwd.map_or_else(|| self.config.cwd.clone(), |cwd| self.config.cwd.join(cwd)); let mut env = create_env( &self.config.permissions.shell_environment_policy, /*thread_id*/ None, diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index bb086e0dd..b72c84e90 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -708,13 +708,13 @@ fn internal_error(message: String) -> JSONRPCErrorError { #[cfg(test)] mod tests { use std::collections::HashMap; - use std::path::PathBuf; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; #[cfg(not(target_os = "windows"))] use tokio::time::Duration; @@ -736,7 +736,7 @@ mod tests { }; ExecRequest::new( vec!["cmd".to_string()], - PathBuf::from("."), + AbsolutePathBuf::current_dir().expect("current dir"), HashMap::new(), /*network*/ None, ExecExpiration::DefaultTimeout, @@ -848,7 +848,7 @@ mod tests { process_id: Some("proc-100".to_string()), exec_request: ExecRequest::new( vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], - PathBuf::from("."), + AbsolutePathBuf::current_dir().expect("current dir"), HashMap::new(), /*network*/ None, ExecExpiration::Cancellation(CancellationToken::new()), diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 5999cf70e..6cf1e8c8c 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1019,10 +1019,9 @@ impl TurnContext { } } - pub(crate) fn resolve_path(&self, path: Option) -> PathBuf { + pub(crate) fn resolve_path(&self, path: Option) -> AbsolutePathBuf { path.as_ref() - .map(PathBuf::from) - .map_or_else(|| self.cwd.to_path_buf(), |p| self.cwd.as_path().join(p)) + .map_or_else(|| self.cwd.clone(), |path| self.cwd.join(path)) } pub(crate) fn compact_prompt(&self) -> &str { diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 427f5ac61..e02d3226a 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -5539,7 +5539,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { "echo hi".to_string(), ] }, - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), expiration: timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index 4f60c2f28..6a1fb25bf 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -125,7 +125,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid "echo hi".to_string(), ] }, - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), expiration: expiration_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 8a5572a36..6eb3c585c 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -82,7 +82,7 @@ pub const IO_DRAIN_TIMEOUT_MS: u64 = 2_000; // 2 s should be plenty for local pi #[derive(Debug)] pub struct ExecParams { pub command: Vec, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub expiration: ExecExpiration, pub capture_policy: ExecCapturePolicy, pub env: HashMap, @@ -293,7 +293,7 @@ pub fn build_exec_request( enforce_managed_network, network: network.as_ref(), sandbox_policy_cwd: sandbox_cwd, - codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), + codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_deref(), use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, @@ -804,7 +804,7 @@ async fn exec( program: PathBuf::from(program), args: args.into(), arg0: arg0_ref, - cwd, + cwd: cwd.to_path_buf(), network_sandbox_policy, // The environment already has attempt-scoped proxy settings from // apply_to_env_for_attempt above. Passing network here would reapply diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index cf00d7050..3cde807c0 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -264,7 +264,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> { let output = exec( ExecParams { command, - cwd: std::env::current_dir()?, + cwd: codex_utils_absolute_path::AbsolutePathBuf::current_dir()?, expiration: 1.into(), capture_policy: ExecCapturePolicy::FullBuffer, env, @@ -304,7 +304,7 @@ async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_p "-c".to_string(), "printf hello; sleep 30 &".to_string(), ], - cwd: std::env::current_dir()?, + cwd: codex_utils_absolute_path::AbsolutePathBuf::current_dir()?, expiration: 1.into(), capture_policy: ExecCapturePolicy::FullBuffer, env: std::env::vars().collect(), @@ -350,7 +350,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result format!("sleep 0.05; head -c {byte_count} /dev/zero | tr '\\0' 'a'"), ]; - let cwd = std::env::current_dir()?; + let cwd = codex_utils_absolute_path::AbsolutePathBuf::current_dir()?; let sandbox_policy = SandboxPolicy::DangerFullAccess; let output = process_exec_tool_call( ExecParams { @@ -723,7 +723,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> "-c".to_string(), "sleep 60 & echo $!; sleep 60".to_string(), ]; - let cwd = std::env::current_dir()?; + let cwd = codex_utils_absolute_path::AbsolutePathBuf::current_dir()?; let env: HashMap = std::env::vars().collect(); let params = ExecParams { command, @@ -780,7 +780,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> #[tokio::test] async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { let command = long_running_command(); - let cwd = std::env::current_dir()?; + let cwd = codex_utils_absolute_path::AbsolutePathBuf::current_dir()?; let env: HashMap = std::env::vars().collect(); let cancel_token = CancellationToken::new(); let cancel_tx = cancel_token.clone(); diff --git a/codex-rs/core/src/memories/usage.rs b/codex-rs/core/src/memories/usage.rs index b98a00982..574567d99 100644 --- a/codex-rs/core/src/memories/usage.rs +++ b/codex-rs/core/src/memories/usage.rs @@ -80,12 +80,20 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec serde_json::from_str::(arguments) .ok() - .map(|params| (params.command, invocation.turn.resolve_path(params.workdir))), + .map(|params| { + ( + params.command, + invocation.turn.resolve_path(params.workdir).to_path_buf(), + ) + }), "shell_command" => serde_json::from_str::(arguments) .ok() .map(|params| { if !invocation.turn.tools_config.allow_login_shell && params.login == Some(true) { - return (Vec::new(), invocation.turn.resolve_path(params.workdir)); + return ( + Vec::new(), + invocation.turn.resolve_path(params.workdir).to_path_buf(), + ); } let use_login_shell = params .login @@ -94,7 +102,10 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec serde_json::from_str::(arguments) .ok() @@ -106,7 +117,10 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec None, } diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 0427de7c7..d241fa826 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -24,8 +24,8 @@ use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxType; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; -use std::path::PathBuf; #[derive(Debug)] pub(crate) struct ExecOptions { @@ -36,7 +36,7 @@ pub(crate) struct ExecOptions { #[derive(Debug)] pub struct ExecRequest { pub command: Vec, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub env: HashMap, pub network: Option, pub expiration: ExecExpiration, @@ -56,7 +56,7 @@ impl ExecRequest { #[allow(clippy::too_many_arguments)] pub fn new( command: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, env: HashMap, network: Option, expiration: ExecExpiration, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 744d8d797..d872116a8 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -160,7 +160,7 @@ pub(crate) async fn execute_user_shell_command( let sandbox_policy = SandboxPolicy::DangerFullAccess; let exec_env = ExecRequest { command: exec_command.clone(), - cwd: cwd.to_path_buf(), + cwd: cwd.clone(), env: exec_env_map, network: turn_context.network.clone(), // TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index fe30f3d75..c9475eeb3 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -18,6 +18,7 @@ use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::user_input::UserInput; +use codex_utils_absolute_path::AbsolutePathBuf; use futures::StreamExt; use futures::stream::FuturesUnordered; use serde::Deserialize; @@ -25,7 +26,6 @@ use serde::Serialize; use serde_json::Value; use std::collections::HashMap; use std::collections::HashSet; -use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::watch::Receiver; @@ -309,7 +309,7 @@ mod spawn_agents_on_csv { let job_id = Uuid::new_v4().to_string(); let output_csv_path = args.output_csv_path.map_or_else( - || default_output_csv_path(input_path.as_path(), job_id.as_str()), + || default_output_csv_path(&input_path, job_id.as_str()), |path| turn.resolve_path(Some(path)), ); let job_suffix = &job_id[..8]; @@ -1089,13 +1089,17 @@ fn is_item_stale(item: &codex_state::AgentJobItem, runtime_timeout: Duration) -> } } -fn default_output_csv_path(input_csv_path: &Path, job_id: &str) -> PathBuf { +fn default_output_csv_path(input_csv_path: &AbsolutePathBuf, job_id: &str) -> AbsolutePathBuf { let stem = input_csv_path + .as_path() .file_stem() .and_then(|stem| stem.to_str()) .unwrap_or("agent_job_output"); let job_suffix = &job_id[..8]; - input_csv_path.with_file_name(format!("{stem}.agent-job-{job_suffix}.csv")) + let output_dir = input_csv_path + .parent() + .unwrap_or_else(|| input_csv_path.clone()); + output_dir.join(format!("{stem}.agent-job-{job_suffix}.csv")) } fn parse_csv(content: &str) -> Result<(Vec, Vec>), String> { diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index ec6fc02cf..022e02c39 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -21,11 +21,11 @@ mod view_image; use codex_sandboxing::policy_transforms::intersect_permission_profiles; use codex_sandboxing::policy_transforms::merge_permission_profiles; use codex_sandboxing::policy_transforms::normalize_additional_permissions; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use serde::Deserialize; use serde_json::Value; use std::path::Path; -use std::path::PathBuf; use crate::codex::Session; use crate::function_tool::FunctionCallError; @@ -63,7 +63,7 @@ where fn parse_arguments_with_base_path( arguments: &str, - base_path: &Path, + base_path: &AbsolutePathBuf, ) -> Result where T: for<'de> Deserialize<'de>, @@ -74,18 +74,14 @@ where fn resolve_workdir_base_path( arguments: &str, - default_cwd: &Path, -) -> Result { + default_cwd: &AbsolutePathBuf, +) -> Result { let arguments: Value = parse_arguments(arguments)?; Ok(arguments .get("workdir") .and_then(Value::as_str) .filter(|workdir| !workdir.is_empty()) - .map(PathBuf::from) - .map_or_else( - || default_cwd.to_path_buf(), - |workdir| crate::util::resolve_path(default_cwd, &workdir), - )) + .map_or_else(|| default_cwd.clone(), |workdir| default_cwd.join(workdir))) } /// Validates feature/policy constraints for `with_additional_permissions` and diff --git a/codex-rs/core/src/tools/handlers/request_permissions.rs b/codex-rs/core/src/tools/handlers/request_permissions.rs index 2590b9d7d..440bb18ca 100644 --- a/codex-rs/core/src/tools/handlers/request_permissions.rs +++ b/codex-rs/core/src/tools/handlers/request_permissions.rs @@ -37,7 +37,7 @@ impl ToolHandler for RequestPermissionsHandler { }; let mut args: RequestPermissionsArgs = - parse_arguments_with_base_path(&arguments, turn.cwd.as_path())?; + parse_arguments_with_base_path(&arguments, &turn.cwd)?; args.permissions = normalize_additional_permissions(args.permissions.into()) .map(codex_protocol::request_permissions::RequestPermissionProfile::from) .map_err(FunctionCallError::RespondToModel)?; diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index e83556624..970afd6f3 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -39,7 +39,6 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ExecCommandSource; use codex_shell_command::is_safe_command::is_known_safe_command; use codex_tools::ShellCommandBackendConfig; -use codex_utils_absolute_path::AbsolutePathBuf; pub struct ShellHandler; @@ -234,9 +233,8 @@ impl ToolHandler for ShellHandler { match payload { ToolPayload::Function { arguments } => { - let cwd = resolve_workdir_base_path(&arguments, turn.cwd.as_path())?; - let params: ShellToolCallParams = - parse_arguments_with_base_path(&arguments, cwd.as_path())?; + let cwd = resolve_workdir_base_path(&arguments, &turn.cwd)?; + let params: ShellToolCallParams = parse_arguments_with_base_path(&arguments, &cwd)?; let prefix_rule = params.prefix_rule.clone(); let exec_params = Self::to_exec_params(¶ms, turn.as_ref(), session.conversation_id); @@ -345,9 +343,8 @@ impl ToolHandler for ShellCommandHandler { ))); }; - let cwd = resolve_workdir_base_path(&arguments, turn.cwd.as_path())?; - let params: ShellCommandToolCallParams = - parse_arguments_with_base_path(&arguments, cwd.as_path())?; + let cwd = resolve_workdir_base_path(&arguments, &turn.cwd)?; + let params: ShellCommandToolCallParams = parse_arguments_with_base_path(&arguments, &cwd)?; let workdir = turn.resolve_path(params.workdir.clone()); maybe_emit_implicit_skill_invocation( session.as_ref(), @@ -466,15 +463,9 @@ impl ShellHandler { } // Intercept apply_patch if present. - let apply_patch_cwd = - AbsolutePathBuf::from_absolute_path(&exec_params.cwd).map_err(|err| { - FunctionCallError::RespondToModel(format!( - "apply_patch verification failed: failed to resolve cwd: {err}" - )) - })?; if let Some(output) = intercept_apply_patch( &exec_params.command, - &apply_patch_cwd, + &exec_params.cwd, fs.as_ref(), exec_params.expiration.timeout_ms(), session.clone(), @@ -491,7 +482,7 @@ impl ShellHandler { let source = ExecCommandSource::Agent; let emitter = ToolEmitter::shell( exec_params.command.clone(), - exec_params.cwd.clone(), + exec_params.cwd.to_path_buf(), source, freeform, ); diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index b83068c66..fc1ad94b4 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -30,7 +30,6 @@ use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TerminalInteractionEvent; use codex_shell_command::is_safe_command::is_known_safe_command; use codex_tools::UnifiedExecShellMode; -use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use std::path::PathBuf; use std::sync::Arc; @@ -189,9 +188,8 @@ impl ToolHandler for UnifiedExecHandler { let response = match tool_name.as_str() { "exec_command" => { - let cwd = resolve_workdir_base_path(&arguments, context.turn.cwd.as_path())?; - let args: ExecCommandArgs = - parse_arguments_with_base_path(&arguments, cwd.as_path())?; + let cwd = resolve_workdir_base_path(&arguments, &context.turn.cwd)?; + let args: ExecCommandArgs = parse_arguments_with_base_path(&arguments, &cwd)?; let workdir = context.turn.resolve_path(args.workdir.clone()); maybe_emit_implicit_skill_invocation( session.as_ref(), @@ -282,18 +280,9 @@ impl ToolHandler for UnifiedExecHandler { } }; - let apply_patch_cwd = match AbsolutePathBuf::from_absolute_path(&cwd) { - Ok(cwd) => cwd, - Err(err) => { - manager.release_process_id(process_id).await; - return Err(FunctionCallError::RespondToModel(format!( - "apply_patch verification failed: failed to resolve cwd: {err}" - ))); - } - }; if let Some(output) = intercept_apply_patch( &command, - &apply_patch_cwd, + &cwd, fs.as_ref(), Some(yield_time_ms), context.session.clone(), 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 7c40eeff1..1bdcee05e 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -7,6 +7,7 @@ use codex_protocol::models::PermissionProfile; use codex_tools::UnifiedExecShellMode; use codex_tools::ZshForkConfig; use codex_utils_absolute_path::AbsolutePathBuf; +use core_test_support::PathExt; use pretty_assertions::assert_eq; use std::fs; use std::sync::Arc; @@ -181,15 +182,15 @@ fn exec_command_args_resolve_relative_additional_permissions_against_workdir() - } }"#; - let base_path = resolve_workdir_base_path(json, cwd.path())?; - let args: ExecCommandArgs = parse_arguments_with_base_path(json, base_path.as_path())?; + let base_path = resolve_workdir_base_path(json, &cwd.path().abs())?; + let args: ExecCommandArgs = parse_arguments_with_base_path(json, &base_path)?; assert_eq!( args.additional_permissions, Some(PermissionProfile { file_system: Some(FileSystemPermissions { read: None, - write: Some(vec![AbsolutePathBuf::try_from(expected_write)?]), + write: Some(vec![expected_write.abs()]), }), ..Default::default() }) diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index 5fa005bac..5e39ae618 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -4,7 +4,6 @@ use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ImageDetail; use codex_protocol::models::ResponseInputItem; use codex_protocol::openai_models::InputModality; -use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_image::PromptImageMode; use codex_utils_image::load_for_prompt_bytes; use serde::Deserialize; @@ -87,10 +86,7 @@ impl ToolHandler for ViewImageHandler { } }; - let abs_path = - AbsolutePathBuf::try_from(turn.resolve_path(Some(args.path))).map_err(|error| { - FunctionCallError::RespondToModel(format!("unable to resolve image path: {error}")) - })?; + let abs_path = turn.resolve_path(Some(args.path)); let Some(environment) = turn.environment.as_ref() else { return Err(FunctionCallError::RespondToModel( "view_image is unavailable in this session".to_string(), diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index b00ecde8e..65ab56ec8 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1050,7 +1050,7 @@ impl JsReplManager { "--experimental-vm-modules".to_string(), kernel_path.to_string_lossy().to_string(), ], - cwd: turn.cwd.to_path_buf(), + cwd: turn.cwd.clone(), env, additional_permissions: None, }; @@ -1068,7 +1068,7 @@ impl JsReplManager { enforce_managed_network: has_managed_network_requirements, network: None, sandbox_policy_cwd: &turn.cwd, - codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), + codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_deref(), use_legacy_landlock: turn.features.use_legacy_landlock(), windows_sandbox_level: turn.windows_sandbox_level, windows_sandbox_private_desktop: turn diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 3104f4eae..b2b7a73f8 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -104,7 +104,7 @@ impl ApplyPatchRuntime { CODEX_CORE_APPLY_PATCH_ARG1.to_string(), req.action.patch.clone(), ], - cwd: req.action.cwd.to_path_buf(), + cwd: req.action.cwd.clone(), // Run apply_patch with a minimal environment for determinism and to avoid leaks. env: HashMap::new(), additional_permissions: req.additional_permissions.clone(), diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 99b5371f1..5ea14b8a8 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -10,6 +10,7 @@ use crate::shell::Shell; use crate::tools::sandboxing::ToolError; use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxCommand; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; @@ -21,7 +22,7 @@ pub(crate) mod unified_exec; /// Validates that at least a program is present. pub(crate) fn build_sandbox_command( command: &[String], - cwd: &Path, + cwd: &AbsolutePathBuf, env: &HashMap, additional_permissions: Option, ) -> Result { @@ -31,7 +32,7 @@ pub(crate) fn build_sandbox_command( Ok(SandboxCommand { program: program.clone().into(), args: args.to_vec(), - cwd: cwd.to_path_buf(), + cwd: cwd.clone(), env: env.clone(), additional_permissions, }) diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 7b0c81ac6..4ff04b002 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -38,14 +38,14 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxablePreference; use codex_shell_command::powershell::prefix_powershell_script_with_utf8; +use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::collections::HashMap; -use std::path::PathBuf; #[derive(Clone, Debug)] pub struct ShellRequest { pub command: Vec, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub timeout_ms: Option, pub env: HashMap, pub explicit_env_overrides: HashMap, @@ -93,7 +93,7 @@ pub struct ShellRuntime { #[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct ApprovalKey { command: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, sandbox_permissions: SandboxPermissions, additional_permissions: Option, } @@ -146,7 +146,7 @@ impl Approvable for ShellRuntime { ) -> BoxFuture<'a, ReviewDecision> { let keys = self.approval_keys(req); let command = req.command.clone(); - let cwd = req.cwd.clone(); + let cwd = req.cwd.to_path_buf(); let retry_reason = ctx.retry_reason.clone(); let reason = retry_reason.clone().or_else(|| req.justification.clone()); let session = ctx.session; 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 b52fd8c86..2e8d65296 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -658,7 +658,7 @@ fn commands_for_intercepted_exec_policy( struct CoreShellCommandExecutor { command: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, sandbox_policy: SandboxPolicy, file_system_sandbox_policy: FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, @@ -827,7 +827,7 @@ impl CoreShellCommandExecutor { let command = SandboxCommand { program: program.clone().into(), args: args.to_vec(), - cwd: workdir.to_path_buf(), + cwd: workdir.clone(), env, additional_permissions, }; @@ -844,7 +844,7 @@ impl CoreShellCommandExecutor { enforce_managed_network: self.network.is_some(), network: self.network.as_ref(), sandbox_policy_cwd: &self.sandbox_policy_cwd, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), + codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(), use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, @@ -857,7 +857,7 @@ impl CoreShellCommandExecutor { Ok(PreparedExec { command: exec_request.command, - cwd: exec_request.cwd, + cwd: exec_request.cwd.to_path_buf(), env: exec_request.env, arg0: exec_request.arg0, }) diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 7b271363b..86b6163b8 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -41,9 +41,9 @@ use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxablePreference; use codex_shell_command::powershell::prefix_powershell_script_with_utf8; use codex_tools::UnifiedExecShellMode; +use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::collections::HashMap; -use std::path::PathBuf; /// Request payload used by the unified-exec runtime after approvals and /// sandbox preferences have been resolved for the current turn. @@ -51,7 +51,7 @@ use std::path::PathBuf; pub struct UnifiedExecRequest { pub command: Vec, pub process_id: i32, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub env: HashMap, pub explicit_env_overrides: HashMap, pub network: Option, @@ -69,7 +69,7 @@ pub struct UnifiedExecRequest { #[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)] pub struct UnifiedExecApprovalKey { pub command: Vec, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub tty: bool, pub sandbox_permissions: SandboxPermissions, pub additional_permissions: Option, @@ -125,7 +125,7 @@ impl Approvable for UnifiedExecRuntime<'_> { let turn = ctx.turn; let call_id = ctx.call_id.to_string(); let command = req.command.clone(); - let cwd = req.cwd.clone(); + let cwd = req.cwd.to_path_buf(); let retry_reason = ctx.retry_reason.clone(); let reason = retry_reason.clone().or_else(|| req.justification.clone()); Box::pin(async move { diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index c2671aabf..b1ce3e7d9 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -348,7 +348,9 @@ impl<'a> SandboxAttempt<'a> { enforce_managed_network: self.enforce_managed_network, network, sandbox_policy_cwd: self.sandbox_cwd, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, + codex_linux_sandbox_exe: self + .codex_linux_sandbox_exe + .map(std::path::PathBuf::as_path), use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index eb25f4ddc..3f3b018df 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -24,12 +24,12 @@ use std::collections::HashMap; use std::collections::HashSet; -use std::path::PathBuf; use std::sync::Arc; use std::sync::Weak; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; use rand::Rng; use rand::rng; use tokio::sync::Mutex; @@ -91,7 +91,7 @@ pub(crate) struct ExecCommandRequest { pub process_id: i32, pub yield_time_ms: u64, pub max_output_tokens: Option, - pub workdir: Option, + pub workdir: Option, pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index a5db73bf4..2cc280e1e 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -51,7 +51,7 @@ fn shell_env() -> HashMap { fn test_exec_request( turn: &TurnContext, command: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, env: HashMap, ) -> ExecRequest { let windows_sandbox_private_desktop = false; @@ -87,7 +87,9 @@ async fn exec_command_with_tty( ) -> Result { let manager = &session.services.unified_exec_manager; let process_id = manager.allocate_process_id().await; - let cwd = workdir.unwrap_or_else(|| turn.cwd.clone().to_path_buf()); + let cwd = workdir + .as_ref() + .map_or_else(|| turn.cwd.clone(), |workdir| turn.cwd.join(workdir)); let command = vec!["bash".to_string(), "-lc".to_string(), cmd.to_string()]; let request = test_exec_request(turn, command.clone(), cwd.clone(), shell_env()); @@ -502,7 +504,7 @@ async fn completed_pipe_commands_preserve_exit_code() -> anyhow::Result<()> { let request = test_exec_request( &turn, vec!["bash".to_string(), "-lc".to_string(), "exit 17".to_string()], - PathBuf::from("/tmp"), + turn.cwd.clone(), shell_env(), ); @@ -544,7 +546,7 @@ async fn unified_exec_uses_remote_exec_server_when_configured() -> anyhow::Resul let request = test_exec_request( &turn, vec!["bash".to_string(), "-i".to_string()], - PathBuf::from("/tmp"), + remote_test_env.cwd().clone(), shell_env(), ); @@ -598,7 +600,7 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<() let request = test_exec_request( &turn, vec!["bash".to_string(), "-lc".to_string(), "echo ok".to_string()], - PathBuf::from("/tmp"), + turn.cwd.clone(), shell_env(), ); diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index bc4ffe0d3..9d77ac4e9 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -2,7 +2,6 @@ use rand::Rng; use std::cmp::Reverse; use std::collections::HashMap; use std::collections::HashSet; -use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -49,6 +48,7 @@ use crate::unified_exec::process::OutputHandles; use crate::unified_exec::process::SpawnLifecycleHandle; use crate::unified_exec::process::UnifiedExecProcess; use codex_protocol::protocol::ExecCommandSource; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_output_truncation::approx_token_count; const UNIFIED_EXEC_ENV: [(&str, &str); 10] = [ @@ -165,7 +165,7 @@ impl UnifiedExecProcessManager { let cwd = request .workdir .clone() - .unwrap_or_else(|| context.turn.cwd.to_path_buf()); + .unwrap_or_else(|| context.turn.cwd.clone()); let process = self .open_session_with_sandbox(&request, cwd.clone(), context) .await; @@ -189,7 +189,7 @@ impl UnifiedExecProcessManager { ); let emitter = ToolEmitter::unified_exec( &request.command, - cwd.clone(), + cwd.to_path_buf(), ExecCommandSource::UnifiedExecStartup, Some(request.process_id.to_string()), ); @@ -255,7 +255,7 @@ impl UnifiedExecProcessManager { Arc::clone(&context.turn), context.call_id.clone(), request.command.clone(), - cwd.clone(), + cwd.to_path_buf(), Some(request.process_id.to_string()), Arc::clone(&transcript), message.clone(), @@ -298,7 +298,7 @@ impl UnifiedExecProcessManager { Arc::clone(&context.turn), context.call_id.clone(), request.command.clone(), - cwd.clone(), + cwd.to_path_buf(), Some(process_id.to_string()), Arc::clone(&transcript), text.clone(), @@ -526,7 +526,7 @@ impl UnifiedExecProcessManager { process: Arc, context: &UnifiedExecContext, command: &[String], - cwd: PathBuf, + cwd: AbsolutePathBuf, started_at: Instant, process_id: i32, tty: bool, @@ -572,7 +572,7 @@ impl UnifiedExecProcessManager { Arc::clone(&context.turn), context.call_id.clone(), command.to_vec(), - cwd, + cwd.to_path_buf(), process_id, transcript, started_at, @@ -605,7 +605,7 @@ impl UnifiedExecProcessManager { .start(codex_exec_server::ExecParams { process_id: exec_server_process_id(process_id).into(), argv: request.command.clone(), - cwd: request.cwd.clone(), + cwd: request.cwd.to_path_buf(), env: request.env.clone(), tty, arg0: request.arg0.clone(), @@ -646,7 +646,7 @@ impl UnifiedExecProcessManager { pub(super) async fn open_session_with_sandbox( &self, request: &ExecCommandRequest, - cwd: PathBuf, + cwd: AbsolutePathBuf, context: &UnifiedExecContext, ) -> Result<(UnifiedExecProcess, Option), UnifiedExecError> { let env = apply_unified_exec_env(create_env( diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 8739fd95e..d819b1cb6 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -16,6 +16,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; +use core_test_support::PathExt; use tempfile::TempDir; fn skip_test() -> bool { @@ -35,7 +36,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result Result { - let cwd = std::env::current_dir().expect("cwd should exist"); + let cwd = AbsolutePathBuf::current_dir().expect("cwd should exist"); let sandbox_cwd = cwd.clone(); let params = ExecParams { command: cmd.iter().copied().map(str::to_owned).collect(), @@ -375,7 +375,7 @@ async fn test_timeout() { /// suite remains green on leaner CI images. #[expect(clippy::expect_used)] async fn assert_network_blocked(cmd: &[&str]) { - let cwd = std::env::current_dir().expect("cwd should exist"); + let cwd = AbsolutePathBuf::current_dir().expect("cwd should exist"); let sandbox_cwd = cwd.clone(); let params = ExecParams { command: cmd.iter().copied().map(str::to_owned).collect(), diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index c89e7cd9b..4ed413735 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -15,10 +15,10 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::ffi::OsString; use std::path::Path; -use std::path::PathBuf; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SandboxType { @@ -66,7 +66,7 @@ pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub env: HashMap, pub additional_permissions: Option, } @@ -74,7 +74,7 @@ pub struct SandboxCommand { #[derive(Debug)] pub struct SandboxExecRequest { pub command: Vec, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub env: HashMap, pub network: Option, pub sandbox: SandboxType, @@ -100,7 +100,7 @@ pub struct SandboxTransformRequest<'a> { // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, - pub codex_linux_sandbox_exe: Option<&'a PathBuf>, + pub codex_linux_sandbox_exe: Option<&'a Path>, pub use_legacy_landlock: bool, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, @@ -232,10 +232,7 @@ impl SandboxManager { let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(os_string_to_command_component(exe.as_os_str().to_owned())); full_command.append(&mut args); - ( - full_command, - Some(linux_sandbox_arg0_override(exe.as_path())), - ) + (full_command, Some(linux_sandbox_arg0_override(exe))) } #[cfg(target_os = "windows")] SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None), diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 9c2821eda..39531f13b 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -74,7 +74,7 @@ fn restricted_file_system_uses_platform_sandbox_without_managed_network() { #[test] fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() { let manager = SandboxManager::new(); - let cwd = std::env::current_dir().expect("current dir"); + let cwd = AbsolutePathBuf::current_dir().expect("current dir"); let exec_request = manager .transform(SandboxTransformRequest { command: SandboxCommand { @@ -113,7 +113,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() #[test] fn transform_additional_permissions_enable_network_for_external_sandbox() { let manager = SandboxManager::new(); - let cwd = std::env::current_dir().expect("current dir"); + let cwd = AbsolutePathBuf::current_dir().expect("current dir"); let temp_dir = TempDir::new().expect("create temp dir"); let path = AbsolutePathBuf::from_absolute_path( canonicalize(temp_dir.path()).expect("canonicalize temp dir"), @@ -167,7 +167,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { #[test] fn transform_additional_permissions_preserves_denied_entries() { let manager = SandboxManager::new(); - let cwd = std::env::current_dir().expect("current dir"); + let cwd = AbsolutePathBuf::current_dir().expect("current dir"); let temp_dir = TempDir::new().expect("create temp dir"); let workspace_root = AbsolutePathBuf::from_absolute_path( canonicalize(temp_dir.path()).expect("canonicalize temp dir"), @@ -247,10 +247,10 @@ fn transform_additional_permissions_preserves_denied_entries() { #[cfg(target_os = "linux")] fn transform_linux_seccomp_request( - codex_linux_sandbox_exe: &std::path::PathBuf, + codex_linux_sandbox_exe: &std::path::Path, ) -> super::SandboxExecRequest { let manager = SandboxManager::new(); - let cwd = std::env::current_dir().expect("current dir"); + let cwd = AbsolutePathBuf::current_dir().expect("current dir"); manager .transform(SandboxTransformRequest { command: SandboxCommand {