From fb304a2d7f72fd333ebf75bc09ec89a70cd4853b Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Wed, 8 Apr 2026 10:00:50 -0700 Subject: [PATCH] Fix Windows sandbox Path env casing --- codex-rs/windows-sandbox-rs/src/env.rs | 60 +++++++++++++++++++++----- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/codex-rs/windows-sandbox-rs/src/env.rs b/codex-rs/windows-sandbox-rs/src/env.rs index 93275e1abd..e2784c86d3 100644 --- a/codex-rs/windows-sandbox-rs/src/env.rs +++ b/codex-rs/windows-sandbox-rs/src/env.rs @@ -6,6 +6,22 @@ use std::fs::{self, File}; use std::io::Write; use std::path::{Path, PathBuf}; +fn find_env_key(env_map: &HashMap, name: &str) -> Option { + env_map + .keys() + .find(|key| key.eq_ignore_ascii_case(name)) + .cloned() +} + +fn get_env_value(env_map: &HashMap, name: &str) -> Option { + find_env_key(env_map, name).and_then(|key| env_map.get(&key).cloned()) +} + +fn insert_env_value(env_map: &mut HashMap, name: &str, value: String) { + let key = find_env_key(env_map, name).unwrap_or_else(|| name.to_string()); + env_map.insert(key, value); +} + pub fn normalize_null_device_env(env_map: &mut HashMap) { let keys: Vec = env_map.keys().cloned().collect(); for k in keys { @@ -30,12 +46,12 @@ pub fn ensure_non_interactive_pager(env_map: &mut HashMap) { // Keep PATH and PATHEXT stable for callers that rely on inheriting the parent process env. pub fn inherit_path_env(env_map: &mut HashMap) { - if !env_map.contains_key("PATH") + if find_env_key(env_map, "PATH").is_none() && let Ok(path) = env::var("PATH") { env_map.insert("PATH".into(), path); } - if !env_map.contains_key("PATHEXT") + if find_env_key(env_map, "PATHEXT").is_none() && let Ok(pathext) = env::var("PATHEXT") { env_map.insert("PATHEXT".into(), pathext); @@ -43,9 +59,7 @@ pub fn inherit_path_env(env_map: &mut HashMap) { } fn prepend_path(env_map: &mut HashMap, prefix: &str) { - let existing = env_map - .get("PATH") - .cloned() + let existing = get_env_value(env_map, "PATH") .or_else(|| env::var("PATH").ok()) .unwrap_or_default(); let parts: Vec = existing.split(';').map(ToString::to_string).collect(); @@ -62,13 +76,11 @@ fn prepend_path(env_map: &mut HashMap, prefix: &str) { new_path.push(';'); new_path.push_str(&existing); } - env_map.insert("PATH".into(), new_path); + insert_env_value(env_map, "PATH", new_path); } fn reorder_pathext_for_stubs(env_map: &mut HashMap) { - let default = env_map - .get("PATHEXT") - .cloned() + let default = get_env_value(env_map, "PATHEXT") .or_else(|| env::var("PATHEXT").ok()) .unwrap_or(".COM;.EXE;.BAT;.CMD".to_string()); let exts: Vec = default @@ -96,7 +108,7 @@ fn reorder_pathext_for_stubs(env_map: &mut HashMap) { let mut combined = Vec::new(); combined.extend(front); combined.extend(rest); - env_map.insert("PATHEXT".into(), combined.join(";")); + insert_env_value(env_map, "PATHEXT", combined.join(";")); } fn ensure_denybin(tools: &[&str], denybin_dir: Option<&Path>) -> Result { @@ -172,3 +184,31 @@ pub fn apply_no_network_to_env(env_map: &mut HashMap) -> Result< reorder_pathext_for_stubs(env_map); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_helpers_preserve_windows_env_key_casing() { + let mut env_map = HashMap::from([ + ("Path".to_string(), r"C:\Users\dev\bin".to_string()), + ("PathExt".to_string(), ".EXE;.CMD;.BAT".to_string()), + ]); + + inherit_path_env(&mut env_map); + prepend_path(&mut env_map, r"C:\denybin"); + reorder_pathext_for_stubs(&mut env_map); + + assert_eq!( + env_map.get("Path"), + Some(&r"C:\denybin;C:\Users\dev\bin".to_string()) + ); + assert_eq!( + env_map.get("PathExt"), + Some(&".CMD;.BAT;.EXE".to_string()) + ); + assert!(!env_map.contains_key("PATH")); + assert!(!env_map.contains_key("PATHEXT")); + } +}