Compare commits

...

12 Commits

Author SHA1 Message Date
Eric Traut
a1e308b77f More fixes 2026-01-13 13:28:40 -08:00
Eric Traut
3a69275d8f Small refactor improvement 2026-01-13 13:23:09 -08:00
Eric Traut
187b1b6cd1 Inlined the call 2026-01-13 13:17:03 -08:00
Eric Traut
f12b5d219b Changed handling of invalid enums to be more consistent with handling of other (non-enum) invalid values. 2026-01-13 13:13:06 -08:00
Eric Traut
2e3a7782f7 Merge remote-tracking branch 'origin/main' into etraut/enum_crash
# Conflicts:
#	codex-rs/core/src/config/mod.rs
2026-01-13 12:57:06 -08:00
Eric Traut
56f86f560a Added comment 2026-01-12 14:56:55 -08:00
Eric Traut
237e916ca6 Tried another approach — a lenient decoder 2026-01-12 14:53:16 -08:00
Eric Traut
55540dde14 Switched to serde to validate (and fall back to default) if enum value is invalid 2026-01-12 14:02:34 -08:00
Eric Traut
d6b38d80dd Simplified change 2026-01-12 13:22:12 -08:00
Eric Traut
30a26ce7b6 Removed closest_value logic 2026-01-12 13:14:58 -08:00
Eric Traut
7baa1870b0 Clippy fix 2026-01-12 12:13:37 -08:00
Eric Traut
cffae61594 Avoid crash on invalid enum values in config.toml
This fixes bug #9104 which results in a panic when config.toml is parsed and one or more enum values are incorrect. Rather than relying on serde to enforce values (and panic if they're incorrect), this change adds a soft failure that falls back to default values.

Ideally, we should report errors like this to the user (for all code paths that parse the config — including the TUI, exec, app server, MCP server, etc.), but that's a much larger change. For this PR, I'm sticking with the current strategy of silently falling back to defaults when there's a bad config value.
2026-01-12 11:57:39 -08:00
6 changed files with 51 additions and 13 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1328,6 +1328,7 @@ dependencies = [
"seccompiler",
"serde",
"serde_json",
"serde_path_to_error",
"serde_yaml",
"serial_test",
"sha1",

View File

@@ -188,6 +188,7 @@ seccompiler = "0.5.0"
sentry = "0.46.0"
serde = "1"
serde_json = "1"
serde_path_to_error = "0.1"
serde_with = "3.16"
serde_yaml = "0.9"
serial_test = "3.2.0"

View File

@@ -64,6 +64,7 @@ reqwest = { workspace = true, features = ["json", "stream"] }
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

@@ -435,11 +435,8 @@ impl ConfigBuilder {
// Note that each layer in ConfigLayerStack should have resolved
// relative paths to absolute paths based on the parent folder of the
// respective config file, so we should be safe to deserialize without
// AbsolutePathBufGuard here.
let config_toml: ConfigToml = merged_toml
.try_into()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
// respective config file, so the base-dir guard is not needed here.
let config_toml = deserialize_config_toml_with_base(merged_toml, None)?;
Config::load_config_with_layer_stack(
config_toml,
harness_overrides,
@@ -496,7 +493,7 @@ pub async fn load_config_as_toml_with_cli_overrides(
.await?;
let merged_toml = config_layer_stack.effective_config();
let cfg = deserialize_config_toml_with_base(merged_toml, codex_home).map_err(|e| {
let cfg = deserialize_config_toml_with_base(merged_toml, Some(codex_home)).map_err(|e| {
tracing::error!("Failed to deserialize overridden config: {e}");
e
})?;
@@ -506,14 +503,13 @@ pub async fn load_config_as_toml_with_cli_overrides(
fn deserialize_config_toml_with_base(
root_value: TomlValue,
config_base_dir: &Path,
config_base_dir: Option<&Path>,
) -> std::io::Result<ConfigToml> {
// This guard ensures that any relative paths that is deserialized into an
// [AbsolutePathBuf] is resolved against `config_base_dir`.
let _guard = AbsolutePathBufGuard::new(config_base_dir);
root_value
.try_into()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
let _guard = config_base_dir.map(AbsolutePathBufGuard::new);
serde_path_to_error::deserialize(root_value)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err.into_inner()))
}
fn filter_mcp_servers_by_requirements(
@@ -764,12 +760,14 @@ pub struct ConfigToml {
pub model_auto_compact_token_limit: Option<i64>,
/// Default approval policy for executing commands.
#[serde(default)]
pub approval_policy: Option<AskForApproval>,
#[serde(default)]
pub shell_environment_policy: ShellEnvironmentPolicyToml,
/// Sandbox mode to use.
#[serde(default)]
pub sandbox_mode: Option<SandboxMode>,
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
@@ -848,6 +846,7 @@ pub struct ConfigToml {
/// Optional URI-based file opener. If set, citations to files in the model
/// output will be hyperlinked using the specified URI scheme.
#[serde(default)]
pub file_opener: Option<UriBasedFileOpener>,
/// Collection of settings that are specific to the TUI.
@@ -861,9 +860,12 @@ pub struct ConfigToml {
/// Defaults to `false`.
pub show_raw_agent_reasoning: Option<bool>,
#[serde(default)]
pub model_reasoning_effort: Option<ReasoningEffort>,
#[serde(default)]
pub model_reasoning_summary: Option<ReasoningSummary>,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
#[serde(default)]
pub model_verbosity: Option<Verbosity>,
/// Override to force-enable reasoning summaries for the configured model.
@@ -957,6 +959,7 @@ impl From<ConfigToml> for UserSavedConfig {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ProjectConfig {
#[serde(default)]
pub trust_level: Option<TrustLevel>,
}
@@ -1749,6 +1752,30 @@ persistence = "none"
);
}
#[tokio::test]
async fn invalid_reasoning_effort_is_rejected() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
tokio::fs::write(
&config_path,
r#"
model_reasoning_effort = "super-high"
"#,
)
.await?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await;
let error = config.expect_err("invalid enum values should reject config");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
Ok(())
}
#[test]
fn tui_config_missing_notifications_field_defaults_to_enabled() {
let cfg = r#"
@@ -2363,7 +2390,7 @@ trust_level = "trusted"
load_config_layers_state(codex_home.path(), Some(cwd), &Vec::new(), overrides).await?;
let cfg = deserialize_config_toml_with_base(
config_layer_stack.effective_config(),
codex_home.path(),
Some(codex_home.path()),
)
.map_err(|e| {
tracing::error!("Failed to deserialize overridden config: {e}");
@@ -2490,7 +2517,7 @@ trust_level = "trusted"
let cfg = deserialize_config_toml_with_base(
config_layer_stack.effective_config(),
codex_home.path(),
Some(codex_home.path()),
)
.map_err(|e| {
tracing::error!("Failed to deserialize overridden config: {e}");

View File

@@ -18,10 +18,15 @@ pub struct ConfigProfile {
/// The key in the `model_providers` map identifying the
/// [`ModelProviderInfo`] to use.
pub model_provider: Option<String>,
#[serde(default)]
pub approval_policy: Option<AskForApproval>,
#[serde(default)]
pub sandbox_mode: Option<SandboxMode>,
#[serde(default)]
pub model_reasoning_effort: Option<ReasoningEffort>,
#[serde(default)]
pub model_reasoning_summary: Option<ReasoningSummary>,
#[serde(default)]
pub model_verbosity: Option<Verbosity>,
pub chatgpt_base_url: Option<String>,
pub experimental_instructions_file: Option<AbsolutePathBuf>,

View File

@@ -352,9 +352,11 @@ pub struct OtelConfigToml {
pub environment: Option<String>,
/// Optional log exporter
#[serde(default)]
pub exporter: Option<OtelExporterKind>,
/// Optional trace exporter
#[serde(default)]
pub trace_exporter: Option<OtelExporterKind>,
}
@@ -624,6 +626,7 @@ pub enum ShellEnvironmentPolicyInherit {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ShellEnvironmentPolicyToml {
#[serde(default)]
pub inherit: Option<ShellEnvironmentPolicyInherit>,
pub ignore_default_excludes: Option<bool>,