diff --git a/codex-rs/windows-sandbox-rs/src/helper_materialization.rs b/codex-rs/windows-sandbox-rs/src/helper_materialization.rs index e5b202dede..21db9d270d 100644 --- a/codex-rs/windows-sandbox-rs/src/helper_materialization.rs +++ b/codex-rs/windows-sandbox-rs/src/helper_materialization.rs @@ -22,18 +22,21 @@ pub(crate) const RESOURCES_DIRNAME: &str = "codex-resources"; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) enum HelperExecutable { CommandRunner, + Setup, } impl HelperExecutable { fn file_name(self) -> &'static str { match self { Self::CommandRunner => "codex-command-runner.exe", + Self::Setup => "codex-windows-sandbox-setup.exe", } } fn label(self) -> &'static str { match self { Self::CommandRunner => "command-runner", + Self::Setup => "setup", } } } @@ -188,6 +191,16 @@ fn sibling_source_path(kind: HelperExecutable) -> Result { } pub(crate) fn bundled_executable_path_for_exe(exe: &Path, file_name: &str) -> Option { + bundled_executable_path_for_exe_direct(exe, file_name).or_else(|| { + let canonical = dunce::canonicalize(exe).ok()?; + if canonical == exe { + return None; + } + bundled_executable_path_for_exe_direct(&canonical, file_name) + }) +} + +fn bundled_executable_path_for_exe_direct(exe: &Path, file_name: &str) -> Option { let dir = exe.parent()?; let direct_candidate = dir.join(file_name); if direct_candidate.is_file() { @@ -458,6 +471,28 @@ mod tests { ); } + #[test] + fn copy_setup_into_shared_bin_dir() { + let tmp = TempDir::new().expect("tempdir"); + let codex_home = tmp.path().join("codex-home"); + let source_dir = tmp.path().join("sibling-source"); + fs::create_dir_all(&source_dir).expect("create source dir"); + let setup_source = source_dir.join("codex-windows-sandbox-setup.exe"); + fs::write(&setup_source, b"setup").expect("setup"); + let setup_suffix = helper_version_suffix(&setup_source).expect("setup suffix"); + let setup_destination = helper_bin_dir(&codex_home) + .join(materialized_file_name(HelperExecutable::Setup, &setup_suffix)); + + let setup_outcome = + copy_from_source_if_needed(&setup_source, &setup_destination).expect("setup copy"); + + assert_eq!(CopyOutcome::ReCopied, setup_outcome); + assert_eq!( + b"setup".as_slice(), + fs::read(&setup_destination).expect("read setup") + ); + } + #[test] fn helper_source_lookup_checks_resource_dir() { let tmp = TempDir::new().expect("tempdir"); @@ -539,6 +574,32 @@ mod tests { assert_eq!(resolved, sibling_helper); } + #[cfg(unix)] + #[test] + fn helper_source_lookup_retries_canonicalized_exe_path() { + let tmp = TempDir::new().expect("tempdir"); + let package_dir = tmp.path().join("package"); + let bin_dir = package_dir.join(BIN_DIRNAME); + let resources_dir = package_dir.join(RESOURCES_DIRNAME); + let visible_dir = tmp.path().join("visible").join(BIN_DIRNAME); + fs::create_dir_all(&bin_dir).expect("create bin dir"); + fs::create_dir_all(&resources_dir).expect("create resources dir"); + fs::create_dir_all(&visible_dir).expect("create visible dir"); + + let real_exe = bin_dir.join("codex.exe"); + let visible_exe = visible_dir.join("codex.exe"); + let helper = resources_dir.join("codex-command-runner.exe"); + fs::write(&real_exe, b"codex").expect("write exe"); + fs::write(&helper, b"runner").expect("write helper"); + std::os::unix::fs::symlink(&real_exe, &visible_exe).expect("symlink visible exe"); + + let resolved = + bundled_executable_path_for_exe(&visible_exe, /*file_name*/ "codex-command-runner.exe") + .expect("helper path"); + + assert_eq!(resolved, helper); + } + #[test] fn helper_version_suffix_uses_cli_version_or_dev_build_metadata() { let tmp = TempDir::new().expect("tempdir"); @@ -559,4 +620,11 @@ mod tests { assert_eq!(file_name, "codex-command-runner-test-suffix.exe"); } + + #[test] + fn materialized_setup_file_name_adds_suffix_before_extension() { + let file_name = materialized_file_name(HelperExecutable::Setup, "test-suffix"); + + assert_eq!(file_name, "codex-windows-sandbox-setup-test-suffix.exe"); + } } diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs index a0f46fe3f8..73c126f5ba 100644 --- a/codex-rs/windows-sandbox-rs/src/setup.rs +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -12,8 +12,10 @@ use std::process::Stdio; use crate::allow::AllowDenyPaths; use crate::allow::compute_allow_paths_for_permissions; +use crate::helper_materialization::HelperExecutable; use crate::helper_materialization::bundled_executable_path_for_exe; use crate::helper_materialization::helper_bin_dir; +use crate::helper_materialization::resolve_helper_for_launch; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; use crate::path_normalization::canonicalize_path; @@ -208,7 +210,7 @@ fn run_setup_refresh_inner( }; let json = serde_json::to_vec(&payload)?; let b64 = BASE64_STANDARD.encode(json); - let exe = find_setup_exe(); + let exe = find_setup_exe(request.codex_home); // Refresh should never request elevation; ensure verb isn't set and we don't trigger UAC. let mut cmd = Command::new(&exe); cmd.arg(&b64).stdout(Stdio::null()).stderr(Stdio::null()); @@ -639,13 +641,12 @@ fn quote_arg(arg: &str) -> String { out } -fn find_setup_exe() -> PathBuf { - if let Ok(exe) = std::env::current_exe() - && let Some(setup_exe) = find_setup_exe_for_current_exe(&exe) - { - return setup_exe; - } - PathBuf::from(SETUP_EXE_FILENAME) +fn find_setup_exe(codex_home: &Path) -> PathBuf { + resolve_helper_for_launch( + HelperExecutable::Setup, + codex_home, + Some(&sandbox_dir(codex_home)), + ) } fn find_setup_exe_for_current_exe(exe: &Path) -> Option { @@ -682,7 +683,7 @@ fn run_setup_exe( use windows_sys::Win32::UI::Shell::SEE_MASK_NOCLOSEPROCESS; use windows_sys::Win32::UI::Shell::SHELLEXECUTEINFOW; use windows_sys::Win32::UI::Shell::ShellExecuteExW; - let exe = find_setup_exe(); + let exe = find_setup_exe(codex_home); let payload_json = serde_json::to_string(payload).map_err(|err| { failure( SetupErrorCode::OrchestratorPayloadSerializeFailed,