Fixes #25628
Summary
Fixes #25628. Signed-in macOS users (classic and NEXT both use CmuxAuthRuntime and the vendored Stack SDK) could lose a valid session for reasons unrelated to the session itself:
- Token refresh deleted the session on any 400/401. Every launch after an update refreshes the expired access token. The SDK treated every 400/401 from
/api/v1/auth/oauth/tokenas a dead refresh token. Stack sends those same statuses when it rejects the client or the request (INVALID_PUBLISHABLE_CLIENT_KEY,PUBLISHABLE_CLIENT_KEY_REQUIRED_FOR_PROJECT,INVALID_OAUTH_CLIENT_ID_OR_SECRET, schema errors, OAuthinvalid_client/invalid_request). Refresh rejection is now classified insideAPIClient:- A rejection that names the refresh token (
REFRESH_TOKEN_NOT_FOUND_OR_EXPIRED,REFRESH_TOKEN_ERROR,PROVIDER_REJECTED,invalid_grant), or a Stack answer that names no error, still clears the session. - A named client rejection, or a page that isn't Stack's (non-JSON, including non-UTF-8), keeps the session.
- A kept rejection starts a cancellation-aware cooldown, which resets when the refresh-token session changes.
- A rejection that names the refresh token (
- A failed Keychain read counted as signed out. macOS passed no
isTokenStorageAvailableto the coordinator, so it always reportedtrue. It now passes a probe fromFallbackTokenStore/KeychainStackTokenStore.- Only temporary failures (
errSecInteractionNotAllowed,errSecNotAvailable, on the current or the legacy account) count as unavailable, and they preserve the cached session. - A permanent failure reads as an empty store. After the file fallback is checked, a session with no usable token still routes to sign-in.
- Restore checks availability before and after its token reads. Validation now also checks it before clearing, when its refresh-token re-read comes back empty.
- Only temporary failures (
Other suspects ruled out:
- The Keychain access group (
7WLXT3NR37.com.cmuxterm.app) and service (bundle-derived) are the same across releases. - Stack doesn't rotate refresh tokens (
alwaysIssueNewRefreshToken: false), so there's no rotation race. - No path clears tokens on update.
No user report ties a specific status to the symptom. These are the code paths that sign out a live session, not observed causes.
This branch carries the review follow-up first pushed as https://github.com/manaflow-ai/cmux/pull/25759 (this branch rejects force pushes, so main is merged in rather than rebased). This PR supersedes it.
Testing
Focused command: the Auth and session lifecycle workflow (.github/workflows/auth-refresh-tests.yml). It runs the SDK refresh suites, including RefreshRejectionTests, and the CmuxAuthRuntime coordinator suites matching AuthCoordinator, which include AuthCoordinatorLockedStorageTests. KeychainStackTokenStoreTests runs in macos / swift-package-tests; at the head it passed with every suite in https://github.com/manaflow-ai/cmux/actions/runs/40834658783.
Red on the base, then green on the head:
- Refresh classification, test-only commit on the base: https://github.com/manaflow-ai/cmux/actions/runs/40408448435 fails. The four "keeps the session" cases read
kept → nil. - Validation re-read, test-only commit
47bf6ec4: https://github.com/manaflow-ai/cmux/actions/runs/40834679035 fails. OnlylockedRefreshReadAfterFailedValidationPreservesStoredSessionfails (1 of 221): the session was cleared and the user signed out. - Head
d28a4101: https://github.com/manaflow-ai/cmux/actions/runs/40834658579 passes: 35 SDK tests (RefreshRejectionTestspassed) and 221 coordinator tests (lockedRefreshReadAfterFailedValidationPreservesStoredSessionpassed).
Observed / Not run
- Observed: the runs above.
- Observed: tagged build
25628-signout-after-updateof exact headd28a4101b6efa4be228d0bc7853c5b2936499c01(fleet job83f0908b40a5a00e2e05ee82, Linux pool), published to the Tag Opener. - Observed: its direct backend
https://cmux-dev-backend-1.tail137216.ts.net:3886/is running and serves the sign-in page. - Not run: a launch and sign-in across a real in-place Sparkle update. A tagged build is a separate bundle, so it can't reproduce an update of the installed app. Coverage is the behavior tests above.
Changelog
Fixed: cmux no longer signs you out after an update when the sign-in service rejects the app's request rather than your session, or when the Keychain is briefly unreadable
Proof
No UI change; the behavior tests above are the proof.
🤖 Generated with Claude Code
Summary by CodeRabbit
- Bug Fixes
- Improved sign-in reliability when secure token storage is temporarily unavailable. Existing sessions are preserved rather than incorrectly cleared during interrupted credential checks.
- Improved handling of token refresh rejections: temporary or unrecognized server responses no longer immediately erase saved credentials, and repeated attempts are spaced out.
- Definitive refresh-token failures, such as expired or invalid credentials, still end the session.