Compare commits

...

9 Commits

Author SHA1 Message Date
Felix Xia
72e650fbe0 Refine optional cloud config bundle failures 2026-06-03 16:19:02 +01:00
Felix Xia
1de0c7aa0d Address product defaults review feedback 2026-06-03 16:04:04 +01:00
Felix Xia
a81cbfcbc5 Merge remote-tracking branch 'origin/main' into codex/product-default-layer-plugin-sharing 2026-06-03 10:30:53 +01:00
Felix Xia
6d6412020a Name backend config loader pair 2026-06-03 10:24:06 +01:00
Felix Xia
d040311ca4 Fix delivered config test fixture 2026-06-02 23:06:18 +01:00
Felix Xia
957063a6de Merge remote-tracking branch 'origin/main' into codex/product-default-layer-plugin-sharing
# Conflicts:
#	codex-rs/app-server-client/src/lib.rs
#	codex-rs/app-server/src/config_manager.rs
#	codex-rs/app-server/src/in_process.rs
#	codex-rs/app-server/src/mcp_refresh.rs
#	codex-rs/app-server/src/message_processor_tracing_tests.rs
#	codex-rs/app-server/src/request_processors/thread_processor_tests.rs
#	codex-rs/app-server/tests/suite/conversation_summary.rs
#	codex-rs/app-server/tests/suite/v2/mcp_resource.rs
#	codex-rs/app-server/tests/suite/v2/remote_thread_store.rs
#	codex-rs/app-server/tests/suite/v2/thread_read.rs
#	codex-rs/app-server/tests/suite/v2/thread_unarchive.rs
#	codex-rs/cloud-config/src/lib.rs
#	codex-rs/config/src/state.rs
#	codex-rs/exec/src/lib.rs
#	codex-rs/tui/src/lib.rs
#	codex-rs/tui/src/session_archive_commands.rs
2026-06-02 22:51:26 +01:00
Felix Xia
138b4174d7 Rename product default layer to product defaults 2026-06-02 16:26:19 +01:00
Felix Xia
d881af7e4f Test plugin sharing requirement override 2026-06-02 15:54:32 +01:00
Felix Xia
9dcf6cf3ca Add product default layer for plugin sharing 2026-06-02 13:16:55 +01:00
47 changed files with 960 additions and 101 deletions

View File

@@ -46,6 +46,7 @@ use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudConfigBundleLoader;
use codex_config::LoaderOverrides;
use codex_config::NoopThreadConfigLoader;
use codex_config::ProductDefaultsLoader;
use codex_config::RemoteThreadConfigLoader;
use codex_config::ThreadConfigLoader;
use codex_core::config::Config;
@@ -339,6 +340,8 @@ pub struct InProcessClientStartArgs {
pub loader_overrides: LoaderOverrides,
/// Whether config API paths should reject unknown config fields.
pub strict_config: bool,
/// Product-owned default config layer.
pub product_defaults: ProductDefaultsLoader,
/// Preloaded cloud config bundle provider.
pub cloud_config_bundle: CloudConfigBundleLoader,
/// Feedback sink used by app-server/core telemetry and logs.
@@ -406,6 +409,7 @@ impl InProcessClientStartArgs {
cli_overrides: self.cli_overrides,
loader_overrides: self.loader_overrides,
strict_config: self.strict_config,
product_defaults: self.product_defaults,
cloud_config_bundle: self.cloud_config_bundle,
thread_config_loader,
feedback: self.feedback,
@@ -1035,6 +1039,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
product_defaults: ProductDefaultsLoader::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
@@ -2199,6 +2204,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
product_defaults: ProductDefaultsLoader::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
@@ -2240,6 +2246,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
product_defaults: ProductDefaultsLoader::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,

View File

@@ -7570,6 +7570,23 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Product-owned default config supplied by OpenAI. This is lower precedence than all customer/admin/user config layers.",
"properties": {
"type": {
"enum": [
"productDefaults"
],
"title": "ProductDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ProductDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {

View File

@@ -3932,6 +3932,23 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Product-owned default config supplied by OpenAI. This is lower precedence than all customer/admin/user config layers.",
"properties": {
"type": {
"enum": [
"productDefaults"
],
"title": "ProductDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ProductDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {

View File

@@ -457,6 +457,23 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Product-owned default config supplied by OpenAI. This is lower precedence than all customer/admin/user config layers.",
"properties": {
"type": {
"enum": [
"productDefaults"
],
"title": "ProductDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ProductDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {

View File

@@ -22,6 +22,23 @@
},
"ConfigLayerSource": {
"oneOf": [
{
"description": "Product-owned default config supplied by OpenAI. This is lower precedence than all customer/admin/user config layers.",
"properties": {
"type": {
"enum": [
"productDefaults"
],
"title": "ProductDefaultsConfigLayerSourceType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ProductDefaultsConfigLayerSource",
"type": "object"
},
{
"description": "Managed preferences layer delivered by MDM (macOS only).",
"properties": {

View File

@@ -3,7 +3,7 @@
// 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": "productDefaults" } | { "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.

View File

@@ -26,6 +26,10 @@ use ts_rs::TS;
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum ConfigLayerSource {
/// Product-owned default config supplied by OpenAI. This is lower
/// precedence than all customer/admin/user config layers.
ProductDefaults,
/// Managed preferences layer delivered by MDM (macOS only).
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
@@ -101,6 +105,7 @@ impl ConfigLayerSource {
/// from a layer with a lower precedence.
pub fn precedence(&self) -> i16 {
match self {
ConfigLayerSource::ProductDefaults => -10,
ConfigLayerSource::Mdm { .. } => 0,
ConfigLayerSource::System { .. } => 10,
ConfigLayerSource::EnterpriseManaged { .. } => 15,

View File

@@ -225,7 +225,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 effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`.
- `config/read` — fetch the effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`. Returned config layers may include `productDefaults`, an OpenAI-supplied defaults layer with lower precedence than customer/admin/user config.
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home), and plugin/session migration items may additionally include structured `details` grouping plugin ids or session metadata.
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish).
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.

View File

@@ -1,8 +1,10 @@
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_config::cloud_config_bundle_loader;
use codex_cloud_config::product_defaults_loader;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLayerStack;
use codex_config::LoaderOverrides;
use codex_config::ProductDefaultsLoader;
use codex_config::ThreadConfigLoader;
use codex_config::loader::load_config_layers_state;
use codex_core::config::Config;
@@ -32,20 +34,34 @@ pub(crate) struct ConfigManager {
loader_overrides: LoaderOverrides,
strict_config: bool,
cloud_config_bundle: Arc<RwLock<CloudConfigBundleLoader>>,
product_defaults: Arc<RwLock<ProductDefaultsLoader>>,
arg0_paths: Arg0DispatchPaths,
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
}
pub(crate) struct ConfigManagerOptions {
pub(crate) codex_home: PathBuf,
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
pub(crate) loader_overrides: LoaderOverrides,
pub(crate) strict_config: bool,
pub(crate) cloud_config_bundle: CloudConfigBundleLoader,
pub(crate) product_defaults: ProductDefaultsLoader,
pub(crate) arg0_paths: Arg0DispatchPaths,
pub(crate) thread_config_loader: Arc<dyn ThreadConfigLoader>,
}
impl ConfigManager {
pub(crate) fn new(
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
strict_config: bool,
cloud_config_bundle: CloudConfigBundleLoader,
arg0_paths: Arg0DispatchPaths,
thread_config_loader: Arc<dyn ThreadConfigLoader>,
) -> Self {
pub(crate) fn new(options: ConfigManagerOptions) -> Self {
let ConfigManagerOptions {
codex_home,
cli_overrides,
loader_overrides,
strict_config,
cloud_config_bundle,
product_defaults,
arg0_paths,
thread_config_loader,
} = options;
Self {
codex_home,
cli_overrides: Arc::new(RwLock::new(cli_overrides)),
@@ -53,6 +69,7 @@ impl ConfigManager {
loader_overrides,
strict_config,
cloud_config_bundle: Arc::new(RwLock::new(cloud_config_bundle)),
product_defaults: Arc::new(RwLock::new(product_defaults)),
arg0_paths,
thread_config_loader: Arc::new(RwLock::new(thread_config_loader)),
}
@@ -80,6 +97,13 @@ impl ConfigManager {
.unwrap_or_default()
}
pub(crate) fn current_product_defaults(&self) -> ProductDefaultsLoader {
self.product_defaults
.read()
.map(|guard| guard.clone())
.unwrap_or_default()
}
pub(crate) fn extend_runtime_feature_enablement<I>(&self, enablement: I) -> Result<(), ()>
where
I: IntoIterator<Item = (String, bool)>,
@@ -97,11 +121,17 @@ impl ConfigManager {
) {
let loader =
cloud_config_bundle_loader(auth_manager, chatgpt_base_url, self.codex_home.clone());
let product_defaults = product_defaults_loader(loader.clone());
if let Ok(mut guard) = self.cloud_config_bundle.write() {
*guard = loader;
} else {
warn!("failed to update cloud config bundle loader");
}
if let Ok(mut guard) = self.product_defaults.write() {
*guard = product_defaults;
} else {
warn!("failed to update product defaults");
}
}
pub(crate) fn replace_thread_config_loader(
@@ -163,11 +193,13 @@ impl ConfigManager {
}
pub(crate) async fn load_default_config(&self) -> std::io::Result<Config> {
let mut config = Config::load_default_with_cli_overrides_for_codex_home(
self.codex_home.clone(),
self.current_cli_overrides(),
)
.await?;
let mut config =
Config::load_default_with_cli_overrides_for_codex_home_and_product_defaults(
self.codex_home.clone(),
self.current_cli_overrides(),
self.current_product_defaults(),
)
.await?;
if self.loader_overrides.user_config_path.is_some()
|| self.loader_overrides.user_config_profile.is_some()
{
@@ -245,6 +277,7 @@ impl ConfigManager {
.strict_config(self.strict_config)
.harness_overrides(typesafe_overrides)
.fallback_cwd(fallback_cwd)
.product_defaults_loader(self.current_product_defaults())
.cloud_config_bundle(self.current_cloud_config_bundle())
.thread_config_loader(self.current_thread_config_loader())
.build()
@@ -273,6 +306,7 @@ impl ConfigManager {
&self.current_cli_overrides(),
codex_config::ConfigLoadOptions {
loader_overrides: self.loader_overrides.clone(),
product_defaults: self.current_product_defaults(),
strict_config: self.strict_config,
cloud_config_bundle: self.current_cloud_config_bundle(),
},
@@ -305,15 +339,16 @@ impl ConfigManager {
loader_overrides: LoaderOverrides,
cloud_config_bundle: CloudConfigBundleLoader,
) -> Self {
Self::new(
Self::new(ConfigManagerOptions {
codex_home,
cli_overrides,
loader_overrides,
/*strict_config*/ false,
strict_config: false,
cloud_config_bundle,
Arg0DispatchPaths::default(),
Arc::new(codex_config::NoopThreadConfigLoader),
)
product_defaults: ProductDefaultsLoader::default(),
arg0_paths: Arg0DispatchPaths::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
})
}
#[cfg(test)]

View File

@@ -618,6 +618,7 @@ fn value_at_path<'a>(root: &'a TomlValue, segments: &[String]) -> Option<&'a Tom
fn override_message(layer: &ConfigLayerSource) -> String {
match layer {
ConfigLayerSource::ProductDefaults => "Resolved from product defaults".to_string(),
ConfigLayerSource::Mdm { domain, key: _ } => {
format!("Overridden by managed policy (MDM): {domain}")
}

View File

@@ -52,6 +52,7 @@ use std::time::Duration;
use crate::analytics_utils::analytics_events_client_from_config;
use crate::config_manager::ConfigManager;
use crate::config_manager::ConfigManagerOptions;
use crate::error_code::OVERLOADED_ERROR_CODE;
use crate::error_code::internal_error;
use crate::error_code::invalid_request;
@@ -79,6 +80,7 @@ use codex_app_server_protocol::ServerRequest;
use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudConfigBundleLoader;
use codex_config::LoaderOverrides;
use codex_config::ProductDefaultsLoader;
use codex_config::ThreadConfigLoader;
use codex_core::config::Config;
use codex_core::resolve_installation_id;
@@ -124,6 +126,8 @@ pub struct InProcessStartArgs {
pub loader_overrides: LoaderOverrides,
/// Whether config API paths should reject unknown config fields.
pub strict_config: bool,
/// Product-owned default config layer derived before app-server startup.
pub product_defaults: ProductDefaultsLoader,
/// Preloaded cloud config bundle provider.
pub cloud_config_bundle: CloudConfigBundleLoader,
/// Loader used to fetch typed thread config sources before a thread starts.
@@ -410,15 +414,16 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
});
let processor_outgoing = Arc::clone(&outgoing_message_sender);
let config_manager = ConfigManager::new(
args.config.codex_home.to_path_buf(),
args.cli_overrides,
args.loader_overrides,
args.strict_config,
args.cloud_config_bundle,
args.arg0_paths.clone(),
args.thread_config_loader,
);
let config_manager = ConfigManager::new(ConfigManagerOptions {
codex_home: args.config.codex_home.to_path_buf(),
cli_overrides: args.cli_overrides,
loader_overrides: args.loader_overrides,
strict_config: args.strict_config,
product_defaults: args.product_defaults,
cloud_config_bundle: args.cloud_config_bundle,
arg0_paths: args.arg0_paths.clone(),
thread_config_loader: args.thread_config_loader,
});
let (processor_tx, mut processor_rx) = mpsc::channel::<ProcessorCommand>(channel_capacity);
let mut processor_handle = tokio::spawn(async move {
let processor = Arc::new(MessageProcessor::new(MessageProcessorArgs {
@@ -772,6 +777,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
product_defaults: ProductDefaultsLoader::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),

View File

@@ -20,6 +20,7 @@ use std::sync::atomic::AtomicBool;
use crate::analytics_utils::analytics_events_client_from_config;
use crate::config_manager::ConfigManager;
use crate::config_manager::ConfigManagerOptions;
use crate::message_processor::MessageProcessor;
use crate::message_processor::MessageProcessorArgs;
use crate::outgoing_message::ConnectionId;
@@ -454,15 +455,16 @@ pub async fn run_main_with_transport_options(
}
.map(Arc::new)
.map_err(std::io::Error::other)?;
let config_manager = ConfigManager::new(
codex_home.to_path_buf(),
cli_kv_overrides.clone(),
let config_manager = ConfigManager::new(ConfigManagerOptions {
codex_home: codex_home.to_path_buf(),
cli_overrides: cli_kv_overrides.clone(),
loader_overrides,
strict_config,
Default::default(),
arg0_paths.clone(),
Arc::new(NoopThreadConfigLoader),
);
cloud_config_bundle: Default::default(),
product_defaults: Default::default(),
arg0_paths: arg0_paths.clone(),
thread_config_loader: Arc::new(NoopThreadConfigLoader),
});
match config_manager
.load_latest_config(/*fallback_cwd*/ None)
.await

View File

@@ -1,4 +1,6 @@
use crate::config_manager::ConfigManager;
#[cfg(test)]
use crate::config_manager::ConfigManagerOptions;
use codex_core::CodexThread;
use codex_core::ThreadManager;
use codex_core::config::Config;
@@ -208,15 +210,16 @@ mod tests {
good_loads: AtomicUsize::new(0),
bad_loads: AtomicUsize::new(0),
});
let config_manager = ConfigManager::new(
temp_dir.path().to_path_buf(),
Vec::new(),
LoaderOverrides::without_managed_config_for_tests(),
/*strict_config*/ false,
CloudConfigBundleLoader::default(),
Arg0DispatchPaths::default(),
loader.clone(),
);
let config_manager = ConfigManager::new(ConfigManagerOptions {
codex_home: temp_dir.path().to_path_buf(),
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::without_managed_config_for_tests(),
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
arg0_paths: Arg0DispatchPaths::default(),
thread_config_loader: loader.clone(),
});
Ok((temp_dir, thread_manager, config_manager, loader))
}

View File

@@ -3,6 +3,7 @@ use super::MessageProcessor;
use super::MessageProcessorArgs;
use crate::analytics_utils::analytics_events_client_from_config;
use crate::config_manager::ConfigManager;
use crate::config_manager::ConfigManagerOptions;
use crate::outgoing_message::ConnectionId;
use crate::outgoing_message::OutgoingMessageSender;
use crate::transport::AppServerTransport;
@@ -235,15 +236,16 @@ async fn build_test_processor(
let (outgoing_tx, outgoing_rx) = mpsc::channel(16);
let auth_manager =
AuthManager::shared_from_config(config.as_ref(), /*enable_codex_api_key_env*/ false).await;
let config_manager = ConfigManager::new(
config.codex_home.to_path_buf(),
Vec::new(),
LoaderOverrides::default(),
/*strict_config*/ false,
CloudConfigBundleLoader::default(),
Arg0DispatchPaths::default(),
Arc::new(codex_config::NoopThreadConfigLoader),
);
let config_manager = ConfigManager::new(ConfigManagerOptions {
codex_home: config.codex_home.to_path_buf(),
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
arg0_paths: Arg0DispatchPaths::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
});
let analytics_events_client =
analytics_events_client_from_config(Arc::clone(&auth_manager), config.as_ref());
let outgoing = Arc::new(OutgoingMessageSender::new(

View File

@@ -46,6 +46,7 @@ mod thread_processor_behavior_tests {
}
use super::super::*;
use crate::config_manager::ConfigManagerOptions;
use crate::outgoing_message::OutgoingEnvelope;
use crate::outgoing_message::OutgoingMessage;
use anyhow::Result;
@@ -610,14 +611,15 @@ mod thread_processor_behavior_tests {
requires_openai_auth: false,
supports_websockets: true,
};
let config_manager = ConfigManager::new(
temp_dir.path().to_path_buf(),
Vec::new(),
LoaderOverrides::default(),
/*strict_config*/ false,
CloudConfigBundleLoader::default(),
Arg0DispatchPaths::default(),
Arc::new(StaticThreadConfigLoader::new(vec![
let config_manager = ConfigManager::new(ConfigManagerOptions {
codex_home: temp_dir.path().to_path_buf(),
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
arg0_paths: Arg0DispatchPaths::default(),
thread_config_loader: Arc::new(StaticThreadConfigLoader::new(vec![
ThreadConfigSource::Session(SessionThreadConfig {
model_provider: Some("session".to_string()),
model_providers: HashMap::from([(
@@ -627,7 +629,7 @@ mod thread_processor_behavior_tests {
features: BTreeMap::from([("plugins".to_string(), false)]),
}),
])),
);
});
let config = config_manager
.load_with_overrides(
Some(HashMap::from([

View File

@@ -150,6 +150,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
cli_overrides: Vec::new(),
loader_overrides,
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),

View File

@@ -201,6 +201,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
cli_overrides: Vec::new(),
loader_overrides,
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),

View File

@@ -76,6 +76,7 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste
cli_overrides: Vec::new(),
loader_overrides,
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(NoopThreadConfigLoader),
feedback: CodexFeedback::new(),

View File

@@ -376,6 +376,7 @@ async fn thread_turns_list_reads_store_history_without_rollout_path() -> Result<
cli_overrides: Vec::new(),
loader_overrides,
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
@@ -442,6 +443,7 @@ async fn thread_read_loaded_include_turns_reads_store_history_without_rollout_pa
cli_overrides: Vec::new(),
loader_overrides,
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
@@ -528,6 +530,7 @@ async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()>
cli_overrides: Vec::new(),
loader_overrides,
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),

View File

@@ -247,6 +247,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
cli_overrides: Vec::new(),
loader_overrides,
strict_config: false,
product_defaults: Default::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),

View File

@@ -91,7 +91,15 @@ pub(crate) fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfi
let config_toml = response
.config_toml
.flatten()
.map(|config_toml| *config_toml)
.map(|config_toml| *config_toml);
let product_defaults = config_toml
.as_ref()
.and_then(|config_toml| config_toml.product_defaults.clone().flatten())
.unwrap_or_default()
.into_iter()
.map(config_fragment_from_delivered)
.collect();
let enterprise_managed = config_toml
.and_then(|config_toml| config_toml.enterprise_managed.flatten())
.unwrap_or_default()
.into_iter()
@@ -109,7 +117,8 @@ pub(crate) fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfi
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
enterprise_managed: config_toml,
product_defaults,
enterprise_managed,
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: requirements_toml,

View File

@@ -4,6 +4,7 @@ use crate::service::CloudConfigBundleService;
use codex_config::CloudConfigBundleLoadError;
use codex_config::CloudConfigBundleLoadErrorCode;
use codex_config::CloudConfigBundleLoader;
use codex_config::ProductDefaultsLoader;
use codex_config::types::AuthCredentialsStoreMode;
use codex_login::AuthManager;
use std::path::PathBuf;
@@ -51,12 +52,34 @@ pub fn cloud_config_bundle_loader(
})
}
#[derive(Clone, Debug)]
pub struct BackendConfigLoaders {
pub cloud_config_bundle: CloudConfigBundleLoader,
pub product_defaults: ProductDefaultsLoader,
}
pub async fn cloud_config_bundle_loader_for_storage(
codex_home: PathBuf,
enable_codex_api_key_env: bool,
credentials_store_mode: AuthCredentialsStoreMode,
chatgpt_base_url: String,
) -> CloudConfigBundleLoader {
backend_config_loaders_for_storage(
codex_home,
enable_codex_api_key_env,
credentials_store_mode,
chatgpt_base_url,
)
.await
.cloud_config_bundle
}
pub async fn backend_config_loaders_for_storage(
codex_home: PathBuf,
enable_codex_api_key_env: bool,
credentials_store_mode: AuthCredentialsStoreMode,
chatgpt_base_url: String,
) -> BackendConfigLoaders {
let auth_manager = AuthManager::shared(
codex_home.clone(),
enable_codex_api_key_env,
@@ -64,5 +87,17 @@ pub async fn cloud_config_bundle_loader_for_storage(
Some(chatgpt_base_url.clone()),
)
.await;
cloud_config_bundle_loader(auth_manager, chatgpt_base_url, codex_home)
let cloud_config_bundle =
cloud_config_bundle_loader(auth_manager, chatgpt_base_url, codex_home);
let product_defaults = product_defaults_loader(cloud_config_bundle.clone());
BackendConfigLoaders {
cloud_config_bundle,
product_defaults,
}
}
pub fn product_defaults_loader(
cloud_config_bundle: CloudConfigBundleLoader,
) -> ProductDefaultsLoader {
ProductDefaultsLoader::from_cloud_config_bundle(cloud_config_bundle)
}

View File

@@ -11,6 +11,7 @@ use tempfile::tempdir;
fn test_bundle() -> CloudConfigBundle {
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: Vec::new(),
enterprise_managed: vec![CloudConfigFragment {
id: "cfg_1".to_string(),
name: "Base config".to_string(),

View File

@@ -10,5 +10,8 @@ mod metrics;
mod service;
mod validation;
pub use bundle_loader::BackendConfigLoaders;
pub use bundle_loader::backend_config_loaders_for_storage;
pub use bundle_loader::cloud_config_bundle_loader;
pub use bundle_loader::cloud_config_bundle_loader_for_storage;
pub use bundle_loader::product_defaults_loader;

View File

@@ -63,6 +63,9 @@ pub(crate) fn bundle_shape_tag(bundle: Option<&CloudConfigBundle>) -> String {
};
let mut sources = Vec::new();
if !bundle.config_toml.product_defaults.is_empty() {
sources.push("product_defaults");
}
if !bundle.config_toml.enterprise_managed.is_empty() {
sources.push("enterprise_config");
}

View File

@@ -48,9 +48,14 @@ fn cloud_config_eligible_auth(auth: &CodexAuth) -> bool {
let Some(plan_type) = auth.account_plan_type() else {
return false;
};
auth.uses_codex_backend()
&& (plan_type.is_business_like()
|| matches!(plan_type, PlanType::Enterprise | PlanType::Edu))
auth.uses_codex_backend() && plan_type.is_workspace_account()
}
fn cloud_config_fail_closed_auth(auth: &CodexAuth) -> bool {
let Some(plan_type) = auth.account_plan_type() else {
return false;
};
plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise | PlanType::Edu)
}
fn optional_bundle(bundle: CloudConfigBundle) -> Option<CloudConfigBundle> {
@@ -117,26 +122,36 @@ where
let _timer =
codex_otel::start_global_timer("codex.cloud_config_bundle.fetch.duration_ms", &[]);
let started_at = Instant::now();
let load_result = timeout(self.timeout, self.load_startup_bundle())
.await
.inspect_err(|_| {
let load_result = match timeout(self.timeout, self.load_startup_bundle()).await {
Ok(load_result) => load_result,
Err(_) => {
let message = format!(
"Timed out waiting for cloud config bundle after {}s",
self.timeout.as_secs()
);
tracing::error!("{message}");
emit_load_metric("startup", "error", /*bundle*/ None);
})
.map_err(|_| {
CloudConfigBundleLoadError::new(
let err = CloudConfigBundleLoadError::new(
CloudConfigBundleLoadErrorCode::Timeout,
/*status_code*/ None,
format!(
"timed out waiting for cloud config bundle after {}s",
self.timeout.as_secs()
),
)
})?;
);
if self.optional_bundle_failure_allowed().await {
tracing::warn!(
error = %err,
"Ignoring optional cloud config bundle timeout"
);
emit_load_metric("startup", "success", /*bundle*/ None);
return Ok(None);
}
tracing::error!("{message}");
emit_load_metric("startup", "error", /*bundle*/ None);
return Err(err);
}
};
let result = match load_result {
Ok(result) => result,
@@ -150,6 +165,7 @@ where
Some(bundle) => {
tracing::info!(
elapsed_ms = started_at.elapsed().as_millis(),
product_defaults_fragments = bundle.config_toml.product_defaults.len(),
config_fragments = bundle.config_toml.enterprise_managed.len(),
requirements_fragments = bundle.requirements_toml.enterprise_managed.len(),
"Cloud config bundle load completed"
@@ -168,6 +184,12 @@ where
Ok(result)
}
async fn optional_bundle_failure_allowed(&self) -> bool {
self.auth_manager.auth().await.as_ref().is_some_and(|auth| {
cloud_config_eligible_auth(auth) && !cloud_config_fail_closed_auth(auth)
})
}
async fn load_startup_bundle(
&self,
) -> Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError> {
@@ -236,9 +258,20 @@ where
while attempt <= CLOUD_CONFIG_BUNDLE_MAX_ATTEMPTS {
match self.client.get_bundle(&auth).await {
Ok(bundle) => {
return self
match self
.validate_and_cache_remote_bundle(&auth, trigger, attempt, bundle)
.await;
.await
{
Ok(bundle) => return Ok(bundle),
Err(err) if !cloud_config_fail_closed_auth(&auth) => {
tracing::warn!(
error = %err,
"Ignoring optional cloud config bundle load failure"
);
return Ok(None);
}
Err(err) => return Err(err),
}
}
Err(BundleRequestError::Retryable(status)) => {
last_status_code = status.status_code();
@@ -255,7 +288,7 @@ where
message,
}) => {
last_status_code = status_code;
match self
let action = match self
.handle_unauthorized(
&mut auth,
&mut auth_recovery,
@@ -264,8 +297,19 @@ where
status_code,
&message,
)
.await?
.await
{
Ok(action) => action,
Err(err) if !cloud_config_fail_closed_auth(&auth) => {
tracing::warn!(
error = %err,
"Ignoring optional cloud config bundle auth failure"
);
return Ok(None);
}
Err(err) => return Err(err),
};
match action {
UnauthorizedRecoveryAction::RetrySameAttempt => continue,
UnauthorizedRecoveryAction::RetryNextAttempt => {
attempt += 1;
@@ -278,6 +322,27 @@ where
break;
}
let err = CloudConfigBundleLoadError::new(
CloudConfigBundleLoadErrorCode::RequestFailed,
last_status_code,
CLOUD_CONFIG_BUNDLE_LOAD_FAILED_MESSAGE,
);
if !cloud_config_fail_closed_auth(&auth) {
emit_fetch_final_metric(
trigger,
"success",
"optional_request_failed",
CLOUD_CONFIG_BUNDLE_MAX_ATTEMPTS,
last_status_code,
/*bundle*/ None,
);
tracing::warn!(
path = %self.cache.path().display(),
error = %err,
"Ignoring optional cloud config bundle request failure"
);
return Ok(None);
}
emit_fetch_final_metric(
trigger,
"error",
@@ -290,11 +355,7 @@ where
path = %self.cache.path().display(),
"{CLOUD_CONFIG_BUNDLE_LOAD_FAILED_MESSAGE}"
);
Err(CloudConfigBundleLoadError::new(
CloudConfigBundleLoadErrorCode::RequestFailed,
last_status_code,
CLOUD_CONFIG_BUNDLE_LOAD_FAILED_MESSAGE,
))
Err(err)
}
async fn validate_and_cache_remote_bundle(

View File

@@ -164,6 +164,7 @@ fn chatgpt_auth_json_with_mode(
fn test_bundle() -> CloudConfigBundle {
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: Vec::new(),
enterprise_managed: vec![test_config_fragment()],
},
requirements_toml: CloudRequirementsTomlBundle {
@@ -191,6 +192,7 @@ fn test_requirements_fragment() -> CloudRequirementsFragment {
fn invalid_config_bundle() -> CloudConfigBundle {
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: Vec::new(),
enterprise_managed: vec![CloudConfigFragment {
id: "cfg_invalid".to_string(),
name: "Invalid config".to_string(),
@@ -307,6 +309,17 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
assert_eq!(
bundle_shape_tag(Some(&CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: vec![test_config_fragment()],
enterprise_managed: Vec::new(),
},
requirements_toml: CloudRequirementsTomlBundle::default(),
})),
"product_defaults"
);
assert_eq!(
bundle_shape_tag(Some(&CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: Vec::new(),
enterprise_managed: vec![test_config_fragment()],
},
requirements_toml: CloudRequirementsTomlBundle::default(),
@@ -325,6 +338,7 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
assert_eq!(
bundle_shape_tag(Some(&CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: Vec::new(),
enterprise_managed: vec![test_config_fragment()],
},
requirements_toml: CloudRequirementsTomlBundle {
@@ -406,8 +420,27 @@ async fn get_bundle_allows_eligible_workspace_plans_and_writes_cache() {
}
#[tokio::test]
async fn get_bundle_skips_team_like_usage_based_plan() {
let fetcher = Arc::new(StaticBundleClient::new(test_bundle()));
async fn get_bundle_allows_team_like_usage_based_plan_for_product_defaults() {
let bundle = test_bundle();
let fetcher = Arc::new(StaticBundleClient::new(bundle.clone()));
let codex_home = tempdir().expect("tempdir");
let service = CloudConfigBundleService::new(
auth_manager_with_plan("self_serve_business_usage_based").await,
fetcher.clone(),
codex_home.path().to_path_buf(),
CLOUD_CONFIG_BUNDLE_TIMEOUT,
);
assert_eq!(service.load_startup_bundle().await, Ok(Some(bundle)));
assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn get_bundle_failure_for_team_like_plan_fails_open() {
let fetcher = Arc::new(SequenceBundleClient::new(vec![
Err(request_error());
CLOUD_CONFIG_BUNDLE_MAX_ATTEMPTS
]));
let codex_home = tempdir().expect("tempdir");
let service = CloudConfigBundleService::new(
auth_manager_with_plan("self_serve_business_usage_based").await,
@@ -417,7 +450,10 @@ async fn get_bundle_skips_team_like_usage_based_plan() {
);
assert_eq!(service.load_startup_bundle().await, Ok(None));
assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 0);
assert_eq!(
fetcher.request_count.load(Ordering::SeqCst),
CLOUD_CONFIG_BUNDLE_MAX_ATTEMPTS
);
}
#[tokio::test]
@@ -588,6 +624,25 @@ async fn get_bundle_times_out() {
);
}
#[tokio::test(start_paused = true)]
async fn get_bundle_timeout_for_team_like_plan_fails_open() {
let codex_home = tempdir().expect("tempdir");
let service = CloudConfigBundleService::new(
auth_manager_with_plan("self_serve_business_usage_based").await,
Arc::new(PendingBundleClient),
codex_home.path().to_path_buf(),
CLOUD_CONFIG_BUNDLE_TIMEOUT,
);
let handle = tokio::spawn(async move { service.load_startup_bundle_with_timeout().await });
tokio::time::advance(CLOUD_CONFIG_BUNDLE_TIMEOUT + Duration::from_millis(1)).await;
assert_eq!(
handle.await.expect("cloud config bundle task"),
Ok(None),
"team-like workspaces should not fail config loading when optional bundle fetch times out"
);
}
#[tokio::test(start_paused = true)]
async fn get_bundle_retries_until_success() {
let fetcher = Arc::new(SequenceBundleClient::new(vec![
@@ -959,6 +1014,11 @@ async fn refresh_from_remote_updates_cached_bundle() {
fn bundle_response_conversion_preserves_fragment_order() {
let response = ConfigBundleResponse {
config_toml: Some(Some(Box::new(codex_backend_client::DeliveredConfigToml {
product_defaults: Some(Some(vec![DeliveredTomlFragment::new(
"cfg_default".to_string(),
"Product defaults".to_string(),
"model = \"default\"".to_string(),
)])),
enterprise_managed: Some(Some(vec![
DeliveredTomlFragment::new(
"cfg_high".to_string(),
@@ -987,6 +1047,11 @@ fn bundle_response_conversion_preserves_fragment_order() {
bundle_from_response(response),
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: vec![CloudConfigFragment {
id: "cfg_default".to_string(),
name: "Product defaults".to_string(),
contents: "model = \"default\"".to_string(),
}],
enterprise_managed: vec![
CloudConfigFragment {
id: "cfg_high".to_string(),

View File

@@ -14,6 +14,13 @@ use serde::Serialize;
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct DeliveredConfigToml {
#[serde(
rename = "product_defaults",
default,
with = "::serde_with::rust::double_option",
skip_serializing_if = "Option::is_none"
)]
pub product_defaults: Option<Option<Vec<models::DeliveredTomlFragment>>>,
#[serde(
rename = "enterprise_managed",
default,
@@ -26,6 +33,7 @@ pub struct DeliveredConfigToml {
impl DeliveredConfigToml {
pub fn new() -> DeliveredConfigToml {
DeliveredConfigToml {
product_defaults: None,
enterprise_managed: None,
}
}

View File

@@ -34,18 +34,23 @@ impl CloudConfigBundle {
requirements_toml,
} = self;
let CloudConfigTomlBundle {
product_defaults,
enterprise_managed: config_enterprise_managed,
} = config_toml;
let CloudRequirementsTomlBundle {
enterprise_managed: requirements_enterprise_managed,
} = requirements_toml;
config_enterprise_managed.is_empty() && requirements_enterprise_managed.is_empty()
product_defaults.is_empty()
&& config_enterprise_managed.is_empty()
&& requirements_enterprise_managed.is_empty()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct CloudConfigTomlBundle {
#[serde(default)]
pub product_defaults: Vec<CloudConfigFragment>,
pub enterprise_managed: Vec<CloudConfigFragment>,
}
@@ -98,6 +103,7 @@ impl CloudConfigBundleLayers {
let CloudConfigBundle {
config_toml:
CloudConfigTomlBundle {
product_defaults: _,
enterprise_managed: config_enterprise_managed,
},
requirements_toml:

View File

@@ -30,6 +30,7 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() {
let layers = CloudConfigBundleLayers::from_bundle(
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: Vec::new(),
enterprise_managed: vec![
CloudConfigFragment {
id: "cfg_high".to_string(),
@@ -98,6 +99,7 @@ fn bundle_layers_can_strict_validate_enterprise_managed_config() {
let err = CloudConfigBundleLayers::from_bundle_strict_config(
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: Vec::new(),
enterprise_managed: vec![CloudConfigFragment {
id: "cfg".to_string(),
name: "Cloud config".to_string(),

View File

@@ -2,6 +2,7 @@ use codex_app_server_protocol::ConfigLayerSource;
pub fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file: &str) -> String {
match source {
ConfigLayerSource::ProductDefaults => "product-defaults".to_string(),
ConfigLayerSource::Mdm { domain, key } => {
format!("MDM ({domain}:{key})")
}

View File

@@ -255,6 +255,7 @@ fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Op
}
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.to_path_buf()),
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::ProductDefaults
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,

View File

@@ -17,6 +17,7 @@ mod merge;
mod overrides;
pub mod permissions_toml;
mod plugin_edit;
mod product_defaults;
pub mod profile_toml;
mod project_root_markers;
mod requirements_exec_policy;
@@ -123,6 +124,8 @@ pub use plugin_edit::PluginConfigEdit;
pub use plugin_edit::apply_user_plugin_config_edits;
pub use plugin_edit::clear_user_plugin;
pub use plugin_edit::set_user_plugin_enabled;
pub use product_defaults::ProductDefaults;
pub use product_defaults::ProductDefaultsLoader;
pub use project_root_markers::default_project_root_markers;
pub use project_root_markers::project_root_markers_from_config;
pub use requirements_exec_policy::RequirementsExecPolicy;

View File

@@ -91,6 +91,7 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
///
/// Configuration is built up from multiple layers in the following order:
///
/// - product OpenAI-supplied product defaults
/// - admin: managed preferences (*)
/// - system `/etc/codex/config.toml` (Unix) or
/// `%ProgramData%\OpenAI\Codex\config.toml` (Windows)
@@ -121,6 +122,7 @@ pub async fn load_config_layers_state(
) -> io::Result<ConfigLayerStack> {
let ConfigLoadOptions {
loader_overrides: overrides,
product_defaults,
strict_config,
cloud_config_bundle,
} = options.into();
@@ -199,6 +201,11 @@ pub async fn load_config_layers_state(
let mut layers = Vec::<ConfigLayerEntry>::new();
let product_defaults = product_defaults.get().await.map_err(io::Error::other)?;
if let Some(product_defaults) = product_defaults.into_config_layer() {
layers.push(product_defaults);
}
let cli_overrides_layer = if cli_overrides.is_empty() {
None
} else {

View File

@@ -0,0 +1,233 @@
use crate::CloudConfigBundleLoadError;
use crate::CloudConfigBundleLoader;
use crate::merge_toml_values;
use crate::state::ConfigLayerEntry;
use codex_app_server_protocol::ConfigLayerSource;
use codex_features::Feature;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use futures::future::Shared;
use std::fmt;
use std::future::Future;
use toml::Value as TomlValue;
use toml::map::Map;
/// Product-owned default config supplied by OpenAI.
///
/// This is intentionally represented as a normal config layer so the existing
/// config merge and requirements enforcement paths determine final behavior.
#[derive(Debug, Clone, PartialEq)]
pub struct ProductDefaults {
config: TomlValue,
}
impl Default for ProductDefaults {
fn default() -> Self {
Self {
config: TomlValue::Table(Map::new()),
}
}
}
impl ProductDefaults {
pub fn from_config(config: TomlValue) -> Self {
Self { config }
}
pub fn from_toml_str(contents: &str) -> Result<Self, toml::de::Error> {
let config = if contents.trim().is_empty() {
TomlValue::Table(Map::new())
} else {
toml::from_str(contents)?
};
Ok(Self::from_config(config))
}
/// Merges backend-delivered fragments ordered highest precedence first.
pub fn from_toml_fragments<'a, I>(fragments: I) -> Result<Self, toml::de::Error>
where
I: IntoIterator<Item = &'a str>,
{
let mut config = TomlValue::Table(Map::new());
let fragments = fragments.into_iter().collect::<Vec<_>>();
for contents in fragments.into_iter().rev() {
let fragment = Self::from_toml_str(contents)?.config;
merge_toml_values(&mut config, &fragment);
}
Ok(Self::from_config(config))
}
pub fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) -> &mut Self {
if !self.config.is_table() {
self.config = TomlValue::Table(Map::new());
}
let TomlValue::Table(root) = &mut self.config else {
return self;
};
let features = root
.entry("features".to_string())
.or_insert_with(|| TomlValue::Table(Map::new()));
if !features.is_table() {
*features = TomlValue::Table(Map::new());
}
let TomlValue::Table(features) = features else {
return self;
};
features.insert(feature.key().to_string(), TomlValue::Boolean(enabled));
self
}
pub fn is_empty(&self) -> bool {
self.config.as_table().is_none_or(Map::is_empty)
}
pub fn config(&self) -> &TomlValue {
&self.config
}
pub fn into_config_layer(self) -> Option<ConfigLayerEntry> {
if self.is_empty() {
None
} else {
Some(ConfigLayerEntry::new(
ConfigLayerSource::ProductDefaults,
self.config,
))
}
}
}
#[derive(Clone)]
pub struct ProductDefaultsLoader {
fut: Shared<BoxFuture<'static, Result<ProductDefaults, CloudConfigBundleLoadError>>>,
}
impl ProductDefaultsLoader {
pub fn new<F>(fut: F) -> Self
where
F: Future<Output = Result<ProductDefaults, CloudConfigBundleLoadError>> + Send + 'static,
{
Self {
fut: fut.boxed().shared(),
}
}
pub fn from_defaults(defaults: ProductDefaults) -> Self {
Self::new(async move { Ok(defaults) })
}
pub fn from_cloud_config_bundle(cloud_config_bundle: CloudConfigBundleLoader) -> Self {
Self::new(async move {
let bundle = match cloud_config_bundle.get().await {
Ok(Some(bundle)) => bundle,
Ok(None) => return Ok(ProductDefaults::default()),
Err(err) => {
tracing::warn!(error = %err, "Failed to load product defaults; continuing without product defaults");
return Ok(ProductDefaults::default());
}
};
let contents = bundle
.config_toml
.product_defaults
.iter()
.map(|fragment| fragment.contents.as_str());
match ProductDefaults::from_toml_fragments(contents) {
Ok(product_defaults) => Ok(product_defaults),
Err(err) => {
tracing::error!(error = %err, "Failed to parse product defaults; continuing without product defaults");
Ok(ProductDefaults::default())
}
}
})
}
pub async fn get(&self) -> Result<ProductDefaults, CloudConfigBundleLoadError> {
self.fut.clone().await
}
}
impl Default for ProductDefaultsLoader {
fn default() -> Self {
Self::from_defaults(ProductDefaults::default())
}
}
impl fmt::Debug for ProductDefaultsLoader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProductDefaultsLoader").finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CloudConfigBundle;
use crate::CloudConfigFragment;
use crate::CloudConfigTomlBundle;
use crate::CloudRequirementsTomlBundle;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
#[test]
fn product_default_fragments_preserve_backend_precedence_order() {
let defaults = ProductDefaults::from_toml_fragments([
"[features]\nplugin_sharing = false\n",
"[features]\nplugin_sharing = true\n",
])
.expect("fragments should parse");
assert_eq!(
defaults
.config()
.get("features")
.and_then(TomlValue::as_table)
.and_then(|features| features.get("plugin_sharing"))
.and_then(TomlValue::as_bool),
Some(false)
);
}
#[tokio::test]
async fn product_defaults_loader_reads_from_shared_bundle() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let cloud_config_bundle = CloudConfigBundleLoader::new(async move {
counter_clone.fetch_add(1, Ordering::SeqCst);
Ok(Some(CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
product_defaults: vec![
CloudConfigFragment {
id: "cfg_default_high".to_string(),
name: "High priority defaults".to_string(),
contents: "[features]\nplugin_sharing = false\n".to_string(),
},
CloudConfigFragment {
id: "cfg_default_low".to_string(),
name: "Low priority defaults".to_string(),
contents: "[features]\nplugin_sharing = true\n".to_string(),
},
],
enterprise_managed: Vec::new(),
},
requirements_toml: CloudRequirementsTomlBundle::default(),
}))
});
let product_defaults =
ProductDefaultsLoader::from_cloud_config_bundle(cloud_config_bundle.clone());
let (bundle, defaults) = tokio::join!(cloud_config_bundle.get(), product_defaults.get());
assert!(bundle.expect("bundle should load").is_some());
let defaults = defaults.expect("product defaults should load");
assert_eq!(
defaults
.config()
.get("features")
.and_then(TomlValue::as_table)
.and_then(|features| features.get("plugin_sharing"))
.and_then(TomlValue::as_bool),
Some(false)
);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
}

View File

@@ -6,6 +6,7 @@ use super::fingerprint::version_for_toml;
use super::key_aliases::normalized_with_key_aliases;
use super::merge::merge_toml_values;
use crate::CloudConfigBundleLoader;
use crate::ProductDefaultsLoader;
use crate::ProfileV2Name;
use codex_app_server_protocol::ConfigLayer;
use codex_app_server_protocol::ConfigLayerMetadata;
@@ -21,6 +22,7 @@ use toml::Value as TomlValue;
#[derive(Debug, Default, Clone)]
pub struct ConfigLoadOptions {
pub loader_overrides: LoaderOverrides,
pub product_defaults: ProductDefaultsLoader,
pub strict_config: bool,
pub cloud_config_bundle: CloudConfigBundleLoader,
}
@@ -29,6 +31,7 @@ impl From<LoaderOverrides> for ConfigLoadOptions {
fn from(loader_overrides: LoaderOverrides) -> Self {
Self {
loader_overrides,
product_defaults: ProductDefaultsLoader::default(),
strict_config: false,
cloud_config_bundle: CloudConfigBundleLoader::default(),
}
@@ -202,6 +205,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::ProductDefaults => None,
ConfigLayerSource::Mdm { .. } => None,
ConfigLayerSource::System { file } => file.parent(),
ConfigLayerSource::EnterpriseManaged { .. } => None,

View File

@@ -21,6 +21,10 @@ impl CloudConfigBundleFixture {
Self::default().add_enterprise_config(contents)
}
pub fn product_defaults(contents: impl Into<String>) -> Self {
Self::default().add_product_defaults(contents)
}
pub fn loader_with_enterprise_requirement(
contents: impl Into<String>,
) -> CloudConfigBundleLoader {
@@ -31,6 +35,10 @@ impl CloudConfigBundleFixture {
Self::enterprise_config(contents).into_loader()
}
pub fn loader_with_product_defaults(contents: impl Into<String>) -> CloudConfigBundleLoader {
Self::product_defaults(contents).into_loader()
}
pub fn add_enterprise_requirement(mut self, contents: impl Into<String>) -> Self {
let index = self.bundle.requirements_toml.enterprise_managed.len() + 1;
self.bundle
@@ -65,6 +73,23 @@ impl CloudConfigBundleFixture {
self
}
pub fn add_product_defaults(mut self, contents: impl Into<String>) -> Self {
let index = self.bundle.config_toml.product_defaults.len() + 1;
self.bundle
.config_toml
.product_defaults
.push(CloudConfigFragment {
id: format!("cfg_default_{index}"),
name: if index == 1 {
"Product defaults".to_string()
} else {
format!("Product defaults {index}")
},
contents: contents.into(),
});
self
}
pub fn into_bundle(self) -> CloudConfigBundle {
self.bundle
}

View File

@@ -350,6 +350,7 @@ fn skill_roots_from_layer_stack_inner(
});
}
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::ProductDefaults
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }

View File

@@ -1875,6 +1875,7 @@ async fn strict_config_rejects_unknown_cloud_config_key() {
&[] as &[(String, TomlValue)],
ConfigLoadOptions {
loader_overrides: LoaderOverrides::without_managed_config_for_tests(),
product_defaults: Default::default(),
strict_config: true,
cloud_config_bundle: CloudConfigBundleFixture::loader_with_enterprise_config(
"unknown_key = true",

View File

@@ -6,6 +6,7 @@ use crate::config::edit::apply_blocking;
use assert_matches::assert_matches;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerEntry;
use codex_config::ProductDefaults;
use codex_config::ProfileV2Name;
use codex_config::RequirementSource;
use codex_config::config_toml::AgentRoleToml;
@@ -9415,6 +9416,118 @@ shell_tool = false
Ok(())
}
#[tokio::test]
async fn product_defaults_sets_plugin_sharing_from_delivered_config() -> std::io::Result<()> {
for (contents, expected_enabled) in [
("[features]\nplugin_sharing = false\n", false),
("[features]\nplugin_sharing = true\n", true),
] {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.product_defaults(
ProductDefaults::from_toml_str(contents).expect("valid product defaults"),
)
.build()
.await?;
assert_eq!(
config.features.enabled(Feature::PluginSharing),
expected_enabled,
"unexpected plugin_sharing default for {contents}"
);
assert_eq!(
config
.config_layer_stack
.get_layers(
codex_config::ConfigLayerStackOrdering::LowestPrecedenceFirst,
/*include_disabled*/ false,
)
.first()
.map(|layer| &layer.name),
Some(&codex_config::ConfigLayerSource::ProductDefaults)
);
}
Ok(())
}
#[tokio::test]
async fn user_config_overrides_product_defaults() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugin_sharing = true
"#,
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.product_defaults(
ProductDefaults::from_toml_str("[features]\nplugin_sharing = false\n")
.expect("valid product defaults"),
)
.build()
.await?;
assert!(config.features.enabled(Feature::PluginSharing));
Ok(())
}
#[tokio::test]
async fn requirements_override_product_defaults_and_user_config() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugin_sharing = true
"#,
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.product_defaults(
ProductDefaults::from_toml_str("[features]\nplugin_sharing = true\n")
.expect("valid product defaults"),
)
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
"[features]\nplugin_sharing = false\n",
),
)
.build()
.await?;
assert!(!config.features.enabled(Feature::PluginSharing));
Ok(())
}
#[tokio::test]
async fn requirements_enable_plugin_sharing_over_disabled_product_default() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.product_defaults(
ProductDefaults::from_toml_str("[features]\nplugin_sharing = false\n")
.expect("valid product defaults"),
)
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
"[features]\nplugin_sharing = true\n",
),
)
.build()
.await?;
assert!(config.features.enabled(Feature::PluginSharing));
Ok(())
}
#[tokio::test]
async fn feature_requirements_auto_review_disables_guardian_approval() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -9532,6 +9645,47 @@ save_fields_resolved_from_model_catalog = false
Ok(())
}
#[tokio::test]
async fn debug_config_lockfile_load_path_ignores_current_product_defaults() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let lock_path = codex_home.path().join("session.config.lock.toml");
std::fs::write(
&lock_path,
format!(
r#"version = {}
codex_version = "older-version"
[config]
"#,
crate::config_lock::CONFIG_LOCK_VERSION
),
)?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
format!(
r#"[debug.config_lockfile]
load_path = '{}'
allow_codex_version_mismatch = true
"#,
lock_path.display()
),
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.product_defaults(
ProductDefaults::from_toml_str("model = \"product-default-model\"\n")
.expect("valid product defaults"),
)
.build()
.await?;
assert_ne!(config.model.as_deref(), Some("product-default-model"));
Ok(())
}
#[tokio::test]
async fn explicit_feature_config_is_normalized_by_requirements() -> std::io::Result<()> {
let codex_home = TempDir::new()?;

View File

@@ -18,6 +18,8 @@ use codex_config::FeatureRequirementsToml;
use codex_config::McpServerIdentity;
use codex_config::McpServerRequirement;
use codex_config::PluginRequirementsToml;
use codex_config::ProductDefaults;
use codex_config::ProductDefaultsLoader;
use codex_config::ProfileV2Name;
use codex_config::ResidencyRequirement;
use codex_config::SandboxModeRequirement;
@@ -1111,6 +1113,7 @@ pub struct ConfigBuilder {
cli_overrides: Option<Vec<(String, TomlValue)>>,
harness_overrides: Option<ConfigOverrides>,
loader_overrides: Option<LoaderOverrides>,
product_defaults: ProductDefaultsLoader,
strict_config: bool,
cloud_config_bundle: CloudConfigBundleLoader,
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
@@ -1138,6 +1141,16 @@ impl ConfigBuilder {
self
}
pub fn product_defaults(mut self, product_defaults: ProductDefaults) -> Self {
self.product_defaults = ProductDefaultsLoader::from_defaults(product_defaults);
self
}
pub fn product_defaults_loader(mut self, product_defaults: ProductDefaultsLoader) -> Self {
self.product_defaults = product_defaults;
self
}
pub fn strict_config(mut self, strict_config: bool) -> Self {
self.strict_config = strict_config;
self
@@ -1172,6 +1185,7 @@ impl ConfigBuilder {
cli_overrides,
harness_overrides,
loader_overrides,
product_defaults,
strict_config,
cloud_config_bundle,
thread_config_loader,
@@ -1197,6 +1211,7 @@ impl ConfigBuilder {
&cli_overrides,
ConfigLoadOptions {
loader_overrides,
product_defaults,
strict_config,
cloud_config_bundle,
},
@@ -1525,6 +1540,19 @@ impl Config {
pub async fn load_default_with_cli_overrides_for_codex_home(
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
Self::load_default_with_cli_overrides_for_codex_home_and_product_defaults(
codex_home,
cli_overrides,
ProductDefaultsLoader::default(),
)
.await
}
pub async fn load_default_with_cli_overrides_for_codex_home_and_product_defaults(
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
product_defaults: ProductDefaultsLoader,
) -> std::io::Result<Self> {
let mut merged = toml::Value::try_from(ConfigToml::default()).map_err(|e| {
std::io::Error::new(
@@ -1532,16 +1560,31 @@ impl Config {
format!("failed to serialize default config: {e}"),
)
})?;
let product_defaults_config_layer = product_defaults
.get()
.await
.map_err(std::io::Error::other)?
.into_config_layer();
if let Some(product_defaults_config_layer) = product_defaults_config_layer.as_ref() {
codex_config::merge_toml_values(&mut merged, &product_defaults_config_layer.config);
}
let cli_layer = codex_config::build_cli_overrides_layer(&cli_overrides);
codex_config::merge_toml_values(&mut merged, &cli_layer);
let codex_home = AbsolutePathBuf::from_absolute_path_checked(codex_home)?;
let config_toml = deserialize_config_toml_with_base(merged, &codex_home)?;
let config_layers = product_defaults_config_layer
.into_iter()
.collect::<Vec<_>>();
Self::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
config_toml,
ConfigOverrides::default(),
codex_home,
ConfigLayerStack::default(),
ConfigLayerStack::new(
config_layers,
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)?,
)
.await
}

View File

@@ -51,7 +51,8 @@ use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStartedNotification;
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
use codex_cloud_config::BackendConfigLoaders;
use codex_cloud_config::backend_config_loaders_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadError;
use codex_config::ConfigLoadOptions;
@@ -345,7 +346,10 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
.chatgpt_base_url
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
let BackendConfigLoaders {
cloud_config_bundle,
product_defaults,
} = backend_config_loaders_for_storage(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
bootstrap_config_toml
@@ -356,6 +360,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
.await;
let run_cli_overrides = cli_kv_overrides.clone();
let run_loader_overrides = loader_overrides.clone();
let run_product_defaults = product_defaults.clone();
let run_cloud_config_bundle = cloud_config_bundle.clone();
let model_provider = if oss {
@@ -438,6 +443,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
.cli_overrides(cli_kv_overrides.clone())
.harness_overrides(overrides)
.loader_overrides(loader_overrides.clone())
.product_defaults_loader(product_defaults.clone())
.strict_config(strict_config)
.cloud_config_bundle(cloud_config_bundle.clone())
.build()
@@ -538,6 +544,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
cli_overrides: run_cli_overrides,
loader_overrides: run_loader_overrides,
strict_config,
product_defaults: run_product_defaults,
cloud_config_bundle: run_cloud_config_bundle,
feedback: CodexFeedback::new(),
log_db: None,
@@ -623,6 +630,7 @@ async fn load_config_toml_or_exit(
cli_kv_overrides,
ConfigLoadOptions {
loader_overrides,
product_defaults: Default::default(),
strict_config,
cloud_config_bundle,
},

View File

@@ -371,6 +371,9 @@ fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf {
ConfigLayerSource::EnterpriseManaged { id, name } => synthetic_layer_path(&format!(
"<enterprise-managed:{name}:{id}>/{CONFIG_TOML_FILE}"
)),
ConfigLayerSource::ProductDefaults => {
synthetic_layer_path("<product-defaults>/config.toml")
}
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
}
@@ -612,6 +615,7 @@ fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSou
ConfigLayerSource::Project { .. } => (HookSource::Project, false),
ConfigLayerSource::Mdm { .. } => (HookSource::Mdm, true),
ConfigLayerSource::EnterpriseManaged { .. } => (HookSource::CloudManagedConfig, true),
ConfigLayerSource::ProductDefaults => (HookSource::CloudManagedConfig, true),
ConfigLayerSource::SessionFlags => (HookSource::SessionFlags, false),
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => {
(HookSource::LegacyManagedConfigFile, true)

View File

@@ -270,6 +270,7 @@ fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>>
match &layer.name {
ConfigLayerSource::SessionFlags => render_session_flag_details(&layer.config),
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::ProductDefaults
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
ConfigLayerSource::System { .. }
@@ -334,6 +335,7 @@ fn non_file_layer_value_label(source: &ConfigLayerSource) -> &'static str {
ConfigLayerSource::Mdm { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
"MDM value"
}
ConfigLayerSource::ProductDefaults => "Product default value",
ConfigLayerSource::EnterpriseManaged { .. } => "Enterprise-managed config value",
ConfigLayerSource::SessionFlags
| ConfigLayerSource::System { .. }

View File

@@ -38,10 +38,12 @@ use codex_app_server_protocol::ThreadListCwdFilter;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey;
use codex_app_server_protocol::ThreadSourceKind;
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
use codex_cloud_config::BackendConfigLoaders;
use codex_cloud_config::backend_config_loaders_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadError;
use codex_config::LoaderOverrides;
use codex_config::ProductDefaultsLoader;
use codex_config::format_config_error_with_source;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
@@ -298,6 +300,7 @@ async fn start_embedded_app_server(
cli_kv_overrides: Vec<(String, toml::Value)>,
loader_overrides: LoaderOverrides,
strict_config: bool,
product_defaults: ProductDefaultsLoader,
cloud_config_bundle: CloudConfigBundleLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
@@ -310,6 +313,7 @@ async fn start_embedded_app_server(
cli_kv_overrides,
loader_overrides,
strict_config,
product_defaults,
cloud_config_bundle,
feedback,
log_db,
@@ -510,6 +514,7 @@ async fn start_app_server(
cli_kv_overrides: Vec<(String, toml::Value)>,
loader_overrides: LoaderOverrides,
strict_config: bool,
product_defaults: ProductDefaultsLoader,
cloud_config_bundle: CloudConfigBundleLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
@@ -523,6 +528,7 @@ async fn start_app_server(
cli_kv_overrides,
loader_overrides,
strict_config,
product_defaults,
cloud_config_bundle,
feedback,
log_db,
@@ -550,6 +556,7 @@ pub(crate) async fn start_app_server_for_picker(
Vec::new(),
LoaderOverrides::default(),
/*strict_config*/ false,
ProductDefaultsLoader::default(),
CloudConfigBundleLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
@@ -584,6 +591,7 @@ async fn start_embedded_app_server_with<F, Fut>(
cli_kv_overrides: Vec<(String, toml::Value)>,
loader_overrides: LoaderOverrides,
strict_config: bool,
product_defaults: ProductDefaultsLoader,
cloud_config_bundle: CloudConfigBundleLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
@@ -611,6 +619,7 @@ where
cli_overrides: cli_kv_overrides,
loader_overrides,
strict_config,
product_defaults,
cloud_config_bundle,
feedback,
log_db,
@@ -991,7 +1000,10 @@ pub async fn run_main(
.chatgpt_base_url
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
let BackendConfigLoaders {
cloud_config_bundle,
product_defaults,
} = backend_config_loaders_for_storage(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
bootstrap_config_toml
@@ -1084,6 +1096,7 @@ pub async fn run_main(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
product_defaults.clone(),
cloud_config_bundle.clone(),
strict_config,
)
@@ -1140,6 +1153,7 @@ pub async fn run_main(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
product_defaults.clone(),
cloud_config_bundle.clone(),
strict_config,
)
@@ -1289,6 +1303,7 @@ pub async fn run_main(
manually_selected_oss_provider,
overrides,
cli_kv_overrides,
product_defaults,
cloud_config_bundle,
feedback,
log_db,
@@ -1311,6 +1326,7 @@ async fn run_ratatui_app(
manually_selected_oss_provider: Option<String>,
overrides: ConfigOverrides,
cli_kv_overrides: Vec<(String, toml::Value)>,
mut product_defaults: ProductDefaultsLoader,
mut cloud_config_bundle: CloudConfigBundleLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
@@ -1373,6 +1389,7 @@ async fn run_ratatui_app(
cli_kv_overrides.clone(),
loader_overrides.clone(),
strict_config,
product_defaults.clone(),
cloud_config_bundle.clone(),
feedback.clone(),
log_db.clone(),
@@ -1460,13 +1477,18 @@ async fn run_ratatui_app(
// and rebuild config. This avoids missing newly available cloud-managed policy due to login
// status detection edge cases.
if show_login_screen && !uses_remote_workspace {
cloud_config_bundle = cloud_config_bundle_loader_for_storage(
let BackendConfigLoaders {
cloud_config_bundle: refreshed_cloud_config_bundle,
product_defaults: refreshed_product_defaults,
} = backend_config_loaders_for_storage(
initial_config.codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
initial_config.cli_auth_credentials_store_mode,
initial_config.chatgpt_base_url.clone(),
)
.await;
cloud_config_bundle = refreshed_cloud_config_bundle;
product_defaults = refreshed_product_defaults;
}
// If the user made an explicit trust decision, or we showed the login flow, reload config
@@ -1478,6 +1500,7 @@ async fn run_ratatui_app(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
product_defaults.clone(),
cloud_config_bundle.clone(),
strict_config,
)
@@ -1682,6 +1705,7 @@ async fn run_ratatui_app(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
product_defaults.clone(),
cloud_config_bundle.clone(),
strict_config,
fallback_cwd,
@@ -1693,6 +1717,7 @@ async fn run_ratatui_app(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
product_defaults.clone(),
cloud_config_bundle.clone(),
strict_config,
)
@@ -1753,6 +1778,7 @@ async fn run_ratatui_app(
cli_kv_overrides.clone(),
loader_overrides.clone(),
strict_config,
product_defaults.clone(),
cloud_config_bundle.clone(),
feedback.clone(),
log_db.clone(),
@@ -1910,6 +1936,7 @@ async fn load_config_or_exit(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
product_defaults: ProductDefaultsLoader,
cloud_config_bundle: CloudConfigBundleLoader,
strict_config: bool,
) -> Config {
@@ -1917,6 +1944,7 @@ async fn load_config_or_exit(
cli_kv_overrides,
overrides,
loader_overrides,
product_defaults,
cloud_config_bundle,
strict_config,
/*fallback_cwd*/ None,
@@ -1928,6 +1956,7 @@ async fn load_config_or_exit_with_fallback_cwd(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
product_defaults: ProductDefaultsLoader,
cloud_config_bundle: CloudConfigBundleLoader,
strict_config: bool,
fallback_cwd: Option<PathBuf>,
@@ -1937,6 +1966,7 @@ async fn load_config_or_exit_with_fallback_cwd(
.cli_overrides(cli_kv_overrides)
.harness_overrides(overrides)
.loader_overrides(loader_overrides)
.product_defaults_loader(product_defaults)
.strict_config(strict_config)
.cloud_config_bundle(cloud_config_bundle)
.fallback_cwd(fallback_cwd)
@@ -1966,6 +1996,7 @@ async fn load_config_toml_or_exit(
cli_kv_overrides,
codex_config::ConfigLoadOptions {
loader_overrides,
product_defaults: Default::default(),
strict_config,
cloud_config_bundle,
},
@@ -2065,6 +2096,7 @@ mod tests {
Vec::new(),
LoaderOverrides::default(),
/*strict_config*/ false,
ProductDefaultsLoader::default(),
CloudConfigBundleLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
@@ -2819,6 +2851,7 @@ mod tests {
Vec::new(),
LoaderOverrides::default(),
/*strict_config*/ false,
ProductDefaultsLoader::default(),
CloudConfigBundleLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,

View File

@@ -1040,6 +1040,7 @@ mod tests {
"https://chatgpt.com/backend-api/".to_string(),
)
.await,
product_defaults: Default::default(),
feedback: codex_feedback::CodexFeedback::new(),
log_db: None,
state_db: None,

View File

@@ -16,7 +16,8 @@ use codex_app_server_protocol::Thread as AppServerThread;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadSortKey;
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
use codex_cloud_config::BackendConfigLoaders;
use codex_cloud_config::backend_config_loaders_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadOptions;
use codex_config::LoaderOverrides;
@@ -251,6 +252,7 @@ async fn start_app_server_for_archive_command(
cli_kv_overrides.clone(),
ConfigLoadOptions {
loader_overrides: loader_overrides.clone(),
product_defaults: Default::default(),
strict_config,
cloud_config_bundle: CloudConfigBundleLoader::default(),
},
@@ -261,7 +263,10 @@ async fn start_app_server_for_archive_command(
.chatgpt_base_url
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
let BackendConfigLoaders {
cloud_config_bundle,
product_defaults,
} = backend_config_loaders_for_storage(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config_toml.cli_auth_credentials_store.unwrap_or_default(),
@@ -299,6 +304,7 @@ async fn start_app_server_for_archive_command(
..Default::default()
})
.loader_overrides(loader_overrides.clone())
.product_defaults_loader(product_defaults.clone())
.strict_config(strict_config)
.cloud_config_bundle(cloud_config_bundle.clone())
.build()
@@ -314,6 +320,7 @@ async fn start_app_server_for_archive_command(
cli_kv_overrides,
loader_overrides,
strict_config,
product_defaults,
cloud_config_bundle,
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,