Skip to content

[Agent Plugins] Fix intermittent re-auth from cross-process refresh-token rotation - #4

Merged
pragati-agrawal-glean merged 6 commits into
mainfrom
pragati/fix-plugin-token-rotation-reauth
Sep 21, 2026
Merged

pragati-agrawal-glean merged 6 commits into
mainfrom
pragati/fix-plugin-token-rotation-reauth

Conversation

@pragati-agrawal-glean

@pragati-agrawal-glean pragati-agrawal-glean commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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_grant or invalid_request and 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:

  1. Read credentials directly from disk. tokens() reloads the credential file through syncTokensFromDisk() on every access. Correctness does not depend on file modification time, and there is no environment-controlled grace-period override.
  2. Wait before invalidating tokens. With a retained client and refresh token, 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.
  3. Retry a sibling-refresh failure once. createRemoteClient() detects a newer token on disk and retries the connection once. Refresh failures use SDK v2's structured OAuthError.code values (invalid_request and invalid_grant) instead of matching human-readable messages.
  4. Keep retries bounded and nullable access explicit. A retry requires a changed token and is limited to one attempt. Unrelated OAuth errors are not retried. An early missing-provider guard rethrows the original error before recovery code accesses the provider.
  5. Reuse the atomic writer. saveCredentials() calls the existing writeFileAtomicSync helper, retaining directory mode 0700 and file mode 0600. 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 main and preserves its SDK v2 types, connection locking, elicitation/policy support, CSRF handling, version plumbing, and bundle layout.

Validation

  • npm run typecheck:bundle passed.
  • npm run test:bundle passed: 369 tests across 24 files.
  • Real SDK auth() tests cover sibling recovery after invalid_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.
  • Retry tests cover both structured refresh error codes, the one-retry limit, unrelated errors, absent auth providers, and missing authorization URLs.
  • Credential-store tests cover permissions, leftover temporary files, and preservation/cleanup when rename fails.
  • npm run build:bundle passed.
  • npm test passed: plugin builds, changelog checks, and Claude/Cursor/Codex package validation.
  • git diff --check origin/main passed for the PR diff.
  • Prior validation (not rerun for this review update): an isolated two-process refresh test against the non-production SST endpoint confirmed changed access and refresh token fingerprints, a stable client ID, credential mode 0600, and no stale temporary files.

No raw credentials are included in this PR.

— sent via Glean Tau

…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
pragati-agrawal-glean force-pushed the pragati/fix-plugin-token-rotation-reauth branch from c640b64 to 10b5eaa Compare July 30, 2026 10:49
Comment thread shared/glean/mcp/src/remote-client.ts Outdated
@pragati-agrawal-glean pragati-agrawal-glean changed the title [glean-vnext] Fix intermittent re-auth from cross-process refresh-token rotation [Agent Plugins] Fix intermittent re-auth from cross-process refresh-token rotation Sep 15, 2026
Comment thread shared/glean/mcp/src/auth-provider.ts Outdated
Comment thread shared/glean/mcp/src/auth-provider.ts
Comment thread shared/glean/mcp/src/token-store.ts Outdated
Comment thread shared/glean/mcp/src/remote-client.ts
Comment thread shared/glean/mcp/src/remote-client.ts
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
pragati-agrawal-glean merged commit 49b2dc9 into main Sep 21, 2026
1 check passed
@pragati-agrawal-glean
pragati-agrawal-glean deleted the pragati/fix-plugin-token-rotation-reauth branch September 21, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants