use crate::config_requirements::ConfigRequirements; use crate::config_requirements::ConfigRequirementsToml; use super::fingerprint::record_origins; use super::fingerprint::version_for_toml; use super::key_aliases::normalized_with_key_aliases; use super::merge::merge_toml_values; use codex_app_server_protocol::ConfigLayer; use codex_app_server_protocol::ConfigLayerMetadata; use codex_app_server_protocol::ConfigLayerSource; use codex_utils_absolute_path::AbsolutePathBuf; use serde_json::Value as JsonValue; use std::collections::HashMap; use std::path::PathBuf; use toml::Value as TomlValue; /// LoaderOverrides overrides managed configuration inputs (primarily for tests). #[derive(Debug, Default, Clone)] pub struct LoaderOverrides { pub managed_config_path: Option, pub ignore_user_config: bool, pub ignore_user_and_project_exec_policy_rules: bool, //TODO(gt): Add a macos_ prefix to this field and remove the target_os check. #[cfg(target_os = "macos")] pub managed_preferences_base64: Option, pub macos_managed_config_requirements_base64: Option, } impl LoaderOverrides { /// Returns overrides that ignore host-managed configuration. /// /// This is intended for tests that should load only repo-controlled config fixtures. pub fn without_managed_config_for_tests() -> Self { Self::with_managed_config_path_for_tests( std::env::temp_dir() .join("codex-config-tests") .join("managed_config.toml"), ) } /// Returns overrides with host MDM disabled and managed config loaded from `managed_config_path`. /// /// This is intended for tests that supply an explicit managed config fixture. pub fn with_managed_config_path_for_tests(managed_config_path: PathBuf) -> Self { Self { managed_config_path: Some(managed_config_path), ignore_user_config: false, ignore_user_and_project_exec_policy_rules: false, #[cfg(target_os = "macos")] managed_preferences_base64: Some(String::new()), macos_managed_config_requirements_base64: Some(String::new()), } } } #[derive(Debug, Clone, PartialEq)] pub struct ConfigLayerEntry { pub name: ConfigLayerSource, pub config: TomlValue, pub raw_toml: Option, pub version: String, pub disabled_reason: Option, } impl ConfigLayerEntry { pub fn new(name: ConfigLayerSource, config: TomlValue) -> Self { let version = version_for_toml(&config); Self { name, config, raw_toml: None, version, disabled_reason: None, } } pub fn new_with_raw_toml(name: ConfigLayerSource, config: TomlValue, raw_toml: String) -> Self { let version = version_for_toml(&config); Self { name, config, raw_toml: Some(raw_toml), version, disabled_reason: None, } } pub fn new_disabled( name: ConfigLayerSource, config: TomlValue, disabled_reason: impl Into, ) -> Self { let version = version_for_toml(&config); Self { name, config, raw_toml: None, version, disabled_reason: Some(disabled_reason.into()), } } pub fn is_disabled(&self) -> bool { self.disabled_reason.is_some() } pub fn raw_toml(&self) -> Option<&str> { self.raw_toml.as_deref() } pub fn metadata(&self) -> ConfigLayerMetadata { ConfigLayerMetadata { name: self.name.clone(), version: self.version.clone(), } } pub fn as_layer(&self) -> ConfigLayer { ConfigLayer { name: self.name.clone(), version: self.version.clone(), config: serde_json::to_value(&self.config).unwrap_or(JsonValue::Null), disabled_reason: self.disabled_reason.clone(), } } // Get the `.codex/` folder associated with this config layer, if any. pub fn config_folder(&self) -> Option { match &self.name { ConfigLayerSource::Mdm { .. } => None, ConfigLayerSource::System { file } => file.parent(), ConfigLayerSource::User { file } => file.parent(), ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()), ConfigLayerSource::SessionFlags => None, ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None, ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigLayerStackOrdering { LowestPrecedenceFirst, HighestPrecedenceFirst, } #[derive(Debug, Clone, Default, PartialEq)] pub struct ConfigLayerStack { /// Layers are listed from lowest precedence (base) to highest (top), so /// later entries in the Vec override earlier ones. layers: Vec, /// Index into [layers] of the user config layer, if any. user_layer_index: Option, /// Constraints that must be enforced when deriving a [Config] from the /// layers. requirements: ConfigRequirements, /// Raw requirements data as loaded from requirements.toml/MDM/legacy /// sources. This preserves the original allow-lists so they can be /// surfaced via APIs. requirements_toml: ConfigRequirementsToml, /// Whether execpolicy should skip `.rules` files from user and project config-layer folders. ignore_user_and_project_exec_policy_rules: bool, } impl ConfigLayerStack { pub fn new( layers: Vec, requirements: ConfigRequirements, requirements_toml: ConfigRequirementsToml, ) -> std::io::Result { let user_layer_index = verify_layer_ordering(&layers)?; Ok(Self { layers, user_layer_index, requirements, requirements_toml, ignore_user_and_project_exec_policy_rules: false, }) } pub fn with_user_and_project_exec_policy_rules_ignored( mut self, ignore_user_and_project_exec_policy_rules: bool, ) -> Self { self.ignore_user_and_project_exec_policy_rules = ignore_user_and_project_exec_policy_rules; self } pub fn ignore_user_and_project_exec_policy_rules(&self) -> bool { self.ignore_user_and_project_exec_policy_rules } /// Returns the raw user config layer, if any. /// /// This does not merge other config layers or apply any requirements. pub fn get_user_layer(&self) -> Option<&ConfigLayerEntry> { self.user_layer_index .and_then(|index| self.layers.get(index)) } pub fn requirements(&self) -> &ConfigRequirements { &self.requirements } pub fn requirements_toml(&self) -> &ConfigRequirementsToml { &self.requirements_toml } /// Creates a new [ConfigLayerStack] using the specified values to inject a /// "user layer" into the stack. If such a layer already exists, it is /// replaced; otherwise, it is inserted into the stack at the appropriate /// position based on precedence rules. pub fn with_user_config(&self, config_toml: &AbsolutePathBuf, user_config: TomlValue) -> Self { let user_layer = ConfigLayerEntry::new( ConfigLayerSource::User { file: config_toml.clone(), }, user_config, ); let mut layers = self.layers.clone(); match self.user_layer_index { Some(index) => { layers[index] = user_layer; Self { layers, user_layer_index: self.user_layer_index, requirements: self.requirements.clone(), requirements_toml: self.requirements_toml.clone(), ignore_user_and_project_exec_policy_rules: self .ignore_user_and_project_exec_policy_rules, } } None => { let user_layer_index = match layers .iter() .position(|layer| layer.name.precedence() > user_layer.name.precedence()) { Some(index) => { layers.insert(index, user_layer); index } None => { layers.push(user_layer); layers.len() - 1 } }; Self { layers, user_layer_index: Some(user_layer_index), requirements: self.requirements.clone(), requirements_toml: self.requirements_toml.clone(), ignore_user_and_project_exec_policy_rules: self .ignore_user_and_project_exec_policy_rules, } } } } /// Returns the merged config-layer view. /// /// This only merges ordinary config layers and does not apply requirements /// such as cloud requirements. pub fn effective_config(&self) -> TomlValue { let mut merged = TomlValue::Table(toml::map::Map::new()); for layer in self.get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ false, ) { merge_toml_values(&mut merged, &layer.config); } merged } /// Returns field origins for the merged config-layer view. /// /// Requirement sources are tracked separately and are not included here. pub fn origins(&self) -> HashMap { let mut origins = HashMap::new(); let mut path = Vec::new(); for layer in self.get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ false, ) { let config = normalized_with_key_aliases(&layer.config, &[]); record_origins(&config, &layer.metadata(), &mut path, &mut origins); } origins } /// Returns config layers from highest precedence to lowest precedence. /// /// Requirement sources are tracked separately and are not included here. pub fn layers_high_to_low(&self) -> Vec<&ConfigLayerEntry> { self.get_layers( ConfigLayerStackOrdering::HighestPrecedenceFirst, /*include_disabled*/ false, ) } /// Returns config layers in the requested precedence order. /// /// Requirement sources are tracked separately and are not included here. pub fn get_layers( &self, ordering: ConfigLayerStackOrdering, include_disabled: bool, ) -> Vec<&ConfigLayerEntry> { let mut layers: Vec<&ConfigLayerEntry> = self .layers .iter() .filter(|layer| include_disabled || !layer.is_disabled()) .collect(); if ordering == ConfigLayerStackOrdering::HighestPrecedenceFirst { layers.reverse(); } layers } } /// Ensures precedence ordering of config layers is correct. Returns the index /// of the user config layer, if any (at most one should exist). fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result> { if !layers.iter().map(|layer| &layer.name).is_sorted() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "config layers are not in correct precedence order", )); } // The previous check ensured `layers` is sorted by precedence, so now we // further verify that: // 1. There is at most one user config layer. // 2. Project layers are ordered from root to cwd. let mut user_layer_index: Option = None; let mut previous_project_dot_codex_folder: Option<&AbsolutePathBuf> = None; for (index, layer) in layers.iter().enumerate() { if matches!(layer.name, ConfigLayerSource::User { .. }) { if user_layer_index.is_some() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "multiple user config layers found", )); } user_layer_index = Some(index); } if let ConfigLayerSource::Project { dot_codex_folder: current_project_dot_codex_folder, } = &layer.name { if let Some(previous) = previous_project_dot_codex_folder { let Some(parent) = previous.as_path().parent() else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "project layer has no parent directory", )); }; if previous == current_project_dot_codex_folder || !current_project_dot_codex_folder .as_path() .ancestors() .any(|ancestor| ancestor == parent) { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "project layers are not ordered from root to cwd", )); } } previous_project_dot_codex_folder = Some(current_project_dot_codex_folder); } } Ok(user_layer_index) } #[cfg(test)] #[path = "state_tests.rs"] mod tests;