Files
codex/codex-rs/cloud-config/src/bundle_loader.rs
T
smurching-oai ec513795a6 Integrate config-server policies with startup and authentication (#57175)
## What changed

- Use the managed configuration loader in CLI, exec, TUI, and app-server configuration flows so `requirements.toml` can select a config server.
- Sign in to the config server when needed during interactive TUI startup and CLI login, reload configuration after CLI sign-in, and clear config-server credentials on CLI logout.
- Preserve the app-server's config-server settings and refresh worker across OpenAI login and logout.
- Require config-server settings to be applied when loading CLI and doctor configuration.

## Testing

Add app-server integration tests using a local HTTPS config server to verify managed provider and instruction settings at startup, preservation across API-key login and logout, and cache reuse after restart and thread resume.

GitOrigin-RevId: 27ee81f7943d2290a5c3fcc0ac556579ec63f8b4
2026-10-26 18:14:57 +00:00

202 lines
7.0 KiB
Rust

use crate::backend::BackendBundleClient;
use crate::chatgpt::ChatGptSource;
use crate::config_server::config_server_http_client_factory;
use crate::config_source::BundleSource;
use crate::service::CLOUD_CONFIG_BUNDLE_TIMEOUT;
use crate::service::CloudConfigBundleService;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigRequirementsToml;
use codex_http_client::HttpClientFactory;
use codex_login::AuthConfig;
use codex_login::AuthManager;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::task::AbortHandle;
use tokio::task::JoinHandle;
pub fn managed_config_auth_manager(
auth: AuthConfig,
requirements: &ConfigRequirementsToml,
) -> std::io::Result<Option<codex_login::OAuthSessionManager>> {
let Some(service_config) = requirements.config_server.as_ref() else {
return Ok(None);
};
let factory = config_server_http_client_factory(service_config, &auth.auth_route_config);
crate::config_server_auth_manager(service_config, auth.codex_home, &factory)
}
/// Existing grants are left to the loader's normal cache and refresh handling.
pub async fn ensure_config_server_login(
auth: AuthConfig,
requirements: &ConfigRequirementsToml,
) -> std::io::Result<()> {
let Some(manager) = managed_config_auth_manager(auth, requirements)? else {
return Ok(());
};
if let Err(error) = manager.authorization_id().await {
if error
.get_ref()
.and_then(|source| source.downcast_ref::<codex_login::OAuthSessionError>())
!= Some(&codex_login::OAuthSessionError::LoginRequired)
{
return Err(error);
}
manager.login().await?;
}
Ok(())
}
pub fn managed_config_bundle_loader(
auth_manager: Arc<AuthManager>,
chatgpt_base_url: String,
codex_home: PathBuf,
factory: HttpClientFactory,
requirements: &ConfigRequirementsToml,
) -> CloudConfigBundleLoader {
if let Some(service_config) = requirements.config_server.as_ref() {
let factory = config_server_http_client_factory(
service_config,
&codex_login::AuthRouteConfig::from_http_client_factory(factory),
);
return crate::config_server_bundle_loader(service_config.clone(), codex_home, factory);
}
let endpoint = codex_backend_client::Client::new(chatgpt_base_url.clone(), factory.clone())
.config_bundle_url();
let policy = factory
.network_policy()
.clone()
.restrict_to_endpoints(endpoint.parse().into_iter().collect());
chatgpt_config_bundle_loader(
auth_manager,
chatgpt_base_url,
codex_home,
factory.with_network_policy(policy),
)
}
pub async fn managed_config_bundle_loader_for_storage(
auth_config: AuthConfig,
requirements: &ConfigRequirementsToml,
enable_codex_api_key_env: bool,
) -> std::io::Result<CloudConfigBundleLoader> {
if let Some(service_config) = requirements.config_server.as_ref() {
let factory =
config_server_http_client_factory(service_config, &auth_config.auth_route_config);
return Ok(crate::config_server_bundle_loader(
service_config.clone(),
auth_config.codex_home,
factory,
));
}
let service =
chatgpt_config_bundle_service_for_storage(auth_config, enable_codex_api_key_env).await?;
Ok(managed_config_bundle_loader_for_service(service).0)
}
struct CloudConfigBundleLoaderLifetime<C: BundleSource> {
service: Arc<CloudConfigBundleService<C>>,
refresh_task: AbortHandle,
}
impl<C: BundleSource> Drop for CloudConfigBundleLoaderLifetime<C> {
fn drop(&mut self) {
self.refresh_task.abort();
}
}
pub fn chatgpt_config_bundle_loader(
auth_manager: Arc<AuthManager>,
chatgpt_base_url: String,
codex_home: PathBuf,
http_client_factory: HttpClientFactory,
) -> CloudConfigBundleLoader {
let service = CloudConfigBundleService::new(
auth_manager,
Arc::new(BackendBundleClient::new(
chatgpt_base_url,
http_client_factory,
)),
codex_home,
CLOUD_CONFIG_BUNDLE_TIMEOUT,
);
let (loader, _) = managed_config_bundle_loader_for_service(service);
loader
}
pub(crate) fn managed_config_bundle_loader_for_service<C>(
service: CloudConfigBundleService<C>,
) -> (CloudConfigBundleLoader, JoinHandle<()>)
where
C: BundleSource + 'static,
{
let service = Arc::new(service);
let background_service = Arc::clone(&service);
let refresh_task = tokio::spawn(async move {
let _ = background_service.get_latest().await;
background_service.refresh_cache_in_background().await;
});
let abort_handle = refresh_task.abort_handle();
let lifetime = Arc::new(CloudConfigBundleLoaderLifetime {
service,
refresh_task: abort_handle,
});
let snapshot_lifetime = Arc::clone(&lifetime);
let policy = lifetime.service.policy.clone();
let loader = CloudConfigBundleLoader::from_getter(move || {
let lifetime = Arc::clone(&lifetime);
async move { lifetime.service.get_latest().await }
})
.with_ema_policy_snapshots(policy, move || {
let lifetime = Arc::clone(&snapshot_lifetime);
async move { lifetime.service.get_latest_snapshot().await }
});
(loader, refresh_task)
}
pub async fn chatgpt_config_bundle_loader_for_storage(
auth_config: AuthConfig,
enable_codex_api_key_env: bool,
) -> std::io::Result<CloudConfigBundleLoader> {
let service =
chatgpt_config_bundle_service_for_storage(auth_config, enable_codex_api_key_env).await?;
let (loader, _) = managed_config_bundle_loader_for_service(service);
Ok(loader)
}
/// Fetches directly from the network on each load, without reading or writing
/// the disk cache or starting a background refresher.
pub async fn chatgpt_config_bundle_loader_for_storage_without_cache(
auth_config: AuthConfig,
enable_codex_api_key_env: bool,
) -> std::io::Result<CloudConfigBundleLoader> {
let service = Arc::new(
chatgpt_config_bundle_service_for_storage(auth_config, enable_codex_api_key_env)
.await?
.without_cache(),
);
Ok(CloudConfigBundleLoader::from_getter(move || {
let service = Arc::clone(&service);
async move { service.load_startup_bundle_with_timeout().await }
}))
}
async fn chatgpt_config_bundle_service_for_storage(
auth_config: AuthConfig,
enable_codex_api_key_env: bool,
) -> std::io::Result<CloudConfigBundleService<ChatGptSource<BackendBundleClient>>> {
let auth_manager =
AuthManager::shared_from_auth_config(auth_config.clone(), enable_codex_api_key_env).await?;
Ok(CloudConfigBundleService::new(
auth_manager,
Arc::new(BackendBundleClient::new(
auth_config
.chatgpt_base_url
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
auth_config.auth_route_config.http_client_factory().clone(),
)),
auth_config.codex_home,
CLOUD_CONFIG_BUNDLE_TIMEOUT,
))
}