mirror of
https://github.com/openai/codex.git
synced 2026-09-10 20:26:47 +00:00
scope proxy-aware auth bootstrap to Win
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user