agcodex_core/
exec_env.rs

1use crate::config_types::EnvironmentVariablePattern;
2use crate::config_types::ShellEnvironmentPolicy;
3use crate::config_types::ShellEnvironmentPolicyInherit;
4use std::collections::HashMap;
5use std::collections::HashSet;
6
7/// Construct an environment map based on the rules in the specified policy. The
8/// resulting map can be passed directly to `Command::envs()` after calling
9/// `env_clear()` to ensure no unintended variables are leaked to the spawned
10/// process.
11///
12/// The derivation follows the algorithm documented in the struct-level comment
13/// for [`ShellEnvironmentPolicy`].
14pub fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap<String, String> {
15    populate_env(std::env::vars(), policy)
16}
17
18fn populate_env<I>(vars: I, policy: &ShellEnvironmentPolicy) -> HashMap<String, String>
19where
20    I: IntoIterator<Item = (String, String)>,
21{
22    // Step 1 – determine the starting set of variables based on the
23    // `inherit` strategy.
24    let mut env_map: HashMap<String, String> = match policy.inherit {
25        ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
26        ShellEnvironmentPolicyInherit::None => HashMap::new(),
27        ShellEnvironmentPolicyInherit::Core => {
28            const CORE_VARS: &[&str] = &[
29                "HOME", "LOGNAME", "PATH", "SHELL", "USER", "USERNAME", "TMPDIR", "TEMP", "TMP",
30            ];
31            let allow: HashSet<&str> = CORE_VARS.iter().copied().collect();
32            vars.into_iter()
33                .filter(|(k, _)| allow.contains(k.as_str()))
34                .collect()
35        }
36    };
37
38    // Internal helper – does `name` match **any** pattern in `patterns`?
39    let matches_any = |name: &str, patterns: &[EnvironmentVariablePattern]| -> bool {
40        patterns.iter().any(|pattern| pattern.matches(name))
41    };
42
43    // Step 2 – Apply the default exclude if not disabled.
44    if !policy.ignore_default_excludes {
45        let default_excludes = vec![
46            EnvironmentVariablePattern::new_case_insensitive("*KEY*"),
47            EnvironmentVariablePattern::new_case_insensitive("*SECRET*"),
48            EnvironmentVariablePattern::new_case_insensitive("*TOKEN*"),
49        ];
50        env_map.retain(|k, _| !matches_any(k, &default_excludes));
51    }
52
53    // Step 3 – Apply custom excludes.
54    if !policy.exclude.is_empty() {
55        env_map.retain(|k, _| !matches_any(k, &policy.exclude));
56    }
57
58    // Step 4 – Apply user-provided overrides.
59    for (key, val) in &policy.r#set {
60        env_map.insert(key.clone(), val.clone());
61    }
62
63    // Step 5 – If include_only is non-empty, keep *only* the matching vars.
64    if !policy.include_only.is_empty() {
65        env_map.retain(|k, _| matches_any(k, &policy.include_only));
66    }
67
68    env_map
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::config_types::ShellEnvironmentPolicyInherit;
75    use maplit::hashmap;
76
77    fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
78        pairs
79            .iter()
80            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
81            .collect()
82    }
83
84    #[test]
85    fn test_core_inherit_and_default_excludes() {
86        let vars = make_vars(&[
87            ("PATH", "/usr/bin"),
88            ("HOME", "/home/user"),
89            ("API_KEY", "secret"),
90            ("SECRET_TOKEN", "t"),
91        ]);
92
93        let policy = ShellEnvironmentPolicy::default(); // inherit Core, default excludes on
94        let result = populate_env(vars, &policy);
95
96        let expected: HashMap<String, String> = hashmap! {
97            "PATH".to_string() => "/usr/bin".to_string(),
98            "HOME".to_string() => "/home/user".to_string(),
99        };
100
101        assert_eq!(result, expected);
102    }
103
104    #[test]
105    fn test_include_only() {
106        let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
107
108        let policy = ShellEnvironmentPolicy {
109            // skip default excludes so nothing is removed prematurely
110            ignore_default_excludes: true,
111            include_only: vec![EnvironmentVariablePattern::new_case_insensitive("*PATH")],
112            ..Default::default()
113        };
114
115        let result = populate_env(vars, &policy);
116
117        let expected: HashMap<String, String> = hashmap! {
118            "PATH".to_string() => "/usr/bin".to_string(),
119        };
120
121        assert_eq!(result, expected);
122    }
123
124    #[test]
125    fn test_set_overrides() {
126        let vars = make_vars(&[("PATH", "/usr/bin")]);
127
128        let mut policy = ShellEnvironmentPolicy {
129            ignore_default_excludes: true,
130            ..Default::default()
131        };
132        policy.r#set.insert("NEW_VAR".to_string(), "42".to_string());
133
134        let result = populate_env(vars, &policy);
135
136        let expected: HashMap<String, String> = hashmap! {
137            "PATH".to_string() => "/usr/bin".to_string(),
138            "NEW_VAR".to_string() => "42".to_string(),
139        };
140
141        assert_eq!(result, expected);
142    }
143
144    #[test]
145    fn test_inherit_all() {
146        let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
147
148        let policy = ShellEnvironmentPolicy {
149            inherit: ShellEnvironmentPolicyInherit::All,
150            ignore_default_excludes: true, // keep everything
151            ..Default::default()
152        };
153
154        let result = populate_env(vars.clone(), &policy);
155        let expected: HashMap<String, String> = vars.into_iter().collect();
156        assert_eq!(result, expected);
157    }
158
159    #[test]
160    fn test_inherit_all_with_default_excludes() {
161        let vars = make_vars(&[("PATH", "/usr/bin"), ("API_KEY", "secret")]);
162
163        let policy = ShellEnvironmentPolicy {
164            inherit: ShellEnvironmentPolicyInherit::All,
165            ..Default::default()
166        };
167
168        let result = populate_env(vars, &policy);
169        let expected: HashMap<String, String> = hashmap! {
170            "PATH".to_string() => "/usr/bin".to_string(),
171        };
172        assert_eq!(result, expected);
173    }
174
175    #[test]
176    fn test_inherit_none() {
177        let vars = make_vars(&[("PATH", "/usr/bin"), ("HOME", "/home")]);
178
179        let mut policy = ShellEnvironmentPolicy {
180            inherit: ShellEnvironmentPolicyInherit::None,
181            ignore_default_excludes: true,
182            ..Default::default()
183        };
184        policy
185            .r#set
186            .insert("ONLY_VAR".to_string(), "yes".to_string());
187
188        let result = populate_env(vars, &policy);
189        let expected: HashMap<String, String> = hashmap! {
190            "ONLY_VAR".to_string() => "yes".to_string(),
191        };
192        assert_eq!(result, expected);
193    }
194}