Stop retrying terminal runtime authentication denials (#60096)

## Why

Invalid assertions and paused or revoked runtime instances cannot recover through request retries or credential refresh. Permanent refresh-token failures also need to retain their sign-in error instead of becoming retryable stream errors.

## What changed

- Recognize terminal runtime denial codes in `401` and `403` response bodies and identity error headers, preserving HTTP diagnostics while stopping retries.
- Skip credential recovery for runtime denials in runtime-enabled model providers and backend requests, while preserving recovery for ordinary credential expiration.
- Propagate permanent refresh-token failures through runtime authentication as `RefreshTokenFailed` errors.

## Testing

Add coverage for body and header denials, preserved HTTP diagnostics, provider-scoped credential recovery, and cached permanent refresh failures. Complete-turn tests verify that runtime denials stop without retries or refreshes while expired credentials recover in both runtime authentication modes.

GitOrigin-RevId: 33f6f707454130e97e13f3b53e29d2c484602152
This commit is contained in:
cooper-oai
2026-11-07 09:48:46 +00:00
committed by copyberry
parent 4be90eb069
commit efdacaf46f
19 changed files with 574 additions and 34 deletions
@@ -183,6 +183,7 @@ impl From<RuntimeAuthError> for AuthError {
| RuntimeAuthError::UnsupportedResourceSelection
| RuntimeAuthError::Changed
| RuntimeAuthError::RefreshFailed
| RuntimeAuthError::RefreshTokenFailed(_)
| RuntimeAuthError::KeyGeneration
| RuntimeAuthError::Registration(
RegistrationError::InvalidEndpoint
@@ -17,6 +17,7 @@ use codex_login::AuthManager;
use codex_login::AuthOwner;
use codex_login::CodexAuth;
use codex_login::RefreshTokenError;
use codex_login::auth::RefreshTokenFailedError;
use http::StatusCode;
use std::sync::Arc;
use tokio::sync::Mutex;
@@ -169,9 +170,8 @@ impl RuntimeAuthBinding {
self.check_source(expected_source)?;
recovery.next().await.map_err(|error| match error {
RefreshTokenError::Policy(denied) => RuntimeAuthError::Policy(denied),
RefreshTokenError::Permanent(_) | RefreshTokenError::Transient(_) => {
RuntimeAuthError::RefreshFailed
}
RefreshTokenError::Permanent(error) => RuntimeAuthError::RefreshTokenFailed(error),
RefreshTokenError::Transient(_) => RuntimeAuthError::RefreshFailed,
})?;
let (auth, http_client_factory) = self
.credentials
@@ -340,8 +340,8 @@ impl RuntimeAuthBinding {
.credentials
.auth_change_state_for(auth)
.ok_or(RuntimeAuthError::Changed)?;
if self.credentials.refresh_failure_for_auth(auth).is_some() {
return Err(RuntimeAuthError::RefreshFailed);
if let Some(error) = self.credentials.refresh_failure_for_auth(auth) {
return Err(RuntimeAuthError::RefreshTokenFailed(error));
}
Ok(state)
}
@@ -390,6 +390,8 @@ pub enum RuntimeAuthError {
Changed,
#[error("runtime registration could not refresh actor credentials")]
RefreshFailed,
#[error(transparent)]
RefreshTokenFailed(#[from] RefreshTokenFailedError),
#[error("failed to generate runtime signing key")]
KeyGeneration,
#[error(transparent)]