Wire shared runtime authentication through threads and API clients (#59177)

## What changed

- Share a lazily registered runtime across threads, model discovery, and extension clients when `runtime_agent_instances` is enabled, while honoring each thread's authentication mode and credential owner.
- Bind model catalogs to the effective provider and authentication context while preserving cache policy and live API-key discovery settings.
- Resolve credentials, routing, and transport policy together for each request. Preserve proxy and TLS settings, validate runtime account selectors, and prevent pooled connections from crossing runtime identities.
- Accept supported ChatGPT subdomains when selecting the registration environment.

## Testing

Add integration coverage for shared runtime registration, dual-header and assertion-only modes, disabled and third-party providers, credential replacement, subagents, and delegated review. Add coverage for catalog cache policy and runtime-aware connection reuse.

GitOrigin-RevId: 99dd9d987c29c93597a648f6586f98ac6a932c3c
This commit is contained in:
cooper-oai
2026-11-03 15:02:57 +00:00
committed by copyberry
parent f7167ac895
commit 079e17a422
71 changed files with 2636 additions and 820 deletions
+1
View File
@@ -393,6 +393,7 @@ pub async fn start(mut args: InProcessStartArgs) -> IoResult<InProcessClientHand
});
}
let initialize = args.initialize.clone();
// Keep embedded startup state off callers' stacks, which are smaller on Windows.
let client = Box::pin(start_uninitialized(args)).await?;
let initialize_response = client
+9 -3
View File
@@ -186,6 +186,7 @@ mod tests {
use codex_home::CodexHomeUserInstructionsProvider;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_protocol::protocol::SessionSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@@ -534,8 +535,14 @@ enabled = false
let thread_manager = Arc::new_cyclic(|thread_manager| {
ThreadManager::new(
&good_config,
auth_manager.clone(),
codex_core::build_models_manager(&good_config, auth_manager.clone()),
codex_core::build_startup_models(
&good_config,
auth_manager.clone(),
"11111111-1111-4111-8111-111111111111".to_string(),
Arc::new(ModelCatalogSettings::disk(
good_config.codex_home.to_path_buf(),
)),
),
codex_core::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::clone(&environment_manager),
@@ -567,7 +574,6 @@ enabled = false
codex_core::passthrough_image_store(),
Arc::clone(&thread_store),
codex_core::local_agent_graph_store_from_state_db(Some(&state_db)),
"11111111-1111-4111-8111-111111111111".to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
)
+9 -5
View File
@@ -90,6 +90,7 @@ use codex_feedback::CodexFeedback;
use codex_goal_extension::GoalService;
use codex_home::CodexHomeUserInstructionsProvider;
use codex_login::AuthManager;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_otel::auth_storage::AuthStorageOriginator;
use codex_protocol::ThreadId;
use codex_protocol::mcp::ClientMcpExtensions;
@@ -372,10 +373,15 @@ impl MessageProcessor {
Arc::clone(&extension_event_sink),
))
});
let manager = ThreadManager::new(
let startup = codex_core::build_startup_models(
config.as_ref(),
auth_manager.clone(),
codex_core::build_models_manager(config.as_ref(), auth_manager.clone()),
installation_id,
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
);
let manager = ThreadManager::new(
config.as_ref(),
startup,
codex_core::CodexAppsToolsCache::default(),
session_source,
environment_manager,
@@ -405,7 +411,6 @@ impl MessageProcessor {
codex_core::passthrough_image_store(),
Arc::clone(&thread_store),
codex_core::local_agent_graph_store_from_state_db(state_db.as_ref()),
installation_id,
Some(app_server_attestation_provider(
outgoing.clone(),
thread_state_manager.clone(),
@@ -425,8 +430,7 @@ impl MessageProcessor {
config_manager.clone(),
startup_cwd,
Arc::clone(&config),
thread_manager.get_models_manager(),
Arc::clone(&auth_manager),
Arc::clone(&thread_manager),
));
let models_refresh_worker = crate::models_refresh_worker::spawn(&model_catalog);
let turn_cost_worker =
+13 -16
View File
@@ -4,9 +4,9 @@ use std::path::PathBuf;
use std::sync::Arc;
use codex_config::ConfigLayerStack;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::openai_models::ModelPreset;
@@ -19,7 +19,7 @@ use crate::config_manager::StartupConfig;
/// Refreshes catalog routing without changing the routes retained by existing threads.
pub(crate) struct ModelCatalog {
config_manager: ConfigManager,
auth_manager: Arc<AuthManager>,
thread_manager: Arc<ThreadManager>,
startup_cwd: Option<PathBuf>,
state: Mutex<CatalogState>,
}
@@ -35,12 +35,12 @@ impl ModelCatalog {
config_manager: ConfigManager,
startup_cwd: Option<PathBuf>,
config: Arc<Config>,
models_manager: SharedModelsManager,
auth_manager: Arc<AuthManager>,
thread_manager: Arc<ThreadManager>,
) -> Self {
let models_manager = thread_manager.get_models_manager();
Self {
config_manager,
auth_manager,
thread_manager,
startup_cwd,
state: Mutex::new(CatalogState {
needs_reload: false,
@@ -72,14 +72,6 @@ impl ModelCatalog {
}
}
pub(crate) async fn set_api_key_model_discovery_enabled(&self, enabled: bool) {
self.state
.lock()
.await
.models_manager
.set_api_key_model_discovery_enabled(enabled);
}
#[expect(
clippy::await_holding_invalid_type,
reason = "catalog reloads are serialized with account invalidation; model fetching holds no lock"
@@ -100,11 +92,16 @@ impl ModelCatalog {
if config.model_provider_id != state.config.model_provider_id
|| config.model_provider != state.config.model_provider
|| config.model_catalog != state.config.model_catalog
|| config.features.enabled(Feature::RuntimeAgentInstances)
!= state
.config
.features
.enabled(Feature::RuntimeAgentInstances)
|| config.runtime_auth_mode != state.config.runtime_auth_mode
{
state.models_manager =
codex_core::build_models_manager(&config, Arc::clone(&self.auth_manager));
state.models_manager = self.thread_manager.models_manager_for_config(&config);
}
state.models_manager.set_api_key_model_discovery_enabled(
self.thread_manager.set_api_key_model_discovery_enabled(
config.features.enabled(Feature::ApiKeyModelDiscovery),
);
state.config = Arc::new(config);
@@ -11,6 +11,7 @@ use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_feedback::CodexFeedback;
use codex_http_client::HttpClientFactory;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_models_manager::manager::ModelsEndpointClient;
use codex_models_manager::manager::ModelsEndpointFuture;
use codex_models_manager::manager::ModelsEndpointResponse;
@@ -28,6 +29,38 @@ use tracing_subscriber::layer::SubscriberExt;
use super::*;
fn test_thread_manager(
config: &codex_core::config::Config,
models_manager: Option<SharedModelsManager>,
auth_manager: Arc<codex_login::AuthManager>,
) -> Arc<codex_core::ThreadManager> {
let factory = match models_manager {
Some(models) => codex_core::test_support::fixed_model_catalog(models),
None => Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
};
let startup = codex_core::build_startup_models(
config,
auth_manager,
uuid::Uuid::new_v4().to_string(),
factory,
);
Arc::new(codex_core::ThreadManager::new(
config,
startup,
codex_core::CodexAppsToolsCache::default(),
codex_protocol::protocol::SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
codex_extension_api::empty_extension_registry(),
Arc::new(codex_core::test_support::EmptyUserInstructionsProvider),
/*analytics_events_client*/ None,
codex_core::passthrough_image_store(),
codex_core::ThreadStoreBuilder::from_config(config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
/*attestation_provider*/ None,
/*external_time_provider*/ None,
))
}
#[derive(Debug)]
struct TestModelsEndpoint {
fetch_count: AtomicUsize,
@@ -148,9 +181,8 @@ async fn refreshes_immediately_periodically_and_stops_when_dropped() {
let catalog = Arc::new(ModelCatalog::new(
config_manager,
Some(config.cwd.to_path_buf()),
config,
Arc::clone(&models_manager),
auth_manager,
Arc::clone(&config),
test_thread_manager(&config, Some(Arc::clone(&models_manager)), auth_manager),
));
let worker = spawn_with_interval(&catalog, Duration::from_millis(/*millis*/ 10));
@@ -232,9 +264,8 @@ async fn background_refresh_blocks_invalid_requirements_then_recovers() -> anyho
let catalog = Arc::new(ModelCatalog::new(
config_manager,
Some(config.cwd.to_path_buf()),
config,
models_manager,
auth_manager,
Arc::clone(&config),
test_thread_manager(&config, Some(models_manager), auth_manager),
));
let feedback = CodexFeedback::new();
// This test uses Tokio's current-thread runtime, so spawned refreshes share the subscriber.
@@ -319,9 +350,11 @@ async fn catalog_reload_recovers_after_cloud_failure() -> anyhow::Result<()> {
/*enable_codex_api_key_env*/ false,
)
.await?;
let models = codex_core::build_models_manager(&config, Arc::clone(&auth));
let catalog = Arc::new(ModelCatalog::new(
manager, /*startup_cwd*/ None, config, models, auth,
manager,
/*startup_cwd*/ None,
Arc::clone(&config),
test_thread_manager(&config, /*models_manager*/ None, auth),
));
failed.store(true, Ordering::SeqCst);
catalog.invalidate().await;
@@ -370,8 +403,12 @@ async fn catalog_reload_recovers_original_project_after_startup_fallback() -> an
/*enable_codex_api_key_env*/ false,
)
.await?;
let models = codex_core::build_models_manager(&config, Arc::clone(&auth));
let catalog = ModelCatalog::new(manager, Some(project_path.clone()), config, models, auth);
let catalog = ModelCatalog::new(
manager,
Some(project_path.clone()),
Arc::clone(&config),
test_thread_manager(&config, /*models_manager*/ None, auth),
);
let mut model = codex_models_manager::bundled_models_response()?
.models
.remove(0);
@@ -102,8 +102,7 @@ async fn background_routing_skips_config_until_chatgpt_sign_in(
manager.clone(),
Some(config.cwd.to_path_buf()),
Arc::clone(&config),
threads.get_models_manager(),
Arc::clone(&auth_manager),
Arc::clone(&threads),
));
let processor = AccountRequestProcessor::new(
Arc::clone(&auth_manager),
@@ -357,16 +357,9 @@ impl ConfigRequestProcessor {
.map_err(|_| internal_error("failed to update feature enablement"))?;
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
self.thread_manager
.get_models_manager()
.set_api_key_model_discovery_enabled(
config.features.enabled(Feature::ApiKeyModelDiscovery),
);
self.model_catalog
.set_api_key_model_discovery_enabled(
config.features.enabled(Feature::ApiKeyModelDiscovery),
)
.await;
self.thread_manager.set_api_key_model_discovery_enabled(
config.features.enabled(Feature::ApiKeyModelDiscovery),
);
if should_start_background_rollout_migration && config.features.enabled(feature) {
self.thread_manager.start_background_rollout_migration();
}