Skip to main content

codex_config/requirements_layers/
layer.rs

1use crate::ConfigRequirementsToml;
2use crate::ManagedHooksRequirementsToml;
3use crate::RequirementSource;
4use crate::RequirementsExecPolicyToml;
5use codex_utils_absolute_path::AbsolutePathBuf;
6use codex_utils_absolute_path::AbsolutePathBufGuard;
7use toml::Value as TomlValue;
8
9use super::stack::RequirementsCompositionError;
10
11#[derive(Clone, Debug)]
12pub struct RequirementsLayerEntry {
13    pub(super) source: RequirementSource,
14    toml: RequirementsLayerToml,
15    base_dir: Option<AbsolutePathBuf>,
16}
17
18impl RequirementsLayerEntry {
19    pub fn from_toml(source: RequirementSource, contents: impl Into<String>) -> Self {
20        Self {
21            source,
22            toml: RequirementsLayerToml::String(contents.into()),
23            base_dir: None,
24        }
25    }
26
27    pub fn from_toml_value(source: RequirementSource, value: TomlValue) -> Self {
28        Self {
29            source,
30            toml: RequirementsLayerToml::Value(value),
31            base_dir: None,
32        }
33    }
34
35    pub fn with_base_dir(mut self, base_dir: AbsolutePathBuf) -> Self {
36        self.base_dir = Some(base_dir);
37        self
38    }
39}
40
41#[derive(Clone, Debug)]
42enum RequirementsLayerToml {
43    String(String),
44    Value(TomlValue),
45}
46
47#[derive(Clone, Debug)]
48pub(super) struct ComposableRequirementsLayer {
49    pub(super) source: RequirementSource,
50    pub(super) regular_toml: TomlValue,
51    pub(super) domain_fields: DomainMergedRequirementsFields,
52}
53
54impl ComposableRequirementsLayer {
55    pub(super) fn from_entry(
56        layer: RequirementsLayerEntry,
57        hostname_resolver: &dyn Fn() -> Option<String>,
58    ) -> Result<Self, RequirementsCompositionError> {
59        let RequirementsLayerEntry {
60            source,
61            toml,
62            base_dir,
63        } = layer;
64        let (mut regular_toml, mut requirements) = {
65            let _guard = base_dir
66                .as_ref()
67                .map(|base_dir| AbsolutePathBufGuard::new(base_dir.as_path()));
68            let regular_toml = parse_layer_toml(&toml, &source)?;
69            let requirements = parse_layer_requirements(&toml, &source)?;
70            (regular_toml, requirements)
71        };
72
73        // Hostname lookup is configuration-driven and may block on DNS, so only
74        // resolve it when this layer contains hostname-based sandbox selectors.
75        let hostname = requirements
76            .remote_sandbox_config
77            .as_ref()
78            .and_then(|_| hostname_resolver());
79        requirements.apply_remote_sandbox_config(hostname.as_deref());
80        materialize_resolved_path_requirements(&mut regular_toml, &requirements)?;
81        materialize_remote_sandbox_config(&mut regular_toml, &requirements)?;
82        strip_special_fields(&mut regular_toml);
83
84        Ok(Self {
85            source,
86            regular_toml,
87            domain_fields: DomainMergedRequirementsFields {
88                rules: requirements.rules,
89                hooks: requirements.hooks,
90                permissions: requirements.permissions,
91            },
92        })
93    }
94}
95
96#[derive(Clone, Debug)]
97pub(super) struct DomainMergedRequirementsFields {
98    pub(super) rules: Option<RequirementsExecPolicyToml>,
99    pub(super) hooks: Option<ManagedHooksRequirementsToml>,
100    pub(super) permissions: Option<crate::config_requirements::PermissionsRequirementsToml>,
101}
102
103fn parse_layer_toml(
104    toml: &RequirementsLayerToml,
105    source: &RequirementSource,
106) -> Result<TomlValue, RequirementsCompositionError> {
107    match toml {
108        RequirementsLayerToml::String(contents) => {
109            toml::from_str(contents).map_err(|err: toml::de::Error| {
110                RequirementsCompositionError::Parse {
111                    layer_source: source.clone(),
112                    message: err.to_string(),
113                }
114            })
115        }
116        RequirementsLayerToml::Value(value) => Ok(value.clone()),
117    }
118}
119
120fn parse_layer_requirements(
121    toml: &RequirementsLayerToml,
122    source: &RequirementSource,
123) -> Result<ConfigRequirementsToml, RequirementsCompositionError> {
124    match toml {
125        RequirementsLayerToml::String(contents) => {
126            toml::from_str(contents).map_err(|err: toml::de::Error| {
127                RequirementsCompositionError::Parse {
128                    layer_source: source.clone(),
129                    message: err.to_string(),
130                }
131            })
132        }
133        RequirementsLayerToml::Value(value) => {
134            value.clone().try_into().map_err(|err: toml::de::Error| {
135                RequirementsCompositionError::Parse {
136                    layer_source: source.clone(),
137                    message: err.to_string(),
138                }
139            })
140        }
141    }
142}
143
144fn materialize_resolved_path_requirements(
145    layer_toml: &mut TomlValue,
146    requirements: &ConfigRequirementsToml,
147) -> Result<(), RequirementsCompositionError> {
148    let Some(table) = layer_toml.as_table_mut() else {
149        return Ok(());
150    };
151
152    for (key, value) in [
153        ("sqlite_home", requirements.sqlite_home.as_ref()),
154        ("log_dir", requirements.log_dir.as_ref()),
155        (
156            "model_catalog_json",
157            requirements.model_catalog_json.as_ref(),
158        ),
159    ] {
160        if let Some(value) = value {
161            table.insert(key.to_string(), toml_value_from_serializable(value)?);
162        }
163    }
164
165    Ok(())
166}
167
168fn materialize_remote_sandbox_config(
169    layer_toml: &mut TomlValue,
170    requirements: &ConfigRequirementsToml,
171) -> Result<(), RequirementsCompositionError> {
172    remove_top_level_field(layer_toml, "remote_sandbox_config");
173    let Some(allowed_sandbox_modes) = requirements.allowed_sandbox_modes.as_ref() else {
174        return Ok(());
175    };
176    let Some(table) = layer_toml.as_table_mut() else {
177        return Ok(());
178    };
179    table.insert(
180        "allowed_sandbox_modes".to_string(),
181        toml_value_from_serializable(allowed_sandbox_modes)?,
182    );
183    Ok(())
184}
185
186fn toml_value_from_serializable<T: serde::Serialize>(
187    value: T,
188) -> Result<TomlValue, RequirementsCompositionError> {
189    TomlValue::try_from(value).map_err(|err| RequirementsCompositionError::ComposedParse {
190        message: err.to_string(),
191    })
192}
193
194fn strip_special_fields(layer_toml: &mut TomlValue) {
195    remove_top_level_field(layer_toml, "rules");
196    remove_top_level_field(layer_toml, "hooks");
197    remove_nested_field_and_prune_empty(layer_toml, &["permissions", "filesystem", "deny_read"]);
198}
199
200fn remove_top_level_field(value: &mut TomlValue, key: &str) -> Option<TomlValue> {
201    value.as_table_mut()?.remove(key)
202}
203
204fn remove_nested_field_and_prune_empty(value: &mut TomlValue, path: &[&str]) -> Option<TomlValue> {
205    let (key, remaining) = path.split_first()?;
206    let table = value.as_table_mut()?;
207    if remaining.is_empty() {
208        return table.remove(*key);
209    }
210
211    let removed = table
212        .get_mut(*key)
213        .and_then(|child| remove_nested_field_and_prune_empty(child, remaining));
214    if table
215        .get(*key)
216        .and_then(TomlValue::as_table)
217        .is_some_and(toml::map::Map::is_empty)
218    {
219        table.remove(*key);
220    }
221    removed
222}