cli: add package path from install context

This commit is contained in:
Michael Bolin
2026-06-03 12:48:38 -07:00
parent 4417e4c193
commit 65f816cbcb
11 changed files with 390 additions and 94 deletions
+61 -2
View File
@@ -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<String, String>,
exec_env_map: &mut HashMap<String, String>,
) -> Vec<String> {
#[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<String, String>,
exec_env_map: &mut HashMap<String, String>,
apply_path_prepend: impl FnOnce(&mut HashMap<String, String>, &mut HashMap<String, String>),
) -> Vec<String> {
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;
@@ -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())
);
}
+33 -4
View File
@@ -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<String, String>, path_entry: &str) -> St
updated_path
}
#[cfg(unix)]
pub(crate) fn apply_package_path_prepend(
env: &mut HashMap<String, String>,
explicit_env_overrides: &mut HashMap<String, String>,
) {
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<String, String>,
explicit_env_overrides: &mut HashMap<String, String>,
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<String, String>,
@@ -116,12 +147,10 @@ pub(crate) fn apply_zsh_fork_path_prepend(
explicit_env_overrides: &mut HashMap<String, String>,
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(
@@ -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() {
@@ -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<ShellRequest, ExecToolCallOutput> 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()
{
@@ -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<UnifiedExecRequest, UnifiedExecProcess> 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<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
}
explicit_env_overrides
};
let environment_is_remote = req.environment.is_remote();
let command = if environment_is_remote {
base_command.to_vec()
} else {