scope proxy-aware auth bootstrap to Win

This commit is contained in:
canvrno-oai
2026-05-27 17:02:27 -07:00
parent 8e906caadd
commit c49dcfbfa2
7 changed files with 227 additions and 24 deletions

View File

@@ -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;

View File

@@ -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<Vec<String>>,
@@ -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
{

View File

@@ -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));
}

View File

@@ -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> {
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<Self> {
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,

View File

@@ -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,

View File

@@ -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<Option<Self>> {
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<Option<Self>> {
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> {
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<Self> {
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<String> {
async fn verified_agent_identity_record(
jwt: &str,
chatgpt_base_url: &str,
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> std::io::Result<AgentIdentityAuthRecord> {
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.

View File

@@ -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);
}
}