diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index 5e2ba0caa5..cd1c6335f6 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -11,6 +11,7 @@ use std::path::PathBuf; pub use debug_sandbox::run_command_under_landlock; pub use debug_sandbox::run_command_under_seatbelt; pub use debug_sandbox::run_command_under_windows_sandbox; +pub use login::login_server_options_from_config; pub use login::read_access_token_from_stdin; pub use login::read_api_key_from_stdin; pub use login::run_login_status; diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 0872abc29f..c95a98bce2 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -13,7 +13,7 @@ use codex_core::config::Config; use codex_login::CLIENT_ID; use codex_login::CodexAuth; use codex_login::ServerOptions; -use codex_login::login_with_access_token; +use codex_login::login_with_access_token_with_proxy_config; use codex_login::login_with_api_key; use codex_login::logout_with_revoke; use codex_login::run_device_code_login; @@ -113,6 +113,16 @@ fn print_login_server_start(actual_port: u16, auth_url: &str) { ); } +pub fn login_server_options_from_config(config: &Config) -> ServerOptions { + ServerOptions::new( + config.codex_home.to_path_buf(), + CLIENT_ID.to_string(), + config.forced_chatgpt_workspace_id.clone(), + config.cli_auth_credentials_store_mode, + ) + .with_network_config(config.network.as_ref()) +} + pub async fn login_with_chatgpt( codex_home: PathBuf, forced_chatgpt_workspace_id: Option>, @@ -198,6 +208,7 @@ pub async fn run_login_with_access_token( access_token: String, ) -> ! { let config = load_config_or_exit(cli_config_overrides).await; + let login_server_opts = login_server_options_from_config(&config); let _login_log_guard = init_login_file_logging(&config); tracing::info!("starting access token login flow"); @@ -206,11 +217,12 @@ pub async fn run_login_with_access_token( std::process::exit(1); } - match login_with_access_token( + match login_with_access_token_with_proxy_config( &config.codex_home, &access_token, config.cli_auth_credentials_store_mode, Some(&config.chatgpt_base_url), + login_server_opts.outbound_proxy_config.as_ref(), ) .await { @@ -369,11 +381,13 @@ pub async fn run_login_with_device_code_fallback_to_browser( pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides).await; + let login_server_opts = login_server_options_from_config(&config); - match CodexAuth::from_auth_storage( + match CodexAuth::from_auth_storage_with_proxy_config( &config.codex_home, config.cli_auth_credentials_store_mode, Some(&config.chatgpt_base_url), + login_server_opts.outbound_proxy_config.as_ref(), ) .await { diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 2a5ce171b7..212c84b5b3 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -10,6 +10,7 @@ use codex_arg0::Arg0DispatchPaths; use codex_arg0::arg0_dispatch_or_else; use codex_chatgpt::apply_command::ApplyCommand; use codex_chatgpt::apply_command::run_apply_command; +use codex_cli::login_server_options_from_config; use codex_cli::read_access_token_from_stdin; use codex_cli::read_api_key_from_stdin; use codex_cli::run_login_status; @@ -1639,9 +1640,13 @@ async fn load_exec_server_remote_auth_provider( let agent_identity_jwt = read_codex_access_token_from_env().ok_or_else(|| { anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set") })?; - let auth = - CodexAuth::from_agent_identity_jwt(&agent_identity_jwt, Some(&config.chatgpt_base_url)) - .await?; + let login_server_opts = login_server_options_from_config(config); + let auth = CodexAuth::from_agent_identity_jwt_with_proxy_config( + &agent_identity_jwt, + Some(&config.chatgpt_base_url), + login_server_opts.outbound_proxy_config.as_ref(), + ) + .await?; return Ok(codex_model_provider::auth_provider_from_auth(&auth)); } diff --git a/codex-rs/login/src/auth/agent_identity.rs b/codex-rs/login/src/auth/agent_identity.rs index 3644713328..bae0d371e3 100644 --- a/codex-rs/login/src/auth/agent_identity.rs +++ b/codex-rs/login/src/auth/agent_identity.rs @@ -1,9 +1,10 @@ use codex_agent_identity::AgentIdentityKey; use codex_agent_identity::register_agent_task; +use codex_client::OutboundProxyConfig; use codex_protocol::account::PlanType as AccountPlanType; use std::env; -use crate::default_client::build_reqwest_client; +use crate::default_client::build_auth_reqwest_client_with_proxy_config; use super::storage::AgentIdentityAuthRecord; @@ -18,14 +19,22 @@ pub struct AgentIdentityAuth { impl AgentIdentityAuth { pub async fn load(record: AgentIdentityAuthRecord) -> std::io::Result { + Self::load_with_proxy_config(record, /*outbound_proxy_config*/ None).await + } + + pub(crate) async fn load_with_proxy_config( + record: AgentIdentityAuthRecord, + outbound_proxy_config: Option<&OutboundProxyConfig>, + ) -> std::io::Result { let agent_identity_authapi_base_url = agent_identity_authapi_base_url(); - let process_task_id = register_agent_task( - &build_reqwest_client(), + let client = build_auth_reqwest_client_with_proxy_config( &agent_identity_authapi_base_url, - key(&record), - ) - .await - .map_err(std::io::Error::other)?; + outbound_proxy_config, + )?; + let process_task_id = + register_agent_task(&client, &agent_identity_authapi_base_url, key(&record)) + .await + .map_err(std::io::Error::other)?; Ok(Self { record, process_task_id, diff --git a/codex-rs/login/src/auth/auth_tests.rs b/codex-rs/login/src/auth/auth_tests.rs index 63ab3e0c42..9c31092ec3 100644 --- a/codex-rs/login/src/auth/auth_tests.rs +++ b/codex-rs/login/src/auth/auth_tests.rs @@ -3,6 +3,8 @@ use crate::auth::storage::FileAuthStorage; use crate::auth::storage::get_auth_file; use crate::token_data::IdTokenInfo; use codex_app_server_protocol::AuthMode; +use codex_client::OutboundProxyConfig; +use codex_client::OutboundProxyMode; use codex_protocol::account::PlanType as AccountPlanType; use codex_protocol::auth::KnownPlan as InternalKnownPlan; use codex_protocol::auth::PlanType as InternalPlanType; @@ -1132,6 +1134,63 @@ async fn agent_identity_plan_type_maps_raw_education_alias() { assert_agent_identity_plan_alias(json!("education"), AccountPlanType::Edu).await; } +#[test] +fn agent_identity_bootstrap_proxy_config_is_scoped_to_windows() { + let proxy_config = OutboundProxyConfig { + mode: OutboundProxyMode::System, + proxy_url: Some("http://proxy.example.test:8080".to_string()), + }; + + let scoped = super::agent_identity_bootstrap_proxy_config(Some(&proxy_config)); + + if cfg!(target_os = "windows") { + assert_eq!(scoped, Some(&proxy_config)); + } else { + assert_eq!(scoped, None); + } +} + +#[tokio::test] +#[serial(codex_auth_env)] +async fn agent_identity_auth_with_proxy_config_loads() { + let record = agent_identity_record("account-id"); + let jwt = + signed_agent_identity_jwt(&record, json!(record.plan_type)).expect("agent identity jwt"); + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/backend-api/wham/agent-identities/jwks")) + .respond_with(ResponseTemplate::new(200).set_body_json(test_jwks_body())) + .expect(1) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(path("/backend-api/v1/agent/agent-runtime-id/task/register")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "task_id": "task-123", + }))) + .expect(1) + .mount(&server) + .await; + let chatgpt_base_url = format!("{}/backend-api", server.uri()); + let _authapi_guard = + EnvVarGuard::set("CODEX_AGENT_IDENTITY_AUTHAPI_BASE_URL", &chatgpt_base_url); + let outbound_proxy_config = OutboundProxyConfig { + mode: OutboundProxyMode::Direct, + proxy_url: None, + }; + + let auth = CodexAuth::from_agent_identity_jwt_with_proxy_config( + &jwt, + Some(&chatgpt_base_url), + Some(&outbound_proxy_config), + ) + .await + .expect("agent identity auth"); + + pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Pro)); + server.verify().await; +} + async fn assert_agent_identity_plan_alias( plan_type: serde_json::Value, expected_plan_type: AccountPlanType, diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 5c1cccc501..56278f92ca 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -32,7 +32,7 @@ pub use crate::auth::storage::AuthDotJson; use crate::auth::storage::AuthStorageBackend; use crate::auth::storage::create_auth_storage; use crate::auth::util::try_parse_error_message; -use crate::default_client::build_reqwest_client; +use crate::default_client::build_auth_reqwest_client_with_proxy_config; use crate::default_client::create_client; use crate::default_client::create_client_with_proxy_config; use crate::outbound_proxy::outbound_proxy_config_from_network_config; @@ -223,7 +223,12 @@ impl CodexAuth { "agent identity auth is missing an agent identity token.", )); }; - return Self::from_agent_identity_jwt(&agent_identity, chatgpt_base_url).await; + return Self::from_agent_identity_jwt_with_proxy_config( + &agent_identity, + chatgpt_base_url, + outbound_proxy_config, + ) + .await; } let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode); @@ -249,13 +254,28 @@ impl CodexAuth { codex_home: &Path, auth_credentials_store_mode: AuthCredentialsStoreMode, chatgpt_base_url: Option<&str>, + ) -> std::io::Result> { + Self::from_auth_storage_with_proxy_config( + codex_home, + auth_credentials_store_mode, + chatgpt_base_url, + /*outbound_proxy_config*/ None, + ) + .await + } + + pub async fn from_auth_storage_with_proxy_config( + codex_home: &Path, + auth_credentials_store_mode: AuthCredentialsStoreMode, + chatgpt_base_url: Option<&str>, + outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> std::io::Result> { load_auth( codex_home, /*enable_codex_api_key_env*/ false, auth_credentials_store_mode, chatgpt_base_url, - /*outbound_proxy_config*/ None, + outbound_proxy_config, ) .await } @@ -263,13 +283,29 @@ impl CodexAuth { pub async fn from_agent_identity_jwt( jwt: &str, chatgpt_base_url: Option<&str>, + ) -> std::io::Result { + Self::from_agent_identity_jwt_with_proxy_config( + jwt, + chatgpt_base_url, + /*outbound_proxy_config*/ None, + ) + .await + } + + pub async fn from_agent_identity_jwt_with_proxy_config( + jwt: &str, + chatgpt_base_url: Option<&str>, + outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> std::io::Result { let base_url = chatgpt_base_url .unwrap_or(DEFAULT_CHATGPT_BACKEND_BASE_URL) .trim_end_matches('/') .to_string(); - let record = verified_agent_identity_record(jwt, &base_url).await?; - Ok(Self::AgentIdentity(AgentIdentityAuth::load(record).await?)) + let outbound_proxy_config = agent_identity_bootstrap_proxy_config(outbound_proxy_config); + let record = verified_agent_identity_record(jwt, &base_url, outbound_proxy_config).await?; + Ok(Self::AgentIdentity( + AgentIdentityAuth::load_with_proxy_config(record, outbound_proxy_config).await?, + )) } pub fn auth_mode(&self) -> AuthMode { @@ -499,15 +535,29 @@ fn read_non_empty_env_var(key: &str) -> Option { async fn verified_agent_identity_record( jwt: &str, chatgpt_base_url: &str, + outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> std::io::Result { AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?; - let jwks = fetch_agent_identity_jwks(&build_reqwest_client(), chatgpt_base_url) + let client = + build_auth_reqwest_client_with_proxy_config(chatgpt_base_url, outbound_proxy_config)?; + let jwks = fetch_agent_identity_jwks(&client, chatgpt_base_url) .await .map_err(std::io::Error::other)?; let claims = decode_agent_identity_jwt(jwt, Some(&jwks)).map_err(std::io::Error::other)?; Ok(claims.into()) } +fn agent_identity_bootstrap_proxy_config( + outbound_proxy_config: Option<&OutboundProxyConfig>, +) -> Option<&OutboundProxyConfig> { + // Keep the proxy-aware bootstrap scoped to the Windows proxy rollout. + if cfg!(target_os = "windows") { + outbound_proxy_config + } else { + None + } +} + /// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)` /// if a file was removed, `Ok(false)` if no auth file was present. pub fn logout( @@ -555,12 +605,34 @@ pub async fn login_with_access_token( access_token: &str, auth_credentials_store_mode: AuthCredentialsStoreMode, chatgpt_base_url: Option<&str>, +) -> std::io::Result<()> { + login_with_access_token_with_proxy_config( + codex_home, + access_token, + auth_credentials_store_mode, + chatgpt_base_url, + /*outbound_proxy_config*/ None, + ) + .await +} + +pub async fn login_with_access_token_with_proxy_config( + codex_home: &Path, + access_token: &str, + auth_credentials_store_mode: AuthCredentialsStoreMode, + chatgpt_base_url: Option<&str>, + outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> std::io::Result<()> { let base_url = chatgpt_base_url .unwrap_or(DEFAULT_CHATGPT_BACKEND_BASE_URL) .trim_end_matches('/') .to_string(); - verified_agent_identity_record(access_token, &base_url).await?; + verified_agent_identity_record( + access_token, + &base_url, + agent_identity_bootstrap_proxy_config(outbound_proxy_config), + ) + .await?; let auth_dot_json = AuthDotJson { auth_mode: Some(ApiAuthMode::AgentIdentity), openai_api_key: None, @@ -773,9 +845,13 @@ async fn load_auth( } if let Some(agent_identity) = read_codex_access_token_from_env() { - return CodexAuth::from_agent_identity_jwt(&agent_identity, chatgpt_base_url) - .await - .map(Some); + return CodexAuth::from_agent_identity_jwt_with_proxy_config( + &agent_identity, + chatgpt_base_url, + outbound_proxy_config, + ) + .await + .map(Some); } // Fall back to the configured persistent store (file/keyring/auto) for managed auth. diff --git a/codex-rs/login/src/outbound_proxy.rs b/codex-rs/login/src/outbound_proxy.rs index edbb92d74b..89281aead5 100644 --- a/codex-rs/login/src/outbound_proxy.rs +++ b/codex-rs/login/src/outbound_proxy.rs @@ -9,7 +9,10 @@ pub(crate) fn outbound_proxy_config_from_network_config( let mode = match network.proxy_mode.unwrap_or_default() { NetworkProxyMode::Auto => OutboundProxyMode::Auto, NetworkProxyMode::Env => OutboundProxyMode::Env, - NetworkProxyMode::System => OutboundProxyMode::System, + // Keep non-Windows users on the legacy path until the resolver-backed + // system proxy rollout expands beyond Windows. + NetworkProxyMode::System if system_proxy_mode_enabled() => OutboundProxyMode::System, + NetworkProxyMode::System => OutboundProxyMode::Auto, NetworkProxyMode::Direct => OutboundProxyMode::Direct, }; OutboundProxyConfig { @@ -17,3 +20,39 @@ pub(crate) fn outbound_proxy_config_from_network_config( proxy_url: network.proxy_url.clone(), } } + +const fn system_proxy_mode_enabled() -> bool { + cfg!(target_os = "windows") +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn default_mode_preserves_legacy_auto_path() { + let network = NetworkConfigToml::default(); + + let config = outbound_proxy_config_from_network_config(&network); + + assert_eq!(config.mode, OutboundProxyMode::Auto); + } + + #[test] + fn explicit_system_mode_is_windows_only() { + let network = NetworkConfigToml { + proxy_mode: Some(NetworkProxyMode::System), + proxy_url: None, + }; + + let config = outbound_proxy_config_from_network_config(&network); + + let expected = if cfg!(target_os = "windows") { + OutboundProxyMode::System + } else { + OutboundProxyMode::Auto + }; + assert_eq!(config.mode, expected); + } +}