Extract codex-config from codex-core (#11389)

`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`.
This commit is contained in:
Michael Bolin
2026-02-11 10:02:49 -08:00
committed by GitHub
parent 7e0178597e
commit 577a416f9a
20 changed files with 292 additions and 163 deletions

View File

@@ -1,62 +0,0 @@
use crate::config_loader::ConfigRequirementsToml;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use futures::future::Shared;
use std::fmt;
use std::future::Future;
#[derive(Clone)]
pub struct CloudRequirementsLoader {
// TODO(gt): This should return a Result once we can fail-closed.
fut: Shared<BoxFuture<'static, Option<ConfigRequirementsToml>>>,
}
impl CloudRequirementsLoader {
pub fn new<F>(fut: F) -> Self
where
F: Future<Output = Option<ConfigRequirementsToml>> + Send + 'static,
{
Self {
fut: fut.boxed().shared(),
}
}
pub async fn get(&self) -> Option<ConfigRequirementsToml> {
self.fut.clone().await
}
}
impl fmt::Debug for CloudRequirementsLoader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CloudRequirementsLoader").finish()
}
}
impl Default for CloudRequirementsLoader {
fn default() -> Self {
Self::new(async { None })
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
#[tokio::test]
async fn shared_future_runs_once() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let loader = CloudRequirementsLoader::new(async move {
counter_clone.fetch_add(1, Ordering::SeqCst);
Some(ConfigRequirementsToml::default())
});
let (first, second) = tokio::join!(loader.get(), loader.get());
assert_eq!(first, second);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,67 +0,0 @@
use codex_app_server_protocol::ConfigLayerMetadata;
use serde_json::Value as JsonValue;
use sha2::Digest;
use sha2::Sha256;
use std::collections::HashMap;
use toml::Value as TomlValue;
pub(super) fn record_origins(
value: &TomlValue,
meta: &ConfigLayerMetadata,
path: &mut Vec<String>,
origins: &mut HashMap<String, ConfigLayerMetadata>,
) {
match value {
TomlValue::Table(table) => {
for (key, val) in table {
path.push(key.clone());
record_origins(val, meta, path, origins);
path.pop();
}
}
TomlValue::Array(items) => {
for (idx, item) in (0_i32..).zip(items.iter()) {
path.push(idx.to_string());
record_origins(item, meta, path, origins);
path.pop();
}
}
_ => {
if !path.is_empty() {
origins.insert(path.join("."), meta.clone());
}
}
}
}
pub(super) fn version_for_toml(value: &TomlValue) -> String {
let json = serde_json::to_value(value).unwrap_or(JsonValue::Null);
let canonical = canonical_json(&json);
let serialized = serde_json::to_vec(&canonical).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(serialized);
let hash = hasher.finalize();
let hex = hash
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
format!("sha256:{hex}")
}
fn canonical_json(value: &JsonValue) -> JsonValue {
match value {
JsonValue::Object(map) => {
let mut sorted = serde_json::Map::new();
let mut keys = map.keys().cloned().collect::<Vec<_>>();
keys.sort();
for key in keys {
if let Some(val) = map.get(&key) {
sorted.insert(key, canonical_json(val));
}
}
JsonValue::Object(sorted)
}
JsonValue::Array(items) => JsonValue::Array(items.iter().map(canonical_json).collect()),
other => other.clone(),
}
}

View File

@@ -1,6 +1,6 @@
use super::config_requirements::ConfigRequirementsToml;
use super::config_requirements::ConfigRequirementsWithSources;
use super::config_requirements::RequirementSource;
use super::ConfigRequirementsToml;
use super::ConfigRequirementsWithSources;
use super::RequirementSource;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use core_foundation::base::TCFType;

View File

@@ -1,18 +0,0 @@
use toml::Value as TomlValue;
/// Merge config `overlay` into `base`, giving `overlay` precedence.
pub fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) {
if let TomlValue::Table(overlay_table) = overlay
&& let TomlValue::Table(base_table) = base
{
for (key, value) in overlay_table {
if let Some(existing) = base_table.get_mut(key) {
merge_toml_values(existing, value);
} else {
base_table.insert(key.clone(), value.clone());
}
}
} else {
*base = overlay.clone();
}
}

View File

@@ -1,14 +1,7 @@
mod cloud_requirements;
mod config_requirements;
mod diagnostics;
mod fingerprint;
mod layer_io;
#[cfg(target_os = "macos")]
mod macos;
mod merge;
mod overrides;
mod requirements_exec_policy;
mod state;
#[cfg(test)]
mod tests;
@@ -16,10 +9,10 @@ mod tests;
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigToml;
use crate::config::deserialize_config_toml_with_base;
use crate::config_loader::config_requirements::ConfigRequirementsWithSources;
use crate::config_loader::layer_io::LoadedConfigLayers;
use crate::git_info::resolve_root_git_project_for_trust;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::ConfigRequirementsWithSources;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::protocol::AskForApproval;
@@ -33,19 +26,27 @@ use std::path::Path;
use std::path::PathBuf;
use toml::Value as TomlValue;
pub use cloud_requirements::CloudRequirementsLoader;
pub use config_requirements::ConfigRequirements;
pub use config_requirements::ConfigRequirementsToml;
pub use config_requirements::ConstrainedWithSource;
pub use config_requirements::McpServerIdentity;
pub use config_requirements::McpServerRequirement;
pub use config_requirements::NetworkConstraints;
pub use config_requirements::NetworkRequirementsToml;
pub use config_requirements::RequirementSource;
pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::Sourced;
pub use config_requirements::WebSearchModeRequirement;
pub use codex_config::CloudRequirementsLoader;
pub use codex_config::ConfigLayerEntry;
pub use codex_config::ConfigLayerStack;
pub use codex_config::ConfigLayerStackOrdering;
pub use codex_config::ConfigRequirements;
pub use codex_config::ConfigRequirementsToml;
pub use codex_config::ConstrainedWithSource;
pub use codex_config::LoaderOverrides;
pub use codex_config::McpServerIdentity;
pub use codex_config::McpServerRequirement;
pub use codex_config::NetworkConstraints;
pub use codex_config::NetworkRequirementsToml;
pub use codex_config::RequirementSource;
pub use codex_config::ResidencyRequirement;
pub use codex_config::SandboxModeRequirement;
pub use codex_config::Sourced;
pub use codex_config::WebSearchModeRequirement;
pub(crate) use codex_config::build_cli_overrides_layer;
pub use codex_config::merge_toml_values;
#[cfg(test)]
pub(crate) use codex_config::version_for_toml;
pub use diagnostics::ConfigError;
pub use diagnostics::ConfigLoadError;
pub use diagnostics::TextPosition;
@@ -56,12 +57,6 @@ pub(crate) use diagnostics::first_layer_config_error_from_entries;
pub use diagnostics::format_config_error;
pub use diagnostics::format_config_error_with_source;
pub(crate) use diagnostics::io_error_from_config_error;
pub use merge::merge_toml_values;
pub(crate) use overrides::build_cli_overrides_layer;
pub use state::ConfigLayerEntry;
pub use state::ConfigLayerStack;
pub use state::ConfigLayerStackOrdering;
pub use state::LoaderOverrides;
/// On Unix systems, load default settings from this file path, if present.
/// Note that /etc/codex/ is treated as a "config folder," so subfolders such
@@ -145,7 +140,7 @@ pub async fn load_config_layers_state(
let cli_overrides_layer = if cli_overrides.is_empty() {
None
} else {
let cli_overrides_layer = overrides::build_cli_overrides_layer(cli_overrides);
let cli_overrides_layer = build_cli_overrides_layer(cli_overrides);
let base_dir = cwd
.as_ref()
.map(AbsolutePathBuf::as_path)

View File

@@ -1,55 +0,0 @@
use toml::Value as TomlValue;
pub(crate) fn default_empty_table() -> TomlValue {
TomlValue::Table(Default::default())
}
pub(crate) fn build_cli_overrides_layer(cli_overrides: &[(String, TomlValue)]) -> TomlValue {
let mut root = default_empty_table();
for (path, value) in cli_overrides {
apply_toml_override(&mut root, path, value.clone());
}
root
}
/// Apply a single dotted-path override onto a TOML value.
fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
use toml::value::Table;
let mut current = root;
let mut segments_iter = path.split('.').peekable();
while let Some(segment) = segments_iter.next() {
let is_last = segments_iter.peek().is_none();
if is_last {
match current {
TomlValue::Table(table) => {
table.insert(segment.to_string(), value);
}
_ => {
let mut table = Table::new();
table.insert(segment.to_string(), value);
*current = TomlValue::Table(table);
}
}
return;
}
match current {
TomlValue::Table(table) => {
current = table
.entry(segment.to_string())
.or_insert_with(|| TomlValue::Table(Table::new()));
}
_ => {
*current = TomlValue::Table(Table::new());
if let TomlValue::Table(tbl) = current {
current = tbl
.entry(segment.to_string())
.or_insert_with(|| TomlValue::Table(Table::new()));
}
}
}
}
}

View File

@@ -1,236 +0,0 @@
use codex_execpolicy::Decision;
use codex_execpolicy::Policy;
use codex_execpolicy::rule::PatternToken;
use codex_execpolicy::rule::PrefixPattern;
use codex_execpolicy::rule::PrefixRule;
use codex_execpolicy::rule::RuleRef;
use multimap::MultiMap;
use serde::Deserialize;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone)]
pub(crate) struct RequirementsExecPolicy {
policy: Policy,
}
impl RequirementsExecPolicy {
pub fn new(policy: Policy) -> Self {
Self { policy }
}
}
impl PartialEq for RequirementsExecPolicy {
fn eq(&self, other: &Self) -> bool {
policy_fingerprint(&self.policy) == policy_fingerprint(&other.policy)
}
}
impl Eq for RequirementsExecPolicy {}
impl AsRef<Policy> for RequirementsExecPolicy {
fn as_ref(&self) -> &Policy {
&self.policy
}
}
fn policy_fingerprint(policy: &Policy) -> Vec<String> {
let mut entries = Vec::new();
for (program, rules) in policy.rules().iter_all() {
for rule in rules {
entries.push(format!("{program}:{rule:?}"));
}
}
entries.sort();
entries
}
/// TOML representation of `[rules]` within `requirements.toml`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RequirementsExecPolicyToml {
pub prefix_rules: Vec<RequirementsExecPolicyPrefixRuleToml>,
}
/// A TOML representation of the `prefix_rule(...)` Starlark builtin.
///
/// This mirrors the builtin defined in `execpolicy/src/parser.rs`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RequirementsExecPolicyPrefixRuleToml {
pub pattern: Vec<RequirementsExecPolicyPatternTokenToml>,
pub decision: Option<RequirementsExecPolicyDecisionToml>,
pub justification: Option<String>,
}
/// TOML-friendly representation of a pattern token.
///
/// Starlark supports either a string token or a list of alternative tokens at
/// each position, but TOML arrays cannot mix strings and arrays. Using an
/// array of tables sidesteps that restriction.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RequirementsExecPolicyPatternTokenToml {
pub token: Option<String>,
pub any_of: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RequirementsExecPolicyDecisionToml {
Allow,
Prompt,
Forbidden,
}
impl RequirementsExecPolicyDecisionToml {
fn as_decision(self) -> Decision {
match self {
Self::Allow => Decision::Allow,
Self::Prompt => Decision::Prompt,
Self::Forbidden => Decision::Forbidden,
}
}
}
#[derive(Debug, Error)]
pub enum RequirementsExecPolicyParseError {
#[error("rules prefix_rules cannot be empty")]
EmptyPrefixRules,
#[error("rules prefix_rule at index {rule_index} has an empty pattern")]
EmptyPattern { rule_index: usize },
#[error(
"rules prefix_rule at index {rule_index} has an invalid pattern token at index {token_index}: {reason}"
)]
InvalidPatternToken {
rule_index: usize,
token_index: usize,
reason: String,
},
#[error("rules prefix_rule at index {rule_index} has an empty justification")]
EmptyJustification { rule_index: usize },
#[error("rules prefix_rule at index {rule_index} is missing a decision")]
MissingDecision { rule_index: usize },
#[error(
"rules prefix_rule at index {rule_index} has decision 'allow', which is not permitted in requirements.toml: Codex merges these rules with other config and uses the most restrictive result (use 'prompt' or 'forbidden')"
)]
AllowDecisionNotAllowed { rule_index: usize },
}
impl RequirementsExecPolicyToml {
/// Convert requirements TOML rules into the internal `.rules`
/// representation used by `codex-execpolicy`.
pub fn to_policy(&self) -> Result<Policy, RequirementsExecPolicyParseError> {
if self.prefix_rules.is_empty() {
return Err(RequirementsExecPolicyParseError::EmptyPrefixRules);
}
let mut rules_by_program: MultiMap<String, RuleRef> = MultiMap::new();
for (rule_index, rule) in self.prefix_rules.iter().enumerate() {
if let Some(justification) = &rule.justification
&& justification.trim().is_empty()
{
return Err(RequirementsExecPolicyParseError::EmptyJustification { rule_index });
}
if rule.pattern.is_empty() {
return Err(RequirementsExecPolicyParseError::EmptyPattern { rule_index });
}
let pattern_tokens = rule
.pattern
.iter()
.enumerate()
.map(|(token_index, token)| parse_pattern_token(token, rule_index, token_index))
.collect::<Result<Vec<_>, _>>()?;
let decision = match rule.decision {
Some(RequirementsExecPolicyDecisionToml::Allow) => {
return Err(RequirementsExecPolicyParseError::AllowDecisionNotAllowed {
rule_index,
});
}
Some(decision) => decision.as_decision(),
None => {
return Err(RequirementsExecPolicyParseError::MissingDecision { rule_index });
}
};
let justification = rule.justification.clone();
let (first_token, remaining_tokens) = pattern_tokens
.split_first()
.ok_or(RequirementsExecPolicyParseError::EmptyPattern { rule_index })?;
let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into();
for head in first_token.alternatives() {
let rule: RuleRef = Arc::new(PrefixRule {
pattern: PrefixPattern {
first: Arc::from(head.as_str()),
rest: rest.clone(),
},
decision,
justification: justification.clone(),
});
rules_by_program.insert(head.clone(), rule);
}
}
Ok(Policy::new(rules_by_program))
}
pub(crate) fn to_requirements_policy(
&self,
) -> Result<RequirementsExecPolicy, RequirementsExecPolicyParseError> {
self.to_policy().map(RequirementsExecPolicy::new)
}
}
fn parse_pattern_token(
token: &RequirementsExecPolicyPatternTokenToml,
rule_index: usize,
token_index: usize,
) -> Result<PatternToken, RequirementsExecPolicyParseError> {
match (&token.token, &token.any_of) {
(Some(single), None) => {
if single.trim().is_empty() {
return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "token cannot be empty".to_string(),
});
}
Ok(PatternToken::Single(single.clone()))
}
(None, Some(alternatives)) => {
if alternatives.is_empty() {
return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "any_of cannot be empty".to_string(),
});
}
if alternatives.iter().any(|alt| alt.trim().is_empty()) {
return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "any_of cannot include empty tokens".to_string(),
});
}
Ok(PatternToken::Alts(alternatives.clone()))
}
(Some(_), Some(_)) => Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "set either token or any_of, not both".to_string(),
}),
(None, None) => Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "set either token or any_of".to_string(),
}),
}
}

View File

@@ -1,311 +0,0 @@
use crate::config_loader::ConfigRequirements;
use crate::config_loader::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)
}

View File

@@ -11,10 +11,10 @@ use crate::config_loader::ConfigLayerEntry;
use crate::config_loader::ConfigLoadError;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use crate::config_loader::config_requirements::ConfigRequirementsWithSources;
use crate::config_loader::config_requirements::RequirementSource;
use crate::config_loader::fingerprint::version_for_toml;
use crate::config_loader::ConfigRequirementsWithSources;
use crate::config_loader::RequirementSource;
use crate::config_loader::load_requirements_toml;
use crate::config_loader::version_for_toml;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval;
@@ -1246,19 +1246,19 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
}
mod requirements_exec_policy_tests {
use super::super::config_requirements::ConfigRequirementsWithSources;
use super::super::requirements_exec_policy::RequirementsExecPolicyDecisionToml;
use super::super::requirements_exec_policy::RequirementsExecPolicyParseError;
use super::super::requirements_exec_policy::RequirementsExecPolicyPatternTokenToml;
use super::super::requirements_exec_policy::RequirementsExecPolicyPrefixRuleToml;
use super::super::requirements_exec_policy::RequirementsExecPolicyToml;
use crate::config_loader::ConfigLayerEntry;
use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use crate::config_loader::ConfigRequirementsWithSources;
use crate::config_loader::RequirementSource;
use crate::exec_policy::load_exec_policy;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::RequirementsExecPolicyDecisionToml;
use codex_config::RequirementsExecPolicyParseError;
use codex_config::RequirementsExecPolicyPatternTokenToml;
use codex_config::RequirementsExecPolicyPrefixRuleToml;
use codex_config::RequirementsExecPolicyToml;
use codex_execpolicy::Decision;
use codex_execpolicy::Evaluation;
use codex_execpolicy::RuleMatch;