diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 23dae42a9d..0dab5a4c57 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -536,6 +536,13 @@ ], "type": "string" }, + { + "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.", + "enum": [ + "hostProvided" + ], + "type": "string" + }, { "description": "Programmatic Codex auth backed by a registered Agent Identity.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 341c325b1d..4f56b6fc45 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6950,6 +6950,13 @@ ], "type": "string" }, + { + "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.", + "enum": [ + "hostProvided" + ], + "type": "string" + }, { "description": "Programmatic Codex auth backed by a registered Agent Identity.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 488bd6016e..9db95a2c97 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1076,6 +1076,13 @@ ], "type": "string" }, + { + "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.", + "enum": [ + "hostProvided" + ], + "type": "string" + }, { "description": "Programmatic Codex auth backed by a registered Agent Identity.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json index 5c1847c56c..cbbed1ce26 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json @@ -25,6 +25,13 @@ ], "type": "string" }, + { + "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.", + "enum": [ + "hostProvided" + ], + "type": "string" + }, { "description": "Programmatic Codex auth backed by a registered Agent Identity.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/AuthMode.ts b/codex-rs/app-server-protocol/schema/typescript/AuthMode.ts index 77bc037ebe..51f390dc71 100644 --- a/codex-rs/app-server-protocol/schema/typescript/AuthMode.ts +++ b/codex-rs/app-server-protocol/schema/typescript/AuthMode.ts @@ -5,4 +5,4 @@ /** * Authentication mode for OpenAI-backed providers. */ -export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey"; +export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "hostProvided" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7b15035575..fe629415d4 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -31,6 +31,13 @@ pub enum AuthMode { #[ts(rename = "chatgptAuthTokens")] #[strum(serialize = "chatgptAuthTokens")] ChatgptAuthTokens, + /// [UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. + /// + /// Codex backend request headers supplied by the process embedding Codex. + #[serde(rename = "hostProvided")] + #[ts(rename = "hostProvided")] + #[strum(serialize = "hostProvided")] + HostProvided, /// Programmatic Codex auth backed by a registered Agent Identity. #[serde(rename = "agentIdentity")] #[ts(rename = "agentIdentity")] @@ -53,7 +60,7 @@ impl AuthMode { pub fn has_chatgpt_account(self) -> bool { match self { Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true, - Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false, + Self::ApiKey | Self::HostProvided | Self::AgentIdentity | Self::BedrockApiKey => false, } } @@ -62,6 +69,7 @@ impl AuthMode { match self { Self::Chatgpt | Self::ChatgptAuthTokens + | Self::HostProvided | Self::AgentIdentity | Self::PersonalAccessToken => true, Self::ApiKey | Self::BedrockApiKey => false, diff --git a/codex-rs/app-server/src/auth_mode.rs b/codex-rs/app-server/src/auth_mode.rs index 67d3ee61b9..802249c9a4 100644 --- a/codex-rs/app-server/src/auth_mode.rs +++ b/codex-rs/app-server/src/auth_mode.rs @@ -12,6 +12,7 @@ pub(crate) fn auth_mode_to_api(auth_mode: AuthMode) -> ApiAuthMode { AuthMode::ApiKey => ApiAuthMode::ApiKey, AuthMode::Chatgpt => ApiAuthMode::Chatgpt, AuthMode::ChatgptAuthTokens => ApiAuthMode::ChatgptAuthTokens, + AuthMode::HostProvided => ApiAuthMode::HostProvided, AuthMode::AgentIdentity => ApiAuthMode::AgentIdentity, AuthMode::PersonalAccessToken => ApiAuthMode::PersonalAccessToken, AuthMode::BedrockApiKey => ApiAuthMode::BedrockApiKey, diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index 4691e22204..38f6147d05 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -812,7 +812,9 @@ impl AccountRequestProcessor { let auth_mode = auth_mode_to_api(auth.api_auth_mode()); let (reported_auth_method, token_opt) = if matches!( auth, - CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) + CodexAuth::HostProvided(_) + | CodexAuth::AgentIdentity(_) + | CodexAuth::PersonalAccessToken(_) ) || include_token && permanent_refresh_failure { diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index ef587cd1d4..37c7653753 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -1327,6 +1327,7 @@ fn stored_auth_mode(auth: &codex_login::AuthDotJson) -> &'static str { AuthMode::ApiKey => "api_key", AuthMode::Chatgpt => "chatgpt", AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens", + AuthMode::HostProvided => "host_provided", AuthMode::AgentIdentity => "agent_identity", AuthMode::PersonalAccessToken => "personal_access_token", AuthMode::BedrockApiKey => "bedrock_api_key", @@ -1397,6 +1398,9 @@ fn stored_auth_issues( issues.push("external ChatGPT auth is missing refresh metadata"); } } + AuthMode::HostProvided => { + issues.push("host-provided auth cannot be loaded from auth storage"); + } AuthMode::AgentIdentity => { if auth .agent_identity @@ -2466,6 +2470,7 @@ fn auth_mode_name(auth: &CodexAuth) -> &'static str { AuthMode::ApiKey => "api_key", AuthMode::Chatgpt => "chatgpt", AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens", + AuthMode::HostProvided => "host_provided", AuthMode::AgentIdentity => "agent_identity", AuthMode::PersonalAccessToken => "personal_access_token", AuthMode::BedrockApiKey => "bedrock_api_key", @@ -2605,6 +2610,7 @@ fn provider_auth_reachability_mode_from_auth( Some( AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens + | AuthMode::HostProvided | AuthMode::AgentIdentity | AuthMode::PersonalAccessToken, ) diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index d3f9b7b96b..8f982f9691 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -449,6 +449,10 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { eprintln!("Logged in using ChatGPT"); std::process::exit(0); } + AuthMode::HostProvided => { + eprintln!("Logged in using host-provided auth"); + std::process::exit(0); + } AuthMode::AgentIdentity => { eprintln!("Logged in using access token"); std::process::exit(0); diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 1f61ab700e..19ba252331 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -26,6 +26,8 @@ use codex_config::McpServerToolConfig; use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; use codex_exec_server::EnvironmentManager; +use codex_login::CodexAuth; +use codex_login::HostProvidedAuth; use codex_protocol::ToolName; use codex_protocol::mcp::McpServerInfo; use codex_protocol::models::PermissionProfile; @@ -68,6 +70,34 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { } } +#[test] +fn host_provided_auth_scopes_codex_apps_cache() { + let auth = CodexAuth::HostProvided( + HostProvidedAuth::new([], "user-123").with_account_id("account-123"), + ); + + assert_eq!( + crate::codex_apps::codex_apps_tools_cache_key(Some(&auth)), + CodexAppsToolsCacheKey { + account_id: Some("account-123".to_string()), + chatgpt_user_id: Some("user-123".to_string()), + is_workspace_account: false, + } + ); +} + +fn create_test_tool_with_connector( + server_name: &str, + tool_name: &str, + connector_id: &str, + connector_name: Option<&str>, +) -> ToolInfo { + let mut tool = create_test_tool(server_name, tool_name); + tool.connector_id = Some(connector_id.to_string()); + tool.connector_name = connector_name.map(ToOwned::to_owned); + tool +} + fn create_codex_apps_tools_cache_context( codex_home: PathBuf, account_id: Option<&str>, diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index c7cfff8a92..11e7609ac5 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -70,6 +70,7 @@ pub use codex_features::Feature; pub use codex_features::Features; pub use codex_home::CodexHomeUserInstructionsProvider; pub use codex_login::AuthManager; +pub use codex_login::HostProvidedAuth; pub use codex_login::default_client::set_default_originator; pub use codex_model_provider_info::OPENAI_PROVIDER_ID; pub use codex_model_provider_info::built_in_model_providers; diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ffa8ae8e20..c1aa8bd369 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -2088,6 +2088,7 @@ impl AuthRequestTelemetryContext { AuthMode::ApiKey | AuthMode::BedrockApiKey => "ApiKey", AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens + | AuthMode::HostProvided | AuthMode::AgentIdentity | AuthMode::PersonalAccessToken => "Chatgpt", }), diff --git a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs index 3836234b84..28d6d7d865 100644 --- a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs +++ b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs @@ -10,6 +10,7 @@ use codex_extension_api::McpServerContribution; use codex_extension_api::McpServerContributionContext; use codex_extension_api::McpServerContributor; use codex_login::CodexAuth; +use codex_login::HostProvidedAuth; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use pretty_assertions::assert_eq; @@ -146,6 +147,24 @@ async fn hosted_apps_mcp_requires_chatgpt_auth() -> TestResult { Ok(()) } +#[tokio::test] +async fn hosted_apps_mcp_accepts_host_provided_codex_auth() -> TestResult { + let codex_home = tempfile::tempdir()?; + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cli_overrides(vec![("features.apps".to_string(), true.into())]) + .build() + .await?; + let auth = CodexAuth::HostProvided(HostProvidedAuth::new([], "user-123")); + let manager = installed_manager(&config); + + let servers = manager.effective_servers(&config, Some(&auth)).await; + + assert!(servers.contains_key(CODEX_APPS_MCP_SERVER_NAME)); + Ok(()) +} + #[tokio::test] async fn disabled_apps_remove_reserved_server_config_for_all_hosts() -> TestResult { let codex_home = tempfile::tempdir()?; diff --git a/codex-rs/login/src/auth/bedrock_api_key_tests.rs b/codex-rs/login/src/auth/bedrock_api_key_tests.rs index 1f8fab68b2..83982252df 100644 --- a/codex-rs/login/src/auth/bedrock_api_key_tests.rs +++ b/codex-rs/login/src/auth/bedrock_api_key_tests.rs @@ -85,6 +85,7 @@ async fn login_with_bedrock_api_key_replaces_openai_auth() -> anyhow::Result<()> CodexAuth::ApiKey(_) | CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) + | CodexAuth::HostProvided(_) | CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => None, }), @@ -148,6 +149,7 @@ async fn bedrock_only_auth_storage_creates_primary_auth() -> anyhow::Result<()> CodexAuth::ApiKey(_) | CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) + | CodexAuth::HostProvided(_) | CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => None, }), diff --git a/codex-rs/login/src/auth/host_provided.rs b/codex-rs/login/src/auth/host_provided.rs new file mode 100644 index 0000000000..3d95f24189 --- /dev/null +++ b/codex-rs/login/src/auth/host_provided.rs @@ -0,0 +1,58 @@ +use std::collections::BTreeMap; +use std::fmt; + +/// Codex backend authentication supplied by the process embedding Codex. +/// +/// The embedding process owns credential validation, rotation, and persistence. +/// Codex keeps this snapshot in memory and uses its headers for backend requests. +#[derive(Clone, PartialEq, Eq)] +pub struct HostProvidedAuth { + headers: BTreeMap, + account_id: Option, + user_id: String, +} + +impl fmt::Debug for HostProvidedAuth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HostProvidedAuth") + .field("headers", &"") + .field("account_id", &self.account_id) + .field("user_id", &self.user_id) + .finish() + } +} + +impl HostProvidedAuth { + /// Creates host-provided auth with backend request headers and a stable user identity. + pub fn new( + headers: impl IntoIterator, + user_id: impl Into, + ) -> Self { + Self { + headers: headers.into_iter().collect(), + account_id: None, + user_id: user_id.into(), + } + } + + /// Adds the account selected by the embedding process. + pub fn with_account_id(mut self, account_id: impl Into) -> Self { + self.account_id = Some(account_id.into()); + self + } + + /// Returns the request headers supplied by the embedding process. + pub fn headers(&self) -> &BTreeMap { + &self.headers + } + + /// Returns the selected account ID, when the embedding process supplied one. + pub fn account_id(&self) -> Option<&str> { + self.account_id.as_deref() + } + + /// Returns the stable user ID supplied by the embedding process. + pub fn user_id(&self) -> &str { + &self.user_id + } +} diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 844410874d..cbeef197fa 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -41,6 +41,7 @@ use super::revoke::revoke_auth_tokens; pub use crate::auth::agent_identity::AgentIdentityAuth; pub use crate::auth::agent_identity::AgentIdentityAuthError; pub use crate::auth::bedrock_api_key::BedrockApiKeyAuth; +use crate::auth::host_provided::HostProvidedAuth; pub use crate::auth::personal_access_token::PersonalAccessTokenAuth; pub use crate::auth::storage::AgentIdentityAuthRecord; pub use crate::auth::storage::AgentIdentityStorage; @@ -71,6 +72,7 @@ pub enum CodexAuth { ApiKey(ApiKeyAuth), Chatgpt(ChatgptAuth), ChatgptAuthTokens(ChatgptAuthTokens), + HostProvided(HostProvidedAuth), AgentIdentity(AgentIdentityAuth), PersonalAccessToken(PersonalAccessTokenAuth), BedrockApiKey(BedrockApiKeyAuth), @@ -145,6 +147,7 @@ impl AgentIdentityBootstrapCooldown { impl PartialEq for CodexAuth { fn eq(&self, other: &Self) -> bool { match (self, other) { + (Self::HostProvided(a), Self::HostProvided(b)) => a == b, (Self::PersonalAccessToken(a), Self::PersonalAccessToken(b)) => a == b, (Self::BedrockApiKey(a), Self::BedrockApiKey(b)) => a == b, _ => self.api_auth_mode() == other.api_auth_mode(), @@ -357,6 +360,11 @@ impl CodexAuth { }; return Ok(Self::BedrockApiKey(auth)); } + if auth_mode == AuthMode::HostProvided { + return Err(std::io::Error::other( + "host-provided auth cannot be loaded from auth storage.", + )); + } let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode); let client = create_default_auth_client(&refresh_token_endpoint(), auth_route_config)?; @@ -376,6 +384,9 @@ impl CodexAuth { } AuthMode::ChatgptAuthTokens => Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { state })), AuthMode::ApiKey => unreachable!("api key mode is handled above"), + AuthMode::HostProvided => { + unreachable!("host-provided auth is never loaded from auth storage") + } AuthMode::AgentIdentity => unreachable!("agent identity mode is handled above"), AuthMode::PersonalAccessToken => { unreachable!("personal access token mode is handled above") @@ -458,6 +469,7 @@ impl CodexAuth { match self { Self::ApiKey(_) => AuthMode::ApiKey, Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => AuthMode::Chatgpt, + Self::HostProvided(_) => AuthMode::HostProvided, Self::AgentIdentity(_) => AuthMode::AgentIdentity, Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken, Self::BedrockApiKey(_) => AuthMode::BedrockApiKey, @@ -470,6 +482,7 @@ impl CodexAuth { Self::ApiKey(_) => AuthMode::ApiKey, Self::Chatgpt(_) => AuthMode::Chatgpt, Self::ChatgptAuthTokens(_) => AuthMode::ChatgptAuthTokens, + Self::HostProvided(_) => AuthMode::HostProvided, Self::AgentIdentity(_) => AuthMode::AgentIdentity, Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken, Self::BedrockApiKey(_) => AuthMode::BedrockApiKey, @@ -506,6 +519,7 @@ impl CodexAuth { Self::ApiKey(auth) => Some(auth.api_key.as_str()), Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) + | Self::HostProvided(_) | Self::AgentIdentity(_) | Self::PersonalAccessToken(_) | Self::BedrockApiKey(_) => None, @@ -536,6 +550,9 @@ impl CodexAuth { Self::AgentIdentity(_) => Err(std::io::Error::other( "agent identity auth does not expose a bearer token", )), + Self::HostProvided(_) => Err(std::io::Error::other( + "host-provided auth does not expose a bearer token", + )), Self::PersonalAccessToken(auth) => Ok(auth.access_token().to_string()), Self::BedrockApiKey(_) => Err(std::io::Error::other( "Bedrock API key auth does not expose a Codex bearer token", @@ -546,6 +563,7 @@ impl CodexAuth { /// Returns `None` if Codex backend auth does not expose an account id. pub fn get_account_id(&self) -> Option { match self { + Self::HostProvided(auth) => auth.account_id().map(str::to_string), Self::AgentIdentity(auth) => Some(auth.account_id().to_string()), Self::PersonalAccessToken(auth) => Some(auth.account_id().to_string()), _ => self.get_current_token_data().and_then(|t| t.account_id), @@ -555,6 +573,7 @@ impl CodexAuth { /// Returns false if Codex backend auth omits the FedRAMP claim. pub fn is_fedramp_account(&self) -> bool { match self { + Self::HostProvided(_) => false, Self::AgentIdentity(auth) => auth.is_fedramp_account(), Self::PersonalAccessToken(auth) => auth.is_fedramp_account(), _ => self @@ -566,6 +585,7 @@ impl CodexAuth { /// Returns `None` if Codex backend auth does not expose an account email. pub fn get_account_email(&self) -> Option { match self { + Self::HostProvided(_) => None, Self::AgentIdentity(auth) => auth.email().map(str::to_string), Self::PersonalAccessToken(auth) => auth.email().map(str::to_string), _ => self.get_current_token_data().and_then(|t| t.id_token.email), @@ -575,6 +595,7 @@ impl CodexAuth { /// Returns `None` if Codex backend auth does not expose a ChatGPT user id. pub fn get_chatgpt_user_id(&self) -> Option { match self { + Self::HostProvided(auth) => Some(auth.user_id().to_string()), Self::AgentIdentity(auth) => Some(auth.chatgpt_user_id().to_string()), Self::PersonalAccessToken(auth) => Some(auth.chatgpt_user_id().to_string()), _ => self @@ -587,6 +608,9 @@ impl CodexAuth { /// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…) /// for UI or product decisions based on the user's subscription. pub fn account_plan_type(&self) -> Option { + if matches!(self, Self::HostProvided(_)) { + return Some(AccountPlanType::Unknown); + } if let Self::AgentIdentity(auth) = self { return Some(auth.plan_type()); } @@ -613,6 +637,7 @@ impl CodexAuth { Self::Chatgpt(auth) => &auth.state, Self::ChatgptAuthTokens(auth) => &auth.state, Self::ApiKey(_) + | Self::HostProvided(_) | Self::AgentIdentity(_) | Self::PersonalAccessToken(_) | Self::BedrockApiKey(_) => return None, @@ -658,6 +683,7 @@ impl CodexAuth { Self::AgentIdentity(auth) => Ok(Some(auth.clone())), Self::ApiKey(_) | Self::ChatgptAuthTokens(_) + | Self::HostProvided(_) | Self::PersonalAccessToken(_) | Self::BedrockApiKey(_) => Ok(None), Self::Chatgpt(_) => { @@ -1086,10 +1112,12 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url( | (ForcedLoginMethod::Api, AuthMode::BedrockApiKey) => None, (ForcedLoginMethod::Chatgpt, AuthMode::Chatgpt) | (ForcedLoginMethod::Chatgpt, AuthMode::ChatgptAuthTokens) + | (ForcedLoginMethod::Chatgpt, AuthMode::HostProvided) | (ForcedLoginMethod::Chatgpt, AuthMode::AgentIdentity) | (ForcedLoginMethod::Chatgpt, AuthMode::PersonalAccessToken) => None, (ForcedLoginMethod::Api, AuthMode::Chatgpt) | (ForcedLoginMethod::Api, AuthMode::ChatgptAuthTokens) + | (ForcedLoginMethod::Api, AuthMode::HostProvided) | (ForcedLoginMethod::Api, AuthMode::AgentIdentity) | (ForcedLoginMethod::Api, AuthMode::PersonalAccessToken) => Some( "API key login is required, but ChatGPT is currently being used. Logging out." @@ -1115,9 +1143,9 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url( if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() { let chatgpt_account_id = match &auth { CodexAuth::ApiKey(_) | CodexAuth::BedrockApiKey(_) => return Ok(()), - CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => { - auth.get_account_id() - } + CodexAuth::HostProvided(_) + | CodexAuth::AgentIdentity(_) + | CodexAuth::PersonalAccessToken(_) => auth.get_account_id(), CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => { let token_data = match auth.get_token_data() { Ok(data) => data, @@ -2172,6 +2200,7 @@ impl AuthManager { | (AuthMode::ChatgptAuthTokens, AuthMode::ChatgptAuthTokens) => { a.get_current_auth_json() == b.get_current_auth_json() } + (AuthMode::HostProvided, AuthMode::HostProvided) => a == b, (AuthMode::AgentIdentity, AuthMode::AgentIdentity) => match (a, b) { (CodexAuth::AgentIdentity(a), CodexAuth::AgentIdentity(b)) => { a.record() == b.record() @@ -2256,6 +2285,11 @@ impl AuthManager { } } + /// Replaces the current auth snapshot with Codex backend auth supplied by the host. + pub fn set_host_provided_auth(&self, auth: HostProvidedAuth) { + let _ = self.set_cached_auth(Some(CodexAuth::HostProvided(auth))); + } + pub fn clear_external_auth(&self) { if let Ok(mut guard) = self.external_auth.write() { *guard = None; @@ -2452,6 +2486,7 @@ impl AuthManager { .await } CodexAuth::ApiKey(_) + | CodexAuth::HostProvided(_) | CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) | CodexAuth::BedrockApiKey(_) => Ok(()), diff --git a/codex-rs/login/src/auth/mod.rs b/codex-rs/login/src/auth/mod.rs index d87c7cc6ad..a1528fce8e 100644 --- a/codex-rs/login/src/auth/mod.rs +++ b/codex-rs/login/src/auth/mod.rs @@ -8,6 +8,7 @@ mod storage; mod util; mod external_bearer; +mod host_provided; mod manager; mod revoke; @@ -15,4 +16,5 @@ pub use bedrock_api_key::BedrockApiKeyAuth; pub use bedrock_api_key::login_with_bedrock_api_key; pub use error::RefreshTokenFailedError; pub use error::RefreshTokenFailedReason; +pub use host_provided::HostProvidedAuth; pub use manager::*; diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 44c2f1c4d3..9382767b63 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -35,6 +35,7 @@ pub use auth::ExternalAuthFuture; pub use auth::ExternalAuthRefreshContext; pub use auth::ExternalAuthRefreshReason; pub use auth::ExternalAuthTokens; +pub use auth::HostProvidedAuth; pub use auth::OPENAI_API_KEY_ENV_VAR; pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; pub use auth::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR; diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index 65517cbd43..58e1515419 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -244,6 +244,7 @@ impl ModelProviderInfo { Some( AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens + | AuthMode::HostProvided | AuthMode::AgentIdentity | AuthMode::PersonalAccessToken ) diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 108308c1bf..aaeebef3f6 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -69,6 +69,7 @@ impl AmazonBedrockModelProvider { CodexAuth::ApiKey(_) | CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) + | CodexAuth::HostProvided(_) | CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => None, }) diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs index 5b667e4549..3adfad6afb 100644 --- a/codex-rs/model-provider/src/auth.rs +++ b/codex-rs/model-provider/src/auth.rs @@ -9,6 +9,7 @@ use codex_api::AuthProvider; use codex_api::SharedAuthProvider; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_login::HostProvidedAuth; use codex_login::auth::AgentIdentityAuth; use codex_login::auth::AgentIdentityAuthError; use codex_login::auth::AgentIdentityAuthPolicy; @@ -16,6 +17,7 @@ use codex_model_provider_info::ModelProviderInfo; use codex_protocol::error::CodexErr; use codex_protocol::protocol::SessionSource; use http::HeaderMap; +use http::HeaderName; use http::HeaderValue; use crate::bearer_auth_provider::BearerAuthProvider; @@ -109,6 +111,30 @@ impl AuthProvider for AgentIdentityAuthProvider { } } +#[derive(Clone, Debug)] +struct HostProvidedAuthProvider { + auth: HostProvidedAuth, +} + +impl AuthProvider for HostProvidedAuthProvider { + fn add_auth_headers(&self, headers: &mut HeaderMap) { + for (name, value) in self.auth.headers() { + let Ok(name) = HeaderName::from_bytes(name.as_bytes()) else { + continue; + }; + let Ok(value) = HeaderValue::from_str(value) else { + continue; + }; + let _ = headers.insert(name, value); + } + if let Some(account_id) = self.auth.account_id() + && let Ok(account_id) = HeaderValue::from_str(account_id) + { + let _ = headers.insert("ChatGPT-Account-ID", account_id); + } + } +} + // 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)] @@ -240,6 +266,9 @@ fn bearer_auth_for_provider( /// Builds request-header auth for a first-party Codex auth snapshot. pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider { match auth { + CodexAuth::HostProvided(auth) => { + Arc::new(HostProvidedAuthProvider { auth: auth.clone() }) + } CodexAuth::AgentIdentity(auth) => { Arc::new(AgentIdentityAuthProvider { auth: auth.clone() }) } @@ -405,6 +434,35 @@ mod tests { } } + #[test] + fn host_provided_auth_provider_uses_host_headers() { + let auth = CodexAuth::HostProvided( + HostProvidedAuth::new( + [( + "x-openai-actor-authorization".to_string(), + "actor-token".to_string(), + )], + "user-123", + ) + .with_account_id("account-123"), + ); + + let headers = auth_provider_from_auth(&auth).to_auth_headers(); + + assert_eq!( + headers + .get("x-openai-actor-authorization") + .and_then(|value| value.to_str().ok()), + Some("actor-token") + ); + assert_eq!( + headers + .get("ChatGPT-Account-ID") + .and_then(|value| value.to_str().ok()), + Some("account-123") + ); + } + #[tokio::test] async fn first_party_run_scope_uses_agent_assertion_and_exposes_telemetry() { let auth = CodexAuth::AgentIdentity( diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 5f101e52cd..32e13950a3 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -286,6 +286,7 @@ impl ModelProvider for ConfiguredModelProvider { } CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) + | CodexAuth::HostProvided(_) | CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => { let email = auth.get_account_email(); diff --git a/codex-rs/otel/src/lib.rs b/codex-rs/otel/src/lib.rs index 87ccbbfeee..2cb029c02f 100644 --- a/codex-rs/otel/src/lib.rs +++ b/codex-rs/otel/src/lib.rs @@ -59,6 +59,7 @@ impl From for TelemetryAuthMode { AuthMode::ApiKey | AuthMode::BedrockApiKey => Self::ApiKey, AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens + | AuthMode::HostProvided | AuthMode::AgentIdentity | AuthMode::PersonalAccessToken => Self::Chatgpt, } diff --git a/codex-rs/protocol/src/auth.rs b/codex-rs/protocol/src/auth.rs index 7c506e87eb..7aad9fd248 100644 --- a/codex-rs/protocol/src/auth.rs +++ b/codex-rs/protocol/src/auth.rs @@ -15,6 +15,10 @@ pub enum AuthMode { #[serde(rename = "chatgptAuthTokens")] #[strum(serialize = "chatgptAuthTokens")] ChatgptAuthTokens, + /// Codex backend request headers supplied by the process embedding Codex. + #[serde(rename = "hostProvided")] + #[strum(serialize = "hostProvided")] + HostProvided, /// Programmatic Codex auth backed by a registered Agent Identity. #[serde(rename = "agentIdentity")] #[strum(serialize = "agentIdentity")] @@ -34,7 +38,7 @@ impl AuthMode { pub fn has_chatgpt_account(self) -> bool { match self { Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true, - Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false, + Self::ApiKey | Self::HostProvided | Self::AgentIdentity | Self::BedrockApiKey => false, } } @@ -43,6 +47,7 @@ impl AuthMode { match self { Self::Chatgpt | Self::ChatgptAuthTokens + | Self::HostProvided | Self::AgentIdentity | Self::PersonalAccessToken => true, Self::ApiKey | Self::BedrockApiKey => false, diff --git a/codex-rs/tui/src/app/app_server_events.rs b/codex-rs/tui/src/app/app_server_events.rs index 06ed4320d8..db71884f4f 100644 --- a/codex-rs/tui/src/app/app_server_events.rs +++ b/codex-rs/tui/src/app/app_server_events.rs @@ -86,6 +86,7 @@ impl App { Some( AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens + | AuthMode::HostProvided | AuthMode::AgentIdentity | AuthMode::PersonalAccessToken ) diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index aeca17091e..421c4c99db 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1202,6 +1202,7 @@ pub(crate) fn status_account_display_from_auth_mode( Some(AuthMode::ApiKey) => Some(StatusAccountDisplay::ApiKey), Some(AuthMode::Chatgpt) | Some(AuthMode::ChatgptAuthTokens) + | Some(AuthMode::HostProvided) | Some(AuthMode::AgentIdentity) | Some(AuthMode::PersonalAccessToken) => Some(StatusAccountDisplay::ChatGpt { email: None, diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index f9392eb2e8..35520dda63 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -950,6 +950,7 @@ impl AuthModeWidget { ApiAuthMode::ApiKey => AuthMode::ApiKey, ApiAuthMode::Chatgpt => AuthMode::Chatgpt, ApiAuthMode::ChatgptAuthTokens => AuthMode::ChatgptAuthTokens, + ApiAuthMode::HostProvided => AuthMode::HostProvided, ApiAuthMode::AgentIdentity => AuthMode::AgentIdentity, ApiAuthMode::PersonalAccessToken => AuthMode::PersonalAccessToken, ApiAuthMode::BedrockApiKey => AuthMode::BedrockApiKey,