diff --git a/codex-rs/app-server/src/config_manager_service.rs b/codex-rs/app-server/src/config_manager_service.rs index 3927fc7b8a..a222d3d598 100644 --- a/codex-rs/app-server/src/config_manager_service.rs +++ b/codex-rs/app-server/src/config_manager_service.rs @@ -143,7 +143,10 @@ impl ConfigManager { .map_err(|err| ConfigManagerError::json("failed to deserialize configuration", err))?; let mut origins = layers.origins(); - origins.retain(|path, _| { + origins.retain(|path, metadata| { + if matches!(&metadata.name, ConfigLayerSource::PackagedDefaults { .. }) { + return false; + } let segments = path.split('.').map(str::to_string).collect::>(); layers .requirements_toml() @@ -160,6 +163,9 @@ impl ConfigManager { layers: params.include_layers.then(|| { layers .all_layers_high_to_low() + .filter(|layer| { + !matches!(&layer.name, ConfigLayerSource::PackagedDefaults { .. }) + }) .map(|layer| config_layer_to_api(layer.as_layer())) .collect() }), @@ -818,10 +824,8 @@ fn compute_override_metadata( effective: &TomlValue, segments: &[String], ) -> Option { - let user_value = match layers.get_active_user_layer() { - Some(user_layer) => value_at_semantic_path(&user_layer.config, segments), - None => return None, - }; + let user_layer = layers.get_active_user_layer()?; + let user_value = value_at_semantic_path(&user_layer.config, segments); let effective_value = value_at_semantic_path(effective, segments); if user_value.is_some() && user_value == effective_value { @@ -833,6 +837,9 @@ fn compute_override_metadata( } let overriding_layer = find_effective_layer(layers, segments)?; + if overriding_layer.name.precedence() <= user_layer.name.precedence() { + return None; + } let message = override_message(&overriding_layer.name); Some(OverriddenMetadata { diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index 6fefac2c2c..216db74b00 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -174,6 +174,29 @@ async fn clear_missing_nested_config_is_noop() -> Result<()> { Ok(()) } +#[tokio::test] +async fn clearing_user_setting_falls_back_to_packaged_default_without_override() -> Result<()> { + let tmp = tempdir()?; + let path = tmp.path().join(CONFIG_TOML_FILE); + std::fs::write(&path, "hide_agent_reasoning = true\n")?; + + let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); + let response = service + .write_value(ConfigValueWriteParams { + file_path: Some(path.display().to_string()), + key_path: "hide_agent_reasoning".to_string(), + value: serde_json::Value::Null, + merge_strategy: MergeStrategy::Replace, + expected_version: None, + }) + .await?; + + assert_eq!(response.status, WriteStatus::Ok); + assert_eq!(response.overridden_metadata, None); + assert_eq!(std::fs::read_to_string(&path)?, ""); + Ok(()) +} + #[tokio::test] async fn clear_user_value_if_matches_clears_matching_value() -> Result<()> { let tmp = tempdir().expect("tempdir"); diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 76d31d7a5c..1b6ef1f9f8 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -270,6 +270,11 @@ sandbox_mode = "workspace-write" profile: None, } ); + assert!( + origins + .values() + .all(|origin| !matches!(&origin.name, ConfigLayerSource::PackagedDefaults { .. })) + ); let layers = layers.expect("layers present"); assert_layers_user_then_optional_system(&layers, user_file)?; diff --git a/codex-rs/config/BUILD.bazel b/codex-rs/config/BUILD.bazel index 2b832782b0..2b54002753 100644 --- a/codex-rs/config/BUILD.bazel +++ b/codex-rs/config/BUILD.bazel @@ -2,5 +2,6 @@ load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "config", + compile_data = ["defaults.toml"], crate_name = "codex_config", ) diff --git a/codex-rs/config/defaults.toml b/codex-rs/config/defaults.toml new file mode 100644 index 0000000000..4983ce1ae3 --- /dev/null +++ b/codex-rs/config/defaults.toml @@ -0,0 +1,17 @@ +# Fixed defaults for packaged Codex clients. +include_permissions_instructions = true +include_apps_instructions = true +include_collaboration_mode_instructions = true +include_environment_context = true +cli_auth_credentials_store = "file" +mcp_oauth_credentials_store = "auto" +project_doc_max_bytes = 32768 +project_doc_fallback_filenames = [] +background_terminal_max_timeout = 300000 +file_opener = "vscode" +hide_agent_reasoning = false +chatgpt_base_url = "https://chatgpt.com/backend-api/" +project_root_markers = [".git"] + +[history] +persistence = "save-all" diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index ceae7853ca..244f7df02f 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -159,12 +159,25 @@ pub async fn load_config_layers_state( ), ) })?; - Some(ConfigLayerEntry::new( + ConfigLayerEntry::new( ConfigLayerSource::PackagedDefaults { file: file.clone() }, resolve_relative_paths_in_config_toml(config, base_dir)?, - )) + ) } else { - None + let file = AbsolutePathBuf::from_absolute_path(std::env::current_exe()?)?; + let raw_toml = include_str!("../../defaults.toml"); + let config = toml::from_str(raw_toml).map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid embedded packaged defaults; this is a Codex build error: {error}"), + ) + })?; + ConfigLayerEntry::new_with_raw_toml( + ConfigLayerSource::PackagedDefaults { file }, + config, + raw_toml.to_owned(), + AbsolutePathBuf::from_absolute_path(codex_home)?, + ) }; let active_user_profile = overrides.user_config_profile.clone(); let ignore_managed_requirements = overrides.ignore_managed_requirements; @@ -250,7 +263,7 @@ pub async fn load_config_layers_state( .map_err(io::Error::other)?; let mut layers = Vec::::new(); - layers.extend(packaged_defaults_layer); + layers.push(packaged_defaults_layer); let cli_overrides_layer = if cli_overrides.is_empty() { None diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 0793850ac1..cc4dfea619 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -831,7 +831,7 @@ async fn managed_goal_token_budget_overrides_user_config() -> anyhow::Result<()> } #[tokio::test] -async fn returns_empty_when_all_layers_missing() { +async fn returns_packaged_defaults_when_other_layers_are_missing() { let tmp = tempdir().expect("tempdir"); let managed_path = tmp.path().join("managed_config.toml"); @@ -865,12 +865,11 @@ async fn returns_empty_when_all_layers_missing() { "expected empty config for user layer when config.toml does not exist" ); - let binding = layers.effective_config(); - let base_table = binding.as_table().expect("base table expected"); - assert!( - base_table.is_empty(), - "expected empty base layer when configs missing" - ); + let packaged_defaults = layers + .layers_low_to_high() + .find(|layer| matches!(layer.name, ConfigLayerSource::PackagedDefaults { .. })) + .expect("packaged defaults layer should always be present"); + assert_eq!(layers.effective_config(), packaged_defaults.config); let num_system_layers = layers .layers_high_to_low() .filter(|layer| matches!(layer.name, ConfigLayerSource::System { .. })) @@ -879,16 +878,6 @@ async fn returns_empty_when_all_layers_missing() { num_system_layers, 1, "system layer should always be present" ); - - #[cfg(not(target_os = "macos"))] - { - let effective = layers.effective_config(); - let table = effective.as_table().expect("top-level table expected"); - assert!( - table.is_empty(), - "expected empty table when configs missing" - ); - } } #[tokio::test] @@ -1004,6 +993,9 @@ async fn includes_thread_config_layers_in_stack() -> anyhow::Result<()> { ConfigLayerSource::System { file: expected_system_config, }, + ConfigLayerSource::PackagedDefaults { + file: AbsolutePathBuf::from_absolute_path(std::env::current_exe()?)?, + }, ] ); assert_eq!( @@ -2322,6 +2314,9 @@ model_provider = "cloud-provider" .map(|layer| layer.name.clone()) .collect::>(), vec![ + ConfigLayerSource::PackagedDefaults { + file: AbsolutePathBuf::from_absolute_path(std::env::current_exe()?)?, + }, ConfigLayerSource::System { file: AbsolutePathBuf::from_absolute_path(&system_config_path)?, },