diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js index 1a43ce7e1f..5cc5194183 100755 --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -75,45 +75,25 @@ if (!platformPackage) { throw new Error(`Unsupported target triple: ${targetTriple}`); } -const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex"; -const localVendorRoot = path.join(__dirname, "..", "vendor"); -const packageBinaryPath = (vendorRoot) => - path.join(vendorRoot, targetTriple, "bin", codexBinaryName); -const legacyBinaryPath = (vendorRoot) => - path.join(vendorRoot, targetTriple, "codex", codexBinaryName); - -function resolveNativePackage(vendorRoot) { - const packageRoot = path.join(vendorRoot, targetTriple); - const binaryPath = packageBinaryPath(vendorRoot); - if (existsSync(binaryPath)) { - return { - binaryPath, - pathDir: path.join(packageRoot, "codex-path"), - }; +function findCodexExecutable() { + let vendorRoot; + try { + const packageJsonPath = require.resolve(`${platformPackage}/package.json`); + vendorRoot = path.join(path.dirname(packageJsonPath), "vendor"); + } catch { + vendorRoot = path.join(__dirname, "..", "vendor"); } - const legacyPath = legacyBinaryPath(vendorRoot); - if (existsSync(legacyPath)) { - return { - binaryPath: legacyPath, - pathDir: path.join(packageRoot, "path"), - }; - } - - return null; -} - -let nativePackage; -try { - const packageJsonPath = require.resolve(`${platformPackage}/package.json`); - nativePackage = resolveNativePackage( - path.join(path.dirname(packageJsonPath), "vendor"), + const codexExecutable = path.join( + vendorRoot, + targetTriple, + "bin", + process.platform === "win32" ? "codex.exe" : "codex", ); -} catch { - nativePackage = resolveNativePackage(localVendorRoot); -} + if (existsSync(codexExecutable)) { + return codexExecutable; + } -if (!nativePackage) { const packageManager = detectPackageManager(); const updateCommand = packageManager === "bun" @@ -124,7 +104,7 @@ if (!nativePackage) { ); } -const { binaryPath, pathDir } = nativePackage; +const binaryPath = findCodexExecutable(); // Use an asynchronous spawn instead of spawnSync so that Node is able to // respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is @@ -132,16 +112,6 @@ const { binaryPath, pathDir } = nativePackage; // and guarantees that when either the child terminates or the parent // receives a fatal signal, both processes exit in a predictable manner. -function getUpdatedPath(newDirs) { - const pathSep = process.platform === "win32" ? ";" : ":"; - const existingPath = process.env.PATH || ""; - const updatedPath = [ - ...newDirs, - ...existingPath.split(pathSep).filter(Boolean), - ].join(pathSep); - return updatedPath; -} - /** * Use heuristics to detect the package manager that was used to install Codex * in order to give the user a hint about how to update it. @@ -167,19 +137,15 @@ function detectPackageManager() { return userAgent ? "npm" : null; } -const additionalDirs = []; -if (existsSync(pathDir)) { - additionalDirs.push(pathDir); -} -const updatedPath = getUpdatedPath(additionalDirs); - -const env = { ...process.env, PATH: updatedPath }; const packageManagerEnvVar = detectPackageManager() === "bun" ? "CODEX_MANAGED_BY_BUN" : "CODEX_MANAGED_BY_NPM"; -env[packageManagerEnvVar] = "1"; -env.CODEX_MANAGED_PACKAGE_ROOT = realpathSync(path.join(__dirname, "..")); +const env = { + ...process.env, + [packageManagerEnvVar]: "1", + CODEX_MANAGED_PACKAGE_ROOT: realpathSync(path.join(__dirname, "..")), +}; const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2183d91b64..e9c171d778 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2145,12 +2145,14 @@ dependencies = [ "anyhow", "codex-apply-patch", "codex-exec-server", + "codex-install-context", "codex-linux-sandbox", "codex-sandboxing", "codex-shell-escalation", "codex-utils-absolute-path", "codex-utils-home-dir", "dotenvy", + "pretty_assertions", "tempfile", "tokio", ] diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 7ee21a770e..55526b4d06 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -16,6 +16,7 @@ workspace = true anyhow = { workspace = true } codex-apply-patch = { workspace = true } codex-exec-server = { workspace = true } +codex-install-context = { workspace = true } codex-linux-sandbox = { workspace = true } codex-sandboxing = { workspace = true } codex-shell-escalation = { workspace = true } @@ -24,3 +25,6 @@ codex-utils-home-dir = { workspace = true } dotenvy = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 87940f1182..1c47923d94 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -1,3 +1,4 @@ +use std::ffi::OsString; use std::fs::File; use std::future::Future; use std::path::Path; @@ -5,6 +6,7 @@ use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_exec_server::CODEX_FS_HELPER_ARG1; +use codex_install_context::InstallContext; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] @@ -137,13 +139,23 @@ pub fn arg0_dispatch() -> Option { // This modifies the environment, which is not thread-safe, so do this // before creating any threads/the Tokio runtime. load_dotenv(); + let existing_path = std::env::var_os("PATH"); + let package_path = + path_env_with_package_path_dir(InstallContext::current(), existing_path.clone()); + let path_for_aliases = package_path.clone().or(existing_path); - match prepend_path_entry_for_codex_aliases() { - Ok(path_entry) => Some(path_entry), + match prepare_path_entry_for_codex_aliases(path_for_aliases) { + Ok((path_entry, updated_path_env_var)) => { + set_path_env_var(updated_path_env_var); + Some(path_entry) + } Err(err) => { + if let Some(package_path) = package_path { + set_path_env_var(package_path); + } // It is possible that Codex will proceed successfully even if - // updating the PATH fails, so warn the user and move on. - eprintln!("WARNING: proceeding, even though we could not update PATH: {err}"); + // creating helper aliases fails, so warn the user and move on. + eprintln!("WARNING: proceeding, even though we could not create PATH aliases: {err}"); None } } @@ -286,14 +298,22 @@ where /// with the hidden `--codex-run-as-apply-patch` flag. /// /// This temporary directory is prepended to the PATH environment variable so -/// that `apply_patch` can be on the PATH without requiring the user to -/// install a separate `apply_patch` executable, simplifying the deployment of -/// Codex CLI. +/// that `apply_patch` can be on the PATH without requiring the user to install +/// a separate executable, simplifying the deployment of Codex CLI. /// Note: In debug builds the temp-dir guard is disabled to ease local testing. /// /// IMPORTANT: This function modifies the PATH environment variable, so it MUST /// be called before multiple threads are spawned. pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result { + let (path_entry_guard, updated_path_env_var) = + prepare_path_entry_for_codex_aliases(std::env::var_os("PATH"))?; + set_path_env_var(updated_path_env_var); + Ok(path_entry_guard) +} + +fn prepare_path_entry_for_codex_aliases( + existing_path: Option, +) -> std::io::Result<(Arg0PathEntryGuard, OsString)> { let codex_home = find_codex_home()?; #[cfg(not(debug_assertions))] { @@ -371,27 +391,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result { - let mut path_env_var = - std::ffi::OsString::with_capacity(path.as_os_str().len() + 1 + existing_path.len()); - path_env_var.push(path); - path_env_var.push(PATH_SEPARATOR); - path_env_var.push(existing_path); - path_env_var - } - None => path.as_os_str().to_owned(), - }; - - unsafe { - std::env::set_var("PATH", updated_path_env_var); - } + let updated_path_env_var = path_env_with_entry(path, existing_path); let paths = Arg0DispatchPaths { codex_self_exe: std::env::current_exe().ok(), @@ -417,7 +417,49 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result, +) -> Option { + let path_dir = install_context + .package_layout + .as_ref() + .and_then(|package_layout| package_layout.path_dir.as_ref())?; + Some(path_env_with_entry(path_dir.as_path(), existing_path)) +} + +fn path_env_with_entry(path_entry: &Path, existing_path: Option) -> OsString { + #[cfg(unix)] + const PATH_SEPARATOR: &str = ":"; + + #[cfg(windows)] + const PATH_SEPARATOR: &str = ";"; + + let capacity = path_entry.as_os_str().len() + + existing_path + .as_ref() + .map_or(0, |existing_path| 1 + existing_path.len()); + let mut path_env_var = OsString::with_capacity(capacity); + path_env_var.push(path_entry); + if let Some(existing_path) = existing_path { + path_env_var.push(PATH_SEPARATOR); + path_env_var.push(existing_path); + } + path_env_var } fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()> { @@ -475,6 +517,11 @@ mod tests { use super::run_main_with_arg0_guard; #[cfg(unix)] use anyhow::ensure; + use codex_install_context::CodexPackageLayout; + use codex_install_context::InstallContext; + use codex_install_context::InstallMethod; + use codex_utils_absolute_path::AbsolutePathBuf; + use pretty_assertions::assert_eq; use std::fs; use std::fs::File; use std::path::Path; @@ -513,6 +560,43 @@ mod tests { Ok(()) } + #[test] + fn path_env_can_prepend_package_path_before_arg0_alias_dir() -> anyhow::Result<()> { + let temp_dir = TempDir::new()?; + let arg0_dir = temp_dir.path().join("arg0"); + let package_dir = temp_dir.path().join("package"); + let bin_dir = package_dir.join("bin"); + let path_dir = package_dir.join("codex-path"); + let existing_dir = temp_dir.path().join("existing-bin"); + fs::create_dir_all(&arg0_dir)?; + fs::create_dir_all(&bin_dir)?; + fs::create_dir_all(&path_dir)?; + fs::create_dir_all(&existing_dir)?; + let path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?; + let install_context = InstallContext { + method: InstallMethod::Other, + package_layout: Some(CodexPackageLayout { + package_dir: AbsolutePathBuf::from_absolute_path(package_dir.canonicalize()?)?, + bin_dir: AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?, + resources_dir: None, + path_dir: Some(path_dir.clone()), + }), + }; + + let package_path = super::path_env_with_package_path_dir( + &install_context, + Some(existing_dir.as_os_str().to_owned()), + ) + .expect("package path dir should update PATH"); + let updated_path = super::path_env_with_entry(&arg0_dir, Some(package_path)); + + assert_eq!( + std::env::split_paths(&updated_path).collect::>(), + vec![arg0_dir, path_dir.as_path().to_path_buf(), existing_dir,], + ); + Ok(()) + } + #[cfg(unix)] #[test] fn run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns() -> anyhow::Result<()> { diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index e016a725c4..66cf4bacea 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -1,9 +1,11 @@ +use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; use codex_network_proxy::PROXY_ACTIVE_ENV_KEY; +use codex_utils_absolute_path::AbsolutePathBuf; use tokio_util::sync::CancellationToken; use tracing::error; use uuid::Uuid; @@ -17,6 +19,8 @@ use crate::session::TurnInput; use crate::session::turn_context::TurnContext; use crate::state::TaskKind; use crate::tools::format_exec_output_str; +#[cfg(unix)] +use crate::tools::runtimes::apply_package_path_prepend; use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot; use crate::tools::runtimes::strip_managed_proxy_env; use crate::turn_timing::now_unix_timestamp_ms; @@ -131,13 +135,13 @@ pub(crate) async fn execute_user_shell_command( if exec_env_map.contains_key(PROXY_ACTIVE_ENV_KEY) { strip_managed_proxy_env(&mut exec_env_map); } - let exec_command = maybe_wrap_shell_lc_with_snapshot( + let exec_command = prepare_user_shell_exec_command( &display_command, session_shell.as_ref(), #[allow(deprecated)] &turn_context.cwd, &turn_context.shell_environment_policy.r#set, - &exec_env_map, + &mut exec_env_map, ); let call_id = Uuid::new_v4().to_string(); @@ -328,6 +332,57 @@ pub(crate) async fn execute_user_shell_command( } } +fn prepare_user_shell_exec_command( + display_command: &[String], + session_shell: &crate::shell::Shell, + cwd: &AbsolutePathBuf, + shell_environment_set: &HashMap, + exec_env_map: &mut HashMap, +) -> Vec { + #[cfg(unix)] + { + prepare_user_shell_exec_command_with_path_prepend( + display_command, + session_shell, + cwd, + shell_environment_set, + exec_env_map, + apply_package_path_prepend, + ) + } + + #[cfg(not(unix))] + { + maybe_wrap_shell_lc_with_snapshot( + display_command, + session_shell, + cwd, + shell_environment_set, + exec_env_map, + ) + } +} + +#[cfg(unix)] +fn prepare_user_shell_exec_command_with_path_prepend( + display_command: &[String], + session_shell: &crate::shell::Shell, + cwd: &AbsolutePathBuf, + shell_environment_set: &HashMap, + exec_env_map: &mut HashMap, + apply_path_prepend: impl FnOnce(&mut HashMap, &mut HashMap), +) -> Vec { + let mut explicit_env_overrides = shell_environment_set.clone(); + apply_path_prepend(exec_env_map, &mut explicit_env_overrides); + maybe_wrap_shell_lc_with_snapshot( + display_command, + session_shell, + cwd, + &explicit_env_overrides, + exec_env_map, + ) +} + async fn persist_user_shell_output( session: &Session, turn_context: &TurnContext, @@ -351,3 +406,7 @@ async fn persist_user_shell_output( .inject_no_new_turn(vec![output_item], Some(turn_context)) .await; } + +#[cfg(all(test, unix))] +#[path = "user_shell_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/tasks/user_shell_tests.rs b/codex-rs/core/src/tasks/user_shell_tests.rs new file mode 100644 index 0000000000..6bee4ea6d4 --- /dev/null +++ b/codex-rs/core/src/tasks/user_shell_tests.rs @@ -0,0 +1,72 @@ +use super::*; +use crate::shell::Shell; +use crate::shell::ShellType; +use crate::shell_snapshot::ShellSnapshot; +use crate::tools::runtimes::apply_path_prepend; +use core_test_support::PathExt; +use pretty_assertions::assert_eq; +use std::path::PathBuf; +use std::process::Command; +use tokio::sync::watch; + +fn shell_with_snapshot( + shell_type: ShellType, + shell_path: &str, + snapshot_path: AbsolutePathBuf, + snapshot_cwd: AbsolutePathBuf, +) -> Shell { + let (_tx, shell_snapshot) = watch::channel(Some(Arc::new(ShellSnapshot { + path: snapshot_path, + cwd: snapshot_cwd, + }))); + Shell { + shell_type, + shell_path: PathBuf::from(shell_path), + shell_snapshot, + } +} + +#[test] +fn user_shell_snapshot_preserves_package_path_prepend() { + let dir = tempfile::tempdir().expect("create temp dir"); + let snapshot_path = dir.path().join("snapshot.sh"); + std::fs::write( + &snapshot_path, + "# Snapshot file\nexport PATH='/snapshot/bin'\n", + ) + .expect("write snapshot"); + let session_shell = shell_with_snapshot( + ShellType::Bash, + "/bin/bash", + snapshot_path.abs(), + dir.path().abs(), + ); + let command = vec![ + "/bin/bash".to_string(), + "-lc".to_string(), + "printf '%s' \"$PATH\"".to_string(), + ]; + let package_path_dir = dir.path().join("codex-path"); + let mut env = HashMap::from([("PATH".to_string(), "/worktree/bin".to_string())]); + let rewritten = prepare_user_shell_exec_command_with_path_prepend( + &command, + &session_shell, + &dir.path().abs(), + &HashMap::new(), + &mut env, + |env, explicit_env_overrides| { + apply_path_prepend(env, explicit_env_overrides, package_path_dir.as_path()); + }, + ); + let output = Command::new(&rewritten[0]) + .args(&rewritten[1..]) + .env("PATH", env.get("PATH").expect("PATH should be set")) + .output() + .expect("run rewritten command"); + + assert!(output.status.success(), "command failed: {output:?}"); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!("{}:/worktree/bin", package_path_dir.display()) + ); +} diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index b237f44c40..d4cda775c7 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -10,6 +10,8 @@ use crate::sandboxing::SandboxPermissions; use crate::shell::Shell; use crate::shell::ShellType; use crate::tools::sandboxing::ToolError; +#[cfg(unix)] +use codex_install_context::InstallContext; #[cfg(target_os = "macos")] use codex_network_proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER; use codex_network_proxy::CUSTOM_CA_ENV_KEYS; @@ -99,6 +101,35 @@ fn prepend_path_entry(env: &mut HashMap, path_entry: &str) -> St updated_path } +#[cfg(unix)] +pub(crate) fn apply_package_path_prepend( + env: &mut HashMap, + explicit_env_overrides: &mut HashMap, +) { + let Some(path_dir) = InstallContext::current() + .package_layout + .as_ref() + .and_then(|package_layout| package_layout.path_dir.as_ref()) + else { + return; + }; + + apply_path_prepend(env, explicit_env_overrides, path_dir.as_path()); +} + +#[cfg(unix)] +pub(crate) fn apply_path_prepend( + env: &mut HashMap, + explicit_env_overrides: &mut HashMap, + path_entry: &Path, +) { + let path_entry = path_entry.to_string_lossy(); + let updated_path = prepend_path_entry(env, path_entry.as_ref()); + // Snapshot wrapping restores explicit overrides after sourcing the shell + // snapshot, so capture this PATH override there as well. + explicit_env_overrides.insert("PATH".to_string(), updated_path); +} + #[cfg(unix)] pub(crate) fn prepend_zsh_fork_bin_to_path( env: &mut HashMap, @@ -116,12 +147,10 @@ pub(crate) fn apply_zsh_fork_path_prepend( explicit_env_overrides: &mut HashMap, shell_zsh_path: &Path, ) { - let Some(updated_path) = prepend_zsh_fork_bin_to_path(env, shell_zsh_path) else { + let Some(zsh_bin_dir) = shell_zsh_path.parent() else { return; }; - // Snapshot wrapping restores explicit overrides after sourcing the shell - // snapshot, so capture this PATH override there as well. - explicit_env_overrides.insert("PATH".to_string(), updated_path); + apply_path_prepend(env, explicit_env_overrides, zsh_bin_dir); } pub(crate) fn disable_powershell_profile_for_elevated_windows_sandbox( diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 369f301cd7..688bd03e67 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -166,6 +166,26 @@ fn explicit_escalation_preserves_user_ca_env() { ); } +#[cfg(unix)] +#[test] +fn apply_path_prepend_records_explicit_path_override() { + let mut env = HashMap::from([("PATH".to_string(), "/usr/bin:/bin".to_string())]); + let mut explicit_env_overrides = HashMap::new(); + + apply_path_prepend( + &mut env, + &mut explicit_env_overrides, + PathBuf::from("/package/codex-path").as_path(), + ); + + let expected = "/package/codex-path:/usr/bin:/bin"; + assert_eq!(env.get("PATH").map(String::as_str), Some(expected)); + assert_eq!( + explicit_env_overrides.get("PATH").map(String::as_str), + Some(expected) + ); +} + #[cfg(unix)] #[test] fn apply_zsh_fork_path_prepend_uses_shell_parent() { @@ -883,6 +903,55 @@ fn maybe_wrap_shell_lc_with_snapshot_applies_explicit_path_override() { assert_eq!(String::from_utf8_lossy(&output.stdout), "/worktree/bin"); } +#[cfg(unix)] +#[test] +fn maybe_wrap_shell_lc_with_snapshot_preserves_package_path_prepend() { + let dir = tempdir().expect("create temp dir"); + let snapshot_path = dir.path().join("snapshot.sh"); + std::fs::write( + &snapshot_path, + "# Snapshot file\nexport PATH='/snapshot/bin'\n", + ) + .expect("write snapshot"); + let session_shell = shell_with_snapshot( + ShellType::Bash, + "/bin/bash", + snapshot_path.abs(), + dir.path().abs(), + ); + let command = vec![ + "/bin/bash".to_string(), + "-lc".to_string(), + "printf '%s' \"$PATH\"".to_string(), + ]; + let package_path_dir = dir.path().join("codex-path"); + let mut env = HashMap::from([("PATH".to_string(), "/worktree/bin".to_string())]); + let mut explicit_env_overrides = HashMap::new(); + apply_path_prepend( + &mut env, + &mut explicit_env_overrides, + package_path_dir.as_path(), + ); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + &dir.path().abs(), + &explicit_env_overrides, + &env, + ); + let output = Command::new(&rewritten[0]) + .args(&rewritten[1..]) + .env("PATH", env.get("PATH").expect("PATH should be set")) + .output() + .expect("run rewritten command"); + + assert!(output.status.success(), "command failed: {output:?}"); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!("{}:/worktree/bin", package_path_dir.display()) + ); +} + #[cfg(unix)] #[test] fn maybe_wrap_shell_lc_with_snapshot_preserves_zsh_fork_path_prepend() { diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 0380f1840d..cabd0dd6bd 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -21,6 +21,8 @@ use crate::tools::flat_tool_name; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::NetworkApprovalSpec; #[cfg(unix)] +use crate::tools::runtimes::apply_package_path_prepend; +#[cfg(unix)] use crate::tools::runtimes::apply_zsh_fork_path_prepend; use crate::tools::runtimes::build_sandbox_command; use crate::tools::runtimes::disable_powershell_profile_for_elevated_windows_sandbox; @@ -252,6 +254,7 @@ impl ToolRuntime for ShellRuntime { let (env, explicit_env_overrides) = { let mut env = env; let mut explicit_env_overrides = explicit_env_overrides; + apply_package_path_prepend(&mut env, &mut explicit_env_overrides); if self.backend == ShellRuntimeBackend::ShellCommandZshFork && let Some(shell_zsh_path) = ctx.session.services.shell_zsh_path.as_deref() { diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index e49c925728..f6b0dda2a0 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -18,6 +18,8 @@ use crate::tools::flat_tool_name; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::NetworkApprovalSpec; #[cfg(unix)] +use crate::tools::runtimes::apply_package_path_prepend; +#[cfg(unix)] use crate::tools::runtimes::apply_zsh_fork_path_prepend; use crate::tools::runtimes::build_sandbox_command; use crate::tools::runtimes::disable_powershell_profile_for_elevated_windows_sandbox; @@ -278,10 +280,14 @@ impl<'a> ToolRuntime for UnifiedExecRunt if let Some(network) = managed_network { network.apply_to_env(&mut env); } + let environment_is_remote = req.environment.is_remote(); let explicit_env_overrides = req.explicit_env_overrides.clone(); #[cfg(unix)] let explicit_env_overrides = { let mut explicit_env_overrides = explicit_env_overrides; + if !environment_is_remote { + apply_package_path_prepend(&mut env, &mut explicit_env_overrides); + } if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode { apply_zsh_fork_path_prepend( &mut env, @@ -291,7 +297,6 @@ impl<'a> ToolRuntime for UnifiedExecRunt } explicit_env_overrides }; - let environment_is_remote = req.environment.is_remote(); let command = if environment_is_remote { base_command.to_vec() } else { diff --git a/codex-rs/install-context/src/lib.rs b/codex-rs/install-context/src/lib.rs index d92a80cbef..63694c5d62 100644 --- a/codex-rs/install-context/src/lib.rs +++ b/codex-rs/install-context/src/lib.rs @@ -184,11 +184,14 @@ impl InstallContext { impl CodexPackageLayout { fn from_exe(exe_path: &Path) -> Option { let canonical_exe = canonical_absolute_path(exe_path)?; - let bin_dir = canonical_exe.parent()?; - if bin_dir.file_name() != Some(OsStr::new(BIN_DIRNAME)) { - return None; + let exe_dir = canonical_exe.parent()?; + match exe_dir.file_name() { + Some(name) if name == OsStr::new(BIN_DIRNAME) => Self::from_package_bin_dir(exe_dir), + Some(_) | None => None, } + } + fn from_package_bin_dir(bin_dir: AbsolutePathBuf) -> Option { let package_dir = bin_dir.parent()?; if !package_dir.join(PACKAGE_METADATA_FILENAME).is_file() { return None;