diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 13db2bb16d..57f1b8e1c3 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -234,6 +234,7 @@ async fn plugin_list_local_succeeds_when_workload_identity_becomes_unavailable() let response: PluginListResponse = to_response(response)?; assert!(response.marketplace_load_errors.is_empty()); + server.verify().await; Ok(()) } diff --git a/codex-rs/cloud-config/src/bundle_loader.rs b/codex-rs/cloud-config/src/bundle_loader.rs index 2d76d29a69..d9bf05d19d 100644 --- a/codex-rs/cloud-config/src/bundle_loader.rs +++ b/codex-rs/cloud-config/src/bundle_loader.rs @@ -7,6 +7,8 @@ use codex_config::CloudConfigBundleLoader; use codex_config::types::AuthCredentialsStoreMode; use codex_login::AuthKeyringBackendKind; use codex_login::AuthManager; +use codex_login::AuthManagerConfig; +use codex_login::WorkloadIdentityConfig; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; @@ -18,6 +20,40 @@ fn refresher_task_slot() -> &'static Mutex>> { REFRESHER_TASK.get_or_init(|| Mutex::new(None)) } +struct StorageAuthManagerConfig { + codex_home: PathBuf, + credentials_store_mode: AuthCredentialsStoreMode, + keyring_backend_kind: AuthKeyringBackendKind, + chatgpt_base_url: String, + workload_identity: Option, +} + +impl AuthManagerConfig for StorageAuthManagerConfig { + fn codex_home(&self) -> PathBuf { + self.codex_home.clone() + } + + fn cli_auth_credentials_store_mode(&self) -> AuthCredentialsStoreMode { + self.credentials_store_mode + } + + fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind { + self.keyring_backend_kind + } + + fn forced_chatgpt_workspace_id(&self) -> Option> { + None + } + + fn chatgpt_base_url(&self) -> String { + self.chatgpt_base_url.clone() + } + + fn workload_identity(&self) -> Option { + self.workload_identity.clone() + } +} + pub fn cloud_config_bundle_loader( auth_manager: Arc, chatgpt_base_url: String, @@ -58,14 +94,20 @@ pub async fn cloud_config_bundle_loader_for_storage( credentials_store_mode: AuthCredentialsStoreMode, keyring_backend_kind: AuthKeyringBackendKind, chatgpt_base_url: String, + workload_identity: Option, ) -> CloudConfigBundleLoader { - let auth_manager = AuthManager::shared( - codex_home.clone(), - enable_codex_api_key_env, + let auth_config = StorageAuthManagerConfig { + codex_home: codex_home.clone(), credentials_store_mode, - Some(chatgpt_base_url.clone()), keyring_backend_kind, - ) - .await; + chatgpt_base_url: chatgpt_base_url.clone(), + workload_identity, + }; + let auth_manager = + AuthManager::shared_from_config(&auth_config, enable_codex_api_key_env).await; cloud_config_bundle_loader(auth_manager, chatgpt_base_url, codex_home) } + +#[cfg(test)] +#[path = "bundle_loader_tests.rs"] +mod tests; diff --git a/codex-rs/cloud-config/src/bundle_loader_tests.rs b/codex-rs/cloud-config/src/bundle_loader_tests.rs new file mode 100644 index 0000000000..4830d45e8a --- /dev/null +++ b/codex-rs/cloud-config/src/bundle_loader_tests.rs @@ -0,0 +1,37 @@ +use super::*; +use codex_config::CloudConfigBundleLoadErrorCode; +use serde_json::json; +use tempfile::tempdir; + +#[tokio::test] +async fn storage_loader_consults_workload_identity_without_stored_auth() { + let codex_home = tempdir().expect("tempdir"); + let missing_token_path = codex_home.path().join("missing-subject-token"); + let workload_identity = serde_json::from_value(json!({ + "identity_provider_id": "idp_test", + "identity_provider_mapping_id": "idpm_test", + "audience": "api://codex-test", + "token_url": "http://127.0.0.1:1/oauth/token", + "credential_source": { + "type": "file", + "path": missing_token_path, + }, + })) + .expect("valid workload identity config"); + + let loader = cloud_config_bundle_loader_for_storage( + codex_home.path().to_path_buf(), + /*enable_codex_api_key_env*/ false, + AuthCredentialsStoreMode::File, + AuthKeyringBackendKind::default(), + "http://127.0.0.1:1/backend-api".to_string(), + Some(workload_identity), + ) + .await; + + let error = loader + .get() + .await + .expect_err("missing WIF source should fail managed config auth"); + assert_eq!(error.code(), CloudConfigBundleLoadErrorCode::Auth); +} diff --git a/codex-rs/core/src/guardian/prompt.rs b/codex-rs/core/src/guardian/prompt.rs index 1031464ca2..114a835214 100644 --- a/codex-rs/core/src/guardian/prompt.rs +++ b/codex-rs/core/src/guardian/prompt.rs @@ -244,7 +244,8 @@ pub(crate) async fn build_guardian_prompt_items_with_parent_turn( fn parent_turn_denied_reads_context(turn: &TurnContext) -> Option { #[allow(deprecated)] let cwd = &turn.cwd; - let file_system_policy = turn.permission_profile.file_system_sandbox_policy(); + let permission_profile = turn.model_visible_permission_profile(); + let file_system_policy = permission_profile.file_system_sandbox_policy(); let mut entries = file_system_policy .get_unreadable_roots_with_cwd(cwd) .into_iter() diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index b19cd9d35e..a689ba8deb 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -463,6 +463,7 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re session.thread_id = fixed_guardian_parent_session_id(); let denied_root = test_path_buf("/repo/private").abs(); let denied_glob = test_path_buf("/repo/private/**").display().to_string(); + let credential_path = test_path_buf("/run/secrets/codex-wif/token").abs(); turn.permission_profile = PermissionProfile::from_runtime_permissions( &FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { @@ -483,9 +484,17 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re }, access: FileSystemAccessMode::Deny, }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: credential_path.clone(), + }, + access: FileSystemAccessMode::Deny, + }, ]), NetworkSandboxPolicy::Restricted, ); + Arc::make_mut(&mut turn.config).workload_identity_credential_deny_paths = + vec![credential_path.clone()]; let session = Arc::new(session); let turn = Arc::new(turn); seed_guardian_parent_history(&session, &turn).await; @@ -511,6 +520,7 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re assert!(text.contains("do not approve escalation whose purpose is to read them")); assert!(text.contains(denied_root.to_string_lossy().as_ref())); assert!(text.contains(&format!("glob `{denied_glob}`"))); + assert!(!text.contains(credential_path.to_string_lossy().as_ref())); Ok(()) } diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index 9f507c13d8..c4a01aa6b2 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -19,6 +19,17 @@ use codex_login::CodexAuth; use codex_utils_path_uri::PathUri; use serde_json::Value as JsonValue; +const OPENAI_FILE_AUTH_RESOLUTION_FAILED_MESSAGE: &str = + "failed to resolve ChatGPT auth for OpenAI file upload"; + +fn model_visible_auth_resolution_error(error: std::io::Error) -> String { + tracing::warn!( + error = %error, + "failed to resolve auth for OpenAI file upload" + ); + OPENAI_FILE_AUTH_RESOLUTION_FAILED_MESSAGE.to_string() +} + pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( sess: &Session, turn_context: &TurnContext, @@ -40,7 +51,7 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( .auth_manager .auth() .await - .map_err(|err| format!("failed to resolve auth for OpenAI file upload: {err}"))?; + .map_err(model_visible_auth_resolution_error)?; let mut rewritten_arguments = arguments.clone(); for field_name in openai_file_input_params { @@ -195,6 +206,17 @@ mod tests { use std::sync::Arc; use tempfile::tempdir; + #[test] + fn auth_resolution_errors_hide_credential_source_details() { + let credential_path = "/run/secrets/codex-wif/subject-token"; + let message = model_visible_auth_resolution_error(std::io::Error::other(format!( + "file credential source could not read {credential_path}" + ))); + + assert_eq!(message, OPENAI_FILE_AUTH_RESOLUTION_FAILED_MESSAGE); + assert!(!message.contains(credential_path)); + } + fn set_primary_environment_cwd(turn_context: &mut TurnContext, cwd: &Path) { let cwd = AbsolutePathBuf::try_from(cwd).expect("absolute path"); turn_context.permission_profile = codex_protocol::models::PermissionProfile::Disabled; diff --git a/codex-rs/core/tests/suite/workload_identity.rs b/codex-rs/core/tests/suite/workload_identity.rs index c0b5eaa497..6b681a9bde 100644 --- a/codex-rs/core/tests/suite/workload_identity.rs +++ b/codex-rs/core/tests/suite/workload_identity.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; use std::path::PathBuf; use anyhow::Result; use anyhow::bail; use base64::Engine; +use codex_config::types::ShellEnvironmentPolicyToml; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_workload_identity::CredentialSourceConfig; @@ -26,17 +28,32 @@ const ACCESS_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:access_token"; #[derive(Serialize)] struct TestConfigToml { workload_identity: WorkloadIdentityConfig, + #[serde(skip_serializing_if = "Option::is_none")] + shell_environment_policy: Option, } fn workload_identity_config_toml(server: &MockServer, token_path: PathBuf) -> Result { + workload_identity_config_toml_with_source( + server, + CredentialSourceConfig::File { path: token_path }, + /*shell_environment_policy*/ None, + ) +} + +fn workload_identity_config_toml_with_source( + server: &MockServer, + credential_source: CredentialSourceConfig, + shell_environment_policy: Option, +) -> Result { Ok(toml::to_string(&TestConfigToml { workload_identity: WorkloadIdentityConfig { identity_provider_id: "idp_test".to_string(), identity_provider_mapping_id: "idpm_test".to_string(), audience: "api://codex-test".to_string(), token_url: format!("{}/oauth/token", server.uri()), - credential_source: CredentialSourceConfig::File { path: token_path }, + credential_source, }, + shell_environment_policy, })?) } @@ -137,6 +154,88 @@ async fn configured_workload_identity_authenticates_responses_turn() -> Result<( Ok(()) } +#[cfg(not(target_os = "windows"))] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn workload_identity_credentials_are_unavailable_to_shell_tools() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = MockServer::start().await; + let credential_dir = TempDir::new()?; + let token_path = credential_dir.path().join("azure-subject-token"); + std::fs::write(&token_path, "external-subject-token\n")?; + let access_token = test_access_token(); + Mock::given(method("POST")) + .and(path("/oauth/token")) + .respond_with(exchange_response(&access_token)) + .expect(1) + .mount(&server) + .await; + + let shell_call_id = "wif-isolation-shell"; + let token_path_string = token_path.to_string_lossy(); + assert!(!token_path_string.contains('\'')); + let command = format!( + "if [ -z \"${{AZURE_FEDERATED_TOKEN_FILE+x}}\" ]; then printf 'env-hidden\\n'; else printf 'env-visible:%s\\n' \"$AZURE_FEDERATED_TOKEN_FILE\"; fi; if cat '{token_path_string}' >/dev/null 2>&1; then printf 'file-readable\\n'; else printf 'file-denied\\n'; fi" + ); + let shell_arguments = serde_json::to_string(&json!({ + "command": command, + "login": false, + "timeout_ms": 5_000, + }))?; + let response_mock = responses::mount_sse_sequence( + &server, + vec![ + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_function_call(shell_call_id, "shell_command", &shell_arguments), + responses::ev_completed("resp-1"), + ]), + responses::sse(vec![ + responses::ev_assistant_message("msg-1", "isolated"), + responses::ev_completed("resp-2"), + ]), + ], + ) + .await; + + let config_toml = workload_identity_config_toml_with_source( + &server, + CredentialSourceConfig::Azure { + token_file: Some(token_path.clone()), + }, + Some(ShellEnvironmentPolicyToml { + r#set: Some(HashMap::from([( + "AZURE_FEDERATED_TOKEN_FILE".to_string(), + "credential-env-must-not-leak".to_string(), + )])), + ..Default::default() + }), + )?; + let mut builder = test_codex_with_workload_identity(config_toml); + let test = builder.build(&server).await?; + test.submit_turn_with_permission_profile( + "verify workload credentials are isolated", + test.config.permissions.permission_profile().clone(), + ) + .await?; + + let output = response_mock + .function_call_output_text(shell_call_id) + .expect("shell output present"); + assert!( + output.contains("env-hidden"), + "unexpected shell output: {output}" + ); + assert!( + output.contains("file-denied"), + "unexpected shell output: {output}" + ); + assert!(!output.contains("credential-env-must-not-leak")); + assert!(!output.contains("external-subject-token")); + server.verify().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn required_workload_identity_failure_blocks_session_startup() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 482785c5cf..128cc355f4 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -357,6 +357,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result .unwrap_or_default(), resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?, chatgpt_base_url, + bootstrap_config_toml.workload_identity.clone(), ) .await; let run_cli_overrides = cli_kv_overrides.clone(); diff --git a/codex-rs/hooks/src/engine/command_runner.rs b/codex-rs/hooks/src/engine/command_runner.rs index 0a0aa64b68..6f76206e8c 100644 --- a/codex-rs/hooks/src/engine/command_runner.rs +++ b/codex-rs/hooks/src/engine/command_runner.rs @@ -138,42 +138,5 @@ fn default_shell_command() -> Command { } #[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::ffi::OsStr; - - use codex_protocol::protocol::HookEventName; - use codex_protocol::protocol::HookSource; - use codex_utils_absolute_path::AbsolutePathBuf; - - use super::*; - - #[test] - fn excluded_environment_variable_overrides_handler_env() { - let variable = "CODEX_WIF_HOOK_ASSERTION"; - let shell = CommandShell { - program: String::new(), - args: Vec::new(), - excluded_environment_variables: vec![variable.to_string()], - }; - let handler = ConfiguredHandler { - event_name: HookEventName::PreToolUse, - matcher: None, - command: "true".to_string(), - timeout_sec: 1, - status_message: None, - source_path: AbsolutePathBuf::current_dir().expect("current dir"), - source: HookSource::Unknown, - display_order: 0, - env: HashMap::from([(variable.to_string(), "secret.assertion".to_string())]), - }; - - let command = build_command(&shell, &handler); - assert!( - command - .as_std() - .get_envs() - .any(|(name, value)| { name == OsStr::new(variable) && value.is_none() }) - ); - } -} +#[path = "command_runner_tests.rs"] +mod tests; diff --git a/codex-rs/hooks/src/engine/command_runner_tests.rs b/codex-rs/hooks/src/engine/command_runner_tests.rs new file mode 100644 index 0000000000..c33cbcaa31 --- /dev/null +++ b/codex-rs/hooks/src/engine/command_runner_tests.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; +use std::ffi::OsStr; + +use codex_protocol::protocol::HookEventName; +use codex_protocol::protocol::HookSource; +use codex_utils_absolute_path::AbsolutePathBuf; + +use super::*; + +#[test] +fn excluded_environment_variable_overrides_handler_env() { + let variable = "CODEX_WIF_HOOK_ASSERTION"; + let shell = CommandShell { + program: String::new(), + args: Vec::new(), + excluded_environment_variables: vec![variable.to_string()], + }; + let handler = ConfiguredHandler { + event_name: HookEventName::PreToolUse, + matcher: None, + command: "true".to_string(), + timeout_sec: 1, + status_message: None, + source_path: AbsolutePathBuf::current_dir().expect("current dir"), + source: HookSource::Unknown, + display_order: 0, + env: HashMap::from([(variable.to_string(), "secret.assertion".to_string())]), + }; + + let command = build_command(&shell, &handler); + assert!( + command + .as_std() + .get_envs() + .any(|(name, value)| { name == OsStr::new(variable) && value.is_none() }) + ); +} diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 568134e0ea..4da3abd4b0 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -29,7 +29,7 @@ use super::access_token::CodexAccessToken; use super::access_token::classify_codex_access_token; use super::external_bearer::BearerTokenRefresher; use super::revoke::revoke_auth_tokens; -use super::workload_identity::WorkloadIdentityExternalAuth; +use super::workload_identity::shared_workload_identity_external_auth; pub use crate::auth::agent_identity::AgentIdentityAuth; pub use crate::auth::bedrock_api_key::BedrockApiKeyAuth; pub use crate::auth::personal_access_token::PersonalAccessTokenAuth; @@ -51,7 +51,6 @@ use codex_protocol::auth::PlanType as InternalPlanType; use codex_protocol::auth::RefreshTokenFailedError; use codex_protocol::auth::RefreshTokenFailedReason; use codex_workload_identity_providers::WorkloadIdentityConfig; -use codex_workload_identity_providers::build_client as build_workload_identity_client; use serde_json::Value; use thiserror::Error; @@ -1961,12 +1960,12 @@ impl AuthManager { .await, ); if let Some(workload_identity) = config.workload_identity() { - let client = build_workload_identity_client( + let external_auth = shared_workload_identity_external_auth( workload_identity, oauth_client_id(), build_reqwest_client(), ); - auth_manager.set_external_auth(Arc::new(WorkloadIdentityExternalAuth::new(client))); + auth_manager.set_external_auth(external_auth); } auth_manager } diff --git a/codex-rs/login/src/auth/workload_identity.rs b/codex-rs/login/src/auth/workload_identity.rs index b6ff943921..039cab807c 100644 --- a/codex-rs/login/src/auth/workload_identity.rs +++ b/codex-rs/login/src/auth/workload_identity.rs @@ -1,5 +1,12 @@ +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::OnceLock; +use std::sync::Weak; + use codex_app_server_protocol::AuthMode; use codex_workload_identity_providers::ConfiguredWorkloadIdentityClient; +use codex_workload_identity_providers::WorkloadIdentityConfig; +use codex_workload_identity_providers::build_client as build_workload_identity_client; use super::ExternalAuth; use super::ExternalAuthFuture; @@ -11,6 +18,45 @@ pub(super) struct WorkloadIdentityExternalAuth { process_isolation_error: Option, } +struct SharedWorkloadIdentityExternalAuth { + config: WorkloadIdentityConfig, + client_id: String, + auth: Weak, +} + +fn shared_workload_identity_auths() -> &'static Mutex> { + static SHARED_AUTHS: OnceLock>> = OnceLock::new(); + SHARED_AUTHS.get_or_init(|| Mutex::new(Vec::new())) +} + +pub(super) fn shared_workload_identity_external_auth( + config: WorkloadIdentityConfig, + client_id: String, + http: reqwest::Client, +) -> Arc { + let mut shared_auths = shared_workload_identity_auths() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + shared_auths.retain(|entry| entry.auth.strong_count() > 0); + if let Some(auth) = shared_auths.iter().find_map(|entry| { + (entry.config == config && entry.client_id == client_id) + .then(|| entry.auth.upgrade()) + .flatten() + }) { + return auth; + } + + let auth = Arc::new(WorkloadIdentityExternalAuth::new( + build_workload_identity_client(config.clone(), client_id.clone(), http), + )); + shared_auths.push(SharedWorkloadIdentityExternalAuth { + config, + client_id, + auth: Arc::downgrade(&auth), + }); + auth +} + impl WorkloadIdentityExternalAuth { pub(super) fn new(client: ConfiguredWorkloadIdentityClient) -> Self { #[cfg(target_os = "windows")] @@ -56,13 +102,21 @@ impl ExternalAuth for WorkloadIdentityExternalAuth { } fn resolve(&self) -> ExternalAuthFuture<'_, Option> { - Box::pin(async move { self.tokens(false).await.map(Some) }) + Box::pin(async move { + self.tokens(/*force_refresh*/ false).await.map(Some) + }) } fn refresh( &self, _context: ExternalAuthRefreshContext, ) -> ExternalAuthFuture<'_, ExternalAuthTokens> { - Box::pin(async move { self.tokens(true).await }) + Box::pin(async move { + self.tokens(/*force_refresh*/ true).await + }) } } + +#[cfg(test)] +#[path = "workload_identity_tests.rs"] +mod tests; diff --git a/codex-rs/login/src/auth/workload_identity_tests.rs b/codex-rs/login/src/auth/workload_identity_tests.rs new file mode 100644 index 0000000000..353b0d122c --- /dev/null +++ b/codex-rs/login/src/auth/workload_identity_tests.rs @@ -0,0 +1,49 @@ +use super::*; +use serde_json::json; + +fn test_config(mapping_id: &str) -> WorkloadIdentityConfig { + serde_json::from_value(json!({ + "identity_provider_id": "idp_test", + "identity_provider_mapping_id": mapping_id, + "audience": "api://codex-test", + "token_url": "http://127.0.0.1:1/oauth/token", + "credential_source": { + "type": "file", + "path": "/run/secrets/codex-wif/subject-token", + }, + })) + .expect("valid workload identity config") +} + +#[test] +fn identical_config_reuses_process_scoped_external_auth() { + let config = test_config("idpm_shared"); + let first = shared_workload_identity_external_auth( + config.clone(), + "client-test".to_string(), + reqwest::Client::new(), + ); + let second = shared_workload_identity_external_auth( + config, + "client-test".to_string(), + reqwest::Client::new(), + ); + + assert!(Arc::ptr_eq(&first, &second)); +} + +#[test] +fn different_config_uses_distinct_external_auth() { + let first = shared_workload_identity_external_auth( + test_config("idpm_first"), + "client-test".to_string(), + reqwest::Client::new(), + ); + let second = shared_workload_identity_external_auth( + test_config("idpm_second"), + "client-test".to_string(), + reqwest::Client::new(), + ); + + assert!(!Arc::ptr_eq(&first, &second)); +} diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 367f276308..0ff790ecc1 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -52,4 +52,5 @@ pub use auth::read_openai_api_key_from_env; pub use auth::save_auth; pub use auth_env_telemetry::AuthEnvTelemetry; pub use auth_env_telemetry::collect_auth_env_telemetry; +pub use codex_workload_identity_providers::WorkloadIdentityConfig; pub use token_data::TokenData; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 002e647263..acf9e7aaae 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -964,6 +964,7 @@ pub async fn run_main( .unwrap_or_default(), resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?, chatgpt_base_url, + bootstrap_config_toml.workload_identity.clone(), ) .await; @@ -1428,6 +1429,7 @@ async fn run_ratatui_app( initial_config.cli_auth_credentials_store_mode, initial_config.auth_keyring_backend_kind(), initial_config.chatgpt_base_url.clone(), + initial_config.workload_identity.clone(), ) .await; } diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index fceb7fb059..1a9f3add70 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -1040,6 +1040,7 @@ mod tests { AuthCredentialsStoreMode::File, AuthKeyringBackendKind::default(), "https://chatgpt.com/backend-api/".to_string(), + /*workload_identity*/ None, ) .await, feedback: codex_feedback::CodexFeedback::new(), diff --git a/codex-rs/tui/src/session_archive_commands.rs b/codex-rs/tui/src/session_archive_commands.rs index 852aaad5ce..8aea81a354 100644 --- a/codex-rs/tui/src/session_archive_commands.rs +++ b/codex-rs/tui/src/session_archive_commands.rs @@ -335,6 +335,7 @@ async fn start_app_server_for_archive_command( config_toml.cli_auth_credentials_store.unwrap_or_default(), resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?, chatgpt_base_url, + config_toml.workload_identity.clone(), ) .await;