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 authored and GitHub committed 2026-06-22 13:03:11 -07:00
1 parent e48ab86693
commit 1659c4a629
37 files changed
+885 -54

No files matched your search

@@ -1041,6 +1041,7 @@ async fn remote_control_start_allows_missing_auth_when_enabled() {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let (transport_event_tx, _transport_event_rx) =
@@ -1870,6 +1871,7 @@ async fn remote_control_waits_for_account_id_before_enrolling() {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let expected_server_name = gethostname().to_string_lossy().trim().to_string();
@@ -1966,6 +1968,7 @@ async fn persisted_enable_does_not_follow_auth_to_an_account_without_a_preferenc
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let remote_control_target =
@@ -185,6 +185,7 @@ async fn list_remote_control_clients_recovers_auth_after_unauthorized() {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let mut fresh_auth = remote_control_auth_dot_json(Some("account_id"));
@@ -270,6 +271,7 @@ async fn list_remote_control_clients_retries_unauthorized_only_once() {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let mut fresh_auth = remote_control_auth_dot_json(Some("account_id"));
@@ -539,6 +539,7 @@ async fn remote_control_handle_recovers_auth_before_refreshing_pairing() {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let mut fresh_auth = remote_control_auth_dot_json(Some("account_id"));
@@ -808,6 +809,7 @@ async fn remote_control_handle_discards_pairing_response_after_auth_change() {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let remote_handle =
@@ -2214,6 +2214,7 @@ mod tests {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let mut auth_recovery = auth_manager.unauthorized_recovery();
@@ -2312,6 +2313,7 @@ mod tests {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let mut auth_recovery = auth_manager.unauthorized_recovery();
@@ -2435,6 +2437,7 @@ mod tests {
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
let mut auth_recovery = auth_manager.unauthorized_recovery();