fix(auth): skip OAuth refresh adapter when credential has no refresh token#85028
Conversation
…token OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
|
Two related follow-ups intentionally not in this PR, kept separate to keep this one a one-line short-circuit: Follow-up B — Surface per-candidate failure timing in the resolver
Follow-up C — Per-candidate refresh budget for non-primary candidates
Doing C is more invasive than B; do B first. cc @RomneyDa |
|
Codex review: needs maintainer review before merge. Workflow note: Future ClawSweeper reviews update this same comment in place. How this review workflow works
Summary Reproducibility: yes. from source inspection: current main reaches adapter.refreshCredential under the 120s timeout even when the selected OAuth credential has no refresh material. I did not run a live gateway repro because this review is read-only and the PR body says a real OAuth profile would be needed. PR rating Rank-up moves:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. Real behavior proof Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land the narrow guard if auth owners agree that token-less stored OAuth records should fail fast after main-store and bootstrap recovery opportunities, with the larger resolver timing diagnostics handled separately. Do we have a high-confidence way to reproduce the issue? Yes, from source inspection: current main reaches adapter.refreshCredential under the 120s timeout even when the selected OAuth credential has no refresh material. I did not run a live gateway repro because this review is read-only and the PR body says a real OAuth profile would be needed. Is this the best way to solve the issue? Yes, the proposed insertion point is the narrowest maintainable fix because it runs after main-store adoption and external bootstrap checks but before the bounded refresh adapter call. The broader resolver warning and fallback-budget ideas are correctly separated as follow-ups. Label changes:
Label justifications:
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against b33deb41594e. |
|
ClawSweeper PR egg ✨ Hatched: 🥚 common Pearl Crabkin Hatch commandComment Hatchability rules:
Rarity: 🥚 common. What is this egg doing here?
|
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
…token (openclaw#85028) OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
Summary
OAuth credentials that loaded without their sidecar material (no
access, norefresh) still entered the refresh path inside the per-profile lock. The adapter call there is bounded byOAUTH_REFRESH_CALL_TIMEOUT_MS = 120_000(src/agents/auth-profiles/constants.ts:59), so the eventualNo API key found for provider "..."surface to the user only landed after a long stall — even though the resolver had no usable refresh material to attempt with in the first place.Short-circuit
doRefreshOAuthTokenWithLock(src/agents/auth-profiles/oauth-manager.ts) to returnnullwhen there is no refresh token. The check is placed after the in-lock main-store adoption (oauth-manager.ts:455) and external bootstrap-credential checks (:498), so sub-agents inheriting fresh tokens from the main store and externally-managed credentials still resolve normally. Only the dead-end "nothing to refresh with" case short-circuits.Companion to #84752 (root cause: re-enabling legacy sidecar rehydration) — this change is the fail-fast layer that improves the diagnostic surface when something else leaves a credential record token-less.
Verification
node scripts/run-vitest.mjs src/agents/auth-profiles/oauth-manager.test.ts— 38 passed.node scripts/run-vitest.mjs src/agents/model-auth.test.ts src/agents/auth-profiles/store-oauth-sidecar.test.ts— 104 passed.skips the refresh adapter when the credential has no refresh tokenassertsrefreshCredentialis not called and the resolver returnsnull.Real behavior proof
No API key found for provider "openai-codex"surfacing only after several minutes of silent waiting on the bounded OAuth refresh adapter call when the stored credential has no usable refresh token.auth-profiles.jsonopenai-codexentry to dropaccess/refresh, but that requires a real OAuth profile to start from.node scripts/run-vitest.mjs src/agents/auth-profiles/oauth-manager.test.ts src/agents/model-auth.test.ts src/agents/auth-profiles/store-oauth-sidecar.test.ts.refreshCredentialnot invoked, resolver returnsnullimmediately).nullfor credentials without refresh material without entering the bounded-timeout refresh adapter.openai-codexprofile (no live OAuth profile available in this environment).Test plan
oauth-manager.test.ts.openai-codexprofile with missing access/refresh now fails fast on the user-perceived embedded-runner turn (left for someone with a reproducible live env).