Codex Handbook
config/src/state.rs 595 lines
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 crate::CloudConfigBundleLoader;use crate::ProfileV2Name;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::Path;use std::path::PathBuf;use toml::Value as TomlValue;/// User-facing config loading behavior that is not part of the config document.#[derive(Debug, Default, Clone)]pub struct ConfigLoadOptions {    pub loader_overrides: LoaderOverrides,    pub strict_config: bool,    pub cloud_config_bundle: CloudConfigBundleLoader,}impl From<LoaderOverrides> for ConfigLoadOptions {    fn from(loader_overrides: LoaderOverrides) -> Self {        Self {            loader_overrides,            strict_config: false,            cloud_config_bundle: CloudConfigBundleLoader::default(),        }    }}/// LoaderOverrides overrides managed configuration inputs (primarily for tests).#[derive(Debug, Default, Clone)]pub struct LoaderOverrides {    pub user_config_path: Option<AbsolutePathBuf>,    pub user_config_profile: Option<ProfileV2Name>,    pub managed_config_path: Option<PathBuf>,    pub system_config_path: Option<PathBuf>,    pub system_requirements_path: Option<PathBuf>,    pub ignore_managed_requirements: bool,    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<String>,    pub macos_managed_config_requirements_base64: Option<String>,}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 {        let base = std::env::temp_dir().join("codex-config-tests");        Self {            user_config_path: None,            user_config_profile: None,            managed_config_path: Some(base.join("managed_config.toml")),            system_config_path: Some(base.join("config.toml")),            system_requirements_path: Some(base.join("requirements.toml")),            ignore_managed_requirements: false,            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()),        }    }    /// Returns overrides with host MDM disabled and managed config loaded from    /// `managed_config_path`. System requirements are loaded from a sibling    /// `requirements.toml` fixture.    ///    /// 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 {        let system_requirements_path = managed_config_path.with_file_name("requirements.toml");        Self {            user_config_path: None,            user_config_profile: None,            managed_config_path: Some(managed_config_path),            system_requirements_path: Some(system_requirements_path),            ..Self::without_managed_config_for_tests()        }    }    pub fn user_config_path(&self, codex_home: &Path) -> std::io::Result<AbsolutePathBuf> {        match self.user_config_path.as_ref() {            Some(path) => Ok(path.clone()),            None => Ok(AbsolutePathBuf::resolve_path_against_base(                crate::CONFIG_TOML_FILE,                codex_home,            )),        }    }}#[derive(Debug, Clone, PartialEq)]pub struct ConfigLayerEntry {    pub name: ConfigLayerSource,    pub config: TomlValue,    pub version: String,    pub disabled_reason: Option<String>,    raw_toml: Option<RawTomlLayer>,    hooks_config_folder_override: Option<AbsolutePathBuf>,}#[derive(Debug, Clone, PartialEq)]struct RawTomlLayer {    contents: String,    base_dir: AbsolutePathBuf,}impl ConfigLayerEntry {    pub fn new(name: ConfigLayerSource, config: TomlValue) -> Self {        let version = version_for_toml(&config);        Self {            name,            config,            version,            disabled_reason: None,            raw_toml: None,            hooks_config_folder_override: None,        }    }    pub fn new_with_raw_toml(        name: ConfigLayerSource,        config: TomlValue,        raw_toml: String,        raw_toml_base_dir: AbsolutePathBuf,    ) -> Self {        let version = version_for_toml(&config);        Self {            name,            config,            version,            disabled_reason: None,            raw_toml: Some(RawTomlLayer {                contents: raw_toml,                base_dir: raw_toml_base_dir,            }),            hooks_config_folder_override: None,        }    }    pub fn new_disabled(        name: ConfigLayerSource,        config: TomlValue,        disabled_reason: impl Into<String>,    ) -> Self {        let version = version_for_toml(&config);        Self {            name,            config,            version,            disabled_reason: Some(disabled_reason.into()),            raw_toml: None,            hooks_config_folder_override: None,        }    }    pub fn is_disabled(&self) -> bool {        self.disabled_reason.is_some()    }    pub fn raw_toml(&self) -> Option<&str> {        self.raw_toml            .as_ref()            .map(|raw_toml| raw_toml.contents.as_str())    }    pub fn raw_toml_base_dir(&self) -> Option<&AbsolutePathBuf> {        self.raw_toml.as_ref().map(|raw_toml| &raw_toml.base_dir)    }    pub(crate) fn with_hooks_config_folder_override(        mut self,        hooks_config_folder_override: Option<AbsolutePathBuf>,    ) -> Self {        self.hooks_config_folder_override = hooks_config_folder_override;        self    }    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<AbsolutePathBuf> {        match &self.name {            ConfigLayerSource::Mdm { .. } => None,            ConfigLayerSource::System { file } => file.parent(),            ConfigLayerSource::EnterpriseManaged { .. } => None,            ConfigLayerSource::User { file, .. } => file.parent(),            ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),            ConfigLayerSource::SessionFlags => None,            ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None,            ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,        }    }    /// Returns the `.codex/` folder that should be used for hook declarations.    ///    /// Project layers normally use their own config folder. Linked Git worktrees    /// can instead point hook discovery at the matching folder from the root    /// checkout while the rest of the project config still comes from the    /// worktree.    pub fn hooks_config_folder(&self) -> Option<AbsolutePathBuf> {        self.hooks_config_folder_override            .clone()            .or_else(|| self.config_folder())    }}#[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<ConfigLayerEntry>,    /// Index into [layers] of the active user config layer, if any.    ///    /// When profile config is active, there can be more than one user layer:    /// the base `$CODEX_HOME/config.toml` layer followed by the profile override    /// layer. This index points at the highest-precedence user layer because that    /// is the writable layer for profile-aware edits.    user_layer_index: Option<usize>,    /// 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,    /// Startup warnings discovered while building this stack.    ///    /// `None` means the loader did not check for stack-level warnings, while    /// `Some(vec![])` means it checked and found nothing to report.    startup_warnings: Option<Vec<String>>,}impl ConfigLayerStack {    pub fn new(        layers: Vec<ConfigLayerEntry>,        requirements: ConfigRequirements,        requirements_toml: ConfigRequirementsToml,    ) -> std::io::Result<Self> {        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,            startup_warnings: None,        })    }    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    }    pub(crate) fn with_startup_warnings(mut self, startup_warnings: Vec<String>) -> Self {        self.startup_warnings = Some(startup_warnings);        self    }    pub fn startup_warnings(&self) -> Option<&[String]> {        self.startup_warnings.as_deref()    }    /// Returns the active raw user config layer, if any.    ///    /// This does not merge other config layers or apply any requirements. When    /// a profile-v2 layer is active, this returns that profile layer rather than    /// the base `$CODEX_HOME/config.toml` layer because the active layer is the    /// writable target for profile-aware edits.    pub fn get_active_user_layer(&self) -> Option<&ConfigLayerEntry> {        self.user_layer_index            .and_then(|index| self.layers.get(index))    }    pub fn get_user_config_file(&self) -> Option<&AbsolutePathBuf> {        let layer = self.get_active_user_layer()?;        let ConfigLayerSource::User { file, .. } = &layer.name else {            return None;        };        Some(file)    }    /// Returns all user config layers in the requested precedence order.    ///    /// With profile-v2 enabled, `LowestPrecedenceFirst` returns the base user    /// config before the profile overlay, while `HighestPrecedenceFirst` returns    /// the profile overlay before the base user config.    pub fn get_user_layers(        &self,        ordering: ConfigLayerStackOrdering,        include_disabled: bool,    ) -> Vec<&ConfigLayerEntry> {        self.get_layers(ordering, include_disabled)            .into_iter()            .filter(|layer| matches!(layer.name, ConfigLayerSource::User { .. }))            .collect()    }    /// Returns the merged config from enabled user layers only.    ///    /// When profile config is active, this includes the base user config followed    /// by the profile override config.    pub fn effective_user_config(&self) -> Option<TomlValue> {        let user_layers = self.get_user_layers(            ConfigLayerStackOrdering::LowestPrecedenceFirst,            /*include_disabled*/ false,        );        if user_layers.is_empty() {            return None;        }        let mut merged = TomlValue::Table(toml::map::Map::new());        for layer in user_layers {            merge_toml_values(&mut merged, &layer.config);        }        Some(merged)    }    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 one    /// 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. When the stack has both base and profile-v2    /// user layers, this updates only the layer whose file matches    /// `config_toml`.    pub fn with_user_config(&self, config_toml: &AbsolutePathBuf, user_config: TomlValue) -> Self {        let profile = self.layers.iter().find_map(|layer| match &layer.name {            ConfigLayerSource::User { file, profile } if file == config_toml => profile                .as_deref()                .and_then(|profile| profile.parse::<ProfileV2Name>().ok()),            _ => None,        });        self.with_user_config_profile(config_toml, profile.as_ref(), user_config)    }    pub fn with_user_config_profile(        &self,        config_toml: &AbsolutePathBuf,        profile: Option<&ProfileV2Name>,        user_config: TomlValue,    ) -> Self {        let user_layer = ConfigLayerEntry::new(            ConfigLayerSource::User {                file: config_toml.clone(),                profile: profile.map(ToString::to_string),            },            user_config,        );        let mut layers = self.layers.clone();        if let Some(index) = layers.iter().position(|layer| {            matches!(                &layer.name,                ConfigLayerSource::User { file, .. } if file == config_toml            )        }) {            layers.remove(index);        }        match layers            .iter()            .position(|layer| layer.name.precedence() > user_layer.name.precedence())        {            Some(index) => layers.insert(index, user_layer),            None => layers.push(user_layer),        }        let user_layer_index = layers.iter().enumerate().rev().find_map(|(index, layer)| {            if matches!(layer.name, ConfigLayerSource::User { .. }) {                Some(index)            } else {                None            }        });        Self {            layers,            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,            startup_warnings: self.startup_warnings.clone(),        }    }    /// Returns a new stack with the user layer copied from `other`, preserving    /// every non-user layer already present in this stack.    pub fn with_user_layer_from(&self, other: &Self) -> Self {        let user_layers = other            .layers            .iter()            .filter(|layer| matches!(layer.name, ConfigLayerSource::User { .. }))            .cloned()            .collect::<Vec<_>>();        let mut layers = self            .layers            .iter()            .filter(|layer| !matches!(layer.name, ConfigLayerSource::User { .. }))            .cloned()            .collect::<Vec<_>>();        for user_layer in user_layers {            match layers                .iter()                .position(|layer| layer.name.precedence() > user_layer.name.precedence())            {                Some(index) => layers.insert(index, user_layer),                None => layers.push(user_layer),            }        }        let user_layer_index = layers.iter().enumerate().rev().find_map(|(index, layer)| {            if matches!(layer.name, ConfigLayerSource::User { .. }) {                Some(index)            } else {                None            }        });        Self {            layers,            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,            startup_warnings: self.startup_warnings.clone(),        }    }    /// Returns the merged config-layer view.    ///    /// This only merges ordinary config layers. Requirements are composed and    /// tracked separately.    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<String, ConfigLayerMetadata> {        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 active user config layer, if any.fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result<Option<usize>> {    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 project layers are ordered from root to cwd. Multiple    // user layers are allowed so a profile override can layer on top of the base    // user config.    let mut user_layer_index: Option<usize> = None;    let mut previous_project_dot_codex_folder: Option<&AbsolutePathBuf> = None;    for (index, layer) in layers.iter().enumerate() {        if matches!(layer.name, ConfigLayerSource::User { .. }) {            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;