`codex-core` had accumulated config loading, requirements parsing, constraint logic, and config-layer state handling in a single crate. This change extracts that subsystem into `codex-config` to reduce `codex-core` rebuild/test surface area and isolate future config work. ## What Changed ### Added `codex-config` - Added new workspace crate `codex-rs/config` (`codex-config`). - Added workspace/build wiring in: - `codex-rs/Cargo.toml` - `codex-rs/config/Cargo.toml` - `codex-rs/config/BUILD.bazel` - Updated lockfiles (`codex-rs/Cargo.lock`, `MODULE.bazel.lock`). - Added `codex-core` -> `codex-config` dependency in `codex-rs/core/Cargo.toml`. ### Moved config internals from `core` into `config` Moved modules to `codex-rs/config/src/`: - `core/src/config/constraint.rs` -> `config/src/constraint.rs` - `core/src/config_loader/cloud_requirements.rs` -> `config/src/cloud_requirements.rs` - `core/src/config_loader/config_requirements.rs` -> `config/src/config_requirements.rs` - `core/src/config_loader/fingerprint.rs` -> `config/src/fingerprint.rs` - `core/src/config_loader/merge.rs` -> `config/src/merge.rs` - `core/src/config_loader/overrides.rs` -> `config/src/overrides.rs` - `core/src/config_loader/requirements_exec_policy.rs` -> `config/src/requirements_exec_policy.rs` - `core/src/config_loader/state.rs` -> `config/src/state.rs` `codex-config` now re-exports this surface from `config/src/lib.rs` at the crate top level. ### Updated `core` to consume/re-export `codex-config` - `core/src/config_loader/mod.rs` now imports/re-exports config-loader types/functions from top-level `codex_config::*`. - Local moved modules were removed from `core/src/config_loader/`. - `core/src/config/mod.rs` now re-exports constraint types from `codex_config`.
311 lines
11 KiB
Rust
311 lines
11 KiB
Rust
use crate::config_requirements::ConfigRequirements;
|
|
use crate::config_requirements::ConfigRequirementsToml;
|
|
|
|
use super::fingerprint::record_origins;
|
|
use super::fingerprint::version_for_toml;
|
|
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<PathBuf>,
|
|
//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>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct ConfigLayerEntry {
|
|
pub name: ConfigLayerSource,
|
|
pub config: TomlValue,
|
|
pub raw_toml: Option<String>,
|
|
pub version: String,
|
|
pub disabled_reason: Option<String>,
|
|
}
|
|
|
|
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<String>,
|
|
) -> 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<AbsolutePathBuf> {
|
|
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<ConfigLayerEntry>,
|
|
|
|
/// Index into [layers] of the user config layer, if any.
|
|
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,
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
/// Returns the user config layer, if any.
|
|
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(),
|
|
}
|
|
}
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn effective_config(&self) -> TomlValue {
|
|
let mut merged = TomlValue::Table(toml::map::Map::new());
|
|
for layer in self.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false) {
|
|
merge_toml_values(&mut merged, &layer.config);
|
|
}
|
|
merged
|
|
}
|
|
|
|
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, false) {
|
|
record_origins(&layer.config, &layer.metadata(), &mut path, &mut origins);
|
|
}
|
|
|
|
origins
|
|
}
|
|
|
|
/// Returns the highest-precedence to lowest-precedence layers, so
|
|
/// `ConfigLayerSource::SessionFlags` would be first, if present.
|
|
pub fn layers_high_to_low(&self) -> Vec<&ConfigLayerEntry> {
|
|
self.get_layers(ConfigLayerStackOrdering::HighestPrecedenceFirst, false)
|
|
}
|
|
|
|
/// Returns the highest-precedence to lowest-precedence layers, so
|
|
/// `ConfigLayerSource::SessionFlags` would be first, if present.
|
|
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<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:
|
|
// 1. There is at most one user config layer.
|
|
// 2. Project layers are ordered from root to cwd.
|
|
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 { .. }) {
|
|
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)
|
|
}
|