Skip to main content

codex_config/
shell_environment_policy.rs

1use codex_protocol::config_types::EnvironmentVariablePattern;
2use codex_protocol::config_types::ShellEnvironmentPolicy;
3use codex_protocol::config_types::ShellEnvironmentPolicyFilter;
4use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
5use schemars::JsonSchema;
6use serde::Deserialize;
7use serde::Serialize;
8use std::collections::BTreeMap;
9use std::collections::HashMap;
10use toml::Value as TomlValue;
11
12/// Policy for building the `env` when spawning a process via shell-like tools.
13#[derive(Serialize, Debug, Clone, PartialEq, Default, JsonSchema)]
14#[schemars(deny_unknown_fields)]
15pub struct ShellEnvironmentPolicyToml {
16    pub inherit: Option<ShellEnvironmentPolicyInherit>,
17
18    pub ignore_default_excludes: Option<bool>,
19
20    /// Legacy list of regular expressions to exclude.
21    pub exclude: Option<Vec<String>>,
22
23    pub r#set: Option<HashMap<String, String>>,
24
25    /// Legacy list of regular expressions to include.
26    pub include_only: Option<Vec<String>>,
27
28    /// Pattern actions used by the canonical table representation.
29    ///
30    /// Ordinary config keeps accepting the legacy arrays above during the
31    /// migration. Requirements will accept only this keyed form, keeping array
32    /// compatibility isolated so the legacy fields can be deprecated later.
33    /// Pattern keys merge case-insensitively across config layers, matching how
34    /// the resulting patterns match environment variable names.
35    pub filters: Option<BTreeMap<String, ShellEnvironmentPolicyFilter>>,
36
37    pub experimental_use_profile: Option<bool>,
38}
39
40#[derive(Deserialize)]
41struct ShellEnvironmentPolicyTomlRaw {
42    inherit: Option<ShellEnvironmentPolicyInherit>,
43    ignore_default_excludes: Option<bool>,
44    exclude: Option<Vec<String>>,
45    r#set: Option<HashMap<String, String>>,
46    include_only: Option<Vec<String>>,
47    filters: Option<BTreeMap<String, ShellEnvironmentPolicyFilter>>,
48    experimental_use_profile: Option<bool>,
49}
50
51#[derive(Deserialize)]
52pub(crate) struct ShellEnvironmentPolicyFilterConfigToml {
53    #[serde(
54        default,
55        rename = "shell_environment_policy",
56        deserialize_with = "deserialize_shell_environment_policy_filters"
57    )]
58    _shell_environment_policy: (),
59}
60
61/// Validates only the shell-environment filter representation in a raw config overlay.
62pub fn validate_shell_environment_policy_filter_config(
63    value: &TomlValue,
64) -> Result<(), toml::de::Error> {
65    let _: ShellEnvironmentPolicyFilterConfigToml = value.clone().try_into()?;
66    Ok(())
67}
68
69fn deserialize_shell_environment_policy_filters<'de, D>(deserializer: D) -> Result<(), D::Error>
70where
71    D: serde::Deserializer<'de>,
72{
73    let value = TomlValue::deserialize(deserializer)?;
74    let Some(policy) = value.as_table() else {
75        return Ok(());
76    };
77    let filter_fields = ["exclude", "include_only", "filters"]
78        .into_iter()
79        .filter_map(|field| {
80            policy
81                .get(field)
82                .cloned()
83                .map(|value| (field.to_string(), value))
84        })
85        .collect();
86    let _: ShellEnvironmentPolicyToml = TomlValue::Table(filter_fields)
87        .try_into()
88        .map_err(|error: toml::de::Error| serde::de::Error::custom(error.message()))?;
89    Ok(())
90}
91
92impl<'de> Deserialize<'de> for ShellEnvironmentPolicyToml {
93    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
94    where
95        D: serde::Deserializer<'de>,
96    {
97        let ShellEnvironmentPolicyTomlRaw {
98            inherit,
99            ignore_default_excludes,
100            exclude,
101            r#set,
102            include_only,
103            filters,
104            experimental_use_profile,
105        } = ShellEnvironmentPolicyTomlRaw::deserialize(deserializer)?;
106        if filters.is_some() && (exclude.is_some() || include_only.is_some()) {
107            return Err(serde::de::Error::custom(
108                "cannot mix `filters` with legacy `exclude` or `include_only`",
109            ));
110        }
111        if let Some(filters) = filters.as_ref() {
112            let mut patterns = std::collections::HashSet::new();
113            for pattern in filters.keys() {
114                if !patterns.insert(pattern.to_lowercase()) {
115                    return Err(serde::de::Error::custom(format!(
116                        "duplicate shell environment filter `{pattern}` ignoring case"
117                    )));
118                }
119            }
120        }
121        Ok(Self {
122            inherit,
123            ignore_default_excludes,
124            exclude,
125            r#set,
126            include_only,
127            filters,
128            experimental_use_profile,
129        })
130    }
131}
132
133impl From<ShellEnvironmentPolicyToml> for ShellEnvironmentPolicy {
134    fn from(toml: ShellEnvironmentPolicyToml) -> Self {
135        let inherit = toml.inherit.unwrap_or(ShellEnvironmentPolicyInherit::All);
136        let ignore_default_excludes = toml.ignore_default_excludes.unwrap_or(true);
137        let (exclude, include_only) = match toml.filters {
138            Some(filters) => filters.into_iter().fold(
139                (Vec::new(), Vec::new()),
140                |(mut exclude, mut include_only), (pattern, filter)| {
141                    match filter {
142                        ShellEnvironmentPolicyFilter::Include => include_only.push(pattern),
143                        ShellEnvironmentPolicyFilter::Exclude => exclude.push(pattern),
144                    }
145                    (exclude, include_only)
146                },
147            ),
148            None => (
149                toml.exclude.unwrap_or_default(),
150                toml.include_only.unwrap_or_default(),
151            ),
152        };
153
154        Self {
155            inherit,
156            ignore_default_excludes,
157            exclude: exclude
158                .into_iter()
159                .map(|pattern| EnvironmentVariablePattern::new_case_insensitive(&pattern))
160                .collect(),
161            r#set: toml.r#set.unwrap_or_default(),
162            include_only: include_only
163                .into_iter()
164                .map(|pattern| EnvironmentVariablePattern::new_case_insensitive(&pattern))
165                .collect(),
166            use_profile: toml.experimental_use_profile.unwrap_or(false),
167        }
168    }
169}
170
171#[cfg(test)]
172#[path = "shell_environment_policy_tests.rs"]
173mod tests;