mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Add shared auth system proxy contract
This commit is contained in:
@@ -335,6 +335,12 @@ impl AccountRequestProcessor {
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
config.cli_auth_credentials_store_mode,
|
||||
)
|
||||
.with_auth_route_config(
|
||||
config
|
||||
.system_proxy
|
||||
.as_ref()
|
||||
.map(codex_login::auth_route_config_from_system_proxy_config),
|
||||
)
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
let opts = {
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::path::PathBuf;
|
||||
pub use debug_sandbox::run_command_under_landlock;
|
||||
pub use debug_sandbox::run_command_under_seatbelt;
|
||||
pub use debug_sandbox::run_command_under_windows_sandbox;
|
||||
pub use login::login_server_options_from_config;
|
||||
pub use login::read_access_token_from_stdin;
|
||||
pub use login::read_api_key_from_stdin;
|
||||
pub use login::run_login_status;
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::login_with_access_token;
|
||||
use codex_login::auth_route_config_from_system_proxy_config;
|
||||
use codex_login::login_with_access_token_with_auth_route_config;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::logout_with_revoke;
|
||||
use codex_login::run_device_code_login;
|
||||
use codex_login::run_login_server;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
@@ -113,17 +114,34 @@ fn print_login_server_start(actual_port: u16, auth_url: &str) {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn login_server_options_from_config(config: &Config) -> ServerOptions {
|
||||
ServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
CLIENT_ID.to_string(),
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
config.cli_auth_credentials_store_mode,
|
||||
)
|
||||
.with_auth_route_config(
|
||||
config
|
||||
.system_proxy
|
||||
.as_ref()
|
||||
.map(auth_route_config_from_system_proxy_config),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn login_with_chatgpt(
|
||||
codex_home: PathBuf,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
system_proxy: Option<&codex_config::types::SystemProxyFeatureConfigToml>,
|
||||
) -> std::io::Result<()> {
|
||||
let opts = ServerOptions::new(
|
||||
codex_home,
|
||||
CLIENT_ID.to_string(),
|
||||
forced_chatgpt_workspace_id,
|
||||
cli_auth_credentials_store_mode,
|
||||
);
|
||||
)
|
||||
.with_auth_route_config(system_proxy.map(auth_route_config_from_system_proxy_config));
|
||||
let server = run_login_server(opts)?;
|
||||
|
||||
print_login_server_start(server.actual_port, &server.auth_url);
|
||||
@@ -147,6 +165,7 @@ pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) ->
|
||||
config.codex_home.to_path_buf(),
|
||||
forced_chatgpt_workspace_id,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.system_proxy.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -195,6 +214,7 @@ pub async fn run_login_with_access_token(
|
||||
access_token: String,
|
||||
) -> ! {
|
||||
let config = load_config_or_exit(cli_config_overrides).await;
|
||||
let login_server_opts = login_server_options_from_config(&config);
|
||||
let _login_log_guard = init_login_file_logging(&config);
|
||||
tracing::info!("starting access token login flow");
|
||||
|
||||
@@ -203,11 +223,12 @@ pub async fn run_login_with_access_token(
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
match login_with_access_token(
|
||||
match login_with_access_token_with_auth_route_config(
|
||||
&config.codex_home,
|
||||
&access_token,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
Some(&config.chatgpt_base_url),
|
||||
login_server_opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -282,6 +303,12 @@ pub async fn run_login_with_device_code(
|
||||
client_id.unwrap_or(CLIENT_ID.to_string()),
|
||||
forced_chatgpt_workspace_id,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
)
|
||||
.with_auth_route_config(
|
||||
config
|
||||
.system_proxy
|
||||
.as_ref()
|
||||
.map(auth_route_config_from_system_proxy_config),
|
||||
);
|
||||
if let Some(iss) = issuer_base_url {
|
||||
opts.issuer = iss;
|
||||
@@ -321,6 +348,12 @@ pub async fn run_login_with_device_code_fallback_to_browser(
|
||||
client_id.unwrap_or(CLIENT_ID.to_string()),
|
||||
forced_chatgpt_workspace_id,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
)
|
||||
.with_auth_route_config(
|
||||
config
|
||||
.system_proxy
|
||||
.as_ref()
|
||||
.map(auth_route_config_from_system_proxy_config),
|
||||
);
|
||||
if let Some(iss) = issuer_base_url {
|
||||
opts.issuer = iss;
|
||||
@@ -364,11 +397,13 @@ pub async fn run_login_with_device_code_fallback_to_browser(
|
||||
|
||||
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
let config = load_config_or_exit(cli_config_overrides).await;
|
||||
let login_server_opts = login_server_options_from_config(&config);
|
||||
|
||||
match CodexAuth::from_auth_storage(
|
||||
match CodexAuth::from_auth_storage_with_auth_route_config(
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
Some(&config.chatgpt_base_url),
|
||||
login_server_opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -409,8 +444,10 @@ 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_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
match logout_with_revoke(&config.codex_home, config.cli_auth_credentials_store_mode).await {
|
||||
match auth_manager.logout_with_revoke().await {
|
||||
Ok(true) => {
|
||||
eprintln!("Successfully logged out");
|
||||
std::process::exit(0);
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_arg0::arg0_dispatch_or_else;
|
||||
use codex_chatgpt::apply_command::ApplyCommand;
|
||||
use codex_chatgpt::apply_command::run_apply_command;
|
||||
use codex_cli::login_server_options_from_config;
|
||||
use codex_cli::read_access_token_from_stdin;
|
||||
use codex_cli::read_api_key_from_stdin;
|
||||
use codex_cli::run_login_status;
|
||||
@@ -1678,9 +1679,13 @@ async fn load_exec_server_remote_auth_provider(
|
||||
let agent_identity_jwt = read_codex_access_token_from_env().ok_or_else(|| {
|
||||
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
|
||||
})?;
|
||||
let auth =
|
||||
CodexAuth::from_agent_identity_jwt(&agent_identity_jwt, Some(&config.chatgpt_base_url))
|
||||
.await?;
|
||||
let login_server_opts = login_server_options_from_config(config);
|
||||
let auth = CodexAuth::from_agent_identity_jwt_with_auth_route_config(
|
||||
&agent_identity_jwt,
|
||||
Some(&config.chatgpt_base_url),
|
||||
login_server_opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
return Ok(codex_model_provider::auth_provider_from_auth(&auth));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_config::CloudConfigBundleLoadErrorCode;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -57,12 +58,43 @@ pub async fn cloud_config_bundle_loader_for_storage(
|
||||
credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: String,
|
||||
) -> CloudConfigBundleLoader {
|
||||
let auth_manager = AuthManager::shared(
|
||||
codex_home.clone(),
|
||||
cloud_config_bundle_loader_for_storage_with_auth_route_config(
|
||||
codex_home,
|
||||
enable_codex_api_key_env,
|
||||
credentials_store_mode,
|
||||
Some(chatgpt_base_url.clone()),
|
||||
chatgpt_base_url,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
}
|
||||
|
||||
/// Loads cloud config with the explicit startup-only auth route policy.
|
||||
pub async fn cloud_config_bundle_loader_for_storage_with_auth_route_config(
|
||||
codex_home: PathBuf,
|
||||
enable_codex_api_key_env: bool,
|
||||
credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: String,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
) -> CloudConfigBundleLoader {
|
||||
let auth_manager = if let Some(auth_route_config) = auth_route_config {
|
||||
Arc::new(
|
||||
AuthManager::new_with_auth_route_config(
|
||||
codex_home.clone(),
|
||||
enable_codex_api_key_env,
|
||||
credentials_store_mode,
|
||||
Some(chatgpt_base_url.clone()),
|
||||
Some(auth_route_config),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
AuthManager::shared(
|
||||
codex_home.clone(),
|
||||
enable_codex_api_key_env,
|
||||
credentials_store_mode,
|
||||
Some(chatgpt_base_url.clone()),
|
||||
)
|
||||
.await
|
||||
};
|
||||
cloud_config_bundle_loader(auth_manager, chatgpt_base_url, codex_home)
|
||||
}
|
||||
|
||||
@@ -12,3 +12,4 @@ mod validation;
|
||||
|
||||
pub use bundle_loader::cloud_config_bundle_loader;
|
||||
pub use bundle_loader::cloud_config_bundle_loader_for_storage;
|
||||
pub use bundle_loader::cloud_config_bundle_loader_for_storage_with_auth_route_config;
|
||||
|
||||
@@ -44,15 +44,29 @@ pub fn normalize_base_url(input: &str) -> String {
|
||||
pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthManager> {
|
||||
// TODO: pass in cli overrides once cloud tasks properly support them.
|
||||
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
|
||||
Some(
|
||||
let auth_route_config = codex_login::bootstrap_auth_route_config_from_system_proxy_config(
|
||||
config.system_proxy.as_ref(),
|
||||
);
|
||||
let chatgpt_base_url = chatgpt_base_url.or(Some(config.chatgpt_base_url));
|
||||
Some(if let Some(auth_route_config) = auth_route_config {
|
||||
// Keep the existing cloud auth path unless platform system proxy discovery is selected.
|
||||
AuthManager::new_with_auth_route_config(
|
||||
config.codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
Some(auth_route_config),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
AuthManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
chatgpt_base_url.or(Some(config.chatgpt_base_url)),
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
|
||||
|
||||
@@ -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,12 @@ 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::OutboundProxyMode;
|
||||
pub use crate::outbound_proxy::RouteFailureClass;
|
||||
pub use crate::outbound_proxy::build_reqwest_client_for_route;
|
||||
pub use crate::request::PreparedRequestBody;
|
||||
pub use crate::request::Request;
|
||||
pub use crate::request::RequestBody;
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
//! Conservative outbound proxy selection for resolver-aware clients.
|
||||
//!
|
||||
//! This module keeps the default path as close as possible to the existing
|
||||
//! reqwest builder behavior. For `Auto`, platform system discovery is tried
|
||||
//! first, explicit environment proxies are delegated back to reqwest as the
|
||||
//! fallback, and the final fallback is the legacy 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);
|
||||
|
||||
/// Environment kill switch reserved for system proxy discovery.
|
||||
///
|
||||
/// Values such as `off`, `false`, `0`, `no`, or `disabled` disable system/PAC
|
||||
/// discovery while still allowing explicit environment proxies to be honored by
|
||||
/// resolver-aware clients.
|
||||
const CODEX_SYSTEM_PROXY_ENV: &str = "CODEX_SYSTEM_PROXY";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SystemProxyEnvOverride {
|
||||
Default,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl SystemProxyEnvOverride {
|
||||
fn from_value(value: Option<&str>) -> Self {
|
||||
let Some(value) = value else {
|
||||
return Self::Default;
|
||||
};
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"off" | "false" | "0" | "no" | "disabled" => Self::Disabled,
|
||||
_ => Self::Default,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_env() -> Self {
|
||||
Self::from_value(
|
||||
/*value*/ std::env::var(CODEX_SYSTEM_PROXY_ENV).ok().as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
const fn system_discovery_enabled(self) -> bool {
|
||||
matches!(self, Self::Default)
|
||||
}
|
||||
}
|
||||
|
||||
/// High-level client path being routed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ClientRouteClass {
|
||||
Auth,
|
||||
Api,
|
||||
WebSocket,
|
||||
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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// How a resolver-aware client should choose an outbound proxy.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OutboundProxyMode {
|
||||
/// Try system discovery where supported, then explicit env proxy settings, then legacy behavior.
|
||||
#[default]
|
||||
Auto,
|
||||
/// Preserve only the existing reqwest/env proxy path.
|
||||
Env,
|
||||
/// Require supported system discovery, including WPAD auto-detect on supported platforms.
|
||||
System,
|
||||
/// Disable proxy use for this client.
|
||||
Direct,
|
||||
}
|
||||
|
||||
/// Optional route-selection inputs for resolver-aware clients.
|
||||
///
|
||||
/// This is intentionally policy-only. Resolved proxy endpoints, PAC URLs, and
|
||||
/// platform resolver details stay internal to the client builder.
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub struct OutboundProxyConfig {
|
||||
mode: OutboundProxyMode,
|
||||
}
|
||||
|
||||
impl OutboundProxyConfig {
|
||||
pub const fn new(mode: OutboundProxyMode) -> Self {
|
||||
Self { mode }
|
||||
}
|
||||
|
||||
pub const fn mode(&self) -> OutboundProxyMode {
|
||||
self.mode
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for OutboundProxyConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("OutboundProxyConfig")
|
||||
.field("mode", &self.mode)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 },
|
||||
|
||||
#[error("System proxy resolution for {route_class} failed: {failure}")]
|
||||
SystemProxyUnavailable {
|
||||
route_class: ClientRouteClass,
|
||||
failure: RouteFailureClass,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<BuildRouteAwareHttpClientError> for io::Error {
|
||||
fn from(error: BuildRouteAwareHttpClientError) -> Self {
|
||||
match error {
|
||||
BuildRouteAwareHttpClientError::CustomCa(error) => error.into(),
|
||||
BuildRouteAwareHttpClientError::InvalidProxyConfig { .. }
|
||||
| BuildRouteAwareHttpClientError::SystemProxyUnavailable { .. } => {
|
||||
io::Error::other(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a reqwest client with conservative route selection and shared CA handling.
|
||||
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(builder, request_url, route_class, config)?;
|
||||
build_reqwest_client_with_custom_ca(builder).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn configure_proxy_for_route(
|
||||
builder: reqwest::ClientBuilder,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
config: Option<&OutboundProxyConfig>,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
let Some(config) = config else {
|
||||
return Ok(builder);
|
||||
};
|
||||
let origin = RequestOrigin::parse(request_url);
|
||||
|
||||
if config.mode == OutboundProxyMode::Direct {
|
||||
return Ok(builder.no_proxy());
|
||||
}
|
||||
|
||||
// Route contract: OS/PAC bypass decisions are returned by platform
|
||||
// resolution as `Direct` and disable proxy use for this client. Env
|
||||
// `NO_PROXY` is an env-derived direct decision; reqwest remains the
|
||||
// authority for applying the env proxy contract to its own env proxies.
|
||||
if origin.as_ref().is_some_and(no_proxy_env_matches_origin) {
|
||||
if config.mode == OutboundProxyMode::Env {
|
||||
return Ok(builder.no_proxy());
|
||||
}
|
||||
return Ok(builder);
|
||||
}
|
||||
|
||||
if config.mode == OutboundProxyMode::Env {
|
||||
return configure_env_proxy_handling(
|
||||
builder,
|
||||
origin.as_ref(),
|
||||
route_class,
|
||||
EnvProxyHandling::ResolveConcreteOrNoProxy,
|
||||
);
|
||||
}
|
||||
|
||||
if !SystemProxyEnvOverride::from_env().system_discovery_enabled() {
|
||||
if config.mode == OutboundProxyMode::System {
|
||||
return Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
});
|
||||
}
|
||||
return configure_env_proxy_handling(
|
||||
builder,
|
||||
origin.as_ref(),
|
||||
route_class,
|
||||
EnvProxyHandling::DelegateToReqwest,
|
||||
);
|
||||
}
|
||||
|
||||
if !system_proxy_supported() {
|
||||
if config.mode == OutboundProxyMode::System {
|
||||
return Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
});
|
||||
}
|
||||
return configure_env_proxy_handling(
|
||||
builder,
|
||||
origin.as_ref(),
|
||||
route_class,
|
||||
EnvProxyHandling::DelegateToReqwest,
|
||||
);
|
||||
}
|
||||
|
||||
let Some(origin) = origin.as_ref() else {
|
||||
return if config.mode == OutboundProxyMode::System {
|
||||
Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
failure: RouteFailureClass::InvalidProxyConfig,
|
||||
})
|
||||
} else {
|
||||
configure_env_proxy_handling(
|
||||
builder,
|
||||
None,
|
||||
route_class,
|
||||
EnvProxyHandling::DelegateToReqwest,
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
let include_auto_detect = matches!(
|
||||
config.mode,
|
||||
OutboundProxyMode::Auto | OutboundProxyMode::System
|
||||
);
|
||||
match resolve_system_proxy(request_url, origin, include_auto_detect) {
|
||||
SystemProxyDecision::Direct => Ok(builder.no_proxy()),
|
||||
SystemProxyDecision::Proxy { url } => configure_concrete_proxy(builder, route_class, &url),
|
||||
SystemProxyDecision::Unavailable { failure } => {
|
||||
if config.mode == OutboundProxyMode::System {
|
||||
Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
failure,
|
||||
})
|
||||
} else {
|
||||
configure_env_proxy_handling(
|
||||
builder,
|
||||
Some(origin),
|
||||
route_class,
|
||||
EnvProxyHandling::DelegateToReqwest,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn system_proxy_supported() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn configure_concrete_proxy(
|
||||
builder: reqwest::ClientBuilder,
|
||||
route_class: ClientRouteClass,
|
||||
proxy_url: &str,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
let proxy = match reqwest::Proxy::all(proxy_url) {
|
||||
Ok(proxy) => proxy,
|
||||
Err(_source) => {
|
||||
return Err(BuildRouteAwareHttpClientError::InvalidProxyConfig { route_class });
|
||||
}
|
||||
};
|
||||
let proxy = proxy.no_proxy(reqwest::NoProxy::from_env());
|
||||
Ok(builder.proxy(proxy))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum EnvProxyHandling {
|
||||
ResolveConcreteOrNoProxy,
|
||||
DelegateToReqwest,
|
||||
}
|
||||
|
||||
fn configure_env_proxy_handling(
|
||||
builder: reqwest::ClientBuilder,
|
||||
origin: Option<&RequestOrigin>,
|
||||
route_class: ClientRouteClass,
|
||||
handling: EnvProxyHandling,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
if let Some(origin) = origin
|
||||
&& let Some(proxy_url) = env_proxy_for_origin(origin)
|
||||
{
|
||||
if handling == EnvProxyHandling::ResolveConcreteOrNoProxy {
|
||||
return configure_concrete_proxy(builder, route_class, &proxy_url);
|
||||
}
|
||||
return Ok(builder);
|
||||
}
|
||||
|
||||
if conventional_proxy_env_present() {
|
||||
return match handling {
|
||||
EnvProxyHandling::ResolveConcreteOrNoProxy => Ok(builder.no_proxy()),
|
||||
EnvProxyHandling::DelegateToReqwest => Ok(builder),
|
||||
};
|
||||
}
|
||||
|
||||
match handling {
|
||||
EnvProxyHandling::ResolveConcreteOrNoProxy => Ok(builder.no_proxy()),
|
||||
EnvProxyHandling::DelegateToReqwest => Ok(builder),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
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_else(|| default_port_for_scheme(&scheme))?;
|
||||
Some(Self { scheme, host, port })
|
||||
}
|
||||
}
|
||||
|
||||
fn default_port_for_scheme(scheme: &str) -> Option<u16> {
|
||||
match scheme {
|
||||
"http" => Some(80),
|
||||
"https" => Some(443),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum SystemProxyDecision {
|
||||
Direct,
|
||||
Proxy { url: String },
|
||||
Unavailable { failure: RouteFailureClass },
|
||||
}
|
||||
|
||||
fn resolve_system_proxy(
|
||||
request_url: &str,
|
||||
origin: &RequestOrigin,
|
||||
include_auto_detect: bool,
|
||||
) -> SystemProxyDecision {
|
||||
if let Some(decision) = cached_system_proxy_decision(request_url, include_auto_detect) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
let decision = resolve_platform_system_proxy(request_url, origin, include_auto_detect);
|
||||
cache_system_proxy_decision(request_url, include_auto_detect, decision.clone());
|
||||
decision
|
||||
}
|
||||
|
||||
fn resolve_platform_system_proxy(
|
||||
_request_url: &str,
|
||||
_origin: &RequestOrigin,
|
||||
_include_auto_detect: bool,
|
||||
) -> 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,
|
||||
include_auto_detect: bool,
|
||||
) -> Option<SystemProxyDecision> {
|
||||
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let mut cache = cache.lock().ok()?;
|
||||
let key = system_proxy_cache_key(request_url, include_auto_detect);
|
||||
let cached = cache.get(&key)?;
|
||||
if cached.expires_at > Instant::now() {
|
||||
return Some(cached.decision.clone());
|
||||
}
|
||||
cache.remove(&key);
|
||||
None
|
||||
}
|
||||
|
||||
fn cache_system_proxy_decision(
|
||||
request_url: &str,
|
||||
include_auto_detect: bool,
|
||||
decision: SystemProxyDecision,
|
||||
) {
|
||||
if matches!(decision, SystemProxyDecision::Unavailable { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
if let Ok(mut cache) = cache.lock() {
|
||||
let now = Instant::now();
|
||||
cache.retain(|_, cached| cached.expires_at > now);
|
||||
cache.insert(
|
||||
system_proxy_cache_key(request_url, include_auto_detect),
|
||||
CachedSystemProxyDecision {
|
||||
decision,
|
||||
expires_at: now + SYSTEM_PROXY_SUCCESS_CACHE_TTL,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn system_proxy_cache_key(request_url: &str, include_auto_detect: bool) -> String {
|
||||
format!("{request_url}:auto_detect={include_auto_detect}")
|
||||
}
|
||||
|
||||
fn conventional_proxy_env_present() -> bool {
|
||||
proxy_env_value("HTTPS_PROXY").is_some()
|
||||
|| proxy_env_value("HTTP_PROXY").is_some()
|
||||
|| proxy_env_value("ALL_PROXY").is_some()
|
||||
}
|
||||
|
||||
fn env_proxy_for_origin(origin: &RequestOrigin) -> Option<String> {
|
||||
if origin.scheme == "https" {
|
||||
proxy_env_value("HTTPS_PROXY").or_else(|| proxy_env_value("ALL_PROXY"))
|
||||
} else if origin.scheme == "http" {
|
||||
proxy_env_value("HTTP_PROXY").or_else(|| proxy_env_value("ALL_PROXY"))
|
||||
} else {
|
||||
proxy_env_value("ALL_PROXY")
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_env_value(upper: &str) -> Option<String> {
|
||||
let lower = upper.to_ascii_lowercase();
|
||||
std::env::var(upper)
|
||||
.ok()
|
||||
.filter(|value| !value.is_empty())
|
||||
.or_else(|| std::env::var(lower).ok().filter(|value| !value.is_empty()))
|
||||
}
|
||||
|
||||
fn no_proxy_env_matches_origin(origin: &RequestOrigin) -> bool {
|
||||
let Some(no_proxy) = proxy_env_value("NO_PROXY") else {
|
||||
return false;
|
||||
};
|
||||
no_proxy_matches_origin(&no_proxy, origin)
|
||||
}
|
||||
|
||||
fn no_proxy_matches_origin(no_proxy: &str, origin: &RequestOrigin) -> bool {
|
||||
no_proxy
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|entry| !entry.is_empty())
|
||||
.any(|entry| no_proxy_entry_matches_origin(entry, origin))
|
||||
}
|
||||
|
||||
fn no_proxy_entry_matches_origin(entry: &str, origin: &RequestOrigin) -> bool {
|
||||
if entry == "*" {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut entry = entry
|
||||
.strip_prefix("http://")
|
||||
.or_else(|| entry.strip_prefix("https://"))
|
||||
.unwrap_or(entry)
|
||||
.trim_matches(['[', ']'])
|
||||
.to_ascii_lowercase();
|
||||
let mut port = None;
|
||||
let parsed_host_port = entry.rsplit_once(':').and_then(|(host, candidate_port)| {
|
||||
if host.contains(':') {
|
||||
return None;
|
||||
}
|
||||
candidate_port
|
||||
.parse::<u16>()
|
||||
.ok()
|
||||
.map(|parsed_port| (host.to_string(), parsed_port))
|
||||
});
|
||||
if let Some((host, parsed_port)) = parsed_host_port {
|
||||
entry = host;
|
||||
port = Some(parsed_port);
|
||||
}
|
||||
if port.is_some_and(|port| port != origin.port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(suffix) = entry.strip_prefix('.') {
|
||||
return origin.host == suffix || origin.host.ends_with(&format!(".{suffix}"));
|
||||
}
|
||||
|
||||
if entry.contains('*') {
|
||||
return wildcard_host_match(&entry, &origin.host);
|
||||
}
|
||||
|
||||
origin.host == entry || origin.host.ends_with(&format!(".{entry}"))
|
||||
}
|
||||
|
||||
fn wildcard_host_match(pattern: &str, host: &str) -> bool {
|
||||
let mut remaining = host;
|
||||
let mut first = true;
|
||||
for part in pattern.split('*') {
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if first && !pattern.starts_with('*') {
|
||||
let Some(stripped) = remaining.strip_prefix(part) else {
|
||||
return false;
|
||||
};
|
||||
remaining = stripped;
|
||||
} else {
|
||||
let Some(index) = remaining.find(part) else {
|
||||
return false;
|
||||
};
|
||||
remaining = &remaining[index + part.len()..];
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
pattern.ends_with('*') || remaining.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_proxy_matches_exact_suffix_wildcard_and_port() {
|
||||
let origin = RequestOrigin {
|
||||
scheme: "https".to_string(),
|
||||
host: "auth.openai.com".to_string(),
|
||||
port: 443,
|
||||
};
|
||||
assert!(no_proxy_matches_origin("auth.openai.com", &origin));
|
||||
assert!(no_proxy_matches_origin(".openai.com", &origin));
|
||||
assert!(no_proxy_matches_origin("*.openai.com", &origin));
|
||||
assert!(no_proxy_matches_origin("auth.openai.com:443", &origin));
|
||||
assert!(!no_proxy_matches_origin("auth.openai.com:8443", &origin));
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ use codex_config::types::OtelExporterKind;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::SkillsConfig;
|
||||
use codex_config::types::SystemProxyFeatureConfigToml;
|
||||
use codex_config::types::SystemProxyFeatureModeToml;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
@@ -9668,6 +9669,107 @@ shell_tool = false
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_proxy_mode_requirement_enables_and_sets_mode() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_config_bundle(
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
r#"
|
||||
[features]
|
||||
system_proxy_mode_system = true
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(config.features.enabled(Feature::SystemProxy));
|
||||
assert_eq!(
|
||||
config.system_proxy,
|
||||
Some(SystemProxyFeatureConfigToml {
|
||||
enabled: Some(true),
|
||||
mode: Some(SystemProxyFeatureModeToml::System),
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("system_proxy_mode_system")),
|
||||
"{:?}",
|
||||
config.startup_warnings
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_proxy_mode_requirement_overrides_configured_mode() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[features.system_proxy]
|
||||
enabled = true
|
||||
mode = "env"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_config_bundle(
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
r#"
|
||||
[features]
|
||||
system_proxy_mode_direct = true
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.system_proxy,
|
||||
Some(SystemProxyFeatureConfigToml {
|
||||
enabled: Some(true),
|
||||
mode: Some(SystemProxyFeatureModeToml::Direct),
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_proxy_mode_requirements_reject_multiple_modes() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let result = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_config_bundle(
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
r#"
|
||||
[features]
|
||||
system_proxy_mode_auto = true
|
||||
system_proxy_mode_system = true
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let message = result
|
||||
.err()
|
||||
.map(|err| err.to_string())
|
||||
.expect("conflicting system proxy mode requirements should fail");
|
||||
assert!(message.contains("at most one"), "{message}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approvals_reviewer_defaults_to_manual_only_without_guardian_feature() -> std::io::Result<()>
|
||||
{
|
||||
|
||||
@@ -13,9 +13,15 @@ use codex_features::Feature;
|
||||
use codex_features::FeatureConfigSource;
|
||||
use codex_features::FeatureOverrides;
|
||||
use codex_features::Features;
|
||||
use codex_features::SystemProxyFeatureModeToml;
|
||||
use codex_features::canonical_feature_for_key;
|
||||
use codex_features::feature_for_key;
|
||||
|
||||
const SYSTEM_PROXY_MODE_AUTO_REQUIREMENT: &str = "system_proxy_mode_auto";
|
||||
const SYSTEM_PROXY_MODE_ENV_REQUIREMENT: &str = "system_proxy_mode_env";
|
||||
const SYSTEM_PROXY_MODE_SYSTEM_REQUIREMENT: &str = "system_proxy_mode_system";
|
||||
const SYSTEM_PROXY_MODE_DIRECT_REQUIREMENT: &str = "system_proxy_mode_direct";
|
||||
|
||||
/// Wrapper around [`Features`] which enforces constraints defined in
|
||||
/// `FeatureRequirementsToml` and provides normalization to ensure constraints
|
||||
/// are satisfied. Constraints are enforced on construction and mutation of
|
||||
@@ -72,7 +78,11 @@ impl ManagedFeatures {
|
||||
value: feature_requirements,
|
||||
source,
|
||||
}) => (
|
||||
parse_feature_requirements(feature_requirements, &source, startup_warnings),
|
||||
parse_feature_requirements_with_mode_gate(
|
||||
&feature_requirements,
|
||||
&source,
|
||||
startup_warnings,
|
||||
)?,
|
||||
Some(source),
|
||||
),
|
||||
None => (BTreeMap::new(), None),
|
||||
@@ -203,24 +213,119 @@ fn feature_requirements_display(feature_requirements: &BTreeMap<Feature, bool>)
|
||||
format!("[{}]", values.join(", "))
|
||||
}
|
||||
|
||||
struct SystemProxyModeRequirement {
|
||||
key: String,
|
||||
mode: SystemProxyFeatureModeToml,
|
||||
}
|
||||
|
||||
pub(crate) fn system_proxy_mode_requirement(
|
||||
feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
|
||||
) -> std::io::Result<Option<SystemProxyFeatureModeToml>> {
|
||||
let Some(Sourced {
|
||||
value: feature_requirements,
|
||||
source,
|
||||
}) = feature_requirements
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
parse_system_proxy_mode_requirement(feature_requirements, source)
|
||||
.map(|requirement| requirement.map(|requirement| requirement.mode))
|
||||
}
|
||||
|
||||
fn parse_feature_requirements_with_mode_gate(
|
||||
feature_requirements: &FeatureRequirementsToml,
|
||||
source: &RequirementSource,
|
||||
startup_warnings: Option<&mut Vec<String>>,
|
||||
) -> std::io::Result<BTreeMap<Feature, bool>> {
|
||||
let system_proxy_mode = parse_system_proxy_mode_requirement(feature_requirements, source)?;
|
||||
let mut pinned_features =
|
||||
parse_feature_requirements(feature_requirements, source, startup_warnings);
|
||||
if let Some(requirement) = system_proxy_mode {
|
||||
if pinned_features.get(&Feature::SystemProxy) == Some(&false) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
ConstraintError::InvalidValue {
|
||||
field_name: "features",
|
||||
candidate: format!("system_proxy=false, {}=true", requirement.key),
|
||||
allowed:
|
||||
"system_proxy=true or omit system_proxy when selecting a system proxy mode"
|
||||
.to_string(),
|
||||
requirement_source: source.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
pinned_features.insert(Feature::SystemProxy, true);
|
||||
}
|
||||
Ok(pinned_features)
|
||||
}
|
||||
|
||||
fn parse_system_proxy_mode_requirement(
|
||||
feature_requirements: &FeatureRequirementsToml,
|
||||
source: &RequirementSource,
|
||||
) -> std::io::Result<Option<SystemProxyModeRequirement>> {
|
||||
let mut selected = None;
|
||||
for (key, enabled) in &feature_requirements.entries {
|
||||
let Some(mode) = system_proxy_mode_requirement_for_key(key) else {
|
||||
continue;
|
||||
};
|
||||
if !*enabled {
|
||||
continue;
|
||||
}
|
||||
if let Some((existing_key, _)) = selected {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
ConstraintError::InvalidValue {
|
||||
field_name: "features",
|
||||
candidate: format!("{existing_key}=true, {key}=true"),
|
||||
allowed: format!(
|
||||
"at most one of {SYSTEM_PROXY_MODE_AUTO_REQUIREMENT}, {SYSTEM_PROXY_MODE_ENV_REQUIREMENT}, {SYSTEM_PROXY_MODE_SYSTEM_REQUIREMENT}, {SYSTEM_PROXY_MODE_DIRECT_REQUIREMENT}"
|
||||
),
|
||||
requirement_source: source.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
selected = Some((key.as_str(), mode));
|
||||
}
|
||||
|
||||
Ok(selected.map(|(key, mode)| SystemProxyModeRequirement {
|
||||
key: key.to_string(),
|
||||
mode,
|
||||
}))
|
||||
}
|
||||
|
||||
fn system_proxy_mode_requirement_for_key(key: &str) -> Option<SystemProxyFeatureModeToml> {
|
||||
match key {
|
||||
SYSTEM_PROXY_MODE_AUTO_REQUIREMENT => Some(SystemProxyFeatureModeToml::Auto),
|
||||
SYSTEM_PROXY_MODE_ENV_REQUIREMENT => Some(SystemProxyFeatureModeToml::Env),
|
||||
SYSTEM_PROXY_MODE_SYSTEM_REQUIREMENT => Some(SystemProxyFeatureModeToml::System),
|
||||
SYSTEM_PROXY_MODE_DIRECT_REQUIREMENT => Some(SystemProxyFeatureModeToml::Direct),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_feature_requirements(
|
||||
feature_requirements: FeatureRequirementsToml,
|
||||
feature_requirements: &FeatureRequirementsToml,
|
||||
source: &RequirementSource,
|
||||
mut startup_warnings: Option<&mut Vec<String>>,
|
||||
) -> BTreeMap<Feature, bool> {
|
||||
let mut pinned_features = BTreeMap::new();
|
||||
for (key, enabled) in feature_requirements.entries {
|
||||
for (key, enabled) in &feature_requirements.entries {
|
||||
let enabled = *enabled;
|
||||
if key == "auto_review" {
|
||||
pinned_features.insert(Feature::GuardianApproval, enabled);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(feature) = canonical_feature_for_key(&key) {
|
||||
if system_proxy_mode_requirement_for_key(key).is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(feature) = canonical_feature_for_key(key) {
|
||||
pinned_features.insert(feature, enabled);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(feature) = feature_for_key(&key) {
|
||||
if let Some(feature) = feature_for_key(key) {
|
||||
push_feature_requirement_warning(
|
||||
&mut startup_warnings,
|
||||
format!(
|
||||
@@ -283,11 +388,11 @@ pub(crate) fn validate_explicit_feature_settings_in_config_toml(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let pinned_features = parse_feature_requirements(
|
||||
feature_requirements.clone(),
|
||||
let pinned_features = parse_feature_requirements_with_mode_gate(
|
||||
feature_requirements,
|
||||
source,
|
||||
/*startup_warnings*/ None,
|
||||
);
|
||||
)?;
|
||||
if pinned_features.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ use codex_config::types::Notice;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::SystemProxyFeatureConfigToml;
|
||||
use codex_config::types::SystemProxyFeatureModeToml;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
@@ -71,6 +72,8 @@ use codex_features::NetworkProxyConfigToml;
|
||||
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_login::auth_route_config_from_system_proxy_config;
|
||||
use codex_mcp::McpConfig;
|
||||
use codex_memories_read::memory_root;
|
||||
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
@@ -1114,6 +1117,12 @@ impl AuthManagerConfig for Config {
|
||||
fn chatgpt_base_url(&self) -> String {
|
||||
self.chatgpt_base_url.clone()
|
||||
}
|
||||
|
||||
fn auth_route_config(&self) -> Option<AuthRouteConfig> {
|
||||
self.system_proxy
|
||||
.as_ref()
|
||||
.map(auth_route_config_from_system_proxy_config)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -2430,6 +2439,7 @@ fn system_proxy_toml_config(
|
||||
fn resolved_system_proxy_config_from_features(
|
||||
cfg: &ConfigToml,
|
||||
features: &Features,
|
||||
mode_requirement: Option<SystemProxyFeatureModeToml>,
|
||||
) -> Option<SystemProxyFeatureConfigToml> {
|
||||
if !features.enabled(Feature::SystemProxy) {
|
||||
return None;
|
||||
@@ -2437,6 +2447,9 @@ fn resolved_system_proxy_config_from_features(
|
||||
let mut config = system_proxy_toml_config(cfg.features.as_ref())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if let Some(mode) = mode_requirement {
|
||||
config.mode = Some(mode);
|
||||
}
|
||||
config.enabled = Some(true);
|
||||
Some(config)
|
||||
}
|
||||
@@ -2444,8 +2457,9 @@ fn resolved_system_proxy_config_from_features(
|
||||
fn resolved_system_proxy_config(
|
||||
cfg: &ConfigToml,
|
||||
features: &ManagedFeatures,
|
||||
mode_requirement: Option<SystemProxyFeatureModeToml>,
|
||||
) -> Option<SystemProxyFeatureConfigToml> {
|
||||
resolved_system_proxy_config_from_features(cfg, features.get())
|
||||
resolved_system_proxy_config_from_features(cfg, features.get(), mode_requirement)
|
||||
}
|
||||
|
||||
/// Resolves `[features.system_proxy]` for the initial cloud-config bootstrap.
|
||||
@@ -2468,7 +2482,11 @@ pub fn resolve_bootstrap_system_proxy_config(
|
||||
);
|
||||
let features =
|
||||
ManagedFeatures::from_configured(configured_features, feature_requirements.cloned())?;
|
||||
Ok(resolved_system_proxy_config(cfg, &features))
|
||||
Ok(resolved_system_proxy_config(
|
||||
cfg,
|
||||
&features,
|
||||
/*mode_requirement*/ None,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
@@ -2734,12 +2752,15 @@ impl Config {
|
||||
},
|
||||
feature_overrides,
|
||||
);
|
||||
let system_proxy_mode_requirement =
|
||||
managed_features::system_proxy_mode_requirement(feature_requirements.as_ref())?;
|
||||
let features = ManagedFeatures::from_configured_with_warnings(
|
||||
configured_features,
|
||||
feature_requirements,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let system_proxy = resolved_system_proxy_config(&cfg, &features);
|
||||
let system_proxy =
|
||||
resolved_system_proxy_config(&cfg, &features, system_proxy_mode_requirement);
|
||||
let enable_network_proxy = features.enabled(Feature::NetworkProxy);
|
||||
let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg);
|
||||
// Keep the configured mode separate so a requirement-constrained mode
|
||||
|
||||
+24
-10
@@ -51,7 +51,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage_with_auth_route_config;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLoadError;
|
||||
use codex_config::ConfigLoadOptions;
|
||||
@@ -64,6 +64,7 @@ use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_config_as_toml_with_cli_and_load_options;
|
||||
use codex_core::config::resolve_bootstrap_system_proxy_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;
|
||||
@@ -72,9 +73,11 @@ use codex_core::path_utils;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_login::AuthConfig;
|
||||
use codex_login::auth_route_config_from_system_proxy_config;
|
||||
use codex_login::bootstrap_auth_route_config_from_system_proxy_config;
|
||||
use codex_login::default_client::set_default_client_residency_requirement;
|
||||
use codex_login::default_client::set_default_originator;
|
||||
use codex_login::enforce_login_restrictions;
|
||||
use codex_login::enforce_login_restrictions_with_auth_route_config;
|
||||
use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use codex_otel::set_parent_from_context;
|
||||
@@ -345,13 +348,17 @@ 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 cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
let system_proxy_config = resolve_bootstrap_system_proxy_config(&bootstrap_config_toml);
|
||||
let auth_route_config =
|
||||
bootstrap_auth_route_config_from_system_proxy_config(system_proxy_config.as_ref());
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage_with_auth_route_config(
|
||||
codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
let run_cli_overrides = cli_kv_overrides.clone();
|
||||
@@ -463,13 +470,20 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
|
||||
set_default_client_residency_requirement(config.enforce_residency.value());
|
||||
|
||||
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,
|
||||
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()),
|
||||
})
|
||||
let auth_route_config = config
|
||||
.system_proxy
|
||||
.as_ref()
|
||||
.map(auth_route_config_from_system_proxy_config);
|
||||
if let Err(err) = enforce_login_restrictions_with_auth_route_config(
|
||||
&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
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.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("{err}");
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use codex_agent_identity::AgentIdentityKey;
|
||||
use codex_agent_identity::agent_task_registration_url;
|
||||
use codex_agent_identity::register_agent_task;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use std::env;
|
||||
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::default_client::build_default_reqwest_client_for_auth_route;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
|
||||
use super::storage::AgentIdentityAuthRecord;
|
||||
|
||||
@@ -18,14 +20,22 @@ pub struct AgentIdentityAuth {
|
||||
|
||||
impl AgentIdentityAuth {
|
||||
pub async fn load(record: AgentIdentityAuthRecord) -> std::io::Result<Self> {
|
||||
Self::load_with_auth_route_config(record, /*auth_route_config*/ None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn load_with_auth_route_config(
|
||||
record: AgentIdentityAuthRecord,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let agent_identity_authapi_base_url = agent_identity_authapi_base_url();
|
||||
let process_task_id = register_agent_task(
|
||||
&build_reqwest_client(),
|
||||
&agent_identity_authapi_base_url,
|
||||
key(&record),
|
||||
)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let task_registration_url =
|
||||
agent_task_registration_url(&agent_identity_authapi_base_url, &record.agent_runtime_id);
|
||||
let client =
|
||||
build_default_reqwest_client_for_auth_route(&task_registration_url, auth_route_config)?;
|
||||
let process_task_id =
|
||||
register_agent_task(&client, &agent_identity_authapi_base_url, key(&record))
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
Ok(Self {
|
||||
record,
|
||||
process_task_id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::AuthRouteConfig;
|
||||
use crate::auth::storage::FileAuthStorage;
|
||||
use crate::auth::storage::get_auth_file;
|
||||
use crate::token_data::IdTokenInfo;
|
||||
@@ -290,6 +291,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -347,6 +349,7 @@ async fn loads_api_key_from_auth_json() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -425,6 +428,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -443,6 +447,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
|
||||
updated_auth_dot_json,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("updated auth should parse");
|
||||
@@ -826,6 +831,7 @@ async fn load_auth_reads_access_token_from_env() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
Some(&chatgpt_base_url),
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("env auth should load")
|
||||
@@ -949,6 +955,7 @@ async fn load_auth_keeps_codex_api_key_env_precedence() {
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("env auth should load")
|
||||
@@ -1316,9 +1323,31 @@ async fn agent_identity_plan_type_maps_raw_education_alias() {
|
||||
assert_agent_identity_plan_alias(json!("education"), AccountPlanType::Edu).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn agent_identity_auth_with_auth_route_config_loads() {
|
||||
let auth_route_config = AuthRouteConfig::direct();
|
||||
|
||||
assert_agent_identity_auth_loads(json!("pro"), AccountPlanType::Pro, Some(&auth_route_config))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn assert_agent_identity_plan_alias(
|
||||
plan_type: serde_json::Value,
|
||||
expected_plan_type: AccountPlanType,
|
||||
) {
|
||||
assert_agent_identity_auth_loads(
|
||||
plan_type,
|
||||
expected_plan_type,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn assert_agent_identity_auth_loads(
|
||||
plan_type: serde_json::Value,
|
||||
expected_plan_type: AccountPlanType,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) {
|
||||
let record = agent_identity_record("account-id");
|
||||
let jwt = signed_agent_identity_jwt(&record, plan_type).expect("agent identity jwt");
|
||||
@@ -1340,9 +1369,13 @@ async fn assert_agent_identity_plan_alias(
|
||||
let chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||
let _authapi_guard =
|
||||
EnvVarGuard::set("CODEX_AGENT_IDENTITY_AUTHAPI_BASE_URL", &chatgpt_base_url);
|
||||
let auth = CodexAuth::from_agent_identity_jwt(&jwt, Some(&chatgpt_base_url))
|
||||
.await
|
||||
.expect("agent identity auth");
|
||||
let auth = CodexAuth::from_agent_identity_jwt_with_auth_route_config(
|
||||
&jwt,
|
||||
Some(&chatgpt_base_url),
|
||||
auth_route_config,
|
||||
)
|
||||
.await
|
||||
.expect("agent identity auth");
|
||||
|
||||
pretty_assertions::assert_eq!(auth.account_plan_type(), Some(expected_plan_type));
|
||||
server.verify().await;
|
||||
@@ -1368,6 +1401,7 @@ async fn plan_type_maps_known_plan() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1396,6 +1430,7 @@ async fn plan_type_maps_self_serve_business_usage_based_plan() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1427,6 +1462,7 @@ async fn plan_type_maps_enterprise_cbp_usage_based_plan() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1458,6 +1494,7 @@ async fn plan_type_maps_unknown_to_unknown() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1486,6 +1523,7 @@ async fn missing_plan_type_maps_to_unknown() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
|
||||
@@ -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.
|
||||
@@ -220,13 +225,71 @@ 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)
|
||||
pub(crate) fn build_auth_reqwest_client_with_auth_route_config(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
build_auth_reqwest_client_with_builder(reqwest::Client::builder(), endpoint, auth_route_config)
|
||||
}
|
||||
|
||||
/// Builds the standard Codex reqwest client for a known auth destination.
|
||||
pub(crate) fn build_default_reqwest_client_for_auth_route(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
let Some(route_config) = auth_route_config.and_then(AuthRouteConfig::route_config) else {
|
||||
return Ok(build_reqwest_client());
|
||||
};
|
||||
|
||||
let builder = default_reqwest_client_builder();
|
||||
if is_sandboxed() {
|
||||
return build_reqwest_client_with_custom_ca(builder).map_err(Into::into);
|
||||
}
|
||||
build_reqwest_client_for_route(
|
||||
builder,
|
||||
endpoint,
|
||||
ClientRouteClass::Auth,
|
||||
Some(route_config),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn create_client_for_auth_route(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<CodexHttpClient, BuildRouteAwareHttpClientError> {
|
||||
if auth_route_config
|
||||
.and_then(AuthRouteConfig::route_config)
|
||||
.is_none()
|
||||
{
|
||||
return Ok(create_client());
|
||||
}
|
||||
|
||||
build_default_reqwest_client_for_auth_route(endpoint, auth_route_config)
|
||||
.map(CodexHttpClient::new)
|
||||
}
|
||||
|
||||
fn build_auth_reqwest_client_with_builder(
|
||||
builder: reqwest::ClientBuilder,
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
build_reqwest_client_for_route(
|
||||
builder,
|
||||
endpoint,
|
||||
ClientRouteClass::Auth,
|
||||
auth_route_config.and_then(AuthRouteConfig::route_config),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn default_headers() -> HeaderMap {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::sanitize_user_agent;
|
||||
use super::*;
|
||||
use crate::AuthRouteConfig;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serial_test::serial;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_get_codex_user_agent() {
|
||||
@@ -89,6 +92,38 @@ async fn test_create_client_sets_default_headers() {
|
||||
set_default_client_residency_requirement(/*enforce_residency*/ None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial(default_client_ca_env)]
|
||||
fn auth_route_client_without_proxy_config_preserves_default_client_fallback() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let missing_ca_path = temp_dir.path().join("missing-ca.pem");
|
||||
let _guard = EnvVarGuard::set(
|
||||
"CODEX_CA_CERTIFICATE",
|
||||
missing_ca_path
|
||||
.to_str()
|
||||
.expect("test CA path should be valid UTF-8"),
|
||||
);
|
||||
|
||||
let no_proxy_client = create_client_for_auth_route(
|
||||
"https://auth.openai.com/oauth/token",
|
||||
/*auth_route_config*/ None,
|
||||
);
|
||||
assert!(no_proxy_client.is_ok());
|
||||
|
||||
let raw_no_proxy_client = build_default_reqwest_client_for_auth_route(
|
||||
"https://auth.openai.com/oauth/token",
|
||||
/*auth_route_config*/ None,
|
||||
);
|
||||
assert!(raw_no_proxy_client.is_ok());
|
||||
|
||||
let auth_route_config = AuthRouteConfig::direct();
|
||||
let explicit_proxy_client = create_client_for_auth_route(
|
||||
"https://auth.openai.com/oauth/token",
|
||||
Some(&auth_route_config),
|
||||
);
|
||||
assert!(explicit_proxy_client.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_suffix_is_sanitized() {
|
||||
let prefix = "codex_cli_rs/0.0.0";
|
||||
@@ -123,3 +158,34 @@ fn test_macos() {
|
||||
.unwrap();
|
||||
assert!(re.is_match(&user_agent));
|
||||
}
|
||||
|
||||
/// Use sparingly.
|
||||
/// TODO: replace this with an injectable env var provider.
|
||||
#[cfg(test)]
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
original: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &str) -> Self {
|
||||
let original = std::env::var_os(key);
|
||||
unsafe {
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.original {
|
||||
Some(value) => std::env::set_var(self.key, value),
|
||||
None => std::env::remove_var(self.key),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use std::sync::atomic::Ordering;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use codex_agent_identity::agent_identity_jwks_url;
|
||||
use codex_agent_identity::decode_agent_identity_jwt;
|
||||
use codex_agent_identity::fetch_agent_identity_jwks;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
@@ -27,7 +28,7 @@ use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use super::access_token::CodexAccessToken;
|
||||
use super::access_token::classify_codex_access_token;
|
||||
use super::external_bearer::BearerTokenRefresher;
|
||||
use super::revoke::revoke_auth_tokens;
|
||||
use super::revoke::revoke_auth_tokens_with_auth_route_config;
|
||||
pub use crate::auth::agent_identity::AgentIdentityAuth;
|
||||
pub use crate::auth::personal_access_token::PersonalAccessTokenAuth;
|
||||
pub use crate::auth::storage::AgentIdentityAuthRecord;
|
||||
@@ -35,12 +36,12 @@ pub use crate::auth::storage::AuthDotJson;
|
||||
use crate::auth::storage::AuthStorageBackend;
|
||||
use crate::auth::storage::create_auth_storage;
|
||||
use crate::auth::util::try_parse_error_message;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::default_client::create_client;
|
||||
use crate::default_client::build_default_reqwest_client_for_auth_route;
|
||||
use crate::default_client::create_client_for_auth_route;
|
||||
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;
|
||||
use codex_client::CodexHttpClient;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::auth::PlanType as InternalPlanType;
|
||||
@@ -87,7 +88,6 @@ pub struct ChatgptAuthTokens {
|
||||
#[derive(Debug, Clone)]
|
||||
struct ChatgptAuthState {
|
||||
auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>,
|
||||
client: CodexHttpClient,
|
||||
}
|
||||
|
||||
const TOKEN_REFRESH_INTERVAL: i64 = 8;
|
||||
@@ -210,9 +210,9 @@ impl CodexAuth {
|
||||
auth_dot_json: AuthDotJson,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let auth_mode = auth_dot_json.resolved_mode();
|
||||
let client = create_client();
|
||||
if auth_mode == ApiAuthMode::ApiKey {
|
||||
let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else {
|
||||
return Err(std::io::Error::other("API key auth is missing a key."));
|
||||
@@ -225,7 +225,12 @@ impl CodexAuth {
|
||||
"agent identity auth is missing an agent identity token.",
|
||||
));
|
||||
};
|
||||
return Self::from_agent_identity_jwt(&agent_identity, chatgpt_base_url).await;
|
||||
return Self::from_agent_identity_jwt_with_auth_route_config(
|
||||
&agent_identity,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if auth_mode == ApiAuthMode::PersonalAccessToken {
|
||||
let Some(personal_access_token) = auth_dot_json.personal_access_token.as_deref() else {
|
||||
@@ -239,7 +244,6 @@ impl CodexAuth {
|
||||
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
|
||||
let state = ChatgptAuthState {
|
||||
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
|
||||
client,
|
||||
};
|
||||
|
||||
match auth_mode {
|
||||
@@ -262,12 +266,28 @@ impl CodexAuth {
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
) -> std::io::Result<Option<Self>> {
|
||||
Self::from_auth_storage_with_auth_route_config(
|
||||
codex_home,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn from_auth_storage_with_auth_route_config(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Option<Self>> {
|
||||
load_auth(
|
||||
codex_home,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -275,13 +295,28 @@ impl CodexAuth {
|
||||
pub async fn from_agent_identity_jwt(
|
||||
jwt: &str,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
) -> std::io::Result<Self> {
|
||||
Self::from_agent_identity_jwt_with_auth_route_config(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn from_agent_identity_jwt_with_auth_route_config(
|
||||
jwt: &str,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<Self> {
|
||||
let base_url = chatgpt_base_url
|
||||
.unwrap_or(DEFAULT_CHATGPT_BACKEND_BASE_URL)
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let record = verified_agent_identity_record(jwt, &base_url).await?;
|
||||
Ok(Self::AgentIdentity(AgentIdentityAuth::load(record).await?))
|
||||
let record = verified_agent_identity_record(jwt, &base_url, auth_route_config).await?;
|
||||
Ok(Self::AgentIdentity(
|
||||
AgentIdentityAuth::load_with_auth_route_config(record, auth_route_config).await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn from_personal_access_token(access_token: &str) -> std::io::Result<Self> {
|
||||
@@ -474,10 +509,8 @@ impl CodexAuth {
|
||||
personal_access_token: None,
|
||||
};
|
||||
|
||||
let client = create_client();
|
||||
let state = ChatgptAuthState {
|
||||
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
|
||||
client,
|
||||
};
|
||||
let dummy_auth_id = NEXT_DUMMY_AUTH_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let storage = create_auth_storage(
|
||||
@@ -507,10 +540,6 @@ impl ChatgptAuth {
|
||||
fn storage(&self) -> &Arc<dyn AuthStorageBackend> {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
fn client(&self) -> &CodexHttpClient {
|
||||
&self.state.client
|
||||
}
|
||||
}
|
||||
|
||||
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
||||
@@ -542,9 +571,12 @@ fn read_non_empty_env_var(key: &str) -> Option<String> {
|
||||
async fn verified_agent_identity_record(
|
||||
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_reqwest_client_for_auth_route(&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)?;
|
||||
@@ -599,6 +631,23 @@ pub async fn login_with_access_token(
|
||||
access_token: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
) -> std::io::Result<()> {
|
||||
login_with_access_token_with_auth_route_config(
|
||||
codex_home,
|
||||
access_token,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn login_with_access_token_with_auth_route_config(
|
||||
codex_home: &Path,
|
||||
access_token: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_dot_json = match classify_codex_access_token(access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
@@ -619,7 +668,7 @@ pub async fn login_with_access_token(
|
||||
.unwrap_or(DEFAULT_CHATGPT_BACKEND_BASE_URL)
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
verified_agent_identity_record(jwt, &base_url).await?;
|
||||
verified_agent_identity_record(jwt, &base_url, auth_route_config).await?;
|
||||
AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::AgentIdentity),
|
||||
openai_api_key: None,
|
||||
@@ -685,11 +734,20 @@ pub struct AuthConfig {
|
||||
}
|
||||
|
||||
pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
|
||||
enforce_login_restrictions_with_auth_route_config(config, /*auth_route_config*/ None).await
|
||||
}
|
||||
|
||||
/// Enforces configured login restrictions using auth-owned HTTP settings.
|
||||
pub async fn enforce_login_restrictions_with_auth_route_config(
|
||||
config: &AuthConfig,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> std::io::Result<()> {
|
||||
let Some(auth) = load_auth(
|
||||
&config.codex_home,
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
config.auth_credentials_store_mode,
|
||||
config.chatgpt_base_url.as_deref(),
|
||||
auth_route_config,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
@@ -805,6 +863,7 @@ async fn load_auth(
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_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() {
|
||||
@@ -823,6 +882,7 @@ async fn load_auth(
|
||||
auth_dot_json,
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
return Ok(Some(auth));
|
||||
@@ -836,7 +896,11 @@ async fn load_auth(
|
||||
.map(Some)
|
||||
}
|
||||
CodexAccessToken::AgentIdentityJwt(jwt) => {
|
||||
CodexAuth::from_agent_identity_jwt(jwt, chatgpt_base_url)
|
||||
CodexAuth::from_agent_identity_jwt_with_auth_route_config(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
@@ -860,6 +924,7 @@ async fn load_auth(
|
||||
auth_dot_json,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
Ok(Some(auth))
|
||||
@@ -895,7 +960,7 @@ fn persist_tokens(
|
||||
// The caller is responsible for persisting any returned tokens.
|
||||
async fn request_chatgpt_token_refresh(
|
||||
refresh_token: String,
|
||||
client: &CodexHttpClient,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<RefreshResponse, RefreshTokenError> {
|
||||
let refresh_request = RefreshRequest {
|
||||
client_id: CLIENT_ID,
|
||||
@@ -904,6 +969,8 @@ async fn request_chatgpt_token_refresh(
|
||||
};
|
||||
|
||||
let endpoint = refresh_token_endpoint();
|
||||
let client = create_client_for_auth_route(&endpoint, auth_route_config)
|
||||
.map_err(|err| RefreshTokenError::Transient(err.into()))?;
|
||||
|
||||
// Use shared client factory to include standard headers
|
||||
let response = client
|
||||
@@ -1353,6 +1420,7 @@ pub struct AuthManager {
|
||||
chatgpt_base_url: Option<String>,
|
||||
refresh_lock: Semaphore,
|
||||
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
}
|
||||
|
||||
/// Configuration view required to construct a shared [`AuthManager`].
|
||||
@@ -1373,6 +1441,11 @@ 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> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AuthManager {
|
||||
@@ -1390,6 +1463,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()
|
||||
}
|
||||
@@ -1405,12 +1479,30 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<String>,
|
||||
) -> Self {
|
||||
Self::new_with_auth_route_config(
|
||||
codex_home,
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_with_auth_route_config(
|
||||
codex_home: PathBuf,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<String>,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
) -> Self {
|
||||
let managed_auth = load_auth(
|
||||
&codex_home,
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
chatgpt_base_url.as_deref(),
|
||||
auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
@@ -1429,6 +1521,7 @@ impl AuthManager {
|
||||
chatgpt_base_url,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1450,6 +1543,7 @@ impl AuthManager {
|
||||
chatgpt_base_url: None,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1470,6 +1564,7 @@ impl AuthManager {
|
||||
chatgpt_base_url: None,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1490,6 +1585,7 @@ impl AuthManager {
|
||||
external_auth: RwLock::new(Some(
|
||||
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
|
||||
)),
|
||||
auth_route_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1629,6 +1725,7 @@ impl AuthManager {
|
||||
self.enable_codex_api_key_env,
|
||||
self.auth_credentials_store_mode,
|
||||
self.chatgpt_base_url.as_deref(),
|
||||
self.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
@@ -1719,13 +1816,16 @@ impl AuthManager {
|
||||
config: &impl AuthManagerConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Arc<Self> {
|
||||
let auth_manager = Self::shared(
|
||||
config.codex_home(),
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
)
|
||||
.await;
|
||||
let auth_manager = Arc::new(
|
||||
Self::new_with_auth_route_config(
|
||||
config.codex_home(),
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
config.auth_route_config(),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id());
|
||||
auth_manager
|
||||
}
|
||||
@@ -1874,7 +1974,12 @@ 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_with_auth_route_config(
|
||||
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(&self.codex_home, self.auth_credentials_store_mode)?;
|
||||
@@ -1993,7 +2098,8 @@ impl AuthManager {
|
||||
auth: &ChatgptAuth,
|
||||
refresh_token: String,
|
||||
) -> Result<(), RefreshTokenError> {
|
||||
let refresh_response = request_chatgpt_token_refresh(refresh_token, auth.client()).await?;
|
||||
let refresh_response =
|
||||
request_chatgpt_token_refresh(refresh_token, self.auth_route_config.as_ref()).await?;
|
||||
|
||||
persist_tokens(
|
||||
auth.storage(),
|
||||
|
||||
@@ -13,5 +13,5 @@ mod revoke;
|
||||
pub use error::RefreshTokenFailedError;
|
||||
pub use error::RefreshTokenFailedReason;
|
||||
pub use manager::*;
|
||||
pub(crate) use revoke::revoke_auth_tokens;
|
||||
pub(crate) use revoke::revoke_auth_tokens_with_auth_route_config;
|
||||
pub(crate) use revoke::should_revoke_auth_tokens;
|
||||
|
||||
@@ -17,7 +17,8 @@ use super::manager::REVOKE_TOKEN_URL;
|
||||
use super::manager::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use super::storage::AuthDotJson;
|
||||
use super::util::try_parse_error_message;
|
||||
use crate::default_client::create_client;
|
||||
use crate::default_client::create_client_for_auth_route;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
use crate::token_data::TokenData;
|
||||
|
||||
const REVOKE_HTTP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -52,15 +53,16 @@ struct RevokeTokenRequest<'a> {
|
||||
client_id: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_auth_tokens(
|
||||
pub(crate) async fn revoke_auth_tokens_with_auth_route_config(
|
||||
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_client_for_auth_route(&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_auth_reqwest_client_with_auth_route_config;
|
||||
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,9 +157,13 @@ 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('/');
|
||||
let api_base_url = format!("{base_url}/api/accounts");
|
||||
let user_code_endpoint = format!("{api_base_url}/deviceauth/usercode");
|
||||
let client = build_auth_reqwest_client_with_auth_route_config(
|
||||
&user_code_endpoint,
|
||||
opts.auth_route_config.as_ref(),
|
||||
)?;
|
||||
let uc = request_user_code(&client, &api_base_url, &opts.client_id).await?;
|
||||
|
||||
Ok(DeviceCode {
|
||||
@@ -174,9 +178,13 @@ 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 api_base_url = format!("{base_url}/api/accounts");
|
||||
let token_endpoint = format!("{api_base_url}/deviceauth/token");
|
||||
let client = build_auth_reqwest_client_with_auth_route_config(
|
||||
&token_endpoint,
|
||||
opts.auth_route_config.as_ref(),
|
||||
)?;
|
||||
|
||||
let code_resp = poll_for_token(
|
||||
&client,
|
||||
@@ -199,6 +207,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}")))?;
|
||||
@@ -217,6 +226,7 @@ pub async fn complete_device_code_login(
|
||||
tokens.access_token,
|
||||
tokens.refresh_token,
|
||||
opts.cli_auth_credentials_store_mode,
|
||||
opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod auth_env_telemetry;
|
||||
pub mod token_data;
|
||||
|
||||
mod device_code_auth;
|
||||
mod outbound_proxy;
|
||||
mod pkce;
|
||||
mod server;
|
||||
|
||||
@@ -37,8 +38,10 @@ pub use auth::RefreshTokenError;
|
||||
pub use auth::UnauthorizedRecovery;
|
||||
pub use auth::default_client;
|
||||
pub use auth::enforce_login_restrictions;
|
||||
pub use auth::enforce_login_restrictions_with_auth_route_config;
|
||||
pub use auth::load_auth_dot_json;
|
||||
pub use auth::login_with_access_token;
|
||||
pub use auth::login_with_access_token_with_auth_route_config;
|
||||
pub use auth::login_with_api_key;
|
||||
pub use auth::logout;
|
||||
pub use auth::logout_with_revoke;
|
||||
@@ -47,4 +50,7 @@ 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 outbound_proxy::auth_route_config_from_system_proxy_config;
|
||||
pub use outbound_proxy::bootstrap_auth_route_config_from_system_proxy_config;
|
||||
pub use token_data::TokenData;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
use codex_client::OutboundProxyConfig;
|
||||
use codex_client::OutboundProxyMode;
|
||||
use codex_config::types::SystemProxyFeatureConfigToml;
|
||||
use codex_config::types::SystemProxyFeatureModeToml;
|
||||
|
||||
/// Stable route-selection policy for auth-owned clients.
|
||||
///
|
||||
/// Auth call sites should accept this type instead of lower-level resolver
|
||||
/// details. Config parsing decides whether to construct one, and the client
|
||||
/// layer remains responsible for platform-specific proxy resolution.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AuthRouteConfig {
|
||||
route_config: Option<OutboundProxyConfig>,
|
||||
}
|
||||
|
||||
impl AuthRouteConfig {
|
||||
pub fn auto() -> Self {
|
||||
Self::from_outbound_proxy_mode(OutboundProxyMode::Auto)
|
||||
}
|
||||
|
||||
pub fn env() -> Self {
|
||||
Self::from_outbound_proxy_mode(OutboundProxyMode::Env)
|
||||
}
|
||||
|
||||
pub fn system() -> Self {
|
||||
Self::from_outbound_proxy_mode(OutboundProxyMode::System)
|
||||
}
|
||||
|
||||
pub fn direct() -> Self {
|
||||
Self::from_outbound_proxy_mode(OutboundProxyMode::Direct)
|
||||
}
|
||||
|
||||
pub(crate) fn route_config(&self) -> Option<&OutboundProxyConfig> {
|
||||
self.route_config.as_ref()
|
||||
}
|
||||
|
||||
fn from_outbound_proxy_mode(mode: OutboundProxyMode) -> Self {
|
||||
Self {
|
||||
route_config: Some(OutboundProxyConfig::new(mode)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auth_route_config_from_system_proxy_config(
|
||||
system_proxy: &SystemProxyFeatureConfigToml,
|
||||
) -> AuthRouteConfig {
|
||||
match system_proxy.mode.unwrap_or_default() {
|
||||
SystemProxyFeatureModeToml::Auto => AuthRouteConfig::auto(),
|
||||
SystemProxyFeatureModeToml::Env => AuthRouteConfig::env(),
|
||||
SystemProxyFeatureModeToml::System => AuthRouteConfig::system(),
|
||||
SystemProxyFeatureModeToml::Direct => AuthRouteConfig::direct(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the auth route config for the explicit platform system-proxy startup path.
|
||||
pub fn bootstrap_auth_route_config_from_system_proxy_config(
|
||||
system_proxy: Option<&SystemProxyFeatureConfigToml>,
|
||||
) -> Option<AuthRouteConfig> {
|
||||
let system_proxy = system_proxy?;
|
||||
if system_proxy.mode != Some(SystemProxyFeatureModeToml::System) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Legacy startup auth builders only need the explicit platform system-proxy selection here.
|
||||
Some(auth_route_config_from_system_proxy_config(system_proxy))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn system_proxy_feature_config_maps_modes_and_startup_gate_together() {
|
||||
use SystemProxyFeatureModeToml as SystemProxyFeatureMode;
|
||||
|
||||
assert_eq!(
|
||||
bootstrap_auth_route_config_from_system_proxy_config(None),
|
||||
None
|
||||
);
|
||||
|
||||
let system_config = AuthRouteConfig {
|
||||
route_config: Some(OutboundProxyConfig::new(OutboundProxyMode::System)),
|
||||
};
|
||||
let cases = [
|
||||
(None, AuthRouteConfig::auto(), None),
|
||||
(
|
||||
Some(SystemProxyFeatureMode::Auto),
|
||||
AuthRouteConfig::auto(),
|
||||
None,
|
||||
),
|
||||
(
|
||||
Some(SystemProxyFeatureMode::Env),
|
||||
AuthRouteConfig::env(),
|
||||
None,
|
||||
),
|
||||
(
|
||||
Some(SystemProxyFeatureMode::Direct),
|
||||
AuthRouteConfig::direct(),
|
||||
None,
|
||||
),
|
||||
(
|
||||
Some(SystemProxyFeatureMode::System),
|
||||
system_config.clone(),
|
||||
Some(system_config),
|
||||
),
|
||||
];
|
||||
|
||||
for (mode, expected_config, expected_startup_config) in cases {
|
||||
let system_proxy = SystemProxyFeatureConfigToml {
|
||||
enabled: Some(true),
|
||||
mode,
|
||||
};
|
||||
assert_eq!(
|
||||
auth_route_config_from_system_proxy_config(&system_proxy),
|
||||
expected_config
|
||||
);
|
||||
assert_eq!(
|
||||
bootstrap_auth_route_config_from_system_proxy_config(Some(&system_proxy)),
|
||||
expected_startup_config
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,12 @@ use std::time::Duration;
|
||||
|
||||
use crate::auth::AuthDotJson;
|
||||
use crate::auth::load_auth_dot_json;
|
||||
use crate::auth::revoke_auth_tokens;
|
||||
use crate::auth::revoke_auth_tokens_with_auth_route_config;
|
||||
use crate::auth::save_auth;
|
||||
use crate::auth::should_revoke_auth_tokens;
|
||||
use crate::default_client::build_auth_reqwest_client_with_auth_route_config;
|
||||
use crate::default_client::originator;
|
||||
use crate::outbound_proxy::AuthRouteConfig;
|
||||
use crate::pkce::PkceCodes;
|
||||
use crate::pkce::generate_pkce;
|
||||
use crate::token_data::TokenData;
|
||||
@@ -37,7 +39,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;
|
||||
@@ -72,6 +73,7 @@ pub struct ServerOptions {
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
pub codex_streamlined_login: bool,
|
||||
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
pub auth_route_config: Option<AuthRouteConfig>,
|
||||
}
|
||||
|
||||
impl ServerOptions {
|
||||
@@ -92,8 +94,14 @@ impl ServerOptions {
|
||||
forced_chatgpt_workspace_id,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_credentials_store_mode,
|
||||
auth_route_config: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_auth_route_config(mut self, auth_route_config: Option<AuthRouteConfig>) -> Self {
|
||||
self.auth_route_config = auth_route_config;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle for a running login callback server.
|
||||
@@ -335,8 +343,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(
|
||||
@@ -352,9 +367,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(),
|
||||
@@ -362,6 +382,7 @@ async fn process_request(
|
||||
tokens.access_token.clone(),
|
||||
tokens.refresh_token.clone(),
|
||||
opts.cli_auth_credentials_store_mode,
|
||||
opts.auth_route_config.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -717,6 +738,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 {
|
||||
@@ -725,8 +747,9 @@ pub(crate) async fn exchange_code_for_tokens(
|
||||
refresh_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_auth_reqwest_client_with_auth_route_config(&token_endpoint, auth_route_config)?;
|
||||
info!(
|
||||
issuer = %sanitize_url_for_logging(issuer),
|
||||
token_endpoint = %sanitize_url_for_logging(&token_endpoint),
|
||||
@@ -793,6 +816,7 @@ pub(crate) async fn persist_tokens_async(
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> io::Result<()> {
|
||||
// Reuse existing synchronous logic but run it off the async runtime.
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
@@ -831,7 +855,9 @@ pub(crate) async fn persist_tokens_async(
|
||||
.map_err(|e| io::Error::other(format!("persist task failed: {e}")))??;
|
||||
|
||||
if should_revoke_auth_tokens(previous_auth.as_ref(), &auth)
|
||||
&& let Err(err) = revoke_auth_tokens(previous_auth.as_ref()).await
|
||||
&& let Err(err) =
|
||||
revoke_auth_tokens_with_auth_route_config(previous_auth.as_ref(), auth_route_config)
|
||||
.await
|
||||
{
|
||||
warn!("failed to revoke superseded auth tokens after login: {err}");
|
||||
}
|
||||
@@ -1137,14 +1163,16 @@ 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_auth_reqwest_client_with_auth_route_config(&token_endpoint, auth_route_config)?;
|
||||
let resp = client
|
||||
.post(token_endpoint)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -1157,8 +1185,11 @@ pub(crate) async fn obtain_api_key(
|
||||
urlencoding::encode("urn:ietf:params:oauth:token-type:id_token")
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
.await;
|
||||
let resp = match resp {
|
||||
Ok(resp) => resp,
|
||||
Err(error) => return Err(io::Error::other(error)),
|
||||
};
|
||||
if !resp.status().is_success() {
|
||||
return Err(io::Error::other(format!(
|
||||
"api key exchange failed with status {}",
|
||||
@@ -1242,6 +1273,7 @@ mod tests {
|
||||
"new-access".to_string(),
|
||||
"new-refresh".to_string(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1302,6 +1334,7 @@ mod tests {
|
||||
"new-access".to_string(),
|
||||
"shared-refresh".to_string(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*auth_route_config*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -121,6 +121,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,
|
||||
@@ -183,6 +184,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,
|
||||
@@ -223,6 +225,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,
|
||||
@@ -264,6 +267,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,
|
||||
@@ -324,6 +328,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,
|
||||
@@ -392,6 +397,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,
|
||||
@@ -537,6 +543,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,
|
||||
@@ -558,6 +565,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,
|
||||
|
||||
+29
-11
@@ -8,6 +8,7 @@ use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options;
|
||||
use crate::legacy_core::config::resolve_bootstrap_system_proxy_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;
|
||||
@@ -38,7 +39,7 @@ use codex_app_server_protocol::ThreadListCwdFilter;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage_with_auth_route_config;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLoadError;
|
||||
use codex_config::LoaderOverrides;
|
||||
@@ -46,9 +47,11 @@ use codex_config::format_config_error_with_source;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_login::AuthConfig;
|
||||
use codex_login::auth_route_config_from_system_proxy_config;
|
||||
use codex_login::bootstrap_auth_route_config_from_system_proxy_config;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_login::default_client::set_default_client_residency_requirement;
|
||||
use codex_login::enforce_login_restrictions;
|
||||
use codex_login::enforce_login_restrictions_with_auth_route_config;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::AltScreenMode;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
@@ -1019,13 +1022,17 @@ pub async fn run_main(
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
let system_proxy_config = resolve_bootstrap_system_proxy_config(&bootstrap_config_toml);
|
||||
let auth_route_config =
|
||||
bootstrap_auth_route_config_from_system_proxy_config(system_proxy_config.as_ref());
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage_with_auth_route_config(
|
||||
codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1220,14 +1227,21 @@ pub async fn run_main(
|
||||
}
|
||||
|
||||
if !app_server_target.uses_remote_workspace() {
|
||||
let auth_route_config = config
|
||||
.system_proxy
|
||||
.as_ref()
|
||||
.map(auth_route_config_from_system_proxy_config);
|
||||
#[allow(clippy::print_stderr)]
|
||||
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,
|
||||
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()),
|
||||
})
|
||||
if let Err(err) = enforce_login_restrictions_with_auth_route_config(
|
||||
&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
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.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("{err}");
|
||||
@@ -1488,11 +1502,15 @@ async fn run_ratatui_app(
|
||||
// and rebuild config. This avoids missing newly available cloud-managed policy due to login
|
||||
// status detection edge cases.
|
||||
if show_login_screen && !uses_remote_workspace {
|
||||
cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
let auth_route_config = bootstrap_auth_route_config_from_system_proxy_config(
|
||||
initial_config.system_proxy.as_ref(),
|
||||
);
|
||||
cloud_config_bundle = cloud_config_bundle_loader_for_storage_with_auth_route_config(
|
||||
initial_config.codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
initial_config.cli_auth_credentials_store_mode,
|
||||
initial_config.chatgpt_base_url.clone(),
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user