From a7b86b6201d35200280e3d24ca5ebccd8122d6ca Mon Sep 17 00:00:00 2001 From: mpc-oai Date: Tue, 25 Aug 2026 02:30:24 +0000 Subject: [PATCH] Retry provider auth commands after initial failures (#40523) ## Why A provider auth command can fail during initial credential resolution, leaving no cached authentication. A subsequent `401` should still give the configured provider one bounded opportunity to recover. ## What changed - Treat configured external authentication with an empty cache as eligible for unauthorized recovery. - Allow authority refresh to invoke the external provider when no prior auth is cached, while preserving the existing requirements for managers without an external provider. ## Testing Added unit and client coverage for a provider command that fails initially and succeeds during `401` recovery. GitOrigin-RevId: 7cab9a03f90ce29b412be8fbd97b297acdad4482 --- codex-rs/core/tests/suite/client.rs | 41 +++++++++++++++++++++++ codex-rs/login/src/auth/auth_tests.rs | 46 +++++++++++++++++++++++++ codex-rs/login/src/auth/manager.rs | 48 +++++++++++++++------------ 3 files changed, 113 insertions(+), 22 deletions(-) diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f459daccb0..556c3844f2 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -745,6 +745,9 @@ impl ProviderAuthCommandFixture { std::fs::write( &script_path, r#"#!/bin/sh +if [ -f fail-until-401 ]; then + exit 1 +fi first_line=$(sed -n '1p' tokens.txt) printf '%s\n' "$first_line" tail -n +2 tokens.txt > tokens.next @@ -767,6 +770,7 @@ mv tokens.next tokens.txt &script_path, r#"@echo off setlocal EnableExtensions DisableDelayedExpansion +if exist fail-until-401 exit /b 1 set "first_line=" tokens.next @@ -1641,6 +1683,10 @@ mv tokens.next tokens.txt &script_path, r#"@echo off setlocal EnableExtensions DisableDelayedExpansion +if exist fail-once ( + del fail-once + exit /b 1 +) set "first_line=" bool { + fn has_refreshable_external_auth(&self) -> bool { self.has_external_auth() && self .auth_cached() .as_ref() - .is_some_and(CodexAuth::is_api_key_auth) + .is_none_or(|auth| auth.is_api_key_auth() || auth.supports_unauthorized_recovery()) } async fn resolve_external_auth( @@ -2817,40 +2817,44 @@ impl AuthManager { async fn refresh_token_from_authority_impl(&self) -> Result<(), RefreshTokenError> { tracing::info!("Refreshing token"); - let auth = match self.auth_cached() { - Some(auth) => auth, - None => return Ok(()), - }; - if let Some(error) = self.refresh_failure_for_auth(&auth) { + let attempted_auth = self.auth_cached(); + if let Some(error) = attempted_auth + .as_ref() + .and_then(|auth| self.refresh_failure_for_auth(auth)) + { return Err(RefreshTokenError::Permanent(error)); } - let attempted_auth = auth.clone(); let result = if self.has_external_auth() { self.refresh_external_auth(ExternalAuthRefreshReason::Unauthorized) .await } else { - match auth { - CodexAuth::Chatgpt(chatgpt_auth) => { + match attempted_auth.as_ref() { + Some(CodexAuth::Chatgpt(chatgpt_auth)) => { let token_data = chatgpt_auth.current_token_data().ok_or_else(|| { RefreshTokenError::Transient(std::io::Error::other( "Token data is not available.", )) })?; - self.refresh_and_persist_chatgpt_token(&chatgpt_auth, token_data.refresh_token) + self.refresh_and_persist_chatgpt_token(chatgpt_auth, token_data.refresh_token) .await } - CodexAuth::ApiKey(_) - | CodexAuth::ChatgptAuthTokens(_) - | CodexAuth::Headers(_) - | CodexAuth::AgentIdentity(_) - | CodexAuth::PersonalAccessToken(_) - | CodexAuth::BedrockApiKey(_) - | CodexAuth::BedrockAccessKeys(_) => Ok(()), + Some( + CodexAuth::ApiKey(_) + | CodexAuth::ChatgptAuthTokens(_) + | CodexAuth::Headers(_) + | CodexAuth::AgentIdentity(_) + | CodexAuth::PersonalAccessToken(_) + | CodexAuth::BedrockApiKey(_) + | CodexAuth::BedrockAccessKeys(_), + ) + | None => Ok(()), } }; - if let Err(RefreshTokenError::Permanent(error)) = &result { - self.record_permanent_refresh_failure_if_unchanged(&attempted_auth, error); + if let Some(attempted_auth) = attempted_auth.as_ref() + && let Err(RefreshTokenError::Permanent(error)) = &result + { + self.record_permanent_refresh_failure_if_unchanged(attempted_auth, error); } result }