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
@@ -113,8 +113,12 @@ async fn publication_harness(
});
harness.manager = ThreadManager::new(
&harness.config,
harness.manager.auth_manager(),
harness.manager.get_models_manager(),
crate::build_startup_models(
&harness.config,
harness.manager.auth_manager(),
"spawn-publication-cleanup-test".to_string(),
crate::test_support::fixed_model_catalog(harness.manager.get_models_manager()),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
harness.manager.environment_manager(),
@@ -124,7 +128,6 @@ async fn publication_harness(
harness.manager.image_store(),
crate::ThreadStoreBuilder::from_config(&harness.config, harness.state_db.clone()).build(),
Some(graph.clone()),
"spawn-publication-cleanup-test".to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
)
+15 -6
View File
@@ -49,6 +49,7 @@ use codex_history::ResumedHistory;
use codex_history::RolloutItem;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::AgentPath;
use codex_protocol::ResponseItemId;
@@ -3785,8 +3786,14 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
harness.manager = ThreadManager::new(
&parent_config,
auth_manager.clone(),
crate::thread_manager::build_models_manager(&parent_config, auth_manager),
crate::build_startup_models(
&parent_config,
auth_manager.clone(),
uuid::Uuid::new_v4().to_string(),
Arc::new(ModelCatalogSettings::disk(
parent_config.codex_home.to_path_buf(),
)),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -3797,7 +3804,6 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
crate::ThreadStoreBuilder::from_config(&parent_config, harness.state_db.clone())
.build(),
crate::thread_manager::local_agent_graph_store_from_state_db(harness.state_db.as_ref()),
uuid::Uuid::new_v4().to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -4941,8 +4947,12 @@ async fn resume_thread_subagent_restores_stored_metadata() {
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
crate::thread_manager::build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
auth_manager.clone(),
uuid::Uuid::new_v4().to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -4952,7 +4962,6 @@ async fn resume_thread_subagent_restores_stored_metadata() {
crate::thread_manager::passthrough_image_store(),
thread_store.clone(),
/*agent_graph_store*/ None,
uuid::Uuid::new_v4().to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
+17 -166
View File
@@ -54,7 +54,6 @@ use codex_api::RealtimeSessionConfig as ApiRealtimeSessionConfig;
use codex_api::Reasoning;
use codex_api::ReasoningContext;
use codex_api::RequestTelemetry;
use codex_api::ReqwestTransport;
use codex_api::ResponseCreateWsRequest;
use codex_api::ResponsesApiRequest;
use codex_api::ResponsesClient as ApiResponsesClient;
@@ -71,22 +70,17 @@ use codex_api::auth_header_telemetry;
use codex_api::build_session_headers;
use codex_api::create_text_param_for_request;
use codex_api::response_create_client_metadata;
use codex_features::RuntimeAuthMode;
use codex_http_client::ClientRouteClass;
use codex_http_client::HttpClientFactory;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::RefreshTokenError;
use codex_login::UnauthorizedRecovery;
use codex_login::default_client::ClientRedirectPolicy;
use codex_login::default_client::add_originator_header;
use codex_login::default_client::create_client_for_route;
use codex_otel::SessionTelemetry;
use codex_otel::WEBSOCKET_CONTINUATION_COUNT_METRIC;
use codex_otel::current_span_w3c_trace_context;
use codex_protocol::ResponseItemId;
use codex_protocol::auth::AuthMode;
use codex_runtime_agent_instance::RuntimeAuthManager;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
@@ -147,14 +141,13 @@ use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
use codex_model_provider::AgentIdentitySessionFallback;
use codex_model_provider::ProviderAuthScope;
use codex_model_provider::ProviderUnauthorizedRecovery;
use codex_model_provider::ResolvedProviderAuth;
use codex_model_provider::ResolvedProviderRequest as CurrentClientSetup;
use codex_model_provider::ResponsesConnectionKey;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::WorkspaceRoutingContext;
use codex_model_provider::create_model_provider;
#[cfg(test)]
use codex_model_provider_info::DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails;
@@ -235,44 +228,6 @@ enum ClientRouting {
ConfiguredProvider,
}
/// Resolved API client setup for a single request attempt.
///
/// Keeping this as a single bundle ensures prewarm and normal request paths
/// share the same auth/provider setup flow.
struct CurrentClientSetup {
auth: Option<CodexAuth>,
auth_owner_generation: Option<u64>,
auth_revision: Option<u64>,
api_provider: ApiProvider,
http_client_factory: HttpClientFactory,
redirect_policy: ClientRedirectPolicy,
api_auth: SharedAuthProvider,
agent_identity_telemetry: Option<AgentIdentityTelemetry>,
runtime_id: Option<String>,
}
impl CurrentClientSetup {
fn build_api_transport(&self, endpoint: &str) -> Result<ReqwestTransport> {
let api_provider = &self.api_provider;
let redirect_policy = if api_provider
.headers
.contains_key(codex_model_provider::ACCOUNT_ROUTING_HEADER)
{
ClientRedirectPolicy::Reject
} else {
self.redirect_policy
};
let client = create_client_for_route(
&self.http_client_factory,
&api_provider.url_for_path(endpoint),
ClientRouteClass::Api,
redirect_policy,
)
.map_err(std::io::Error::from)?;
Ok(ReqwestTransport::from_http_client(client))
}
}
#[derive(Clone, Copy)]
struct RequestRouteTelemetry {
endpoint: &'static str,
@@ -309,7 +264,6 @@ pub struct ModelClient {
executed_tool_calls: Option<ExecutedToolCalls>,
// Resolved once when the session is created, like other session feature flags.
api_key_cyber_access_programs: cyber_access_program::ApiKeyCyberAccessPrograms,
runtime_auth: Option<(Arc<RuntimeAuthManager>, RuntimeAuthMode)>,
}
/// A turn-scoped streaming session created from a [`ModelClient`].
@@ -581,10 +535,9 @@ impl ModelClient {
/// HTTP client factory must come from the effective session configuration so every transport
/// observes the resolved outbound proxy policy.
pub fn new(
auth_manager: Option<Arc<AuthManager>>,
model_provider: SharedModelProvider,
agent_identity_policy: AgentIdentityAuthPolicy,
thread_id: ThreadId,
provider_info: ModelProviderInfo,
session_source: SessionSource,
originator: String,
model_verbosity: Option<VerbosityConfig>,
@@ -600,7 +553,6 @@ impl ModelClient {
workspace_routing: WorkspaceRoutingContext,
request_contributors: Vec<Arc<dyn codex_extension_api::ModelRequestContributor>>,
) -> Self {
let model_provider = create_model_provider(provider_info, auth_manager);
let codex_api_key_env_enabled = model_provider
.auth_manager()
.as_ref()
@@ -647,7 +599,6 @@ impl ModelClient {
codex_responses_headers: None,
event_sender: None,
http_client_factory,
runtime_auth: None,
restored_history: false,
request_contributors,
executed_tool_calls: None,
@@ -672,24 +623,6 @@ impl ModelClient {
self
}
/// Use the harness-owned runtime for this first-party provider. The owner must share this
/// client's credential manager. Provider-specific overrides need their own runtime owner.
pub fn with_runtime_auth(
mut self,
manager: Arc<RuntimeAuthManager>,
mode: RuntimeAuthMode,
) -> Result<Self> {
let info = self.state.provider.info();
codex_model_provider::validate_runtime_destination(info.base_url.as_deref())?;
codex_model_provider::validate_runtime_provider(
info,
self.auth_manager().as_ref(),
&manager,
)?;
self.runtime_auth = Some((manager, mode));
Ok(self)
}
pub(crate) fn with_session_context(
mut self,
prompt_cache_key: PromptCacheKey,
@@ -834,7 +767,7 @@ impl ModelClient {
if let Some(api_provider) = api_provider_override {
client_setup.api_provider = api_provider;
}
let transport = client_setup.build_api_transport(REALTIME_CALLS_ENDPOINT)?;
let transport = client_setup.http_transport(REALTIME_CALLS_ENDPOINT)?;
let response =
ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth)
.create_with_session_and_headers(sdp, session_config, extra_headers)
@@ -882,7 +815,7 @@ impl ModelClient {
let client_setup = self
.current_client_setup(ClientRouting::ConfiguredProvider)
.await?;
let transport = client_setup.build_api_transport(MEMORIES_SUMMARIZE_ENDPOINT)?;
let transport = client_setup.http_transport(MEMORIES_SUMMARIZE_ENDPOINT)?;
let request_telemetry = Self::build_request_telemetry(
session_telemetry,
AuthRequestTelemetryContext::new(
@@ -1206,61 +1139,6 @@ impl ModelClient {
/// This centralizes setup used by both prewarm and normal request paths so they stay in
/// lockstep when auth/provider resolution changes.
async fn current_client_setup(&self, routing: ClientRouting) -> Result<CurrentClientSetup> {
if let Some((manager, mode)) = &self.runtime_auth {
let selected = codex_model_provider::select_runtime_auth(
manager,
&self.state.provider.info().build_header_map()?,
)
.await
.map_err(|error| self.state.provider.map_api_error(error.into()))?;
let auth = selected.auth().clone();
let auth_revision = Some(selected.change_state().generation);
let auth_owner_generation = Some(selected.change_state().owner_generation);
let api_provider = self.state.provider.api_provider_for_auth(Some(&auth))?;
let (api_provider, redirect_policy) = match routing {
ClientRouting::Workspace => {
let resolved = self
.state
.provider
.route_responses_api_provider(
api_provider,
Some(&auth),
&self.state.workspace_routing,
)
.await?;
(resolved.provider, resolved.redirect_policy)
}
ClientRouting::ConfiguredProvider => (api_provider, ClientRedirectPolicy::Default),
};
let runtime = selected.runtime();
codex_model_provider::validate_runtime_selectors(
&api_provider.headers,
runtime.actor().info(),
)?;
let runtime_id = Some(runtime.id().to_string());
let http_client_factory = selected.http_client_factory().clone();
let resolved_auth = self
.state
.provider
.compose_auth(ResolvedProviderAuth {
auth: selected.clone().into_auth_provider(*mode),
agent_identity_telemetry: None,
runtime: Some(selected),
})
.await?;
tracing::debug!(runtime_auth_mode = ?mode, "selected runtime authentication");
return Ok(CurrentClientSetup {
auth: Some(auth),
auth_owner_generation,
auth_revision,
api_provider,
http_client_factory,
redirect_policy,
api_auth: resolved_auth.auth,
agent_identity_telemetry: None,
runtime_id,
});
}
self.provider_client_setup(routing, &self.state.provider)
.await
}
@@ -1284,46 +1162,19 @@ impl ModelClient {
routing: ClientRouting,
provider: &SharedModelProvider,
) -> Result<CurrentClientSetup> {
let routing = &routing;
codex_model_provider::prepare_provider_request(
let routing = match routing {
ClientRouting::Workspace => Some(&self.state.workspace_routing),
ClientRouting::ConfiguredProvider => None,
};
codex_model_provider::resolve_request(
provider.as_ref(),
provider.info().auth.is_none(),
|state| async move {
let auth = provider.auth().await;
let (api_provider, redirect_policy) = match routing {
ClientRouting::Workspace => {
let resolved = provider
.responses_api_provider(&self.state.workspace_routing)
.await?;
(resolved.provider, resolved.redirect_policy)
}
ClientRouting::ConfiguredProvider => (
provider.api_provider().await?,
ClientRedirectPolicy::Default,
),
};
let resolved_auth = provider
.api_auth_for_scope(ProviderAuthScope {
agent_identity_policy: self.agent_identity_policy,
session_source: self.state.session_source.clone(),
agent_identity_session_fallback: self
.state
.agent_identity_session_fallback
.clone(),
})
.await?;
Ok(CurrentClientSetup {
auth,
auth_owner_generation: state.map(|state| state.owner_generation),
auth_revision: state.map(|state| state.generation),
api_provider,
http_client_factory: self.http_client_factory.clone(),
redirect_policy,
api_auth: resolved_auth.auth,
agent_identity_telemetry: resolved_auth.agent_identity_telemetry,
runtime_id: None,
})
},
Some(ProviderAuthScope {
agent_identity_policy: self.agent_identity_policy,
session_source: self.state.session_source.clone(),
agent_identity_session_fallback: self.state.agent_identity_session_fallback.clone(),
}),
routing,
self.http_client_factory.clone(),
)
.await
}
@@ -1953,7 +1804,7 @@ impl ResponsesHttpExecutor<'_> {
.client
.responses_headers(client_setup.auth.as_ref(), &model_info.slug);
tracing::Span::current().record("api.path", "/responses");
let transport = client_setup.build_api_transport("/responses")?;
let transport = client_setup.http_transport("/responses")?;
let request_auth_context = AuthRequestTelemetryContext::new(
client_setup.auth.as_ref().map(CodexAuth::auth_mode),
client_setup.api_auth.as_ref(),
+19 -24
View File
@@ -70,16 +70,19 @@ async fn selected_runtime_transport_stays_revoked(mode: RuntimeAuthMode) {
));
let mut client = test_model_client(SessionSource::Exec);
client.http_client_factory = factory;
Arc::get_mut(&mut client.state).unwrap().provider = create_model_provider(
ModelProviderInfo::create_openai_provider(Some(format!("{}/v1", server.uri()))),
Some(manager),
);
let client = client.with_runtime_auth(runtime, mode).unwrap();
let setup = client
Arc::get_mut(&mut client.state).unwrap().provider = codex_model_provider::ProviderAuthContext {
credentials: manager,
runtime_auth: Some((runtime, mode)),
}
.model_provider(ModelProviderInfo::create_openai_provider(
/*base_url*/ None,
));
let mut setup = client
.current_client_setup(ClientRouting::Workspace)
.await
.unwrap();
let transport = setup.build_api_transport("/responses").unwrap();
setup.api_provider.base_url = format!("{}/v1", server.uri());
let transport = setup.http_transport("/responses").unwrap();
let request = setup
.api_provider
.build_request(http::Method::POST, "/responses");
@@ -130,15 +133,10 @@ async fn selected_runtime_transport_stays_revoked(mode: RuntimeAuthMode) {
server.verify().await;
}
#[test_case::test_case(RuntimeAuthMode::DualHeader, false; "dual_header")]
#[test_case::test_case(RuntimeAuthMode::AssertionOnly, false; "assertion_only")]
#[test_case::test_case(RuntimeAuthMode::DualHeader, true; "runtime_provider_dual_header")]
#[test_case::test_case(RuntimeAuthMode::AssertionOnly, true; "runtime_provider_assertion_only")]
#[test_case::test_case(RuntimeAuthMode::DualHeader; "dual_header")]
#[test_case::test_case(RuntimeAuthMode::AssertionOnly; "assertion_only")]
#[tokio::test]
async fn realtime_keeps_provider_auth_until_runtime_integration(
mode: RuntimeAuthMode,
runtime_provider: bool,
) {
async fn realtime_keeps_provider_auth_until_runtime_integration(mode: RuntimeAuthMode) {
let (mut client, _) = model_client_with_counting_attestation(/*include_attestation*/ true);
let registration = MockServer::start().await;
let manager = client.auth_manager().unwrap();
@@ -158,16 +156,13 @@ async fn realtime_keeps_provider_auth_until_runtime_integration(
},
format!("{}/register", registration.uri()).parse().unwrap(),
));
if runtime_provider {
let info = client.state.provider.info().clone();
Arc::get_mut(&mut client.state).unwrap().provider =
codex_model_provider::ProviderAuthContext {
credentials: manager,
runtime_auth: Some((Arc::clone(&runtime), mode)),
}
.model_provider(info);
let info = client.state.provider.info().clone();
Arc::get_mut(&mut client.state).unwrap().provider = codex_model_provider::ProviderAuthContext {
credentials: manager,
runtime_auth: Some((runtime, mode)),
}
let client = client.with_runtime_auth(runtime, mode).unwrap();
.model_provider(info);
assert!(client.state.provider.runtime_auth().is_some());
let sideband = client
.realtime_sideband_headers(http::HeaderMap::new())
.await
+11 -12
View File
@@ -125,10 +125,9 @@ fn test_model_client_with_thread_id(
) -> ModelClient {
let provider = create_oss_provider_with_base_url("https://example.com/v1", WireApi::Responses);
ModelClient::new(
/*auth_manager*/ None,
codex_model_provider::create_model_provider(provider, /*auth_manager*/ None),
AgentIdentityAuthPolicy::JwtOnly,
thread_id,
provider,
session_source,
"test_originator".to_string(),
/*model_verbosity*/ None,
@@ -227,13 +226,14 @@ async fn workspace_routed_http_rejects_redirects_without_a_routing_header(
format!("{}/register", registration.uri()).parse().unwrap(),
));
let mut client = test_model_client(SessionSource::Exec);
Arc::get_mut(&mut client.state).unwrap().provider = create_model_provider(
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
Some(manager),
);
if let Some(mode) = runtime_mode {
client = client.with_runtime_auth(runtime, mode).unwrap();
}
Arc::get_mut(&mut client.state).unwrap().provider =
codex_model_provider::ProviderAuthContext {
credentials: manager,
runtime_auth: runtime_mode.map(|mode| (runtime, mode)),
}
.model_provider(ModelProviderInfo::create_openai_provider(
/*base_url*/ None,
));
let mut setup = client
.current_client_setup(super::ClientRouting::Workspace)
.await
@@ -246,7 +246,7 @@ async fn workspace_routed_http_rejects_redirects_without_a_routing_header(
}
// Exercise the resolved route's redirect policy against loopback HTTP servers.
setup.api_provider.base_url = origin.uri();
let transport = setup.build_api_transport("/responses").unwrap();
let transport = setup.http_transport("/responses").unwrap();
let request = setup
.api_provider
.build_request(http::Method::POST, "/responses")
@@ -2147,10 +2147,9 @@ fn model_client_with_counting_attestation(
)
};
let model_client = ModelClient::new(
auth_manager,
codex_model_provider::create_model_provider(provider, auth_manager),
AgentIdentityAuthPolicy::JwtOnly,
ThreadId::new(),
provider,
SessionSource::Exec,
"test_originator".to_string(),
/*model_verbosity*/ None,
+1
View File
@@ -114,6 +114,7 @@ pub(crate) async fn run_codex_thread_interactive(
instructions,
installation_id: parent_session.installation_id.clone(),
auth_manager,
runtime_auth: parent_session.services.runtime_auth.clone(),
models_manager,
git_root_discovery: Arc::clone(&parent_session.services.git_root_discovery),
environment_manager: parent_session
+13
View File
@@ -76,6 +76,7 @@ use codex_features::Features;
use codex_features::FeaturesToml;
use codex_features::MultiAgentV2ConfigToml;
use codex_features::NetworkProxyConfigToml;
use codex_features::RuntimeAuthMode;
use codex_features::SleepToolMode;
use codex_features::TokenBudgetConfigToml;
use codex_git_utils::resolve_root_git_project_for_trust;
@@ -1166,6 +1167,8 @@ pub struct Config {
pub current_time_reminder: Option<CurrentTimeReminderConfig>,
/// How the sleep tool is selected when its feature gate is enabled.
pub sleep_tool_mode: SleepToolMode,
/// Authentication mode used when runtime agent instances are enabled.
pub runtime_auth_mode: RuntimeAuthMode,
/// Centralized feature flags; source of truth for feature gating.
pub features: ManagedFeatures,
@@ -4033,6 +4036,15 @@ impl Config {
FeatureToml::Config(config) => config.mode,
})
.unwrap_or_default();
let runtime_auth_mode = cfg
.features
.as_ref()
.and_then(|features| features.runtime_agent_instances.as_ref())
.and_then(|feature| match feature {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => config.mode,
})
.unwrap_or_default();
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
let agent_roles =
@@ -4754,6 +4766,7 @@ impl Config {
rollout_budget,
current_time_reminder,
sleep_tool_mode,
runtime_auth_mode,
features,
runtime_feature_defaults: BTreeMap::new(),
suppress_unstable_features_warning: cfg
+6 -3
View File
@@ -27,8 +27,12 @@ pub(crate) fn install(session: &Session, config: &Config) {
);
let manager = Arc::new(crate::ThreadManager::new(
config,
Arc::clone(&session.services.auth_manager),
Arc::clone(&session.services.models_manager),
crate::build_startup_models(
config,
Arc::clone(&session.services.auth_manager),
session.installation_id.clone(),
crate::test_support::fixed_model_catalog(Arc::clone(&session.services.models_manager)),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
session.services.turn_environments.environment_manager(),
@@ -40,7 +44,6 @@ pub(crate) fn install(session: &Session, config: &Config) {
crate::passthrough_image_store(),
Arc::clone(&session.services.thread_store),
/*agent_graph_store*/ None,
session.installation_id.clone(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
));
+5
View File
@@ -183,10 +183,12 @@ pub use thread_manager::InternalSessionParent;
pub use thread_manager::NewThread;
pub use thread_manager::PromptCacheKey;
pub use thread_manager::StartThreadOptions;
pub use thread_manager::StartupModels;
pub use thread_manager::ThreadManager;
pub use thread_manager::ThreadRegistration;
pub use thread_manager::ThreadShutdownReport;
pub use thread_manager::build_models_manager;
pub use thread_manager::build_startup_models;
pub use thread_manager::local_agent_graph_store_from_state_db;
pub use thread_manager::passthrough_image_store;
pub use thread_store_builder::ThreadStoreBuilder;
@@ -281,3 +283,6 @@ pub mod otel_init;
// Captured environment bindings can be passed back to ThreadManager by internal reviewers.
pub use environment_selection::TurnEnvironmentSnapshot;
pub use environment_selection::validate_environment_ids_and_cwds;
mod runtime_auth;
pub use runtime_auth::build_runtime_auth_manager;
+8 -3
View File
@@ -5,6 +5,7 @@ use codex_exec_server::ExecServerRuntimeOptions;
use codex_extension_api::ExtensionRegistry;
use codex_extension_api::UserInstructionsProvider;
use codex_login::AuthManager;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ResponseItem;
@@ -44,10 +45,15 @@ pub async fn build_prompt_input(
let thread_store = ThreadStoreBuilder::from_config(&config, state_db.clone()).build();
let installation_id = resolve_installation_id(&config.codex_home).await?;
let thread_manager = ThreadManager::new(
let startup = crate::build_startup_models(
&config,
Arc::clone(&auth_manager),
crate::thread_manager::build_models_manager(&config, Arc::clone(&auth_manager)),
installation_id,
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
);
let thread_manager = ThreadManager::new(
&config,
startup,
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(
@@ -65,7 +71,6 @@ pub async fn build_prompt_input(
crate::thread_manager::passthrough_image_store(),
thread_store,
crate::local_agent_graph_store_from_state_db(state_db.as_ref()),
installation_id,
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
+53
View File
@@ -0,0 +1,53 @@
//! Construct and share the harness-owned runtime credential context.
use crate::config::Config;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_runtime_agent_instance::RuntimeAuthManager;
use codex_runtime_agent_instance::RuntimeRegistrationMetadata;
use codex_runtime_agent_instance::RuntimeRole;
use std::sync::Arc;
use std::sync::OnceLock;
// A later thread may enable runtime auth even when harness startup left it disabled.
pub(crate) type RuntimeAuthSlot = Arc<OnceLock<Arc<RuntimeAuthManager>>>;
/// Share this owner between thread sessions and first-party API clients.
/// Construction does not register a runtime or generate its key.
pub fn build_runtime_auth_manager(
config: &Config,
credentials: Arc<AuthManager>,
installation_id: &str,
) -> Option<Arc<RuntimeAuthManager>> {
runtime_for_config(
&RuntimeAuthSlot::default(),
config,
credentials,
installation_id,
)
}
pub(crate) fn runtime_for_config(
slot: &RuntimeAuthSlot,
config: &Config,
credentials: Arc<AuthManager>,
installation_id: &str,
) -> Option<Arc<RuntimeAuthManager>> {
config
.features
.enabled(Feature::RuntimeAgentInstances)
.then(|| {
Arc::clone(slot.get_or_init(|| {
Arc::new(RuntimeAuthManager::for_chatgpt(
credentials,
installation_id.to_owned(),
config.chatgpt_base_url.clone(),
RuntimeRegistrationMetadata {
harness: Some("codex".to_string()),
harness_version: Some(env!("CARGO_PKG_VERSION").to_string()),
role: RuntimeRole::Orchestrator,
},
))
}))
})
}
+4
View File
@@ -218,6 +218,10 @@ impl Session {
published: false,
};
let auth = self.services.auth_manager.auth().await;
// Capture after claiming the refresh so a new invalidation cannot be consumed
// with the environment snapshot taken before its permissions changed.
let environments = self.services.turn_environments.snapshot().await;
let environment_selections = environments.all_selections();
let desired = self.latest_mcp_desired_state(auth, environments).await;
let selected_capability_roots = self
.resolve_selected_capability_roots_for_step(&desired.environments)
+19 -6
View File
@@ -2,6 +2,7 @@ pub(crate) mod startup;
use crate::PromptCacheKey;
use crate::WithTurnExtensionData;
use crate::runtime_auth::RuntimeAuthSlot;
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -235,7 +236,6 @@ use crate::thread_rollout_truncation::initial_history_has_prior_user_turns;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerSource;
use codex_config::types::McpServerConfig;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails;
@@ -509,6 +509,7 @@ pub(crate) struct SessionSpawnArgs {
pub(crate) instructions: SessionInstructions,
pub(crate) installation_id: String,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) runtime_auth: RuntimeAuthSlot,
pub(crate) models_manager: SharedModelsManager,
pub(crate) git_root_discovery: Arc<GitRootDiscovery>,
pub(crate) environment_manager: Arc<EnvironmentManager>,
@@ -630,6 +631,7 @@ impl Session {
instructions,
installation_id,
auth_manager,
runtime_auth,
models_manager,
git_root_discovery,
environment_manager,
@@ -758,12 +760,19 @@ impl Session {
};
let mut config = Arc::new(config);
let configured_runtime = crate::runtime_auth::runtime_for_config(
&runtime_auth,
config.as_ref(),
Arc::clone(&auth_manager),
&installation_id,
);
let refresh_strategy = if session_source.is_non_root_agent() {
codex_models_manager::manager::RefreshStrategy::Offline
} else {
codex_models_manager::manager::RefreshStrategy::OnlineIfUncached
};
if config.model.is_none()
if configured_runtime.is_some()
|| config.model.is_none()
|| !matches!(
refresh_strategy,
codex_models_manager::manager::RefreshStrategy::Offline
@@ -951,12 +960,15 @@ impl Session {
let enable_full_access_form_input = !root_capabilities_restricted
&& matches!(thread_source.as_ref(), Some(ThreadSource::User))
&& client_mcp_extensions.contains(OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID);
let provider_auth = codex_model_provider::ProviderAuthContext {
credentials: Arc::clone(&auth_manager),
runtime_auth: configured_runtime.map(|runtime| (runtime, config.runtime_auth_mode)),
};
// Replace any inherited context before startup hooks see this thread's effective auth.
thread_extension_init.insert(provider_auth.clone());
let session_configuration = SessionConfiguration {
turn_extension_init,
provider: create_model_provider(
config.model_provider.clone(),
Some(Arc::clone(&auth_manager)),
),
provider: provider_auth.model_provider(config.model_provider.clone()),
step_settings: Arc::new(StepSettings {
collaboration_mode,
reasoning_summary: config.model_reasoning_summary,
@@ -1014,6 +1026,7 @@ impl Session {
instructions,
installation_id,
auth_manager.clone(),
runtime_auth,
models_manager.clone(),
git_root_discovery,
model_info,
+46 -43
View File
@@ -20,6 +20,7 @@ use crate::mcp_tool_call::McpToolApprovalContext;
use crate::responses_metadata::CodexResponsesMetadata;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::CompactionTurnMetadata;
use crate::runtime_auth::RuntimeAuthSlot;
use crate::shell_snapshot::ShellSnapshot;
use crate::shell_snapshot::SnapshotCredentialBrokerState;
use crate::state::ActiveTurn;
@@ -833,6 +834,7 @@ impl Session {
mut instructions: SessionInstructions,
installation_id: String,
auth_manager: Arc<AuthManager>,
runtime_auth: RuntimeAuthSlot,
models_manager: SharedModelsManager,
git_root_discovery: Arc<GitRootDiscovery>,
model_info: CatalogModel,
@@ -1794,6 +1796,48 @@ impl Session {
);
let reasoning_effort_override_enabled =
config.features.enabled(Feature::ReasoningEffortOverride) && !title_request;
let model_client = ModelClient::new(
Arc::clone(&session_configuration.provider),
if config.features.enabled(Feature::UseAgentIdentity) {
AgentIdentityAuthPolicy::ChatGptAuth
} else {
AgentIdentityAuthPolicy::JwtOnly
},
thread_id,
session_configuration.session_source.clone(),
session_configuration.originator.clone(),
config.model_verbosity,
config.features.enabled(Feature::ContentItemKinds),
reasoning_effort_override_enabled,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
Self::build_model_client_beta_features_header(config.as_ref()),
/*concurrent_reasoning_summaries_enabled*/
config
.features
.enabled(Feature::ConcurrentReasoningSummaries),
config.features.enabled(Feature::OutputTokenReplay),
attestation_provider.clone(),
config.http_client_factory(),
workspace_routing.as_ref().clone(),
extensions.model_request_contributors().to_vec(),
)
.with_executed_tool_calls(executed_tool_calls.clone())
.with_restored_history(matches!(
&initial_history,
InitialHistory::Resumed(_) | InitialHistory::Forked(_)
))
.with_session_context(
prompt_cache_key,
crate::guardian::prompt_cache_key_override_for_review_session(
&session_configuration.session_source,
session_configuration.parent_thread_id,
)
.or(fork_cache_key),
tx_event.clone(),
codex_responses_headers,
crate::cyber_access_program::ApiKeyCyberAccessPrograms::from_config(&config),
);
// Once tracing starts, startup cleanup must own the session before the next await.
let rollout_thread_trace = if matches!(
session_configuration.session_source,
@@ -1832,6 +1876,7 @@ impl Session {
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
exec_policy,
auth_manager: Arc::clone(&auth_manager),
runtime_auth,
openai_file_upload_client_pool: RouteAwareClientPool::new_without_request_logging(
config.http_client_factory(),
ClientRouteClass::Api,
@@ -1866,49 +1911,7 @@ impl Session {
thread_store: Arc::clone(&thread_store),
attestation_provider: attestation_provider.clone(),
time_provider,
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
if config.features.enabled(Feature::UseAgentIdentity) {
AgentIdentityAuthPolicy::ChatGptAuth
} else {
AgentIdentityAuthPolicy::JwtOnly
},
thread_id,
session_configuration.provider.info().clone(),
session_configuration.session_source.clone(),
session_configuration.originator.clone(),
config.model_verbosity,
config.features.enabled(Feature::ContentItemKinds),
reasoning_effort_override_enabled,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
Self::build_model_client_beta_features_header(config.as_ref()),
/*concurrent_reasoning_summaries_enabled*/
config
.features
.enabled(Feature::ConcurrentReasoningSummaries),
config.features.enabled(Feature::OutputTokenReplay),
attestation_provider,
config.http_client_factory(),
workspace_routing.as_ref().clone(),
extensions.model_request_contributors().to_vec(),
)
.with_executed_tool_calls(executed_tool_calls.clone())
.with_restored_history(matches!(
&initial_history,
InitialHistory::Resumed(_) | InitialHistory::Forked(_)
))
.with_session_context(
prompt_cache_key,
crate::guardian::prompt_cache_key_override_for_review_session(
&session_configuration.session_source,
session_configuration.parent_thread_id,
)
.or(fork_cache_key),
tx_event.clone(),
codex_responses_headers,
crate::cyber_access_program::ApiKeyCyberAccessPrograms::from_config(&config),
),
model_client,
executed_tool_calls: executed_tool_calls.clone(),
code_mode_service: crate::tools::code_mode::CodeModeService::new(
thread_id,
+16 -6
View File
@@ -895,10 +895,12 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
let thread_id = ThreadId::try_from("00000000-0000-4000-8000-000000000001")
.expect("test thread id should be valid");
crate::client::ModelClient::new(
/*auth_manager*/ None,
codex_model_provider::create_model_provider(
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
/*auth_manager*/ None,
),
AgentIdentityAuthPolicy::JwtOnly,
thread_id,
ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None),
codex_protocol::protocol::SessionSource::Exec,
"test_originator".to_string(),
/*model_verbosity*/ None,
@@ -7077,6 +7079,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
exec_policy,
auth_manager: auth_manager.clone(),
runtime_auth: Default::default(),
openai_file_upload_client_pool: RouteAwareClientPool::new_without_request_logging(
config.http_client_factory(),
ClientRouteClass::Api,
@@ -7116,10 +7119,12 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
attestation_provider: None,
time_provider: Arc::new(crate::current_time::SystemTimeProvider),
model_client: ModelClient::new(
Some(auth_manager.clone()),
codex_model_provider::create_model_provider(
session_configuration.provider.info().clone(),
Some(auth_manager.clone()),
),
AgentIdentityAuthPolicy::JwtOnly,
thread_id,
session_configuration.provider.info().clone(),
session_configuration.session_source.clone(),
session_configuration.originator.clone(),
config.model_verbosity,
@@ -7350,6 +7355,7 @@ async fn make_session_with_config_and_rx(
SessionInstructions::default(),
"11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
/*runtime_auth*/ Default::default(),
models_manager,
Arc::default(),
model_info,
@@ -7489,6 +7495,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
SessionInstructions::default(),
"11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
/*runtime_auth*/ Default::default(),
models_manager,
Arc::default(),
model_info,
@@ -9407,6 +9414,7 @@ where
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
exec_policy,
auth_manager: Arc::clone(&auth_manager),
runtime_auth: Default::default(),
openai_file_upload_client_pool: RouteAwareClientPool::new_without_request_logging(
config.http_client_factory(),
ClientRouteClass::Api,
@@ -9446,10 +9454,12 @@ where
attestation_provider: None,
time_provider: Arc::new(crate::current_time::SystemTimeProvider),
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
codex_model_provider::create_model_provider(
session_configuration.provider.info().clone(),
Some(Arc::clone(&auth_manager)),
),
AgentIdentityAuthPolicy::JwtOnly,
thread_id,
session_configuration.provider.info().clone(),
session_configuration.session_source.clone(),
session_configuration.originator.clone(),
config.model_verbosity,
@@ -1338,6 +1338,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
instructions: Default::default(),
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
runtime_auth: Default::default(),
models_manager,
git_root_discovery: Arc::default(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
+2
View File
@@ -15,6 +15,7 @@ use crate::environment_selection::ThreadEnvironments;
use crate::exec_policy::ExecPolicyManager;
use crate::mcp::McpManager;
use crate::mcp_tool_exposure::McpHandlerCache;
use crate::runtime_auth::RuntimeAuthSlot;
use crate::tools::ExecutedToolCalls;
use crate::tools::code_mode::CodeModeService;
use crate::tools::handlers::ToolSearchHandlerCache;
@@ -62,6 +63,7 @@ pub(crate) struct SessionServices {
pub(crate) show_raw_agent_reasoning: bool,
pub(crate) exec_policy: Arc<ExecPolicyManager>,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) runtime_auth: RuntimeAuthSlot,
/// Upload-only clients shared across turns without logging signed blob URLs.
pub(crate) openai_file_upload_client_pool: RouteAwareClientPool,
pub(crate) models_manager: SharedModelsManager,
+24
View File
@@ -41,6 +41,30 @@ use crate::responses_metadata::subagent_metadata_kind;
use crate::thread_manager;
use crate::unified_exec;
/// Supply fixed model metadata to tests without implementing a provider.
pub fn fixed_model_catalog(
manager: SharedModelsManager,
) -> Arc<dyn codex_model_provider::ModelCatalogFactory> {
struct FixedCatalog(SharedModelsManager);
impl codex_model_provider::ModelCatalogFactory for FixedCatalog {
fn build(
&self,
_provider: codex_model_provider::SharedModelProvider,
_config_model_catalog: Option<codex_protocol::openai_models::ModelsResponse>,
_api_key_model_discovery_default: bool,
) -> SharedModelsManager {
Arc::clone(&self.0)
}
fn set_api_key_model_discovery_enabled(&self, enabled: bool) {
self.0.set_api_key_model_discovery_enabled(enabled);
}
}
Arc::new(FixedCatalog(manager))
}
static TEST_MODEL_PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
let mut response = bundled_models_response()
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
+110 -37
View File
@@ -1,4 +1,5 @@
mod managed;
mod model_catalog;
mod shared_instructions;
mod shutdown;
@@ -72,7 +73,6 @@ use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR;
use codex_login::default_client::originator;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::OPENAI_PROVIDER_ID;
use codex_models_manager::manager::RefreshStrategy;
@@ -429,6 +429,7 @@ struct ThreadSpawnRequest {
startup: Option<Arc<crate::session::startup::SessionStartup>>,
options: StartThreadOptions,
auth_manager: Arc<AuthManager>,
inherited_runtime_auth: Option<crate::runtime_auth::RuntimeAuthSlot>,
agent_control: AgentControlInit,
parent_thread_id: Option<ThreadId>,
parent_originator: Option<String>,
@@ -452,6 +453,7 @@ impl ThreadSpawnRequest {
startup: None,
options,
auth_manager,
inherited_runtime_auth: None,
agent_control: agent_control.into(),
parent_thread_id: None,
parent_originator: None,
@@ -521,8 +523,8 @@ pub(crate) struct ThreadManagerState {
thread_id_generator: ThreadIdGenerator,
agent_control_factory: Option<AgentControlFactory>,
auth_manager: Arc<AuthManager>,
models_manager: SharedModelsManager,
models_manager_provider: ModelProviderInfo,
runtime_auth: crate::runtime_auth::RuntimeAuthSlot,
model_catalog: model_catalog::ThreadModelCatalog,
git_root_discovery: Arc<GitRootDiscovery>,
environment_manager: Arc<EnvironmentManager>,
starting_mcp_runtimes: std::sync::Mutex<Vec<std::sync::Weak<AtomicBool>>>,
@@ -544,20 +546,9 @@ pub(crate) struct ThreadManagerState {
ops_log: Option<SharedCapturedOps>,
}
pub fn build_models_manager(
config: &Config,
auth_manager: Arc<AuthManager>,
) -> SharedModelsManager {
let provider = create_model_provider(config.model_provider.clone(), Some(auth_manager));
let manager = provider.models_manager(
config.codex_home.to_path_buf(),
config.model_catalog.clone(),
);
manager.set_api_key_model_discovery_enabled(
config.features.enabled(Feature::ApiKeyModelDiscovery),
);
manager
}
pub use model_catalog::StartupModels;
pub use model_catalog::build_models_manager;
pub use model_catalog::build_startup_models;
/// Constructs the default image store that preserves inline images.
pub fn passthrough_image_store() -> Arc<dyn AttachmentStore> {
@@ -577,8 +568,7 @@ impl ThreadManager {
#[allow(clippy::too_many_arguments)]
pub fn new(
config: &Config,
auth_manager: Arc<AuthManager>,
models_manager: SharedModelsManager,
startup: StartupModels,
codex_apps_tools_cache: CodexAppsToolsCache,
session_source: SessionSource,
environment_manager: Arc<EnvironmentManager>,
@@ -588,10 +578,15 @@ impl ThreadManager {
image_store: Arc<dyn AttachmentStore>,
thread_store: Arc<dyn ThreadStore>,
agent_graph_store: Option<Arc<dyn AgentGraphStore>>,
installation_id: String,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
external_time_provider: Option<Arc<dyn TimeProvider>>,
) -> Self {
let StartupModels {
credentials: auth_manager,
runtime_auth,
installation_id,
catalog: model_catalog,
} = startup;
let codex_home = config.codex_home.clone();
let restriction_product = session_source.restriction_product();
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
@@ -629,8 +624,7 @@ impl ThreadManager {
thread_created_tx,
thread_id_generator: default_thread_id_generator(),
agent_control_factory: None,
models_manager,
models_manager_provider: config.model_provider.clone(),
model_catalog,
git_root_discovery: Arc::default(),
environment_manager,
starting_mcp_runtimes: std::sync::Mutex::new(Vec::new()),
@@ -646,6 +640,7 @@ impl ThreadManager {
attestation_provider,
external_time_provider,
auth_manager,
runtime_auth,
session_source,
installation_id,
analytics_events_client,
@@ -834,14 +829,24 @@ impl ThreadManager {
let agent_graph_store = local_agent_graph_store_from_state_db(state_db.as_ref());
Self {
state: Arc::new(ThreadManagerState {
runtime_auth: Default::default(),
threads: Arc::new(RwLock::new(HashMap::new())),
shared_thread_instructions: Default::default(),
thread_created_tx,
thread_id_generator: default_thread_id_generator(),
agent_control_factory: None,
models_manager_provider: provider.clone(),
models_manager: create_model_provider(provider, Some(auth_manager.clone()))
.models_manager(codex_home, /*config_model_catalog*/ None),
model_catalog: model_catalog::ThreadModelCatalog::new(
provider,
/*catalog*/ None,
Arc::new(codex_models_manager::manager::ModelCatalogSettings::disk(
codex_home,
)),
/*api_key_model_discovery_default*/ false,
codex_model_provider::ProviderAuthContext {
credentials: Arc::clone(&auth_manager),
runtime_auth: None,
},
),
git_root_discovery: Arc::default(),
environment_manager,
starting_mcp_runtimes: std::sync::Mutex::new(Vec::new()),
@@ -982,7 +987,31 @@ impl ThreadManager {
}
pub fn get_models_manager(&self) -> SharedModelsManager {
self.state.models_manager.clone()
self.state.model_catalog.manager.clone()
}
/// Select the app-server catalog using the same owner as thread requests.
pub fn models_manager_for_config(&self, config: &Config) -> SharedModelsManager {
let credentials = self.auth_manager();
let runtime = crate::runtime_auth::runtime_for_config(
&self.state.runtime_auth,
config,
Arc::clone(&credentials),
&self.state.installation_id,
);
self.state.model_catalog.for_config(
config,
config.model_catalog.as_ref(),
credentials,
runtime,
/*same_owner*/ true,
)
}
pub fn set_api_key_model_discovery_enabled(&self, enabled: bool) {
self.state
.model_catalog
.set_api_key_model_discovery_enabled(enabled);
}
pub async fn list_models(
@@ -991,13 +1020,14 @@ impl ThreadManager {
http_client_factory: codex_http_client::HttpClientFactory,
) -> Vec<ModelPreset> {
self.state
.models_manager
.model_catalog
.manager
.list_models(refresh_strategy, http_client_factory)
.await
}
pub fn list_collaboration_modes(&self) -> Vec<CollaborationModeMask> {
self.state.models_manager.list_collaboration_modes()
self.state.model_catalog.manager.list_collaboration_modes()
}
pub async fn list_thread_ids(&self) -> Vec<ThreadId> {
@@ -2081,8 +2111,9 @@ impl ThreadManagerState {
(options.client_mcp_extensions, options.prompt_cache_key) =
self.inherited_child_settings(parent_thread_id).await;
let forked = matches!(options.initial_history, InitialHistory::Forked(_));
let mut request =
ThreadSpawnRequest::new(options, Arc::clone(&self.auth_manager), agent_control);
let (auth_manager, runtime_auth) = self.inherited_child_auth(parent_thread_id).await;
let mut request = ThreadSpawnRequest::new(options, auth_manager, agent_control);
request.inherited_runtime_auth = Some(runtime_auth);
request.parent_thread_id = parent_thread_id;
request.inherited_exec_policy = inherited_exec_policy;
if forked {
@@ -2103,8 +2134,9 @@ impl ThreadManagerState {
options.reserved_thread_id.ok_or_else(|| {
CodexErr::InvalidRequest("prepared child startup requires a reserved thread ID".into())
})?;
let mut request =
ThreadSpawnRequest::new(options, Arc::clone(&self.auth_manager), agent_control);
let (auth_manager, runtime_auth) = self.inherited_child_auth(Some(parent_thread_id)).await;
let mut request = ThreadSpawnRequest::new(options, auth_manager, agent_control);
request.inherited_runtime_auth = Some(runtime_auth);
request.ready_notification = ThreadReadyNotification::Defer;
request.parent_thread_id = Some(parent_thread_id);
let history_initialization = match &request.options.initial_history {
@@ -2175,6 +2207,23 @@ impl ThreadManagerState {
Box::pin(self.spawn_thread(request)).await
}
async fn inherited_child_auth(
&self,
parent_thread_id: Option<ThreadId>,
) -> (Arc<AuthManager>, crate::runtime_auth::RuntimeAuthSlot) {
let threads = self.threads.read().await;
match parent_thread_id.and_then(|id| threads.get(&id)) {
Some(parent) => (
Arc::clone(&parent.session.services.auth_manager),
Arc::clone(&parent.session.services.runtime_auth),
),
None => (
Arc::clone(&self.auth_manager),
Arc::clone(&self.runtime_auth),
),
}
}
async fn inherited_child_settings(
&self,
parent_thread_id: Option<ThreadId>,
@@ -2226,6 +2275,7 @@ impl ThreadManagerState {
startup,
options,
auth_manager,
inherited_runtime_auth,
agent_control,
parent_thread_id,
parent_originator,
@@ -2527,14 +2577,36 @@ impl ThreadManagerState {
} else {
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile
};
let models_manager = if config.model_provider == self.models_manager_provider {
Arc::clone(&self.models_manager)
// Resume may supply a different credential owner. Keep its runtime separate from the
// harness default, and let sessions using that parent's credentials inherit its owner.
let runtime_auth = if let Some(runtime_auth) = inherited_runtime_auth {
runtime_auth
} else if Arc::ptr_eq(&auth_manager, &self.auth_manager) {
Arc::clone(&self.runtime_auth)
} else {
build_models_manager(&config, Arc::clone(&auth_manager))
let threads = self.threads.read().await;
parent_thread_id
.or(forked_from_thread_id)
.and_then(|id| threads.get(&id))
.filter(|parent| Arc::ptr_eq(&parent.session.services.auth_manager, &auth_manager))
.map(|parent| Arc::clone(&parent.session.services.runtime_auth))
.unwrap_or_default()
};
let models_manager = models_manager
let runtime = crate::runtime_auth::runtime_for_config(
&runtime_auth,
&config,
Arc::clone(&auth_manager),
&self.installation_id,
);
let manager = self.model_catalog.for_thread(
&config,
Arc::clone(&auth_manager),
runtime,
Arc::ptr_eq(&auth_manager, &self.auth_manager),
);
let models_manager = manager
.for_originator(&startup_context.originator)
.unwrap_or(models_manager);
.unwrap_or(manager);
let spawn_result = Session::spawn(SessionSpawnArgs {
attached_thread,
startup,
@@ -2544,6 +2616,7 @@ impl ThreadManagerState {
instructions: startup_context.instructions,
installation_id: self.installation_id.clone(),
auth_manager,
runtime_auth,
models_manager,
git_root_discovery: Arc::clone(&self.git_root_discovery),
environment_manager: Arc::clone(&self.environment_manager),
@@ -0,0 +1,166 @@
//! Keep each catalog bound to its provider and auth owner while preserving host cache policy.
use crate::config::Config;
use crate::runtime_auth::RuntimeAuthSlot;
use crate::runtime_auth::runtime_for_config;
use codex_features::Feature;
use codex_features::RuntimeAuthMode;
use codex_login::AuthManager;
use codex_model_provider::ModelCatalogFactory;
use codex_model_provider::ProviderAuthContext;
use codex_model_provider_info::ModelProviderInfo;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::openai_models::ModelsResponse;
use codex_runtime_agent_instance::RuntimeAuthManager;
use std::sync::Arc;
/// The startup catalog and the credential/runtime owner used to construct it.
pub struct StartupModels {
pub(super) credentials: Arc<AuthManager>,
pub(super) runtime_auth: RuntimeAuthSlot,
pub(super) installation_id: String,
pub(super) catalog: ThreadModelCatalog,
}
impl StartupModels {
/// Share the startup catalog with host discovery and prefetch callers.
pub fn models_manager(&self) -> SharedModelsManager {
Arc::clone(&self.catalog.manager)
}
}
/// Construct the catalog and runtime owner together using the host's catalog factory.
/// Use `ModelCatalogSettings` for standard provider catalogs with a disk, custom, or no cache.
/// Runtime registration remains lazy; this does not register an instance or generate a key.
pub fn build_startup_models(
config: &Config,
credentials: Arc<AuthManager>,
installation_id: String,
factory: Arc<dyn ModelCatalogFactory>,
) -> StartupModels {
let runtime_auth = RuntimeAuthSlot::default();
let runtime = runtime_for_config(
&runtime_auth,
config,
Arc::clone(&credentials),
&installation_id,
);
let catalog = ThreadModelCatalog::new(
config.model_provider.clone(),
config.model_catalog.clone(),
factory,
config.features.enabled(Feature::ApiKeyModelDiscovery),
ProviderAuthContext {
credentials: Arc::clone(&credentials),
runtime_auth: runtime.map(|runtime| (runtime, config.runtime_auth_mode)),
},
);
StartupModels {
credentials,
runtime_auth,
installation_id,
catalog,
}
}
pub(super) struct ThreadModelCatalog {
pub(super) manager: SharedModelsManager,
provider: ModelProviderInfo,
catalog: Option<ModelsResponse>,
factory: Arc<dyn ModelCatalogFactory>,
api_key_model_discovery_default: bool,
runtime_mode: Option<RuntimeAuthMode>,
}
pub fn build_models_manager(
config: &Config,
credentials: Arc<AuthManager>,
runtime: Option<Arc<RuntimeAuthManager>>,
) -> SharedModelsManager {
let auth = ProviderAuthContext {
credentials,
runtime_auth: runtime
.filter(|_| config.features.enabled(Feature::RuntimeAgentInstances))
.map(|runtime| (runtime, config.runtime_auth_mode)),
};
ModelCatalogSettings::disk(config.codex_home.to_path_buf()).build(
auth.model_provider(config.model_provider.clone()),
config.model_catalog.clone(),
config.features.enabled(Feature::ApiKeyModelDiscovery),
)
}
impl ThreadModelCatalog {
pub(super) fn new(
provider: ModelProviderInfo,
catalog: Option<ModelsResponse>,
factory: Arc<dyn ModelCatalogFactory>,
api_key_model_discovery_default: bool,
auth: ProviderAuthContext,
) -> Self {
let runtime_mode = auth.runtime_auth.as_ref().map(|(_, mode)| *mode);
let manager = factory.build(
auth.model_provider(provider.clone()),
catalog.clone(),
api_key_model_discovery_default,
);
Self {
manager,
provider,
catalog,
factory,
api_key_model_discovery_default,
runtime_mode,
}
}
pub(super) fn for_thread(
&self,
config: &Config,
credentials: Arc<AuthManager>,
runtime: Option<Arc<RuntimeAuthManager>>,
same_owner: bool,
) -> SharedModelsManager {
let models = config.model_catalog.as_ref().or_else(|| {
self.catalog
.as_ref()
.filter(|_| self.provider == config.model_provider)
});
self.for_config(config, models, credentials, runtime, same_owner)
}
pub(super) fn for_config(
&self,
config: &Config,
models: Option<&ModelsResponse>,
credentials: Arc<AuthManager>,
runtime: Option<Arc<RuntimeAuthManager>>,
same_owner: bool,
) -> SharedModelsManager {
let runtime = runtime.map(|runtime| (runtime, config.runtime_auth_mode));
let api_key_model_discovery_default =
config.features.enabled(Feature::ApiKeyModelDiscovery);
if same_owner
&& self.runtime_mode == runtime.as_ref().map(|(_, mode)| *mode)
&& self.provider == config.model_provider
&& self.catalog.as_ref() == models
&& self.api_key_model_discovery_default == api_key_model_discovery_default
{
return Arc::clone(&self.manager);
}
let auth = ProviderAuthContext {
credentials,
runtime_auth: runtime,
};
self.factory.build(
auth.model_provider(config.model_provider.clone()),
models.cloned(),
api_key_model_discovery_default,
)
}
pub(super) fn set_api_key_model_discovery_enabled(&self, enabled: bool) {
self.factory.set_api_key_model_discovery_enabled(enabled);
}
}
+214 -51
View File
@@ -25,6 +25,7 @@ use codex_extension_api::SessionIsolation;
use codex_extension_api::empty_extension_registry;
use codex_history::InitialHistory;
use codex_history::ResumedHistory;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::ResponseItemId;
use codex_protocol::approvals::ExecPolicyAmendment;
@@ -678,6 +679,128 @@ async fn thread_id_generator_does_not_replace_resumed_thread_id() {
.expect("shut down resumed thread");
}
#[test_case::test_case(false; "local_child")]
#[test_case::test_case(true; "host_child")]
#[tokio::test]
async fn child_session_inherits_resumed_parent_auth(host_child: bool) {
let home = tempdir().expect("temporary home");
let mut config = test_config().await;
config.codex_home = home.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let manager = ThreadManager::with_models_provider_and_home_for_tests(
CodexAuth::from_api_key("key-a"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let original = manager
.start_thread(StartThreadOptions {
history_mode: Some(ThreadHistoryMode::Legacy),
..StartThreadOptions::new(config.clone())
})
.await
.expect("start original thread");
original.thread.ensure_rollout_materialized().await;
original
.thread
.flush_rollout()
.await
.expect("flush original history");
original
.thread
.shutdown_and_wait()
.await
.expect("stop original thread");
manager.remove_thread(&original.thread_id).await;
let replacement = AuthManager::from_auth_for_testing_with_home(
CodexAuth::from_api_key("key-b"),
config.codex_home.to_path_buf(),
);
let parent = manager
.resume_thread_with_history(
config.clone(),
InitialHistory::Resumed(ResumedHistory {
history_revision: None,
conversation_id: original.thread_id,
history: Arc::new(Vec::new()),
rollout_path: original.thread.rollout_path(),
}),
Arc::clone(&replacement),
/*parent_trace*/ None,
ClientMcpExtensions::default(),
)
.await
.expect("resume with replacement credentials");
let options = StartThreadOptions {
reserved_thread_id: host_child.then(|| manager.reserve_thread_id()),
session_source: Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: parent.thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
..StartThreadOptions::new(config)
};
let control = parent
.thread
.session
.services
.local_agent_runtime
.control(parent.thread.session.session_id());
let child = if host_child {
manager
.state
.spawn_host_child_thread(
options,
control.into(),
parent.thread_id,
crate::agent::host::PreparedChildInheritance {
instructions: parent.thread.session.inherited_instructions().await,
exec_policy: None,
originator: parent.thread.session.originator().await,
attachment_source: None,
},
)
.await
} else {
manager
.state
.spawn_child_thread(
options,
control,
Some(parent.thread_id),
/*inherited_exec_policy*/ None,
)
.await
}
.expect("spawn child under the resumed parent");
assert!(
Arc::ptr_eq(&child.thread.session.services.auth_manager, &replacement),
"child must use the resumed parent's credentials",
);
assert!(
Arc::ptr_eq(
&child.thread.session.services.runtime_auth,
&parent.thread.session.services.runtime_auth,
),
"child must share the resumed parent's runtime owner",
);
assert!(
!Arc::ptr_eq(
&child.thread.session.services.runtime_auth,
&manager.state.runtime_auth
),
"replacement-auth descendants must stay separate from the harness runtime",
);
let report = manager
.shutdown_all_threads_bounded(Duration::from_secs(10))
.await;
assert_eq!(report.completed.len(), 2);
}
#[tokio::test]
async fn child_session_inherits_client_mcp_extensions() {
let temp_dir = tempdir().expect("tempdir");
@@ -1250,8 +1373,12 @@ async fn mcp_invalidation_refreshes_threads_that_are_still_starting() {
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let manager = Arc::new(ThreadManager::new(
&config,
Arc::clone(&auth_manager),
build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
Arc::clone(&auth_manager),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -1261,7 +1388,6 @@ async fn mcp_invalidation_refreshes_threads_that_are_still_starting() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
));
@@ -1918,8 +2044,12 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -1929,7 +2059,6 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2365,8 +2494,12 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2376,7 +2509,6 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2525,8 +2657,12 @@ async fn explicit_installation_id_skips_codex_home_file() {
let thread_store = ThreadStoreBuilder::from_config(&config, state_db.clone()).build();
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
auth_manager.clone(),
installation_id.clone(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2536,7 +2672,6 @@ async fn explicit_installation_id_skips_codex_home_file() {
passthrough_image_store(),
thread_store,
local_agent_graph_store_from_state_db(state_db.as_ref()),
installation_id.clone(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2569,8 +2704,12 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2580,7 +2719,6 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2635,8 +2773,12 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2646,7 +2788,6 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2708,8 +2849,12 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
let thread_store = ThreadStoreBuilder::from_config(&config, state_db.clone()).build();
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2719,7 +2864,6 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
passthrough_image_store(),
thread_store,
local_agent_graph_store_from_state_db(state_db.as_ref()),
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2795,8 +2939,12 @@ async fn subtree_listing_uses_injected_graph_store_without_state_db() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2806,7 +2954,6 @@ async fn subtree_listing_uses_injected_graph_store_without_state_db() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
Some(agent_graph_store),
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2843,8 +2990,12 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
.expect("configured in-memory store");
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2854,7 +3005,6 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
passthrough_image_store(),
thread_store.clone(),
local_agent_graph_store_from_state_db(state_db.as_ref()),
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -2953,8 +3103,12 @@ async fn metadata_update_without_result_reads_only_when_the_caller_needs_the_thr
.expect("configured in-memory store");
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -2964,7 +3118,6 @@ async fn metadata_update_without_result_reads_only_when_the_caller_needs_the_thr
passthrough_image_store(),
thread_store.clone(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -3080,8 +3233,12 @@ async fn new_uses_active_provider_for_model_refresh() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -3091,7 +3248,6 @@ async fn new_uses_active_provider_for_model_refresh() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -3157,7 +3313,7 @@ async fn new_uses_active_provider_for_model_refresh() {
}
#[tokio::test]
async fn injected_models_manager_controls_refresh_policy() {
async fn startup_catalog_cache_policy_controls_refresh() {
let server = MockServer::start().await;
let _ = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
let _ = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
@@ -3172,15 +3328,14 @@ async fn injected_models_manager_controls_refresh_policy() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = create_model_provider(
config.model_provider.clone(),
Some(Arc::clone(&auth_manager)),
);
let models_manager = provider.models_manager_without_cache(config.model_catalog.clone());
let manager = ThreadManager::new(
&config,
auth_manager,
models_manager,
crate::build_startup_models(
&config,
auth_manager,
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::default()),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Custom("test-embedder".to_string()),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -3190,7 +3345,6 @@ async fn injected_models_manager_controls_refresh_policy() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, /*state_db*/ None).build(),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -3460,8 +3614,12 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
let state_db = init_state_db(&config).await;
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -3471,7 +3629,6 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, state_db.clone()).build(),
local_agent_graph_store_from_state_db(state_db.as_ref()),
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -3572,8 +3729,12 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
let state_db = init_state_db(&config).await;
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -3583,7 +3744,6 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, state_db.clone()).build(),
local_agent_graph_store_from_state_db(state_db.as_ref()),
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -3677,8 +3837,12 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
let state_db = init_state_db(&config).await;
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
crate::build_startup_models(
&config,
auth_manager.clone(),
TEST_INSTALLATION_ID.to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -3688,7 +3852,6 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, state_db.clone()).build(),
local_agent_graph_store_from_state_db(state_db.as_ref()),
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
@@ -37,6 +37,7 @@ use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::built_in_model_providers;
use codex_models_manager::manager::ModelCatalogSettings;
use codex_models_manager::manager::StaticModelsManager;
use codex_otel::MULTI_AGENT_SPAWN_FAILURE_METRIC;
use codex_otel::MetricsClient;
@@ -4319,8 +4320,12 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
crate::thread_manager::build_models_manager(&config, auth_manager),
crate::build_startup_models(
&config,
auth_manager.clone(),
"11111111-1111-4111-8111-111111111111".to_string(),
Arc::new(ModelCatalogSettings::disk(config.codex_home.to_path_buf())),
),
crate::CodexAppsToolsCache::default(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
@@ -4330,7 +4335,6 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
crate::thread_manager::passthrough_image_store(),
ThreadStoreBuilder::from_config(&config, state_db.clone()).build(),
local_agent_graph_store_from_state_db(state_db.as_ref()),
"11111111-1111-4111-8111-111111111111".to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);