fix(auth): preserve explicitly granted pairing scopes - #9785
fix(auth): preserve explicitly granted pairing scopes#9785juliusmarminge wants to merge 4 commits into
Conversation
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR changes production authentication and authorization behavior across server, browser, mobile, and shared client-runtime flows, including scope issuance and grant replacement. It also adds static-analysis suppression directives, so the sensitive runtime surface and review policy require human review. No code changes detected at You can add or adjust custom eligibility rules. Learn more. |
d52b9a1 to
fa5d32f
Compare
fa5d32f to
687b77d
Compare
| Effect.mapError((cause) => | ||
| cause._tag === "BootstrapCredentialScopeNotGrantedError" | ||
| ? new ServerAuthScopeNotGrantedError({}) | ||
| : toBootstrapExchangeError(cause), | ||
| ), |
There was a problem hiding this comment.
This hand-rolls tag discrimination inside mapError. Consider recovering the known tagged failure with Effect.catchTags first and leaving mapError as the unconditional wrapper (the pattern used elsewhere in this repo, e.g. ProjectFaviconResolver.ts).
| Effect.mapError((cause) => | |
| cause._tag === "BootstrapCredentialScopeNotGrantedError" | |
| ? new ServerAuthScopeNotGrantedError({}) | |
| : toBootstrapExchangeError(cause), | |
| ), | |
| Effect.catchTags({ | |
| BootstrapCredentialScopeNotGrantedError: () => | |
| Effect.fail(new ServerAuthScopeNotGrantedError({})), | |
| }), | |
| Effect.mapError(toBootstrapExchangeError), |
Posted via Macroscope — Effect Service Conventions
Clients request the full default scope set when pairing, which rejects deliberately limited grants. A rejected exchange can consume the pairing link, and an already-authenticated browser skips replacement pairing links.
Use the scopes granted by the pairing token across web, desktop, and mobile. Validate requested scopes before consuming a link, add repeatable
--scopeoptions to the pairing and session commands, and let an explicit browser pairing link replace the current grant. Successful browser pairing reloads the app to refresh its WebSocket credentials; a rejected token preserves the existing session and shows the error.Existing clients retain their current scopes until they pair again with a fresh grant. Reconnecting does not add permissions.
Tests added cover selected CLI scopes and persistence, rejected exchanges preserving a usable link, clients retaining custom grants, and browser replacement with cached authentication, query/hash tokens, and invalid-token recovery.
Model: GPT-6. Harness: Codex.
Note
Medium Risk
Changes authentication scope negotiation, pairing consumption semantics, and a public client-runtime service shape; mistakes could over-grant, under-grant, or leave stale sessions, though behavior is heavily tested.
Overview
Pairing and token exchange now honor the scopes on the grant instead of clients always asking for the full default set.
PairingGrantStore.consumeaccepts optionalrequestedScopes, rejects out-of-grant requests withBootstrapCredentialScopeNotGrantedErrorbefore burning one-time links, and the DB consume path applies the same check.EnvironmentAuth.exchangeBootstrapCredentialForAccessTokenforwards those scopes into consume and maps that error toServerAuthScopeNotGrantedError.CLI and docs: Repeatable
--scopeont3 pair,auth pairing create, andauth session issue(viaauthScopesFlag) replaces the previous fixed standard/admin defaults when you pass flags; invalid scope literals fail at parse time.Clients:
ClientPresentationdrops itsscopesfield; web, mobile, and client-runtime stop sending default scopes on OAuth/bootstrap so sessions inherit the pairing grant (or omitscopeto get the full grant). Web: visiting/pairwith a token setsexplicitPairingRequestedso an already-signed-in browser must re-pair; success useswindow.location.replace("/")to refresh WebSocket credentials; failed replacement keeps the old session.Breaking: Out-of-tree
ClientPresentationimplementations must dropscopes; browser re-pair no longer uses client-side navigation only.Reviewed by Cursor Bugbot for commit 687b77d. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Preserve explicitly granted pairing scopes and add CLI
--scopeflagsPairingGrantStore.consumeso requesting ungranted scopes fails and leaves the credential available for later use.--scopeoptions to thet3 pair,auth pairing create, andauth session issueCLI commands, defaulting to standard client or administrative scopes.AuthStandardClientScopesfrom theClientPresentationcontract across mobile, web, and client-runtime so bootstrap grants dictate the authorized scopes./pairon an already authenticated browser.ClientPresentationservice contract no longer includes ascopesfield, breaking out-of-tree implementations that rely on it. Successful browser re-pairing now performs a full application reload viawindow.location.replaceinstead of a client-side router transition.Macroscope summarized 687b77d.