diff --git a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs index e1146987bf..c37da06707 100644 --- a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs +++ b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs @@ -165,8 +165,10 @@ fn spawn_read_acl_helper(payload: &Payload, _log: &mut dyn Write) -> Result<()> let payload_json = serde_json::to_vec(&read_payload)?; let payload_b64 = BASE64.encode(payload_json); let exe = std::env::current_exe().context("locate setup helper")?; + let spawn_cwd = crate::setup::helper_spawn_dir(&payload.codex_home); Command::new(&exe) .arg(payload_b64) + .current_dir(spawn_cwd) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs index 1995c33365..0dd4e33ef5 100644 --- a/codex-rs/windows-sandbox-rs/src/setup.rs +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -226,14 +226,17 @@ fn run_setup_refresh_inner( } }; // Refresh should never request elevation; ensure verb isn't set and we don't trigger UAC. + let spawn_cwd = helper_spawn_dir(request.codex_home); let mut cmd = Command::new(&exe); - cmd.arg(&b64).stdout(Stdio::null()).stderr(Stdio::null()); - let cwd = std::env::current_dir().unwrap_or_else(|_| request.codex_home.to_path_buf()); + cmd.arg(&b64) + .current_dir(&spawn_cwd) + .stdout(Stdio::null()) + .stderr(Stdio::null()); log_note( &format!( "setup refresh: spawning {} (cwd={}, payload_len={})", exe.display(), - cwd.display(), + spawn_cwd.display(), b64.len() ), Some(&sbx_dir), @@ -242,7 +245,7 @@ fn run_setup_refresh_inner( let message = format!( "setup refresh failed to launch helper: helper={}, cwd={}, log={}, error={err}", exe.display(), - cwd.display(), + spawn_cwd.display(), log_path.display() ); log_note(&format!("setup refresh: {message}"), Some(&sbx_dir)); @@ -405,6 +408,20 @@ fn gather_helper_read_roots(codex_home: &Path) -> Vec { vec![helper_dir] } +pub(crate) fn helper_spawn_dir(codex_home: &Path) -> PathBuf { + // Launch helper processes from a Codex-local directory instead of inheriting + // the workspace cwd, which may be a UNC-backed mount that Windows helper + // process creation handles poorly. + let helper_dir = helper_bin_dir(codex_home); + if std::fs::create_dir_all(&helper_dir).is_ok() { + return helper_dir; + } + + let sandbox_dir = sandbox_dir(codex_home); + let _ = std::fs::create_dir_all(&sandbox_dir); + sandbox_dir +} + fn gather_full_read_roots_for_permissions( command_cwd: &Path, permissions: &ResolvedWindowsSandboxPermissions, @@ -758,9 +775,11 @@ fn run_setup_exe( } }; + let spawn_cwd = helper_spawn_dir(codex_home); if !needs_elevation { let status = Command::new(&exe) .arg(&payload_b64) + .current_dir(&spawn_cwd) .creation_flags(0x08000000) // CREATE_NO_WINDOW .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -795,12 +814,14 @@ fn run_setup_exe( let params = quote_arg(&payload_b64); let params_w = crate::winutil::to_wide(params); let verb_w = crate::winutil::to_wide("runas"); + let directory_w = crate::winutil::to_wide(&spawn_cwd); let mut sei: SHELLEXECUTEINFOW = unsafe { std::mem::zeroed() }; sei.cbSize = std::mem::size_of::() as u32; sei.fMask = SEE_MASK_NOCLOSEPROCESS; sei.lpVerb = verb_w.as_ptr(); sei.lpFile = exe_w.as_ptr(); sei.lpParameters = params_w.as_ptr(); + sei.lpDirectory = directory_w.as_ptr(); // Hide the window for the elevated helper. sei.nShow = 0; // SW_HIDE let ok = unsafe { ShellExecuteExW(&mut sei) }; @@ -1662,6 +1683,17 @@ mod tests { assert!(roots.contains(&expected)); } + #[test] + fn helper_spawn_dir_uses_local_helper_directory() { + let tmp = TempDir::new().expect("tempdir"); + let codex_home = tmp.path().join("codex-home"); + + let spawn_dir = super::helper_spawn_dir(&codex_home); + + assert_eq!(spawn_dir, helper_bin_dir(&codex_home)); + assert!(spawn_dir.is_dir(), "helper spawn dir should be created"); + } + #[test] fn workspace_write_roots_remain_readable() { let tmp = TempDir::new().expect("tempdir");