Support packaged defaults in config layering (#37810)

## What changed

- Load an optional package-supplied config file as the lowest-precedence
  configuration layer, so every existing layer can override its values.
- Report the layer as `packagedDefaults`, including its source path, through
  config diagnostics and the app-server protocol.
- Return an error when a configured packaged defaults file is missing.

## Testing

- Cover precedence across packaged, system, user, and session layers.
- Cover the missing-file error path.

GitOrigin-RevId: 52406890f4d580eedabc90bf2a9ee3affa36fee1
This commit is contained in:
jif
2026-08-10 11:22:55 +00:00
committed by copyberry
parent 09f47c8785
commit 34ecac1f2b
19 changed files with 283 additions and 6 deletions
@@ -8509,6 +8509,32 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Default configuration supplied with the installed Codex package.",
"properties": {
"file": {
"allOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
}
],
"description": "Path to the packaged default configuration file."
},
"type": {
"enum": [
"packagedDefaults"
],
"title": "PackagedDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"type"
],
"title": "PackagedDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {
@@ -4674,6 +4674,32 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Default configuration supplied with the installed Codex package.",
"properties": {
"file": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Path to the packaged default configuration file."
},
"type": {
"enum": [
"packagedDefaults"
],
"title": "PackagedDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"type"
],
"title": "PackagedDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {
@@ -477,6 +477,32 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Default configuration supplied with the installed Codex package.",
"properties": {
"file": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Path to the packaged default configuration file."
},
"type": {
"enum": [
"packagedDefaults"
],
"title": "PackagedDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"type"
],
"title": "PackagedDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {
@@ -22,6 +22,32 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Default configuration supplied with the installed Codex package.",
"properties": {
"file": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Path to the packaged default configuration file."
},
"type": {
"enum": [
"packagedDefaults"
],
"title": "PackagedDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"type"
],
"title": "PackagedDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {
@@ -3,7 +3,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type ConfigLayerSource = { "type": "mdm", domain: string, key: string, } | { "type": "system",
export type ConfigLayerSource = { "type": "packagedDefaults",
/**
* Path to the packaged default configuration file.
*/
file: AbsolutePathBuf, } | { "type": "mdm", domain: string, key: string, } | { "type": "system",
/**
* This is the path to the system config.toml file, though it is not
* guaranteed to exist.
@@ -27,6 +27,14 @@ use std::path::PathBuf;
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum ConfigLayerSource {
/// Default configuration supplied with the installed Codex package.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
PackagedDefaults {
/// Path to the packaged default configuration file.
file: AbsolutePathBuf,
},
/// Managed preferences layer delivered by MDM (macOS only).
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
@@ -102,6 +110,7 @@ impl ConfigLayerSource {
/// from a layer with a lower precedence.
pub fn precedence(&self) -> i16 {
match self {
ConfigLayerSource::PackagedDefaults { .. } => -10,
ConfigLayerSource::Mdm { .. } => 0,
ConfigLayerSource::System { .. } => 10,
ConfigLayerSource::EnterpriseManaged { .. } => 15,
+1 -1
View File
@@ -272,7 +272,7 @@ Example with notification opt-out:
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
- `config/read` — fetch the runtime-effective config after resolving config layering and managed requirements, including opaque `desktop` values stored in `config.toml`.
- `config/read` — fetch the runtime-effective config after resolving config layering and managed requirements, including opaque `desktop` values stored in `config.toml`. When configured, the `packagedDefaults` layer has the lowest precedence.
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome`, optional `cwds`, and an optional `migrationSource` selector. Omitted, `null`, or unrecognized migration-source values retain the default behavior. The deprecated optional `source` field remains accepted for compatibility but does not select the migration source. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, memory, session metadata, or other artifact names. The response also includes connector candidates inferred from detected source sessions, with a normalized display `name`, the number of detected sessions that used the connector, and the source metadata field used for detection.
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Pass the same optional `migrationSource` used for detection so the server reads from the matching source; omitted, `null`, or unrecognized values retain the default behavior. The optional `source` identifies the product that initiated the import, while the optional opaque `providerId` attributes analytics to the provider selected by that product without affecting migration-source selection. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
- `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Successful session entries include the original imported title when one was available. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection.
+3
View File
@@ -13,6 +13,9 @@ use codex_config::ConfigLayerSource;
/// function instead of a `From` implementation.
pub(crate) fn config_layer_source_to_api(source: ConfigLayerSource) -> ApiConfigLayerSource {
match source {
ConfigLayerSource::PackagedDefaults { file } => {
ApiConfigLayerSource::PackagedDefaults { file }
}
ConfigLayerSource::Mdm { domain, key } => ApiConfigLayerSource::Mdm { domain, key },
ConfigLayerSource::System { file } => ApiConfigLayerSource::System { file },
ConfigLayerSource::EnterpriseManaged { id, name } => {
@@ -781,6 +781,9 @@ fn value_at_semantic_path<'a>(root: &'a TomlValue, segments: &[String]) -> Optio
fn override_message(layer: &ConfigLayerSource) -> String {
match layer {
ConfigLayerSource::PackagedDefaults { file } => {
format!("Overridden by packaged defaults: {}", file.display())
}
ConfigLayerSource::Mdm { domain, key: _ } => {
format!("Overridden by managed policy (MDM): {domain}")
}
@@ -4,6 +4,8 @@ use serde_json::Value as JsonValue;
/// Provenance for one layer in the effective Codex configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigLayerSource {
/// Default configuration supplied with the installed Codex package.
PackagedDefaults { file: AbsolutePathBuf },
/// Managed preferences delivered by MDM.
Mdm { domain: String, key: String },
/// Host-wide configuration loaded from a file.
@@ -30,6 +32,7 @@ impl ConfigLayerSource {
/// from a layer with a lower precedence.
pub fn precedence(&self) -> i16 {
match self {
ConfigLayerSource::PackagedDefaults { .. } => -10,
ConfigLayerSource::Mdm { .. } => 0,
ConfigLayerSource::System { .. } => 10,
ConfigLayerSource::EnterpriseManaged { .. } => 15,
@@ -74,6 +77,9 @@ pub struct ConfigLayer {
pub fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file: &str) -> String {
match source {
ConfigLayerSource::PackagedDefaults { file } => {
format!("packaged defaults ({})", file.as_path().display())
}
ConfigLayerSource::Mdm { domain, key } => {
format!("MDM ({domain}:{key})")
}
+1
View File
@@ -244,6 +244,7 @@ where
fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Option<PathBuf> {
match &layer.name {
ConfigLayerSource::PackagedDefaults { file } => Some(file.to_path_buf()),
ConfigLayerSource::System { file } => Some(file.to_path_buf()),
ConfigLayerSource::User { file, .. } => Some(file.to_path_buf()),
ConfigLayerSource::Project { dot_codex_folder } => {
+32
View File
@@ -101,6 +101,7 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
///
/// Configuration is built up from multiple layers in the following order:
///
/// - package: optional default configuration supplied with the Codex package
/// - admin: managed preferences (*)
/// - system `/etc/codex/config.toml` (Unix) or
/// `%ProgramData%\OpenAI\Codex\config.toml` (Windows)
@@ -134,6 +135,36 @@ pub async fn load_config_layers_state(
strict_config,
cloud_config_bundle,
} = options.into();
let packaged_defaults_layer = if let Some(file) = &overrides.packaged_defaults_path {
let config = layer_io::read_config_from_path(
fs,
file,
/*log_missing_as_info*/ false,
strict_config,
)
.await?
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("packaged defaults config file {} not found", file.display()),
)
})?;
let base_dir = file.as_path().parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"packaged defaults config file {} has no parent directory",
file.display()
),
)
})?;
Some(ConfigLayerEntry::new(
ConfigLayerSource::PackagedDefaults { file: file.clone() },
resolve_relative_paths_in_config_toml(config, base_dir)?,
))
} else {
None
};
let active_user_profile = overrides.user_config_profile.clone();
let ignore_managed_requirements = overrides.ignore_managed_requirements;
let ignore_user_config = overrides.ignore_user_config;
@@ -218,6 +249,7 @@ pub async fn load_config_layers_state(
.map_err(io::Error::other)?;
let mut layers = Vec::<ConfigLayerEntry>::new();
layers.extend(packaged_defaults_layer);
let cli_overrides_layer = if cli_overrides.is_empty() {
None
+106
View File
@@ -115,6 +115,112 @@ impl ExecutorFileSystem for TestFileSystem {
}
}
#[tokio::test]
async fn packaged_defaults_have_lower_precedence_than_existing_config_layers() {
let tmp = tempdir().expect("tempdir");
let packaged_defaults_path =
AbsolutePathBuf::resolve_path_against_base("packaged-defaults.toml", tmp.path());
let system_config_path = tmp.path().join("system.toml");
let user_config_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(
packaged_defaults_path.as_path(),
r#"
model = "packaged-model"
model_provider = "packaged-provider"
model_context_window = 120000
"#,
)
.expect("write packaged defaults");
std::fs::write(
&system_config_path,
r#"
model = "system-model"
model_provider = "system-provider"
"#,
)
.expect("write system config");
std::fs::write(&user_config_path, r#"model = "user-model""#).expect("write user config");
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.packaged_defaults_path = Some(packaged_defaults_path.clone());
overrides.system_config_path = Some(system_config_path.clone());
let stack = load_config_layers_state(
&TestFileSystem,
tmp.path(),
/*cwd*/ None,
&[(
"model".to_string(),
TomlValue::String("session-model".to_string()),
)],
overrides,
&crate::NoopThreadConfigLoader,
)
.await
.expect("load config layers");
assert_eq!(
stack
.all_layers_low_to_high()
.map(|layer| layer.name.clone())
.collect::<Vec<_>>(),
vec![
ConfigLayerSource::PackagedDefaults {
file: packaged_defaults_path,
},
ConfigLayerSource::System {
file: AbsolutePathBuf::from_absolute_path(system_config_path)
.expect("absolute system config path"),
},
ConfigLayerSource::User {
file: AbsolutePathBuf::from_absolute_path(user_config_path)
.expect("absolute user config path"),
profile: None,
},
ConfigLayerSource::SessionFlags,
]
);
assert_eq!(
stack.effective_config(),
toml::toml! {
model = "session-model"
model_provider = "system-provider"
model_context_window = 120000
}
.into()
);
}
#[tokio::test]
async fn missing_packaged_defaults_file_returns_an_error() {
let tmp = tempdir().expect("tempdir");
let packaged_defaults_path =
AbsolutePathBuf::resolve_path_against_base("packaged-defaults.toml", tmp.path());
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.packaged_defaults_path = Some(packaged_defaults_path.clone());
let err = load_config_layers_state(
&TestFileSystem,
tmp.path(),
/*cwd*/ None,
&[],
overrides,
&crate::NoopThreadConfigLoader,
)
.await
.expect_err("an explicitly configured packaged defaults file must exist");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
assert_eq!(
err.to_string(),
format!(
"packaged defaults config file {} not found",
packaged_defaults_path.display()
)
);
}
#[tokio::test]
async fn profile_v2_rejects_matching_legacy_profile_in_base_user_config() {
let tmp = tempdir().expect("tempdir");
+4
View File
@@ -41,6 +41,8 @@ impl From<LoaderOverrides> for ConfigLoadOptions {
/// LoaderOverrides overrides managed configuration inputs (primarily for tests).
#[derive(Debug, Default, Clone)]
pub struct LoaderOverrides {
/// Optional configuration file supplied with the installed Codex package.
pub packaged_defaults_path: Option<AbsolutePathBuf>,
pub user_config_path: Option<AbsolutePathBuf>,
pub user_config_profile: Option<ProfileV2Name>,
pub managed_config_path: Option<PathBuf>,
@@ -64,6 +66,7 @@ impl LoaderOverrides {
pub fn without_managed_config_for_tests() -> Self {
let base = std::env::temp_dir().join("codex-config-tests");
Self {
packaged_defaults_path: None,
user_config_path: None,
user_config_profile: None,
managed_config_path: Some(base.join("managed_config.toml")),
@@ -212,6 +215,7 @@ impl ConfigLayerEntry {
// Get the `.codex/` folder associated with this config layer, if any.
pub fn config_folder(&self) -> Option<AbsolutePathBuf> {
match &self.name {
ConfigLayerSource::PackagedDefaults { .. } => None,
ConfigLayerSource::Mdm { .. } => None,
ConfigLayerSource::System { file } => file.parent(),
ConfigLayerSource::EnterpriseManaged { .. } => None,
+2 -1
View File
@@ -117,7 +117,8 @@ fn roots_from_layer_stack(
SkillScope::Admin,
));
}
ConfigLayerSource::Mdm { .. }
ConfigLayerSource::PackagedDefaults { .. }
| ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
+3 -1
View File
@@ -375,7 +375,8 @@ fn load_toml_hooks_from_layer(
fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf {
match &layer.name {
ConfigLayerSource::System { file }
ConfigLayerSource::PackagedDefaults { file }
| ConfigLayerSource::System { file }
| ConfigLayerSource::User { file, .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => file.clone(),
ConfigLayerSource::Project { dot_codex_folder } => layer
@@ -716,6 +717,7 @@ fn hook_trusted_hash(is_managed: bool, state: Option<&HookStateToml>) -> Option<
fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSource, bool) {
match source {
ConfigLayerSource::PackagedDefaults { .. } => (HookSource::Unknown, false),
ConfigLayerSource::System { .. } => (HookSource::System, true),
ConfigLayerSource::User { .. } => (HookSource::User, false),
ConfigLayerSource::Project { .. } => (HookSource::Project, false),
+4 -2
View File
@@ -404,7 +404,8 @@ fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>>
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
ConfigLayerSource::System { .. }
ConfigLayerSource::PackagedDefaults { .. }
| ConfigLayerSource::System { .. }
| ConfigLayerSource::User { .. }
| ConfigLayerSource::Project { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => Vec::new(),
@@ -467,7 +468,8 @@ fn non_file_layer_value_label(source: &ConfigLayerSource) -> &'static str {
"MDM value"
}
ConfigLayerSource::EnterpriseManaged { .. } => "Enterprise-managed config value",
ConfigLayerSource::SessionFlags
ConfigLayerSource::PackagedDefaults { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::System { .. }
| ConfigLayerSource::User { .. }
| ConfigLayerSource::Project { .. }