Skip to main content

codex_protocol/
shell_environment.rs

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
8/// Construct a shell environment from the supplied process environment and
9/// shell-environment policy.
10pub 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        // This is a workaround to address the failures we are seeing in the
29        // following tests when run via Bazel on Windows:
30        //
31        // ```
32        // suite::shell_command::unicode_output::with_login
33        // suite::shell_command::unicode_output::without_login
34        // ```
35        //
36        // Currently, we can only reproduce these failures in CI, which makes
37        // iteration times long, so we include this quick fix for now to unblock
38        // getting the Windows Bazel build running.
39        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    // Step 1 - determine the starting set of variables based on the
55    // `inherit` strategy.
56    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    // Step 2 - Apply the default exclude if not disabled.
80    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    // Step 3 - Apply custom excludes.
90    if !policy.exclude.is_empty() {
91        env_map.retain(|k, _| !matches_any(k, &policy.exclude));
92    }
93
94    // Step 4 - Apply user-provided overrides.
95    for (key, val) in &policy.r#set {
96        env_map.insert(key.clone(), val.clone());
97    }
98
99    // Step 5 - If include_only is non-empty, keep only the matching vars.
100    if !policy.include_only.is_empty() {
101        env_map.retain(|k, _| matches_any(k, &policy.include_only));
102    }
103
104    // Step 6 - Populate the thread ID environment variable when provided.
105    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    // Core path resolution
121    "PATH",
122    "PATHEXT",
123    // Shell and system roots
124    "SHELL",
125    "COMSPEC",
126    "SYSTEMROOT",
127    "SYSTEMDRIVE",
128    // User context and profiles
129    "USERNAME",
130    "USERDOMAIN",
131    "USERPROFILE",
132    "HOMEDRIVE",
133    "HOMEPATH",
134    // Program locations
135    "PROGRAMFILES",
136    "PROGRAMFILES(X86)",
137    "PROGRAMW6432",
138    "PROGRAMDATA",
139    // App data and caches
140    "LOCALAPPDATA",
141    "APPDATA",
142    // Temp locations
143    "TEMP",
144    "TMP",
145    "TMPDIR",
146    // Common shells/pwsh hints
147    "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        // Check a few sample vars instead of the full Windows core list.
181        let result = populate_env(vars, &policy, /*thread_id*/ 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, /*thread_id*/ 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, /*thread_id*/ 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}