Skip to content

fix(auth): preserve explicitly granted pairing scopes - #9785

Open
juliusmarminge wants to merge 4 commits into
mainfrom
t3code/auth-pairing-scopes
Open

fix(auth): preserve explicitly granted pairing scopes#9785
juliusmarminge wants to merge 4 commits into
mainfrom
t3code/auth-pairing-scopes

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 4, 2026

Copy link
Copy Markdown
Member

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 --scope options 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.consume accepts optional requestedScopes, rejects out-of-grant requests with BootstrapCredentialScopeNotGrantedError before burning one-time links, and the DB consume path applies the same check. EnvironmentAuth.exchangeBootstrapCredentialForAccessToken forwards those scopes into consume and maps that error to ServerAuthScopeNotGrantedError.

CLI and docs: Repeatable --scope on t3 pair, auth pairing create, and auth session issue (via authScopesFlag) replaces the previous fixed standard/admin defaults when you pass flags; invalid scope literals fail at parse time.

Clients: ClientPresentation drops its scopes field; web, mobile, and client-runtime stop sending default scopes on OAuth/bootstrap so sessions inherit the pairing grant (or omit scope to get the full grant). Web: visiting /pair with a token sets explicitPairingRequested so an already-signed-in browser must re-pair; success uses window.location.replace("/") to refresh WebSocket credentials; failed replacement keeps the old session.

Breaking: Out-of-tree ClientPresentation implementations must drop scopes; 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 --scope flags

  • Moves scope validation into PairingGrantStore.consume so requesting ungranted scopes fails and leaves the credential available for later use.
  • Adds repeated --scope options to the t3 pair, auth pairing create, and auth session issue CLI commands, defaulting to standard client or administrative scopes.
  • Removes AuthStandardClientScopes from the ClientPresentation contract across mobile, web, and client-runtime so bootstrap grants dictate the authorized scopes.
  • Adds explicit pairing state to the web primary auth flow to force the pairing surface when opening /pair on an already authenticated browser.
  • Behavioral Change: The ClientPresentation service contract no longer includes a scopes field, breaking out-of-tree implementations that rely on it. Successful browser re-pairing now performs a full application reload via window.location.replace instead of a client-side router transition.

Macroscope summarized 687b77d.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 4, 2026
@juliusmarminge juliusmarminge changed the title t3code/auth pairing scopes fix(auth): preserve explicitly granted pairing scopes Sep 4, 2026
@juliusmarminge
juliusmarminge marked this pull request as ready for review September 4, 2026 21:15
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.6 KiB 13.6 KiB −1 B (−0.0%) 15.1 KiB
Codex Thread snapshot wire 7.0 KiB 7.0 KiB −8 B (−0.1%) 7.3 KiB
Codex Live turn WebSocket wire 6.6 KiB 6.6 KiB +7 B (+0.1%) 7.8 KiB
Codex Live turn WebSocket decoded 57.1 KiB 57.1 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 10 10 0 (0.0%) 21
Claude Total thread wire 13.5 KiB 13.6 KiB +36 B (+0.3%) 15.1 KiB
Claude Thread snapshot wire 7.0 KiB 7.0 KiB +3 B (+0.0%) 7.3 KiB
Claude Live turn WebSocket wire 6.5 KiB 6.5 KiB +33 B (+0.5%) 7.8 KiB
Claude Live turn WebSocket decoded 57.8 KiB 57.9 KiB +44 B (+0.1%) 66.4 KiB
Claude Live turn messages 9 10 +1 (+11.1%) 21

Baseline: 4d3907f · PR result: 687b77d · Source CI: success

Scenario and decoded snapshot size

10 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.

  • Codex decoded thread snapshot: 113.8 KiB
  • Claude decoded thread snapshot: 114.5 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

macroscopeapp Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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 687b77d. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarminge force-pushed the t3code/auth-pairing-scopes branch 2 times, most recently from d52b9a1 to fa5d32f Compare September 4, 2026 22:59
@juliusmarminge
juliusmarminge force-pushed the t3code/auth-pairing-scopes branch from fa5d32f to 687b77d Compare September 5, 2026 00:08
Comment on lines +740 to +744
Effect.mapError((cause) =>
cause._tag === "BootstrapCredentialScopeNotGrantedError"
? new ServerAuthScopeNotGrantedError({})
: toBootstrapExchangeError(cause),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant