feat(ai): add Cursor OAuth authentication and builtin provider - #905
Merged
Conversation
Cursor (Pro/Ultra/Teams) signs in through a browser deep-link + poll handshake: /login cursor opens cursor.com/loginDeepControl with a PKCE S256 challenge and polls api2.cursor.sh/auth/poll with capped geometric backoff until the browser approval releases the tokens. Refresh exchanges the stored refresh token at auth/exchange_user_api_key under the credential-store lock and keeps the previous refresh token when the server does not rotate it. Compared to the upstream oh-my-pi flow: definitive poll rejections (400/401/403/410) fail fast instead of being retried as network hiccups (upstream swallows its own polling error inside its try block), the poll sleep is abort-aware, response shapes are validated strictly, and error messages never echo raw bodies or token material. The provider is authentication-only: Cursor chat runs on a protobuf Connect-RPC agent protocol that is not ported, so it registers with an empty model catalog and empty API map; the stored access token resolves through the standard auth pipeline for integrations that speak the protocol. Co-authored-by: YeonGyu-Kim <public.kim.yeon.gyu@gmail.com>
Co-authored-by: YeonGyu-Kim <public.kim.yeon.gyu@gmail.com>
Co-authored-by: YeonGyu-Kim <public.kim.yeon.gyu@gmail.com>
…uth-auth # Conflicts: # packages/coding-agent/CHANGELOG.md Co-authored-by: YeonGyu-Kim <public.kim.yeon.gyu@gmail.com>
code-yeongyu
marked this pull request as ready for review
August 16, 2026 16:12
…uth-auth # Conflicts: # packages/coding-agent/CHANGELOG.md Co-authored-by: YeonGyu-Kim <public.kim.yeon.gyu@gmail.com>
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.
What
Adds Cursor (Pro/Ultra/Teams) OAuth authentication as a first-class senpi auth flow with a registered builtin
cursorprovider./login cursorworks end-to-end: browser deep-link approval, token polling, storage inauth.json, and automatic refresh under the credential-store lock.The provider is authentication-only for now: Cursor chat runs on a protobuf Connect-RPC agent protocol (
agent.v1.AgentService) that is not ported, so the provider registers with an empty model catalog and an empty API map — nothing becomes selectable in pickers, andModels.getAuth("cursor")resolves the stored access token for integrations that speak the protocol.Flow design (referenced implementations: can1357/oh-my-pi, router-for-me/CLIProxyAPI)
Cursor uses a browser deep-link + poll handshake instead of a device-code or loopback-callback grant:
logingenerates a PKCE S256 pair and a request uuid, then notifiesauth_urlforhttps://cursor.com/loginDeepControl?challenge&uuid&mode=login&redirectTarget=cli.https://api2.cursor.sh/auth/poll?uuid&verifierwith capped geometric backoff (1s ×1.2 up to 10s, 150 attempts). 404 = "not approved yet"; 400/401/403/410 fail fast as definitive rejections; 429 keeps polling without burning the transient budget; network errors and 5xx tolerate 3 consecutive failures (a pending poll resets the counter).refreshPOSTs the stored refresh token as a bearer toauth/exchange_user_api_keyand keeps the previous refresh token when the server does not rotate it (CLIProxyAPI's rotation-fallback pattern). Senpi'sModelsalready runs refresh under the store lock, covering CLIProxyAPI's singleflight dedupe.expclaim minus a 5-minute skew, with a 1-hour fallback for unreadable tokens.Improvements over the upstream oh-my-pi flow:
try, so a definitive 401 was caught and retried as if it were a network hiccup. Here terminal statuses fail immediately with the server detail.errorstrings, never raw bodies or token material.Changes
packages/ai/src/auth/oauth/cursor.ts(new) — the flow;load.ts+bun-oauth.tsloader registrationpackages/ai/src/providers/cursor.ts(new),providers/all.ts,types.ts(KnownProvider)packages/coding-agent: display name,defaultModelPerProvidertotality entry (inert until models exist),docs/providers.mdchanges.mdentries (packages/ai/src,packages/coding-agent/src/core) and[Unreleased]CHANGELOG entries in both packagespackages/ai/test/cursor-oauth.test.ts(19 tests: backoff timing, terminal-vs-transient classification, abort, timeout budget, refresh rotation/failure, JWT expiry, provider registration) + existing suites extendedValidation
npm run checkgreen (biome, pinned deps, ts-imports, shrinkwrap/install locks,tsc --noEmit, browser smoke)packages/aisuite: 2054 passed / 0 failed; coding-agentmodel-resolver+ oauth-related suites greenlocal-ignore/qa-evidence/20260816-cursor-oauth/(not committed, per policy):--self-test: 48/48--self-test: 8/8get_auth_providerslists cursor as OAuth;login_start cursoremits the realcursor.com/loginDeepControldeep link with valid PKCE challenge + uuid;login_cancelaborts with"Login cancelled"before any poll request leaves the machine; no credential persisted; realauth.jsonchecksum unchanged — 7/7Summary by cubic
Adds Cursor OAuth authentication and a builtin
cursorprovider. Previously Cursor sign-in was unsupported; now/login cursoropens a browser deep link, polls until approved, stores tokens inauth.json, and auto-refreshes. The provider is authentication-only and exposes no models, so pickers and routing are unchanged.packages/ai/src/auth/oauth/cursor.ts: 404 waits; 400/401/403/410 fail fast; 429 backs off; abort-aware waits; refresh viaapi2.cursor.sh/auth/exchange_user_api_keykeeps the prior refresh token when not rotated; expiry derives from the access-token JWTexpminus 5 minutes (1-hour fallback).packages/ai:auth/oauth/load.ts,bun-oauth.ts,providers/cursor.ts,providers/all.ts, andtypes.ts(addscursor).packages/coding-agentadds the display name and a defaultcursor: "auto"entry (inert until models exist). Docs updated inREADME.md, provider docs, and QA env-var references (no env var forcursor).packages/ai/test/cursor-oauth.test.tsplus updates to related suites). No migration required. To validate, run/login cursor; no models appear yet.Written for commit 8d4d7ea. Summary will update on new commits.