mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Surface refresh-token rejection errors during workspace routing (#57304)
## Why When workspace discovery receives a 401 and token refresh is permanently rejected, the generic discovery error hides the guidance to sign in again. ## What changed Propagate cached permanent refresh-token failures for the current credentials through workspace routing and model-provider resolution as `CodexErr::RefreshTokenFailed`. Preserve account-change checks and only surface the cached rejection when external authentication is absent and the auth generation is unchanged. ## Testing Add an app-server regression test that simulates `refresh_token_reused` during workspace discovery and verifies that the turn fails with `Unauthorized` and tells the user to sign in again. GitOrigin-RevId: cf89c71fd6a2103e56d637838b0cb34d58b3cc32
This commit is contained in:
@@ -8,9 +8,11 @@ use codex_app_server_protocol::AccountRoutingOverride;
|
||||
use codex_backend_client::AccountEntry;
|
||||
use codex_config::ConfigLoadError;
|
||||
use codex_http_client::NetworkPolicyDenied;
|
||||
use codex_login::RefreshTokenError;
|
||||
use codex_login::WorkspaceRouting;
|
||||
use codex_login::WorkspaceRoutingRequest;
|
||||
use codex_login::WorkspaceRoutingResolver;
|
||||
use codex_login::auth::RefreshTokenFailedError;
|
||||
use codex_model_provider::ProviderAccount;
|
||||
use codex_model_provider::ProviderAccountError;
|
||||
use codex_model_provider::ProviderAccountState;
|
||||
@@ -70,6 +72,8 @@ pub(super) enum WorkspaceRoutingError {
|
||||
AccountChanged,
|
||||
#[error("workspace routing discovery unauthorized (401)")]
|
||||
DiscoveryUnauthorized,
|
||||
#[error(transparent)]
|
||||
RefreshTokenRejected(RefreshTokenFailedError),
|
||||
#[error("workspace routing discovery failed")]
|
||||
DiscoveryFailed,
|
||||
#[error("selected workspace missing from routing discovery")]
|
||||
@@ -188,6 +192,9 @@ impl WorkspaceRoutingResolver for AccountRequestProcessor {
|
||||
.map(|account| account.workspace_routing)
|
||||
.map_err(|error| match error {
|
||||
AccountReadError::InvalidAccount(error) => io::Error::other(error),
|
||||
AccountReadError::Routing(WorkspaceRoutingError::RefreshTokenRejected(
|
||||
error,
|
||||
)) => io::Error::new(io::ErrorKind::PermissionDenied, error),
|
||||
AccountReadError::Routing(error) => io::Error::new(
|
||||
if error == WorkspaceRoutingError::DiscoveryUnauthorized {
|
||||
io::ErrorKind::PermissionDenied
|
||||
@@ -398,14 +405,31 @@ impl AccountRequestProcessor {
|
||||
Ok(response) => break response,
|
||||
Err(error) => {
|
||||
let unauthorized = error.is_unauthorized();
|
||||
if unauthorized
|
||||
&& recovery.has_next()
|
||||
&& recovery.next().await.is_ok()
|
||||
{
|
||||
if unauthorized && recovery.has_next() {
|
||||
let result = recovery.next().await;
|
||||
let refreshed = *current_auth_changes.borrow();
|
||||
if refreshed.owner_generation != auth_state.owner_generation {
|
||||
return Err(WorkspaceRoutingError::AccountChanged.into());
|
||||
}
|
||||
if let Err(error) = result
|
||||
&& refreshed.generation == discovery_generation
|
||||
{
|
||||
let cached_failure = self
|
||||
.auth_manager
|
||||
.refresh_failure_for_auth(&discovery_auth);
|
||||
if let RefreshTokenError::Permanent(failed) = error
|
||||
&& !self.auth_manager.has_external_auth()
|
||||
&& cached_failure.as_ref() == Some(&failed)
|
||||
{
|
||||
return Err(
|
||||
WorkspaceRoutingError::RefreshTokenRejected(failed)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
return Err(
|
||||
WorkspaceRoutingError::DiscoveryUnauthorized.into()
|
||||
);
|
||||
}
|
||||
discovery_generation = refreshed.generation;
|
||||
discovery_auth = self
|
||||
.auth_manager
|
||||
|
||||
Reference in New Issue
Block a user