chore: move config diagnostics out of codex-core (#12427)

## Why

Compiling `codex-rs/core` is a bottleneck for local iteration, so this
change continues the ongoing extraction of config-related functionality
out of `codex-core` and into `codex-config`.

The goal is not just to move code, but to reduce `codex-core` ownership
and indirection so more code depends on `codex-config` directly.

## What Changed

- Moved config diagnostics logic from
`core/src/config_loader/diagnostics.rs` into
`config/src/diagnostics.rs`.
- Updated `codex-core` to use `codex-config` diagnostics types/functions
directly where possible.
- Removed the `core/src/config_loader/diagnostics.rs` shim module
entirely; the remaining `ConfigToml`-specific calls are in
`core/src/config_loader/mod.rs`.
- Moved `CONFIG_TOML_FILE` into `codex-config` and updated existing
references to use `codex_config::CONFIG_TOML_FILE` directly.
- Added a direct `codex-config` dependency to `codex-cli` for its
`CONFIG_TOML_FILE` use.
This commit is contained in:
Michael Bolin 2026-02-20 23:19:29 -08:00 committed by GitHub
parent bb0ac5be70
commit 1a220ad77d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 82 additions and 48 deletions

5
codex-rs/Cargo.lock generated
View file

@ -1470,6 +1470,7 @@ dependencies = [
"codex-arg0",
"codex-chatgpt",
"codex-cloud-tasks",
"codex-config",
"codex-core",
"codex-exec",
"codex-execpolicy",
@ -1600,10 +1601,13 @@ dependencies = [
"pretty_assertions",
"serde",
"serde_json",
"serde_path_to_error",
"sha2",
"thiserror 2.0.18",
"tokio",
"toml 0.9.11+spec-1.1.0",
"toml_edit 0.24.0+spec-1.1.0",
"tracing",
]
[[package]]
@ -1681,7 +1685,6 @@ dependencies = [
"seccompiler",
"serde",
"serde_json",
"serde_path_to_error",
"serde_yaml",
"serial_test",
"sha1",

View file

@ -26,6 +26,7 @@ codex-arg0 = { workspace = true }
codex-chatgpt = { workspace = true }
codex-cloud-tasks = { path = "../cloud-tasks" }
codex-utils-cli = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-exec = { workspace = true }
codex-execpolicy = { workspace = true }

View file

@ -870,7 +870,7 @@ fn maybe_print_under_development_feature_warning(
return;
}
let config_path = codex_home.join(codex_core::config::CONFIG_TOML_FILE);
let config_path = codex_home.join(codex_config::CONFIG_TOML_FILE);
eprintln!(
"Under-development features enabled: {feature}. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in {}.",
config_path.display()

View file

@ -16,9 +16,13 @@ futures = { workspace = true, features = ["alloc", "std"] }
multimap = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_path_to_error = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
toml = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }

View file

@ -1,10 +1,12 @@
//! Helpers for mapping config parse/validation failures to file locations and
//! rendering them in a user-friendly way.
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigToml;
use crate::ConfigLayerEntry;
use crate::ConfigLayerStack;
use crate::ConfigLayerStackOrdering;
use codex_app_server_protocol::ConfigLayerSource;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use serde::de::DeserializeOwned;
use serde_path_to_error::Path as SerdePath;
use serde_path_to_error::Segment as SerdeSegment;
use std::fmt;
@ -17,10 +19,6 @@ use toml_edit::Item;
use toml_edit::Table;
use toml_edit::Value;
use super::ConfigLayerEntry;
use super::ConfigLayerStack;
use super::ConfigLayerStackOrdering;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextPosition {
pub line: usize,
@ -88,7 +86,7 @@ impl std::error::Error for ConfigLoadError {
}
}
pub(crate) fn io_error_from_config_error(
pub fn io_error_from_config_error(
kind: io::ErrorKind,
error: ConfigError,
source: Option<toml::de::Error>,
@ -96,7 +94,7 @@ pub(crate) fn io_error_from_config_error(
io::Error::new(kind, ConfigLoadError::new(error, source))
}
pub(crate) fn config_error_from_toml(
pub fn config_error_from_toml(
path: impl AsRef<Path>,
contents: &str,
err: toml::de::Error,
@ -108,7 +106,7 @@ pub(crate) fn config_error_from_toml(
ConfigError::new(path.as_ref().to_path_buf(), range, err.message())
}
pub(crate) fn config_error_from_config_toml(
pub fn config_error_from_typed_toml<T: DeserializeOwned>(
path: impl AsRef<Path>,
contents: &str,
) -> Option<ConfigError> {
@ -117,7 +115,7 @@ pub(crate) fn config_error_from_config_toml(
Err(err) => return Some(config_error_from_toml(path, contents, err)),
};
let result: Result<ConfigToml, _> = serde_path_to_error::deserialize(deserializer);
let result: Result<T, _> = serde_path_to_error::deserialize(deserializer);
match result {
Ok(_) => None,
Err(err) => {
@ -136,28 +134,36 @@ pub(crate) fn config_error_from_config_toml(
}
}
pub(crate) async fn first_layer_config_error(layers: &ConfigLayerStack) -> Option<ConfigError> {
pub async fn first_layer_config_error<T: DeserializeOwned>(
layers: &ConfigLayerStack,
config_toml_file: &str,
) -> Option<ConfigError> {
// When the merged config fails schema validation, we surface the first concrete
// per-file error to point users at a specific file and range rather than an
// opaque merged-layer failure.
first_layer_config_error_for_entries(
first_layer_config_error_for_entries::<T, _>(
layers.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false),
config_toml_file,
)
.await
}
pub(crate) async fn first_layer_config_error_from_entries(
pub async fn first_layer_config_error_from_entries<T: DeserializeOwned>(
layers: &[ConfigLayerEntry],
config_toml_file: &str,
) -> Option<ConfigError> {
first_layer_config_error_for_entries(layers.iter()).await
first_layer_config_error_for_entries::<T, _>(layers.iter(), config_toml_file).await
}
async fn first_layer_config_error_for_entries<'a, I>(layers: I) -> Option<ConfigError>
async fn first_layer_config_error_for_entries<'a, T: DeserializeOwned, I>(
layers: I,
config_toml_file: &str,
) -> Option<ConfigError>
where
I: IntoIterator<Item = &'a ConfigLayerEntry>,
{
for layer in layers {
let Some(path) = config_path_for_layer(layer) else {
let Some(path) = config_path_for_layer(layer, config_toml_file) else {
continue;
};
let contents = match tokio::fs::read_to_string(&path).await {
@ -174,7 +180,7 @@ where
continue;
};
let _guard = AbsolutePathBufGuard::new(parent);
if let Some(error) = config_error_from_config_toml(&path, &contents) {
if let Some(error) = config_error_from_typed_toml::<T>(&path, &contents) {
return Some(error);
}
}
@ -182,12 +188,12 @@ where
None
}
fn config_path_for_layer(layer: &ConfigLayerEntry) -> Option<PathBuf> {
fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Option<PathBuf> {
match &layer.name {
ConfigLayerSource::System { file } => Some(file.to_path_buf()),
ConfigLayerSource::User { file } => Some(file.to_path_buf()),
ConfigLayerSource::Project { dot_codex_folder } => {
Some(dot_codex_folder.as_path().join(CONFIG_TOML_FILE))
Some(dot_codex_folder.as_path().join(config_toml_file))
}
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.to_path_buf()),
ConfigLayerSource::Mdm { .. }

View file

@ -1,12 +1,15 @@
mod cloud_requirements;
mod config_requirements;
mod constraint;
mod diagnostics;
mod fingerprint;
mod merge;
mod overrides;
mod requirements_exec_policy;
mod state;
pub const CONFIG_TOML_FILE: &str = "config.toml";
pub use cloud_requirements::CloudRequirementsLoader;
pub use config_requirements::ConfigRequirements;
pub use config_requirements::ConfigRequirementsToml;
@ -24,6 +27,17 @@ pub use config_requirements::WebSearchModeRequirement;
pub use constraint::Constrained;
pub use constraint::ConstraintError;
pub use constraint::ConstraintResult;
pub use diagnostics::ConfigError;
pub use diagnostics::ConfigLoadError;
pub use diagnostics::TextPosition;
pub use diagnostics::TextRange;
pub use diagnostics::config_error_from_toml;
pub use diagnostics::config_error_from_typed_toml;
pub use diagnostics::first_layer_config_error;
pub use diagnostics::first_layer_config_error_from_entries;
pub use diagnostics::format_config_error;
pub use diagnostics::format_config_error_with_source;
pub use diagnostics::io_error_from_config_error;
pub use fingerprint::version_for_toml;
pub use merge::merge_toml_values;
pub use overrides::build_cli_overrides_layer;

View file

@ -78,7 +78,6 @@ rmcp = { workspace = true, default-features = false, features = [
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_path_to_error = { workspace = true }
serde_yaml = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }

View file

@ -120,7 +120,6 @@ use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::codex_thread::ThreadConfigSnapshot;
use crate::compact::collect_user_messages;
use crate::config::CONFIG_TOML_FILE;
use crate::config::Config;
use crate::config::Constrained;
use crate::config::ConstraintResult;
@ -136,6 +135,7 @@ use crate::error::CodexErr;
use crate::error::Result as CodexResult;
#[cfg(test)]
use crate::exec::StreamOutput;
use codex_config::CONFIG_TOML_FILE;
#[derive(Debug, PartialEq)]
pub enum SteerInputError {

View file

@ -1,9 +1,9 @@
use crate::config::CONFIG_TOML_FILE;
use crate::config::types::McpServerConfig;
use crate::config::types::Notice;
use crate::path_utils::resolve_symlink_write_paths;
use crate::path_utils::write_atomically;
use anyhow::Context;
use codex_config::CONFIG_TOML_FILE;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::openai_models::ReasoningEffort;

View file

@ -117,8 +117,6 @@ pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option<usize> = Some(6);
pub(crate) const DEFAULT_AGENT_MAX_DEPTH: i32 = 1;
pub const CONFIG_TOML_FILE: &str = "config.toml";
#[cfg(test)]
pub(crate) fn test_config() -> Config {
let codex_home = tempdir().expect("create temp dir");
@ -2302,6 +2300,7 @@ mod tests {
use crate::config::types::Notifications;
use crate::config_loader::RequirementSource;
use crate::features::Feature;
use codex_config::CONFIG_TOML_FILE;
use super::*;
use core_test_support::test_absolute_path;

View file

@ -1,4 +1,3 @@
use super::CONFIG_TOML_FILE;
use super::ConfigToml;
use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
@ -26,6 +25,7 @@ use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::OverriddenMetadata;
use codex_app_server_protocol::WriteStatus;
use codex_config::CONFIG_TOML_FILE;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde_json::Value as JsonValue;
use std::borrow::Cow;

View file

@ -1,10 +1,10 @@
use super::LoaderOverrides;
use super::diagnostics::config_error_from_toml;
use super::diagnostics::io_error_from_config_error;
#[cfg(target_os = "macos")]
use super::macos::ManagedAdminConfigLayer;
#[cfg(target_os = "macos")]
use super::macos::load_managed_admin_config_layer;
use codex_config::config_error_from_toml;
use codex_config::io_error_from_config_error;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::io;
use std::path::Path;

View file

@ -1,4 +1,3 @@
mod diagnostics;
mod layer_io;
#[cfg(target_os = "macos")]
mod macos;
@ -6,12 +5,12 @@ mod macos;
#[cfg(test)]
mod tests;
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigToml;
use crate::config::deserialize_config_toml_with_base;
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::CONFIG_TOML_FILE;
use codex_config::ConfigRequirementsWithSources;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::TrustLevel;
@ -27,9 +26,11 @@ use std::path::PathBuf;
use toml::Value as TomlValue;
pub use codex_config::CloudRequirementsLoader;
pub use codex_config::ConfigError;
pub use codex_config::ConfigLayerEntry;
pub use codex_config::ConfigLayerStack;
pub use codex_config::ConfigLayerStackOrdering;
pub use codex_config::ConfigLoadError;
pub use codex_config::ConfigRequirements;
pub use codex_config::ConfigRequirementsToml;
pub use codex_config::ConstrainedWithSource;
@ -42,21 +43,17 @@ pub use codex_config::RequirementSource;
pub use codex_config::ResidencyRequirement;
pub use codex_config::SandboxModeRequirement;
pub use codex_config::Sourced;
pub use codex_config::TextPosition;
pub use codex_config::TextRange;
pub use codex_config::WebSearchModeRequirement;
pub(crate) use codex_config::build_cli_overrides_layer;
pub(crate) use codex_config::config_error_from_toml;
pub use codex_config::format_config_error;
pub use codex_config::format_config_error_with_source;
pub(crate) use codex_config::io_error_from_config_error;
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;
pub use diagnostics::TextRange;
pub(crate) use diagnostics::config_error_from_toml;
pub(crate) use diagnostics::first_layer_config_error;
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;
/// 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
@ -68,6 +65,17 @@ const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData";
const DEFAULT_PROJECT_ROOT_MARKERS: &[&str] = &[".git"];
pub(crate) async fn first_layer_config_error(layers: &ConfigLayerStack) -> Option<ConfigError> {
codex_config::first_layer_config_error::<ConfigToml>(layers, CONFIG_TOML_FILE).await
}
pub(crate) async fn first_layer_config_error_from_entries(
layers: &[ConfigLayerEntry],
) -> Option<ConfigError> {
codex_config::first_layer_config_error_from_entries::<ConfigToml>(layers, CONFIG_TOML_FILE)
.await
}
/// To build up the set of admin-enforced constraints, we build up from multiple
/// configuration layers in the following order, but a constraint defined in an
/// earlier layer cannot be overridden by a later layer:

View file

@ -1,6 +1,5 @@
use super::LoaderOverrides;
use super::load_config_layers_state;
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigBuilder;
use crate::config::ConfigOverrides;
use crate::config::ConfigToml;
@ -15,6 +14,7 @@ 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_config::CONFIG_TOML_FILE;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval;
@ -153,7 +153,7 @@ async fn returns_config_error_for_schema_error_in_user_config() {
let config_error = config_error_from_io(&err);
let _guard = codex_utils_absolute_path::AbsolutePathBufGuard::new(tmp.path());
let expected_config_error =
super::diagnostics::config_error_from_config_toml(&config_path, contents)
codex_config::config_error_from_typed_toml::<ConfigToml>(&config_path, contents)
.expect("schema error");
assert_eq!(config_error, &expected_config_error);
}
@ -166,7 +166,7 @@ fn schema_error_points_to_feature_value() {
std::fs::write(&config_path, contents).expect("write config");
let _guard = codex_utils_absolute_path::AbsolutePathBufGuard::new(tmp.path());
let error = super::diagnostics::config_error_from_config_toml(&config_path, contents)
let error = codex_config::config_error_from_typed_toml::<ConfigToml>(&config_path, contents)
.expect("schema error");
let value_line = contents.lines().nth(1).expect("value line");

View file

@ -5,13 +5,13 @@
//! booleans through multiple types, call sites consult a single `Features`
//! container attached to `Config`.
use crate::config::CONFIG_TOML_FILE;
use crate::config::Config;
use crate::config::ConfigToml;
use crate::config::profile::ConfigProfile;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::WarningEvent;
use codex_config::CONFIG_TOML_FILE;
use codex_otel::OtelManager;
use schemars::JsonSchema;
use serde::Deserialize;

View file

@ -1,4 +1,3 @@
use crate::config::CONFIG_TOML_FILE;
use crate::config::NetworkToml;
use crate::config::PermissionsToml;
use crate::config::find_codex_home;
@ -11,6 +10,7 @@ use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
use codex_network_proxy::ConfigReloader;
use codex_network_proxy::ConfigState;
use codex_network_proxy::NetworkProxyConfig;

View file

@ -806,7 +806,6 @@ fn extract_frontmatter(contents: &str) -> Option<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigBuilder;
use crate::config::ConfigOverrides;
use crate::config::ConfigToml;
@ -817,6 +816,7 @@ mod tests {
use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use codex_config::CONFIG_TOML_FILE;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;

View file

@ -1,8 +1,8 @@
#![allow(clippy::unwrap_used, clippy::expect_used)]
use codex_config::CONFIG_TOML_FILE;
use codex_core::CodexAuth;
use codex_core::NewThread;
use codex_core::config::CONFIG_TOML_FILE;
use codex_core::features::Feature;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InitialHistory;