1use crate::config_types::EnvironmentVariablePattern;
2use crate::config_types::ShellEnvironmentPolicy;
3use crate::config_types::ShellEnvironmentPolicyInherit;
4use std::collections::HashMap;
5
6pub const CODEX_THREAD_ID_ENV_VAR: &str = "CODEX_THREAD_ID";
7
8pub fn create_env(
11 policy: &ShellEnvironmentPolicy,
12 thread_id: Option<&str>,
13) -> HashMap<String, String> {
14 create_env_from_vars(std::env::vars(), policy, thread_id)
15}
16
17pub fn create_env_from_vars<I>(
18 vars: I,
19 policy: &ShellEnvironmentPolicy,
20 thread_id: Option<&str>,
21) -> HashMap<String, String>
22where
23 I: IntoIterator<Item = (String, String)>,
24{
25 let mut env_map = populate_env(vars, policy, thread_id);
26
27 if cfg!(target_os = "windows") {
28 if !env_map.keys().any(|k| k.eq_ignore_ascii_case("PATHEXT")) {
40 env_map.insert("PATHEXT".to_string(), ".COM;.EXE;.BAT;.CMD".to_string());
41 }
42 }
43 env_map
44}
45
46pub fn populate_env<I>(
47 vars: I,
48 policy: &ShellEnvironmentPolicy,
49 thread_id: Option<&str>,
50) -> HashMap<String, String>
51where
52 I: IntoIterator<Item = (String, String)>,
53{
54 let mut env_map: HashMap<String, String> = match policy.inherit {
57 ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
58 ShellEnvironmentPolicyInherit::None => HashMap::new(),
59 ShellEnvironmentPolicyInherit::Core => {
60 #[cfg(not(target_os = "windows"))]
61 let core_env_vars = UNIX_CORE_ENV_VARS;
62 #[cfg(target_os = "windows")]
63 let core_env_vars = WINDOWS_CORE_ENV_VARS;
64
65 vars.into_iter()
66 .filter(|(k, _)| {
67 core_env_vars
68 .iter()
69 .any(|allowed| allowed.eq_ignore_ascii_case(k))
70 })
71 .collect()
72 }
73 };
74
75 let matches_any = |name: &str, patterns: &[EnvironmentVariablePattern]| -> bool {
76 patterns.iter().any(|pattern| pattern.matches(name))
77 };
78
79 if !policy.ignore_default_excludes {
81 let default_excludes = vec![
82 EnvironmentVariablePattern::new_case_insensitive("*KEY*"),
83 EnvironmentVariablePattern::new_case_insensitive("*SECRET*"),
84 EnvironmentVariablePattern::new_case_insensitive("*TOKEN*"),
85 ];
86 env_map.retain(|k, _| !matches_any(k, &default_excludes));
87 }
88
89 if !policy.exclude.is_empty() {
91 env_map.retain(|k, _| !matches_any(k, &policy.exclude));
92 }
93
94 for (key, val) in &policy.r#set {
96 env_map.insert(key.clone(), val.clone());
97 }
98
99 if !policy.include_only.is_empty() {
101 env_map.retain(|k, _| matches_any(k, &policy.include_only));
102 }
103
104 if let Some(thread_id) = thread_id {
106 env_map.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.to_string());
107 }
108
109 env_map
110}
111
112#[cfg(not(target_os = "windows"))]
113const UNIX_CORE_ENV_VARS: &[&str] = &[
114 "PATH", "SHELL", "TMPDIR", "TEMP", "TMP", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME",
115 "USER",
116];
117
118#[cfg(target_os = "windows")]
119pub const WINDOWS_CORE_ENV_VARS: &[&str] = &[
120 "PATH",
122 "PATHEXT",
123 "SHELL",
125 "COMSPEC",
126 "SYSTEMROOT",
127 "SYSTEMDRIVE",
128 "USERNAME",
130 "USERDOMAIN",
131 "USERPROFILE",
132 "HOMEDRIVE",
133 "HOMEPATH",
134 "PROGRAMFILES",
136 "PROGRAMFILES(X86)",
137 "PROGRAMW6432",
138 "PROGRAMDATA",
139 "LOCALAPPDATA",
141 "APPDATA",
142 "TEMP",
144 "TMP",
145 "TMPDIR",
146 "POWERSHELL",
148 "PWSH",
149];
150
151#[cfg(all(test, target_os = "windows"))]
152mod windows_tests {
153 use super::*;
154 use pretty_assertions::assert_eq;
155
156 fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
157 pairs
158 .iter()
159 .map(|(key, value)| (key.to_string(), value.to_string()))
160 .collect()
161 }
162
163 #[test]
164 #[cfg(target_os = "windows")]
165 fn core_inherit_preserves_windows_startup_vars_case_insensitively() {
166 let vars = make_vars(&[
167 ("Shell", "C:\\Program Files\\Git\\bin\\bash.exe"),
168 ("SystemRoot", "C:\\Windows"),
169 ("AppData", "C:\\Users\\codex\\AppData\\Roaming"),
170 ("TmpDir", "C:\\Temp\\custom"),
171 ("OPENAI_API_KEY", "secret"),
172 ]);
173
174 let policy = ShellEnvironmentPolicy {
175 inherit: ShellEnvironmentPolicyInherit::Core,
176 ignore_default_excludes: true,
177 ..Default::default()
178 };
179
180 let result = populate_env(vars, &policy, None);
182 let expected = HashMap::from([
183 (
184 "Shell".to_string(),
185 "C:\\Program Files\\Git\\bin\\bash.exe".to_string(),
186 ),
187 ("SystemRoot".to_string(), "C:\\Windows".to_string()),
188 (
189 "AppData".to_string(),
190 "C:\\Users\\codex\\AppData\\Roaming".to_string(),
191 ),
192 ("TmpDir".to_string(), "C:\\Temp\\custom".to_string()),
193 ]);
194
195 assert_eq!(result, expected);
196 }
197
198 #[test]
199 #[cfg(target_os = "windows")]
200 fn create_env_inserts_pathext_on_windows_when_missing() {
201 let policy = ShellEnvironmentPolicy {
202 inherit: ShellEnvironmentPolicyInherit::None,
203 ignore_default_excludes: true,
204 ..Default::default()
205 };
206
207 let result = create_env_from_vars(Vec::new(), &policy, None);
208 let expected = HashMap::from([("PATHEXT".to_string(), ".COM;.EXE;.BAT;.CMD".to_string())]);
209
210 assert_eq!(result, expected);
211 }
212}
213
214#[cfg(all(test, not(target_os = "windows")))]
215mod non_windows_tests {
216 use super::*;
217 use pretty_assertions::assert_eq;
218
219 fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
220 pairs
221 .iter()
222 .map(|(key, value)| (key.to_string(), value.to_string()))
223 .collect()
224 }
225
226 #[test]
227 fn core_inherit_preserves_non_windows_core_vars_case_insensitively() {
228 let vars = make_vars(&[
229 ("path", "/usr/bin"),
230 ("home", "/home/codex"),
231 ("TmpDir", "/tmp/custom"),
232 ("OPENAI_API_KEY", "secret"),
233 ]);
234
235 let policy = ShellEnvironmentPolicy {
236 inherit: ShellEnvironmentPolicyInherit::Core,
237 ignore_default_excludes: true,
238 ..Default::default()
239 };
240
241 let result = populate_env(vars, &policy, None);
242 let expected = HashMap::from([
243 ("path".to_string(), "/usr/bin".to_string()),
244 ("home".to_string(), "/home/codex".to_string()),
245 ("TmpDir".to_string(), "/tmp/custom".to_string()),
246 ]);
247
248 assert_eq!(result, expected);
249 }
250}