fix(codex): adopt refresh_token from auth.json even without access_token (#70097)
Two defects in the openai-codex credential pool recovery path: Defect 1 — adoption path silently no-ops when store_access is empty _sync_codex_entry_from_auth_store() skipped adoption when the auth store had no access_token (only last_refresh). When another process rotated the token pair, the stale profile's entry kept the consumed refresh_token and replayed it, getting refresh_token_reused and going terminally DEAD. Fix: also adopt when store_refresh differs from entry_refresh, even when store_access is empty. Keep the entry's existing access_token in that case (store_access or entry.access_token). Defect 2 — false 'auth refreshed' success log _try_refresh_codex_client_credentials() returned True whenever resolve_codex_runtime_credentials() returned any non-empty credentials, including the same stale token when the underlying refresh failed. The conversation loop then logged 'auth refreshed after 401' right before the retry failed with the identical token_expired. Fix: compare the access token before/after the refresh. If unchanged, return False so the 401-retry path logs the truth. Fixes #70097
This commit is contained in:
@@ -811,19 +811,43 @@ class CredentialPool:
|
||||
# Adopt auth.json tokens when either side differs. Codex refresh
|
||||
# tokens are single-use too, so a fresh refresh_token from
|
||||
# another process means our entry's pair is consumed/stale.
|
||||
#
|
||||
# Also adopt when the store has a refresh_token but no
|
||||
# access_token — another process may have rotated the pair
|
||||
# and the store entry's access_token was already consumed;
|
||||
# the important signal is the refresh_token difference.
|
||||
entry_access = entry.access_token or ""
|
||||
entry_refresh = entry.refresh_token or ""
|
||||
should_adopt = False
|
||||
if store_access and (
|
||||
store_access != entry_access
|
||||
or (store_refresh and store_refresh != entry_refresh)
|
||||
):
|
||||
should_adopt = True
|
||||
elif (
|
||||
store_refresh
|
||||
and store_refresh != entry_refresh
|
||||
and not store_access
|
||||
):
|
||||
# Store has only a refresh_token (no access_token) —
|
||||
# another process rotated the pair. Adopt the
|
||||
# refresh_token so we don't replay the consumed one.
|
||||
logger.info(
|
||||
"Pool entry %s: auth.json has newer refresh_token "
|
||||
"but no access_token; adopting refresh_token to "
|
||||
"avoid replaying consumed token",
|
||||
entry.id,
|
||||
)
|
||||
should_adopt = True
|
||||
|
||||
if should_adopt:
|
||||
logger.debug(
|
||||
"Pool entry %s: syncing Codex tokens from auth.json "
|
||||
"(refreshed by another process)",
|
||||
entry.id,
|
||||
)
|
||||
field_updates: Dict[str, Any] = {
|
||||
"access_token": store_access,
|
||||
"access_token": store_access or entry.access_token,
|
||||
"refresh_token": store_refresh or entry.refresh_token,
|
||||
"last_status": None,
|
||||
"last_status_at": None,
|
||||
|
||||
15
run_agent.py
15
run_agent.py
@@ -5130,10 +5130,12 @@ class AIAgent:
|
||||
if self.provider == "openai-codex":
|
||||
from hermes_cli.auth import resolve_codex_runtime_credentials
|
||||
|
||||
old_key = str(self.api_key or "").strip()
|
||||
creds = resolve_codex_runtime_credentials(force_refresh=force)
|
||||
else:
|
||||
from hermes_cli.auth import resolve_xai_oauth_runtime_credentials
|
||||
|
||||
old_key = str(self.api_key or "").strip()
|
||||
creds = resolve_xai_oauth_runtime_credentials(force_refresh=force)
|
||||
except Exception as exc:
|
||||
logger.debug("%s credential refresh failed: %s", self.provider, exc)
|
||||
@@ -5146,6 +5148,19 @@ class AIAgent:
|
||||
if not isinstance(base_url, str) or not base_url.strip():
|
||||
return False
|
||||
|
||||
# Defect 2 fix: return False when no NEW token was actually minted.
|
||||
# resolve_codex_runtime_credentials returns the same stale token
|
||||
# when the underlying refresh fails (failure is debug-only).
|
||||
# Comparing the access token (api_key) before/after detects this.
|
||||
new_key = api_key.strip()
|
||||
if old_key and new_key == old_key:
|
||||
logger.debug(
|
||||
"%s credential refresh returned the same token; "
|
||||
"refresh likely failed silently",
|
||||
self.provider,
|
||||
)
|
||||
return False
|
||||
|
||||
self.api_key = api_key.strip()
|
||||
self.base_url = base_url.strip().rstrip("/")
|
||||
self._client_kwargs["api_key"] = self.api_key
|
||||
|
||||
Reference in New Issue
Block a user