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
+94 -27
View File
@@ -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();
+2 -1
View File
@@ -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,
+15 -16
View File
@@ -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)