config: add strict config parsing

This commit is contained in:
Michael Bolin
2026-05-01 16:04:58 -07:00
parent 466798aa83
commit bbec96c976
39 changed files with 1051 additions and 118 deletions
@@ -18,6 +18,7 @@ use codex_config::RequirementSource;
use codex_config::SessionThreadConfig;
use codex_config::StaticThreadConfigLoader;
use codex_config::ThreadConfigSource;
use codex_config::config_error_from_ignored_toml_fields;
use codex_config::config_error_from_toml;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
@@ -228,6 +229,115 @@ async fn returns_config_error_for_schema_error_in_user_config() {
assert_eq!(config_error, &expected_config_error);
}
#[tokio::test]
async fn strict_config_rejects_unknown_user_config_key() {
let tmp = tempdir().expect("tempdir");
let contents = "model = \"gpt-5\"\nunknown_key = true";
let config_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&config_path, contents).expect("write config");
let err = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
.strict_config(/*strict_config*/ true)
.build()
.await
.expect_err("expected error");
let config_error = config_error_from_io(&err);
let expected_config_error =
config_error_from_ignored_toml_fields::<ConfigToml>(&config_path, contents)
.expect("unknown field error");
assert_eq!(config_error, &expected_config_error);
}
#[tokio::test]
async fn strict_config_rejects_unknown_cli_override_key() {
let tmp = tempdir().expect("tempdir");
let err = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
.cli_overrides(vec![(
"foo".to_string(),
TomlValue::String("bar".to_string()),
)])
.strict_config(/*strict_config*/ true)
.build()
.await
.expect_err("expected error");
assert_eq!(
err.to_string(),
"unknown configuration field `foo` in -c/--config override"
);
}
#[tokio::test]
async fn strict_config_rejects_unknown_feature_cli_override_key() {
let tmp = tempdir().expect("tempdir");
let err = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
.cli_overrides(vec![("features.foo".to_string(), TomlValue::Boolean(true))])
.strict_config(/*strict_config*/ true)
.build()
.await
.expect_err("expected error");
assert_eq!(
err.to_string(),
"unknown configuration field `features.foo` in -c/--config override"
);
}
#[tokio::test]
async fn strict_config_rejects_unknown_feature_user_config_key() {
let tmp = tempdir().expect("tempdir");
let contents = "[features]\nfoo = true";
let config_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&config_path, contents).expect("write config");
let err = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
.strict_config(/*strict_config*/ true)
.build()
.await
.expect_err("expected error");
let config_error = config_error_from_io(&err);
assert_eq!(
config_error.message,
"unknown configuration field `features.foo`"
);
assert_eq!(config_error.range.start.line, 2);
assert_eq!(config_error.range.start.column, 1);
}
#[test]
fn strict_config_points_to_unknown_nested_key() {
let tmp = tempdir().expect("tempdir");
let contents = "[mcp_servers.local]\ncommand = \"echo\"\nunknown_key = true";
let config_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&config_path, contents).expect("write config");
let error = config_error_from_ignored_toml_fields::<ConfigToml>(&config_path, contents)
.expect("unknown field error");
assert_eq!(
error.message,
"unknown configuration field `mcp_servers.local.unknown_key`"
);
assert_eq!(error.range.start.line, 3);
assert_eq!(error.range.start.column, 1);
}
#[test]
fn schema_error_points_to_feature_value() {
let tmp = tempdir().expect("tempdir");
+35 -3
View File
@@ -15,7 +15,6 @@ use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
use codex_config::ConstrainedWithSource;
use codex_config::FeatureRequirementsToml;
use codex_config::LoaderOverrides;
use codex_config::McpServerIdentity;
use codex_config::McpServerRequirement;
use codex_config::PluginRequirementsToml;
@@ -132,9 +131,11 @@ mod network_proxy_spec;
mod permissions;
#[cfg(test)]
mod schema;
pub use codex_config::ConfigLoadOptions;
pub use codex_config::Constrained;
pub use codex_config::ConstraintError;
pub use codex_config::ConstraintResult;
pub use codex_config::LoaderOverrides;
pub use codex_network_proxy::NetworkProxyAuditMetadata;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
pub use codex_sandboxing::system_bwrap_warning;
@@ -878,6 +879,7 @@ pub struct ConfigBuilder {
cli_overrides: Option<Vec<(String, TomlValue)>>,
harness_overrides: Option<ConfigOverrides>,
loader_overrides: Option<LoaderOverrides>,
strict_config: bool,
cloud_requirements: CloudRequirementsLoader,
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
fallback_cwd: Option<PathBuf>,
@@ -904,6 +906,11 @@ impl ConfigBuilder {
self
}
pub fn strict_config(mut self, strict_config: bool) -> Self {
self.strict_config = strict_config;
self
}
pub fn cloud_requirements(mut self, cloud_requirements: CloudRequirementsLoader) -> Self {
self.cloud_requirements = cloud_requirements;
self
@@ -933,6 +940,7 @@ impl ConfigBuilder {
cli_overrides,
harness_overrides,
loader_overrides,
strict_config,
cloud_requirements,
thread_config_loader,
fallback_cwd,
@@ -955,7 +963,10 @@ impl ConfigBuilder {
&codex_home,
Some(cwd),
&cli_overrides,
loader_overrides,
ConfigLoadOptions {
loader_overrides,
strict_config,
},
cloud_requirements,
thread_config_loader
.as_deref()
@@ -1132,6 +1143,17 @@ impl Config {
.await
}
pub async fn load_with_cli_overrides_and_loader_overrides(
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
) -> std::io::Result<Self> {
ConfigBuilder::default()
.cli_overrides(cli_overrides)
.loader_overrides(loader_overrides)
.build()
.await
}
/// Load a default configuration when user config files are invalid.
pub async fn load_default_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
@@ -1212,13 +1234,23 @@ pub async fn load_config_as_toml_with_cli_and_loader_overrides(
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
) -> std::io::Result<ConfigToml> {
load_config_as_toml_with_cli_and_load_options(codex_home, cwd, cli_overrides, loader_overrides)
.await
}
pub async fn load_config_as_toml_with_cli_and_load_options(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
options: impl Into<ConfigLoadOptions>,
) -> std::io::Result<ConfigToml> {
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home,
cwd.cloned(),
&cli_overrides,
loader_overrides,
options,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)