Compare commits

...

4 Commits

Author SHA1 Message Date
William Woodruff
1f0aff9614 Address Codex's feedback
Signed-off-by: William Woodruff <ww@openai.com>
2026-06-01 11:48:36 -04:00
William Woodruff
af65def564 Filter RUST_LOG in shell snapshots as well
Also, add tests.

Signed-off-by: William Woodruff <ww@openai.com>
2026-06-01 11:15:30 -04:00
William Woodruff
8e5c0b5ba0 Push filter up a layer
Signed-off-by: William Woodruff <ww@openai.com>
2026-06-01 11:15:30 -04:00
William Woodruff
55c6b25ea9 [codex] Don't propagate RUST_LOG to subprocesses by default
Signed-off-by: William Woodruff <ww@openai.com>
2026-06-01 11:15:30 -04:00
4 changed files with 183 additions and 9 deletions

View File

@@ -34,6 +34,7 @@ const SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(10);
const SNAPSHOT_RETENTION: Duration = Duration::from_secs(60 * 60 * 24 * 3); // 3 days retention.
const SNAPSHOT_DIR: &str = "shell_snapshots";
const EXCLUDED_EXPORT_VARS: &[&str] = &["PWD", "OLDPWD"];
const EXCLUDED_INHERITED_VARS: &[&str] = &["RUST_LOG"];
impl ShellSnapshot {
pub fn start_snapshotting(
@@ -287,6 +288,7 @@ async fn run_script_with_timeout(
// returns a ref of handler.
let mut handler = Command::new(&args[0]);
handler.args(&args[1..]);
remove_inherited_snapshot_vars(&mut handler);
handler.stdin(Stdio::null());
handler.current_dir(cwd);
#[cfg(unix)]
@@ -311,6 +313,12 @@ async fn run_script_with_timeout(
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn remove_inherited_snapshot_vars(handler: &mut Command) {
for name in EXCLUDED_INHERITED_VARS {
handler.env_remove(name);
}
}
fn excluded_exports_regex() -> String {
EXCLUDED_EXPORT_VARS.join("|")
}

View File

@@ -147,6 +147,88 @@ fn bash_snapshot_filters_invalid_exports() -> Result<()> {
Ok(())
}
#[cfg(unix)]
fn assert_snapshot_preserves_profile_rust_log(
shell: &str,
snapshot_script: String,
profile_name: &str,
) -> Result<()> {
let home = tempdir()?;
let profile_path = home.path().join(profile_name);
std::fs::write(&profile_path, "export RUST_LOG=profile\n")?;
let mut snapshot = Command::new(shell);
snapshot
.arg("-c")
.arg(snapshot_script)
.env("HOME", home.path())
.env_remove("BASH_ENV")
.env_remove("ENV")
.env("RUST_LOG", "warn")
.env("RUST_LOG_STYLE", "always");
if shell == "/bin/sh" {
snapshot.env("ENV", &profile_path);
}
for name in EXCLUDED_INHERITED_VARS {
snapshot.env_remove(name);
}
let output = snapshot.output()?;
assert!(output.status.success());
let snapshot_path = home.path().join("snapshot.sh");
std::fs::write(&snapshot_path, &output.stdout)?;
let validate = Command::new(shell)
.arg("-c")
.arg(". \"$1\"; printf '%s' \"$RUST_LOG\"")
.arg(shell)
.arg(&snapshot_path)
.env("HOME", home.path())
.env("BASH_ENV", "/dev/null")
.env_remove("ENV")
.env_remove("RUST_LOG")
.output()?;
assert!(validate.status.success());
assert_eq!(String::from_utf8_lossy(&validate.stdout), "profile");
Ok(())
}
#[cfg(unix)]
#[test]
fn bash_snapshot_preserves_profile_rust_log() -> Result<()> {
assert_snapshot_preserves_profile_rust_log("/bin/bash", bash_snapshot_script(), ".bashrc")
}
#[cfg(unix)]
#[test]
fn sh_snapshot_preserves_profile_rust_log() -> Result<()> {
assert_snapshot_preserves_profile_rust_log("/bin/sh", sh_snapshot_script(), ".profile")
}
#[cfg(target_os = "macos")]
#[test]
fn zsh_snapshot_preserves_profile_rust_log() -> Result<()> {
assert_snapshot_preserves_profile_rust_log("/bin/zsh", zsh_snapshot_script(), ".zshrc")
}
#[cfg(unix)]
#[tokio::test]
async fn snapshot_shell_does_not_inherit_rust_log() -> Result<()> {
let mut command = tokio::process::Command::new("/bin/sh");
command
.arg("-c")
.arg("printf '%s' \"${RUST_LOG-unset}\"")
.env("RUST_LOG", "warn");
remove_inherited_snapshot_vars(&mut command);
let output = command.output().await?;
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "unset");
Ok(())
}
#[cfg(unix)]
#[test]
fn bash_snapshot_preserves_multiline_exports() -> Result<()> {

View File

@@ -203,11 +203,12 @@ pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;
/// Deriving the `env` based on this policy works as follows:
/// 1. Create an initial map based on the `inherit` policy.
/// 2. If `ignore_default_excludes` is false, filter the map using the default
/// 2. Remove inherited `RUST_LOG` unless `include_only` explicitly retains it.
/// 3. If `ignore_default_excludes` is false, filter the map using the default
/// exclude pattern(s), which are: `"*KEY*"`, `"*SECRET*"`, and `"*TOKEN*"`.
/// 3. If `exclude` is not empty, filter the map using the provided patterns.
/// 4. Insert any entries from `r#set` into the map.
/// 5. If non-empty, filter the map using the `include_only` patterns.
/// 4. If `exclude` is not empty, filter the map using the provided patterns.
/// 5. Insert any entries from `r#set` into the map.
/// 6. If non-empty, filter the map using the `include_only` patterns.
#[derive(Debug, Clone, PartialEq)]
pub struct ShellEnvironmentPolicy {
/// Starting point when building the environment.

View File

@@ -76,7 +76,16 @@ where
patterns.iter().any(|pattern| pattern.matches(name))
};
// Step 2 - Apply the default exclude if not disabled.
// Step 2 - Harnesses like Codex Desktop and the VS Code extension spawn the
// app-server with `RUST_LOG=warn` by default. Do not pass that inherited
// value to subprocesses unless the user explicitly includes it.
let include_inherited_rust_log =
!policy.include_only.is_empty() && matches_any("RUST_LOG", &policy.include_only);
if !include_inherited_rust_log {
env_map.retain(|key, _| !is_rust_log(key));
}
// Step 3 - Apply the default exclude if not disabled.
if !policy.ignore_default_excludes {
let default_excludes = vec![
EnvironmentVariablePattern::new_case_insensitive("*KEY*"),
@@ -86,22 +95,22 @@ where
env_map.retain(|k, _| !matches_any(k, &default_excludes));
}
// Step 3 - Apply custom excludes.
// Step 4 - Apply custom excludes.
if !policy.exclude.is_empty() {
env_map.retain(|k, _| !matches_any(k, &policy.exclude));
}
// Step 4 - Apply user-provided overrides.
// Step 5 - Apply user-provided overrides.
for (key, val) in &policy.r#set {
env_map.insert(key.clone(), val.clone());
}
// Step 5 - If include_only is non-empty, keep only the matching vars.
// Step 6 - If include_only is non-empty, keep only the matching vars.
if !policy.include_only.is_empty() {
env_map.retain(|k, _| matches_any(k, &policy.include_only));
}
// Step 6 - Populate the thread ID environment variable when provided.
// Step 7 - Populate the thread ID environment variable when provided.
if let Some(thread_id) = thread_id {
env_map.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.to_string());
}
@@ -109,6 +118,14 @@ where
env_map
}
fn is_rust_log(name: &str) -> bool {
if cfg!(target_os = "windows") {
name.eq_ignore_ascii_case("RUST_LOG")
} else {
name == "RUST_LOG"
}
}
#[cfg(not(target_os = "windows"))]
const UNIX_CORE_ENV_VARS: &[&str] = &[
"PATH", "SHELL", "TMPDIR", "TEMP", "TMP", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME",
@@ -148,6 +165,59 @@ pub const WINDOWS_CORE_ENV_VARS: &[&str] = &[
"PWSH",
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inherited_rust_log_is_removed() {
let vars = vec![
("PATH".to_string(), "/usr/bin".to_string()),
("RUST_LOG".to_string(), "warn".to_string()),
];
let result = populate_env(
vars,
&ShellEnvironmentPolicy::default(),
/*thread_id*/ None,
);
assert_eq!(
result,
HashMap::from([("PATH".to_string(), "/usr/bin".to_string())])
);
}
#[test]
fn explicitly_included_rust_log_is_preserved() {
let vars = vec![("RUST_LOG".to_string(), "codex_core=trace".to_string())];
let policy = ShellEnvironmentPolicy {
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("RUST_LOG")],
..Default::default()
};
let result = populate_env(vars.clone(), &policy, /*thread_id*/ None);
assert_eq!(result, vars.into_iter().collect());
}
#[test]
fn explicit_rust_log_override_is_preserved() {
let vars = vec![("RUST_LOG".to_string(), "warn".to_string())];
let policy = ShellEnvironmentPolicy {
r#set: HashMap::from([("RUST_LOG".to_string(), "codex_core=trace".to_string())]),
..Default::default()
};
let result = populate_env(vars, &policy, /*thread_id*/ None);
assert_eq!(
result,
HashMap::from([("RUST_LOG".to_string(), "codex_core=trace".to_string())])
);
}
}
#[cfg(all(test, target_os = "windows"))]
mod windows_tests {
use super::*;
@@ -247,4 +317,17 @@ mod non_windows_tests {
assert_eq!(result, expected);
}
#[test]
fn similarly_named_unix_env_vars_are_preserved() {
let vars = make_vars(&[("rust_log", "warn"), ("Rust_Log", "debug")]);
let result = populate_env(
vars.clone(),
&ShellEnvironmentPolicy::default(),
/*thread_id*/ None,
);
assert_eq!(result, vars.into_iter().collect());
}
}