1use crate::CONFIG_TOML_FILE;
2use crate::config_requirements::ConfigRequirements;
3use crate::config_requirements::ConfigRequirementsToml;
4use crate::format_config_layer_source;
5
6use super::fingerprint::record_origins;
7use super::fingerprint::version_for_toml;
8use super::key_aliases::normalized_with_key_aliases;
9use super::merge::merge_toml_values;
10use crate::CloudConfigBundleLoader;
11use crate::ConfigLayer;
12use crate::ConfigLayerMetadata;
13use crate::ConfigLayerSource;
14use crate::ProfileV2Name;
15use crate::shell_environment_policy::validate_shell_environment_policy_filter_config;
16use codex_utils_absolute_path::AbsolutePathBuf;
17use serde_json::Value as JsonValue;
18use std::collections::HashMap;
19use std::path::Path;
20use std::path::PathBuf;
21use toml::Value as TomlValue;
22
23#[derive(Debug, Default, Clone)]
25pub struct ConfigLoadOptions {
26 pub loader_overrides: LoaderOverrides,
27 pub strict_config: bool,
28 pub cloud_config_bundle: CloudConfigBundleLoader,
29}
30
31impl From<LoaderOverrides> for ConfigLoadOptions {
32 fn from(loader_overrides: LoaderOverrides) -> Self {
33 Self {
34 loader_overrides,
35 strict_config: false,
36 cloud_config_bundle: CloudConfigBundleLoader::default(),
37 }
38 }
39}
40
41#[derive(Debug, Default, Clone)]
43pub struct LoaderOverrides {
44 pub user_config_path: Option<AbsolutePathBuf>,
45 pub user_config_profile: Option<ProfileV2Name>,
46 pub managed_config_path: Option<PathBuf>,
47 pub system_config_path: Option<PathBuf>,
48 pub system_requirements_path: Option<PathBuf>,
49 pub ignore_managed_requirements: bool,
50 pub ignore_user_config: bool,
51 pub ignore_user_and_project_exec_policy_rules: bool,
52 #[cfg(target_os = "macos")]
54 pub managed_preferences_base64: Option<String>,
55 pub macos_managed_config_requirements_base64: Option<String>,
56}
57
58impl LoaderOverrides {
59 pub fn without_managed_config_for_tests() -> Self {
63 let base = std::env::temp_dir().join("codex-config-tests");
64 Self {
65 user_config_path: None,
66 user_config_profile: None,
67 managed_config_path: Some(base.join("managed_config.toml")),
68 system_config_path: Some(base.join("config.toml")),
69 system_requirements_path: Some(base.join("requirements.toml")),
70 ignore_managed_requirements: false,
71 ignore_user_config: false,
72 ignore_user_and_project_exec_policy_rules: false,
73 #[cfg(target_os = "macos")]
74 managed_preferences_base64: Some(String::new()),
75 macos_managed_config_requirements_base64: Some(String::new()),
76 }
77 }
78
79 pub fn with_managed_config_path_for_tests(managed_config_path: PathBuf) -> Self {
85 let system_requirements_path = managed_config_path.with_file_name("requirements.toml");
86 Self {
87 user_config_path: None,
88 user_config_profile: None,
89 managed_config_path: Some(managed_config_path),
90 system_requirements_path: Some(system_requirements_path),
91 ..Self::without_managed_config_for_tests()
92 }
93 }
94
95 pub fn user_config_path(&self, codex_home: &Path) -> std::io::Result<AbsolutePathBuf> {
96 match self.user_config_path.as_ref() {
97 Some(path) => Ok(path.clone()),
98 None => Ok(AbsolutePathBuf::resolve_path_against_base(
99 crate::CONFIG_TOML_FILE,
100 codex_home,
101 )),
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
107pub struct ConfigLayerEntry {
108 pub name: ConfigLayerSource,
109 pub config: TomlValue,
110 pub version: String,
111 pub disabled_reason: Option<String>,
112 raw_toml: Option<RawTomlLayer>,
113 hooks_config_folder_override: Option<AbsolutePathBuf>,
114}
115
116#[derive(Debug, Clone, PartialEq)]
117struct RawTomlLayer {
118 contents: String,
119 base_dir: AbsolutePathBuf,
120}
121
122impl ConfigLayerEntry {
123 pub fn new(name: ConfigLayerSource, config: TomlValue) -> Self {
124 let version = version_for_toml(&config);
125 Self {
126 name,
127 config,
128 version,
129 disabled_reason: None,
130 raw_toml: None,
131 hooks_config_folder_override: None,
132 }
133 }
134
135 pub fn new_with_raw_toml(
136 name: ConfigLayerSource,
137 config: TomlValue,
138 raw_toml: String,
139 raw_toml_base_dir: AbsolutePathBuf,
140 ) -> Self {
141 let version = version_for_toml(&config);
142 Self {
143 name,
144 config,
145 version,
146 disabled_reason: None,
147 raw_toml: Some(RawTomlLayer {
148 contents: raw_toml,
149 base_dir: raw_toml_base_dir,
150 }),
151 hooks_config_folder_override: None,
152 }
153 }
154
155 pub fn new_disabled(
156 name: ConfigLayerSource,
157 config: TomlValue,
158 disabled_reason: impl Into<String>,
159 ) -> Self {
160 let version = version_for_toml(&config);
161 Self {
162 name,
163 config,
164 version,
165 disabled_reason: Some(disabled_reason.into()),
166 raw_toml: None,
167 hooks_config_folder_override: None,
168 }
169 }
170
171 pub fn is_disabled(&self) -> bool {
172 self.disabled_reason.is_some()
173 }
174
175 pub fn raw_toml(&self) -> Option<&str> {
176 self.raw_toml
177 .as_ref()
178 .map(|raw_toml| raw_toml.contents.as_str())
179 }
180
181 pub fn raw_toml_base_dir(&self) -> Option<&AbsolutePathBuf> {
182 self.raw_toml.as_ref().map(|raw_toml| &raw_toml.base_dir)
183 }
184
185 pub(crate) fn with_hooks_config_folder_override(
186 mut self,
187 hooks_config_folder_override: Option<AbsolutePathBuf>,
188 ) -> Self {
189 self.hooks_config_folder_override = hooks_config_folder_override;
190 self
191 }
192
193 pub fn metadata(&self) -> ConfigLayerMetadata {
194 ConfigLayerMetadata {
195 name: self.name.clone(),
196 version: self.version.clone(),
197 }
198 }
199
200 pub fn as_layer(&self) -> ConfigLayer {
201 ConfigLayer {
202 name: self.name.clone(),
203 version: self.version.clone(),
204 config: serde_json::to_value(&self.config).unwrap_or(JsonValue::Null),
205 disabled_reason: self.disabled_reason.clone(),
206 }
207 }
208
209 pub fn config_folder(&self) -> Option<AbsolutePathBuf> {
211 match &self.name {
212 ConfigLayerSource::Mdm { .. } => None,
213 ConfigLayerSource::System { file } => file.parent(),
214 ConfigLayerSource::EnterpriseManaged { .. } => None,
215 ConfigLayerSource::User { file, .. } => file.parent(),
216 ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
217 ConfigLayerSource::SessionFlags => None,
218 ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None,
219 ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,
220 }
221 }
222
223 pub fn hooks_config_folder(&self) -> Option<AbsolutePathBuf> {
230 self.hooks_config_folder_override
231 .clone()
232 .or_else(|| self.config_folder())
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum ConfigLayerStackOrdering {
238 LowestPrecedenceFirst,
239 HighestPrecedenceFirst,
240}
241
242#[derive(Debug, Clone, Default, PartialEq)]
243pub struct ConfigLayerStack {
244 layers: Vec<ConfigLayerEntry>,
247
248 user_layer_index: Option<usize>,
255
256 requirements: ConfigRequirements,
259
260 requirements_toml: ConfigRequirementsToml,
264
265 ignore_user_and_project_exec_policy_rules: bool,
267
268 startup_warnings: Option<Vec<String>>,
273}
274
275impl ConfigLayerStack {
276 pub fn new(
277 layers: Vec<ConfigLayerEntry>,
278 requirements: ConfigRequirements,
279 requirements_toml: ConfigRequirementsToml,
280 ) -> std::io::Result<Self> {
281 validate_enabled_config_layers(&layers)?;
282 let user_layer_index = verify_layer_ordering(&layers)?;
283 Ok(Self {
284 layers,
285 user_layer_index,
286 requirements,
287 requirements_toml,
288 ignore_user_and_project_exec_policy_rules: false,
289 startup_warnings: None,
290 })
291 }
292
293 pub fn with_user_and_project_exec_policy_rules_ignored(
294 mut self,
295 ignore_user_and_project_exec_policy_rules: bool,
296 ) -> Self {
297 self.ignore_user_and_project_exec_policy_rules = ignore_user_and_project_exec_policy_rules;
298 self
299 }
300
301 pub fn ignore_user_and_project_exec_policy_rules(&self) -> bool {
302 self.ignore_user_and_project_exec_policy_rules
303 }
304
305 pub(crate) fn with_startup_warnings(mut self, startup_warnings: Vec<String>) -> Self {
306 self.startup_warnings = Some(startup_warnings);
307 self
308 }
309
310 pub fn startup_warnings(&self) -> Option<&[String]> {
311 self.startup_warnings.as_deref()
312 }
313
314 pub fn get_active_user_layer(&self) -> Option<&ConfigLayerEntry> {
321 self.user_layer_index
322 .and_then(|index| self.layers.get(index))
323 }
324
325 pub fn get_user_config_file(&self) -> Option<&AbsolutePathBuf> {
326 let layer = self.get_active_user_layer()?;
327 let ConfigLayerSource::User { file, .. } = &layer.name else {
328 return None;
329 };
330 Some(file)
331 }
332
333 pub fn get_user_layers(
339 &self,
340 ordering: ConfigLayerStackOrdering,
341 include_disabled: bool,
342 ) -> Vec<&ConfigLayerEntry> {
343 self.get_layers(ordering, include_disabled)
344 .into_iter()
345 .filter(|layer| matches!(layer.name, ConfigLayerSource::User { .. }))
346 .collect()
347 }
348
349 pub fn effective_user_config(&self) -> Option<TomlValue> {
354 let user_layers = self.get_user_layers(
355 ConfigLayerStackOrdering::LowestPrecedenceFirst,
356 false,
357 );
358 if user_layers.is_empty() {
359 return None;
360 }
361
362 let mut merged = TomlValue::Table(toml::map::Map::new());
363 for layer in user_layers {
364 merge_toml_values(&mut merged, &layer.config);
365 }
366 Some(merged)
367 }
368
369 pub fn requirements(&self) -> &ConfigRequirements {
370 &self.requirements
371 }
372
373 pub fn requirements_toml(&self) -> &ConfigRequirementsToml {
374 &self.requirements_toml
375 }
376
377 pub fn with_user_config(
384 &self,
385 config_toml: &AbsolutePathBuf,
386 user_config: TomlValue,
387 ) -> std::io::Result<Self> {
388 let profile = self.layers.iter().find_map(|layer| match &layer.name {
389 ConfigLayerSource::User { file, profile } if file == config_toml => profile
390 .as_deref()
391 .and_then(|profile| profile.parse::<ProfileV2Name>().ok()),
392 _ => None,
393 });
394 self.with_user_config_profile(config_toml, profile.as_ref(), user_config)
395 }
396
397 pub fn with_user_config_profile(
398 &self,
399 config_toml: &AbsolutePathBuf,
400 profile: Option<&ProfileV2Name>,
401 user_config: TomlValue,
402 ) -> std::io::Result<Self> {
403 let user_layer = ConfigLayerEntry::new(
404 ConfigLayerSource::User {
405 file: config_toml.clone(),
406 profile: profile.map(ToString::to_string),
407 },
408 user_config,
409 );
410 validate_enabled_config_layers(std::slice::from_ref(&user_layer))?;
411
412 let mut layers = self.layers.clone();
413 if let Some(index) = layers.iter().position(|layer| {
414 matches!(
415 &layer.name,
416 ConfigLayerSource::User { file, .. } if file == config_toml
417 )
418 }) {
419 layers.remove(index);
420 }
421 match layers
422 .iter()
423 .position(|layer| layer.name.precedence() > user_layer.name.precedence())
424 {
425 Some(index) => layers.insert(index, user_layer),
426 None => layers.push(user_layer),
427 }
428 let user_layer_index = layers.iter().enumerate().rev().find_map(|(index, layer)| {
429 if matches!(layer.name, ConfigLayerSource::User { .. }) {
430 Some(index)
431 } else {
432 None
433 }
434 });
435 Ok(Self {
436 layers,
437 user_layer_index,
438 requirements: self.requirements.clone(),
439 requirements_toml: self.requirements_toml.clone(),
440 ignore_user_and_project_exec_policy_rules: self
441 .ignore_user_and_project_exec_policy_rules,
442 startup_warnings: self.startup_warnings.clone(),
443 })
444 }
445
446 pub fn with_user_layer_from(&self, other: &Self) -> Self {
449 let user_layers = other
450 .layers
451 .iter()
452 .filter(|layer| matches!(layer.name, ConfigLayerSource::User { .. }))
453 .cloned()
454 .collect::<Vec<_>>();
455 let mut layers = self
456 .layers
457 .iter()
458 .filter(|layer| !matches!(layer.name, ConfigLayerSource::User { .. }))
459 .cloned()
460 .collect::<Vec<_>>();
461 for user_layer in user_layers {
462 match layers
463 .iter()
464 .position(|layer| layer.name.precedence() > user_layer.name.precedence())
465 {
466 Some(index) => layers.insert(index, user_layer),
467 None => layers.push(user_layer),
468 }
469 }
470 let user_layer_index = layers.iter().enumerate().rev().find_map(|(index, layer)| {
471 if matches!(layer.name, ConfigLayerSource::User { .. }) {
472 Some(index)
473 } else {
474 None
475 }
476 });
477 Self {
478 layers,
479 user_layer_index,
480 requirements: self.requirements.clone(),
481 requirements_toml: self.requirements_toml.clone(),
482 ignore_user_and_project_exec_policy_rules: self
483 .ignore_user_and_project_exec_policy_rules,
484 startup_warnings: self.startup_warnings.clone(),
485 }
486 }
487
488 pub fn effective_config(&self) -> TomlValue {
493 let mut merged = TomlValue::Table(toml::map::Map::new());
494 for layer in self.get_layers(
495 ConfigLayerStackOrdering::LowestPrecedenceFirst,
496 false,
497 ) {
498 merge_toml_values(&mut merged, &layer.config);
499 }
500 merged
501 }
502
503 pub fn origins(&self) -> HashMap<String, ConfigLayerMetadata> {
507 let mut origins = HashMap::new();
508 let mut path = Vec::new();
509
510 for layer in self.get_layers(
511 ConfigLayerStackOrdering::LowestPrecedenceFirst,
512 false,
513 ) {
514 let config = normalized_with_key_aliases(&layer.config, &[]);
515 record_origins(&config, &layer.metadata(), &mut path, &mut origins);
516 }
517
518 origins
519 }
520
521 pub fn layers_high_to_low(&self) -> Vec<&ConfigLayerEntry> {
525 self.get_layers(
526 ConfigLayerStackOrdering::HighestPrecedenceFirst,
527 false,
528 )
529 }
530
531 pub fn get_layers(
535 &self,
536 ordering: ConfigLayerStackOrdering,
537 include_disabled: bool,
538 ) -> Vec<&ConfigLayerEntry> {
539 let mut layers: Vec<&ConfigLayerEntry> = self
540 .layers
541 .iter()
542 .filter(|layer| include_disabled || !layer.is_disabled())
543 .collect();
544 if ordering == ConfigLayerStackOrdering::HighestPrecedenceFirst {
545 layers.reverse();
546 }
547 layers
548 }
549}
550
551pub(crate) fn validate_enabled_config_layers(layers: &[ConfigLayerEntry]) -> std::io::Result<()> {
553 for layer in layers.iter().filter(|layer| !layer.is_disabled()) {
554 validate_shell_environment_policy_filter_config(&layer.config).map_err(|error| {
555 std::io::Error::new(
556 std::io::ErrorKind::InvalidData,
557 format!(
558 "invalid shell environment policy in {}: {error}",
559 format_config_layer_source(&layer.name, CONFIG_TOML_FILE)
560 ),
561 )
562 })?;
563 }
564 Ok(())
565}
566
567fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result<Option<usize>> {
570 if !layers.iter().map(|layer| &layer.name).is_sorted() {
571 return Err(std::io::Error::new(
572 std::io::ErrorKind::InvalidData,
573 "config layers are not in correct precedence order",
574 ));
575 }
576
577 let mut user_layer_index: Option<usize> = None;
582 let mut previous_project_dot_codex_folder: Option<&AbsolutePathBuf> = None;
583 for (index, layer) in layers.iter().enumerate() {
584 if matches!(layer.name, ConfigLayerSource::User { .. }) {
585 user_layer_index = Some(index);
586 }
587
588 if let ConfigLayerSource::Project {
589 dot_codex_folder: current_project_dot_codex_folder,
590 } = &layer.name
591 {
592 if let Some(previous) = previous_project_dot_codex_folder {
593 let Some(parent) = previous.as_path().parent() else {
594 return Err(std::io::Error::new(
595 std::io::ErrorKind::InvalidData,
596 "project layer has no parent directory",
597 ));
598 };
599 if previous == current_project_dot_codex_folder
600 || !current_project_dot_codex_folder
601 .as_path()
602 .ancestors()
603 .any(|ancestor| ancestor == parent)
604 {
605 return Err(std::io::Error::new(
606 std::io::ErrorKind::InvalidData,
607 "project layers are not ordered from root to cwd",
608 ));
609 }
610 }
611 previous_project_dot_codex_folder = Some(current_project_dot_codex_folder);
612 }
613 }
614
615 Ok(user_layer_index)
616}
617
618#[cfg(test)]
619#[path = "state_tests.rs"]
620mod tests;