[Agent Plugins] Fix intermittent re-auth from cross-process refresh-token rotation - #4
Merged
pragati-agrawal-glean merged 6 commits intoSep 21, 2026
Conversation
pragati-agrawal-glean
requested review from
eshwar-sundar-glean,
mohit-gupta-glean and
swarup-padhi-glean
July 30, 2026 07:20
…en rotation Port of gleanwork/glean-plugins-vnext#44 (squashed; full history and E2E evidence there). Each host session runs its own plugin process sharing one credentials file. The Glean OAuth server rotates refresh tokens on every refresh with no grace period, so when one session refreshes, every other session's in-memory copy is revoked; their next refresh gets invalid_grant, the SDK wipes the SHARED store, and the user sees [SETUP_REQUIRED] — plus every other live session dies with them. Fixes (E2E-verified on an experimental pod against real prod /oauth — bug reproduced on demand with the old build, silent recovery in both race shapes with this change): - tokens()/syncTokensFromDisk: mtime-guarded re-read of the shared store so a sibling's rotated grant is picked up before the SDK refreshes. - invalidateCredentials('tokens'): adopt a newer on-disk token instead of wiping — with a grace-window poll (GLEAN_ROTATION_GRACE_MS, 2s) because the loser's invalid_grant usually lands milliseconds before the winner's write. - Connect-level sibling-refresh retry: concurrent refreshes of the same grant make fosite fail the loser with invalid_request (not invalid_grant — observed live), which the SDK rethrows raw; recognize refresh-shaped failures, wait out the grace window, retry once. - saveCredentials: temp-file + rename so concurrent writers can't leave a torn store that parses as wiped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pragati-agrawal-glean
force-pushed
the
pragati/fix-plugin-token-rotation-reauth
branch
from
July 30, 2026 10:49
c640b64 to
10b5eaa
Compare
david-hamilton-glean
approved these changes
Aug 20, 2026
Merge current main and adapt token recovery to MCP SDK v2. Use 500ms polling, preserve explicit client resets, reuse the atomic credential writer, and make the optional-provider guard explicit. Add SDK-flow and retry/permissions regression coverage. — sent via Glean Tau
pragati-agrawal-glean
deleted the
pragati/fix-plugin-token-rotation-reauth
branch
September 21, 2026 05:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Multiple MCP host sessions can share one credential file while each plugin process keeps its own in-memory OAuth grant. When the OAuth server rotates refresh tokens, one process can refresh successfully while a sibling still holds the revoked refresh token. The sibling may then receive
invalid_grantorinvalid_requestand incorrectly clear the shared credentials, causing[SETUP_REQUIRED]for otherwise healthy sessions.Fix
This PR adds bounded recovery for cross-process token rotation in the agent-plugins credential store:
tokens()reloads the credential file throughsyncTokensFromDisk()on every access. Correctness does not depend on file modification time, and there is no environment-controlled grace-period override.invalidateCredentials("tokens")waits up to a fixed two-second grace period for a sibling to persist a changed access token, polling every 500 ms. It adopts the sibling's grant when available and otherwise clears tokens. SDK v2 client errors invalidate"client"followed by"tokens"; the retained-client guard keeps that reset immediate. Explicit"all"and"client"invalidation are not suppressed by a sibling token.createRemoteClient()detects a newer token on disk and retries the connection once. Refresh failures use SDK v2's structuredOAuthError.codevalues (invalid_requestandinvalid_grant) instead of matching human-readable messages.saveCredentials()calls the existingwriteFileAtomicSynchelper, retaining directory mode0700and file mode0600. The helper sets temporary-file permissions before rename and removes temporary files when a write fails. Atomic writes prevent partial files; they are not cross-process mutual exclusion.The branch includes current
mainand preserves its SDK v2 types, connection locking, elicitation/policy support, CSRF handling, version plumbing, and bundle layout.Validation
npm run typecheck:bundlepassed.npm run test:bundlepassed: 369 tests across 24 files.auth()tests cover sibling recovery afterinvalid_grant, genuine invalidation after two seconds, and immediate client-error resets, including a sibling write between client and token invalidation. HTTP is faked and credentials use an isolated test directory.npm run build:bundlepassed.npm testpassed: plugin builds, changelog checks, and Claude/Cursor/Codex package validation.git diff --check origin/mainpassed for the PR diff.0600, and no stale temporary files.No raw credentials are included in this PR.
— sent via Glean Tau