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:
cooper-oai
2026-08-13 19:35:22 +00:00
committed by copyberry
parent 781445f7c6
commit 990218bbbd
33 changed files with 368 additions and 212 deletions
@@ -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)]
+18 -3
View File
@@ -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 {
+17 -2
View File
@@ -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
{
+4 -4
View File
@@ -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(
+1 -1
View File
@@ -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, &[])
+8 -8
View File
@@ -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 {
+2 -1
View File
@@ -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())
+9 -7
View File
@@ -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 {
+21
View File
@@ -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()?;