Skip to content

feat(ai): add Cursor OAuth authentication and builtin provider - #905

Merged
cursor[bot] merged 5 commits into
mainfrom
cursor/feat-cursor-oauth-auth
Aug 16, 2026
Merged

feat(ai): add Cursor OAuth authentication and builtin provider#905
cursor[bot] merged 5 commits into
mainfrom
cursor/feat-cursor-oauth-auth

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Aug 16, 2026

Copy link
Copy Markdown
Owner

What

Adds Cursor (Pro/Ultra/Teams) OAuth authentication as a first-class senpi auth flow with a registered builtin cursor provider. /login cursor works end-to-end: browser deep-link approval, token polling, storage in auth.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, and Models.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:

  1. login generates a PKCE S256 pair and a request uuid, then notifies auth_url for https://cursor.com/loginDeepControl?challenge&uuid&mode=login&redirectTarget=cli.
  2. The CLI polls https://api2.cursor.sh/auth/poll?uuid&verifier with 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).
  3. refresh POSTs the stored refresh token as a bearer to auth/exchange_user_api_key and keeps the previous refresh token when the server does not rotate it (CLIProxyAPI's rotation-fallback pattern). Senpi's Models already runs refresh under the store lock, covering CLIProxyAPI's singleflight dedupe.
  4. Token expiry derives from the access-token JWT exp claim minus a 5-minute skew, with a 1-hour fallback for unreadable tokens.

Improvements over the upstream oh-my-pi flow:

  • Fixes a self-swallowed error bug: upstream throws its polling error inside its own 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.
  • Abort-aware polling: the poll sleep listens on the interaction signal (upstream's loop cannot be cancelled).
  • Strict response-shape validation; error messages carry HTTP status + short server error strings, never raw bodies or token material.

Changes

  • packages/ai/src/auth/oauth/cursor.ts (new) — the flow; load.ts + bun-oauth.ts loader registration
  • packages/ai/src/providers/cursor.ts (new), providers/all.ts, types.ts (KnownProvider)
  • packages/coding-agent: display name, defaultModelPerProvider totality entry (inert until models exist), docs/providers.md
  • changes.md entries (packages/ai/src, packages/coding-agent/src/core) and [Unreleased] CHANGELOG entries in both packages
  • Tests: packages/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 extended

Validation

  • npm run check green (biome, pinned deps, ts-imports, shrinkwrap/install locks, tsc --noEmit, browser smoke)
  • Full packages/ai suite: 2054 passed / 0 failed; coding-agent model-resolver + oauth-related suites green
  • senpi-qa evidence under local-ignore/qa-evidence/20260816-cursor-oauth/ (not committed, per policy):
    • Channel 3 mock loop --self-test: 48/48
    • Channel 4 CLI smoke --self-test: 8/8
    • Targeted real-CLI RPC run (sandboxed, no credentials): get_auth_providers lists cursor as OAuth; login_start cursor emits the real cursor.com/loginDeepControl deep link with valid PKCE challenge + uuid; login_cancel aborts with "Login cancelled" before any poll request leaves the machine; no credential persisted; real auth.json checksum unchanged — 7/7
Open in Web Open in Cursor 

Summary by cubic

Adds Cursor OAuth authentication and a builtin cursor provider. Previously Cursor sign-in was unsupported; now /login cursor opens a browser deep link, polls until approved, stores tokens in auth.json, and auto-refreshes. The provider is authentication-only and exposes no models, so pickers and routing are unchanged.

  • Implements a deep-link + poll OAuth flow in packages/ai/src/auth/oauth/cursor.ts: 404 waits; 400/401/403/410 fail fast; 429 backs off; abort-aware waits; refresh via api2.cursor.sh/auth/exchange_user_api_key keeps the prior refresh token when not rotated; expiry derives from the access-token JWT exp minus 5 minutes (1-hour fallback).
  • Registers the flow and provider in packages/ai: auth/oauth/load.ts, bun-oauth.ts, providers/cursor.ts, providers/all.ts, and types.ts (adds cursor). packages/coding-agent adds the display name and a default cursor: "auto" entry (inert until models exist). Docs updated in README.md, provider docs, and QA env-var references (no env var for cursor).
  • Tests cover polling/backoff, abort/timeouts, refresh rotation, JWT expiry, and provider registration (packages/ai/test/cursor-oauth.test.ts plus 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.

Review in cubic

cursoragent and others added 4 commits August 16, 2026 16:08
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
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>
@cursor
cursor Bot merged commit 99bd6f6 into main Aug 16, 2026
17 checks passed
@cursor
cursor Bot deleted the cursor/feat-cursor-oauth-auth branch August 16, 2026 16:30
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.

2 participants