From 0bbaf6f714f4a95540981c1bd22cf88a6cb608ab Mon Sep 17 00:00:00 2001 From: adrian Date: Mon, 20 Apr 2026 12:08:22 -0700 Subject: [PATCH] Revert "[codex] Use background agent task auth for backend calls (#18094)" This reverts commit 904c751a4068e5a4f7005ef165f5d3afa756f420. --- codex-rs/Cargo.lock | 1 - codex-rs/analytics/src/client.rs | 22 +- .../app-server/src/codex_message_processor.rs | 218 ++--- codex-rs/codex-mcp/src/lib.rs | 4 - codex-rs/codex-mcp/src/mcp/mod.rs | 92 +- codex-rs/core-skills/src/remote.rs | 31 +- codex-rs/core/src/agent_identity.rs | 3 - codex-rs/core/src/config/mod.rs | 13 +- codex-rs/core/src/connectors.rs | 53 +- codex-rs/core/src/mcp.rs | 20 +- codex-rs/core/src/session/handlers.rs | 14 +- codex-rs/core/src/session/mcp.rs | 15 +- codex-rs/core/src/session/mod.rs | 2 +- codex-rs/core/src/session/session.rs | 14 +- codex-rs/core/src/session/tests.rs | 1 - codex-rs/login/Cargo.toml | 1 - codex-rs/login/src/agent_identity.rs | 834 ------------------ codex-rs/login/src/auth/agent_assertion.rs | 1 - codex-rs/login/src/auth/auth_tests.rs | 1 - codex-rs/login/src/auth/manager.rs | 132 +-- codex-rs/login/src/auth/storage.rs | 2 - codex-rs/login/src/auth/storage_tests.rs | 1 - codex-rs/login/src/lib.rs | 3 - 23 files changed, 163 insertions(+), 1315 deletions(-) delete mode 100644 codex-rs/login/src/agent_identity.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e7eb7fc80c..dcb2894471 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2385,7 +2385,6 @@ dependencies = [ "codex-terminal-detection", "codex-utils-template", "core_test_support", - "crypto_box", "ed25519-dalek", "keyring", "once_cell", diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index 25ab40953f..1a4b5defe9 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -297,7 +297,7 @@ impl AnalyticsEventsClient { } async fn send_track_events( - auth_manager: &Arc, + auth_manager: &AuthManager, base_url: &str, events: Vec, ) { @@ -310,11 +310,9 @@ async fn send_track_events( if !auth.is_chatgpt_auth() { return; } - let Some(authorization_header_value) = auth_manager - .chatgpt_authorization_header_for_auth(&auth) - .await - else { - return; + let access_token = match auth.get_token() { + Ok(token) => token, + Err(_) => return, }; let Some(account_id) = auth.get_account_id() else { return; @@ -324,17 +322,15 @@ async fn send_track_events( let url = format!("{base_url}/codex/analytics-events/events"); let payload = TrackEventsRequest { events }; - let mut request = create_client() + let response = create_client() .post(&url) .timeout(ANALYTICS_EVENTS_TIMEOUT) - .header("authorization", authorization_header_value) + .bearer_auth(&access_token) .header("chatgpt-account-id", &account_id) .header("Content-Type", "application/json") - .json(&payload); - if auth.is_fedramp_account() { - request = request.header("X-OpenAI-Fedramp", "true"); - } - let response = request.send().await; + .json(&payload) + .send() + .await; match response { Ok(response) if response.status().is_success() => {} diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 21cef82844..b8e569d18b 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -286,9 +286,9 @@ use codex_login::run_login_server; use codex_mcp::McpRuntimeEnvironment; use codex_mcp::McpServerStatusSnapshot; use codex_mcp::McpSnapshotDetail; -use codex_mcp::collect_mcp_server_status_snapshot_with_detail_and_authorization_header; +use codex_mcp::collect_mcp_server_status_snapshot_with_detail; use codex_mcp::discover_supported_scopes; -use codex_mcp::effective_mcp_servers_with_authorization_header; +use codex_mcp::effective_mcp_servers; use codex_mcp::resolve_oauth_scopes; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::ThreadId; @@ -5655,8 +5655,7 @@ impl CodexMessageProcessor { let mcp_config = config .to_mcp_config(self.thread_manager.plugins_manager().as_ref()) .await; - let auth_manager = Arc::clone(&self.auth_manager); - let auth = auth_manager.auth().await; + let auth = self.auth_manager.auth().await; let runtime_environment = match self.thread_manager.environment_manager().current().await { Ok(Some(environment)) => { // Status listing has no turn cwd. This fallback is used only @@ -5681,113 +5680,122 @@ impl CodexMessageProcessor { }; tokio::spawn(async move { - let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) { - McpServerStatusDetail::Full => McpSnapshotDetail::Full, - McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly, - }; - - let background_authorization_header_value = if let Some(auth) = auth.as_ref() { - auth_manager - .chatgpt_authorization_header_for_auth(auth) - .await - } else { - None - }; - let snapshot = collect_mcp_server_status_snapshot_with_detail_and_authorization_header( - &mcp_config, - auth.as_ref(), - request.request_id.to_string(), + Self::list_mcp_server_status_task( + outgoing, + request, + params, + config, + mcp_config, + auth, runtime_environment, - detail, - background_authorization_header_value.as_deref(), ) .await; - - let effective_servers = effective_mcp_servers_with_authorization_header( - &mcp_config, - auth.as_ref(), - background_authorization_header_value.as_deref(), - ); - let McpServerStatusSnapshot { - tools_by_server, - resources, - resource_templates, - auth_statuses, - } = snapshot; - - let mut server_names: Vec = config - .mcp_servers - .keys() - .cloned() - // Include built-in/plugin MCP servers that are present in the - // effective runtime config even when they are not user-declared in - // `config.mcp_servers`. - .chain(effective_servers.keys().cloned()) - .chain(auth_statuses.keys().cloned()) - .chain(resources.keys().cloned()) - .chain(resource_templates.keys().cloned()) - .collect(); - server_names.sort(); - server_names.dedup(); - - let total = server_names.len(); - let limit = params.limit.unwrap_or(total as u32).max(1) as usize; - let effective_limit = limit.min(total); - let start = match params.cursor { - Some(cursor) => match cursor.parse::() { - Ok(idx) => idx, - Err(_) => { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("invalid cursor: {cursor}"), - data: None, - }; - outgoing.send_error(request, error).await; - return; - } - }, - None => 0, - }; - - if start > total { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("cursor {start} exceeds total MCP servers {total}"), - data: None, - }; - outgoing.send_error(request, error).await; - return; - } - - let end = start.saturating_add(effective_limit).min(total); - - let data: Vec = server_names[start..end] - .iter() - .map(|name| McpServerStatus { - name: name.clone(), - tools: tools_by_server.get(name).cloned().unwrap_or_default(), - resources: resources.get(name).cloned().unwrap_or_default(), - resource_templates: resource_templates.get(name).cloned().unwrap_or_default(), - auth_status: auth_statuses - .get(name) - .cloned() - .unwrap_or(CoreMcpAuthStatus::Unsupported) - .into(), - }) - .collect(); - - let next_cursor = if end < total { - Some(end.to_string()) - } else { - None - }; - - let response = ListMcpServerStatusResponse { data, next_cursor }; - - outgoing.send_response(request, response).await; }); } + async fn list_mcp_server_status_task( + outgoing: Arc, + request_id: ConnectionRequestId, + params: ListMcpServerStatusParams, + config: Config, + mcp_config: codex_mcp::McpConfig, + auth: Option, + runtime_environment: McpRuntimeEnvironment, + ) { + let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) { + McpServerStatusDetail::Full => McpSnapshotDetail::Full, + McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly, + }; + + let snapshot = collect_mcp_server_status_snapshot_with_detail( + &mcp_config, + auth.as_ref(), + request_id.request_id.to_string(), + runtime_environment, + detail, + ) + .await; + + let effective_servers = effective_mcp_servers(&mcp_config, auth.as_ref()); + let McpServerStatusSnapshot { + tools_by_server, + resources, + resource_templates, + auth_statuses, + } = snapshot; + + let mut server_names: Vec = config + .mcp_servers + .keys() + .cloned() + // Include built-in/plugin MCP servers that are present in the + // effective runtime config even when they are not user-declared in + // `config.mcp_servers`. + .chain(effective_servers.keys().cloned()) + .chain(auth_statuses.keys().cloned()) + .chain(resources.keys().cloned()) + .chain(resource_templates.keys().cloned()) + .collect(); + server_names.sort(); + server_names.dedup(); + + let total = server_names.len(); + let limit = params.limit.unwrap_or(total as u32).max(1) as usize; + let effective_limit = limit.min(total); + let start = match params.cursor { + Some(cursor) => match cursor.parse::() { + Ok(idx) => idx, + Err(_) => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("invalid cursor: {cursor}"), + data: None, + }; + outgoing.send_error(request_id, error).await; + return; + } + }, + None => 0, + }; + + if start > total { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("cursor {start} exceeds total MCP servers {total}"), + data: None, + }; + outgoing.send_error(request_id, error).await; + return; + } + + let end = start.saturating_add(effective_limit).min(total); + + let data: Vec = server_names[start..end] + .iter() + .map(|name| McpServerStatus { + name: name.clone(), + tools: tools_by_server.get(name).cloned().unwrap_or_default(), + resources: resources.get(name).cloned().unwrap_or_default(), + resource_templates: resource_templates.get(name).cloned().unwrap_or_default(), + auth_status: auth_statuses + .get(name) + .cloned() + .unwrap_or(CoreMcpAuthStatus::Unsupported) + .into(), + }) + .collect(); + + let next_cursor = if end < total { + Some(end.to_string()) + } else { + None + }; + + let response = ListMcpServerStatusResponse { data, next_cursor }; + + outgoing.send_response(request_id, response).await; + } + async fn read_mcp_resource( &self, request_id: ConnectionRequestId, diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index 5fb634d906..766465f052 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -16,18 +16,15 @@ pub use mcp::ToolPluginProvenance; pub use mcp::canonical_mcp_server_key; pub use mcp::collect_mcp_server_status_snapshot; pub use mcp::collect_mcp_server_status_snapshot_with_detail; -pub use mcp::collect_mcp_server_status_snapshot_with_detail_and_authorization_header; pub use mcp::collect_mcp_snapshot; pub use mcp::collect_mcp_snapshot_from_manager; pub use mcp::collect_mcp_snapshot_from_manager_with_detail; pub use mcp::collect_mcp_snapshot_with_detail; -pub use mcp::collect_mcp_snapshot_with_detail_and_authorization_header; pub use mcp::collect_missing_mcp_dependencies; pub use mcp::compute_auth_statuses; pub use mcp::configured_mcp_servers; pub use mcp::discover_supported_scopes; pub use mcp::effective_mcp_servers; -pub use mcp::effective_mcp_servers_with_authorization_header; pub use mcp::group_tools_by_server; pub use mcp::mcp_permission_prompt_is_auto_approved; pub use mcp::oauth_login_support; @@ -37,7 +34,6 @@ pub use mcp::should_retry_without_scopes; pub use mcp::split_qualified_tool_name; pub use mcp::tool_plugin_provenance; pub use mcp::with_codex_apps_mcp; -pub use mcp::with_codex_apps_mcp_with_authorization_header; pub use mcp_connection_manager::CodexAppsToolsCacheKey; pub use mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT; pub use mcp_connection_manager::MCP_SANDBOX_STATE_META_CAPABILITY; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 6a8d9c0a97..448259d5b7 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -213,25 +213,14 @@ fn codex_apps_mcp_bearer_token(auth: Option<&CodexAuth>) -> Option { } } -fn codex_apps_mcp_http_headers( - auth: Option<&CodexAuth>, - authorization_header_value: Option<&str>, -) -> Option> { +fn codex_apps_mcp_http_headers(auth: Option<&CodexAuth>) -> Option> { let mut headers = HashMap::new(); - if let Some(authorization_header_value) = authorization_header_value { - headers.insert( - "Authorization".to_string(), - authorization_header_value.to_string(), - ); - } else if let Some(token) = codex_apps_mcp_bearer_token(auth) { + 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 auth.is_some_and(CodexAuth::is_fedramp_account) { - headers.insert("X-OpenAI-Fedramp".to_string(), "true".to_string()); - } if headers.is_empty() { None } else { @@ -265,16 +254,12 @@ 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>, - authorization_header_value: Option<&str>, -) -> McpServerConfig { +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, authorization_header_value) + codex_apps_mcp_http_headers(auth) }; let url = codex_apps_mcp_url(config); @@ -302,25 +287,14 @@ fn codex_apps_mcp_server_config( } pub fn with_codex_apps_mcp( - servers: HashMap, - auth: Option<&CodexAuth>, - config: &McpConfig, -) -> HashMap { - with_codex_apps_mcp_with_authorization_header( - servers, auth, config, /*authorization_header_value*/ None, - ) -} - -pub fn with_codex_apps_mcp_with_authorization_header( mut servers: HashMap, auth: Option<&CodexAuth>, config: &McpConfig, - authorization_header_value: Option<&str>, ) -> HashMap { if config.apps_enabled && auth.is_some_and(CodexAuth::is_chatgpt_auth) { servers.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), - codex_apps_mcp_server_config(config, auth, authorization_header_value), + codex_apps_mcp_server_config(config, auth), ); } else { servers.remove(CODEX_APPS_MCP_SERVER_NAME); @@ -335,19 +309,9 @@ pub fn configured_mcp_servers(config: &McpConfig) -> HashMap, -) -> HashMap { - effective_mcp_servers_with_authorization_header( - config, auth, /*authorization_header_value*/ None, - ) -} - -pub fn effective_mcp_servers_with_authorization_header( - config: &McpConfig, - auth: Option<&CodexAuth>, - authorization_header_value: Option<&str>, ) -> HashMap { let servers = configured_mcp_servers(config); - with_codex_apps_mcp_with_authorization_header(servers, auth, config, authorization_header_value) + with_codex_apps_mcp(servers, auth, config) } pub fn tool_plugin_provenance(config: &McpConfig) -> ToolPluginProvenance { @@ -377,27 +341,7 @@ pub async fn collect_mcp_snapshot_with_detail( runtime_environment: McpRuntimeEnvironment, detail: McpSnapshotDetail, ) -> McpListToolsResponseEvent { - collect_mcp_snapshot_with_detail_and_authorization_header( - config, - auth, - submit_id, - runtime_environment, - detail, - /*authorization_header_value*/ None, - ) - .await -} - -pub async fn collect_mcp_snapshot_with_detail_and_authorization_header( - config: &McpConfig, - auth: Option<&CodexAuth>, - submit_id: String, - runtime_environment: McpRuntimeEnvironment, - detail: McpSnapshotDetail, - authorization_header_value: Option<&str>, -) -> McpListToolsResponseEvent { - let mcp_servers = - effective_mcp_servers_with_authorization_header(config, auth, authorization_header_value); + let mcp_servers = effective_mcp_servers(config, auth); let tool_plugin_provenance = tool_plugin_provenance(config); if mcp_servers.is_empty() { return McpListToolsResponseEvent { @@ -472,27 +416,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( runtime_environment: McpRuntimeEnvironment, detail: McpSnapshotDetail, ) -> McpServerStatusSnapshot { - collect_mcp_server_status_snapshot_with_detail_and_authorization_header( - config, - auth, - submit_id, - runtime_environment, - detail, - /*authorization_header_value*/ None, - ) - .await -} - -pub async fn collect_mcp_server_status_snapshot_with_detail_and_authorization_header( - config: &McpConfig, - auth: Option<&CodexAuth>, - submit_id: String, - runtime_environment: McpRuntimeEnvironment, - detail: McpSnapshotDetail, - authorization_header_value: Option<&str>, -) -> McpServerStatusSnapshot { - let mcp_servers = - effective_mcp_servers_with_authorization_header(config, auth, authorization_header_value); + let mcp_servers = effective_mcp_servers(config, auth); let tool_plugin_provenance = tool_plugin_provenance(config); if mcp_servers.is_empty() { return McpServerStatusSnapshot { diff --git a/codex-rs/core-skills/src/remote.rs b/codex-rs/core-skills/src/remote.rs index c85c78ff8d..2dc620b864 100644 --- a/codex-rs/core-skills/src/remote.rs +++ b/codex-rs/core-skills/src/remote.rs @@ -6,9 +6,7 @@ use std::path::Path; use std::path::PathBuf; use std::time::Duration; -use codex_login::BackgroundAgentTaskAuthMode; use codex_login::CodexAuth; -use codex_login::cached_background_agent_task_authorization_header_value; use codex_login::default_client::build_reqwest_client; const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30); @@ -114,15 +112,13 @@ pub async fn list_remote_skills( .get(&url) .timeout(REMOTE_SKILLS_API_TIMEOUT) .query(&query_params); - let authorization_header_value = authorization_header_value_for_auth(auth) + let token = auth + .get_token() .context("Failed to read auth token for remote skills")?; - request = request.header("authorization", authorization_header_value); + request = request.bearer_auth(token); if let Some(account_id) = auth.get_account_id() { request = request.header("chatgpt-account-id", account_id); } - if auth.is_fedramp_account() { - request = request.header("X-OpenAI-Fedramp", "true"); - } let response = request .send() .await @@ -161,15 +157,13 @@ pub async fn export_remote_skill( let url = format!("{base_url}/hazelnuts/{skill_id}/export"); let mut request = client.get(&url).timeout(REMOTE_SKILLS_API_TIMEOUT); - let authorization_header_value = authorization_header_value_for_auth(auth) + let token = auth + .get_token() .context("Failed to read auth token for remote skills")?; - request = request.header("authorization", authorization_header_value); + request = request.bearer_auth(token); if let Some(account_id) = auth.get_account_id() { request = request.header("chatgpt-account-id", account_id); } - if auth.is_fedramp_account() { - request = request.header("X-OpenAI-Fedramp", "true"); - } let response = request .send() @@ -207,19 +201,6 @@ pub async fn export_remote_skill( }) } -fn authorization_header_value_for_auth(auth: &CodexAuth) -> std::io::Result { - if let Ok(Some(authorization_header_value)) = - cached_background_agent_task_authorization_header_value( - auth, - BackgroundAgentTaskAuthMode::Disabled, - ) - { - Ok(authorization_header_value) - } else { - auth.get_token().map(|token| format!("Bearer {token}")) - } -} - fn safe_join(base: &Path, name: &str) -> Result { let path = Path::new(name); for component in path.components() { diff --git a/codex-rs/core/src/agent_identity.rs b/codex-rs/core/src/agent_identity.rs index 3a4c6787f2..5bd15de76c 100644 --- a/codex-rs/core/src/agent_identity.rs +++ b/codex-rs/core/src/agent_identity.rs @@ -412,7 +412,6 @@ impl StoredAgentIdentity { agent_runtime_id: self.agent_runtime_id.clone(), agent_private_key: self.private_key_pkcs8_base64.clone(), registered_at: self.registered_at.clone(), - background_task_id: None, } } @@ -673,7 +672,6 @@ mod tests { agent_runtime_id: "agent_invalid".to_string(), agent_private_key: "not-valid-base64".to_string(), registered_at: "2026-01-01T00:00:00Z".to_string(), - background_task_id: None, }) .expect("seed invalid identity"); @@ -713,7 +711,6 @@ mod tests { agent_runtime_id: "agent_old".to_string(), agent_private_key: stale_key.private_key_pkcs8_base64, registered_at: "2026-01-01T00:00:00Z".to_string(), - background_task_id: None, }) .expect("seed stale identity"); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 2fef40a427..cd55e7e351 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -49,7 +49,7 @@ use codex_config::types::UriBasedFileOpener; use codex_config::types::WindowsSandboxModeToml; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::LOCAL_FS; -pub use codex_features::Feature; +use codex_features::Feature; use codex_features::FeatureConfigSource; use codex_features::FeatureOverrides; use codex_features::FeatureToml; @@ -58,7 +58,6 @@ use codex_features::FeaturesToml; use codex_features::MultiAgentV2ConfigToml; use codex_git_utils::resolve_root_git_project_for_trust; use codex_login::AuthManagerConfig; -use codex_login::BackgroundAgentTaskAuthMode; use codex_mcp::McpConfig; use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; use codex_model_provider_info::ModelProviderInfo; @@ -630,16 +629,6 @@ impl AuthManagerConfig for Config { fn forced_chatgpt_workspace_id(&self) -> Option { self.forced_chatgpt_workspace_id.clone() } - - fn chatgpt_base_url(&self) -> Option { - Some(self.chatgpt_base_url.clone()) - } - - fn background_agent_task_auth_mode(&self) -> BackgroundAgentTaskAuthMode { - BackgroundAgentTaskAuthMode::from_feature_enabled( - self.features.enabled(Feature::UseAgentIdentity), - ) - } } #[derive(Debug, Clone, Default)] diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 965521ae3d..933ce0ac76 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -43,7 +43,7 @@ use codex_mcp::ToolInfo; use codex_mcp::ToolPluginProvenance; use codex_mcp::codex_apps_tools_cache_key; use codex_mcp::compute_auth_statuses; -use codex_mcp::with_codex_apps_mcp_with_authorization_header; +use codex_mcp::with_codex_apps_mcp; const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); @@ -220,20 +220,8 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( }); } - let background_authorization_header_value = if let Some(auth) = auth.as_ref() { - auth_manager - .chatgpt_authorization_header_for_auth(auth) - .await - } else { - None - }; let mcp_config = config.to_mcp_config(plugins_manager.as_ref()).await; - let mcp_servers = with_codex_apps_mcp_with_authorization_header( - HashMap::new(), - auth.as_ref(), - &mcp_config, - background_authorization_header_value.as_deref(), - ); + let mcp_servers = with_codex_apps_mcp(HashMap::new(), auth.as_ref(), &mcp_config); if mcp_servers.is_empty() { return Ok(AccessibleConnectorsStatus { connectors: Vec::new(), @@ -435,18 +423,6 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( }; let access_token = token_data.access_token.clone(); let account_id = account_id.to_string(); - let is_fedramp_account = token_data.id_token.is_fedramp_account(); - let authorization_header_value = { - let auth_manager = - AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); - match auth { - Some(auth) if auth.is_chatgpt_auth() => auth_manager - .chatgpt_authorization_header_for_auth(auth) - .await - .unwrap_or_else(|| format!("Bearer {access_token}")), - _ => format!("Bearer {access_token}"), - } - }; let is_workspace_account = token_data.id_token.is_workspace_account(); let cache_key = AllConnectorsCacheKey::new( config.chatgpt_base_url.clone(), @@ -460,15 +436,14 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( is_workspace_account, /*force_refetch*/ false, |path| { - let authorization_header_value = authorization_header_value.clone(); + let access_token = access_token.clone(); let account_id = account_id.clone(); async move { - chatgpt_get_request_with_authorization_header::( + chatgpt_get_request_with_token::( config, path, - authorization_header_value.as_str(), + access_token.as_str(), account_id.as_str(), - is_fedramp_account, ) .await } @@ -477,25 +452,23 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( .await } -async fn chatgpt_get_request_with_authorization_header( +async fn chatgpt_get_request_with_token( config: &Config, path: String, - authorization_header_value: &str, + access_token: &str, account_id: &str, - is_fedramp_account: bool, ) -> anyhow::Result { let client = create_client(); let url = format!("{}{}", config.chatgpt_base_url, path); - let mut request = client + let response = client .get(&url) - .header("authorization", authorization_header_value) + .bearer_auth(access_token) .header("chatgpt-account-id", account_id) .header("Content-Type", "application/json") - .timeout(DIRECTORY_CONNECTORS_TIMEOUT); - if is_fedramp_account { - request = request.header("X-OpenAI-Fedramp", "true"); - } - let response = request.send().await.context("failed to send request")?; + .timeout(DIRECTORY_CONNECTORS_TIMEOUT) + .send() + .await + .context("failed to send request")?; if response.status().is_success() { response diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 715a1300d8..0d4c26991d 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -7,7 +7,7 @@ use codex_config::McpServerConfig; use codex_login::CodexAuth; use codex_mcp::ToolPluginProvenance; use codex_mcp::configured_mcp_servers; -use codex_mcp::effective_mcp_servers_with_authorization_header; +use codex_mcp::effective_mcp_servers; use codex_mcp::tool_plugin_provenance as collect_tool_plugin_provenance; #[derive(Clone)] @@ -29,25 +29,9 @@ impl McpManager { &self, config: &Config, auth: Option<&CodexAuth>, - ) -> HashMap { - self.effective_servers_with_authorization_header( - config, auth, /*authorization_header_value*/ None, - ) - .await - } - - pub async fn effective_servers_with_authorization_header( - &self, - config: &Config, - auth: Option<&CodexAuth>, - authorization_header_value: Option<&str>, ) -> HashMap { let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await; - effective_mcp_servers_with_authorization_header( - &mcp_config, - auth, - authorization_header_value, - ) + effective_mcp_servers(&mcp_config, auth) } pub async fn tool_plugin_provenance(&self, config: &Config) -> ToolPluginProvenance { diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index b45b6fc10e..72cfc57822 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -471,22 +471,10 @@ pub async fn reload_user_config(sess: &Arc) { pub async fn list_mcp_tools(sess: &Session, config: &Arc, sub_id: String) { let mcp_connection_manager = sess.services.mcp_connection_manager.read().await; let auth = sess.services.auth_manager.auth().await; - let background_authorization_header_value = if let Some(auth) = auth.as_ref() { - sess.services - .auth_manager - .chatgpt_authorization_header_for_auth(auth) - .await - } else { - None - }; let mcp_servers = sess .services .mcp_manager - .effective_servers_with_authorization_header( - config, - auth.as_ref(), - background_authorization_header_value.as_deref(), - ) + .effective_servers(config, auth.as_ref()) .await; let snapshot = collect_mcp_snapshot_from_manager( &mcp_connection_manager, diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index d15ca2cae8..8b7935a50c 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -190,20 +190,7 @@ impl Session { .mcp_manager .tool_plugin_provenance(config.as_ref()) .await; - let background_authorization_header_value = if let Some(auth) = auth.as_ref() { - self.services - .auth_manager - .chatgpt_authorization_header_for_auth(auth) - .await - } else { - None - }; - let mcp_servers = with_codex_apps_mcp_with_authorization_header( - mcp_servers, - auth.as_ref(), - &mcp_config, - background_authorization_header_value.as_deref(), - ); + 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 mut guard = self.services.mcp_startup_cancellation_token.lock().await; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index f159546575..e90b9f831b 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -293,7 +293,7 @@ use crate::unified_exec::UnifiedExecProcessManager; use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_git_utils::get_git_repo_root; use codex_mcp::compute_auth_statuses; -use codex_mcp::with_codex_apps_mcp_with_authorization_header; +use codex_mcp::with_codex_apps_mcp; use codex_otel::SessionTelemetry; use codex_otel::THREAD_STARTED_METRIC; use codex_otel::TelemetryAuthMode; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index a2080bbeab..7b42e37d40 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -333,20 +333,8 @@ impl Session { let mcp_manager_for_mcp = Arc::clone(&mcp_manager); let auth_and_mcp_fut = async move { let auth = auth_manager_clone.auth().await; - let authorization_header_value = match auth.as_ref() { - Some(auth) => { - auth_manager_clone - .chatgpt_authorization_header_for_auth(auth) - .await - } - None => None, - }; let mcp_servers = mcp_manager_for_mcp - .effective_servers_with_authorization_header( - &config_for_mcp, - auth.as_ref(), - authorization_header_value.as_deref(), - ) + .effective_servers(&config_for_mcp, auth.as_ref()) .await; let auth_statuses = compute_auth_statuses( mcp_servers.iter(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index bd41da9f20..d5a3cbd37a 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -4468,7 +4468,6 @@ fn seed_stored_identity( agent_runtime_id: stored_identity.agent_runtime_id.clone(), agent_private_key: stored_identity.private_key_pkcs8_base64.clone(), registered_at: stored_identity.registered_at.clone(), - background_task_id: None, }) .expect("store identity"); diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index c56c891e1d..16d1836024 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -21,7 +21,6 @@ codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-terminal-detection = { workspace = true } codex-utils-template = { workspace = true } -crypto_box = { workspace = true } ed25519-dalek = { workspace = true } once_cell = { workspace = true } os_info = { workspace = true } diff --git a/codex-rs/login/src/agent_identity.rs b/codex-rs/login/src/agent_identity.rs deleted file mode 100644 index 864c45ad0f..0000000000 --- a/codex-rs/login/src/agent_identity.rs +++ /dev/null @@ -1,834 +0,0 @@ -use std::collections::BTreeMap; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context; -use anyhow::Result; -use base64::Engine as _; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use chrono::SecondsFormat; -use chrono::Utc; -use codex_protocol::protocol::SessionSource; -use crypto_box::SecretKey as Curve25519SecretKey; -use ed25519_dalek::Signer as _; -use ed25519_dalek::SigningKey; -use ed25519_dalek::VerifyingKey; -use ed25519_dalek::pkcs8::DecodePrivateKey; -use ed25519_dalek::pkcs8::EncodePrivateKey; -use rand::TryRngCore; -use rand::rngs::OsRng; -use serde::Deserialize; -use serde::Serialize; -use sha2::Digest as _; -use sha2::Sha512; -use tokio::sync::Semaphore; -use tracing::debug; -use tracing::info; -use tracing::warn; - -use crate::AgentIdentityAuthRecord; -use crate::AuthManager; -use crate::CodexAuth; -use crate::default_client::create_client; - -const AGENT_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15); -const AGENT_TASK_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15); -const AGENT_IDENTITY_BISCUIT_TIMEOUT: Duration = Duration::from_secs(15); - -#[derive(Clone)] -pub(crate) struct BackgroundAgentTaskManager { - auth_manager: Arc, - chatgpt_base_url: String, - auth_mode: BackgroundAgentTaskAuthMode, - abom: AgentBillOfMaterials, - ensure_lock: Arc, -} - -impl std::fmt::Debug for BackgroundAgentTaskManager { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BackgroundAgentTaskManager") - .field("chatgpt_base_url", &self.chatgpt_base_url) - .field("auth_mode", &self.auth_mode) - .field("abom", &self.abom) - .finish_non_exhaustive() - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum BackgroundAgentTaskAuthMode { - Enabled, - #[default] - Disabled, -} - -impl BackgroundAgentTaskAuthMode { - pub fn from_feature_enabled(enabled: bool) -> Self { - if enabled { - Self::Enabled - } else { - Self::Disabled - } - } - - fn is_enabled(self) -> bool { - matches!(self, Self::Enabled) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -struct StoredAgentIdentity { - binding_id: String, - chatgpt_account_id: String, - chatgpt_user_id: Option, - agent_runtime_id: String, - private_key_pkcs8_base64: String, - public_key_ssh: String, - registered_at: String, - background_task_id: Option, - abom: AgentBillOfMaterials, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -struct AgentBillOfMaterials { - agent_version: String, - agent_harness_id: String, - running_location: String, -} - -#[derive(Debug, Serialize)] -struct RegisterAgentRequest { - abom: AgentBillOfMaterials, - agent_public_key: String, - capabilities: Vec, -} - -#[derive(Debug, Deserialize)] -struct RegisterAgentResponse { - agent_runtime_id: String, -} - -#[derive(Debug, Serialize)] -struct RegisterTaskRequest { - signature: String, - timestamp: String, -} - -#[derive(Debug, Deserialize)] -struct RegisterTaskResponse { - encrypted_task_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct AgentIdentityBinding { - binding_id: String, - chatgpt_account_id: String, - chatgpt_user_id: Option, - access_token: String, -} - -struct GeneratedAgentKeyMaterial { - private_key_pkcs8_base64: String, - public_key_ssh: String, -} - -impl BackgroundAgentTaskManager { - #[cfg(test)] - pub(crate) fn new( - auth_manager: Arc, - chatgpt_base_url: String, - session_source: SessionSource, - ) -> Self { - Self::new_with_auth_mode( - auth_manager, - chatgpt_base_url, - session_source, - BackgroundAgentTaskAuthMode::Disabled, - ) - } - - pub(crate) fn new_with_auth_mode( - auth_manager: Arc, - chatgpt_base_url: String, - session_source: SessionSource, - auth_mode: BackgroundAgentTaskAuthMode, - ) -> Self { - Self { - auth_manager, - chatgpt_base_url: normalize_chatgpt_base_url(&chatgpt_base_url), - auth_mode, - abom: build_abom(session_source), - ensure_lock: Arc::new(Semaphore::new(/*permits*/ 1)), - } - } - - pub(crate) async fn authorization_header_value_for_auth( - &self, - auth: &CodexAuth, - ) -> Result> { - if !self.auth_mode.is_enabled() { - debug!("skipping background agent task auth because agent identity is disabled"); - return Ok(None); - } - - if !supports_background_agent_task_auth(&self.chatgpt_base_url) { - debug!( - chatgpt_base_url = %self.chatgpt_base_url, - "skipping background agent task auth for unsupported backend host" - ); - return Ok(None); - } - - let Some(binding) = - AgentIdentityBinding::from_auth(auth, self.auth_manager.forced_chatgpt_workspace_id()) - else { - debug!("skipping background agent task auth because ChatGPT auth is unavailable"); - return Ok(None); - }; - - let _guard = self - .ensure_lock - .acquire() - .await - .context("background agent task ensure semaphore closed")?; - let mut stored_identity = self - .ensure_registered_identity_for_binding(auth, &binding) - .await?; - let background_task_id = match stored_identity.background_task_id.clone() { - Some(background_task_id) => background_task_id, - _ => { - let background_task_id = self - .register_background_task_for_identity(&binding, &stored_identity) - .await?; - stored_identity.background_task_id = Some(background_task_id.clone()); - self.store_identity(auth, &stored_identity)?; - background_task_id - } - }; - - Ok(Some(authorization_header_for_task( - &stored_identity, - &background_task_id, - )?)) - } - - pub(crate) async fn authorization_header_value_or_bearer( - &self, - auth: &CodexAuth, - ) -> Option { - match self.authorization_header_value_for_auth(auth).await { - Ok(Some(authorization_header_value)) => Some(authorization_header_value), - Ok(None) => auth - .get_token() - .ok() - .filter(|token| !token.is_empty()) - .map(|token| format!("Bearer {token}")), - Err(error) => { - warn!( - error = %error, - "falling back to bearer authorization because background agent task auth failed" - ); - auth.get_token() - .ok() - .filter(|token| !token.is_empty()) - .map(|token| format!("Bearer {token}")) - } - } - } - - async fn ensure_registered_identity_for_binding( - &self, - auth: &CodexAuth, - binding: &AgentIdentityBinding, - ) -> Result { - if let Some(stored_identity) = self.load_stored_identity(auth, binding)? { - return Ok(stored_identity); - } - - let stored_identity = self.register_agent_identity(binding).await?; - self.store_identity(auth, &stored_identity)?; - Ok(stored_identity) - } - - async fn register_agent_identity( - &self, - binding: &AgentIdentityBinding, - ) -> Result { - let key_material = generate_agent_key_material()?; - let request_body = RegisterAgentRequest { - abom: self.abom.clone(), - agent_public_key: key_material.public_key_ssh.clone(), - capabilities: Vec::new(), - }; - - let url = agent_registration_url(&self.chatgpt_base_url); - let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?; - let client = create_client(); - let response = client - .post(&url) - .header("X-OpenAI-Authorization", human_biscuit) - .json(&request_body) - .timeout(AGENT_REGISTRATION_TIMEOUT) - .send() - .await - .with_context(|| { - format!("failed to send agent identity registration request to {url}") - })?; - - if response.status().is_success() { - let response_body = response - .json::() - .await - .with_context(|| format!("failed to parse agent identity response from {url}"))?; - let stored_identity = StoredAgentIdentity { - binding_id: binding.binding_id.clone(), - chatgpt_account_id: binding.chatgpt_account_id.clone(), - chatgpt_user_id: binding.chatgpt_user_id.clone(), - agent_runtime_id: response_body.agent_runtime_id, - private_key_pkcs8_base64: key_material.private_key_pkcs8_base64, - public_key_ssh: key_material.public_key_ssh, - registered_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true), - background_task_id: None, - abom: self.abom.clone(), - }; - info!( - agent_runtime_id = %stored_identity.agent_runtime_id, - binding_id = %binding.binding_id, - "registered background agent identity" - ); - return Ok(stored_identity); - } - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - anyhow::bail!("agent identity registration failed with status {status} from {url}: {body}") - } - - async fn register_background_task_for_identity( - &self, - binding: &AgentIdentityBinding, - stored_identity: &StoredAgentIdentity, - ) -> Result { - let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); - let request_body = RegisterTaskRequest { - signature: sign_task_registration_payload(stored_identity, ×tamp)?, - timestamp, - }; - - let client = create_client(); - let url = - agent_task_registration_url(&self.chatgpt_base_url, &stored_identity.agent_runtime_id); - let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?; - let response = client - .post(&url) - .header("X-OpenAI-Authorization", human_biscuit) - .json(&request_body) - .timeout(AGENT_TASK_REGISTRATION_TIMEOUT) - .send() - .await - .with_context(|| format!("failed to send background agent task request to {url}"))?; - - if response.status().is_success() { - let response_body = response - .json::() - .await - .with_context(|| format!("failed to parse background task response from {url}"))?; - let background_task_id = - decrypt_task_id_response(stored_identity, &response_body.encrypted_task_id)?; - info!( - agent_runtime_id = %stored_identity.agent_runtime_id, - task_id = %background_task_id, - "registered background agent task" - ); - return Ok(background_task_id); - } - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - anyhow::bail!( - "background agent task registration failed with status {status} from {url}: {body}" - ) - } - - async fn mint_human_biscuit( - &self, - binding: &AgentIdentityBinding, - target_method: &str, - target_url: &str, - ) -> Result { - let url = agent_identity_biscuit_url(&self.chatgpt_base_url); - let request_id = agent_identity_request_id()?; - let client = create_client(); - let response = client - .get(&url) - .bearer_auth(&binding.access_token) - .header("X-Request-Id", request_id.clone()) - .header("X-Original-Method", target_method) - .header("X-Original-Url", target_url) - .timeout(AGENT_IDENTITY_BISCUIT_TIMEOUT) - .send() - .await - .with_context(|| format!("failed to send agent identity biscuit request to {url}"))?; - - if response.status().is_success() { - let human_biscuit = response - .headers() - .get("x-openai-authorization") - .context("agent identity biscuit response did not include x-openai-authorization")? - .to_str() - .context("agent identity biscuit response header was not valid UTF-8")? - .to_string(); - info!( - request_id = %request_id, - "minted human biscuit for background agent task" - ); - return Ok(human_biscuit); - } - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - anyhow::bail!( - "agent identity biscuit minting failed with status {status} from {url}: {body}" - ) - } - - fn load_stored_identity( - &self, - auth: &CodexAuth, - binding: &AgentIdentityBinding, - ) -> Result> { - let Some(record) = auth.get_agent_identity(&binding.chatgpt_account_id) else { - return Ok(None); - }; - - let stored_identity = - match StoredAgentIdentity::from_auth_record(binding, record, self.abom.clone()) { - Ok(stored_identity) => stored_identity, - Err(error) => { - warn!( - binding_id = %binding.binding_id, - error = %error, - "stored agent identity is invalid; deleting cached value" - ); - auth.remove_agent_identity()?; - return Ok(None); - } - }; - - if !stored_identity.matches_binding(binding) { - warn!( - binding_id = %binding.binding_id, - "stored agent identity binding no longer matches current auth; deleting cached value" - ); - auth.remove_agent_identity()?; - return Ok(None); - } - - if let Err(error) = stored_identity.validate_key_material() { - warn!( - agent_runtime_id = %stored_identity.agent_runtime_id, - binding_id = %binding.binding_id, - error = %error, - "stored agent identity key material is invalid; deleting cached value" - ); - auth.remove_agent_identity()?; - return Ok(None); - } - - Ok(Some(stored_identity)) - } - - fn store_identity( - &self, - auth: &CodexAuth, - stored_identity: &StoredAgentIdentity, - ) -> Result<()> { - auth.set_agent_identity(stored_identity.to_auth_record())?; - Ok(()) - } -} - -pub fn cached_background_agent_task_authorization_header_value( - auth: &CodexAuth, - auth_mode: BackgroundAgentTaskAuthMode, -) -> Result> { - if !auth_mode.is_enabled() { - return Ok(None); - } - - let Some(binding) = AgentIdentityBinding::from_auth(auth, /*forced_workspace_id*/ None) else { - return Ok(None); - }; - let Some(record) = auth.get_agent_identity(&binding.chatgpt_account_id) else { - return Ok(None); - }; - let stored_identity = - StoredAgentIdentity::from_auth_record(&binding, record, build_abom(SessionSource::Cli))?; - if !stored_identity.matches_binding(&binding) { - return Ok(None); - } - stored_identity.validate_key_material()?; - let Some(background_task_id) = stored_identity.background_task_id.as_ref() else { - return Ok(None); - }; - authorization_header_for_task(&stored_identity, background_task_id).map(Some) -} - -impl StoredAgentIdentity { - fn from_auth_record( - binding: &AgentIdentityBinding, - record: AgentIdentityAuthRecord, - abom: AgentBillOfMaterials, - ) -> Result { - if record.workspace_id != binding.chatgpt_account_id { - anyhow::bail!( - "stored agent identity workspace {:?} does not match current workspace {:?}", - record.workspace_id, - binding.chatgpt_account_id - ); - } - let signing_key = signing_key_from_private_key_pkcs8_base64(&record.agent_private_key)?; - Ok(Self { - binding_id: binding.binding_id.clone(), - chatgpt_account_id: binding.chatgpt_account_id.clone(), - chatgpt_user_id: record.chatgpt_user_id, - agent_runtime_id: record.agent_runtime_id.clone(), - private_key_pkcs8_base64: record.agent_private_key, - public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()), - registered_at: record.registered_at, - background_task_id: record.background_task_id, - abom, - }) - } - - fn to_auth_record(&self) -> AgentIdentityAuthRecord { - AgentIdentityAuthRecord { - workspace_id: self.chatgpt_account_id.clone(), - chatgpt_user_id: self.chatgpt_user_id.clone(), - agent_runtime_id: self.agent_runtime_id.clone(), - agent_private_key: self.private_key_pkcs8_base64.clone(), - registered_at: self.registered_at.clone(), - background_task_id: self.background_task_id.clone(), - } - } - - fn matches_binding(&self, binding: &AgentIdentityBinding) -> bool { - binding.matches_parts( - &self.binding_id, - &self.chatgpt_account_id, - self.chatgpt_user_id.as_deref(), - ) - } - - fn validate_key_material(&self) -> Result<()> { - let signing_key = self.signing_key()?; - let derived_public_key = encode_ssh_ed25519_public_key(&signing_key.verifying_key()); - anyhow::ensure!( - self.public_key_ssh == derived_public_key, - "stored public key does not match the private key" - ); - Ok(()) - } - - fn signing_key(&self) -> Result { - signing_key_from_private_key_pkcs8_base64(&self.private_key_pkcs8_base64) - } -} - -impl AgentIdentityBinding { - fn matches_parts( - &self, - binding_id: &str, - chatgpt_account_id: &str, - chatgpt_user_id: Option<&str>, - ) -> bool { - binding_id == self.binding_id - && chatgpt_account_id == self.chatgpt_account_id - && match self.chatgpt_user_id.as_deref() { - Some(expected_user_id) => chatgpt_user_id == Some(expected_user_id), - None => true, - } - } - - fn from_auth(auth: &CodexAuth, forced_workspace_id: Option) -> Option { - if !auth.is_chatgpt_auth() { - return None; - } - - let token_data = auth.get_token_data().ok()?; - let resolved_account_id = - forced_workspace_id - .filter(|value| !value.is_empty()) - .or(token_data - .account_id - .clone() - .filter(|value| !value.is_empty()))?; - - Some(Self { - binding_id: format!("chatgpt-account-{resolved_account_id}"), - chatgpt_account_id: resolved_account_id, - chatgpt_user_id: token_data - .id_token - .chatgpt_user_id - .filter(|value| !value.is_empty()), - access_token: token_data.access_token, - }) - } -} - -fn authorization_header_for_task( - stored_identity: &StoredAgentIdentity, - background_task_id: &str, -) -> Result { - let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); - let signature = sign_agent_assertion_payload(stored_identity, background_task_id, ×tamp)?; - let payload = serde_json::to_vec(&BTreeMap::from([ - ( - "agent_runtime_id", - stored_identity.agent_runtime_id.as_str(), - ), - ("signature", signature.as_str()), - ("task_id", background_task_id), - ("timestamp", timestamp.as_str()), - ])) - .context("failed to serialize agent assertion envelope")?; - Ok(format!( - "AgentAssertion {}", - URL_SAFE_NO_PAD.encode(payload) - )) -} - -fn sign_agent_assertion_payload( - stored_identity: &StoredAgentIdentity, - background_task_id: &str, - timestamp: &str, -) -> Result { - let signing_key = stored_identity.signing_key()?; - let payload = format!( - "{}:{background_task_id}:{timestamp}", - stored_identity.agent_runtime_id - ); - Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes())) -} - -fn sign_task_registration_payload( - stored_identity: &StoredAgentIdentity, - timestamp: &str, -) -> Result { - let signing_key = stored_identity.signing_key()?; - let payload = format!("{}:{timestamp}", stored_identity.agent_runtime_id); - Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes())) -} - -fn decrypt_task_id_response( - stored_identity: &StoredAgentIdentity, - encrypted_task_id: &str, -) -> Result { - let signing_key = stored_identity.signing_key()?; - let ciphertext = BASE64_STANDARD - .decode(encrypted_task_id) - .context("encrypted task id is not valid base64")?; - let plaintext = curve25519_secret_key_from_signing_key(&signing_key) - .unseal(&ciphertext) - .map_err(|_| anyhow::anyhow!("failed to decrypt encrypted task id"))?; - String::from_utf8(plaintext).context("decrypted task id is not valid UTF-8") -} - -fn curve25519_secret_key_from_signing_key(signing_key: &SigningKey) -> Curve25519SecretKey { - let digest = Sha512::digest(signing_key.to_bytes()); - let mut secret_key = [0u8; 32]; - secret_key.copy_from_slice(&digest[..32]); - secret_key[0] &= 248; - secret_key[31] &= 127; - secret_key[31] |= 64; - Curve25519SecretKey::from(secret_key) -} - -fn build_abom(session_source: SessionSource) -> AgentBillOfMaterials { - AgentBillOfMaterials { - agent_version: env!("CARGO_PKG_VERSION").to_string(), - agent_harness_id: match &session_source { - SessionSource::VSCode => "codex-app".to_string(), - SessionSource::Cli - | SessionSource::Exec - | SessionSource::Mcp - | SessionSource::Custom(_) - | SessionSource::SubAgent(_) - | SessionSource::Unknown => "codex-cli".to_string(), - }, - running_location: format!("{}-{}", session_source, std::env::consts::OS), - } -} - -fn generate_agent_key_material() -> Result { - let mut secret_key_bytes = [0u8; 32]; - OsRng - .try_fill_bytes(&mut secret_key_bytes) - .context("failed to generate agent identity private key bytes")?; - let signing_key = SigningKey::from_bytes(&secret_key_bytes); - let private_key_pkcs8 = signing_key - .to_pkcs8_der() - .context("failed to encode agent identity private key as PKCS#8")?; - - Ok(GeneratedAgentKeyMaterial { - private_key_pkcs8_base64: BASE64_STANDARD.encode(private_key_pkcs8.as_bytes()), - public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()), - }) -} - -fn encode_ssh_ed25519_public_key(verifying_key: &VerifyingKey) -> String { - let mut blob = Vec::with_capacity(4 + 11 + 4 + 32); - append_ssh_string(&mut blob, b"ssh-ed25519"); - append_ssh_string(&mut blob, verifying_key.as_bytes()); - format!("ssh-ed25519 {}", BASE64_STANDARD.encode(blob)) -} - -fn append_ssh_string(buf: &mut Vec, value: &[u8]) { - buf.extend_from_slice(&(value.len() as u32).to_be_bytes()); - buf.extend_from_slice(value); -} - -fn signing_key_from_private_key_pkcs8_base64(private_key_pkcs8_base64: &str) -> Result { - let private_key = BASE64_STANDARD - .decode(private_key_pkcs8_base64) - .context("stored agent identity private key is not valid base64")?; - SigningKey::from_pkcs8_der(&private_key) - .context("stored agent identity private key is not valid PKCS#8") -} - -fn agent_registration_url(chatgpt_base_url: &str) -> String { - let trimmed = chatgpt_base_url.trim_end_matches('/'); - format!("{trimmed}/v1/agent/register") -} - -fn agent_task_registration_url(chatgpt_base_url: &str, agent_runtime_id: &str) -> String { - let trimmed = chatgpt_base_url.trim_end_matches('/'); - format!("{trimmed}/v1/agent/{agent_runtime_id}/task/register") -} - -fn agent_identity_biscuit_url(chatgpt_base_url: &str) -> String { - let trimmed = chatgpt_base_url.trim_end_matches('/'); - format!("{trimmed}/authenticate_app_v2") -} - -fn agent_identity_request_id() -> Result { - let mut request_id_bytes = [0u8; 16]; - OsRng - .try_fill_bytes(&mut request_id_bytes) - .context("failed to generate agent identity request id")?; - Ok(format!( - "codex-agent-identity-{}", - URL_SAFE_NO_PAD.encode(request_id_bytes) - )) -} - -fn normalize_chatgpt_base_url(chatgpt_base_url: &str) -> String { - let mut base_url = chatgpt_base_url.trim_end_matches('/').to_string(); - for suffix in [ - "/wham/remote/control/server/enroll", - "/wham/remote/control/server", - ] { - if let Some(stripped) = base_url.strip_suffix(suffix) { - base_url = stripped.to_string(); - break; - } - } - if (base_url.starts_with("https://chatgpt.com") - || base_url.starts_with("https://chat.openai.com")) - && !base_url.contains("/backend-api") - { - base_url = format!("{base_url}/backend-api"); - } - if let Some(stripped) = base_url.strip_suffix("/codex") { - stripped.to_string() - } else { - base_url - } -} - -fn supports_background_agent_task_auth(chatgpt_base_url: &str) -> bool { - let Ok(url) = url::Url::parse(chatgpt_base_url) else { - return false; - }; - let Some(host) = url.host_str() else { - return false; - }; - host == "chatgpt.com" - || host == "chat.openai.com" - || host == "chatgpt-staging.com" - || host.ends_with(".chatgpt.com") - || host.ends_with(".chatgpt-staging.com") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn disabled_background_agent_task_auth_returns_none_for_supported_host() { - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let auth_manager = AuthManager::from_auth_for_testing(auth.clone()); - let manager = BackgroundAgentTaskManager::new_with_auth_mode( - auth_manager, - "https://chatgpt.com/backend-api".to_string(), - SessionSource::Cli, - BackgroundAgentTaskAuthMode::Disabled, - ); - - let authorization_header_value = manager - .authorization_header_value_for_auth(&auth) - .await - .expect("disabled manager should not fail"); - - assert_eq!(None, authorization_header_value); - } - - #[tokio::test] - async fn default_background_agent_task_auth_returns_none_for_supported_host() { - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let auth_manager = AuthManager::from_auth_for_testing(auth.clone()); - let manager = BackgroundAgentTaskManager::new( - auth_manager, - "https://chatgpt.com/backend-api".to_string(), - SessionSource::Cli, - ); - - let authorization_header_value = manager - .authorization_header_value_for_auth(&auth) - .await - .expect("default manager should not fail"); - - assert_eq!(None, authorization_header_value); - } - - #[test] - fn cached_background_agent_task_auth_honors_disabled_mode() { - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let key_material = generate_agent_key_material().expect("generate key material"); - auth.set_agent_identity(AgentIdentityAuthRecord { - workspace_id: "account_id".to_string(), - chatgpt_user_id: None, - agent_runtime_id: "agent_123".to_string(), - agent_private_key: key_material.private_key_pkcs8_base64, - registered_at: "2026-04-13T12:00:00Z".to_string(), - background_task_id: Some("task_123".to_string()), - }) - .expect("set agent identity"); - - let disabled_authorization_header_value = - cached_background_agent_task_authorization_header_value( - &auth, - BackgroundAgentTaskAuthMode::Disabled, - ) - .expect("disabled cached auth should not fail"); - let enabled_authorization_header_value = - cached_background_agent_task_authorization_header_value( - &auth, - BackgroundAgentTaskAuthMode::Enabled, - ) - .expect("enabled cached auth should not fail"); - - assert_eq!(None, disabled_authorization_header_value); - assert!(enabled_authorization_header_value.is_some()); - } -} diff --git a/codex-rs/login/src/auth/agent_assertion.rs b/codex-rs/login/src/auth/agent_assertion.rs index ce24c57d08..e4a1731f65 100644 --- a/codex-rs/login/src/auth/agent_assertion.rs +++ b/codex-rs/login/src/auth/agent_assertion.rs @@ -167,7 +167,6 @@ mod tests { agent_runtime_id: agent_runtime_id.to_string(), agent_private_key: BASE64_STANDARD.encode(private_key.as_bytes()), registered_at: "2026-03-23T12:00:00Z".to_string(), - background_task_id: None, } } } diff --git a/codex-rs/login/src/auth/auth_tests.rs b/codex-rs/login/src/auth/auth_tests.rs index 326adea6e4..fed4973070 100644 --- a/codex-rs/login/src/auth/auth_tests.rs +++ b/codex-rs/login/src/auth/auth_tests.rs @@ -211,7 +211,6 @@ fn chatgpt_auth_persists_agent_identity_for_workspace() { agent_runtime_id: "agent_123".to_string(), agent_private_key: "pkcs8-base64".to_string(), registered_at: "2026-04-13T12:00:00Z".to_string(), - background_task_id: None, }; auth.set_agent_identity(record.clone()) diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 961202e2ff..7634488f45 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -15,7 +15,7 @@ use std::sync::Mutex; use std::sync::RwLock; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; -use tokio::sync::Semaphore; +use tokio::sync::Mutex as AsyncMutex; use tokio::sync::watch; use codex_app_server_protocol::AuthMode; @@ -43,12 +43,9 @@ use codex_protocol::auth::KnownPlan as InternalKnownPlan; use codex_protocol::auth::PlanType as InternalPlanType; use codex_protocol::auth::RefreshTokenFailedError; use codex_protocol::auth::RefreshTokenFailedReason; -use codex_protocol::protocol::SessionSource; use serde_json::Value; use thiserror::Error; -use crate::agent_identity::BackgroundAgentTaskAuthMode; -use crate::agent_identity::BackgroundAgentTaskManager; /// Authentication mechanism used by the current user. #[derive(Debug, Clone)] pub enum CodexAuth { @@ -378,7 +375,7 @@ impl CodexAuth { .filter(|identity| identity.workspace_id == workspace_id) } - pub fn set_agent_identity(&self, mut record: AgentIdentityAuthRecord) -> std::io::Result<()> { + pub fn set_agent_identity(&self, record: AgentIdentityAuthRecord) -> std::io::Result<()> { let (state, storage) = match self { Self::Chatgpt(auth) => (&auth.state, &auth.storage), Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage), @@ -391,13 +388,6 @@ impl CodexAuth { let mut auth = guard .clone() .ok_or_else(|| std::io::Error::other("auth data is not available"))?; - if record.background_task_id.is_none() - && let Some(existing) = auth.agent_identity.as_ref() - && existing.workspace_id == record.workspace_id - && existing.agent_runtime_id == record.agent_runtime_id - { - record.background_task_id = existing.background_task_id.clone(); - } auth.agent_identity = Some(record); storage.save(&auth)?; *guard = Some(auth); @@ -1198,9 +1188,7 @@ pub struct AuthManager { enable_codex_api_key_env: bool, auth_credentials_store_mode: AuthCredentialsStoreMode, forced_chatgpt_workspace_id: RwLock>, - chatgpt_base_url: RwLock>, - background_agent_task_auth_mode: RwLock, - refresh_lock: Semaphore, + refresh_lock: AsyncMutex<()>, external_auth: RwLock>>, auth_state_tx: watch::Sender<()>, } @@ -1220,16 +1208,6 @@ pub trait AuthManagerConfig { /// Returns the workspace ID that ChatGPT auth should be restricted to, if any. fn forced_chatgpt_workspace_id(&self) -> Option; - - /// Returns the ChatGPT backend base URL used for first-party backend authorization. - fn chatgpt_base_url(&self) -> Option { - None - } - - /// Returns whether default ChatGPT backend authorization may use background AgentAssertion. - fn background_agent_task_auth_mode(&self) -> BackgroundAgentTaskAuthMode { - BackgroundAgentTaskAuthMode::Disabled - } } impl Debug for AuthManager { @@ -1246,11 +1224,6 @@ impl Debug for AuthManager { "forced_chatgpt_workspace_id", &self.forced_chatgpt_workspace_id, ) - .field("chatgpt_base_url", &self.chatgpt_base_url) - .field( - "background_agent_task_auth_mode", - &self.background_agent_task_auth_mode, - ) .field("has_external_auth", &self.has_external_auth()) .finish_non_exhaustive() } @@ -1283,9 +1256,7 @@ impl AuthManager { enable_codex_api_key_env, auth_credentials_store_mode, forced_chatgpt_workspace_id: RwLock::new(None), - chatgpt_base_url: RwLock::new(None), - background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled), - refresh_lock: Semaphore::new(/*permits*/ 1), + refresh_lock: AsyncMutex::new(()), external_auth: RwLock::new(None), auth_state_tx, } @@ -1305,9 +1276,7 @@ impl AuthManager { enable_codex_api_key_env: false, auth_credentials_store_mode: AuthCredentialsStoreMode::File, forced_chatgpt_workspace_id: RwLock::new(None), - chatgpt_base_url: RwLock::new(None), - background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled), - refresh_lock: Semaphore::new(/*permits*/ 1), + refresh_lock: AsyncMutex::new(()), external_auth: RwLock::new(None), auth_state_tx, }) @@ -1326,9 +1295,7 @@ impl AuthManager { enable_codex_api_key_env: false, auth_credentials_store_mode: AuthCredentialsStoreMode::File, forced_chatgpt_workspace_id: RwLock::new(None), - chatgpt_base_url: RwLock::new(None), - background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled), - refresh_lock: Semaphore::new(/*permits*/ 1), + refresh_lock: AsyncMutex::new(()), external_auth: RwLock::new(None), auth_state_tx, }) @@ -1345,9 +1312,7 @@ impl AuthManager { enable_codex_api_key_env: false, auth_credentials_store_mode: AuthCredentialsStoreMode::File, forced_chatgpt_workspace_id: RwLock::new(None), - chatgpt_base_url: RwLock::new(None), - background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled), - refresh_lock: Semaphore::new(/*permits*/ 1), + refresh_lock: AsyncMutex::new(()), external_auth: RwLock::new(Some( Arc::new(BearerTokenRefresher::new(config)) as Arc )), @@ -1565,85 +1530,6 @@ impl AuthManager { Ok(Some(record)) } - pub fn set_chatgpt_backend_auth_config( - &self, - chatgpt_base_url: Option, - background_agent_task_auth_mode: BackgroundAgentTaskAuthMode, - ) { - let mut changed = false; - if let Ok(mut guard) = self.chatgpt_base_url.write() - && *guard != chatgpt_base_url - { - *guard = chatgpt_base_url; - changed = true; - } - if let Ok(mut guard) = self.background_agent_task_auth_mode.write() - && *guard != background_agent_task_auth_mode - { - *guard = background_agent_task_auth_mode; - changed = true; - } - if changed { - self.auth_state_tx.send_replace(()); - } - } - - fn chatgpt_backend_auth_config(&self) -> (Option, BackgroundAgentTaskAuthMode) { - let chatgpt_base_url = self - .chatgpt_base_url - .read() - .ok() - .and_then(|guard| guard.clone()); - let auth_mode = self - .background_agent_task_auth_mode - .read() - .ok() - .map(|guard| *guard) - .unwrap_or_default(); - (chatgpt_base_url, auth_mode) - } - - /// Returns the default authorization header for ChatGPT backend requests. - /// - /// This uses background AgentAssertion when configured and available, otherwise it falls back - /// to the ChatGPT bearer token. Low-level bootstrap calls that must never use AgentAssertion - /// should use [`Self::chatgpt_bearer_authorization_header_for_auth`] instead. - pub async fn chatgpt_authorization_header(self: &Arc) -> Option { - let auth = self.auth().await?; - self.chatgpt_authorization_header_for_auth(&auth).await - } - - pub async fn chatgpt_authorization_header_for_auth( - self: &Arc, - auth: &CodexAuth, - ) -> Option { - if !auth.is_chatgpt_auth() { - return None; - } - - let (chatgpt_base_url, auth_mode) = self.chatgpt_backend_auth_config(); - let Some(chatgpt_base_url) = chatgpt_base_url else { - return Self::chatgpt_bearer_authorization_header_for_auth(auth); - }; - - BackgroundAgentTaskManager::new_with_auth_mode( - Arc::clone(self), - chatgpt_base_url, - SessionSource::Cli, - auth_mode, - ) - .authorization_header_value_or_bearer(auth) - .await - } - - pub fn chatgpt_bearer_token_for_auth(auth: &CodexAuth) -> Option { - auth.get_token().ok().filter(|token| !token.is_empty()) - } - - pub fn chatgpt_bearer_authorization_header_for_auth(auth: &CodexAuth) -> Option { - Self::chatgpt_bearer_token_for_auth(auth).map(|token| format!("Bearer {token}")) - } - pub fn subscribe_auth_state(&self) -> watch::Receiver<()> { self.auth_state_tx.subscribe() } @@ -1686,10 +1572,6 @@ impl AuthManager { config.cli_auth_credentials_store_mode(), ); auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id()); - auth_manager.set_chatgpt_backend_auth_config( - config.chatgpt_base_url(), - config.background_agent_task_auth_mode(), - ); auth_manager } diff --git a/codex-rs/login/src/auth/storage.rs b/codex-rs/login/src/auth/storage.rs index 5b752ee672..f0d7a3169c 100644 --- a/codex-rs/login/src/auth/storage.rs +++ b/codex-rs/login/src/auth/storage.rs @@ -52,8 +52,6 @@ pub struct AgentIdentityAuthRecord { pub agent_runtime_id: String, pub agent_private_key: String, pub registered_at: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub background_task_id: Option, } pub(super) fn get_auth_file(codex_home: &Path) -> PathBuf { diff --git a/codex-rs/login/src/auth/storage_tests.rs b/codex-rs/login/src/auth/storage_tests.rs index e00f09d464..2e1cc85029 100644 --- a/codex-rs/login/src/auth/storage_tests.rs +++ b/codex-rs/login/src/auth/storage_tests.rs @@ -69,7 +69,6 @@ async fn file_storage_persists_agent_identity() -> anyhow::Result<()> { agent_runtime_id: "agent_123".to_string(), agent_private_key: "pkcs8-base64".to_string(), registered_at: "2026-04-13T12:00:00Z".to_string(), - background_task_id: None, }), }; diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index af0e13b9b7..046d878e87 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -1,4 +1,3 @@ -pub mod agent_identity; pub mod auth; pub mod auth_env_telemetry; pub mod token_data; @@ -18,8 +17,6 @@ pub use server::ServerOptions; pub use server::ShutdownHandle; pub use server::run_login_server; -pub use agent_identity::BackgroundAgentTaskAuthMode; -pub use agent_identity::cached_background_agent_task_authorization_header_value; pub use auth::AgentIdentityAuthRecord; pub use auth::AgentTaskAuthorizationTarget; pub use auth::AuthConfig;