mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Embed defaults in the packaged config layer (#38179)
## What changed - Add an embedded `defaults.toml` and always install it as the lowest-precedence configuration layer when no packaged-defaults path is supplied. - Keep packaged defaults out of config RPC layer and origin metadata. - Report a user setting as overridden only when the effective layer has higher precedence, so clearing a setting can fall back to its packaged default without a false override. ## Testing - Cover loading the packaged layer when other configuration files are absent. - Cover config RPC filtering and clearing a user value that falls back to a packaged default. GitOrigin-RevId: c1c0e1e681e4ac1be2602550972533443c5df51b
This commit is contained in:
@@ -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::<Vec<_>>();
|
||||
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<OverriddenMetadata> {
|
||||
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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -2,5 +2,6 @@ load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "config",
|
||||
compile_data = ["defaults.toml"],
|
||||
crate_name = "codex_config",
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
@@ -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::<ConfigLayerEntry>::new();
|
||||
layers.extend(packaged_defaults_layer);
|
||||
layers.push(packaged_defaults_layer);
|
||||
|
||||
let cli_overrides_layer = if cli_overrides.is_empty() {
|
||||
None
|
||||
|
||||
@@ -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<_>>(),
|
||||
vec![
|
||||
ConfigLayerSource::PackagedDefaults {
|
||||
file: AbsolutePathBuf::from_absolute_path(std::env::current_exe()?)?,
|
||||
},
|
||||
ConfigLayerSource::System {
|
||||
file: AbsolutePathBuf::from_absolute_path(&system_config_path)?,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user