mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
e48ab86693
commit
1659c4a629
@@ -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();
|
||||
|
||||
@@ -359,6 +359,7 @@ impl AccountRequestProcessor {
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.auth_route_config(),
|
||||
)
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -58,6 +59,7 @@ pub async fn cloud_config_bundle_loader_for_storage(
|
||||
credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
chatgpt_base_url: String,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
) -> CloudConfigBundleLoader {
|
||||
let auth_manager = AuthManager::shared(
|
||||
codex_home.clone(),
|
||||
@@ -66,6 +68,7 @@ pub async fn cloud_config_bundle_loader_for_storage(
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
Some(chatgpt_base_url.clone()),
|
||||
keyring_backend_kind,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
cloud_config_bundle_loader(auth_manager, chatgpt_base_url, codex_home)
|
||||
|
||||
@@ -53,6 +53,7 @@ async fn auth_manager_with_api_key() -> Arc<AuthManager> {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
@@ -83,6 +84,7 @@ async fn auth_manager_with_plan_and_identity(
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
@@ -108,6 +110,7 @@ async fn auth_manager_with_agent_identity_business_plan() -> Arc<AuthManager> {
|
||||
task_id: Some("task-123".to_string()),
|
||||
},
|
||||
"https://auth.openai.com/api/accounts",
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("agent identity record should be complete"),
|
||||
@@ -686,6 +689,7 @@ async fn get_bundle_recovers_after_unauthorized_reload() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -742,6 +746,7 @@ async fn get_bundle_recovers_after_unauthorized_reload_updates_cache_identity()
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -806,6 +811,7 @@ async fn get_bundle_surfaces_auth_recovery_message() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -872,6 +878,7 @@ async fn get_bundle_unauthorized_without_recovery_uses_generic_message() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
@@ -52,6 +52,7 @@ pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthM
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url.or(Some(config.chatgpt_base_url.clone())),
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.auth_route_config(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ mod chatgpt_hosts;
|
||||
mod custom_ca;
|
||||
mod default_client;
|
||||
mod error;
|
||||
mod outbound_proxy;
|
||||
mod request;
|
||||
mod retry;
|
||||
mod sse;
|
||||
@@ -25,6 +26,11 @@ pub use crate::default_client::CodexHttpClient;
|
||||
pub use crate::default_client::CodexRequestBuilder;
|
||||
pub use crate::error::StreamError;
|
||||
pub use crate::error::TransportError;
|
||||
pub use crate::outbound_proxy::BuildRouteAwareHttpClientError;
|
||||
pub use crate::outbound_proxy::ClientRouteClass;
|
||||
pub use crate::outbound_proxy::OutboundProxyConfig;
|
||||
pub use crate::outbound_proxy::RouteFailureClass;
|
||||
pub use crate::outbound_proxy::build_reqwest_client_for_route;
|
||||
pub use crate::request::EncodedJsonBody;
|
||||
pub use crate::request::PreparedRequestBody;
|
||||
pub use crate::request::Request;
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
//! Conservative outbound proxy selection for resolver-aware clients.
|
||||
//!
|
||||
//! When enabled, platform system discovery is tried first, explicit environment
|
||||
//! proxies are the fallback, and the final fallback is a direct connection.
|
||||
//! When disabled, callers retain the existing reqwest builder behavior.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::custom_ca::BuildCustomCaTransportError;
|
||||
use crate::custom_ca::build_reqwest_client_with_custom_ca;
|
||||
use thiserror::Error;
|
||||
|
||||
const SYSTEM_PROXY_SUCCESS_CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
const SYSTEM_PROXY_UNAVAILABLE_CACHE_TTL: Duration = Duration::from_secs(5);
|
||||
const SYSTEM_PROXY_CACHE_MAX_ENTRIES: usize = 256;
|
||||
|
||||
/// Coarse semantic bucket for the HTTP or WebSocket client being constructed.
|
||||
///
|
||||
/// This is not the selected proxy route or a concrete endpoint. It labels the
|
||||
/// product path that owns the client so proxy-resolution diagnostics can
|
||||
/// distinguish auth, API, WebSocket, and miscellaneous traffic without exposing
|
||||
/// endpoint details.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ClientRouteClass {
|
||||
/// Login, token refresh/revoke, PAT, and agent identity auth traffic.
|
||||
Auth,
|
||||
/// First-party API traffic that is not part of the auth flow.
|
||||
Api,
|
||||
/// WebSocket traffic.
|
||||
WebSocket,
|
||||
/// Call sites without a more specific route class.
|
||||
Other,
|
||||
}
|
||||
|
||||
impl fmt::Display for ClientRouteClass {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Auth => "auth",
|
||||
Self::Api => "api",
|
||||
Self::WebSocket => "wss",
|
||||
Self::Other => "other",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Coarse failure class for route selection errors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RouteFailureClass {
|
||||
ProxyResolutionUnavailable,
|
||||
ConnectTimeout,
|
||||
ProxyAuthenticationRequired,
|
||||
TlsError,
|
||||
InvalidProxyConfig,
|
||||
UnsupportedProxyScheme,
|
||||
ResolverError,
|
||||
}
|
||||
|
||||
impl fmt::Display for RouteFailureClass {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::ProxyResolutionUnavailable => "proxy_resolution_unavailable",
|
||||
Self::ConnectTimeout => "connect_timeout",
|
||||
Self::ProxyAuthenticationRequired => "proxy_407",
|
||||
Self::TlsError => "tls_error",
|
||||
Self::InvalidProxyConfig => "invalid_proxy_config",
|
||||
Self::UnsupportedProxyScheme => "unsupported_proxy_scheme",
|
||||
Self::ResolverError => "resolver_error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker enabling fixed system/PAC/WPAD, environment, then direct routing.
|
||||
/// Resolved endpoints and platform details remain internal to the client builder.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OutboundProxyConfig;
|
||||
|
||||
impl OutboundProxyConfig {
|
||||
pub const fn respect_system_proxy() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
/// Error while building a resolver-aware reqwest client.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BuildRouteAwareHttpClientError {
|
||||
#[error(transparent)]
|
||||
CustomCa(#[from] BuildCustomCaTransportError),
|
||||
|
||||
#[error("Failed to configure outbound proxy selected for {route_class}")]
|
||||
InvalidProxyConfig { route_class: ClientRouteClass },
|
||||
}
|
||||
|
||||
impl From<BuildRouteAwareHttpClientError> for io::Error {
|
||||
fn from(error: BuildRouteAwareHttpClientError) -> Self {
|
||||
match error {
|
||||
BuildRouteAwareHttpClientError::CustomCa(error) => error.into(),
|
||||
BuildRouteAwareHttpClientError::InvalidProxyConfig { .. } => io::Error::other(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a reqwest client with conservative route selection and shared CA handling.
|
||||
///
|
||||
/// Unavailable platform resolution falls back to environment proxies and then direct. Errors after
|
||||
/// a route is selected are returned without trying another route.
|
||||
pub fn build_reqwest_client_for_route(
|
||||
builder: reqwest::ClientBuilder,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
config: Option<&OutboundProxyConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
let builder =
|
||||
configure_proxy_for_route(&ProcessEnv, builder, request_url, route_class, config)?;
|
||||
build_reqwest_client_with_custom_ca(builder).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn configure_proxy_for_route(
|
||||
env: &dyn EnvSource,
|
||||
builder: reqwest::ClientBuilder,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
config: Option<&OutboundProxyConfig>,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
if config.is_none() {
|
||||
return Ok(builder);
|
||||
}
|
||||
let origin = RequestOrigin::parse(request_url);
|
||||
|
||||
let Some(origin) = origin.as_ref() else {
|
||||
return configure_env_proxy_handling(env, builder, /*origin*/ None, route_class);
|
||||
};
|
||||
|
||||
match resolve_system_proxy(request_url, origin) {
|
||||
SystemProxyDecision::Direct => Ok(builder.no_proxy()),
|
||||
SystemProxyDecision::Proxy { url } => {
|
||||
configure_concrete_proxy(builder, route_class, &url, /*no_proxy*/ None)
|
||||
}
|
||||
SystemProxyDecision::Unavailable { .. } => {
|
||||
configure_env_proxy_handling(env, builder, Some(origin), route_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_concrete_proxy(
|
||||
builder: reqwest::ClientBuilder,
|
||||
route_class: ClientRouteClass,
|
||||
proxy_url: &str,
|
||||
no_proxy: Option<reqwest::NoProxy>,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
let proxy = match reqwest::Proxy::all(proxy_url) {
|
||||
Ok(proxy) => proxy,
|
||||
Err(_source) => {
|
||||
return Err(BuildRouteAwareHttpClientError::InvalidProxyConfig { route_class });
|
||||
}
|
||||
};
|
||||
Ok(builder.proxy(proxy.no_proxy(no_proxy)))
|
||||
}
|
||||
|
||||
fn configure_env_proxy_handling(
|
||||
env: &dyn EnvSource,
|
||||
builder: reqwest::ClientBuilder,
|
||||
origin: Option<&RequestOrigin>,
|
||||
route_class: ClientRouteClass,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
if let Some(origin) = origin {
|
||||
let proxy_url = match origin.scheme.as_str() {
|
||||
"https" => {
|
||||
proxy_env_value(env, "HTTPS_PROXY").or_else(|| proxy_env_value(env, "ALL_PROXY"))
|
||||
}
|
||||
"http" => {
|
||||
proxy_env_value(env, "HTTP_PROXY").or_else(|| proxy_env_value(env, "ALL_PROXY"))
|
||||
}
|
||||
_ => proxy_env_value(env, "ALL_PROXY"),
|
||||
};
|
||||
if let Some(proxy_url) = proxy_url {
|
||||
let no_proxy = proxy_env_value(env, "NO_PROXY")
|
||||
.and_then(|value| reqwest::NoProxy::from_string(&value));
|
||||
return configure_concrete_proxy(builder, route_class, &proxy_url, no_proxy);
|
||||
}
|
||||
}
|
||||
Ok(builder.no_proxy())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
struct RequestOrigin {
|
||||
scheme: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl RequestOrigin {
|
||||
fn parse(request_url: &str) -> Option<Self> {
|
||||
let uri = request_url.parse::<http::Uri>().ok()?;
|
||||
let scheme = uri.scheme_str()?.to_ascii_lowercase();
|
||||
let host = uri.host()?.trim_matches(['[', ']']).to_ascii_lowercase();
|
||||
let port = uri.port_u16().or(match scheme.as_str() {
|
||||
"http" => Some(80),
|
||||
"https" => Some(443),
|
||||
_ => None,
|
||||
})?;
|
||||
Some(Self { scheme, host, port })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "Direct and Proxy are constructed by platform resolvers added in later PRs"
|
||||
)]
|
||||
enum SystemProxyDecision {
|
||||
Direct,
|
||||
Proxy { url: String },
|
||||
Unavailable { failure: RouteFailureClass },
|
||||
}
|
||||
|
||||
fn resolve_system_proxy(request_url: &str, origin: &RequestOrigin) -> SystemProxyDecision {
|
||||
if let Some(decision) = cached_system_proxy_decision(request_url) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
let decision = resolve_platform_system_proxy(request_url, origin);
|
||||
cache_system_proxy_decision(request_url, decision.clone());
|
||||
decision
|
||||
}
|
||||
|
||||
fn resolve_platform_system_proxy(
|
||||
_request_url: &str,
|
||||
_origin: &RequestOrigin,
|
||||
) -> SystemProxyDecision {
|
||||
SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CachedSystemProxyDecision {
|
||||
decision: SystemProxyDecision,
|
||||
expires_at: Instant,
|
||||
}
|
||||
|
||||
static SYSTEM_PROXY_CACHE: OnceLock<Mutex<HashMap<String, CachedSystemProxyDecision>>> =
|
||||
OnceLock::new();
|
||||
|
||||
fn cached_system_proxy_decision(request_url: &str) -> Option<SystemProxyDecision> {
|
||||
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let mut cache = cache.lock().ok()?;
|
||||
let cached = cache.get(request_url)?;
|
||||
if cached.expires_at > Instant::now() {
|
||||
return Some(cached.decision.clone());
|
||||
}
|
||||
cache.remove(request_url);
|
||||
None
|
||||
}
|
||||
|
||||
fn cache_system_proxy_decision(request_url: &str, decision: SystemProxyDecision) {
|
||||
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
if let Ok(mut cache) = cache.lock() {
|
||||
insert_system_proxy_cache_entry(&mut cache, request_url, decision, Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_system_proxy_cache_entry(
|
||||
cache: &mut HashMap<String, CachedSystemProxyDecision>,
|
||||
request_url: &str,
|
||||
decision: SystemProxyDecision,
|
||||
now: Instant,
|
||||
) {
|
||||
let ttl = match &decision {
|
||||
SystemProxyDecision::Direct | SystemProxyDecision::Proxy { .. } => {
|
||||
SYSTEM_PROXY_SUCCESS_CACHE_TTL
|
||||
}
|
||||
SystemProxyDecision::Unavailable { .. } => SYSTEM_PROXY_UNAVAILABLE_CACHE_TTL,
|
||||
};
|
||||
|
||||
cache.retain(|_, cached| cached.expires_at > now);
|
||||
if cache.len() >= SYSTEM_PROXY_CACHE_MAX_ENTRIES
|
||||
&& !cache.contains_key(request_url)
|
||||
&& let Some(request_url_to_evict) = cache
|
||||
.iter()
|
||||
.min_by_key(|(_, cached)| cached.expires_at)
|
||||
.map(|(request_url, _)| request_url.clone())
|
||||
{
|
||||
cache.remove(&request_url_to_evict);
|
||||
}
|
||||
cache.insert(
|
||||
request_url.to_string(),
|
||||
CachedSystemProxyDecision {
|
||||
decision,
|
||||
expires_at: now + ttl,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
trait EnvSource {
|
||||
fn var(&self, key: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
struct ProcessEnv;
|
||||
|
||||
impl EnvSource for ProcessEnv {
|
||||
fn var(&self, key: &str) -> Option<String> {
|
||||
std::env::var(key).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_env_value(env: &dyn EnvSource, upper: &str) -> Option<String> {
|
||||
let lower = upper.to_ascii_lowercase();
|
||||
env.var(upper)
|
||||
.or_else(|| env.var(&lower))
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "outbound_proxy_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,133 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
|
||||
struct MapEnv {
|
||||
values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl EnvSource for MapEnv {
|
||||
fn var(&self, key: &str) -> Option<String> {
|
||||
self.values.get(key).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_env_value_matches_reqwest_casing_precedence() {
|
||||
let env = MapEnv {
|
||||
values: HashMap::from([
|
||||
("HTTPS_PROXY".to_string(), "upper".to_string()),
|
||||
("https_proxy".to_string(), "lower".to_string()),
|
||||
("http_proxy".to_string(), "lower-only".to_string()),
|
||||
("ALL_PROXY".to_string(), String::new()),
|
||||
("all_proxy".to_string(), "masked".to_string()),
|
||||
]),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
proxy_env_value(&env, "HTTPS_PROXY"),
|
||||
Some("upper".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
proxy_env_value(&env, "HTTP_PROXY"),
|
||||
Some("lower-only".to_string())
|
||||
);
|
||||
assert_eq!(proxy_env_value(&env, "ALL_PROXY"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_fallback_reads_injected_proxy_environment() {
|
||||
let env = MapEnv {
|
||||
values: HashMap::from([("HTTPS_PROXY".to_string(), "://invalid".to_string())]),
|
||||
};
|
||||
let origin = RequestOrigin::parse("https://auth.openai.com/oauth/token").expect("valid URL");
|
||||
let result = configure_env_proxy_handling(
|
||||
&env,
|
||||
reqwest::Client::builder(),
|
||||
Some(&origin),
|
||||
ClientRouteClass::Auth,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(BuildRouteAwareHttpClientError::InvalidProxyConfig {
|
||||
route_class: ClientRouteClass::Auth,
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enabled_environment_proxy_routes_request_through_proxy() {
|
||||
let listener =
|
||||
std::net::TcpListener::bind(("127.0.0.1", 0)).expect("local proxy listener should bind");
|
||||
let proxy_addr = listener
|
||||
.local_addr()
|
||||
.expect("local proxy listener should have an address");
|
||||
let proxy_thread = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("proxy should accept a request");
|
||||
let mut buffer = [0_u8; 4096];
|
||||
let size = stream.read(&mut buffer).expect("proxy should read request");
|
||||
stream
|
||||
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
|
||||
.expect("proxy should write response");
|
||||
String::from_utf8_lossy(&buffer[..size]).into_owned()
|
||||
});
|
||||
let env = MapEnv {
|
||||
values: HashMap::from([("HTTP_PROXY".to_string(), format!("http://{proxy_addr}"))]),
|
||||
};
|
||||
let request_url = "http://enabled-proxy.test/proxy-check";
|
||||
let config = OutboundProxyConfig::respect_system_proxy();
|
||||
let builder = configure_proxy_for_route(
|
||||
&env,
|
||||
reqwest::Client::builder().timeout(Duration::from_secs(2)),
|
||||
request_url,
|
||||
ClientRouteClass::Auth,
|
||||
Some(&config),
|
||||
)
|
||||
.expect("enabled proxy route should configure");
|
||||
|
||||
let response = builder
|
||||
.build()
|
||||
.expect("proxy client should build")
|
||||
.get(request_url)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should use local proxy");
|
||||
let proxy_request = proxy_thread.join().expect("proxy thread should finish");
|
||||
|
||||
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
||||
assert_eq!(
|
||||
proxy_request.lines().next(),
|
||||
Some("GET http://enabled-proxy.test/proxy-check HTTP/1.1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_system_proxy_decision_is_cached() {
|
||||
let request_url = "https://unavailable-cache.test/oauth/token";
|
||||
let decision = SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
|
||||
cache_system_proxy_decision(request_url, decision.clone());
|
||||
|
||||
assert_eq!(cached_system_proxy_decision(request_url), Some(decision));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_proxy_cache_is_bounded() {
|
||||
let mut cache = HashMap::new();
|
||||
let now = Instant::now();
|
||||
|
||||
for index in 0..=SYSTEM_PROXY_CACHE_MAX_ENTRIES {
|
||||
insert_system_proxy_cache_entry(
|
||||
&mut cache,
|
||||
&format!("https://bounded-cache.test/{index}"),
|
||||
SystemProxyDecision::Direct,
|
||||
now,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(cache.len(), SYSTEM_PROXY_CACHE_MAX_ENTRIES);
|
||||
}
|
||||
@@ -72,6 +72,7 @@ use codex_features::TokenBudgetConfigToml;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_install_context::InstallContext;
|
||||
use codex_login::AuthManagerConfig;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use codex_mcp::McpConfig;
|
||||
use codex_mcp::McpPluginAttribution;
|
||||
use codex_mcp::McpServerRegistration;
|
||||
@@ -1201,6 +1202,10 @@ impl AuthManagerConfig for Config {
|
||||
fn chatgpt_base_url(&self) -> String {
|
||||
self.chatgpt_base_url.clone()
|
||||
}
|
||||
|
||||
fn auth_route_config(&self) -> Option<AuthRouteConfig> {
|
||||
Config::auth_route_config(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -1454,6 +1459,11 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auth_route_config(&self) -> Option<AuthRouteConfig> {
|
||||
self.respect_system_proxy
|
||||
.then(AuthRouteConfig::respect_system_proxy)
|
||||
}
|
||||
|
||||
/// Build the plugin-manager input from the effective config.
|
||||
pub fn plugins_config_input(&self) -> PluginsConfigInput {
|
||||
PluginsConfigInput::new(
|
||||
@@ -2737,6 +2747,15 @@ pub fn resolve_bootstrap_respect_system_proxy(
|
||||
Ok(features.get().enabled(Feature::RespectSystemProxy))
|
||||
}
|
||||
|
||||
/// Resolves auth route settings for the initial cloud-config bootstrap.
|
||||
pub fn resolve_bootstrap_auth_route_config(
|
||||
cfg: &ConfigToml,
|
||||
feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
|
||||
) -> std::io::Result<Option<AuthRouteConfig>> {
|
||||
resolve_bootstrap_respect_system_proxy(cfg, feature_requirements)
|
||||
.map(|enabled| enabled.then(AuthRouteConfig::respect_system_proxy))
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
web_search_mode: &Constrained<WebSearchMode>,
|
||||
permission_profile: &PermissionProfile,
|
||||
|
||||
@@ -1385,6 +1385,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to load CodexAuth")
|
||||
|
||||
@@ -66,6 +66,7 @@ use codex_core::config::ConfigTomlLoadResult;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use codex_core::config::resolve_bootstrap_auth_route_config;
|
||||
use codex_core::config::resolve_oss_provider;
|
||||
use codex_core::config::resolve_profile_v2_config_path;
|
||||
use codex_core::find_thread_meta_by_name_str;
|
||||
@@ -349,6 +350,14 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let auth_route_config = resolve_bootstrap_auth_route_config(
|
||||
bootstrap_config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
@@ -357,6 +366,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
let run_cli_overrides = cli_kv_overrides.clone();
|
||||
@@ -468,6 +478,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
|
||||
set_default_client_residency_requirement(config.enforce_residency.value());
|
||||
|
||||
let auth_route_config = config.auth_route_config();
|
||||
if let Err(err) = enforce_login_restrictions(&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
@@ -475,6 +486,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
forced_login_method: config.forced_login_method,
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
|
||||
auth_route_config,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -3,6 +3,9 @@ use std::sync::Arc;
|
||||
|
||||
use codex_agent_identity::AgentIdentityKey;
|
||||
use codex_agent_identity::ChatGptEnvironment;
|
||||
use codex_agent_identity::agent_identity_jwks_url;
|
||||
use codex_agent_identity::agent_registration_url;
|
||||
use codex_agent_identity::agent_task_registration_url;
|
||||
use codex_agent_identity::build_abom;
|
||||
use codex_agent_identity::decode_agent_identity_jwt;
|
||||
use codex_agent_identity::fetch_agent_identity_jwks;
|
||||
@@ -15,7 +18,8 @@ use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::default_client::build_default_auth_reqwest_client;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
|
||||
use super::storage::AgentIdentityAuthRecord;
|
||||
|
||||
@@ -96,13 +100,18 @@ impl AgentIdentityAuth {
|
||||
pub async fn from_record(
|
||||
mut record: AgentIdentityAuthRecord,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
public_key_ssh_from_private_key_pkcs8_base64(&record.agent_private_key)
|
||||
.map_err(std::io::Error::other)?;
|
||||
if record_needs_task_registration(&record) {
|
||||
record.task_id = Some(
|
||||
register_task_for_record_with_retries(&record, agent_identity_authapi_base_url)
|
||||
.await?,
|
||||
register_task_for_record_with_retries(
|
||||
&record,
|
||||
agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
@@ -114,9 +123,10 @@ impl AgentIdentityAuth {
|
||||
jwt: &str,
|
||||
chatgpt_base_url: &str,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let record = verified_record_from_jwt(jwt, chatgpt_base_url).await?;
|
||||
Self::from_record(record, agent_identity_authapi_base_url).await
|
||||
let record = verified_record_from_jwt(jwt, chatgpt_base_url, auth_route_config).await?;
|
||||
Self::from_record(record, agent_identity_authapi_base_url, auth_route_config).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -163,9 +173,11 @@ pub(super) async fn register_managed_chatgpt_agent_identity(
|
||||
binding: ManagedChatGptAgentIdentityBinding,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
session_source: SessionSource,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<AgentIdentityAuth> {
|
||||
let key_material = generate_agent_key_material().map_err(std::io::Error::other)?;
|
||||
let client = build_reqwest_client();
|
||||
let registration_url = agent_registration_url(agent_identity_authapi_base_url);
|
||||
let client = build_default_auth_reqwest_client(®istration_url, auth_route_config)?;
|
||||
let runtime_id = retry_registration(|| async {
|
||||
register_agent_identity(
|
||||
&client,
|
||||
@@ -200,7 +212,7 @@ pub(super) async fn register_managed_chatgpt_agent_identity(
|
||||
chatgpt_account_is_fedramp: binding.chatgpt_account_is_fedramp,
|
||||
task_id: None,
|
||||
};
|
||||
AgentIdentityAuth::from_record(record, agent_identity_authapi_base_url)
|
||||
AgentIdentityAuth::from_record(record, agent_identity_authapi_base_url, auth_route_config)
|
||||
.await
|
||||
.map_err(|err| classify_bootstrap_error("agent task registration", err))
|
||||
}
|
||||
@@ -208,9 +220,12 @@ pub(super) async fn register_managed_chatgpt_agent_identity(
|
||||
pub(super) async fn verified_record_from_jwt(
|
||||
jwt: &str,
|
||||
chatgpt_base_url: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<AgentIdentityAuthRecord> {
|
||||
AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?;
|
||||
let jwks = fetch_agent_identity_jwks(&build_reqwest_client(), chatgpt_base_url)
|
||||
let jwks_url = agent_identity_jwks_url(chatgpt_base_url);
|
||||
let client = build_default_auth_reqwest_client(&jwks_url, auth_route_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)?;
|
||||
@@ -285,19 +300,24 @@ where
|
||||
async fn register_task_for_record_with_retries(
|
||||
record: &AgentIdentityAuthRecord,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<String> {
|
||||
let task_registration_url =
|
||||
agent_task_registration_url(agent_identity_authapi_base_url, &record.agent_runtime_id);
|
||||
let client = build_default_auth_reqwest_client(&task_registration_url, auth_route_config)?;
|
||||
retry_registration(|| async {
|
||||
register_task_for_record(record, agent_identity_authapi_base_url).await
|
||||
register_task_for_record(&client, record, agent_identity_authapi_base_url).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn register_task_for_record(
|
||||
client: &reqwest::Client,
|
||||
record: &AgentIdentityAuthRecord,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
) -> std::io::Result<String> {
|
||||
register_agent_task(
|
||||
&build_reqwest_client(),
|
||||
client,
|
||||
agent_identity_authapi_base_url,
|
||||
key_for_record(record),
|
||||
)
|
||||
@@ -370,6 +390,7 @@ mod tests {
|
||||
let auth = AgentIdentityAuth::from_record(
|
||||
agent_identity_record_with_generated_key(),
|
||||
&server.uri(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -414,6 +435,7 @@ mod tests {
|
||||
&jwt,
|
||||
&format!("{}/backend-api", server.uri()),
|
||||
&server.uri(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -456,6 +478,7 @@ mod tests {
|
||||
let auth = AgentIdentityAuth::from_record(
|
||||
agent_identity_record_with_generated_key(),
|
||||
&server.uri(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ async fn login_with_access_token_writes_agent_identity_jwt() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
Some(&chatgpt_base_url),
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("login_with_access_token should succeed");
|
||||
@@ -181,6 +182,7 @@ async fn stored_agent_identity_jwt_keeps_auth_json_unchanged() -> anyhow::Result
|
||||
Some(&chatgpt_base_url),
|
||||
AuthKeyringBackendKind::Direct,
|
||||
Some(&authapi_base_url),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("auth should load");
|
||||
@@ -226,6 +228,7 @@ async fn login_with_access_token_writes_only_personal_access_token() {
|
||||
Some(&allowed_workspaces),
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("personal access token login should succeed");
|
||||
@@ -278,6 +281,7 @@ async fn login_with_access_token_rejects_personal_access_token_workspace_mismatc
|
||||
Some(&allowed_workspaces),
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("personal access token workspace mismatch should fail");
|
||||
@@ -310,6 +314,7 @@ async fn login_with_access_token_rejects_invalid_personal_access_token() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("invalid personal access token should fail");
|
||||
@@ -333,6 +338,7 @@ async fn login_with_access_token_rejects_invalid_jwt() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("invalid access token should fail");
|
||||
@@ -364,6 +370,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("auth should load");
|
||||
@@ -373,6 +380,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
AgentIdentityAuthPolicy::JwtOnly,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await?
|
||||
@@ -403,6 +411,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
AgentIdentityAuthPolicy::ChatGptAuth,
|
||||
Some(&server.uri()),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await?
|
||||
@@ -412,6 +421,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
AgentIdentityAuthPolicy::ChatGptAuth,
|
||||
Some(&server.uri()),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await?
|
||||
@@ -442,6 +452,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("auth should reload");
|
||||
@@ -450,6 +461,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
AgentIdentityAuthPolicy::ChatGptAuth,
|
||||
Some(&server.uri()),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await?
|
||||
@@ -482,6 +494,7 @@ async fn chatgpt_auth_retries_transient_agent_identity_registration() -> anyhow:
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("auth should load");
|
||||
@@ -510,6 +523,7 @@ async fn chatgpt_auth_retries_transient_agent_identity_registration() -> anyhow:
|
||||
AgentIdentityAuthPolicy::ChatGptAuth,
|
||||
Some(&server.uri()),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await?
|
||||
@@ -546,6 +560,7 @@ async fn chatgpt_auth_registration_retry_exhaustion_is_fallback_eligible() -> an
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("auth should load");
|
||||
@@ -563,6 +578,7 @@ async fn chatgpt_auth_registration_retry_exhaustion_is_fallback_eligible() -> an
|
||||
AgentIdentityAuthPolicy::ChatGptAuth,
|
||||
Some(&server.uri()),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await
|
||||
@@ -605,6 +621,7 @@ async fn chatgpt_auth_task_registration_retry_exhaustion_is_fallback_eligible()
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("auth should load");
|
||||
@@ -625,6 +642,7 @@ async fn chatgpt_auth_task_registration_retry_exhaustion_is_fallback_eligible()
|
||||
AgentIdentityAuthPolicy::ChatGptAuth,
|
||||
Some(&server.uri()),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await
|
||||
@@ -659,6 +677,7 @@ async fn chatgpt_auth_non_retryable_registration_error_is_hard_failure() -> anyh
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("auth should load");
|
||||
@@ -676,6 +695,7 @@ async fn chatgpt_auth_non_retryable_registration_error_is_hard_failure() -> anyh
|
||||
AgentIdentityAuthPolicy::ChatGptAuth,
|
||||
Some(&server.uri()),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.await
|
||||
@@ -717,6 +737,7 @@ async fn agent_identity_jwt_task_registration_retry_exhaustion_is_strict() -> an
|
||||
&agent_identity,
|
||||
Some(&chatgpt_base_url),
|
||||
&authapi_base_url,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("agent identity jwt task retry exhaustion should fail");
|
||||
@@ -747,6 +768,7 @@ async fn login_with_access_token_rejects_unsigned_jwt() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
Some(&chatgpt_base_url),
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("unsigned access token should fail");
|
||||
@@ -768,6 +790,7 @@ async fn missing_auth_json_returns_none() {
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("call should succeed");
|
||||
@@ -797,6 +820,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -858,6 +882,7 @@ async fn loads_api_key_from_auth_json() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -908,6 +933,7 @@ async fn unauthorized_recovery_reports_mode_and_step_names() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
let managed = UnauthorizedRecovery {
|
||||
@@ -952,6 +978,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -972,6 +999,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("updated auth should parse");
|
||||
@@ -1277,6 +1305,7 @@ async fn build_config(
|
||||
forced_login_method,
|
||||
forced_chatgpt_workspace_id,
|
||||
chatgpt_base_url: None,
|
||||
auth_route_config: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,6 +1388,7 @@ async fn load_auth_reads_access_token_from_env() {
|
||||
Some(&chatgpt_base_url),
|
||||
AuthKeyringBackendKind::Direct,
|
||||
Some(&authapi_base_url),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("env auth should load")
|
||||
@@ -1406,6 +1436,7 @@ async fn load_auth_reads_personal_access_token_from_env() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("env auth should load")
|
||||
@@ -1459,6 +1490,7 @@ async fn auth_manager_rejects_env_personal_access_token_workspace_mismatch() {
|
||||
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1498,6 +1530,7 @@ async fn auth_manager_rejects_stored_personal_access_token_workspace_mismatch()
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("personal access token login should succeed");
|
||||
@@ -1509,6 +1542,7 @@ async fn auth_manager_rejects_stored_personal_access_token_workspace_mismatch()
|
||||
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1542,6 +1576,7 @@ async fn personal_access_token_does_not_offer_unauthorized_recovery() {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -1574,6 +1609,7 @@ async fn load_auth_keeps_codex_api_key_env_precedence() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("env auth should load")
|
||||
@@ -1670,6 +1706,7 @@ async fn enforce_login_restrictions_logs_out_for_personal_access_token_workspace
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("personal access token login should succeed");
|
||||
@@ -1681,6 +1718,7 @@ async fn enforce_login_restrictions_logs_out_for_personal_access_token_workspace
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
chatgpt_base_url: None,
|
||||
auth_route_config: None,
|
||||
};
|
||||
|
||||
let err = super::enforce_login_restrictions(&config)
|
||||
@@ -1804,6 +1842,7 @@ async fn enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismat
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
chatgpt_base_url: Some(chatgpt_base_url),
|
||||
auth_route_config: None,
|
||||
};
|
||||
|
||||
let err = super::enforce_login_restrictions_with_agent_identity_authapi_base_url(
|
||||
@@ -2056,6 +2095,7 @@ async fn assert_agent_identity_plan_alias(
|
||||
&jwt,
|
||||
Some(&chatgpt_base_url),
|
||||
&authapi_base_url,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("agent identity auth");
|
||||
@@ -2087,6 +2127,7 @@ async fn plan_type_maps_known_plan() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -2118,6 +2159,7 @@ async fn plan_type_maps_self_serve_business_usage_based_plan() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -2152,6 +2194,7 @@ async fn plan_type_maps_enterprise_cbp_usage_based_plan() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -2186,6 +2229,7 @@ async fn plan_type_maps_unknown_to_unknown() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -2217,6 +2261,7 @@ async fn missing_plan_type_maps_to_unknown() {
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
|
||||
@@ -63,6 +63,7 @@ async fn login_with_bedrock_api_key_replaces_openai_auth() -> anyhow::Result<()>
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -111,6 +112,7 @@ async fn logout_removes_bedrock_auth() -> anyhow::Result<()> {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -135,6 +137,7 @@ async fn bedrock_only_auth_storage_creates_primary_auth() -> anyhow::Result<()>
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
//! workspace.
|
||||
|
||||
use codex_client::BuildCustomCaTransportError;
|
||||
use codex_client::BuildRouteAwareHttpClientError;
|
||||
use codex_client::ClientRouteClass;
|
||||
use codex_client::CodexHttpClient;
|
||||
pub use codex_client::CodexRequestBuilder;
|
||||
use codex_client::build_reqwest_client_for_route;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_client::with_chatgpt_cloudflare_cookie_store;
|
||||
use codex_terminal_detection::user_agent;
|
||||
@@ -17,6 +20,8 @@ use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
|
||||
/// Set this to add a suffix to the User-Agent string.
|
||||
///
|
||||
/// It is not ideal that we're using a global singleton for this.
|
||||
@@ -189,6 +194,9 @@ fn sanitize_user_agent(candidate: String, fallback: &str) -> String {
|
||||
}
|
||||
|
||||
/// Create an HTTP client with default `originator` and `User-Agent` headers set.
|
||||
///
|
||||
/// This supported default path preserves reqwest's existing proxy behavior and does not opt into
|
||||
/// Codex's route-aware system/PAC resolution.
|
||||
pub fn create_client() -> CodexHttpClient {
|
||||
let inner = build_reqwest_client();
|
||||
CodexHttpClient::new(inner)
|
||||
@@ -200,6 +208,10 @@ pub fn create_client() -> CodexHttpClient {
|
||||
/// policy, then layers in shared custom CA handling from `CODEX_CA_CERTIFICATE` /
|
||||
/// `SSL_CERT_FILE`. The function remains infallible for compatibility with existing call sites, so
|
||||
/// a custom-CA or builder failure is logged and falls back to `reqwest::Client::new()`.
|
||||
///
|
||||
/// This supported default path preserves reqwest's existing proxy behavior and does not opt into
|
||||
/// Codex's route-aware system/PAC resolution. Auth callers with route settings must use
|
||||
/// `build_default_auth_reqwest_client` or `create_default_auth_client`.
|
||||
pub fn build_reqwest_client() -> reqwest::Client {
|
||||
try_build_reqwest_client().unwrap_or_else(|error| {
|
||||
tracing::warn!(error = %error, "failed to build default reqwest client");
|
||||
@@ -220,13 +232,58 @@ pub fn build_reqwest_client() -> reqwest::Client {
|
||||
/// Callers that need a structured CA-loading failure instead of the legacy logged fallback can use
|
||||
/// this method directly.
|
||||
pub fn try_build_reqwest_client() -> Result<reqwest::Client, BuildCustomCaTransportError> {
|
||||
build_reqwest_client_with_custom_ca(default_reqwest_client_builder())
|
||||
}
|
||||
|
||||
fn default_reqwest_client_builder() -> reqwest::ClientBuilder {
|
||||
let mut builder = reqwest::Client::builder().default_headers(default_headers());
|
||||
if is_sandboxed() {
|
||||
builder = builder.no_proxy();
|
||||
}
|
||||
builder = with_chatgpt_cloudflare_cookie_store(builder);
|
||||
with_chatgpt_cloudflare_cookie_store(builder)
|
||||
}
|
||||
|
||||
build_reqwest_client_with_custom_ca(builder)
|
||||
/// Builds a raw reqwest client for an auth endpoint without Codex default headers.
|
||||
pub(crate) fn build_raw_auth_reqwest_client(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
build_reqwest_client_for_route(
|
||||
reqwest::Client::builder(),
|
||||
endpoint,
|
||||
ClientRouteClass::Auth,
|
||||
auth_route_config.map(AuthRouteConfig::route_config),
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the default Codex reqwest client for an auth endpoint.
|
||||
pub(crate) fn build_default_auth_reqwest_client(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
let Some(route_config) = auth_route_config.map(AuthRouteConfig::route_config) else {
|
||||
return Ok(build_reqwest_client());
|
||||
};
|
||||
|
||||
if is_sandboxed() {
|
||||
// Preserve the sandbox's existing no-proxy policy; sandboxed command egress is routed
|
||||
// separately through network-proxy.
|
||||
return Ok(build_reqwest_client());
|
||||
}
|
||||
build_reqwest_client_for_route(
|
||||
default_reqwest_client_builder(),
|
||||
endpoint,
|
||||
ClientRouteClass::Auth,
|
||||
Some(route_config),
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the default Codex HTTP client wrapper for an auth endpoint.
|
||||
pub(crate) fn create_default_auth_client(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<CodexHttpClient, BuildRouteAwareHttpClientError> {
|
||||
build_default_auth_reqwest_client(endpoint, auth_route_config).map(CodexHttpClient::new)
|
||||
}
|
||||
|
||||
pub fn default_headers() -> HeaderMap {
|
||||
|
||||
@@ -49,6 +49,8 @@ use crate::auth::storage::AuthStorageBackend;
|
||||
use crate::auth::storage::create_auth_storage;
|
||||
use crate::auth::util::try_parse_error_message;
|
||||
use crate::default_client::create_client;
|
||||
use crate::default_client::create_default_auth_client;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
use crate::token_data::TokenData;
|
||||
use crate::token_data::parse_chatgpt_jwt_claims;
|
||||
use crate::token_data::parse_jwt_expiration;
|
||||
@@ -237,6 +239,7 @@ impl CodexAuth {
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
agent_identity_authapi_base_url: Option<&str>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let auth_mode = auth_dot_json.resolved_mode();
|
||||
if auth_mode == ApiAuthMode::ApiKey {
|
||||
@@ -263,14 +266,18 @@ impl CodexAuth {
|
||||
&jwt,
|
||||
&base_url,
|
||||
agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
return Ok(Self::AgentIdentity(auth));
|
||||
}
|
||||
AgentIdentityStorage::Record(record) => {
|
||||
let auth =
|
||||
AgentIdentityAuth::from_record(record, agent_identity_authapi_base_url)
|
||||
.await?;
|
||||
let auth = AgentIdentityAuth::from_record(
|
||||
record,
|
||||
agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
return Ok(Self::AgentIdentity(auth));
|
||||
}
|
||||
}
|
||||
@@ -281,7 +288,8 @@ impl CodexAuth {
|
||||
"personal access token auth is missing a personal access token.",
|
||||
));
|
||||
};
|
||||
return Self::from_personal_access_token(personal_access_token).await;
|
||||
return Self::from_personal_access_token(personal_access_token, auth_route_config)
|
||||
.await;
|
||||
}
|
||||
if auth_mode == ApiAuthMode::BedrockApiKey {
|
||||
let Some(auth) = auth_dot_json.bedrock_api_key else {
|
||||
@@ -293,9 +301,10 @@ impl CodexAuth {
|
||||
}
|
||||
|
||||
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
|
||||
let client = create_default_auth_client(&refresh_token_endpoint(), auth_route_config)?;
|
||||
let state = ChatgptAuthState {
|
||||
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
|
||||
client: create_client(),
|
||||
client,
|
||||
};
|
||||
|
||||
match auth_mode {
|
||||
@@ -324,6 +333,7 @@ impl CodexAuth {
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Option<Self>> {
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(chatgpt_base_url).ok();
|
||||
@@ -335,6 +345,7 @@ impl CodexAuth {
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
agent_identity_authapi_base_url.as_deref(),
|
||||
auth_route_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -342,12 +353,14 @@ impl CodexAuth {
|
||||
pub async fn from_agent_identity_jwt(
|
||||
jwt: &str,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let agent_identity_authapi_base_url = agent_identity_authapi_base_url(chatgpt_base_url)?;
|
||||
Self::from_agent_identity_jwt_with_authapi_base_url(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
&agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -356,19 +369,29 @@ impl CodexAuth {
|
||||
jwt: &str,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let base_url = chatgpt_base_url
|
||||
.unwrap_or(ChatGptEnvironment::default().chatgpt_base_url())
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
Ok(Self::AgentIdentity(
|
||||
AgentIdentityAuth::from_jwt(jwt, &base_url, agent_identity_authapi_base_url).await?,
|
||||
AgentIdentityAuth::from_jwt(
|
||||
jwt,
|
||||
&base_url,
|
||||
agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn from_personal_access_token(access_token: &str) -> std::io::Result<Self> {
|
||||
pub async fn from_personal_access_token(
|
||||
access_token: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
Ok(Self::PersonalAccessToken(
|
||||
PersonalAccessTokenAuth::load(access_token).await?,
|
||||
PersonalAccessTokenAuth::load(access_token, auth_route_config).await?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -568,6 +591,7 @@ impl CodexAuth {
|
||||
policy: AgentIdentityAuthPolicy,
|
||||
agent_identity_authapi_base_url: Option<&str>,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
session_source: SessionSource,
|
||||
) -> std::io::Result<Option<AgentIdentityAuth>> {
|
||||
match self {
|
||||
@@ -583,6 +607,7 @@ impl CodexAuth {
|
||||
self.ensure_managed_chatgpt_agent_identity(
|
||||
require_agent_identity_authapi_base_url(agent_identity_authapi_base_url)?,
|
||||
forced_chatgpt_workspace_id,
|
||||
auth_route_config,
|
||||
session_source,
|
||||
)
|
||||
.await
|
||||
@@ -595,6 +620,7 @@ impl CodexAuth {
|
||||
&self,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
session_source: SessionSource,
|
||||
) -> std::io::Result<AgentIdentityAuth> {
|
||||
let binding =
|
||||
@@ -607,9 +633,13 @@ impl CodexAuth {
|
||||
&& record_matches_managed_chatgpt_binding(&record, &binding)
|
||||
{
|
||||
let should_persist = record_needs_task_registration(&record);
|
||||
let auth = AgentIdentityAuth::from_record(record, agent_identity_authapi_base_url)
|
||||
.await
|
||||
.map_err(|err| classify_bootstrap_error("agent task registration", err))?;
|
||||
let auth = AgentIdentityAuth::from_record(
|
||||
record,
|
||||
agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| classify_bootstrap_error("agent task registration", err))?;
|
||||
if should_persist {
|
||||
self.persist_managed_chatgpt_agent_identity_record(auth.record().clone())?;
|
||||
}
|
||||
@@ -620,6 +650,7 @@ impl CodexAuth {
|
||||
binding,
|
||||
agent_identity_authapi_base_url,
|
||||
session_source,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
self.persist_managed_chatgpt_agent_identity_record(auth.record().clone())?;
|
||||
@@ -789,6 +820,7 @@ pub async fn logout_with_revoke(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<bool> {
|
||||
let auth_dot_json = match load_auth_dot_json(
|
||||
codex_home,
|
||||
@@ -801,7 +833,7 @@ pub async fn logout_with_revoke(
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Err(err) = revoke_auth_tokens(auth_dot_json.as_ref()).await {
|
||||
if let Err(err) = revoke_auth_tokens(auth_dot_json.as_ref(), auth_route_config).await {
|
||||
tracing::warn!("failed to revoke auth tokens during logout: {err}");
|
||||
}
|
||||
logout_all_stores(
|
||||
@@ -843,10 +875,11 @@ pub async fn login_with_access_token(
|
||||
forced_chatgpt_workspace_id: Option<&[String]>,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_dot_json = match classify_codex_access_token(access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
let auth = PersonalAccessTokenAuth::load(access_token).await?;
|
||||
let auth = PersonalAccessTokenAuth::load(access_token, auth_route_config).await?;
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
|
||||
AuthDotJson {
|
||||
// Infer PAT auth from the credential field so older Codex builds can still
|
||||
@@ -865,7 +898,7 @@ pub async fn login_with_access_token(
|
||||
.unwrap_or(ChatGptEnvironment::default().chatgpt_base_url())
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
verified_record_from_jwt(jwt, &base_url).await?;
|
||||
verified_record_from_jwt(jwt, &base_url, auth_route_config).await?;
|
||||
AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::AgentIdentity),
|
||||
openai_api_key: None,
|
||||
@@ -954,8 +987,10 @@ pub struct AuthConfig {
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
pub auth_route_config: Option<AuthRouteConfig>,
|
||||
}
|
||||
|
||||
/// Enforces configured login restrictions using auth-owned HTTP settings.
|
||||
pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(config.chatgpt_base_url.as_deref()).ok();
|
||||
@@ -978,6 +1013,7 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url(
|
||||
config.chatgpt_base_url.as_deref(),
|
||||
config.keyring_backend_kind,
|
||||
agent_identity_authapi_base_url,
|
||||
config.auth_route_config.as_ref(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
@@ -1113,6 +1149,7 @@ fn logout_all_stores(
|
||||
Ok(removed_ephemeral || removed_managed)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn load_auth(
|
||||
codex_home: &Path,
|
||||
enable_codex_api_key_env: bool,
|
||||
@@ -1121,6 +1158,7 @@ async fn load_auth(
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
agent_identity_authapi_base_url: Option<&str>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Option<CodexAuth>> {
|
||||
// API key via env var takes precedence over any other auth method.
|
||||
if enable_codex_api_key_env && let Some(api_key) = read_codex_api_key_from_env() {
|
||||
@@ -1142,6 +1180,7 @@ async fn load_auth(
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
if let CodexAuth::PersonalAccessToken(auth) = &auth {
|
||||
@@ -1153,7 +1192,7 @@ async fn load_auth(
|
||||
if let Some(access_token) = read_codex_access_token_from_env() {
|
||||
return match classify_codex_access_token(&access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
let auth = PersonalAccessTokenAuth::load(access_token).await?;
|
||||
let auth = PersonalAccessTokenAuth::load(access_token, auth_route_config).await?;
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
|
||||
Ok(Some(CodexAuth::PersonalAccessToken(auth)))
|
||||
}
|
||||
@@ -1162,6 +1201,7 @@ async fn load_auth(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
require_agent_identity_authapi_base_url(agent_identity_authapi_base_url)?,
|
||||
auth_route_config,
|
||||
)
|
||||
}
|
||||
.await
|
||||
@@ -1192,6 +1232,7 @@ async fn load_auth(
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
agent_identity_authapi_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
if let CodexAuth::PersonalAccessToken(auth) = &auth {
|
||||
@@ -1237,7 +1278,6 @@ async fn request_chatgpt_token_refresh(
|
||||
grant_type: "refresh_token",
|
||||
refresh_token,
|
||||
};
|
||||
|
||||
let endpoint = refresh_token_endpoint();
|
||||
|
||||
// Use shared client factory to include standard headers
|
||||
@@ -1702,6 +1742,7 @@ pub struct AuthManager {
|
||||
refresh_lock: Semaphore,
|
||||
agent_identity_lock: Semaphore,
|
||||
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
}
|
||||
|
||||
/// Configuration view required to construct a shared [`AuthManager`].
|
||||
@@ -1725,6 +1766,9 @@ pub trait AuthManagerConfig {
|
||||
|
||||
/// Returns the ChatGPT backend base URL used for first-party backend authorization.
|
||||
fn chatgpt_base_url(&self) -> String;
|
||||
|
||||
/// Returns route-selection settings for auth-owned clients.
|
||||
fn auth_route_config(&self) -> Option<AuthRouteConfig>;
|
||||
}
|
||||
|
||||
impl Debug for AuthManager {
|
||||
@@ -1743,6 +1787,7 @@ impl Debug for AuthManager {
|
||||
&self.forced_chatgpt_workspace_id,
|
||||
)
|
||||
.field("chatgpt_base_url", &self.chatgpt_base_url)
|
||||
.field("auth_route_config", &self.auth_route_config)
|
||||
.field("has_external_auth", &self.has_external_auth())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
@@ -1764,6 +1809,7 @@ impl AuthManager {
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
) -> Self {
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(chatgpt_base_url.as_deref()).ok();
|
||||
@@ -1775,6 +1821,7 @@ impl AuthManager {
|
||||
chatgpt_base_url.as_deref(),
|
||||
keyring_backend_kind,
|
||||
agent_identity_authapi_base_url.as_deref(),
|
||||
auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
@@ -1796,6 +1843,7 @@ impl AuthManager {
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1820,6 +1868,7 @@ impl AuthManager {
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1843,6 +1892,7 @@ impl AuthManager {
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1874,6 +1924,7 @@ impl AuthManager {
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1897,6 +1948,7 @@ impl AuthManager {
|
||||
external_auth: RwLock::new(Some(
|
||||
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
|
||||
)),
|
||||
auth_route_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1958,6 +2010,7 @@ impl AuthManager {
|
||||
policy,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
self.forced_chatgpt_workspace_id(),
|
||||
self.auth_route_config.as_ref(),
|
||||
session_source,
|
||||
)
|
||||
.await;
|
||||
@@ -1966,6 +2019,7 @@ impl AuthManager {
|
||||
policy,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
self.forced_chatgpt_workspace_id(),
|
||||
self.auth_route_config.as_ref(),
|
||||
session_source,
|
||||
)
|
||||
.await
|
||||
@@ -2074,6 +2128,7 @@ impl AuthManager {
|
||||
self.chatgpt_base_url.as_deref(),
|
||||
self.keyring_backend_kind,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
self.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
@@ -2149,6 +2204,7 @@ impl AuthManager {
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(
|
||||
Self::new(
|
||||
@@ -2158,6 +2214,7 @@ impl AuthManager {
|
||||
forced_chatgpt_workspace_id,
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
auth_route_config,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
@@ -2175,6 +2232,7 @@ impl AuthManager {
|
||||
config.forced_chatgpt_workspace_id(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.auth_route_config(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2328,7 +2386,9 @@ impl AuthManager {
|
||||
let auth_dot_json = self
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.get_current_auth_json());
|
||||
if let Err(err) = revoke_auth_tokens(auth_dot_json.as_ref()).await {
|
||||
if let Err(err) =
|
||||
revoke_auth_tokens(auth_dot_json.as_ref(), self.auth_route_config.as_ref()).await
|
||||
{
|
||||
tracing::warn!("failed to revoke auth tokens during logout: {err}");
|
||||
}
|
||||
let result = logout_all_stores(
|
||||
|
||||
@@ -5,7 +5,8 @@ use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
|
||||
use crate::default_client::create_client;
|
||||
use crate::default_client::create_default_auth_client;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
|
||||
const PROD_AUTHAPI_BASE_URL: &str = "https://auth.openai.com/api/accounts";
|
||||
const CODEX_AUTHAPI_BASE_URL_ENV_VAR: &str = "CODEX_AUTHAPI_BASE_URL";
|
||||
@@ -36,13 +37,18 @@ impl fmt::Debug for PersonalAccessTokenAuth {
|
||||
}
|
||||
|
||||
impl PersonalAccessTokenAuth {
|
||||
pub(super) async fn load(access_token: &str) -> std::io::Result<Self> {
|
||||
pub(super) async fn load(
|
||||
access_token: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let authapi_base_url = env::var(CODEX_AUTHAPI_BASE_URL_ENV_VAR)
|
||||
.ok()
|
||||
.map(|base_url| base_url.trim().trim_end_matches('/').to_string())
|
||||
.filter(|base_url| !base_url.is_empty())
|
||||
.unwrap_or_else(|| PROD_AUTHAPI_BASE_URL.to_string());
|
||||
hydrate_personal_access_token(&create_client(), &authapi_base_url, access_token).await
|
||||
let endpoint = whoami_endpoint(&authapi_base_url);
|
||||
let client = create_default_auth_client(&endpoint, auth_route_config)?;
|
||||
hydrate_personal_access_token(&client, &endpoint, access_token).await
|
||||
}
|
||||
|
||||
pub fn access_token(&self) -> &str {
|
||||
@@ -72,12 +78,11 @@ impl PersonalAccessTokenAuth {
|
||||
|
||||
async fn hydrate_personal_access_token(
|
||||
client: &CodexHttpClient,
|
||||
authapi_base_url: &str,
|
||||
endpoint: &str,
|
||||
access_token: &str,
|
||||
) -> std::io::Result<PersonalAccessTokenAuth> {
|
||||
let endpoint = format!("{}{WHOAMI_PATH}", authapi_base_url.trim_end_matches('/'));
|
||||
let response = client
|
||||
.get(&endpoint)
|
||||
.get(endpoint)
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await
|
||||
@@ -107,6 +112,10 @@ async fn hydrate_personal_access_token(
|
||||
})
|
||||
}
|
||||
|
||||
fn whoami_endpoint(authapi_base_url: &str) -> String {
|
||||
format!("{}{WHOAMI_PATH}", authapi_base_url.trim_end_matches('/'))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "personal_access_token_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::default_client::create_client;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use wiremock::Mock;
|
||||
@@ -29,7 +30,8 @@ async fn hydrate_sends_bearer_token_and_preserves_metadata() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let auth = hydrate_personal_access_token(&create_client(), &server.uri(), "at-example")
|
||||
let endpoint = whoami_endpoint(&server.uri());
|
||||
let auth = hydrate_personal_access_token(&create_client(), &endpoint, "at-example")
|
||||
.await
|
||||
.expect("personal access token hydration should succeed");
|
||||
|
||||
@@ -59,7 +61,8 @@ async fn hydrate_rejects_missing_email() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let err = hydrate_personal_access_token(&create_client(), &server.uri(), "at-example")
|
||||
let endpoint = whoami_endpoint(&server.uri());
|
||||
let err = hydrate_personal_access_token(&create_client(), &endpoint, "at-example")
|
||||
.await
|
||||
.expect_err("personal access token hydration should reject missing email");
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ use super::manager::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use super::manager::oauth_client_id;
|
||||
use super::storage::AuthDotJson;
|
||||
use super::util::try_parse_error_message;
|
||||
use crate::default_client::create_client;
|
||||
use crate::default_client::create_default_auth_client;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
use crate::token_data::TokenData;
|
||||
|
||||
const REVOKE_HTTP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -53,13 +54,14 @@ struct RevokeTokenRequest<'a> {
|
||||
|
||||
pub(super) async fn revoke_auth_tokens(
|
||||
auth_dot_json: Option<&AuthDotJson>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let Some((token, kind)) = auth_dot_json.and_then(revocable_token) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let client = create_client();
|
||||
let endpoint = revoke_token_endpoint();
|
||||
let client = create_default_auth_client(&endpoint, auth_route_config)?;
|
||||
revoke_oauth_token(&client, endpoint.as_str(), token, kind, REVOKE_HTTP_TIMEOUT).await
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ use serde::de::{self};
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::default_client::build_raw_auth_reqwest_client;
|
||||
use crate::pkce::PkceCodes;
|
||||
use crate::server::ServerOptions;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use std::io;
|
||||
|
||||
const ANSI_BLUE: &str = "\x1b[94m";
|
||||
@@ -157,8 +157,10 @@ fn print_device_code_prompt(verification_url: &str, code: &str) {
|
||||
}
|
||||
|
||||
pub async fn request_device_code(opts: &ServerOptions) -> std::io::Result<DeviceCode> {
|
||||
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
||||
let base_url = opts.issuer.trim_end_matches('/');
|
||||
// The route selected for the issuer is reused for all device-auth endpoint paths; the endpoint
|
||||
// paths are not resolved separately.
|
||||
let client = build_raw_auth_reqwest_client(base_url, opts.auth_route_config.as_ref())?;
|
||||
let api_base_url = format!("{base_url}/api/accounts");
|
||||
let uc = request_user_code(&client, &api_base_url, &opts.client_id).await?;
|
||||
|
||||
@@ -174,8 +176,8 @@ pub async fn complete_device_code_login(
|
||||
opts: ServerOptions,
|
||||
device_code: DeviceCode,
|
||||
) -> std::io::Result<()> {
|
||||
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
||||
let base_url = opts.issuer.trim_end_matches('/');
|
||||
let client = build_raw_auth_reqwest_client(base_url, opts.auth_route_config.as_ref())?;
|
||||
let api_base_url = format!("{base_url}/api/accounts");
|
||||
|
||||
let code_resp = poll_for_token(
|
||||
@@ -199,6 +201,7 @@ pub async fn complete_device_code_login(
|
||||
&redirect_uri,
|
||||
&pkce,
|
||||
&code_resp.authorization_code,
|
||||
opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| std::io::Error::other(format!("device code exchange failed: {err}")))?;
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod auth_env_telemetry;
|
||||
pub mod token_data;
|
||||
|
||||
mod device_code_auth;
|
||||
mod outbound_proxy;
|
||||
mod pkce;
|
||||
mod server;
|
||||
|
||||
@@ -53,4 +54,5 @@ pub use auth::read_openai_api_key_from_env;
|
||||
pub use auth::save_auth;
|
||||
pub use auth_env_telemetry::AuthEnvTelemetry;
|
||||
pub use auth_env_telemetry::collect_auth_env_telemetry;
|
||||
pub use outbound_proxy::AuthRouteConfig;
|
||||
pub use token_data::TokenData;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use codex_client::OutboundProxyConfig;
|
||||
|
||||
/// Auth-layer adapter around client-owned proxy policy.
|
||||
///
|
||||
/// `AuthConfig` carries this value while endpoint resolution and platform details remain in the
|
||||
/// client layer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AuthRouteConfig {
|
||||
route_config: OutboundProxyConfig,
|
||||
}
|
||||
|
||||
impl AuthRouteConfig {
|
||||
pub fn respect_system_proxy() -> Self {
|
||||
Self {
|
||||
route_config: OutboundProxyConfig::respect_system_proxy(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn route_config(&self) -> &OutboundProxyConfig {
|
||||
&self.route_config
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,9 @@ use std::time::Duration;
|
||||
use crate::auth::AuthDotJson;
|
||||
use crate::auth::AuthKeyringBackendKind;
|
||||
use crate::auth::save_auth;
|
||||
use crate::default_client::build_raw_auth_reqwest_client;
|
||||
use crate::default_client::originator;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
use crate::pkce::PkceCodes;
|
||||
use crate::pkce::generate_pkce;
|
||||
use crate::token_data::TokenData;
|
||||
@@ -35,7 +37,6 @@ use crate::token_data::parse_chatgpt_jwt_claims;
|
||||
use base64::Engine;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_utils_template::Template;
|
||||
use rand::RngCore;
|
||||
@@ -71,6 +72,7 @@ pub struct ServerOptions {
|
||||
pub codex_streamlined_login: bool,
|
||||
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
pub auth_keyring_backend_kind: AuthKeyringBackendKind,
|
||||
pub auth_route_config: Option<AuthRouteConfig>,
|
||||
}
|
||||
|
||||
impl ServerOptions {
|
||||
@@ -81,6 +83,7 @@ impl ServerOptions {
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
@@ -93,6 +96,7 @@ impl ServerOptions {
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_credentials_store_mode,
|
||||
auth_keyring_backend_kind,
|
||||
auth_route_config,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,8 +340,15 @@ async fn process_request(
|
||||
}
|
||||
};
|
||||
|
||||
match exchange_code_for_tokens(&opts.issuer, &opts.client_id, redirect_uri, pkce, &code)
|
||||
.await
|
||||
match exchange_code_for_tokens(
|
||||
&opts.issuer,
|
||||
&opts.client_id,
|
||||
redirect_uri,
|
||||
pkce,
|
||||
&code,
|
||||
opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tokens) => {
|
||||
if let Err(message) = ensure_workspace_allowed(
|
||||
@@ -353,9 +364,14 @@ async fn process_request(
|
||||
);
|
||||
}
|
||||
// Obtain API key via token-exchange and persist
|
||||
let api_key = obtain_api_key(&opts.issuer, &opts.client_id, &tokens.id_token)
|
||||
.await
|
||||
.ok();
|
||||
let api_key = obtain_api_key(
|
||||
&opts.issuer,
|
||||
&opts.client_id,
|
||||
&tokens.id_token,
|
||||
opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
if let Err(err) = persist_tokens_async(
|
||||
&opts.codex_home,
|
||||
api_key.clone(),
|
||||
@@ -719,6 +735,7 @@ pub(crate) async fn exchange_code_for_tokens(
|
||||
redirect_uri: &str,
|
||||
pkce: &PkceCodes,
|
||||
code: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> io::Result<ExchangedTokens> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TokenResponse {
|
||||
@@ -727,7 +744,9 @@ pub(crate) async fn exchange_code_for_tokens(
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
||||
// The route selected for the issuer is reused for token exchange; the token endpoint path is
|
||||
// not resolved separately.
|
||||
let client = build_raw_auth_reqwest_client(issuer.trim_end_matches('/'), auth_route_config)?;
|
||||
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
|
||||
info!(
|
||||
issuer = %sanitize_url_for_logging(issuer),
|
||||
@@ -1129,14 +1148,15 @@ pub(crate) async fn obtain_api_key(
|
||||
issuer: &str,
|
||||
client_id: &str,
|
||||
id_token: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> io::Result<String> {
|
||||
// Token exchange for an API key access token
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ExchangeResp {
|
||||
access_token: String,
|
||||
}
|
||||
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
||||
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
|
||||
let client = build_raw_auth_reqwest_client(&token_endpoint, auth_route_config)?;
|
||||
let resp = client
|
||||
.post(token_endpoint)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
@@ -1220,6 +1220,7 @@ impl RefreshTokenTestContext {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ fn server_opts(
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
cli_auth_credentials_store_mode,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
@@ -277,6 +278,7 @@ async fn device_code_login_integration_persists_without_api_key_on_exchange_fail
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
@@ -332,6 +334,7 @@ async fn device_code_login_integration_handles_error_payload() -> anyhow::Result
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
|
||||
@@ -122,6 +122,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
|
||||
let opts = ServerOptions {
|
||||
codex_home: server_home,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: 0,
|
||||
@@ -185,6 +186,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> {
|
||||
let opts = ServerOptions {
|
||||
codex_home: server_home,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: 0,
|
||||
@@ -226,6 +228,7 @@ async fn login_server_includes_forced_workspaces_as_one_query_param() -> Result<
|
||||
let opts = ServerOptions {
|
||||
codex_home,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: 0,
|
||||
@@ -268,6 +271,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
|
||||
let opts = ServerOptions {
|
||||
codex_home: codex_home.clone(),
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: 0,
|
||||
@@ -329,6 +333,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error()
|
||||
let opts = ServerOptions {
|
||||
codex_home: codex_home.clone(),
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: 0,
|
||||
@@ -398,6 +403,7 @@ async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<
|
||||
let opts = ServerOptions {
|
||||
codex_home: codex_home.clone(),
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: 0,
|
||||
@@ -507,6 +513,7 @@ async fn falls_back_to_registered_fallback_port_when_default_port_is_in_use() ->
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
@@ -545,6 +552,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
let first_opts = ServerOptions {
|
||||
codex_home: first_codex_home,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer: issuer.clone(),
|
||||
port: 0,
|
||||
@@ -567,6 +575,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
let second_opts = ServerOptions {
|
||||
codex_home: second_codex_home,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: login_port,
|
||||
|
||||
@@ -61,6 +61,7 @@ async fn logout_with_revoke_revokes_refresh_token_then_removes_auth() -> Result<
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -119,6 +120,7 @@ async fn logout_with_revoke_uses_stored_auth_when_access_token_env_is_set() -> R
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -161,6 +163,7 @@ async fn logout_with_revoke_removes_auth_when_revoke_fails() -> Result<()> {
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -204,6 +207,7 @@ async fn auth_manager_logout_with_revoke_uses_cached_auth() -> Result<()> {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
save_auth(
|
||||
|
||||
@@ -232,6 +232,7 @@ c2ln",
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("auth should load")
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::ConfigTomlLoadResult;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_route_config;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
use crate::legacy_core::format_exec_policy_error_with_source;
|
||||
@@ -957,6 +958,14 @@ pub async fn run_main(
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let auth_route_config = resolve_bootstrap_auth_route_config(
|
||||
bootstrap_config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
@@ -965,6 +974,7 @@ pub async fn run_main(
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1154,6 +1164,7 @@ pub async fn run_main(
|
||||
}
|
||||
|
||||
if !app_server_target.uses_remote_workspace() {
|
||||
let auth_route_config = config.auth_route_config();
|
||||
#[allow(clippy::print_stderr)]
|
||||
if let Err(err) = enforce_login_restrictions(&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
@@ -1162,6 +1173,7 @@ pub async fn run_main(
|
||||
forced_login_method: config.forced_login_method,
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
|
||||
auth_route_config,
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -1429,6 +1441,7 @@ async fn run_ratatui_app(
|
||||
initial_config.cli_auth_credentials_store_mode,
|
||||
initial_config.auth_keyring_backend_kind(),
|
||||
initial_config.chatgpt_base_url.clone(),
|
||||
initial_config.auth_route_config(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1040,6 +1040,7 @@ mod tests {
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
"https://chatgpt.com/backend-api/".to_string(),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await,
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_route_config;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
use codex_app_server_protocol::Thread as AppServerThread;
|
||||
@@ -329,12 +330,21 @@ async fn start_app_server_for_archive_command(
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let auth_route_config = resolve_bootstrap_auth_route_config(
|
||||
config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config_toml.cli_auth_credentials_store.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user