mirror of
https://github.com/openai/codex.git
synced 2026-10-15 16:48:52 +00:00
Fail closed when workload identity initialization fails (#38424)
## What changed - Treat workload identity environment markers as an explicit authentication selection, even when another process credential is present. - Return initialization errors from `AuthManager` and propagate them through commands and services instead of continuing with an unusable authentication state. - Make `codex login status` validate workload identity, keep the TUI on an embedded app server for local workload identity, and reject workload identity in `codex mcp-server`, where it is unsupported. ## Testing - Cover workload identity precedence and partial configuration errors. - Verify login status reports an unreadable identity assertion and app-server routing enforces the supported workload identity topology. GitOrigin-RevId: efc6b6b4cd4d61652617de82aaa3d7ffc75d6618
This commit is contained in:
@@ -771,7 +771,7 @@ async fn managed_auth_policy_survives_unusable_requirements_file_changes() -> Re
|
||||
let auth_manager = codex_login::AuthManager::shared_from_config(
|
||||
&startup, /*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
std::fs::write(
|
||||
&requirements_path,
|
||||
"allowed_login_methods = [\"chatgpt\"]\nallowed_chatgpt_workspaces = []\n",
|
||||
|
||||
@@ -406,14 +406,15 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
|
||||
args.config.auth_config().validate()?;
|
||||
let channel_capacity = args.channel_capacity.max(1);
|
||||
let installation_id = resolve_installation_id(&args.config.codex_home).await?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(args.config.as_ref(), args.enable_codex_api_key_env)
|
||||
.await
|
||||
.map_err(IoError::other)?;
|
||||
let (client_tx, mut client_rx) = mpsc::channel::<InProcessClientMessage>(channel_capacity);
|
||||
let (event_tx, event_rx) = mpsc::channel::<InProcessServerEvent>(channel_capacity);
|
||||
|
||||
let runtime_handle = tokio::spawn(async move {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::channel::<OutgoingEnvelope>(channel_capacity);
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(args.config.as_ref(), args.enable_codex_api_key_env)
|
||||
.await;
|
||||
let analytics_events_client =
|
||||
analytics_events_client_from_config(Arc::clone(&auth_manager), args.config.as_ref());
|
||||
let analytics_events_flush_client = analytics_events_client.clone();
|
||||
|
||||
@@ -506,7 +506,9 @@ pub async fn run_main_with_transport_options(
|
||||
config_manager
|
||||
.replace_thread_config_loader(Arc::clone(&discovered_thread_config_loader));
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
config_manager.replace_cloud_config_bundle_loader(
|
||||
auth_manager,
|
||||
config.chatgpt_base_url.clone(),
|
||||
@@ -760,7 +762,9 @@ pub async fn run_main_with_transport_options(
|
||||
drop(unix_socket_startup_lock);
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
let remote_control_enabled = remote_control_policy == RemoteControlPolicy::Allowed
|
||||
&& remote_control_explicitly_requested
|
||||
|
||||
@@ -234,7 +234,9 @@ async fn build_test_processor(
|
||||
) {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::channel(16);
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config.as_ref(), /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(config.as_ref(), /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect("test auth manager");
|
||||
let config_manager = ConfigManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
Vec::new(),
|
||||
|
||||
@@ -66,7 +66,7 @@ pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
|
||||
) -> anyhow::Result<T> {
|
||||
let chatgpt_base_url = &config.chatgpt_base_url;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await?;
|
||||
let auth = auth_manager
|
||||
.auth()
|
||||
.await
|
||||
|
||||
@@ -32,18 +32,18 @@ const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const CONNECTOR_METADATA_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const DEFAULT_APPS_PRODUCT_SKU: &str = "codex";
|
||||
|
||||
async fn apps_enabled(config: &Config) -> bool {
|
||||
async fn apps_enabled(config: &Config) -> anyhow::Result<bool> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await?;
|
||||
let auth = auth_manager.auth().await;
|
||||
config
|
||||
Ok(config
|
||||
.features
|
||||
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|
||||
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend)))
|
||||
}
|
||||
|
||||
async fn connector_auth(config: &Config) -> anyhow::Result<CodexAuth> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await?;
|
||||
let auth = auth_manager
|
||||
.auth()
|
||||
.await
|
||||
@@ -56,7 +56,7 @@ async fn connector_auth(config: &Config) -> anyhow::Result<CodexAuth> {
|
||||
}
|
||||
|
||||
pub async fn list_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
|
||||
if !apps_enabled(config).await {
|
||||
if !apps_enabled(config).await? {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let (connectors_result, accessible_result) = tokio::join!(
|
||||
@@ -82,7 +82,7 @@ pub async fn list_cached_all_connectors(
|
||||
config: &Config,
|
||||
plugin_apps: &[AppConnectorId],
|
||||
) -> Option<Vec<AppInfo>> {
|
||||
if !apps_enabled(config).await {
|
||||
if !apps_enabled(config).await.ok()? {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ pub async fn list_all_connectors_with_options(
|
||||
force_refetch: bool,
|
||||
plugin_apps: &[AppConnectorId],
|
||||
) -> anyhow::Result<Vec<AppInfo>> {
|
||||
if !apps_enabled(config).await {
|
||||
if !apps_enabled(config).await? {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let auth = connector_auth(config).await?;
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(super) async fn bootstrap_cloud_config_bundle(
|
||||
bootstrap_auth_config(codex_home.as_path(), &bootstrap_config)?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -350,8 +350,9 @@ async fn build_report(
|
||||
let config_result = load_config(root_config_overrides, interactive, arg0_paths).await;
|
||||
match &config_result {
|
||||
Ok(config) => {
|
||||
let auth_manager =
|
||||
let auth_manager_result =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await;
|
||||
let auth_manager = auth_manager_result.as_ref().ok().cloned();
|
||||
let reachability_plan = provider_reachability_plan(config);
|
||||
let (
|
||||
config_check,
|
||||
@@ -370,13 +371,27 @@ async fn build_report(
|
||||
reachability_check,
|
||||
) = tokio::join!(
|
||||
async { run_sync_check("config", progress.clone(), || config_check(config)) },
|
||||
async { run_sync_check("auth", progress.clone(), || auth_check(config)) },
|
||||
async {
|
||||
run_sync_check("auth", progress.clone(), || match &auth_manager_result {
|
||||
Ok(_) => auth_check(config),
|
||||
Err(error) => DoctorCheck::new(
|
||||
"auth.load",
|
||||
"auth",
|
||||
CheckStatus::Fail,
|
||||
"authentication could not be initialized",
|
||||
)
|
||||
.detail(error.to_string())
|
||||
.remediation(
|
||||
"Fix the reported authentication error, then rerun codex doctor.",
|
||||
),
|
||||
})
|
||||
},
|
||||
async { run_sync_check("updates", progress.clone(), || updates_check(config)) },
|
||||
async { run_sync_check("network", progress.clone(), network_check) },
|
||||
run_async_check(
|
||||
"websocket",
|
||||
progress.clone(),
|
||||
websocket_reachability_check(config, Some(auth_manager)),
|
||||
websocket_reachability_check(config, auth_manager),
|
||||
),
|
||||
run_async_check("MCP", progress.clone(), mcp_check(config)),
|
||||
async {
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::is_workload_identity_selected;
|
||||
use codex_login::login_with_access_token;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::logout_with_revoke;
|
||||
@@ -439,8 +441,21 @@ 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;
|
||||
|
||||
match config
|
||||
.auth_config()
|
||||
if is_workload_identity_selected() {
|
||||
match AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await {
|
||||
Ok(_) => {
|
||||
eprintln!("Logged in using workload identity");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error checking login status: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let auth_config = config.auth_config();
|
||||
match auth_config
|
||||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1826,7 +1826,7 @@ async fn load_exec_server_remote_auth_provider(
|
||||
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
|
||||
})?;
|
||||
let auth = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.await?
|
||||
.auth()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Agent Identity authentication is unavailable"))?;
|
||||
@@ -1914,7 +1914,7 @@ async fn load_exec_server_remote_auth(
|
||||
missing_auth_error: &'static str,
|
||||
) -> anyhow::Result<codex_login::CodexAuth> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await?;
|
||||
|
||||
let auth = match auth_manager.auth().await {
|
||||
Some(auth) => auth,
|
||||
@@ -2076,7 +2076,7 @@ async fn run_debug_prompt_input_command(
|
||||
config.codex_home.clone(),
|
||||
));
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?;
|
||||
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::new();
|
||||
codex_git_attribution::install(
|
||||
&mut extensions,
|
||||
@@ -2122,7 +2122,7 @@ async fn run_debug_models_command(
|
||||
.build()
|
||||
.await?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ true).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ true).await?;
|
||||
let models_manager = build_models_manager(&config, auth_manager);
|
||||
models_manager
|
||||
.raw_model_catalog(
|
||||
|
||||
@@ -212,7 +212,7 @@ async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceAr
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let manager = plugins_manager_for_config(&config);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await?);
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let marketplace_listing = manager
|
||||
.discover_marketplaces_for_config(&plugins_input, &[])
|
||||
|
||||
@@ -523,14 +523,14 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_mcp_manager(config: &Config) -> McpManager {
|
||||
async fn load_mcp_manager(config: &Config) -> Result<McpManager> {
|
||||
let plugins_manager = Arc::new(plugins_manager_for_config(config));
|
||||
plugins_manager.set_auth_mode(load_cli_auth_mode(config).await);
|
||||
McpManager::new(plugins_manager)
|
||||
plugins_manager.set_auth_mode(load_cli_auth_mode(config).await?);
|
||||
Ok(McpManager::new(plugins_manager))
|
||||
}
|
||||
|
||||
async fn run_login(config: &Config, login_args: LoginArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
|
||||
let LoginArgs {
|
||||
@@ -599,7 +599,7 @@ async fn run_login(config: &Config, login_args: LoginArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run_logout(config: &Config, logout_args: LogoutArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
|
||||
let LogoutArgs { name } = logout_args;
|
||||
@@ -629,9 +629,9 @@ async fn run_logout(config: &Config, logout_args: LogoutArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run_list(config: &Config, list_args: ListArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await?;
|
||||
let auth = auth_manager.auth().await;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
let effective_mcp_servers = mcp_manager.effective_servers(config, auth.as_ref()).await;
|
||||
@@ -894,7 +894,7 @@ async fn run_list(config: &Config, list_args: ListArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run_get(config: &Config, get_args: GetArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
|
||||
let Some(server) = mcp_servers.get(&get_args.name) else {
|
||||
|
||||
@@ -38,7 +38,8 @@ pub(super) async fn load_mcp_config(
|
||||
.context("failed to resolve cloud configuration authentication")?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.context("failed to initialize cloud configuration authentication")?;
|
||||
|
||||
ConfigBuilder::default()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
|
||||
@@ -591,7 +591,7 @@ async fn load_plugin_command_context(
|
||||
.context("failed to load configuration")?;
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let manager = plugins_manager_for_config(&config);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await?);
|
||||
Ok(PluginCommandContext {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
plugins_input,
|
||||
@@ -599,12 +599,14 @@ async fn load_plugin_command_context(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn load_cli_auth_mode(config: &Config) -> Option<AuthMode> {
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true)
|
||||
.await
|
||||
.auth()
|
||||
.await
|
||||
.map(|auth| auth.api_auth_mode())
|
||||
pub(crate) async fn load_cli_auth_mode(config: &Config) -> Result<Option<AuthMode>> {
|
||||
Ok(
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true)
|
||||
.await?
|
||||
.auth()
|
||||
.await
|
||||
.map(|auth| auth.api_auth_mode()),
|
||||
)
|
||||
}
|
||||
|
||||
struct PluginSelection {
|
||||
|
||||
@@ -9,7 +9,10 @@ use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CODEX_ACCESS_TOKEN_ENV_VAR;
|
||||
use codex_login::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_protocol::shell_environment::OPENAI_FEDERATION_RULE_ID_ENV_VAR;
|
||||
use codex_protocol::shell_environment::OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR;
|
||||
use predicates::str::contains;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
@@ -81,6 +84,24 @@ fn login_status_reports_auth_storage_errors() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_status_validates_configured_workload_identity() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_file_auth_config(codex_home.path())?;
|
||||
let missing_assertion = codex_home.path().join("missing-identity-token");
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.env_remove(CODEX_ACCESS_TOKEN_ENV_VAR)
|
||||
.env(OPENAI_FEDERATION_RULE_ID_ENV_VAR, "rule-test")
|
||||
.env(OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR, &missing_assertion)
|
||||
.args(["login", "status"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("could not read workload identity assertion file"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_with_access_token_rejects_invalid_jwt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -87,19 +87,25 @@ where
|
||||
pub async fn cloud_config_bundle_loader_for_storage(
|
||||
auth_config: AuthConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> CloudConfigBundleLoader {
|
||||
let codex_home = auth_config.codex_home.clone();
|
||||
let chatgpt_base_url = auth_config
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let http_client_factory = auth_config.auth_route_config.http_client_factory().clone();
|
||||
) -> std::io::Result<CloudConfigBundleLoader> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_auth_config(auth_config, enable_codex_api_key_env).await;
|
||||
AuthManager::shared_from_auth_config(auth_config.clone(), enable_codex_api_key_env).await?;
|
||||
Ok(cloud_config_bundle_loader_from_auth_config(
|
||||
auth_config,
|
||||
auth_manager,
|
||||
))
|
||||
}
|
||||
|
||||
fn cloud_config_bundle_loader_from_auth_config(
|
||||
auth_config: AuthConfig,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
) -> CloudConfigBundleLoader {
|
||||
cloud_config_bundle_loader(
|
||||
auth_manager,
|
||||
chatgpt_base_url,
|
||||
codex_home,
|
||||
http_client_factory,
|
||||
auth_config
|
||||
.chatgpt_base_url
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
|
||||
auth_config.codex_home,
|
||||
auth_config.auth_route_config.http_client_factory().clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,8 +61,18 @@ pub async fn load_auth_manager(
|
||||
let http_client_factory = config.http_client_factory();
|
||||
let mut auth_config = config.auth_config();
|
||||
auth_config.chatgpt_base_url = chatgpt_base_url.or(Some(config.chatgpt_base_url.clone()));
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_auth_config(auth_config, /*enable_codex_api_key_env*/ false).await;
|
||||
let auth_manager = match AuthManager::shared_from_auth_config(
|
||||
auth_config,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(auth_manager) => auth_manager,
|
||||
Err(error) => {
|
||||
append_error_log(format!("failed to load auth: {error}"));
|
||||
return (None, http_client_factory);
|
||||
}
|
||||
};
|
||||
(Some(auth_manager), http_client_factory)
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,9 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools(
|
||||
config: &Config,
|
||||
) -> Option<Vec<AppInfo>> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.ok()?;
|
||||
let auth = auth_manager.auth().await;
|
||||
if !config
|
||||
.features
|
||||
@@ -205,7 +207,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await?;
|
||||
let auth = auth_manager.auth().await;
|
||||
if !config
|
||||
.features
|
||||
@@ -427,8 +429,11 @@ async fn cached_directory_connectors_for_tool_suggest_with_auth(
|
||||
let auth = if let Some(auth) = auth {
|
||||
Some(auth)
|
||||
} else {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
let Ok(auth_manager) =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
loaded_auth = auth_manager.auth().await;
|
||||
loaded_auth.as_ref()
|
||||
};
|
||||
|
||||
@@ -33,7 +33,9 @@ pub async fn build_prompt_input(
|
||||
config.ephemeral = true;
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.map_err(|err| CodexErr::Fatal(err.to_string()))?;
|
||||
|
||||
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
|
||||
config.codex_self_exe.clone(),
|
||||
|
||||
@@ -289,6 +289,7 @@ async fn run_script_with_timeout(
|
||||
// Handler is kept as guard to control the drop. The `mut` pattern is required because .args()
|
||||
// returns a ref of handler.
|
||||
let mut handler = Command::new(&args[0]);
|
||||
codex_protocol::shell_environment::scrub_non_inheritable_env_vars(handler.as_std_mut());
|
||||
handler.args(&args[1..]);
|
||||
handler.stdin(Stdio::null());
|
||||
handler.current_dir(cwd);
|
||||
|
||||
@@ -81,6 +81,7 @@ use codex_history::RolloutLine;
|
||||
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::is_workload_identity_selected;
|
||||
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;
|
||||
@@ -342,11 +343,15 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
)
|
||||
.await;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
let bootstrap_auth_config = bootstrap_auth_config(&codex_home, &bootstrap_config)?;
|
||||
// API keys cannot fetch workspace-managed configuration. Preserve the
|
||||
// existing ChatGPT bootstrap identity even when model requests allow
|
||||
// CODEX_API_KEY.
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
bootstrap_auth_config(&codex_home, &bootstrap_config)?,
|
||||
bootstrap_auth_config,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
let run_cli_overrides = cli_kv_overrides.clone();
|
||||
let run_loader_overrides = loader_overrides.clone();
|
||||
let run_cloud_config_bundle = cloud_config_bundle.clone();
|
||||
@@ -460,7 +465,9 @@ 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(&config.auth_config()).await {
|
||||
if !is_workload_identity_selected()
|
||||
&& let Err(err) = enforce_login_restrictions(&config.auth_config()).await
|
||||
{
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
@@ -1809,19 +1809,17 @@ fn auth_config_from_preserves_all_fields() {
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn try_shared_from_config_rejects_partial_workload_identity_configuration() {
|
||||
async fn shared_from_config_prefers_workload_identity_to_explicit_access_token() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let config = test_auth_manager_config(codex_home.path());
|
||||
let _access_token_guard = remove_access_token_env_var();
|
||||
let _access_token_guard = EnvVarGuard::set(CODEX_ACCESS_TOKEN_ENV_VAR, "at-explicit");
|
||||
let _rule_guard = EnvVarGuard::set(OPENAI_FEDERATION_RULE_ID_ENV_VAR, "rule-one");
|
||||
let _assertion_file_guard = EnvVarGuard::remove(OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR);
|
||||
|
||||
let error =
|
||||
AuthManager::try_shared_from_config(&config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect_err("partial workload identity config should fail closed");
|
||||
let error = AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect_err("partial workload identity config should fail closed");
|
||||
|
||||
assert!(matches!(error, RefreshTokenError::Permanent(_)));
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
@@ -2148,7 +2146,9 @@ async fn auth_manager_rejects_disallowed_stored_and_external_auth() {
|
||||
.await;
|
||||
config.managed_auth_policy.allowed_login_methods = Some(vec![ForcedLoginMethod::Chatgpt]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect("auth manager");
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
assert!(
|
||||
@@ -2177,7 +2177,9 @@ async fn api_only_policy_rejects_access_tokens_before_hydration() {
|
||||
.await;
|
||||
config.managed_auth_policy.allowed_login_methods = Some(vec![ForcedLoginMethod::Api]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect("auth manager");
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
assert!(
|
||||
@@ -2210,7 +2212,9 @@ async fn workspace_policy_rejects_agent_identity_before_hydration() {
|
||||
config.managed_auth_policy.allowed_chatgpt_workspaces =
|
||||
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect("auth manager");
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
drop(access_token_guard);
|
||||
@@ -2243,7 +2247,9 @@ async fn workspace_policy_rejects_agent_identity_before_hydration() {
|
||||
config.managed_auth_policy.allowed_chatgpt_workspaces =
|
||||
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect("auth manager");
|
||||
assert_eq!(manager.auth().await, None);
|
||||
}
|
||||
|
||||
@@ -2279,7 +2285,9 @@ async fn workspace_policy_checks_the_selected_request_account() {
|
||||
)
|
||||
.await;
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.expect("auth manager");
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
}
|
||||
|
||||
@@ -206,6 +206,37 @@ pub enum RefreshTokenError {
|
||||
Transient(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Error returned when constructing an [`AuthManager`] from resolved configuration.
|
||||
#[derive(Debug, Error)]
|
||||
#[error(transparent)]
|
||||
pub struct AuthManagerInitializationError(AuthManagerInitializationErrorSource);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum AuthManagerInitializationErrorSource {
|
||||
#[error(transparent)]
|
||||
WorkloadIdentityConfiguration(WorkloadIdentitySessionError),
|
||||
#[error(transparent)]
|
||||
InitialAuth(RefreshTokenError),
|
||||
}
|
||||
|
||||
impl From<WorkloadIdentitySessionError> for AuthManagerInitializationError {
|
||||
fn from(error: WorkloadIdentitySessionError) -> Self {
|
||||
Self(AuthManagerInitializationErrorSource::WorkloadIdentityConfiguration(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefreshTokenError> for AuthManagerInitializationError {
|
||||
fn from(error: RefreshTokenError) -> Self {
|
||||
Self(AuthManagerInitializationErrorSource::InitialAuth(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthManagerInitializationError> for std::io::Error {
|
||||
fn from(error: AuthManagerInitializationError) -> Self {
|
||||
Self::other(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ExternalAuthRefreshReason {
|
||||
Unauthorized,
|
||||
@@ -2615,40 +2646,20 @@ impl AuthManager {
|
||||
)
|
||||
}
|
||||
|
||||
/// Convenience constructor returning an `Arc` wrapper from resolved config.
|
||||
/// Builds a shared manager and activates process-configured workload identity when selected.
|
||||
pub async fn shared_from_config(
|
||||
config: &impl AuthManagerConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Arc<Self> {
|
||||
) -> Result<Arc<Self>, AuthManagerInitializationError> {
|
||||
Self::shared_from_auth_config(auth_config_from(config), enable_codex_api_key_env).await
|
||||
}
|
||||
|
||||
/// Builds a shared manager using restrictions resolved before authentication.
|
||||
/// Activates workload identity against an auth config resolved before full runtime config.
|
||||
pub async fn shared_from_auth_config(
|
||||
auth_config: AuthConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self::new_from_auth_config(auth_config, enable_codex_api_key_env).await)
|
||||
}
|
||||
|
||||
/// Constructs a manager and activates process-configured workload identity when selected.
|
||||
pub async fn try_shared_from_config(
|
||||
config: &impl AuthManagerConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Result<Arc<Self>, RefreshTokenError> {
|
||||
Self::try_shared_from_auth_config(auth_config_from(config), enable_codex_api_key_env).await
|
||||
}
|
||||
|
||||
/// Activates workload identity against an auth config resolved before full runtime config.
|
||||
pub async fn try_shared_from_auth_config(
|
||||
auth_config: AuthConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Result<Arc<Self>, RefreshTokenError> {
|
||||
let external_auth = WorkloadIdentityExternalAuth::from_process_config(
|
||||
&auth_config,
|
||||
enable_codex_api_key_env,
|
||||
)
|
||||
.map_err(configured_workload_identity_error)?;
|
||||
) -> Result<Arc<Self>, AuthManagerInitializationError> {
|
||||
let external_auth = WorkloadIdentityExternalAuth::from_process_config(&auth_config)?;
|
||||
let mut manager = Self::new_from_auth_config(auth_config, enable_codex_api_key_env).await;
|
||||
manager.workload_identity_selected = external_auth.is_some();
|
||||
let manager = Arc::new(manager);
|
||||
@@ -2967,13 +2978,6 @@ fn auth_config_from(config: &impl AuthManagerConfig) -> AuthConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_workload_identity_error(error: WorkloadIdentitySessionError) -> RefreshTokenError {
|
||||
RefreshTokenError::Permanent(RefreshTokenFailedError::new(
|
||||
RefreshTokenFailedReason::Other,
|
||||
error.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "auth_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -26,8 +26,6 @@ use super::ExternalAuthRefreshContext;
|
||||
use super::RefreshTokenError;
|
||||
use super::RefreshTokenFailedError;
|
||||
use super::RefreshTokenFailedReason;
|
||||
use super::read_codex_access_token_from_env;
|
||||
use super::read_codex_api_key_from_env;
|
||||
use crate::AuthRouteConfig;
|
||||
|
||||
const PROD_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
|
||||
@@ -97,22 +95,21 @@ pub(super) enum WorkloadIdentitySessionError {
|
||||
InvalidConfiguration(String),
|
||||
}
|
||||
|
||||
pub fn is_workload_identity_selected(
|
||||
auth_config: &AuthConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> bool {
|
||||
!has_explicit_process_auth(auth_config, enable_codex_api_key_env)
|
||||
&& ProcessEnvironment::read().has_marker()
|
||||
/// Returns whether workload identity was selected through process configuration.
|
||||
///
|
||||
/// Either marker selects workload identity. Partial configuration then fails validation rather
|
||||
/// than falling back to another credential source.
|
||||
pub fn is_workload_identity_selected() -> bool {
|
||||
ProcessEnvironment::read().has_marker()
|
||||
}
|
||||
|
||||
fn resolve_config(
|
||||
chatgpt_base_url: &str,
|
||||
environment: ProcessEnvironment,
|
||||
has_explicit_process_auth: bool,
|
||||
chatgpt_login_allowed: bool,
|
||||
auth_route_config: AuthRouteConfig,
|
||||
) -> Result<Option<WorkloadIdentitySessionConfig>, WorkloadIdentitySessionError> {
|
||||
if has_explicit_process_auth || !environment.has_marker() {
|
||||
if !environment.has_marker() {
|
||||
return Ok(None);
|
||||
}
|
||||
if !chatgpt_login_allowed {
|
||||
@@ -140,14 +137,6 @@ fn resolve_config(
|
||||
}))
|
||||
}
|
||||
|
||||
fn has_explicit_process_auth(auth_config: &AuthConfig, enable_codex_api_key_env: bool) -> bool {
|
||||
(enable_codex_api_key_env
|
||||
&& auth_config.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
&& read_codex_api_key_from_env().is_some())
|
||||
|| (auth_config.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
&& read_codex_access_token_from_env().is_some())
|
||||
}
|
||||
|
||||
fn required_path(
|
||||
value: Option<OsString>,
|
||||
variable: &'static str,
|
||||
@@ -296,17 +285,6 @@ struct WorkloadIdentitySessionEntry {
|
||||
}
|
||||
|
||||
impl WorkloadIdentitySessionRegistry {
|
||||
fn has_active_session(&self) -> Result<bool, WorkloadIdentitySessionError> {
|
||||
let entry = self
|
||||
.entry
|
||||
.lock()
|
||||
.map_err(|_| WorkloadIdentitySessionError::RegistryUnavailable)?;
|
||||
Ok(entry
|
||||
.as_ref()
|
||||
.and_then(|active| active.session.upgrade())
|
||||
.is_some())
|
||||
}
|
||||
|
||||
fn session(
|
||||
&self,
|
||||
config: WorkloadIdentitySessionConfig,
|
||||
@@ -347,17 +325,14 @@ pub(super) struct WorkloadIdentityExternalAuth {
|
||||
impl WorkloadIdentityExternalAuth {
|
||||
pub(super) fn from_process_config(
|
||||
auth_config: &AuthConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Result<Option<Self>, WorkloadIdentitySessionError> {
|
||||
let registry = process_registry();
|
||||
let active_session = registry.has_active_session()?;
|
||||
resolve_config(
|
||||
auth_config
|
||||
.chatgpt_base_url
|
||||
.as_deref()
|
||||
.unwrap_or("https://chatgpt.com/backend-api"),
|
||||
ProcessEnvironment::read(),
|
||||
has_explicit_process_auth(auth_config, enable_codex_api_key_env) && !active_session,
|
||||
auth_config.is_login_method_allowed(ForcedLoginMethod::Chatgpt),
|
||||
auth_config.auth_route_config.clone(),
|
||||
)?
|
||||
|
||||
@@ -26,14 +26,12 @@ fn complete_environment() -> ProcessEnvironment {
|
||||
|
||||
fn resolve_for_test(
|
||||
environment: ProcessEnvironment,
|
||||
has_explicit_process_auth: bool,
|
||||
chatgpt_login_allowed: bool,
|
||||
chatgpt_base_url: &str,
|
||||
) -> Result<Option<WorkloadIdentitySessionConfig>, WorkloadIdentitySessionError> {
|
||||
resolve_config(
|
||||
chatgpt_base_url,
|
||||
environment,
|
||||
has_explicit_process_auth,
|
||||
chatgpt_login_allowed,
|
||||
auth_route_config(OutboundProxyPolicy::ReqwestDefault),
|
||||
)
|
||||
@@ -44,27 +42,12 @@ fn markers_select_wif_and_partial_configuration_fails_closed() {
|
||||
assert!(
|
||||
resolve_for_test(
|
||||
ProcessEnvironment::default(),
|
||||
/*has_explicit_process_auth*/ false,
|
||||
/*chatgpt_login_allowed*/ true,
|
||||
"https://chatgpt.com/backend-api",
|
||||
)
|
||||
.expect("no markers")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
resolve_for_test(
|
||||
ProcessEnvironment {
|
||||
federation_rule_id: Some("partial".into()),
|
||||
..Default::default()
|
||||
},
|
||||
/*has_explicit_process_auth*/ true,
|
||||
/*chatgpt_login_allowed*/ true,
|
||||
"https://chatgpt.com/backend-api",
|
||||
)
|
||||
.expect("explicit auth wins")
|
||||
.is_none()
|
||||
);
|
||||
|
||||
for (environment, missing) in [
|
||||
(
|
||||
ProcessEnvironment {
|
||||
@@ -83,7 +66,6 @@ fn markers_select_wif_and_partial_configuration_fails_closed() {
|
||||
] {
|
||||
let error = resolve_for_test(
|
||||
environment,
|
||||
/*has_explicit_process_auth*/ false,
|
||||
/*chatgpt_login_allowed*/ true,
|
||||
"https://chatgpt.com/backend-api",
|
||||
)
|
||||
@@ -98,7 +80,6 @@ fn markers_select_wif_and_partial_configuration_fails_closed() {
|
||||
assert!(
|
||||
resolve_for_test(
|
||||
relative,
|
||||
/*has_explicit_process_auth*/ false,
|
||||
/*chatgpt_login_allowed*/ true,
|
||||
"https://chatgpt.com/backend-api",
|
||||
)
|
||||
@@ -112,7 +93,6 @@ fn markers_select_wif_and_partial_configuration_fails_closed() {
|
||||
fn auth_policy_and_app_environment_are_enforced() {
|
||||
let policy_error = resolve_for_test(
|
||||
complete_environment(),
|
||||
/*has_explicit_process_auth*/ false,
|
||||
/*chatgpt_login_allowed*/ false,
|
||||
"https://chatgpt.com/backend-api",
|
||||
)
|
||||
@@ -133,7 +113,6 @@ fn auth_policy_and_app_environment_are_enforced() {
|
||||
] {
|
||||
let config = resolve_for_test(
|
||||
complete_environment(),
|
||||
/*has_explicit_process_auth*/ false,
|
||||
/*chatgpt_login_allowed*/ true,
|
||||
chatgpt_base_url,
|
||||
)
|
||||
@@ -145,7 +124,6 @@ fn auth_policy_and_app_environment_are_enforced() {
|
||||
|
||||
let error = resolve_for_test(
|
||||
complete_environment(),
|
||||
/*has_explicit_process_auth*/ false,
|
||||
/*chatgpt_login_allowed*/ true,
|
||||
"https://example.invalid/backend-api",
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ pub use auth::AuthHeaders;
|
||||
pub use auth::AuthKeyringBackendKind;
|
||||
pub use auth::AuthManager;
|
||||
pub use auth::AuthManagerConfig;
|
||||
pub use auth::AuthManagerInitializationError;
|
||||
pub use auth::CLIENT_ID;
|
||||
pub use auth::CLIENT_ID_OVERRIDE_ENV_VAR;
|
||||
pub use auth::CODEX_ACCESS_TOKEN_ENV_VAR;
|
||||
|
||||
@@ -64,6 +64,7 @@ pub async fn run_main(
|
||||
cli_config_overrides: CliConfigOverrides,
|
||||
strict_config: bool,
|
||||
) -> IoResult<()> {
|
||||
reject_workload_identity(codex_login::is_workload_identity_selected())?;
|
||||
// Parse CLI overrides once and derive the base Config eagerly so later
|
||||
// components do not need to work with raw TOML values.
|
||||
let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| {
|
||||
@@ -161,7 +162,7 @@ pub async fn run_main(
|
||||
state_db,
|
||||
installation_id,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
async move {
|
||||
while let Some(msg) = incoming_rx.recv().await {
|
||||
match msg {
|
||||
@@ -207,6 +208,20 @@ pub async fn run_main(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reject_workload_identity(workload_identity_selected: bool) -> IoResult<()> {
|
||||
if workload_identity_selected {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
"workload identity is not supported by `codex mcp-server`",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "workload_identity_tests.rs"]
|
||||
mod workload_identity_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -54,13 +54,14 @@ impl MessageProcessor {
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
installation_id: String,
|
||||
) -> Self {
|
||||
) -> std::io::Result<Self> {
|
||||
let outgoing = Arc::new(outgoing);
|
||||
let auth_manager = AuthManager::shared_from_config(
|
||||
config.as_ref(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new(
|
||||
config.codex_home.clone(),
|
||||
));
|
||||
@@ -110,13 +111,13 @@ impl MessageProcessor {
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
));
|
||||
Self {
|
||||
Ok(Self {
|
||||
outgoing,
|
||||
initialized: false,
|
||||
arg0_paths,
|
||||
thread_manager,
|
||||
active_turns,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn process_request(&mut self, request: JsonRpcRequest<ClientRequest>) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use super::reject_workload_identity;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn workload_identity_markers_are_rejected() {
|
||||
let error = reject_workload_identity(/*workload_identity_selected*/ true)
|
||||
.expect_err("mcp-server does not support workload identity");
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"workload identity is not supported by `codex mcp-server`"
|
||||
);
|
||||
reject_workload_identity(/*workload_identity_selected*/ false)
|
||||
.expect("mcp-server remains available without workload identity");
|
||||
}
|
||||
@@ -117,7 +117,7 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
let state_db = init_state_db(&config).await;
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?;
|
||||
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
|
||||
config.codex_self_exe.clone(),
|
||||
config.codex_linux_sandbox_exe.clone(),
|
||||
|
||||
+94
-27
@@ -51,6 +51,7 @@ use codex_login::AuthConfig;
|
||||
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::is_workload_identity_selected;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
use codex_protocol::config_types::AltScreenMode;
|
||||
@@ -834,8 +835,18 @@ fn app_server_target_for_launch(
|
||||
explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
|
||||
default_daemon_socket: Option<AbsolutePathBuf>,
|
||||
can_reuse_implicit_local_daemon: bool,
|
||||
) -> AppServerTarget {
|
||||
match explicit_remote_endpoint {
|
||||
workload_identity_selected: bool,
|
||||
) -> std::io::Result<AppServerTarget> {
|
||||
if workload_identity_selected {
|
||||
if explicit_remote_endpoint.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"workload identity must be configured on the remote app-server host",
|
||||
));
|
||||
}
|
||||
return Ok(AppServerTarget::Embedded);
|
||||
}
|
||||
Ok(match explicit_remote_endpoint {
|
||||
Some(endpoint) => AppServerTarget::Remote { endpoint },
|
||||
None if can_reuse_implicit_local_daemon => {
|
||||
default_daemon_socket.map_or(AppServerTarget::Embedded, |socket_path| {
|
||||
@@ -845,7 +856,20 @@ fn app_server_target_for_launch(
|
||||
})
|
||||
}
|
||||
None => AppServerTarget::Embedded,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn cloud_config_bundle_for_app_server_target(
|
||||
app_server_target: &AppServerTarget,
|
||||
bootstrap_config: &ConfigTomlLoadResult,
|
||||
codex_home: &Path,
|
||||
) -> std::io::Result<CloudConfigBundleLoader> {
|
||||
cloud_config_bundle_loader_for_storage(
|
||||
app_server_target
|
||||
.auth_config_for_cloud_loader(bootstrap_auth_config(codex_home, bootstrap_config)?),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn loader_overrides_are_default(loader_overrides: &LoaderOverrides) -> bool {
|
||||
@@ -939,12 +963,14 @@ pub async fn run_main(
|
||||
launch_loader_overrides.user_config_path = Some(user_config_path);
|
||||
launch_loader_overrides.user_config_profile = Some(profile_v2.clone());
|
||||
}
|
||||
let reuse_implicit_local_daemon = can_reuse_implicit_local_daemon(
|
||||
&cli_kv_overrides,
|
||||
&launch_loader_overrides,
|
||||
strict_config,
|
||||
cli.bypass_hook_trust,
|
||||
);
|
||||
let workload_identity_selected = is_workload_identity_selected();
|
||||
let reuse_implicit_local_daemon = !workload_identity_selected
|
||||
&& can_reuse_implicit_local_daemon(
|
||||
&cli_kv_overrides,
|
||||
&launch_loader_overrides,
|
||||
strict_config,
|
||||
cli.bypass_hook_trust,
|
||||
);
|
||||
let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
|
||||
maybe_probe_default_daemon_socket(&codex_home).await
|
||||
} else {
|
||||
@@ -954,7 +980,8 @@ pub async fn run_main(
|
||||
explicit_remote_endpoint,
|
||||
default_daemon,
|
||||
reuse_implicit_local_daemon,
|
||||
);
|
||||
workload_identity_selected,
|
||||
)?;
|
||||
let remote_cwd_override = cli
|
||||
.cwd
|
||||
.clone()
|
||||
@@ -995,12 +1022,12 @@ pub async fn run_main(
|
||||
)
|
||||
.await;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
app_server_target
|
||||
.auth_config_for_cloud_loader(bootstrap_auth_config(&codex_home, &bootstrap_config)?),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
let cloud_config_bundle = cloud_config_bundle_for_app_server_target(
|
||||
&app_server_target,
|
||||
&bootstrap_config,
|
||||
&codex_home,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
let cwd_override = if app_server_target.uses_remote_workspace() {
|
||||
None
|
||||
@@ -1090,11 +1117,15 @@ pub async fn run_main(
|
||||
)
|
||||
.await;
|
||||
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
app_server_target.auth_config_for_cloud_loader(config.auth_config()),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
let cloud_config_bundle = if workload_identity_selected {
|
||||
cloud_config_bundle
|
||||
} else {
|
||||
cloud_config_bundle_loader_for_storage(
|
||||
app_server_target.auth_config_for_cloud_loader(config.auth_config()),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
let environment_manager = Arc::new(
|
||||
prepared_environment_manager
|
||||
.build(Some(local_runtime_paths), config.http_client_factory())
|
||||
@@ -1160,7 +1191,7 @@ pub async fn run_main(
|
||||
}
|
||||
}
|
||||
|
||||
if !app_server_target.uses_remote_workspace() {
|
||||
if !app_server_target.uses_remote_workspace() && !workload_identity_selected {
|
||||
#[allow(clippy::print_stderr)]
|
||||
if let Err(err) = enforce_login_restrictions(&config.auth_config()).await {
|
||||
eprintln!("{err}");
|
||||
@@ -1289,6 +1320,7 @@ async fn run_ratatui_app(
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> color_eyre::Result<AppExitInfo> {
|
||||
let uses_remote_workspace = app_server_target.uses_remote_workspace();
|
||||
let workload_identity_selected = is_workload_identity_selected();
|
||||
color_eyre::install()?;
|
||||
|
||||
tooltips::announcement::prewarm(initial_config.http_client_factory());
|
||||
@@ -1379,7 +1411,9 @@ async fn run_ratatui_app(
|
||||
!uses_remote_workspace && should_show_trust_screen(&initial_config);
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut trust_decision_was_made = false;
|
||||
let login_status = if initial_config.model_provider.requires_openai_auth {
|
||||
let login_status = if workload_identity_selected {
|
||||
LoginStatus::AuthMode(AuthMode::Chatgpt)
|
||||
} else if initial_config.model_provider.requires_openai_auth {
|
||||
let Some(app_server) = app_server.as_mut() else {
|
||||
unreachable!("app server should exist when auth is required");
|
||||
};
|
||||
@@ -1430,12 +1464,12 @@ async fn run_ratatui_app(
|
||||
// If this onboarding run included the login step, always refresh the cloud config bundle
|
||||
// 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 {
|
||||
if show_login_screen && !uses_remote_workspace && !workload_identity_selected {
|
||||
cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
initial_config.auth_config(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
}
|
||||
|
||||
// If the user made an explicit trust decision, or we showed the login flow, reload config
|
||||
@@ -2520,7 +2554,8 @@ mod tests {
|
||||
/*explicit_remote_endpoint*/ None,
|
||||
Some(socket_path.clone()),
|
||||
/*can_reuse_implicit_local_daemon*/ true,
|
||||
);
|
||||
/*workload_identity_selected*/ false,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
target,
|
||||
@@ -2542,7 +2577,8 @@ mod tests {
|
||||
Some(explicit_endpoint.clone()),
|
||||
Some(AbsolutePathBuf::relative_to_current_dir("default.sock")?),
|
||||
/*can_reuse_implicit_local_daemon*/ false,
|
||||
);
|
||||
/*workload_identity_selected*/ false,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
target,
|
||||
@@ -2563,12 +2599,43 @@ mod tests {
|
||||
/*explicit_remote_endpoint*/ None,
|
||||
Some(socket_path),
|
||||
/*can_reuse_implicit_local_daemon*/ false,
|
||||
);
|
||||
/*workload_identity_selected*/ false,
|
||||
)?;
|
||||
|
||||
assert_eq!(target, AppServerTarget::Embedded);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workload_identity_requires_an_embedded_app_server() -> color_eyre::Result<()> {
|
||||
let default_socket = AbsolutePathBuf::relative_to_current_dir("default.sock")?;
|
||||
assert_eq!(
|
||||
app_server_target_for_launch(
|
||||
/*explicit_remote_endpoint*/ None,
|
||||
Some(default_socket),
|
||||
/*can_reuse_implicit_local_daemon*/ true,
|
||||
/*workload_identity_selected*/ true,
|
||||
)?,
|
||||
AppServerTarget::Embedded
|
||||
);
|
||||
|
||||
let explicit_endpoint = RemoteAppServerEndpoint::UnixSocket {
|
||||
socket_path: AbsolutePathBuf::relative_to_current_dir("explicit.sock")?,
|
||||
};
|
||||
let error = app_server_target_for_launch(
|
||||
Some(explicit_endpoint),
|
||||
/*default_daemon_socket*/ None,
|
||||
/*can_reuse_implicit_local_daemon*/ false,
|
||||
/*workload_identity_selected*/ true,
|
||||
)
|
||||
.expect_err("remote hosts must own workload identity");
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"workload identity must be configured on the remote app-server host"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_reuse_implicit_local_daemon_requires_default_launch_config() -> color_eyre::Result<()> {
|
||||
let mut loader_overrides = LoaderOverrides::default();
|
||||
|
||||
@@ -1070,7 +1070,8 @@ mod tests {
|
||||
auth_config.clone(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await,
|
||||
.await
|
||||
.expect("test cloud config loader"),
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
|
||||
@@ -13,7 +13,6 @@ use crate::Cli;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::bootstrap_auth_config;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
@@ -22,7 +21,6 @@ use crate::named_session_lookup::SessionCollection;
|
||||
use crate::named_session_lookup::SessionNameLookupMode;
|
||||
use codex_app_server_protocol::Thread as AppServerThread;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLoadOptions;
|
||||
use codex_config::LoaderOverrides;
|
||||
@@ -278,12 +276,14 @@ async fn start_app_server_for_archive_command(
|
||||
launch_loader_overrides.user_config_profile = Some(profile_v2.clone());
|
||||
}
|
||||
|
||||
let reuse_implicit_local_daemon = super::can_reuse_implicit_local_daemon(
|
||||
&cli_kv_overrides,
|
||||
&launch_loader_overrides,
|
||||
strict_config,
|
||||
cli.bypass_hook_trust,
|
||||
);
|
||||
let workload_identity_selected = codex_login::is_workload_identity_selected();
|
||||
let reuse_implicit_local_daemon = !workload_identity_selected
|
||||
&& super::can_reuse_implicit_local_daemon(
|
||||
&cli_kv_overrides,
|
||||
&launch_loader_overrides,
|
||||
strict_config,
|
||||
cli.bypass_hook_trust,
|
||||
);
|
||||
let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
|
||||
super::maybe_probe_default_daemon_socket(codex_home.as_path()).await
|
||||
} else {
|
||||
@@ -293,7 +293,8 @@ async fn start_app_server_for_archive_command(
|
||||
explicit_remote_endpoint,
|
||||
default_daemon,
|
||||
reuse_implicit_local_daemon,
|
||||
);
|
||||
workload_identity_selected,
|
||||
)?;
|
||||
let remote_cwd_override = cli
|
||||
.cwd
|
||||
.clone()
|
||||
@@ -337,14 +338,12 @@ async fn start_app_server_for_archive_command(
|
||||
.await
|
||||
.wrap_err("failed to load config.toml")?;
|
||||
let config_toml = &bootstrap_config.config_toml;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
app_server_target.auth_config_for_cloud_loader(bootstrap_auth_config(
|
||||
codex_home.as_path(),
|
||||
&bootstrap_config,
|
||||
)?),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
let cloud_config_bundle = super::cloud_config_bundle_for_app_server_target(
|
||||
&app_server_target,
|
||||
&bootstrap_config,
|
||||
codex_home.as_path(),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
let model_provider = if cli.oss {
|
||||
resolve_oss_provider(cli.oss_provider.as_deref(), config_toml)
|
||||
|
||||
Reference in New Issue
Block a user