codex_config/requirements_layers/
layer.rs1use 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 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}