diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 95bfca5b5d06c..a49a12c9fbc6f 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -1056,39 +1056,31 @@ class CredentialPool: self._mark_exhausted(entry, None) return None - # Codex OAuth refresh tokens are single-use. The sync→POST→write-back - # sequence below must run atomically across Hermes processes: otherwise - # two processes can both adopt the same on-disk token, both POST it, and - # the loser gets ``refresh_token_reused``. Serialize the whole sequence - # through the shared cross-process auth-store flock (the same lock and - # extended-timeout pattern used by resolve_codex_runtime_credentials()). - # When a waiter finally acquires the lock, the in-lock re-sync below - # picks up the rotated token the winner persisted and skips the POST. - if self.provider == "openai-codex": - refresh_timeout_seconds = auth_mod.env_float( - "HERMES_CODEX_REFRESH_TIMEOUT_SECONDS", 20 + # Codex and xAI OAuth refresh tokens are single-use. The + # sync→POST→write-back sequence below must run atomically across Hermes + # processes: otherwise two processes can both adopt the same on-disk + # token, both POST it, and the loser gets ``refresh_token_reused``. + # Serialize the whole sequence through the shared cross-process + # auth-store flock (the same lock and extended-timeout pattern used by + # resolve_codex_runtime_credentials()). When a waiter finally acquires + # the lock, the in-lock re-sync below picks up the rotated token the + # winner persisted and skips the POST. + if self.provider in ("openai-codex", "xai-oauth"): + sync_entry = ( + self._sync_codex_entry_from_auth_store + if self.provider == "openai-codex" + else self._sync_xai_oauth_entry_from_pool_store ) - lock_timeout = max( - float(auth_mod.AUTH_LOCK_TIMEOUT_SECONDS), - float(refresh_timeout_seconds) + 5.0, - ) - with _auth_store_lock(timeout_seconds=lock_timeout): - synced = self._sync_codex_entry_from_auth_store(entry) - if synced is not entry: - entry = synced - if not force and not self._entry_needs_refresh(entry): - return entry - return self._refresh_entry_impl(entry, force=force) - if self.provider == "xai-oauth": - refresh_timeout_seconds = auth_mod.env_float( - "HERMES_XAI_REFRESH_TIMEOUT_SECONDS", 20 - ) - lock_timeout = max( - float(auth_mod.AUTH_LOCK_TIMEOUT_SECONDS), - float(refresh_timeout_seconds) + 5.0, - ) - with _auth_store_lock(timeout_seconds=lock_timeout): - synced = self._sync_xai_oauth_entry_from_pool_store(entry) + with _auth_store_lock( + timeout_seconds=self._single_use_refresh_lock_timeout() + ): + synced = sync_entry(entry) + if self.provider == "openai-codex": + if synced is not entry: + entry = synced + if not force and not self._entry_needs_refresh(entry): + return entry + return self._refresh_entry_impl(entry, force=force) if ( synced.access_token != entry.access_token or synced.refresh_token != entry.refresh_token @@ -1097,6 +1089,25 @@ class CredentialPool: return self._refresh_entry_impl(synced, force=force) return self._refresh_entry_impl(entry, force=force) + def _single_use_refresh_lock_timeout(self) -> float: + """Lock timeout for single-use-refresh-token providers. + + Covers the configured refresh POST timeout plus a margin so a slow + token endpoint cannot make the flock give up before the refresh + resolves. Reads the provider's ``HERMES_*_REFRESH_TIMEOUT_SECONDS`` + override. + """ + env_var = ( + "HERMES_CODEX_REFRESH_TIMEOUT_SECONDS" + if self.provider == "openai-codex" + else "HERMES_XAI_REFRESH_TIMEOUT_SECONDS" + ) + refresh_timeout_seconds = auth_mod.env_float(env_var, 20) + return max( + float(auth_mod.AUTH_LOCK_TIMEOUT_SECONDS), + float(refresh_timeout_seconds) + 5.0, + ) + def _refresh_entry_impl( self, entry: PooledCredential, *, force: bool ) -> Optional[PooledCredential]: