mirror of
https://github.com/openai/codex.git
synced 2026-06-04 04:12:03 +00:00
Compare commits
9 Commits
starr/agen
...
codex/prod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e650fbe0 | ||
|
|
1de0c7aa0d | ||
|
|
a81cbfcbc5 | ||
|
|
6d6412020a | ||
|
|
d040311ca4 | ||
|
|
957063a6de | ||
|
|
138b4174d7 | ||
|
|
d881af7e4f | ||
|
|
9dcf6cf3ca |
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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})")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
233
codex-rs/config/src/product_defaults.rs
Normal file
233
codex-rs/config/src/product_defaults.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -350,6 +350,7 @@ fn skill_roots_from_layer_stack_inner(
|
||||
});
|
||||
}
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::ProductDefaults
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 { .. }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user