diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index fcd8c0eb44..8c5e2080e6 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -414,6 +414,13 @@ enum EnsureConversationListenerResult { ConnectionClosed, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RefreshTokenRequestOutcome { + NotAttemptedOrSucceeded, + FailedTransiently, + FailedPermanently, +} + pub(crate) struct CodexMessageProcessorArgs { pub(crate) auth_manager: Arc, pub(crate) thread_manager: Arc, @@ -1325,25 +1332,26 @@ impl CodexMessageProcessor { } } - async fn refresh_token_if_requested(&self, do_refresh: bool) -> bool { + async fn refresh_token_if_requested(&self, do_refresh: bool) -> RefreshTokenRequestOutcome { if self.auth_manager.is_external_auth_active() { - return false; + return RefreshTokenRequestOutcome::NotAttemptedOrSucceeded; } if do_refresh && let Err(err) = self.auth_manager.refresh_token().await { let failed_reason = err.failed_reason(); if failed_reason.is_none() { tracing::warn!("failed to refresh token while getting account: {err}"); + return RefreshTokenRequestOutcome::FailedTransiently; } - return failed_reason.is_some(); + return RefreshTokenRequestOutcome::FailedPermanently; } - false + RefreshTokenRequestOutcome::NotAttemptedOrSucceeded } async fn get_auth_status(&self, request_id: ConnectionRequestId, params: GetAuthStatusParams) { let include_token = params.include_token.unwrap_or(false); let do_refresh = params.refresh_token.unwrap_or(false); - let refresh_failed_permanently = self.refresh_token_if_requested(do_refresh).await; + let refresh_outcome = self.refresh_token_if_requested(do_refresh).await; // Determine whether auth is required based on the active model provider. // If a custom provider is configured with `requires_openai_auth == false`, @@ -1362,8 +1370,11 @@ impl CodexMessageProcessor { Some(auth) => { let auth_mode = auth.api_auth_mode(); let (reported_auth_method, token_opt) = if include_token - && (refresh_failure.is_some() || refresh_failed_permanently) - { + && (refresh_failure.is_some() + || matches!( + refresh_outcome, + RefreshTokenRequestOutcome::FailedPermanently + )) { (Some(auth_mode), None) } else { match auth.get_token() { diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index bb6888e180..3ac1fc01b0 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -815,11 +815,13 @@ struct CachedAuth { auth: Option, /// Callback used to refresh external auth by asking the parent app for new tokens. external_refresher: Option>, - poisoned_managed_auth: Option, + /// Permanent refresh failure cached for the current managed auth snapshot so + /// later refresh attempts for the same credentials fail fast without network. + permanent_refresh_failure: Option, } #[derive(Clone, Debug)] -struct PoisonedManagedAuth { +struct PermanentRefreshFailure { auth_dot_json: AuthDotJson, error: RefreshTokenFailedError, } @@ -836,11 +838,11 @@ impl Debug for CachedAuth { &self.external_refresher.as_ref().map(|_| "present"), ) .field( - "poisoned_managed_auth", + "permanent_refresh_failure", &self - .poisoned_managed_auth + .permanent_refresh_failure .as_ref() - .map(|poisoned| poisoned.error.reason), + .map(|failure| failure.error.reason), ) .finish() } @@ -1012,7 +1014,7 @@ impl AuthManager { inner: RwLock::new(CachedAuth { auth: managed_auth, external_refresher: None, - poisoned_managed_auth: None, + permanent_refresh_failure: None, }), enable_codex_api_key_env, auth_credentials_store_mode, @@ -1025,7 +1027,7 @@ impl AuthManager { let cached = CachedAuth { auth: Some(auth), external_refresher: None, - poisoned_managed_auth: None, + permanent_refresh_failure: None, }; Arc::new(Self { @@ -1045,7 +1047,7 @@ impl AuthManager { let cached = CachedAuth { auth: Some(auth), external_refresher: None, - poisoned_managed_auth: None, + permanent_refresh_failure: None, }; Arc::new(Self { codex_home, @@ -1063,7 +1065,7 @@ impl AuthManager { pub fn refresh_failure(&self) -> Option { let auth = self.auth_cached()?; - self.refresh_failure_for_auth(&auth) + self.permanent_refresh_failure_for_auth(&auth) } /// Current cached auth (clone). May be `None` if not logged in or load failed. @@ -1140,17 +1142,20 @@ impl AuthManager { } } - fn refresh_failure_for_auth(&self, auth: &CodexAuth) -> Option { + fn permanent_refresh_failure_for_auth( + &self, + auth: &CodexAuth, + ) -> Option { let auth_dot_json = auth.get_current_auth_json()?; self.inner .read() .ok() - .and_then(|cached| cached.poisoned_managed_auth.clone()) - .filter(|poisoned| poisoned.auth_dot_json == auth_dot_json) - .map(|poisoned| poisoned.error) + .and_then(|cached| cached.permanent_refresh_failure.clone()) + .filter(|failure| failure.auth_dot_json == auth_dot_json) + .map(|failure| failure.error) } - fn poison_managed_auth_if_unchanged( + fn record_permanent_refresh_failure_if_unchanged( &self, attempted_auth: &CodexAuth, error: &RefreshTokenFailedError, @@ -1166,7 +1171,7 @@ impl AuthManager { .and_then(CodexAuth::get_current_auth_json) .is_some_and(|current| current == attempted_auth_dot_json); if current_auth_matches { - guard.poisoned_managed_auth = Some(PoisonedManagedAuth { + guard.permanent_refresh_failure = Some(PermanentRefreshFailure { auth_dot_json: attempted_auth_dot_json, error: error.clone(), }); @@ -1188,18 +1193,18 @@ impl AuthManager { if let Ok(mut guard) = self.inner.write() { let previous = guard.auth.as_ref(); let changed = !AuthManager::auths_equal(previous, new_auth.as_ref()); - let poisoned_auth_still_matches = guard - .poisoned_managed_auth + let permanent_refresh_failure_still_matches = guard + .permanent_refresh_failure .as_ref() - .and_then(|poisoned| { + .and_then(|failure| { new_auth .as_ref() .and_then(CodexAuth::get_current_auth_json) - .map(|current| current == poisoned.auth_dot_json) + .map(|current| current == failure.auth_dot_json) }) .unwrap_or(false); - if !poisoned_auth_still_matches { - guard.poisoned_managed_auth = None; + if !permanent_refresh_failure_still_matches { + guard.permanent_refresh_failure = None; } tracing::info!("Reloaded auth, changed: {changed}"); guard.auth = new_auth; @@ -1267,7 +1272,7 @@ impl AuthManager { pub async fn refresh_token(&self) -> Result<(), RefreshTokenError> { let auth_before_reload = self.auth_cached(); if let Some(auth_before_reload) = auth_before_reload.as_ref() - && let Some(error) = self.refresh_failure_for_auth(auth_before_reload) + && let Some(error) = self.permanent_refresh_failure_for_auth(auth_before_reload) { return Err(RefreshTokenError::Permanent(error)); } @@ -1301,7 +1306,7 @@ impl AuthManager { Some(auth) => auth, None => return Ok(()), }; - if let Some(error) = self.refresh_failure_for_auth(&auth) { + if let Some(error) = self.permanent_refresh_failure_for_auth(&auth) { return Err(RefreshTokenError::Permanent(error)); } @@ -1323,7 +1328,7 @@ impl AuthManager { CodexAuth::ApiKey(_) => Ok(()), }; if let Err(RefreshTokenError::Permanent(error)) = &result { - self.poison_managed_auth_if_unchanged(&attempted_auth, error); + self.record_permanent_refresh_failure_if_unchanged(&attempted_auth, error); } result } @@ -1348,7 +1353,7 @@ impl AuthManager { } async fn refresh_if_stale(&self, auth: &CodexAuth) -> Result { - if let Some(error) = self.refresh_failure_for_auth(auth) { + if let Some(error) = self.permanent_refresh_failure_for_auth(auth) { return Err(RefreshTokenError::Permanent(error)); } let chatgpt_auth = match auth {