mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Simplify session env file exports
This commit is contained in:
@@ -40,6 +40,7 @@ use serde_json::Value;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::HookAdditionalContext;
|
||||
use crate::event_mapping::parse_turn_item;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
@@ -124,6 +125,7 @@ pub(crate) async fn run_pending_session_start_hooks(
|
||||
source: session_start_source,
|
||||
},
|
||||
};
|
||||
let captures_shell_env = matches!(target, StartHookTarget::SessionStart { .. });
|
||||
let request = codex_hooks::SessionStartRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
#[allow(deprecated)]
|
||||
@@ -135,15 +137,30 @@ pub(crate) async fn run_pending_session_start_hooks(
|
||||
};
|
||||
let hooks = sess.hooks();
|
||||
let preview_runs = hooks.preview_session_start(&request);
|
||||
if run_context_injecting_hook(
|
||||
sess,
|
||||
turn_context,
|
||||
preview_runs,
|
||||
hooks.run_session_start(request, Some(turn_context.sub_id.clone())),
|
||||
)
|
||||
.await
|
||||
.record_additional_contexts(sess, turn_context)
|
||||
.await
|
||||
emit_hook_started_events(sess, turn_context, preview_runs).await;
|
||||
let outcome = hooks
|
||||
.run_session_start(request, Some(turn_context.sub_id.clone()))
|
||||
.await;
|
||||
if captures_shell_env {
|
||||
let base_env = create_env(
|
||||
&turn_context.shell_environment_policy,
|
||||
Some(sess.conversation_id),
|
||||
);
|
||||
let cwd = turn_context
|
||||
.environments
|
||||
.primary()
|
||||
.map(|environment| environment.cwd.as_path())
|
||||
.unwrap_or_else(|| turn_context.config.cwd.as_path());
|
||||
if let Err(error) = sess.capture_shell_env_exports(cwd, &base_env).await {
|
||||
tracing::warn!("failed to capture SessionStart shell environment: {error:#}");
|
||||
}
|
||||
}
|
||||
let outcome: ContextInjectingHookOutcome = outcome.into();
|
||||
emit_hook_completed_events(sess, turn_context, outcome.hook_events).await;
|
||||
if outcome
|
||||
.outcome
|
||||
.record_additional_contexts(sess, turn_context)
|
||||
.await
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3223,16 +3223,27 @@ impl Session {
|
||||
Arc::clone(&self.services.user_shell)
|
||||
}
|
||||
|
||||
pub(crate) fn shell_env_file_path(&self) -> Option<&Path> {
|
||||
self.services
|
||||
.shell_env_file
|
||||
.as_ref()
|
||||
.map(crate::shell_env_file::ShellEnvFile::path)
|
||||
pub(crate) async fn capture_shell_env_exports(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
base_env: &HashMap<String, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(shell_env_file) = self.services.shell_env_file.as_ref() {
|
||||
let shell = self.user_shell();
|
||||
shell_env_file
|
||||
.capture_exports(shell.as_ref(), cwd, base_env)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn insert_shell_env_file(&self, env: &mut HashMap<String, String>) {
|
||||
pub(crate) fn apply_shell_env_exports(
|
||||
&self,
|
||||
env: &mut HashMap<String, String>,
|
||||
explicit_env_overrides: &HashMap<String, String>,
|
||||
) {
|
||||
if let Some(shell_env_file) = self.services.shell_env_file.as_ref() {
|
||||
shell_env_file.insert_into_env(env);
|
||||
shell_env_file.apply_exports(env, explicit_env_overrides);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3317,7 +3328,7 @@ async fn build_hooks_for_config(
|
||||
let plugin_hook_load_warnings = plugin_outcome.effective_plugin_hook_warnings();
|
||||
let mut command_env = HashMap::new();
|
||||
if let Some(shell_env_file) = shell_env_file {
|
||||
shell_env_file.insert_into_env(&mut command_env);
|
||||
shell_env_file.insert_path_into_env(&mut command_env);
|
||||
}
|
||||
Hooks::new(HooksConfig {
|
||||
legacy_notify_argv: config.notify.clone(),
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::shell_environment::CODEX_ENV_FILE_ENV_VAR;
|
||||
use codex_protocol::shell_environment::CODEX_THREAD_ID_ENV_VAR;
|
||||
use tempfile::TempPath;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
|
||||
/// Session-owned script that hooks can populate with exported shell state.
|
||||
///
|
||||
/// Local shell tool commands source this file before running so lifecycle hook
|
||||
/// setup remains scoped to the active session rather than persistent config.
|
||||
/// Only lifecycle hooks receive the writable file path. After SessionStart
|
||||
/// hooks finish, Codex captures supported exported variables and passes those
|
||||
/// values to later commands without exposing the writable path.
|
||||
pub(crate) struct ShellEnvFile {
|
||||
path: TempPath,
|
||||
exports: Mutex<HashMap<String, Option<String>>>,
|
||||
}
|
||||
|
||||
impl ShellEnvFile {
|
||||
@@ -40,6 +49,7 @@ impl ShellEnvFile {
|
||||
.context("failed to create temporary shell env file")?;
|
||||
Ok(Self {
|
||||
path: file.into_temp_path(),
|
||||
exports: Mutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,12 +57,184 @@ impl ShellEnvFile {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn insert_into_env(&self, env: &mut HashMap<String, String>) {
|
||||
pub(crate) fn insert_path_into_env(&self, env: &mut HashMap<String, String>) {
|
||||
env.insert(
|
||||
CODEX_ENV_FILE_ENV_VAR.to_string(),
|
||||
self.path().to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sources the hook-writable env file once and stores the resulting
|
||||
/// environment diff.
|
||||
///
|
||||
/// The temp file remains an input channel for SessionStart hooks, but later
|
||||
/// commands receive only captured variable changes. Running both a baseline
|
||||
/// environment dump and a sourced dump lets shell syntax such as command
|
||||
/// substitution behave naturally without keeping `CODEX_ENV_FILE` available
|
||||
/// after hook execution.
|
||||
pub(crate) async fn capture_exports(
|
||||
&self,
|
||||
shell: &Shell,
|
||||
cwd: &Path,
|
||||
base_env: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if !matches!(
|
||||
shell.shell_type,
|
||||
ShellType::Zsh | ShellType::Bash | ShellType::Sh
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut capture_env = base_env.clone();
|
||||
capture_env.remove(CODEX_ENV_FILE_ENV_VAR);
|
||||
self.insert_path_into_env(&mut capture_env);
|
||||
|
||||
let baseline = capture_env_from_shell(shell, cwd, DUMP_ENV_SCRIPT, &capture_env).await?;
|
||||
let captured = capture_env_from_shell(
|
||||
shell,
|
||||
cwd,
|
||||
SOURCE_ENV_FILE_AND_DUMP_ENV_SCRIPT,
|
||||
&capture_env,
|
||||
)
|
||||
.await?;
|
||||
let exports = diff_env(&baseline, &captured);
|
||||
*self
|
||||
.exports
|
||||
.lock()
|
||||
.map_err(|_| anyhow!("shell env exports lock poisoned"))? = exports;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies captured SessionStart environment changes to a command
|
||||
/// environment without exposing the writable env-file path.
|
||||
///
|
||||
/// Explicit shell-environment policy values are layered back on top so
|
||||
/// configured overrides keep precedence, and runtime-owned values such as
|
||||
/// the Codex thread id are preserved rather than accepting hook-written
|
||||
/// replacements.
|
||||
pub(crate) fn apply_exports(
|
||||
&self,
|
||||
env: &mut HashMap<String, String>,
|
||||
explicit_env_overrides: &HashMap<String, String>,
|
||||
) {
|
||||
let thread_id = env.get(CODEX_THREAD_ID_ENV_VAR).cloned();
|
||||
let exports = self
|
||||
.exports
|
||||
.lock()
|
||||
.map(|exports| exports.clone())
|
||||
.unwrap_or_default();
|
||||
for (key, value) in exports {
|
||||
if ignored_capture_key(&key) {
|
||||
continue;
|
||||
}
|
||||
match value {
|
||||
Some(value) => {
|
||||
env.insert(key, value);
|
||||
}
|
||||
None => {
|
||||
env.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, value) in explicit_env_overrides {
|
||||
env.insert(key.clone(), value.clone());
|
||||
}
|
||||
if let Some(thread_id) = thread_id {
|
||||
env.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id);
|
||||
}
|
||||
env.remove(CODEX_ENV_FILE_ENV_VAR);
|
||||
}
|
||||
}
|
||||
|
||||
const DUMP_ENV_SCRIPT: &str = r#"if [ -x /usr/bin/env ]; then
|
||||
/usr/bin/env -0
|
||||
else
|
||||
env -0
|
||||
fi"#;
|
||||
|
||||
const SOURCE_ENV_FILE_AND_DUMP_ENV_SCRIPT: &str = r#"if [ -n "${CODEX_ENV_FILE:-}" ] && [ -f "$CODEX_ENV_FILE" ]; then
|
||||
if . "$CODEX_ENV_FILE" >/dev/null 2>&1; then
|
||||
:
|
||||
fi
|
||||
fi
|
||||
if [ -x /usr/bin/env ]; then
|
||||
/usr/bin/env -0
|
||||
else
|
||||
env -0
|
||||
fi"#;
|
||||
|
||||
async fn capture_env_from_shell(
|
||||
shell: &Shell,
|
||||
cwd: &Path,
|
||||
script: &str,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<HashMap<String, String>> {
|
||||
let output = Command::new(&shell.shell_path)
|
||||
.current_dir(cwd)
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.env_clear()
|
||||
.envs(env)
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("failed to run {}", shell.shell_path.display()))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!(
|
||||
"failed to capture shell environment with {}: {stderr}",
|
||||
shell.shell_path.display()
|
||||
);
|
||||
}
|
||||
parse_env_output(&output.stdout)
|
||||
}
|
||||
|
||||
fn parse_env_output(output: &[u8]) -> Result<HashMap<String, String>> {
|
||||
let mut env = HashMap::new();
|
||||
for entry in output.split(|byte| *byte == 0) {
|
||||
if entry.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some(separator) = entry.iter().position(|byte| *byte == b'=') else {
|
||||
continue;
|
||||
};
|
||||
let key = String::from_utf8(entry[..separator].to_vec())
|
||||
.context("captured shell environment key was not UTF-8")?;
|
||||
let value = String::from_utf8(entry[separator + 1..].to_vec())
|
||||
.context("captured shell environment value was not UTF-8")?;
|
||||
env.insert(key, value);
|
||||
}
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
fn diff_env(
|
||||
baseline: &HashMap<String, String>,
|
||||
captured: &HashMap<String, String>,
|
||||
) -> HashMap<String, Option<String>> {
|
||||
let mut exports = HashMap::new();
|
||||
for (key, value) in captured {
|
||||
if ignored_capture_key(key) {
|
||||
continue;
|
||||
}
|
||||
if baseline.get(key) != Some(value) {
|
||||
exports.insert(key.clone(), Some(value.clone()));
|
||||
}
|
||||
}
|
||||
for key in baseline.keys() {
|
||||
if ignored_capture_key(key) {
|
||||
continue;
|
||||
}
|
||||
if !captured.contains_key(key) {
|
||||
exports.insert(key.clone(), None);
|
||||
}
|
||||
}
|
||||
exports
|
||||
}
|
||||
|
||||
fn ignored_capture_key(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
CODEX_ENV_FILE_ENV_VAR | CODEX_THREAD_ID_ENV_VAR | "PWD" | "OLDPWD" | "SHLVL" | "_"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(windows)))]
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use codex_protocol::ThreadId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
use crate::shell::empty_shell_snapshot_receiver;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
@@ -15,3 +21,103 @@ fn shell_env_file_is_removed_when_session_owner_drops() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[tokio::test]
|
||||
async fn shell_env_file_applies_exports_without_exposing_writable_path() -> Result<()> {
|
||||
let env_file = ShellEnvFile::new(ThreadId::new())?;
|
||||
let base_env = HashMap::from([
|
||||
("PATH".to_string(), "/usr/bin".to_string()),
|
||||
(
|
||||
CODEX_THREAD_ID_ENV_VAR.to_string(),
|
||||
"real-thread".to_string(),
|
||||
),
|
||||
]);
|
||||
std::fs::write(
|
||||
env_file.path(),
|
||||
"\
|
||||
export CODEX_SESSION_START_TEST='from-session-start'
|
||||
export PATH=\"/plugin/bin:$PATH\"
|
||||
export CODEX_ENV_FILE='/tmp/poison'
|
||||
export CODEX_THREAD_ID='poisoned-thread'
|
||||
export EXPLICIT_OVERRIDE='from-hook'
|
||||
",
|
||||
)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
env_file
|
||||
.capture_exports(&test_shell(), cwd.as_path(), &base_env)
|
||||
.await?;
|
||||
|
||||
let mut env = base_env;
|
||||
env.insert(
|
||||
CODEX_ENV_FILE_ENV_VAR.to_string(),
|
||||
env_file.path().display().to_string(),
|
||||
);
|
||||
let explicit_env_overrides =
|
||||
HashMap::from([("EXPLICIT_OVERRIDE".to_string(), "from-policy".to_string())]);
|
||||
|
||||
env_file.apply_exports(&mut env, &explicit_env_overrides);
|
||||
|
||||
assert_eq!(
|
||||
env,
|
||||
HashMap::from([
|
||||
("PATH".to_string(), "/plugin/bin:/usr/bin".to_string()),
|
||||
(
|
||||
"CODEX_SESSION_START_TEST".to_string(),
|
||||
"from-session-start".to_string(),
|
||||
),
|
||||
(
|
||||
CODEX_THREAD_ID_ENV_VAR.to_string(),
|
||||
"real-thread".to_string(),
|
||||
),
|
||||
("EXPLICIT_OVERRIDE".to_string(), "from-policy".to_string()),
|
||||
])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[tokio::test]
|
||||
async fn shell_env_file_sources_shell_code_once() -> Result<()> {
|
||||
let env_file = ShellEnvFile::new(ThreadId::new())?;
|
||||
std::fs::write(
|
||||
env_file.path(),
|
||||
"\
|
||||
echo hidden
|
||||
export SAFE=value
|
||||
export COMMAND_SUBSTITUTION=$(printf unsafe)
|
||||
export FUNCTION_DEF='() { echo unsafe; }'
|
||||
",
|
||||
)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
env_file
|
||||
.capture_exports(&test_shell(), cwd.as_path(), &HashMap::new())
|
||||
.await?;
|
||||
|
||||
let mut env = HashMap::new();
|
||||
env_file.apply_exports(&mut env, &HashMap::new());
|
||||
|
||||
assert_eq!(
|
||||
env,
|
||||
HashMap::from([
|
||||
("SAFE".to_string(), "value".to_string()),
|
||||
("COMMAND_SUBSTITUTION".to_string(), "unsafe".to_string()),
|
||||
(
|
||||
"FUNCTION_DEF".to_string(),
|
||||
"() { echo unsafe; }".to_string(),
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn test_shell() -> Shell {
|
||||
Shell {
|
||||
shell_type: ShellType::Sh,
|
||||
shell_path: PathBuf::from("/bin/sh"),
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,10 @@ pub(crate) async fn execute_user_shell_command(
|
||||
&turn_context.shell_environment_policy,
|
||||
Some(session.conversation_id),
|
||||
);
|
||||
session.insert_shell_env_file(&mut exec_env_map);
|
||||
session.apply_shell_env_exports(
|
||||
&mut exec_env_map,
|
||||
&turn_context.shell_environment_policy.r#set,
|
||||
);
|
||||
if exec_env_map.contains_key(PROXY_ACTIVE_ENV_KEY) {
|
||||
for key in PROXY_ENV_KEYS {
|
||||
exec_env_map.remove(*key);
|
||||
@@ -150,7 +153,6 @@ pub(crate) async fn execute_user_shell_command(
|
||||
session_shell.as_ref(),
|
||||
#[allow(deprecated)]
|
||||
&turn_context.cwd,
|
||||
session.shell_env_file_path(),
|
||||
&turn_context.shell_environment_policy.r#set,
|
||||
&exec_env_map,
|
||||
);
|
||||
|
||||
@@ -96,7 +96,7 @@ impl ShellCommandHandler {
|
||||
let cwd = turn_context.resolve_path(params.workdir.clone());
|
||||
|
||||
let mut env = create_env(&turn_context.shell_environment_policy, Some(thread_id));
|
||||
session.insert_shell_env_file(&mut env);
|
||||
session.apply_shell_env_exports(&mut env, &turn_context.shell_environment_policy.r#set);
|
||||
|
||||
Ok(ExecParams {
|
||||
command,
|
||||
|
||||
@@ -18,10 +18,6 @@ use codex_network_proxy::PROXY_ENV_KEYS;
|
||||
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::shell_environment::CODEX_ENV_FILE_ENV_VAR;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -75,29 +71,6 @@ pub(crate) fn exec_env_for_sandbox_permissions(
|
||||
env
|
||||
}
|
||||
|
||||
/// Adds the read required by a shell wrapper that sources `CODEX_ENV_FILE`.
|
||||
///
|
||||
/// Callers apply this only while preparing execution, after requested
|
||||
/// permissions have already been considered for approval.
|
||||
pub(crate) fn with_shell_env_file_read_permission(
|
||||
mut additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
env_file: Option<&Path>,
|
||||
) -> Option<AdditionalPermissionProfile> {
|
||||
let Some(path) = non_empty_shell_env_file_path(env_file) else {
|
||||
return additional_permissions;
|
||||
};
|
||||
let Ok(path) = AbsolutePathBuf::from_absolute_path(path) else {
|
||||
return additional_permissions;
|
||||
};
|
||||
let profile = additional_permissions.get_or_insert_with(Default::default);
|
||||
let file_system = profile.file_system.get_or_insert_with(Default::default);
|
||||
file_system.entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
});
|
||||
additional_permissions
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn prepend_path_entry(env: &mut HashMap<String, String>, path_entry: &str) -> String {
|
||||
let updated_path = match env.get("PATH") {
|
||||
@@ -165,33 +138,26 @@ pub(crate) fn disable_powershell_profile_for_elevated_windows_sandbox(
|
||||
command
|
||||
}
|
||||
|
||||
/// POSIX-only wrapper for commands produced by `Shell::derive_exec_args` for
|
||||
/// Bash/Zsh/sh when shell setup must be sourced before the user script:
|
||||
///
|
||||
/// shell -c/-lc "<script>"
|
||||
/// => shell -c/-lc ". ENV_FILE; <script>"
|
||||
/// POSIX-only wrapper for commands produced by `Shell::derive_exec_args` when a
|
||||
/// login-shell snapshot must be sourced before the user script:
|
||||
///
|
||||
/// shell -lc "<script>" with a matching snapshot
|
||||
/// => user_shell -c ". SNAPSHOT; . ENV_FILE; exec shell -c <script>"
|
||||
/// => user_shell -c ". SNAPSHOT; exec shell -c <script>"
|
||||
///
|
||||
/// This wrapper script uses POSIX constructs (`if`, `.`, `exec`) so it can
|
||||
/// be run by Bash/Zsh/sh. The env file is sourced for either `-c` or `-lc`
|
||||
/// commands and is independent of cwd. A snapshot remains restricted to
|
||||
/// login commands in its matching cwd. Without a snapshot, the original
|
||||
/// script stays in the original shell so login-shell-local initialization
|
||||
/// remains visible to it.
|
||||
/// be run by Bash/Zsh/sh. A snapshot remains restricted to login commands in
|
||||
/// its matching cwd.
|
||||
///
|
||||
/// `explicit_env_overrides` and `env` are intentionally separate inputs.
|
||||
/// `explicit_env_overrides` contains policy-driven shell env overrides that
|
||||
/// should win after the snapshot is sourced, while `env` is the full live exec
|
||||
/// environment. We need access to both so snapshot restore logic can preserve
|
||||
/// runtime-only vars like `CODEX_THREAD_ID` and `CODEX_ENV_FILE` without
|
||||
/// pretending they came from the explicit override policy.
|
||||
/// runtime-only vars like `CODEX_THREAD_ID` without pretending they came from
|
||||
/// the explicit override policy.
|
||||
pub(crate) fn maybe_wrap_shell_command_with_runtime_env(
|
||||
command: &[String],
|
||||
session_shell: &Shell,
|
||||
cwd: &AbsolutePathBuf,
|
||||
env_file: Option<&Path>,
|
||||
explicit_env_overrides: &HashMap<String, String>,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Vec<String> {
|
||||
@@ -205,78 +171,45 @@ pub(crate) fn maybe_wrap_shell_command_with_runtime_env(
|
||||
}
|
||||
|
||||
let flag = command[1].as_str();
|
||||
if flag != "-c" && flag != "-lc" {
|
||||
if flag != "-lc" {
|
||||
return command.to_vec();
|
||||
}
|
||||
|
||||
// Login-shell snapshots and hook-written exports have separate lifetimes
|
||||
// and eligibility rules. When both apply, restore login state first, then
|
||||
// layer the session hook exports over it.
|
||||
let snapshot = (flag == "-lc")
|
||||
.then(|| session_shell.shell_snapshot())
|
||||
.flatten()
|
||||
.filter(|snapshot| {
|
||||
snapshot.path.exists()
|
||||
&& path_utils::paths_match_after_normalization(snapshot.cwd.as_path(), cwd)
|
||||
});
|
||||
let env_file = non_empty_shell_env_file_path(env_file);
|
||||
if snapshot.is_none() && env_file.is_none() {
|
||||
let Some(snapshot) = session_shell.shell_snapshot().filter(|snapshot| {
|
||||
snapshot.path.exists()
|
||||
&& path_utils::paths_match_after_normalization(snapshot.cwd.as_path(), cwd)
|
||||
}) else {
|
||||
return command.to_vec();
|
||||
}
|
||||
};
|
||||
|
||||
let has_snapshot = snapshot.is_some();
|
||||
let mut override_env = explicit_env_overrides.clone();
|
||||
for key in [CODEX_THREAD_ID_ENV_VAR, CODEX_ENV_FILE_ENV_VAR] {
|
||||
if let Some(value) = env.get(key) {
|
||||
override_env.insert(key.to_string(), value.clone());
|
||||
}
|
||||
if let Some(thread_id) = env.get(CODEX_THREAD_ID_ENV_VAR) {
|
||||
override_env.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.clone());
|
||||
}
|
||||
let (override_captures, override_exports) = build_override_exports(&override_env);
|
||||
let (proxy_captures, proxy_exports) = build_proxy_env_exports();
|
||||
let override_captures = join_shell_blocks([override_captures, proxy_captures]);
|
||||
let override_exports = join_shell_blocks([override_exports, proxy_exports]);
|
||||
let mut source_commands = Vec::new();
|
||||
if let Some(snapshot) = snapshot {
|
||||
let path = shell_single_quote(&snapshot.path.to_string_lossy());
|
||||
source_commands.push(format!("if . '{path}' >/dev/null 2>&1; then :; fi"));
|
||||
}
|
||||
if let Some(env_file) = env_file {
|
||||
let path = shell_single_quote(&env_file.to_string_lossy());
|
||||
source_commands.push(format!("if . '{path}' >/dev/null 2>&1; then :; fi"));
|
||||
}
|
||||
let source_commands = join_shell_blocks(source_commands);
|
||||
let execution = if has_snapshot {
|
||||
let original_shell = shell_single_quote(&command[0]);
|
||||
let original_script = shell_single_quote(&command[2]);
|
||||
let trailing_args = command[3..]
|
||||
.iter()
|
||||
.map(|arg| format!(" '{}'", shell_single_quote(arg)))
|
||||
.collect::<String>();
|
||||
format!("exec '{original_shell}' -c '{original_script}'{trailing_args}")
|
||||
} else {
|
||||
command[2].clone()
|
||||
};
|
||||
let path = shell_single_quote(&snapshot.path.to_string_lossy());
|
||||
let source_commands = format!("if . '{path}' >/dev/null 2>&1; then :; fi");
|
||||
let original_shell = shell_single_quote(&command[0]);
|
||||
let original_script = shell_single_quote(&command[2]);
|
||||
let trailing_args = command[3..]
|
||||
.iter()
|
||||
.map(|arg| format!(" '{}'", shell_single_quote(arg)))
|
||||
.collect::<String>();
|
||||
let execution = format!("exec '{original_shell}' -c '{original_script}'{trailing_args}");
|
||||
let rewritten_script = if override_exports.is_empty() {
|
||||
format!("{source_commands}\n\n{execution}")
|
||||
} else {
|
||||
format!("{override_captures}\n\n{source_commands}\n\n{override_exports}\n\n{execution}")
|
||||
};
|
||||
|
||||
if has_snapshot {
|
||||
vec![
|
||||
session_shell.shell_path.to_string_lossy().to_string(),
|
||||
"-c".to_string(),
|
||||
rewritten_script,
|
||||
]
|
||||
} else {
|
||||
let mut rewritten_command = command.to_vec();
|
||||
rewritten_command[2] = rewritten_script;
|
||||
rewritten_command
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_shell_env_file_path(env_file: Option<&Path>) -> Option<&Path> {
|
||||
env_file.filter(|path| std::fs::metadata(path).is_ok_and(|metadata| metadata.len() > 0))
|
||||
vec![
|
||||
session_shell.shell_path.to_string_lossy().to_string(),
|
||||
"-c".to_string(),
|
||||
rewritten_script,
|
||||
]
|
||||
}
|
||||
|
||||
fn build_override_exports(explicit_env_overrides: &HashMap<String, String>) -> (String, String) {
|
||||
|
||||
@@ -20,17 +20,12 @@ use codex_network_proxy::PROXY_ENV_KEYS;
|
||||
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
@@ -230,7 +225,6 @@ fn maybe_wrap_shell_lc_with_snapshot_bootstraps_in_user_shell() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -242,79 +236,8 @@ fn maybe_wrap_shell_lc_with_snapshot_bootstraps_in_user_shell() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_command_sources_session_env_file_without_snapshot() {
|
||||
fn maybe_wrap_shell_command_skips_non_login_command_without_snapshot() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let env_file = dir.path().join("session-env.sh");
|
||||
std::fs::write(
|
||||
&env_file,
|
||||
"export FROM_SESSION_START='hook-value'\nexport CODEX_ENV_FILE='/overwritten'\n",
|
||||
)
|
||||
.expect("write env file");
|
||||
let session_shell = Shell {
|
||||
shell_type: ShellType::Bash,
|
||||
shell_path: PathBuf::from("/bin/bash"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-c".to_string(),
|
||||
"printf '%s|%s' \"$FROM_SESSION_START\" \"$CODEX_ENV_FILE\"".to_string(),
|
||||
];
|
||||
let env_file_value = env_file.to_string_lossy().to_string();
|
||||
let env = HashMap::from([(CODEX_ENV_FILE_ENV_VAR.to_string(), env_file_value.clone())]);
|
||||
|
||||
let rewritten = maybe_wrap_shell_command_with_runtime_env(
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
Some(env_file.as_path()),
|
||||
&HashMap::new(),
|
||||
&env,
|
||||
);
|
||||
let output = Command::new(&rewritten[0])
|
||||
.args(&rewritten[1..])
|
||||
.env(CODEX_ENV_FILE_ENV_VAR, &env_file_value)
|
||||
.output()
|
||||
.expect("run rewritten command");
|
||||
|
||||
assert!(output.status.success(), "command failed: {output:?}");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
format!("hook-value|{env_file_value}")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn populated_session_env_file_adds_runtime_read_permission() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let env_file = dir.path().join("session-env.sh");
|
||||
std::fs::write(&env_file, "export FROM_SESSION_START='hook-value'\n").expect("write env file");
|
||||
|
||||
let additional_permissions = with_shell_env_file_read_permission(
|
||||
/*additional_permissions*/ None,
|
||||
Some(env_file.as_path()),
|
||||
)
|
||||
.expect("session env file should add a permission");
|
||||
|
||||
assert_eq!(
|
||||
additional_permissions.file_system,
|
||||
Some(codex_protocol::models::FileSystemPermissions {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: env_file.abs()
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_command_skips_empty_session_env_file() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let env_file = dir.path().join("session-env.sh");
|
||||
std::fs::write(&env_file, "").expect("write env file");
|
||||
let session_shell = Shell {
|
||||
shell_type: ShellType::Bash,
|
||||
shell_path: PathBuf::from("/bin/bash"),
|
||||
@@ -330,7 +253,6 @@ fn maybe_wrap_shell_command_skips_empty_session_env_file() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
Some(env_file.as_path()),
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -338,56 +260,6 @@ fn maybe_wrap_shell_command_skips_empty_session_env_file() {
|
||||
assert_eq!(rewritten, command);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn maybe_wrap_shell_command_keeps_login_initialization_before_session_env_file() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let env_file = dir.path().join("session-env.sh");
|
||||
std::fs::write(&env_file, "export FROM_SESSION_START='hook-value'\n").expect("write env file");
|
||||
let shell_path = dir.path().join("login-shell.sh");
|
||||
std::fs::write(
|
||||
&shell_path,
|
||||
"#!/bin/sh\nif [ \"$1\" = \"-lc\" ]; then\n LOGIN_ONLY='from-login'\nfi\nscript=\"$2\"\nshift 2\neval \"$script\"\n",
|
||||
)
|
||||
.expect("write login shell");
|
||||
let mut permissions = std::fs::metadata(&shell_path)
|
||||
.expect("read login shell permissions")
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
std::fs::set_permissions(&shell_path, permissions).expect("make login shell executable");
|
||||
let shell_path = shell_path.to_string_lossy().to_string();
|
||||
let session_shell = Shell {
|
||||
shell_type: ShellType::Bash,
|
||||
shell_path: PathBuf::from(&shell_path),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let command = vec![
|
||||
shell_path,
|
||||
"-lc".to_string(),
|
||||
"printf '%s|%s' \"$LOGIN_ONLY\" \"$FROM_SESSION_START\"".to_string(),
|
||||
];
|
||||
|
||||
let rewritten = maybe_wrap_shell_command_with_runtime_env(
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
Some(env_file.as_path()),
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
let output = Command::new(&rewritten[0])
|
||||
.args(&rewritten[1..])
|
||||
.output()
|
||||
.expect("run rewritten command");
|
||||
|
||||
assert!(output.status.success(), "command failed: {output:?}");
|
||||
assert_eq!(rewritten[1], "-lc");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
"from-login|hook-value"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_escapes_single_quotes() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
@@ -409,7 +281,6 @@ fn maybe_wrap_shell_lc_with_snapshot_escapes_single_quotes() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -438,7 +309,6 @@ fn maybe_wrap_shell_lc_with_snapshot_uses_bash_bootstrap_shell() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -470,7 +340,6 @@ fn maybe_wrap_shell_lc_with_snapshot_uses_sh_bootstrap_shell() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -504,7 +373,6 @@ fn maybe_wrap_shell_lc_with_snapshot_preserves_trailing_args() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -540,7 +408,6 @@ fn maybe_wrap_shell_lc_with_snapshot_skips_when_cwd_mismatch() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&command_cwd.abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -570,7 +437,6 @@ fn maybe_wrap_shell_lc_with_snapshot_accepts_dot_alias_cwd() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&command_cwd.abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -607,7 +473,6 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_explicit_override_precedence() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&explicit_env_overrides,
|
||||
&HashMap::from([("TEST_ENV_SNAPSHOT".to_string(), "worktree".to_string())]),
|
||||
);
|
||||
@@ -648,7 +513,6 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_codex_thread_id_from_env() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::from([("CODEX_THREAD_ID".to_string(), "nested-thread".to_string())]),
|
||||
);
|
||||
@@ -691,7 +555,6 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_proxy_env_from_process_env() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -749,7 +612,6 @@ fn maybe_wrap_shell_lc_with_snapshot_refreshes_codex_proxy_git_ssh_command() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -795,7 +657,6 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_custom_git_ssh_command() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -842,7 +703,6 @@ fn maybe_wrap_shell_lc_with_snapshot_clears_stale_codex_git_ssh_command_without_
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -880,7 +740,6 @@ fn maybe_wrap_shell_lc_with_snapshot_keeps_user_proxy_env_when_proxy_inactive()
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -931,7 +790,6 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_live_env_when_snapshot_proxy_activ
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::from([(
|
||||
"HTTP_PROXY".to_string(),
|
||||
@@ -977,7 +835,6 @@ fn maybe_wrap_shell_lc_with_snapshot_keeps_snapshot_path_without_override() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
);
|
||||
@@ -1015,7 +872,6 @@ fn maybe_wrap_shell_lc_with_snapshot_applies_explicit_path_override() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&explicit_env_overrides,
|
||||
&HashMap::from([("PATH".to_string(), "/worktree/bin".to_string())]),
|
||||
);
|
||||
@@ -1064,7 +920,6 @@ fn maybe_wrap_shell_lc_with_snapshot_preserves_zsh_fork_path_prepend() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&explicit_env_overrides,
|
||||
&env,
|
||||
);
|
||||
@@ -1109,7 +964,6 @@ fn maybe_wrap_shell_lc_with_snapshot_does_not_embed_override_values_in_argv() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&explicit_env_overrides,
|
||||
&HashMap::from([(
|
||||
"OPENAI_API_KEY".to_string(),
|
||||
@@ -1158,7 +1012,6 @@ fn maybe_wrap_shell_lc_with_snapshot_preserves_unset_override_variables() {
|
||||
&command,
|
||||
&session_shell,
|
||||
&dir.path().abs(),
|
||||
/*env_file*/ None,
|
||||
&explicit_env_overrides,
|
||||
&HashMap::new(),
|
||||
);
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::tools::runtimes::build_sandbox_command;
|
||||
use crate::tools::runtimes::disable_powershell_profile_for_elevated_windows_sandbox;
|
||||
use crate::tools::runtimes::exec_env_for_sandbox_permissions;
|
||||
use crate::tools::runtimes::maybe_wrap_shell_command_with_runtime_env;
|
||||
use crate::tools::runtimes::with_shell_env_file_read_permission;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
use crate::tools::sandboxing::ApprovalCtx;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
@@ -249,12 +248,10 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
||||
}
|
||||
(env, explicit_env_overrides)
|
||||
};
|
||||
let shell_env_file_path = ctx.session.shell_env_file_path();
|
||||
let command = maybe_wrap_shell_command_with_runtime_env(
|
||||
&req.command,
|
||||
session_shell.as_ref(),
|
||||
&req.cwd,
|
||||
shell_env_file_path,
|
||||
&explicit_env_overrides,
|
||||
&env,
|
||||
);
|
||||
@@ -281,10 +278,7 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
let additional_permissions = with_shell_env_file_read_permission(
|
||||
req.additional_permissions.clone(),
|
||||
shell_env_file_path,
|
||||
);
|
||||
let additional_permissions = req.additional_permissions.clone();
|
||||
let command = build_sandbox_command(&command, &req.cwd, &env, additional_permissions)?;
|
||||
let mut expiration: crate::exec::ExecExpiration = req.timeout_ms.into();
|
||||
expiration = expiration.with_cancellation(req.cancellation_token.clone());
|
||||
|
||||
@@ -17,7 +17,6 @@ use crate::shell::ShellType;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
use crate::tools::runtimes::exec_env_for_sandbox_permissions;
|
||||
use crate::tools::runtimes::prepend_zsh_fork_bin_to_path;
|
||||
use crate::tools::runtimes::with_shell_env_file_read_permission;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
@@ -120,10 +119,7 @@ pub(super) async fn try_run_zsh_fork(
|
||||
|
||||
let mut env = exec_env_for_sandbox_permissions(&req.env, req.sandbox_permissions);
|
||||
prepend_zsh_fork_bin_to_path(&mut env, shell_zsh_path);
|
||||
let additional_permissions = with_shell_env_file_read_permission(
|
||||
req.additional_permissions.clone(),
|
||||
ctx.session.shell_env_file_path(),
|
||||
);
|
||||
let additional_permissions = req.additional_permissions.clone();
|
||||
let command = build_sandbox_command(command, &req.cwd, &env, additional_permissions)?;
|
||||
let options = ExecOptions {
|
||||
expiration: req.timeout_ms.into(),
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::tools::runtimes::disable_powershell_profile_for_elevated_windows_sand
|
||||
use crate::tools::runtimes::exec_env_for_sandbox_permissions;
|
||||
use crate::tools::runtimes::maybe_wrap_shell_command_with_runtime_env;
|
||||
use crate::tools::runtimes::shell::zsh_fork_backend;
|
||||
use crate::tools::runtimes::with_shell_env_file_read_permission;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
use crate::tools::sandboxing::ApprovalCtx;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
@@ -277,18 +276,13 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
}
|
||||
explicit_env_overrides
|
||||
};
|
||||
let environment_is_remote = req.environment.is_remote();
|
||||
let shell_env_file_path = (!environment_is_remote)
|
||||
.then(|| ctx.session.shell_env_file_path())
|
||||
.flatten();
|
||||
let command = if environment_is_remote {
|
||||
let command = if req.environment.is_remote() {
|
||||
base_command.to_vec()
|
||||
} else {
|
||||
maybe_wrap_shell_command_with_runtime_env(
|
||||
base_command,
|
||||
session_shell.as_ref(),
|
||||
&req.cwd,
|
||||
shell_env_file_path,
|
||||
&explicit_env_overrides,
|
||||
&env,
|
||||
)
|
||||
@@ -304,10 +298,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
} else {
|
||||
command
|
||||
};
|
||||
let additional_permissions = with_shell_env_file_read_permission(
|
||||
req.additional_permissions.clone(),
|
||||
shell_env_file_path,
|
||||
);
|
||||
let additional_permissions = req.additional_permissions.clone();
|
||||
|
||||
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
|
||||
let command =
|
||||
|
||||
@@ -1005,7 +1005,9 @@ impl UnifiedExecProcessManager {
|
||||
context.session.conversation_id.to_string(),
|
||||
);
|
||||
if !request.environment.is_remote() {
|
||||
context.session.insert_shell_env_file(&mut env);
|
||||
context
|
||||
.session
|
||||
.apply_shell_env_exports(&mut env, &context.turn.shell_environment_policy.r#set);
|
||||
}
|
||||
let env = apply_unified_exec_env(env);
|
||||
let exec_server_env_config = ExecServerEnvConfig {
|
||||
|
||||
@@ -2717,7 +2717,7 @@ async fn assert_session_start_env_file_reaches_bash_surface(
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = format!("session-start-env-file-{}", surface.slug());
|
||||
let command = "printf '%s' \"$CODEX_SESSION_START_TEST\"";
|
||||
let command = "printf '%s|%s' \"$CODEX_SESSION_START_TEST\" \"${CODEX_ENV_FILE:+configured}\"";
|
||||
let tool_call = match surface {
|
||||
BashRewriteSurface::ExecCommand => ev_function_call(
|
||||
&call_id,
|
||||
@@ -2772,6 +2772,10 @@ async fn assert_session_start_env_file_reaches_bash_surface(
|
||||
output.contains("from-session-start"),
|
||||
"expected hook-exported value in {surface:?} output: {output}"
|
||||
);
|
||||
assert!(
|
||||
output.contains("from-session-start|"),
|
||||
"expected CODEX_ENV_FILE to stay hidden from {surface:?} command: {output}"
|
||||
);
|
||||
assert!(
|
||||
!output.contains("leaked-to-pre-tool-use"),
|
||||
"PreToolUse should not receive CODEX_ENV_FILE: {output}"
|
||||
@@ -2781,17 +2785,17 @@ async fn assert_session_start_env_file_reaches_bash_surface(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_start_env_file_is_sourced_by_shell_command() -> Result<()> {
|
||||
async fn session_start_env_file_exports_reach_shell_command() -> Result<()> {
|
||||
assert_session_start_env_file_reaches_bash_surface(BashRewriteSurface::ShellCommand).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_start_env_file_is_sourced_by_exec_command() -> Result<()> {
|
||||
async fn session_start_env_file_exports_reach_exec_command() -> Result<()> {
|
||||
assert_session_start_env_file_reaches_bash_surface(BashRewriteSurface::ExecCommand).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shell_command_exposes_codex_env_file_when_hooks_are_disabled() -> Result<()> {
|
||||
async fn shell_command_hides_codex_env_file_when_hooks_are_disabled() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_windows!(Ok(()));
|
||||
|
||||
@@ -2837,7 +2841,7 @@ async fn shell_command_exposes_codex_env_file_when_hooks_are_disabled() -> Resul
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(
|
||||
requests[1]
|
||||
!requests[1]
|
||||
.function_call_output(call_id)
|
||||
.to_string()
|
||||
.contains("configured")
|
||||
|
||||
Reference in New Issue
Block a user