PAC 2 - Add shared auth system proxy contract (#26707)

## Summary

Stacked on #26706.

Adds the shared auth/system-proxy contract that later platform resolver
PRs plug into. This PR moves Codex-owned auth and startup HTTP clients
through a common route-aware boundary, but does not yet add Windows or
macOS system proxy resolution.

The default path remains unchanged when `respect_system_proxy` is absent
or disabled.

## Implementation

- Adds `codex-client/src/outbound_proxy.rs` with the shared
route-selection model:
  - `OutboundProxyConfig`;
  - `ClientRouteClass`;
  - `RouteFailureClass`;
  - `build_reqwest_client_for_route`.
- Preserves the existing reqwest/default-client behavior when no route
config is supplied.
- Uses the fixed MVP routing policy when route config is supplied:
platform system/PAC/WPAD discovery, then explicit env proxy variables,
then direct connection.
- Keeps platform-specific system discovery behind the shared client
boundary. This PR provides the contract and fallback behavior; later
resolver PRs plug in Windows and macOS discovery.
- Adds `login::AuthRouteConfig` so auth call sites depend on a small
policy type instead of platform resolver details.
- Maps the resolved `Config.respect_system_proxy` boolean into
`AuthRouteConfig` for auth-owned clients.
- Wires the route config through browser login, device-code login,
access-token login, login status, logout/revoke, token refresh, API-key
exchange, app-server account login, TUI/app startup, cloud-config
bootstrap, cloud tasks, plugin auth, and exec startup config loading.

## End-user behavior

- No behavior changes by default.
- When `respect_system_proxy = true`, auth-owned clients opt into the
shared route-aware client path.
- On platforms without a resolver implementation in this PR, system
discovery is unavailable and the route-aware path falls back to explicit
env proxy handling, then direct connection.
- Custom CA handling remains separate from proxy route selection and
still runs through the shared client builder.
- No proxy URLs, PAC contents, or resolved platform details are exposed
through the public config surface introduced here.

## Tests

Adds or updates coverage for:

- preserving default auth-client fallback behavior when no route config
is provided;
- injected environment-proxy fallback without mutating process
environment;
- existing login-server E2E flows using explicit `auth_route_config:
None` to guard unchanged default behavior;
- updated auth manager, login, logout, cloud-config, startup, and
plugin-auth call sites passing route config explicitly.
This commit is contained in:
canvrno-oai
2026-06-22 13:03:11 -07:00
committed by GitHub
parent e48ab86693
commit 1659c4a629
37 changed files with 885 additions and 54 deletions
+20 -1
View File
@@ -11,6 +11,7 @@ use codex_app_server_protocol::AuthMode;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::Config;
use codex_login::AuthKeyringBackendKind;
use codex_login::AuthRouteConfig;
use codex_login::CLIENT_ID;
use codex_login::CodexAuth;
use codex_login::ServerOptions;
@@ -119,11 +120,13 @@ async fn clear_existing_auth_before_login(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
auth_keyring_backend_kind: AuthKeyringBackendKind,
auth_route_config: Option<&AuthRouteConfig>,
) {
if let Err(err) = logout_with_revoke(
codex_home,
auth_credentials_store_mode,
auth_keyring_backend_kind,
auth_route_config,
)
.await
{
@@ -136,11 +139,13 @@ pub async fn login_with_chatgpt(
forced_chatgpt_workspace_id: Option<Vec<String>>,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
auth_keyring_backend_kind: AuthKeyringBackendKind,
auth_route_config: Option<AuthRouteConfig>,
) -> std::io::Result<()> {
clear_existing_auth_before_login(
&codex_home,
cli_auth_credentials_store_mode,
auth_keyring_backend_kind,
auth_route_config.as_ref(),
)
.await;
@@ -150,6 +155,7 @@ pub async fn login_with_chatgpt(
forced_chatgpt_workspace_id,
cli_auth_credentials_store_mode,
auth_keyring_backend_kind,
auth_route_config,
);
let server = run_login_server(opts)?;
@@ -169,12 +175,12 @@ pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) ->
}
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
match login_with_chatgpt(
config.codex_home.to_path_buf(),
forced_chatgpt_workspace_id,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
config.auth_route_config(),
)
.await
{
@@ -232,6 +238,7 @@ pub async fn run_login_with_access_token(
std::process::exit(1);
}
let auth_route_config = config.auth_route_config();
match login_with_access_token(
&config.codex_home,
&access_token,
@@ -239,6 +246,7 @@ pub async fn run_login_with_access_token(
config.forced_chatgpt_workspace_id.as_deref(),
Some(&config.chatgpt_base_url),
config.auth_keyring_backend_kind(),
auth_route_config.as_ref(),
)
.await
{
@@ -307,10 +315,12 @@ pub async fn run_login_with_device_code(
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
let auth_route_config = config.auth_route_config();
clear_existing_auth_before_login(
&config.codex_home,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
auth_route_config.as_ref(),
)
.await;
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
@@ -320,6 +330,7 @@ pub async fn run_login_with_device_code(
forced_chatgpt_workspace_id,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
auth_route_config,
);
if let Some(iss) = issuer_base_url {
opts.issuer = iss;
@@ -352,10 +363,12 @@ pub async fn run_login_with_device_code_fallback_to_browser(
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
let auth_route_config = config.auth_route_config();
clear_existing_auth_before_login(
&config.codex_home,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
auth_route_config.as_ref(),
)
.await;
@@ -366,6 +379,7 @@ pub async fn run_login_with_device_code_fallback_to_browser(
forced_chatgpt_workspace_id,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
auth_route_config,
);
if let Some(iss) = issuer_base_url {
opts.issuer = iss;
@@ -409,12 +423,14 @@ 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 auth_route_config = config.auth_route_config();
match CodexAuth::from_auth_storage(
&config.codex_home,
config.cli_auth_credentials_store_mode,
Some(&config.chatgpt_base_url),
config.auth_keyring_backend_kind(),
auth_route_config.as_ref(),
)
.await
{
@@ -459,11 +475,13 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides).await;
let auth_route_config = config.auth_route_config();
match logout_with_revoke(
&config.codex_home,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
auth_route_config.as_ref(),
)
.await
{
@@ -536,6 +554,7 @@ mod tests {
codex_home.path(),
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
+7 -3
View File
@@ -1738,9 +1738,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 auth_route_config = config.auth_route_config();
let auth = CodexAuth::from_agent_identity_jwt(
&agent_identity_jwt,
Some(&config.chatgpt_base_url),
auth_route_config.as_ref(),
)
.await?;
return Ok(codex_model_provider::auth_provider_from_auth(&auth));
}
+2
View File
@@ -566,11 +566,13 @@ pub(crate) async fn load_cli_auth_mode(config: &Config) -> Option<AuthMode> {
return Some(CodexAuth::from_api_key(&api_key).api_auth_mode());
}
let auth_route_config = config.auth_route_config();
CodexAuth::from_auth_storage(
&config.codex_home,
config.cli_auth_credentials_store_mode,
Some(&config.chatgpt_base_url),
config.auth_keyring_backend_kind(),
auth_route_config.as_ref(),
)
.await
.ok()