From 5882f3f95e1fc727fe46631073c1d5918e4ae3ca Mon Sep 17 00:00:00 2001 From: efrazer-oai Date: Thu, 23 Apr 2026 17:14:02 -0700 Subject: [PATCH] refactor: route Codex auth through AuthProvider (#18811) ## Summary This PR moves Codex backend request authentication from direct bearer-token handling to `AuthProvider`. The new `codex-auth-provider` crate defines the shared request-auth trait. `CodexAuth::provider()` returns a provider that can apply all headers needed for the selected auth mode. This lets ChatGPT token auth and AgentIdentity auth share the same callsite path: - ChatGPT token auth applies bearer auth plus account/FedRAMP headers where needed. - AgentIdentity auth applies AgentAssertion plus account/FedRAMP headers where needed. Reference old stack: https://github.com/openai/codex/pull/17387/changes ## Callsite Migration | Area | Change | | --- | --- | | backend-client | accepts an `AuthProvider` instead of a raw token/header | | chatgpt client/connectors | applies auth through `CodexAuth::provider()` | | cloud tasks | keeps Codex-backend gating, applies auth through provider | | cloud requirements | uses Codex-backend auth checks and provider headers | | app-server remote control | applies provider headers for backend calls | | MCP Apps/connectors | gates on `uses_codex_backend()` and keys caches from generic account getters | | model refresh | treats AgentIdentity as Codex-backend auth | | OpenAI file upload path | rejects non-Codex-backend auth before applying headers | | core client setup | keeps model-provider auth flow and allows AgentIdentity through provider-backed OpenAI auth | ## Stack 1. https://github.com/openai/codex/pull/18757: full revert 2. https://github.com/openai/codex/pull/18871: isolated Agent Identity crate 3. https://github.com/openai/codex/pull/18785: explicit AgentIdentity auth mode and startup task allocation 4. This PR: migrate Codex backend auth callsites through AuthProvider 5. https://github.com/openai/codex/pull/18904: accept AgentIdentity JWTs and load `CODEX_AGENT_IDENTITY` ## Testing Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI. --- codex-rs/Cargo.lock | 16 +- codex-rs/analytics/Cargo.toml | 1 + codex-rs/analytics/src/client.rs | 12 +- codex-rs/app-server/Cargo.toml | 2 + .../app-server/src/codex_message_processor.rs | 10 +- codex-rs/app-server/src/message_processor.rs | 2 +- .../src/transport/remote_control/enroll.rs | 10 +- .../src/transport/remote_control/websocket.rs | 14 +- codex-rs/backend-client/Cargo.toml | 2 + codex-rs/backend-client/src/client.rs | 51 +++--- codex-rs/chatgpt/Cargo.toml | 2 +- codex-rs/chatgpt/src/apply_command.rs | 4 - codex-rs/chatgpt/src/chatgpt_client.rs | 30 ++-- codex-rs/chatgpt/src/chatgpt_token.rs | 36 ----- codex-rs/chatgpt/src/connectors.rs | 56 ++++--- codex-rs/chatgpt/src/lib.rs | 1 - codex-rs/cli/src/mcp_cmd.rs | 8 +- codex-rs/cloud-requirements/src/lib.rs | 12 +- codex-rs/cloud-tasks-client/Cargo.toml | 1 + codex-rs/cloud-tasks-client/src/http.rs | 9 +- codex-rs/cloud-tasks/Cargo.toml | 2 +- codex-rs/cloud-tasks/src/lib.rs | 26 ++- codex-rs/cloud-tasks/src/util.rs | 42 +---- codex-rs/codex-api/src/auth.rs | 7 + codex-rs/codex-mcp/Cargo.toml | 2 + codex-rs/codex-mcp/src/mcp/auth.rs | 36 +++-- codex-rs/codex-mcp/src/mcp/mod.rs | 68 +++----- .../codex-mcp/src/mcp_connection_manager.rs | 40 +++-- codex-rs/core-plugins/Cargo.toml | 1 + codex-rs/core-plugins/src/remote.rs | 13 +- codex-rs/core-plugins/src/remote_legacy.rs | 41 ++--- codex-rs/core-skills/Cargo.toml | 1 + codex-rs/core-skills/src/remote.rs | 33 ++-- codex-rs/core/src/arc_monitor.rs | 37 ++--- codex-rs/core/src/client.rs | 2 +- codex-rs/core/src/connectors.rs | 62 ++++--- codex-rs/core/src/mcp_openai_file.rs | 17 +- codex-rs/core/src/plugins/manager.rs | 16 +- codex-rs/core/src/session/handlers.rs | 7 +- codex-rs/core/src/session/mcp.rs | 4 +- codex-rs/core/src/session/mod.rs | 1 - codex-rs/core/src/session/session.rs | 2 + codex-rs/core/src/session/turn_context.rs | 13 +- codex-rs/login/src/auth/agent_identity.rs | 4 + codex-rs/login/src/auth/manager.rs | 12 ++ codex-rs/model-provider/Cargo.toml | 1 + codex-rs/model-provider/src/auth.rs | 153 +++++++++++++----- .../src/bearer_auth_provider.rs | 8 + codex-rs/model-provider/src/lib.rs | 2 + codex-rs/models-manager/src/manager.rs | 19 +-- codex-rs/protocol/src/account.rs | 29 ++++ codex-rs/rmcp-client/Cargo.toml | 1 + .../rmcp-client/src/http_client_adapter.rs | 20 ++- codex-rs/rmcp-client/src/rmcp_client.rs | 38 +++-- .../tests/streamable_http_test_support.rs | 2 + 55 files changed, 551 insertions(+), 490 deletions(-) delete mode 100644 codex-rs/chatgpt/src/chatgpt_token.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3f26f563f..b39807784 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1773,6 +1773,7 @@ dependencies = [ "codex-app-server-protocol", "codex-git-utils", "codex-login", + "codex-model-provider", "codex-plugin", "codex-protocol", "codex-utils-absolute-path", @@ -1840,6 +1841,7 @@ dependencies = [ "chrono", "clap", "codex-analytics", + "codex-api", "codex-app-server-protocol", "codex-arg0", "codex-backend-client", @@ -1856,6 +1858,7 @@ dependencies = [ "codex-git-utils", "codex-login", "codex-mcp", + "codex-model-provider", "codex-model-provider-info", "codex-models-manager", "codex-otel", @@ -2045,9 +2048,11 @@ name = "codex-backend-client" version = "0.0.0" dependencies = [ "anyhow", + "codex-api", "codex-backend-openapi-models", "codex-client", "codex-login", + "codex-model-provider", "codex-protocol", "pretty_assertions", "reqwest", @@ -2071,11 +2076,11 @@ dependencies = [ "anyhow", "clap", "codex-app-server-protocol", - "codex-config", "codex-connectors", "codex-core", "codex-git-utils", "codex-login", + "codex-model-provider", "codex-utils-cargo-bin", "codex-utils-cli", "pretty_assertions", @@ -2203,7 +2208,6 @@ version = "0.0.0" dependencies = [ "anyhow", "async-trait", - "base64 0.22.1", "chrono", "clap", "codex-client", @@ -2212,6 +2216,7 @@ dependencies = [ "codex-core", "codex-git-utils", "codex-login", + "codex-model-provider", "codex-tui", "codex-utils-cli", "crossterm", @@ -2236,6 +2241,7 @@ dependencies = [ "anyhow", "async-trait", "chrono", + "codex-api", "codex-backend-client", "codex-git-utils", "serde", @@ -2460,6 +2466,7 @@ dependencies = [ "codex-exec-server", "codex-git-utils", "codex-login", + "codex-model-provider", "codex-otel", "codex-plugin", "codex-protocol", @@ -2491,6 +2498,7 @@ dependencies = [ "codex-config", "codex-exec-server", "codex-login", + "codex-model-provider", "codex-otel", "codex-protocol", "codex-skills", @@ -2849,10 +2857,12 @@ version = "0.0.0" dependencies = [ "anyhow", "async-channel", + "codex-api", "codex-async-utils", "codex-config", "codex-exec-server", "codex-login", + "codex-model-provider", "codex-otel", "codex-plugin", "codex-protocol", @@ -2912,6 +2922,7 @@ name = "codex-model-provider" version = "0.0.0" dependencies = [ "async-trait", + "codex-agent-identity", "codex-api", "codex-aws-auth", "codex-client", @@ -3154,6 +3165,7 @@ dependencies = [ "anyhow", "axum", "bytes", + "codex-api", "codex-client", "codex-config", "codex-exec-server", diff --git a/codex-rs/analytics/Cargo.toml b/codex-rs/analytics/Cargo.toml index f706814d4..918e7edc7 100644 --- a/codex-rs/analytics/Cargo.toml +++ b/codex-rs/analytics/Cargo.toml @@ -16,6 +16,7 @@ workspace = true codex-app-server-protocol = { workspace = true } codex-git-utils = { workspace = true } codex-login = { workspace = true } +codex-model-provider = { workspace = true } codex-plugin = { workspace = true } codex-protocol = { workspace = true } os_info = { workspace = true } diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index a3a20231f..e145a00d1 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -312,16 +312,9 @@ async fn send_track_events( let Some(auth) = auth_manager.auth().await else { return; }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { return; } - let access_token = match auth.get_token() { - Ok(token) => token, - Err(_) => return, - }; - let Some(account_id) = auth.get_account_id() else { - return; - }; let base_url = base_url.trim_end_matches('/'); let url = format!("{base_url}/codex/analytics-events/events"); @@ -330,8 +323,7 @@ async fn send_track_events( let response = create_client() .post(&url) .timeout(ANALYTICS_EVENTS_TIMEOUT) - .bearer_auth(&access_token) - .header("chatgpt-account-id", &account_id) + .headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers()) .header("Content-Type", "application/json") .json(&payload) .send() diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index e38e7cb5b..06ed624c3 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -30,6 +30,7 @@ axum = { workspace = true, default-features = false, features = [ "ws", ] } codex-analytics = { workspace = true } +codex-api = { workspace = true } codex-arg0 = { workspace = true } codex-cloud-requirements = { workspace = true } codex-config = { workspace = true } @@ -48,6 +49,7 @@ codex-file-search = { workspace = true } codex-chatgpt = { workspace = true } codex-login = { workspace = true } codex-mcp = { workspace = true } +codex-model-provider = { workspace = true } codex-models-manager = { workspace = true } codex-protocol = { workspace = true } codex-app-server-protocol = { workspace = true } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ae7514a9c..c94568947 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1943,7 +1943,7 @@ impl CodexMessageProcessor { }); }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { return Err(JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, message: "chatgpt authentication required to notify workspace owner".to_string(), @@ -1998,7 +1998,7 @@ impl CodexMessageProcessor { }); }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { return Err(JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, message: "chatgpt authentication required to read rate limits".to_string(), @@ -5909,8 +5909,8 @@ impl CodexMessageProcessor { let environment_manager = self.thread_manager.environment_manager(); let runtime_environment = match environment_manager.default_environment() { Some(environment) => { - // Status listing has no turn cwd. This fallback is used by - // stdio MCPs whose config omits `cwd`. + // Status listing has no turn cwd. This fallback is used only + // by executor-backed stdio MCPs whose config omits `cwd`. McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()) } None => McpRuntimeEnvironment::new( @@ -6414,7 +6414,7 @@ impl CodexMessageProcessor { let auth = self.auth_manager.auth().await; if !config .features - .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth)) + .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend)) { self.outgoing .send_response( diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 48e2aa6a1..c53440404 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1078,7 +1078,7 @@ impl MessageProcessor { let auth = self.auth_manager.auth().await; if !config.features.apps_enabled_for_auth( auth.as_ref() - .is_some_and(codex_login::CodexAuth::is_chatgpt_auth), + .is_some_and(codex_login::CodexAuth::uses_codex_backend), ) { return; } diff --git a/codex-rs/app-server/src/transport/remote_control/enroll.rs b/codex-rs/app-server/src/transport/remote_control/enroll.rs index dbe18c835..ba69c459e 100644 --- a/codex-rs/app-server/src/transport/remote_control/enroll.rs +++ b/codex-rs/app-server/src/transport/remote_control/enroll.rs @@ -2,6 +2,7 @@ use super::protocol::EnrollRemoteServerRequest; use super::protocol::EnrollRemoteServerResponse; use super::protocol::RemoteControlTarget; use axum::http::HeaderMap; +use codex_api::SharedAuthProvider; use codex_login::default_client::build_reqwest_client; use codex_state::RemoteControlEnrollmentRecord; use codex_state::StateRuntime; @@ -27,9 +28,8 @@ pub(super) struct RemoteControlEnrollment { pub(super) server_name: String, } -#[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct RemoteControlConnectionAuth { - pub(super) bearer_token: String, + pub(super) auth_provider: SharedAuthProvider, pub(super) account_id: String, } @@ -199,10 +199,12 @@ pub(super) async fn enroll_remote_control_server( app_server_version: env!("CARGO_PKG_VERSION"), }; let client = build_reqwest_client(); + let mut auth_headers = HeaderMap::new(); + auth.auth_provider.add_auth_headers(&mut auth_headers); let http_request = client .post(enroll_url) .timeout(REMOTE_CONTROL_ENROLL_TIMEOUT) - .bearer_auth(&auth.bearer_token) + .headers(auth_headers) .header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id) .json(&request); @@ -445,7 +447,7 @@ mod tests { let err = enroll_remote_control_server( &remote_control_target, &RemoteControlConnectionAuth { - bearer_token: "Access Token".to_string(), + auth_provider: codex_model_provider::unauthenticated_auth_provider(), account_id: "account_id".to_string(), }, ) diff --git a/codex-rs/app-server/src/transport/remote_control/websocket.rs b/codex-rs/app-server/src/transport/remote_control/websocket.rs index 4eb58a87f..464832e34 100644 --- a/codex-rs/app-server/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server/src/transport/remote_control/websocket.rs @@ -680,11 +680,9 @@ fn build_remote_control_websocket_request( "x-codex-protocol-version", REMOTE_CONTROL_PROTOCOL_VERSION, )?; - set_remote_control_header( - headers, - "authorization", - &format!("Bearer {}", auth.bearer_token), - )?; + let mut auth_headers = tungstenite::http::HeaderMap::new(); + auth.auth_provider.add_auth_headers(&mut auth_headers); + headers.extend(auth_headers); set_remote_control_header(headers, REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)?; if let Some(subscribe_cursor) = subscribe_cursor { set_remote_control_header( @@ -712,7 +710,7 @@ pub(crate) async fn load_remote_control_auth( reloaded = true; continue; }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { break auth; } if auth.get_account_id().is_none() && !reloaded { @@ -723,7 +721,7 @@ pub(crate) async fn load_remote_control_auth( break auth; }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { return Err(io::Error::new( ErrorKind::PermissionDenied, "remote control requires ChatGPT authentication; API key auth is not supported", @@ -731,7 +729,7 @@ pub(crate) async fn load_remote_control_auth( } Ok(RemoteControlConnectionAuth { - bearer_token: auth.get_token().map_err(io::Error::other)?, + auth_provider: codex_model_provider::auth_provider_from_auth(&auth), account_id: auth.get_account_id().ok_or_else(|| { io::Error::new( ErrorKind::WouldBlock, diff --git a/codex-rs/backend-client/Cargo.toml b/codex-rs/backend-client/Cargo.toml index 1707d45b1..d2e374ae2 100644 --- a/codex-rs/backend-client/Cargo.toml +++ b/codex-rs/backend-client/Cargo.toml @@ -17,8 +17,10 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } codex-backend-openapi-models = { path = "../codex-backend-openapi-models" } +codex-api = { workspace = true } codex-client = { workspace = true } codex-login = { workspace = true } +codex-model-provider = { workspace = true } codex-protocol = { workspace = true } [dev-dependencies] diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index b96395b01..6365d527e 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -5,6 +5,7 @@ use crate::types::RateLimitReachedKind as BackendRateLimitReachedKind; use crate::types::RateLimitStatusPayload; use crate::types::TurnAttemptsSiblingTurnsResponse; use anyhow::Result; +use codex_api::SharedAuthProvider; use codex_client::build_reqwest_client_with_custom_ca; use codex_client::with_chatgpt_cloudflare_cookie_store; use codex_login::CodexAuth; @@ -15,7 +16,6 @@ use codex_protocol::protocol::RateLimitReachedType; use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow; use reqwest::StatusCode; -use reqwest::header::AUTHORIZATION; use reqwest::header::CONTENT_TYPE; use reqwest::header::HeaderMap; use reqwest::header::HeaderName; @@ -113,17 +113,33 @@ impl PathStyle { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Client { base_url: String, http: reqwest::Client, - bearer_token: Option, + auth_provider: SharedAuthProvider, user_agent: Option, chatgpt_account_id: Option, chatgpt_account_is_fedramp: bool, path_style: PathStyle, } +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Client") + .field("base_url", &self.base_url) + .field("auth_provider", &"") + .field("user_agent", &self.user_agent) + .field("chatgpt_account_id", &self.chatgpt_account_id) + .field( + "chatgpt_account_is_fedramp", + &self.chatgpt_account_is_fedramp, + ) + .field("path_style", &self.path_style) + .finish_non_exhaustive() + } +} + impl Client { pub fn new(base_url: impl Into) -> Result { let mut base_url = base_url.into(); @@ -145,7 +161,7 @@ impl Client { Ok(Self { base_url, http, - bearer_token: None, + auth_provider: codex_model_provider::unauthenticated_auth_provider(), user_agent: None, chatgpt_account_id: None, chatgpt_account_is_fedramp: false, @@ -154,21 +170,13 @@ impl Client { } pub fn from_auth(base_url: impl Into, auth: &CodexAuth) -> Result { - let token = auth.get_token().map_err(anyhow::Error::from)?; - let mut client = Self::new(base_url)? + Ok(Self::new(base_url)? .with_user_agent(get_codex_user_agent()) - .with_bearer_token(token); - if let Some(account_id) = auth.get_account_id() { - client = client.with_chatgpt_account_id(account_id); - } - if auth.is_fedramp_account() { - client = client.with_fedramp_routing_header(); - } - Ok(client) + .with_auth_provider(codex_model_provider::auth_provider_from_auth(auth))) } - pub fn with_bearer_token(mut self, token: impl Into) -> Self { - self.bearer_token = Some(token.into()); + pub fn with_auth_provider(mut self, auth: SharedAuthProvider) -> Self { + self.auth_provider = auth; self } @@ -201,12 +209,7 @@ impl Client { } else { h.insert(USER_AGENT, HeaderValue::from_static("codex-cli")); } - if let Some(token) = &self.bearer_token { - let value = format!("Bearer {token}"); - if let Ok(hv) = HeaderValue::from_str(&value) { - h.insert(AUTHORIZATION, hv); - } - } + self.auth_provider.add_auth_headers(&mut h); if let Some(acc) = &self.chatgpt_account_id && let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id") && let Ok(hv) = HeaderValue::from_str(acc) @@ -819,7 +822,7 @@ mod tests { let codex_client = Client { base_url: "https://example.test".to_string(), http: reqwest::Client::new(), - bearer_token: None, + auth_provider: codex_model_provider::unauthenticated_auth_provider(), user_agent: None, chatgpt_account_id: None, chatgpt_account_is_fedramp: false, @@ -833,7 +836,7 @@ mod tests { let chatgpt_client = Client { base_url: "https://chatgpt.com/backend-api".to_string(), http: reqwest::Client::new(), - bearer_token: None, + auth_provider: codex_model_provider::unauthenticated_auth_provider(), user_agent: None, chatgpt_account_id: None, chatgpt_account_is_fedramp: false, diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index 354449934..ce9aa627d 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -12,10 +12,10 @@ anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-app-server-protocol = { workspace = true } codex-connectors = { workspace = true } -codex-config = { workspace = true } codex-core = { workspace = true } codex-git-utils = { workspace = true } codex-login = { workspace = true } +codex-model-provider = { workspace = true } codex-utils-cli = { workspace = true } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } diff --git a/codex-rs/chatgpt/src/apply_command.rs b/codex-rs/chatgpt/src/apply_command.rs index 1a9553955..70fe4481d 100644 --- a/codex-rs/chatgpt/src/apply_command.rs +++ b/codex-rs/chatgpt/src/apply_command.rs @@ -6,7 +6,6 @@ use codex_git_utils::ApplyGitRequest; use codex_git_utils::apply_git_patch; use codex_utils_cli::CliConfigOverrides; -use crate::chatgpt_token::init_chatgpt_token_from_auth; use crate::get_task::GetTaskResponse; use crate::get_task::OutputItem; use crate::get_task::PrOutputItem; @@ -32,9 +31,6 @@ pub async fn run_apply_command( ) .await?; - init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode) - .await?; - let task_response = get_task(&config, apply_cli.task_id).await?; apply_diff_from_task(task_response, cwd).await } diff --git a/codex-rs/chatgpt/src/chatgpt_client.rs b/codex-rs/chatgpt/src/chatgpt_client.rs index fa3a63dad..0f9bef956 100644 --- a/codex-rs/chatgpt/src/chatgpt_client.rs +++ b/codex-rs/chatgpt/src/chatgpt_client.rs @@ -1,9 +1,7 @@ use codex_core::config::Config; +use codex_login::AuthManager; use codex_login::default_client::create_client; -use crate::chatgpt_token::get_chatgpt_token_data; -use crate::chatgpt_token::init_chatgpt_token_from_auth; - use anyhow::Context; use serde::de::DeserializeOwned; use std::time::Duration; @@ -22,24 +20,28 @@ pub(crate) async fn chatgpt_get_request_with_timeout( timeout: Option, ) -> anyhow::Result { let chatgpt_base_url = &config.chatgpt_base_url; - init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode) - .await?; + let auth_manager = + AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); + let auth = auth_manager + .auth() + .await + .ok_or_else(|| anyhow::anyhow!("ChatGPT auth not available"))?; + anyhow::ensure!( + auth.uses_codex_backend(), + "ChatGPT backend requests require Codex backend auth" + ); + anyhow::ensure!( + auth.get_account_id().is_some(), + "ChatGPT account ID not available, please re-run `codex login`" + ); // Make direct HTTP request to ChatGPT backend API with the token let client = create_client(); let url = format!("{chatgpt_base_url}{path}"); - let token = - get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?; - - let account_id = token.account_id.ok_or_else(|| { - anyhow::anyhow!("ChatGPT account ID not available, please re-run `codex login`") - }); - let mut request = client .get(&url) - .bearer_auth(&token.access_token) - .header("chatgpt-account-id", account_id?) + .headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers()) .header("Content-Type", "application/json"); if let Some(timeout) = timeout { diff --git a/codex-rs/chatgpt/src/chatgpt_token.rs b/codex-rs/chatgpt/src/chatgpt_token.rs deleted file mode 100644 index fe19c3015..000000000 --- a/codex-rs/chatgpt/src/chatgpt_token.rs +++ /dev/null @@ -1,36 +0,0 @@ -use codex_config::types::AuthCredentialsStoreMode; -use codex_login::AuthManager; -use codex_login::token_data::TokenData; -use std::path::Path; -use std::sync::LazyLock; -use std::sync::RwLock; - -static CHATGPT_TOKEN: LazyLock>> = LazyLock::new(|| RwLock::new(None)); - -pub fn get_chatgpt_token_data() -> Option { - CHATGPT_TOKEN.read().ok()?.clone() -} - -pub fn set_chatgpt_token_data(value: TokenData) { - if let Ok(mut guard) = CHATGPT_TOKEN.write() { - *guard = Some(value); - } -} - -/// Initialize the ChatGPT token from auth.json file -pub async fn init_chatgpt_token_from_auth( - codex_home: &Path, - auth_credentials_store_mode: AuthCredentialsStoreMode, -) -> std::io::Result<()> { - let auth_manager = AuthManager::new( - codex_home.to_path_buf(), - /*enable_codex_api_key_env*/ false, - auth_credentials_store_mode, - /*chatgpt_base_url*/ None, - ); - if let Some(auth) = auth_manager.auth().await { - let token_data = auth.get_token_data()?; - set_chatgpt_token_data(token_data); - } - Ok(()) -} diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 4c6f05a68..62e804094 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -2,8 +2,6 @@ use std::collections::HashSet; use std::time::Duration; use crate::chatgpt_client::chatgpt_get_request_with_timeout; -use crate::chatgpt_token::get_chatgpt_token_data; -use crate::chatgpt_token::init_chatgpt_token_from_auth; use codex_app_server_protocol::AppInfo; use codex_connectors::AllConnectorsCacheKey; @@ -23,22 +21,32 @@ use codex_core::plugins::PluginsManager; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::default_client::originator; -use codex_login::token_data::TokenData; const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); async fn apps_enabled(config: &Config) -> bool { - let auth_manager = AuthManager::shared( - config.codex_home.to_path_buf(), - /*enable_codex_api_key_env*/ false, - config.cli_auth_credentials_store_mode, - Some(config.chatgpt_base_url.clone()), - ); + let auth_manager = + AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); let auth = auth_manager.auth().await; config .features - .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth)) + .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend)) } + +async fn connector_auth(config: &Config) -> anyhow::Result { + let auth_manager = + AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); + let auth = auth_manager + .auth() + .await + .ok_or_else(|| anyhow::anyhow!("ChatGPT auth not available"))?; + anyhow::ensure!( + auth.uses_codex_backend(), + "ChatGPT connectors require Codex backend auth" + ); + Ok(auth) +} + pub async fn list_connectors(config: &Config) -> anyhow::Result> { if !apps_enabled(config).await { return Ok(Vec::new()); @@ -66,14 +74,8 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option> return Some(Vec::new()); } - if init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode) - .await - .is_err() - { - return None; - } - let token_data = get_chatgpt_token_data()?; - let cache_key = all_connectors_cache_key(config, &token_data); + let auth = connector_auth(config).await.ok()?; + let cache_key = all_connectors_cache_key(config, &auth); let connectors = codex_connectors::cached_all_connectors(&cache_key)?; let connectors = merge_plugin_connectors( connectors, @@ -95,15 +97,11 @@ pub async fn list_all_connectors_with_options( if !apps_enabled(config).await { return Ok(Vec::new()); } - init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode) - .await?; - - let token_data = - get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?; - let cache_key = all_connectors_cache_key(config, &token_data); + let auth = connector_auth(config).await?; + let cache_key = all_connectors_cache_key(config, &auth); let connectors = codex_connectors::list_all_connectors_with_options( cache_key, - token_data.id_token.is_workspace_account(), + auth.is_workspace_account(), force_refetch, |path| async move { chatgpt_get_request_with_timeout::( @@ -128,12 +126,12 @@ pub async fn list_all_connectors_with_options( )) } -fn all_connectors_cache_key(config: &Config, token_data: &TokenData) -> AllConnectorsCacheKey { +fn all_connectors_cache_key(config: &Config, auth: &CodexAuth) -> AllConnectorsCacheKey { AllConnectorsCacheKey::new( config.chatgpt_base_url.clone(), - token_data.account_id.clone(), - token_data.id_token.chatgpt_user_id.clone(), - token_data.id_token.is_workspace_account(), + auth.get_account_id(), + auth.get_chatgpt_user_id(), + auth.is_workspace_account(), ) } diff --git a/codex-rs/chatgpt/src/lib.rs b/codex-rs/chatgpt/src/lib.rs index 0d39bb932..057478db1 100644 --- a/codex-rs/chatgpt/src/lib.rs +++ b/codex-rs/chatgpt/src/lib.rs @@ -1,5 +1,4 @@ pub mod apply_command; mod chatgpt_client; -mod chatgpt_token; pub mod connectors; pub mod get_task; diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index d413f72dd..c5b475132 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -486,8 +486,12 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> let mut entries: Vec<_> = mcp_servers.iter().collect(); entries.sort_by(|(a, _), (b, _)| a.cmp(b)); - let auth_statuses = - compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; + let auth_statuses = compute_auth_statuses( + mcp_servers.iter(), + config.mcp_oauth_credentials_store_mode, + /*auth*/ None, + ) + .await; if list_args.json { let json_entries: Vec<_> = entries diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index ca9ec56fe..8c51888a1 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -176,13 +176,7 @@ fn verify_cache_signature(payload_bytes: &[u8], signature: &str) -> bool { } fn auth_identity(auth: &CodexAuth) -> (Option, Option) { - let token_data = auth.get_token_data().ok(); - let chatgpt_user_id = token_data - .as_ref() - .and_then(|token_data| token_data.id_token.chatgpt_user_id.as_deref()) - .map(str::to_owned); - let account_id = auth.get_account_id(); - (chatgpt_user_id, account_id) + (auth.get_chatgpt_user_id(), auth.get_account_id()) } fn cache_payload_bytes(payload: &CloudRequirementsCacheSignedPayload) -> Option> { @@ -338,7 +332,7 @@ impl CloudRequirementsService { let Some(plan_type) = auth.account_plan_type() else { return Ok(None); }; - if !auth.is_chatgpt_auth() + if !auth.uses_codex_backend() || !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise)) { return Ok(None); @@ -558,7 +552,7 @@ impl CloudRequirementsService { let Some(plan_type) = auth.account_plan_type() else { return false; }; - if !auth.is_chatgpt_auth() + if !auth.uses_codex_backend() || !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise)) { return false; diff --git a/codex-rs/cloud-tasks-client/Cargo.toml b/codex-rs/cloud-tasks-client/Cargo.toml index cdfcba47b..929c3e313 100644 --- a/codex-rs/cloud-tasks-client/Cargo.toml +++ b/codex-rs/cloud-tasks-client/Cargo.toml @@ -15,6 +15,7 @@ workspace = true anyhow = { workspace = true } async-trait = { workspace = true } chrono = { workspace = true, features = ["serde"] } +codex-api = { workspace = true } codex-backend-client = { workspace = true } codex-git-utils = { workspace = true } serde = { version = "1", features = ["derive"] } diff --git a/codex-rs/cloud-tasks-client/src/http.rs b/codex-rs/cloud-tasks-client/src/http.rs index 4ea098022..46fed812b 100644 --- a/codex-rs/cloud-tasks-client/src/http.rs +++ b/codex-rs/cloud-tasks-client/src/http.rs @@ -14,6 +14,7 @@ use crate::api::TaskText; use chrono::DateTime; use chrono::Utc; +use codex_api::SharedAuthProvider; use codex_backend_client as backend; use codex_backend_client::CodeTaskDetailsResponseExt; use codex_git_utils::ApplyGitRequest; @@ -32,13 +33,13 @@ impl HttpClient { Ok(Self { base_url, backend }) } - pub fn with_bearer_token(mut self, token: impl Into) -> Self { - self.backend = self.backend.clone().with_bearer_token(token); + pub fn with_user_agent(mut self, ua: impl Into) -> Self { + self.backend = self.backend.clone().with_user_agent(ua); self } - pub fn with_user_agent(mut self, ua: impl Into) -> Self { - self.backend = self.backend.clone().with_user_agent(ua); + pub fn with_auth_provider(mut self, auth: SharedAuthProvider) -> Self { + self.backend = self.backend.clone().with_auth_provider(auth); self } diff --git a/codex-rs/cloud-tasks/Cargo.toml b/codex-rs/cloud-tasks/Cargo.toml index 30e8b73a8..6429c1edc 100644 --- a/codex-rs/cloud-tasks/Cargo.toml +++ b/codex-rs/cloud-tasks/Cargo.toml @@ -13,7 +13,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -base64 = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } codex-client = { workspace = true } @@ -23,6 +22,7 @@ codex-cloud-tasks-mock-client = { workspace = true } codex-core = { workspace = true } codex-git-utils = { workspace = true } codex-login = { path = "../login" } +codex-model-provider = { workspace = true } codex-tui = { workspace = true } codex-utils-cli = { workspace = true } crossterm = { workspace = true, features = ["event-stream"] } diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index 7006d52b9..e8d6b545b 100644 --- a/codex-rs/cloud-tasks/src/lib.rs +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -68,7 +68,7 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result }; append_error_log(format!("startup: base_url={base_url} path_style={style}")); - let auth_manager = util::load_auth_manager().await; + let auth_manager = util::load_auth_manager(Some(base_url.clone())).await; let auth = match auth_manager.as_ref() { Some(manager) => manager.auth().await, None => None, @@ -87,23 +87,17 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result append_error_log(format!("auth: mode=ChatGPT account_id={acc}")); } - let token = match auth.get_token() { - Ok(t) if !t.is_empty() => t, - _ => { - eprintln!( - "Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'." - ); - std::process::exit(1); - } - }; + if !auth.uses_codex_backend() { + eprintln!( + "Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'." + ); + std::process::exit(1); + } - http = http.with_bearer_token(token.clone()); - if let Some(acc) = auth - .get_account_id() - .or_else(|| util::extract_chatgpt_account_id(&token)) - { + let auth_provider = codex_model_provider::auth_provider_from_auth(&auth); + http = http.with_auth_provider(auth_provider); + if let Some(acc) = auth.get_account_id() { append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}")); - http = http.with_chatgpt_account_id(acc); } Ok(BackendContext { diff --git a/codex-rs/cloud-tasks/src/util.rs b/codex-rs/cloud-tasks/src/util.rs index 525ea3b59..e433b892e 100644 --- a/codex-rs/cloud-tasks/src/util.rs +++ b/codex-rs/cloud-tasks/src/util.rs @@ -1,4 +1,3 @@ -use base64::Engine as _; use chrono::DateTime; use chrono::Local; use chrono::Utc; @@ -42,39 +41,20 @@ pub fn normalize_base_url(input: &str) -> String { base_url } -/// Extract the ChatGPT account id from a JWT token, when present. -pub fn extract_chatgpt_account_id(token: &str) -> Option { - let mut parts = token.split('.'); - let (_h, payload_b64, _s) = match (parts.next(), parts.next(), parts.next()) { - (Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s), - _ => return None, - }; - let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(payload_b64) - .ok()?; - let v: serde_json::Value = serde_json::from_slice(&payload_bytes).ok()?; - v.get("https://api.openai.com/auth") - .and_then(|auth| auth.get("chatgpt_account_id")) - .and_then(|id| id.as_str()) - .map(str::to_string) -} - -pub async fn load_auth_manager() -> Option { +pub async fn load_auth_manager(chatgpt_base_url: Option) -> Option { // TODO: pass in cli overrides once cloud tasks properly support them. let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?; Some(AuthManager::new( config.codex_home.to_path_buf(), /*enable_codex_api_key_env*/ false, config.cli_auth_credentials_store_mode, - Some(config.chatgpt_base_url), + chatgpt_base_url.or(Some(config.chatgpt_base_url)), )) } /// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`, /// and optional `ChatGPT-Account-Id`. pub async fn build_chatgpt_headers() -> HeaderMap { - use reqwest::header::AUTHORIZATION; - use reqwest::header::HeaderName; use reqwest::header::HeaderValue; use reqwest::header::USER_AGENT; @@ -85,23 +65,11 @@ pub async fn build_chatgpt_headers() -> HeaderMap { USER_AGENT, HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")), ); - if let Some(am) = load_auth_manager().await + if let Some(am) = load_auth_manager(/*chatgpt_base_url*/ None).await && let Some(auth) = am.auth().await - && let Ok(tok) = auth.get_token() - && !tok.is_empty() + && auth.uses_codex_backend() { - let v = format!("Bearer {tok}"); - if let Ok(hv) = HeaderValue::from_str(&v) { - headers.insert(AUTHORIZATION, hv); - } - if let Some(acc) = auth - .get_account_id() - .or_else(|| extract_chatgpt_account_id(&tok)) - && let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id") - && let Ok(hv) = HeaderValue::from_str(&acc) - { - headers.insert(name, hv); - } + headers.extend(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers()); } headers } diff --git a/codex-rs/codex-api/src/auth.rs b/codex-rs/codex-api/src/auth.rs index e1130c770..41394a225 100644 --- a/codex-rs/codex-api/src/auth.rs +++ b/codex-rs/codex-api/src/auth.rs @@ -34,6 +34,13 @@ pub trait AuthProvider: Send + Sync { /// used by telemetry and non-HTTP request paths. fn add_auth_headers(&self, headers: &mut HeaderMap); + /// Returns any auth headers that are available without request body access. + fn to_auth_headers(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + self.add_auth_headers(&mut headers); + headers + } + /// Applies auth to a complete outbound request and returns the request to send. /// /// The input `request` is moved into this method. Implementations may mutate diff --git a/codex-rs/codex-mcp/Cargo.toml b/codex-rs/codex-mcp/Cargo.toml index 0aec1f3aa..a9aacb192 100644 --- a/codex-rs/codex-mcp/Cargo.toml +++ b/codex-rs/codex-mcp/Cargo.toml @@ -15,9 +15,11 @@ workspace = true anyhow = { workspace = true } async-channel = { workspace = true } codex-async-utils = { workspace = true } +codex-api = { workspace = true } codex-config = { workspace = true } codex-exec-server = { workspace = true } codex-login = { workspace = true } +codex-model-provider = { workspace = true } codex-otel = { workspace = true } codex-plugin = { workspace = true } codex-protocol = { workspace = true } diff --git a/codex-rs/codex-mcp/src/mcp/auth.rs b/codex-rs/codex-mcp/src/mcp/auth.rs index 27d7e1335..9c605c16f 100644 --- a/codex-rs/codex-mcp/src/mcp/auth.rs +++ b/codex-rs/codex-mcp/src/mcp/auth.rs @@ -1,7 +1,10 @@ use std::collections::HashMap; use anyhow::Result; +use codex_config::McpServerConfig; +use codex_config::McpServerTransportConfig; use codex_config::types::OAuthCredentialsStoreMode; +use codex_login::CodexAuth; use codex_protocol::protocol::McpAuthStatus; use codex_rmcp_client::OAuthProviderError; use codex_rmcp_client::determine_streamable_http_auth_status; @@ -9,8 +12,7 @@ use codex_rmcp_client::discover_streamable_http_oauth; use futures::future::join_all; use tracing::warn; -use codex_config::McpServerConfig; -use codex_config::McpServerTransportConfig; +use super::CODEX_APPS_MCP_SERVER_NAME; #[derive(Debug, Clone)] pub struct McpOAuthLoginConfig { @@ -126,6 +128,7 @@ pub struct McpAuthStatusEntry { pub async fn compute_auth_statuses<'a, I>( servers: I, store_mode: OAuthCredentialsStoreMode, + auth: Option<&CodexAuth>, ) -> HashMap where I: IntoIterator, @@ -133,14 +136,24 @@ where let futures = servers.into_iter().map(|(name, config)| { let name = name.clone(); let config = config.clone(); - async move { - let auth_status = match compute_auth_status(&name, &config, store_mode).await { - Ok(status) => status, - Err(error) => { - warn!("failed to determine auth status for MCP server `{name}`: {error:?}"); - McpAuthStatus::Unsupported + let has_runtime_auth = name == CODEX_APPS_MCP_SERVER_NAME + && auth.is_some_and(CodexAuth::uses_codex_backend) + && matches!( + &config.transport, + McpServerTransportConfig::StreamableHttp { + bearer_token_env_var: None, + .. } - }; + ); + async move { + let auth_status = + match compute_auth_status(&name, &config, store_mode, has_runtime_auth).await { + Ok(status) => status, + Err(error) => { + warn!("failed to determine auth status for MCP server `{name}`: {error:?}"); + McpAuthStatus::Unsupported + } + }; let entry = McpAuthStatusEntry { config, auth_status, @@ -156,11 +169,16 @@ async fn compute_auth_status( server_name: &str, config: &McpServerConfig, store_mode: OAuthCredentialsStoreMode, + has_runtime_auth: bool, ) -> Result { if !config.enabled { return Ok(McpAuthStatus::Unsupported); } + if has_runtime_auth { + return Ok(McpAuthStatus::BearerToken); + } + match &config.transport { McpServerTransportConfig::Stdio { .. } => Ok(McpAuthStatus::Unsupported), McpServerTransportConfig::StreamableHttp { diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 97053cbe5..1061a6a54 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -205,31 +205,6 @@ fn codex_apps_mcp_bearer_token_env_var() -> Option { } } -fn codex_apps_mcp_bearer_token(auth: Option<&CodexAuth>) -> Option { - let token = auth.and_then(|auth| auth.get_token().ok())?; - let token = token.trim(); - if token.is_empty() { - None - } else { - Some(token.to_string()) - } -} - -fn codex_apps_mcp_http_headers(auth: Option<&CodexAuth>) -> Option> { - let mut headers = HashMap::new(); - if let Some(token) = codex_apps_mcp_bearer_token(auth) { - headers.insert("Authorization".to_string(), format!("Bearer {token}")); - } - if let Some(account_id) = auth.and_then(CodexAuth::get_account_id) { - headers.insert("ChatGPT-Account-ID".to_string(), account_id); - } - if headers.is_empty() { - None - } else { - Some(headers) - } -} - fn normalize_codex_apps_base_url(base_url: &str) -> String { let mut base_url = base_url.trim_end_matches('/').to_string(); if (base_url.starts_with("https://chatgpt.com") @@ -256,20 +231,14 @@ pub(crate) fn codex_apps_mcp_url(config: &McpConfig) -> String { codex_apps_mcp_url_for_base_url(&config.chatgpt_base_url) } -fn codex_apps_mcp_server_config(config: &McpConfig, auth: Option<&CodexAuth>) -> McpServerConfig { - let bearer_token_env_var = codex_apps_mcp_bearer_token_env_var(); - let http_headers = if bearer_token_env_var.is_some() { - None - } else { - codex_apps_mcp_http_headers(auth) - }; +fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig { let url = codex_apps_mcp_url(config); McpServerConfig { transport: McpServerTransportConfig::StreamableHttp { url, - bearer_token_env_var, - http_headers, + bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(), + http_headers: None, env_http_headers: None, }, experimental_environment: None, @@ -293,10 +262,10 @@ pub fn with_codex_apps_mcp( auth: Option<&CodexAuth>, config: &McpConfig, ) -> HashMap { - if config.apps_enabled && auth.is_some_and(CodexAuth::is_chatgpt_auth) { + if config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend) { servers.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), - codex_apps_mcp_server_config(config, auth), + codex_apps_mcp_server_config(config), ); } else { servers.remove(CODEX_APPS_MCP_SERVER_NAME); @@ -329,8 +298,12 @@ pub async fn read_mcp_resource( ) -> anyhow::Result { let mut mcp_servers = effective_mcp_servers(config, auth); mcp_servers.retain(|name, _| name == server); - let auth_statuses = - compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; + let auth_statuses = compute_auth_statuses( + mcp_servers.iter(), + config.mcp_oauth_credentials_store_mode, + auth, + ) + .await; let (tx_event, rx_event) = unbounded(); drop(rx_event); let (manager, cancel_token) = McpConnectionManager::new( @@ -345,6 +318,7 @@ pub async fn read_mcp_resource( config.codex_home.clone(), codex_apps_tools_cache_key(auth), tool_plugin_provenance(config), + auth, ) .await; @@ -395,8 +369,12 @@ pub async fn collect_mcp_snapshot_with_detail( }; } - let auth_status_entries = - compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; + let auth_status_entries = compute_auth_statuses( + mcp_servers.iter(), + config.mcp_oauth_credentials_store_mode, + auth, + ) + .await; let (tx_event, rx_event) = unbounded(); drop(rx_event); @@ -413,6 +391,7 @@ pub async fn collect_mcp_snapshot_with_detail( config.codex_home.clone(), codex_apps_tools_cache_key(auth), tool_plugin_provenance, + auth, ) .await; @@ -470,8 +449,12 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( }; } - let auth_status_entries = - compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; + let auth_status_entries = compute_auth_statuses( + mcp_servers.iter(), + config.mcp_oauth_credentials_store_mode, + auth, + ) + .await; let (tx_event, rx_event) = unbounded(); drop(rx_event); @@ -488,6 +471,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( config.codex_home.clone(), codex_apps_tools_cache_key(auth), tool_plugin_provenance, + auth, ) .await; diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager.rs b/codex-rs/codex-mcp/src/mcp_connection_manager.rs index 1e1e0fd3f..aca8828af 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager.rs @@ -32,6 +32,7 @@ use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use async_channel::Sender; +use codex_api::SharedAuthProvider; use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; use codex_config::Constrained; @@ -121,21 +122,10 @@ fn sha1_hex(s: &str) -> String { } pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCacheKey { - let token_data = auth.and_then(|auth| auth.get_token_data().ok()); - let account_id = token_data - .as_ref() - .and_then(|token_data| token_data.account_id.clone()); - let chatgpt_user_id = token_data - .as_ref() - .and_then(|token_data| token_data.id_token.chatgpt_user_id.clone()); - let is_workspace_account = token_data - .as_ref() - .is_some_and(|token_data| token_data.id_token.is_workspace_account()); - CodexAppsToolsCacheKey { - account_id, - chatgpt_user_id, - is_workspace_account, + account_id: auth.and_then(CodexAuth::get_account_id), + chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id), + is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account), } } @@ -497,6 +487,7 @@ impl AsyncManagedClient { codex_apps_tools_cache_context: Option, tool_plugin_provenance: Arc, runtime_environment: McpRuntimeEnvironment, + runtime_auth_provider: Option, ) -> Self { let tool_filter = ToolFilter::from_config(&config); let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot( @@ -519,6 +510,7 @@ impl AsyncManagedClient { config.clone(), store_mode, runtime_environment, + runtime_auth_provider, ) .await?, ); @@ -758,6 +750,7 @@ impl McpConnectionManager { codex_home: PathBuf, codex_apps_tools_cache_key: CodexAppsToolsCacheKey, tool_plugin_provenance: ToolPluginProvenance, + auth: Option<&CodexAuth>, ) -> (Self, CancellationToken) { let cancel_token = CancellationToken::new(); let mut clients = HashMap::new(); @@ -767,6 +760,9 @@ impl McpConnectionManager { ElicitationRequestManager::new(approval_policy.value(), initial_sandbox_policy); let tool_plugin_provenance = Arc::new(tool_plugin_provenance); let startup_submit_id = submit_id.clone(); + let codex_apps_auth_provider = auth + .filter(|auth| auth.uses_codex_backend()) + .map(codex_model_provider::auth_provider_from_auth); let mcp_servers = mcp_servers.clone(); for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) { if let Some(origin) = transport_origin(&cfg.transport) { @@ -790,6 +786,19 @@ impl McpConnectionManager { } else { None }; + let uses_env_bearer_token = match &cfg.transport { + McpServerTransportConfig::StreamableHttp { + bearer_token_env_var, + .. + } => bearer_token_env_var.is_some(), + McpServerTransportConfig::Stdio { .. } => false, + }; + let runtime_auth_provider = + if server_name == CODEX_APPS_MCP_SERVER_NAME && !uses_env_bearer_token { + codex_apps_auth_provider.clone() + } else { + None + }; let async_managed_client = AsyncManagedClient::new( server_name.clone(), cfg, @@ -800,6 +809,7 @@ impl McpConnectionManager { codex_apps_tools_cache_context, Arc::clone(&tool_plugin_provenance), runtime_environment.clone(), + runtime_auth_provider, ); clients.insert(server_name.clone(), async_managed_client.clone()); let tx_event = tx_event.clone(); @@ -1533,6 +1543,7 @@ async fn make_rmcp_client( config: McpServerConfig, store_mode: OAuthCredentialsStoreMode, runtime_environment: McpRuntimeEnvironment, + runtime_auth_provider: Option, ) -> Result { let McpServerConfig { transport, @@ -1612,6 +1623,7 @@ async fn make_rmcp_client( env_http_headers, store_mode, runtime_environment.environment().get_http_client(), + runtime_auth_provider, ) .await .map_err(StartupOutcomeError::from) diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index 036b16036..8a0e4f772 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -19,6 +19,7 @@ codex-core-skills = { workspace = true } codex-exec-server = { workspace = true } codex-git-utils = { workspace = true } codex-login = { workspace = true } +codex-model-provider = { workspace = true } codex-otel = { workspace = true } codex-plugin = { workspace = true } codex-protocol = { workspace = true } diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index add99f2be..2b16f435b 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -608,7 +608,7 @@ fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePlu let Some(auth) = auth else { return Err(RemotePluginCatalogError::AuthRequired); }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { return Err(RemotePluginCatalogError::UnsupportedAuthMode); } Ok(auth) @@ -618,16 +618,9 @@ fn authenticated_request( request: RequestBuilder, auth: &CodexAuth, ) -> Result { - let token = auth - .get_token() - .map_err(RemotePluginCatalogError::AuthToken)?; - let mut request = request + Ok(request .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) - .bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } - Ok(request) + .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers())) } async fn send_and_decode Deserialize<'de>>( diff --git a/codex-rs/core-plugins/src/remote_legacy.rs b/codex-rs/core-plugins/src/remote_legacy.rs index 7b57ab132..dcf9f79eb 100644 --- a/codex-rs/core-plugins/src/remote_legacy.rs +++ b/codex-rs/core-plugins/src/remote_legacy.rs @@ -123,23 +123,17 @@ pub async fn fetch_remote_plugin_status( let Some(auth) = auth else { return Err(RemotePluginFetchError::AuthRequired); }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { return Err(RemotePluginFetchError::UnsupportedAuthMode); } let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/plugins/list"); let client = build_reqwest_client(); - let token = auth - .get_token() - .map_err(RemotePluginFetchError::AuthToken)?; - let mut request = client + let request = client .get(&url) .timeout(REMOTE_PLUGIN_FETCH_TIMEOUT) - .bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } + .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); let response = request .send() @@ -176,14 +170,9 @@ pub async fn fetch_remote_featured_plugin_ids( )]) .timeout(REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT); - if let Some(auth) = auth.filter(|auth| auth.is_chatgpt_auth()) { - let token = auth - .get_token() - .map_err(RemotePluginFetchError::AuthToken)?; - request = request.bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } + if let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) { + request = + request.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); } let response = request @@ -223,11 +212,13 @@ pub async fn uninstall_remote_plugin( Ok(()) } -fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePluginMutationError> { +fn ensure_codex_backend_auth( + auth: Option<&CodexAuth>, +) -> Result<&CodexAuth, RemotePluginMutationError> { let Some(auth) = auth else { return Err(RemotePluginMutationError::AuthRequired); }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { return Err(RemotePluginMutationError::UnsupportedAuthMode); } Ok(auth) @@ -243,19 +234,13 @@ async fn post_remote_plugin_mutation( plugin_id: &str, action: &str, ) -> Result { - let auth = ensure_chatgpt_auth(auth)?; + let auth = ensure_codex_backend_auth(auth)?; let url = remote_plugin_mutation_url(config, plugin_id, action)?; let client = build_reqwest_client(); - let token = auth - .get_token() - .map_err(RemotePluginMutationError::AuthToken)?; - let mut request = client + let request = client .post(url.clone()) .timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT) - .bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } + .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); let response = request .send() diff --git a/codex-rs/core-skills/Cargo.toml b/codex-rs/core-skills/Cargo.toml index 355374114..4324d29de 100644 --- a/codex-rs/core-skills/Cargo.toml +++ b/codex-rs/core-skills/Cargo.toml @@ -19,6 +19,7 @@ codex-app-server-protocol = { workspace = true } codex-config = { workspace = true } codex-exec-server = { workspace = true } codex-login = { workspace = true } +codex-model-provider = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-skills = { workspace = true } diff --git a/codex-rs/core-skills/src/remote.rs b/codex-rs/core-skills/src/remote.rs index 2dc620b86..1ca7cd0cb 100644 --- a/codex-rs/core-skills/src/remote.rs +++ b/codex-rs/core-skills/src/remote.rs @@ -48,11 +48,11 @@ fn as_query_product_surface(product_surface: RemoteSkillProductSurface) -> &'sta } } -fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> { +fn ensure_codex_backend_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> { let Some(auth) = auth else { anyhow::bail!("chatgpt authentication required for remote skill scopes"); }; - if !auth.is_chatgpt_auth() { + if !auth.uses_codex_backend() { anyhow::bail!( "chatgpt authentication required for remote skill scopes; api key auth is not supported" ); @@ -94,7 +94,7 @@ pub async fn list_remote_skills( enabled: Option, ) -> Result> { let base_url = chatgpt_base_url.trim_end_matches('/'); - let auth = ensure_chatgpt_auth(auth)?; + let auth = ensure_codex_backend_auth(auth)?; let url = format!("{base_url}/hazelnuts"); let product_surface = as_query_product_surface(product_surface); @@ -108,17 +108,11 @@ pub async fn list_remote_skills( } let client = build_reqwest_client(); - let mut request = client + let request = client .get(&url) .timeout(REMOTE_SKILLS_API_TIMEOUT) - .query(&query_params); - let token = auth - .get_token() - .context("Failed to read auth token for remote skills")?; - request = request.bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } + .query(&query_params) + .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); let response = request .send() .await @@ -150,20 +144,15 @@ pub async fn export_remote_skill( auth: Option<&CodexAuth>, skill_id: &str, ) -> Result { - let auth = ensure_chatgpt_auth(auth)?; + let auth = ensure_codex_backend_auth(auth)?; let client = build_reqwest_client(); let base_url = chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/hazelnuts/{skill_id}/export"); - let mut request = client.get(&url).timeout(REMOTE_SKILLS_API_TIMEOUT); - - let token = auth - .get_token() - .context("Failed to read auth token for remote skills")?; - request = request.bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } + let request = client + .get(&url) + .timeout(REMOTE_SKILLS_API_TIMEOUT) + .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); let response = request .send() diff --git a/codex-rs/core/src/arc_monitor.rs b/codex-rs/core/src/arc_monitor.rs index ecd7f3966..08b746517 100644 --- a/codex-rs/core/src/arc_monitor.rs +++ b/codex-rs/core/src/arc_monitor.rs @@ -9,7 +9,6 @@ use crate::compact::content_items_to_text; use crate::event_mapping::is_contextual_user_message_content; use crate::session::session::Session; use crate::session::turn_context::TurnContext; -use codex_login::CodexAuth; use codex_login::default_client::build_reqwest_client; use codex_protocol::models::MessagePhase; use codex_protocol::models::ResponseItem; @@ -104,28 +103,15 @@ pub(crate) async fn monitor_action( ) -> ArcMonitorOutcome { let auth = match turn_context.auth_manager.as_ref() { Some(auth_manager) => match auth_manager.auth().await { - Some(auth) if auth.is_chatgpt_auth() => Some(auth), + Some(auth) if auth.uses_codex_backend() => Some(auth), _ => None, }, None => None, }; - let token = if let Some(token) = read_non_empty_env_var(CODEX_ARC_MONITOR_TOKEN) { - token - } else { - let Some(auth) = auth.as_ref() else { - return ArcMonitorOutcome::Ok; - }; - match auth.get_token() { - Ok(token) => token, - Err(err) => { - warn!( - error = %err, - "skipping safety monitor because auth token is unavailable" - ); - return ArcMonitorOutcome::Ok; - } - } - }; + let env_token = read_non_empty_env_var(CODEX_ARC_MONITOR_TOKEN); + if env_token.is_none() && auth.is_none() { + return ArcMonitorOutcome::Ok; + } let url = read_non_empty_env_var(CODEX_ARC_MONITOR_ENDPOINT_OVERRIDE).unwrap_or_else(|| { format!( @@ -143,13 +129,12 @@ pub(crate) async fn monitor_action( let body = build_arc_monitor_request(sess, turn_context, action, protection_client_callsite).await; let client = build_reqwest_client(); - let mut request = client - .post(&url) - .timeout(ARC_MONITOR_TIMEOUT) - .json(&body) - .bearer_auth(token); - if let Some(account_id) = auth.as_ref().and_then(CodexAuth::get_account_id) { - request = request.header("chatgpt-account-id", account_id); + let mut request = client.post(&url).timeout(ARC_MONITOR_TIMEOUT).json(&body); + if let Some(token) = env_token { + request = request.bearer_auth(token); + } else if let Some(auth) = auth.as_ref() { + request = + request.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); } let response = match request.send().await { diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index fd6d7faa0..cb63ca455 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1127,7 +1127,7 @@ impl ModelClientSession { fn responses_request_compression(&self, auth: Option<&CodexAuth>) -> Compression { if self.client.state.enable_request_compression - && auth.is_some_and(CodexAuth::is_chatgpt_auth) + && auth.is_some_and(CodexAuth::uses_codex_backend) && self.client.state.provider.info().is_openai() { Compression::Zstd diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 7641b4cb6..968b93214 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -8,6 +8,7 @@ use std::time::Instant; use anyhow::Context; use async_channel::unbounded; +use codex_api::SharedAuthProvider; pub use codex_app_server_protocol::AppBranding; pub use codex_app_server_protocol::AppInfo; pub use codex_app_server_protocol::AppMetadata; @@ -16,7 +17,6 @@ use codex_connectors::DirectoryListResponse; use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; -use codex_login::token_data::TokenData; use codex_protocol::protocol::SandboxPolicy; use codex_tools::DiscoverableTool; use rmcp::model::ToolAnnotations; @@ -253,8 +253,12 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( }); } - let auth_status_entries = - compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; + let auth_status_entries = compute_auth_statuses( + mcp_servers.iter(), + config.mcp_oauth_credentials_store_mode, + auth.as_ref(), + ) + .await; let (tx_event, rx_event) = unbounded(); drop(rx_event); @@ -275,6 +279,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), ToolPluginProvenance::default(), + auth.as_ref(), ) .await; @@ -351,16 +356,9 @@ fn accessible_connectors_cache_key( config: &Config, auth: Option<&CodexAuth>, ) -> AccessibleConnectorsCacheKey { - let token_data: Option = auth.and_then(|auth| auth.get_token_data().ok()); - let account_id = token_data - .as_ref() - .and_then(|token_data| token_data.account_id.clone()); - let chatgpt_user_id = token_data - .as_ref() - .and_then(|token_data| token_data.id_token.chatgpt_user_id.clone()); - let is_workspace_account = token_data - .as_ref() - .is_some_and(|token_data| token_data.id_token.is_workspace_account()); + let account_id = auth.and_then(CodexAuth::get_account_id); + let chatgpt_user_id = auth.and_then(CodexAuth::get_chatgpt_user_id); + let is_workspace_account = auth.is_some_and(CodexAuth::is_workspace_account); AccessibleConnectorsCacheKey { chatgpt_base_url: config.chatgpt_base_url.clone(), account_id, @@ -431,31 +429,29 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( return Ok(Vec::new()); } - let token_data = if let Some(auth) = auth { - auth.get_token_data().ok() + let loaded_auth; + let auth = if let Some(auth) = auth { + Some(auth) } else { let auth_manager = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); - auth_manager - .auth() - .await - .and_then(|auth| auth.get_token_data().ok()) + loaded_auth = auth_manager.auth().await; + loaded_auth.as_ref() }; - let Some(token_data) = token_data else { + let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else { return Ok(Vec::new()); }; - let account_id = match token_data.account_id.as_deref() { + let account_id = match auth.get_account_id() { Some(account_id) if !account_id.is_empty() => account_id, _ => return Ok(Vec::new()), }; - let access_token = token_data.access_token.clone(); - let account_id = account_id.to_string(); - let is_workspace_account = token_data.id_token.is_workspace_account(); + let auth_provider = codex_model_provider::auth_provider_from_auth(auth); + let is_workspace_account = auth.is_workspace_account(); let cache_key = AllConnectorsCacheKey::new( config.chatgpt_base_url.clone(), Some(account_id.clone()), - token_data.id_token.chatgpt_user_id.clone(), + auth.get_chatgpt_user_id(), is_workspace_account, ); @@ -464,14 +460,12 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( is_workspace_account, /*force_refetch*/ false, |path| { - let access_token = access_token.clone(); - let account_id = account_id.clone(); + let auth_provider = auth_provider.clone(); async move { - chatgpt_get_request_with_token::( + chatgpt_get_request_with_auth_provider::( config, path, - access_token.as_str(), - account_id.as_str(), + auth_provider, ) .await } @@ -480,18 +474,16 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( .await } -async fn chatgpt_get_request_with_token( +async fn chatgpt_get_request_with_auth_provider( config: &Config, path: String, - access_token: &str, - account_id: &str, + auth_provider: SharedAuthProvider, ) -> anyhow::Result { let client = create_client(); let url = format!("{}{}", config.chatgpt_base_url, path); let response = client .get(&url) - .bearer_auth(access_token) - .header("chatgpt-account-id", account_id) + .headers(auth_provider.to_auth_headers()) .header("Content-Type", "application/json") .timeout(DIRECTORY_CONNECTORS_TIMEOUT) .send() diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index d6e6d1f9c..0e0d4a600 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -14,7 +14,6 @@ use crate::session::session::Session; use crate::session::turn_context::TurnContext; use codex_api::upload_local_file; use codex_login::CodexAuth; -use codex_model_provider::BearerAuthProvider; use serde_json::Value as JsonValue; pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( @@ -109,17 +108,15 @@ async fn build_uploaded_local_argument_value( "ChatGPT auth is required to upload local files for Codex Apps tools".to_string(), ); }; - let token_data = auth - .get_token_data() - .map_err(|error| format!("failed to read ChatGPT auth for file upload: {error}"))?; - let upload_auth = BearerAuthProvider { - token: Some(token_data.access_token), - account_id: token_data.account_id, - is_fedramp_account: auth.is_fedramp_account(), - }; + if !auth.uses_codex_backend() { + return Err( + "ChatGPT auth is required to upload local files for Codex Apps tools".to_string(), + ); + } + let upload_auth = codex_model_provider::auth_provider_from_auth(auth); let uploaded = upload_local_file( turn_context.config.chatgpt_base_url.trim_end_matches('/'), - &upload_auth, + upload_auth.as_ref(), &resolved_path, ) .await diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index d47f2c35b..842616f94 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -124,21 +124,11 @@ fn featured_plugin_ids_cache_key( config: &Config, auth: Option<&CodexAuth>, ) -> FeaturedPluginIdsCacheKey { - let token_data = auth.and_then(|auth| auth.get_token_data().ok()); - let account_id = token_data - .as_ref() - .and_then(|token_data| token_data.account_id.clone()); - let chatgpt_user_id = token_data - .as_ref() - .and_then(|token_data| token_data.id_token.chatgpt_user_id.clone()); - let is_workspace_account = token_data - .as_ref() - .is_some_and(|token_data| token_data.id_token.is_workspace_account()); FeaturedPluginIdsCacheKey { chatgpt_base_url: config.chatgpt_base_url.clone(), - account_id, - chatgpt_user_id, - is_workspace_account, + account_id: auth.and_then(CodexAuth::get_account_id), + chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id), + is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account), } } diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 7656082c0..dd022482b 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -543,7 +543,12 @@ pub async fn list_mcp_tools(sess: &Session, config: &Arc, sub_id: String .await; let snapshot = collect_mcp_snapshot_from_manager( &mcp_connection_manager, - compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await, + compute_auth_statuses( + mcp_servers.iter(), + config.mcp_oauth_credentials_store_mode, + auth.as_ref(), + ) + .await, ) .await; let event = Event { diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 350d6505a..99cdae53e 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -219,7 +219,8 @@ impl Session { .tool_plugin_provenance(config.as_ref()) .await; let mcp_servers = with_codex_apps_mcp(mcp_servers, auth.as_ref(), &mcp_config); - let auth_statuses = compute_auth_statuses(mcp_servers.iter(), store_mode).await; + let auth_statuses = + compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await; { let mut guard = self.services.mcp_startup_cancellation_token.lock().await; guard.cancel(); @@ -243,6 +244,7 @@ impl Session { config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), tool_plugin_provenance, + auth.as_ref(), ) .await; { diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 22a322b2a..1e9efa732 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -45,7 +45,6 @@ use chrono::Local; use chrono::Utc; use codex_analytics::AnalyticsEventsClient; use codex_analytics::SubAgentThreadStartedInput; -use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; use codex_config::types::OAuthCredentialsStoreMode; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 42e98ea58..e2c21ddb2 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -396,6 +396,7 @@ impl Session { let auth_statuses = compute_auth_statuses( mcp_servers.iter(), config_for_mcp.mcp_oauth_credentials_store_mode, + auth.as_ref(), ) .await; (auth, mcp_servers, auth_statuses) @@ -887,6 +888,7 @@ impl Session { config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth), tool_plugin_provenance, + auth, ) .instrument(info_span!( "session_init.mcp_manager_init", diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 2d547b65a..e9ecb66e7 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -7,10 +7,7 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool { - matches!( - auth_manager.and_then(AuthManager::auth_mode), - Some(AuthMode::Chatgpt) - ) + auth_manager.is_some_and(AuthManager::current_auth_uses_codex_backend) } #[derive(Clone, Debug)] @@ -105,13 +102,11 @@ impl TurnContext { } pub(crate) fn apps_enabled(&self) -> bool { - let is_chatgpt_auth = self + let uses_codex_backend = self .auth_manager .as_deref() - .and_then(AuthManager::auth_cached) - .as_ref() - .is_some_and(CodexAuth::is_chatgpt_auth); - self.features.apps_enabled_for_auth(is_chatgpt_auth) + .is_some_and(AuthManager::current_auth_uses_codex_backend); + self.features.apps_enabled_for_auth(uses_codex_backend) } pub(crate) async fn with_model(&self, model: String, models_manager: &ModelsManager) -> Self { diff --git a/codex-rs/login/src/auth/agent_identity.rs b/codex-rs/login/src/auth/agent_identity.rs index e8f81f39f..5f2dc9cfc 100644 --- a/codex-rs/login/src/auth/agent_identity.rs +++ b/codex-rs/login/src/auth/agent_identity.rs @@ -39,6 +39,10 @@ impl AgentIdentityAuth { &self.record } + pub fn process_task_id(&self) -> Option<&str> { + self.process_task_id.get().map(String::as_str) + } + pub async fn ensure_runtime(&self, chatgpt_base_url: Option) -> std::io::Result<()> { self.process_task_id .get_or_try_init(|| async { diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 6cc87386f..419c6a4ba 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -397,6 +397,11 @@ impl CodexAuth { }) } + pub fn is_workspace_account(&self) -> bool { + self.account_plan_type() + .is_some_and(AccountPlanType::is_workspace_account) + } + /// Returns `None` if token-backed ChatGPT auth is unavailable. fn get_current_auth_json(&self) -> Option { let state = match self { @@ -1709,6 +1714,13 @@ impl AuthManager { self.auth_cached().as_ref().map(CodexAuth::auth_mode) } + pub fn current_auth_uses_codex_backend(&self) -> bool { + matches!( + self.auth_mode(), + Some(AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens | AuthMode::AgentIdentity) + ) + } + fn is_stale_for_proactive_refresh(auth: &CodexAuth) -> bool { let chatgpt_auth = match auth { CodexAuth::Chatgpt(chatgpt_auth) => chatgpt_auth, diff --git a/codex-rs/model-provider/Cargo.toml b/codex-rs/model-provider/Cargo.toml index 72fee8135..f5ff5b10c 100644 --- a/codex-rs/model-provider/Cargo.toml +++ b/codex-rs/model-provider/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] async-trait = { workspace = true } codex-api = { workspace = true } +codex-agent-identity = { workspace = true } codex-aws-auth = { workspace = true } codex-client = { workspace = true } codex-login = { workspace = true } diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs index 64640dcc9..d9e31e782 100644 --- a/codex-rs/model-provider/src/auth.rs +++ b/codex-rs/model-provider/src/auth.rs @@ -1,12 +1,73 @@ use std::sync::Arc; +use codex_agent_identity::AgentIdentityKey; +use codex_agent_identity::AgentTaskAuthorizationTarget; +use codex_agent_identity::authorization_header_for_agent_task; +use codex_api::AuthProvider; use codex_api::SharedAuthProvider; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_model_provider_info::ModelProviderInfo; +use http::HeaderMap; +use http::HeaderValue; use crate::bearer_auth_provider::BearerAuthProvider; +#[derive(Clone, Debug)] +struct AgentIdentityAuthProvider { + auth: codex_login::auth::AgentIdentityAuth, +} + +impl AuthProvider for AgentIdentityAuthProvider { + fn add_auth_headers(&self, headers: &mut HeaderMap) { + let record = self.auth.record(); + let header_value = self + .auth + .process_task_id() + .ok_or_else(|| std::io::Error::other("agent identity process task is not initialized")) + .and_then(|task_id| { + authorization_header_for_agent_task( + AgentIdentityKey { + agent_runtime_id: &record.agent_runtime_id, + private_key_pkcs8_base64: &record.agent_private_key, + }, + AgentTaskAuthorizationTarget { + agent_runtime_id: &record.agent_runtime_id, + task_id, + }, + ) + .map_err(std::io::Error::other) + }); + + if let Ok(header_value) = header_value + && let Ok(header) = HeaderValue::from_str(&header_value) + { + let _ = headers.insert(http::header::AUTHORIZATION, header); + } + + if let Ok(header) = HeaderValue::from_str(self.auth.account_id()) { + let _ = headers.insert("ChatGPT-Account-ID", header); + } + + if self.auth.is_fedramp_account() { + let _ = headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true")); + } + } +} + +// Some providers are meant to send no auth headers. Examples include local OSS +// providers and custom test providers with `requires_openai_auth = false`. +#[derive(Clone, Debug)] +struct UnauthenticatedAuthProvider; + +impl AuthProvider for UnauthenticatedAuthProvider { + fn add_auth_headers(&self, _headers: &mut HeaderMap) {} +} + +pub fn unauthenticated_auth_provider() -> SharedAuthProvider { + Arc::new(UnauthenticatedAuthProvider) +} + /// Returns the provider-scoped auth manager when this provider uses command-backed auth. /// /// Providers without custom auth continue using the caller-supplied base manager, when present. @@ -20,45 +81,63 @@ pub(crate) fn auth_manager_for_provider( } } -fn bearer_auth_provider_from_auth( - auth: Option<&CodexAuth>, - provider: &ModelProviderInfo, -) -> codex_protocol::error::Result { - if let Some(api_key) = provider.api_key()? { - return Ok(BearerAuthProvider { - token: Some(api_key), - account_id: None, - is_fedramp_account: false, - }); - } - - if let Some(token) = provider.experimental_bearer_token.clone() { - return Ok(BearerAuthProvider { - token: Some(token), - account_id: None, - is_fedramp_account: false, - }); - } - - if let Some(auth) = auth { - let token = auth.get_token()?; - Ok(BearerAuthProvider { - token: Some(token), - account_id: auth.get_account_id(), - is_fedramp_account: auth.is_fedramp_account(), - }) - } else { - Ok(BearerAuthProvider { - token: None, - account_id: None, - is_fedramp_account: false, - }) - } -} - pub(crate) fn resolve_provider_auth( auth: Option<&CodexAuth>, provider: &ModelProviderInfo, ) -> codex_protocol::error::Result { - Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?)) + if let Some(auth) = bearer_auth_for_provider(provider)? { + return Ok(Arc::new(auth)); + } + + Ok(match auth { + Some(auth) => auth_provider_from_auth(auth), + None => unauthenticated_auth_provider(), + }) +} + +fn bearer_auth_for_provider( + provider: &ModelProviderInfo, +) -> codex_protocol::error::Result> { + if let Some(api_key) = provider.api_key()? { + return Ok(Some(BearerAuthProvider::new(api_key))); + } + + if let Some(token) = provider.experimental_bearer_token.clone() { + return Ok(Some(BearerAuthProvider::new(token))); + } + + Ok(None) +} + +/// Builds request-header auth for a first-party Codex auth snapshot. +pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider { + match auth { + CodexAuth::AgentIdentity(auth) => { + Arc::new(AgentIdentityAuthProvider { auth: auth.clone() }) + } + CodexAuth::ApiKey(_) | CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => { + Arc::new(BearerAuthProvider { + token: auth.get_token().ok(), + account_id: auth.get_account_id(), + is_fedramp_account: auth.is_fedramp_account(), + }) + } + } +} + +#[cfg(test)] +mod tests { + use codex_model_provider_info::WireApi; + use codex_model_provider_info::create_oss_provider_with_base_url; + + use super::*; + + #[test] + fn unauthenticated_auth_provider_adds_no_headers() { + let provider = + create_oss_provider_with_base_url("http://localhost:11434/v1", WireApi::Responses); + let auth = resolve_provider_auth(/*auth*/ None, &provider).expect("auth should resolve"); + + assert!(auth.to_auth_headers().is_empty()); + } } diff --git a/codex-rs/model-provider/src/bearer_auth_provider.rs b/codex-rs/model-provider/src/bearer_auth_provider.rs index 5a24ca6f7..a28e06922 100644 --- a/codex-rs/model-provider/src/bearer_auth_provider.rs +++ b/codex-rs/model-provider/src/bearer_auth_provider.rs @@ -11,6 +11,14 @@ pub struct BearerAuthProvider { } impl BearerAuthProvider { + pub fn new(token: String) -> Self { + Self { + token: Some(token), + account_id: None, + is_fedramp_account: false, + } + } + pub fn for_test(token: Option<&str>, account_id: Option<&str>) -> Self { Self { token: token.map(str::to_string), diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index f12c6a914..f5454edd3 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -3,6 +3,8 @@ mod auth; mod bearer_auth_provider; mod provider; +pub use auth::auth_provider_from_auth; +pub use auth::unauthenticated_auth_provider; pub use bearer_auth_provider::BearerAuthProvider; pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider; pub use provider::ModelProvider; diff --git a/codex-rs/models-manager/src/manager.rs b/codex-rs/models-manager/src/manager.rs index c029960a7..34f9f7a78 100644 --- a/codex-rs/models-manager/src/manager.rs +++ b/codex-rs/models-manager/src/manager.rs @@ -9,7 +9,6 @@ use codex_api::ReqwestTransport; use codex_api::TransportError; use codex_api::auth_header_telemetry; use codex_api::map_api_error; -use codex_app_server_protocol::AuthMode; use codex_feedback::FeedbackRequestTags; use codex_feedback::emit_feedback_request_tags_with_auth_env; use codex_login::AuthEnvTelemetry; @@ -407,11 +406,13 @@ impl ModelsManager { return Ok(()); } - let auth_mode = self + let uses_codex_backend = self .provider - .auth_manager() - .and_then(|auth_manager| auth_manager.auth_mode()); - if auth_mode != Some(AuthMode::Chatgpt) && !self.provider.info().has_command_auth() { + .auth() + .await + .as_ref() + .is_some_and(CodexAuth::uses_codex_backend); + if !uses_codex_backend && !self.provider.info().has_command_auth() { if matches!( refresh_strategy, RefreshStrategy::Offline | RefreshStrategy::OnlineIfUncached @@ -536,12 +537,12 @@ impl ModelsManager { remote_models.sort_by(|a, b| a.priority.cmp(&b.priority)); let mut presets: Vec = remote_models.into_iter().map(Into::into).collect(); - let auth_mode = self + let uses_codex_backend = self .provider .auth_manager() - .and_then(|auth_manager| auth_manager.auth_mode()); - let chatgpt_mode = matches!(auth_mode, Some(AuthMode::Chatgpt)); - presets = ModelPreset::filter_by_auth(presets, chatgpt_mode); + .as_deref() + .is_some_and(AuthManager::current_auth_uses_codex_backend); + presets = ModelPreset::filter_by_auth(presets, uses_codex_backend); ModelPreset::mark_default_by_picker_visibility(&mut presets); diff --git a/codex-rs/protocol/src/account.rs b/codex-rs/protocol/src/account.rs index bb46329a5..5832381cb 100644 --- a/codex-rs/protocol/src/account.rs +++ b/codex-rs/protocol/src/account.rs @@ -35,6 +35,18 @@ impl PlanType { pub fn is_business_like(self) -> bool { matches!(self, Self::Business | Self::EnterpriseCbpUsageBased) } + + pub fn is_workspace_account(self) -> bool { + matches!( + self, + Self::Team + | Self::SelfServeBusinessUsageBased + | Self::Business + | Self::EnterpriseCbpUsageBased + | Self::Enterprise + | Self::Edu + ) + } } #[cfg(test)] @@ -84,4 +96,21 @@ mod tests { assert_eq!(PlanType::EnterpriseCbpUsageBased.is_business_like(), true); assert_eq!(PlanType::Team.is_business_like(), false); } + + #[test] + fn workspace_account_helper_includes_usage_based_workspace_plans() { + assert_eq!(PlanType::Team.is_workspace_account(), true); + assert_eq!( + PlanType::SelfServeBusinessUsageBased.is_workspace_account(), + true + ); + assert_eq!(PlanType::Business.is_workspace_account(), true); + assert_eq!( + PlanType::EnterpriseCbpUsageBased.is_workspace_account(), + true + ); + assert_eq!(PlanType::Enterprise.is_workspace_account(), true); + assert_eq!(PlanType::Edu.is_workspace_account(), true); + assert_eq!(PlanType::Pro.is_workspace_account(), false); + } } diff --git a/codex-rs/rmcp-client/Cargo.toml b/codex-rs/rmcp-client/Cargo.toml index 40e461314..c4f056892 100644 --- a/codex-rs/rmcp-client/Cargo.toml +++ b/codex-rs/rmcp-client/Cargo.toml @@ -13,6 +13,7 @@ axum = { workspace = true, default-features = false, features = [ "http1", "tokio", ] } +codex-api = { workspace = true } codex-client = { workspace = true } codex-config = { workspace = true } codex-exec-server = { workspace = true } diff --git a/codex-rs/rmcp-client/src/http_client_adapter.rs b/codex-rs/rmcp-client/src/http_client_adapter.rs index 0656b8ce3..a1e6680e6 100644 --- a/codex-rs/rmcp-client/src/http_client_adapter.rs +++ b/codex-rs/rmcp-client/src/http_client_adapter.rs @@ -11,6 +11,7 @@ use std::io; use std::sync::Arc; use bytes::Bytes; +use codex_api::SharedAuthProvider; use codex_exec_server::ExecServerError; use codex_exec_server::HttpClient; use codex_exec_server::HttpHeader; @@ -43,6 +44,7 @@ const NON_JSON_RESPONSE_BODY_PREVIEW_BYTES: usize = 8_192; pub(crate) struct StreamableHttpClientAdapter { http_client: Arc, default_headers: HeaderMap, + auth_provider: Option, } #[derive(Debug, thiserror::Error)] @@ -56,10 +58,15 @@ pub(crate) enum StreamableHttpClientAdapterError { } impl StreamableHttpClientAdapter { - pub(crate) fn new(http_client: Arc, default_headers: HeaderMap) -> Self { + pub(crate) fn new( + http_client: Arc, + default_headers: HeaderMap, + auth_provider: Option, + ) -> Self { Self { http_client, default_headers, + auth_provider, } } } @@ -75,6 +82,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter { auth_token: Option, ) -> std::result::Result> { let mut headers = self.default_headers.clone(); + self.add_auth_headers(&mut headers); insert_header( &mut headers, ACCEPT, @@ -171,6 +179,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter { auth_token: Option, ) -> std::result::Result<(), StreamableHttpError> { let mut headers = self.default_headers.clone(); + self.add_auth_headers(&mut headers); if let Some(auth_token) = auth_token { insert_header( &mut headers, @@ -223,6 +232,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter { StreamableHttpError, > { let mut headers = self.default_headers.clone(); + self.add_auth_headers(&mut headers); insert_header( &mut headers, ACCEPT, @@ -297,6 +307,14 @@ impl StreamableHttpClient for StreamableHttpClientAdapter { } } +impl StreamableHttpClientAdapter { + fn add_auth_headers(&self, headers: &mut HeaderMap) { + if let Some(auth_provider) = &self.auth_provider { + headers.extend(auth_provider.to_auth_headers()); + } + } +} + fn body_preview(body: impl Into) -> String { let mut body_preview = body.into(); let body_len = body_preview.len(); diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index 0608e00d7..5cdb1d441 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -11,6 +11,7 @@ use std::time::Instant; use anyhow::Result; use anyhow::anyhow; +use codex_api::SharedAuthProvider; use codex_client::build_reqwest_client_with_custom_ca; use codex_config::types::McpServerEnvVar; use codex_exec_server::HttpClient; @@ -108,6 +109,7 @@ enum TransportRecipe { env_http_headers: Option>, store_mode: OAuthCredentialsStoreMode, http_client: Arc, + auth_provider: Option, }, } @@ -306,6 +308,7 @@ impl RmcpClient { env_http_headers: Option>, store_mode: OAuthCredentialsStoreMode, http_client: Arc, + auth_provider: Option, ) -> Result { let transport_recipe = TransportRecipe::StreamableHttp { server_name: server_name.to_string(), @@ -315,6 +318,7 @@ impl RmcpClient { env_http_headers, store_mode, http_client, + auth_provider, }; let transport = Self::create_pending_transport(&transport_recipe).await?; Ok(Self { @@ -667,22 +671,25 @@ impl RmcpClient { env_http_headers, store_mode, http_client, + auth_provider, } => { let default_headers = build_default_headers(http_headers.clone(), env_http_headers.clone())?; - let initial_oauth_tokens = - if bearer_token.is_none() && !default_headers.contains_key(AUTHORIZATION) { - match load_oauth_tokens(server_name, url, *store_mode) { - Ok(tokens) => tokens, - Err(err) => { - warn!("failed to read tokens for server `{server_name}`: {err}"); - None - } + let initial_oauth_tokens = if bearer_token.is_none() + && auth_provider.is_none() + && !default_headers.contains_key(AUTHORIZATION) + { + match load_oauth_tokens(server_name, url, *store_mode) { + Ok(tokens) => tokens, + Err(err) => { + warn!("failed to read tokens for server `{server_name}`: {err}"); + None } - } else { - None - }; + } + } else { + None + }; if let Some(initial_tokens) = initial_oauth_tokens.clone() { match create_oauth_transport_and_runtime( @@ -722,6 +729,7 @@ impl RmcpClient { StreamableHttpClientAdapter::new( Arc::clone(http_client), default_headers, + /*auth_provider*/ None, ), http_config, ); @@ -737,7 +745,11 @@ impl RmcpClient { } let transport = StreamableHttpClientTransport::with_client( - StreamableHttpClientAdapter::new(Arc::clone(http_client), default_headers), + StreamableHttpClientAdapter::new( + Arc::clone(http_client), + default_headers, + auth_provider.clone(), + ), http_config, ); Ok(PendingTransport::StreamableHttp { transport }) @@ -958,7 +970,7 @@ async fn create_oauth_transport_and_runtime( }; let auth_client = AuthClient::new( - StreamableHttpClientAdapter::new(http_client, default_headers), + StreamableHttpClientAdapter::new(http_client, default_headers, /*auth_provider*/ None), manager, ); let auth_manager = auth_client.auth_manager.clone(); diff --git a/codex-rs/rmcp-client/tests/streamable_http_test_support.rs b/codex-rs/rmcp-client/tests/streamable_http_test_support.rs index ec7f7dc6f..cfff33ab4 100644 --- a/codex-rs/rmcp-client/tests/streamable_http_test_support.rs +++ b/codex-rs/rmcp-client/tests/streamable_http_test_support.rs @@ -98,6 +98,7 @@ pub(crate) async fn create_client(base_url: &str) -> anyhow::Result /*env_http_headers*/ None, OAuthCredentialsStoreMode::File, Environment::default_for_tests().get_http_client(), + /*auth_provider*/ None, ) .await?; @@ -135,6 +136,7 @@ pub(crate) async fn create_remote_client( /*env_http_headers*/ None, OAuthCredentialsStoreMode::File, Arc::new(http_client), + /*auth_provider*/ None, ) .await?;