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
@@ -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",
+4 -3
View File
@@ -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();
+6 -2
View File
@@ -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(),
+1 -1
View File
@@ -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
+8 -8
View File
@@ -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)]
+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()?;
+17 -11
View File
@@ -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(),
)
}
+12 -2
View File
@@ -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)
}
+9 -4
View File
@@ -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()
};
+3 -1
View File
@@ -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(),
+1
View File
@@ -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);
+10 -3
View File
@@ -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);
}
+20 -12
View File
@@ -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);
}
+36 -32
View File
@@ -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;
+7 -32
View File
@@ -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",
)
+1
View File
@@ -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;
+16 -1
View File
@@ -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::*;
+5 -4
View File
@@ -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");
}
+1 -1
View File
@@ -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
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)