diff --git a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs index 5572c1bf8e..1912f73812 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs @@ -432,13 +432,14 @@ pub fn main() -> Result<()> { File::from_raw_handle(h_pipe_out as _) })); - let req = match read_spawn_request(&mut pipe_read) { + let mut req = match read_spawn_request(&mut pipe_read) { Ok(v) => v, Err(err) => { let _ = send_error(&pipe_write, "spawn_failed", err.to_string()); return Err(err); } }; + codex_windows_sandbox::overlay_profile_env_from_current_process(&mut req.env); let ipc_spawn = match spawn_ipc_process(&req) { Ok(value) => value, diff --git a/codex-rs/windows-sandbox-rs/src/env.rs b/codex-rs/windows-sandbox-rs/src/env.rs index c3f85cfffd..e7c1178eb2 100644 --- a/codex-rs/windows-sandbox-rs/src/env.rs +++ b/codex-rs/windows-sandbox-rs/src/env.rs @@ -42,6 +42,68 @@ pub fn inherit_path_env(env_map: &mut HashMap) { } } +fn merge_delimited_env_value(current: &str, existing: Option<&str>, delimiter: char) -> String { + let mut merged: Vec = Vec::new(); + let mut seen: Vec = Vec::new(); + for source in [Some(current), existing] { + let Some(source) = source else { + continue; + }; + for entry in source.split(delimiter) { + let trimmed = entry.trim(); + if trimmed.is_empty() { + continue; + } + let key = trimmed.to_ascii_lowercase(); + if seen.iter().any(|value| value == &key) { + continue; + } + seen.push(key); + merged.push(trimmed.to_string()); + } + } + merged.join(&delimiter.to_string()) +} + +fn overlay_profile_env_from_lookup(env_map: &mut HashMap, lookup: F) +where + F: Fn(&str) -> Option, +{ + for key in [ + "USERNAME", + "USERPROFILE", + "HOMEDRIVE", + "HOMEPATH", + "APPDATA", + "LOCALAPPDATA", + "TEMP", + "TMP", + ] { + if let Some(value) = lookup(key) { + env_map.insert(key.to_string(), value); + } + } + if let Some(current_path) = lookup("PATH") { + let merged = merge_delimited_env_value(¤t_path, env_map.get("PATH").map(String::as_str), ';'); + env_map.insert("PATH".to_string(), merged); + } + if let Some(current_pathext) = lookup("PATHEXT") { + let merged = merge_delimited_env_value( + ¤t_pathext, + env_map.get("PATHEXT").map(String::as_str), + ';', + ); + env_map.insert("PATHEXT".to_string(), merged); + } + if let Some(home) = lookup("HOME").or_else(|| lookup("USERPROFILE")) { + env_map.insert("HOME".to_string(), home); + } +} + +pub fn overlay_profile_env_from_current_process(env_map: &mut HashMap) { + overlay_profile_env_from_lookup(env_map, |key| env::var(key).ok()); +} + fn prepend_path(env_map: &mut HashMap, prefix: &str) { let existing = env_map .get("PATH") @@ -172,3 +234,54 @@ 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::overlay_profile_env_from_lookup; + use std::collections::HashMap; + + #[test] + fn overlay_profile_env_prefers_current_user_profile_values() { + let mut env_map = HashMap::from([ + ("USERNAME".to_string(), "Eliot".to_string()), + ("USERPROFILE".to_string(), r"C:\Users\Eliot".to_string()), + ("APPDATA".to_string(), r"C:\Users\Eliot\AppData\Roaming".to_string()), + ("LOCALAPPDATA".to_string(), r"C:\Users\Eliot\AppData\Local".to_string()), + ("PATH".to_string(), r"C:\Tools;C:\Users\Eliot\AppData\Roaming\fnm".to_string()), + ("PATHEXT".to_string(), ".EXE;.CMD".to_string()), + ]); + + let current = HashMap::from([ + ("USERNAME", "CodexSandboxOnline"), + ("USERPROFILE", r"C:\Users\CodexSandboxOnline"), + ( + "APPDATA", + r"C:\Users\CodexSandboxOnline\AppData\Roaming", + ), + ( + "LOCALAPPDATA", + r"C:\Users\CodexSandboxOnline\AppData\Local", + ), + ( + "PATH", + r"C:\Users\CodexSandboxOnline\AppData\Roaming\fnm;C:\Tools", + ), + ("PATHEXT", ".COM;.EXE"), + ]); + + overlay_profile_env_from_lookup(&mut env_map, |key| current.get(key).map(|value| value.to_string())); + + assert_eq!(env_map.get("USERNAME").unwrap(), "CodexSandboxOnline"); + assert_eq!(env_map.get("USERPROFILE").unwrap(), r"C:\Users\CodexSandboxOnline"); + assert_eq!( + env_map.get("APPDATA").unwrap(), + r"C:\Users\CodexSandboxOnline\AppData\Roaming" + ); + assert_eq!( + env_map.get("PATH").unwrap(), + r"C:\Users\CodexSandboxOnline\AppData\Roaming\fnm;C:\Tools;C:\Users\Eliot\AppData\Roaming\fnm" + ); + assert_eq!(env_map.get("PATHEXT").unwrap(), ".COM;.EXE;.CMD"); + assert_eq!(env_map.get("HOME").unwrap(), r"C:\Users\CodexSandboxOnline"); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index b0de8c2e02..e7aab15daa 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -78,6 +78,8 @@ pub use elevated_impl::ElevatedSandboxCaptureRequest; #[cfg(target_os = "windows")] pub use elevated_impl::run_windows_sandbox_capture as run_windows_sandbox_capture_elevated; #[cfg(target_os = "windows")] +pub use env::overlay_profile_env_from_current_process; +#[cfg(target_os = "windows")] pub use helper_materialization::resolve_current_exe_for_launch; #[cfg(target_os = "windows")] pub use hide_users::hide_current_user_profile_dir;