diff --git a/apps/server/src/provider/Layers/CursorProvider.test.ts b/apps/server/src/provider/Layers/CursorProvider.test.ts index e969a7beab41..cd13da67a4b9 100644 --- a/apps/server/src/provider/Layers/CursorProvider.test.ts +++ b/apps/server/src/provider/Layers/CursorProvider.test.ts @@ -18,6 +18,7 @@ import { discoverCursorModelsViaAcp, getCursorFallbackModels, getCursorParameterizedModelPickerUnsupportedMessage, + applyCursorApiKeyAuth, parseCursorAboutOutput, parseCursorCliConfigChannel, parseCursorVersionDate, @@ -354,13 +355,14 @@ describe("buildCursorProviderSnapshot", () => { version: "2026.04.09-f2b0fcd", status: "error", auth: { status: "unauthenticated" }, - message: "Cursor Agent is not authenticated. Run `agent login` and try again.", + message: + "Cursor Agent is not authenticated. Run `agent login` or set `CURSOR_API_KEY` and try again.", }, discoveryWarning: cursorAcpDiscoveryFailedMessage, }), ).toMatchObject({ status: "error", - message: `Cursor Agent is not authenticated. Run \`agent login\` and try again. ${cursorAcpDiscoveryFailedMessage}`, + message: `Cursor Agent is not authenticated. Run \`agent login\` or set \`CURSOR_API_KEY\` and try again. ${cursorAcpDiscoveryFailedMessage}`, models: [ { slug: "claude-sonnet-4-6", @@ -555,7 +557,8 @@ describe("parseCursorAboutOutput", () => { auth: { status: "unauthenticated", }, - message: "Cursor Agent is not authenticated. Run `agent login` and try again.", + message: + "Cursor Agent is not authenticated. Run `agent login` or set `CURSOR_API_KEY` and try again.", }); }); @@ -576,11 +579,85 @@ describe("parseCursorAboutOutput", () => { auth: { status: "unauthenticated", }, - message: "Cursor Agent is not authenticated. Run `agent login` and try again.", + message: + "Cursor Agent is not authenticated. Run `agent login` or set `CURSOR_API_KEY` and try again.", }); }); }); +describe("applyCursorApiKeyAuth", () => { + const loggedOut = parseCursorAboutOutput({ + code: 0, + stdout: JSON.stringify({ + cliVersion: "2026.08.11-e8db854", + subscriptionTier: null, + userEmail: null, + }), + stderr: "", + }); + + it("keeps a saved login when CURSOR_API_KEY is also set", () => { + const loggedIn = parseCursorAboutOutput({ + code: 0, + stdout: JSON.stringify({ + cliVersion: "2026.08.11-e8db854", + subscriptionTier: "Ultra", + userEmail: "user@example.com", + }), + stderr: "", + }); + + expect(applyCursorApiKeyAuth(loggedIn, { CURSOR_API_KEY: "crsr_test" })).toEqual(loggedIn); + }); + + it("treats a null about email as API-key auth when CURSOR_API_KEY is set", () => { + expect(applyCursorApiKeyAuth(loggedOut, { CURSOR_API_KEY: "crsr_test" })).toEqual({ + version: "2026.08.11-e8db854", + status: "ready", + auth: { + status: "authenticated", + type: "apiKey", + label: "Cursor API Key", + }, + }); + }); + + it("leaves a logged-out about probe unauthenticated without CURSOR_API_KEY", () => { + expect(applyCursorApiKeyAuth(loggedOut, {})).toEqual(loggedOut); + }); + + it("does not treat an unknown about probe as API-key auth", () => { + const unknown = parseCursorAboutOutput({ + code: 0, + stdout: JSON.stringify({ + cliVersion: "2026.08.11-e8db854", + subscriptionTier: null, + }), + stderr: "", + }); + + expect(unknown).toEqual({ + version: "2026.08.11-e8db854", + status: "ready", + auth: { status: "unknown" }, + }); + expect(applyCursorApiKeyAuth(unknown, { CURSOR_API_KEY: "crsr_test" })).toEqual(unknown); + }); + + it("does not treat an unverifiable about probe as API-key auth", () => { + const unverifiable = parseCursorAboutOutput({ + code: 1, + stdout: "Could not verify Cursor Agent authentication status.", + stderr: "", + }); + + expect(unverifiable.auth.status).toBe("unknown"); + expect(applyCursorApiKeyAuth(unverifiable, { CURSOR_API_KEY: "crsr_test" })).toEqual( + unverifiable, + ); + }); +}); + describe("Cursor parameterized model picker preview gating", () => { it("parses Cursor CLI version dates from build versions", () => { expect(parseCursorVersionDate("2026.04.08-c4e73a3")).toBe(20260408); diff --git a/apps/server/src/provider/Layers/CursorProvider.ts b/apps/server/src/provider/Layers/CursorProvider.ts index fee4306c4c5c..dfc5cefd7bdf 100644 --- a/apps/server/src/provider/Layers/CursorProvider.ts +++ b/apps/server/src/provider/Layers/CursorProvider.ts @@ -50,6 +50,12 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts" const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect( CursorListAvailableModelsResponse, ); +export const CURSOR_API_KEY_ENV = "CURSOR_API_KEY"; + +export function hasCursorApiKey(environment?: NodeJS.ProcessEnv): boolean { + return (environment?.[CURSOR_API_KEY_ENV]?.trim() ?? "").length > 0; +} + const CURSOR_PRESENTATION = { displayName: "Cursor", badgeLabel: "Early Access", @@ -420,6 +426,7 @@ const makeCursorAcpProbeRuntime = ( cwd: process.cwd(), clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" }, authMethodId: "cursor_login", + skipAuthenticate: hasCursorApiKey(environment), clientCapabilities: CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES, }).pipe(Layer.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner))), ); @@ -836,7 +843,8 @@ export function parseCursorAboutOutput(result: CommandResult): CursorAboutResult version, status: "error", auth: { status: "unauthenticated" }, - message: "Cursor Agent is not authenticated. Run `agent login` and try again.", + message: + "Cursor Agent is not authenticated. Run `agent login` or set `CURSOR_API_KEY` and try again.", }; } @@ -869,7 +877,8 @@ export function parseCursorAboutOutput(result: CommandResult): CursorAboutResult version, status: "error", auth: { status: "unauthenticated" }, - message: "Cursor Agent is not authenticated. Run `agent login` and try again.", + message: + "Cursor Agent is not authenticated. Run `agent login` or set `CURSOR_API_KEY` and try again.", }; } @@ -929,7 +938,8 @@ export function parseCursorAboutOutput(result: CommandResult): CursorAboutResult version, status: "error", auth: { status: "unauthenticated" }, - message: "Cursor Agent is not authenticated. Run `agent login` and try again.", + message: + "Cursor Agent is not authenticated. Run `agent login` or set `CURSOR_API_KEY` and try again.", }; } @@ -941,6 +951,25 @@ export function parseCursorAboutOutput(result: CommandResult): CursorAboutResult }; } +export function applyCursorApiKeyAuth( + parsed: CursorAboutResult, + environment?: NodeJS.ProcessEnv, +): CursorAboutResult { + if (!hasCursorApiKey(environment) || parsed.auth.status !== "unauthenticated") { + return parsed; + } + + return { + version: parsed.version, + status: "ready", + auth: { + status: "authenticated", + type: "apiKey", + label: "Cursor API Key", + }, + }; +} + const runCursorCommand = ( cursorSettings: CursorSettings, args: ReadonlyArray, @@ -1055,7 +1084,10 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( }); } - const parsed = parseCursorAboutOutput(aboutProbe.success.value); + const parsed = applyCursorApiKeyAuth( + parseCursorAboutOutput(aboutProbe.success.value), + environment, + ); const cursorCliConfigChannel = yield* readCursorCliConfigChannel(); const parameterizedModelPickerUnsupportedMessage = getCursorParameterizedModelPickerUnsupportedMessage({ diff --git a/apps/server/src/provider/acp/AcpSessionRuntime.ts b/apps/server/src/provider/acp/AcpSessionRuntime.ts index 0dcf72895fd8..bb73a48e075a 100644 --- a/apps/server/src/provider/acp/AcpSessionRuntime.ts +++ b/apps/server/src/provider/acp/AcpSessionRuntime.ts @@ -76,6 +76,7 @@ export interface AcpSessionRuntimeOptions { readonly version: string; }; readonly authMethodId: string; + readonly skipAuthenticate?: boolean; readonly mcpServers?: ReadonlyArray; readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect; readonly protocolLogging?: { @@ -548,15 +549,17 @@ export const make = ( acp.agent.initialize(initializePayload), ); - const authenticatePayload = { - methodId: options.authMethodId, - } satisfies EffectAcpSchema.AuthenticateRequest; + if (!options.skipAuthenticate) { + const authenticatePayload = { + methodId: options.authMethodId, + } satisfies EffectAcpSchema.AuthenticateRequest; - yield* runLoggedRequest( - "authenticate", - authenticatePayload, - acp.agent.authenticate(authenticatePayload), - ); + yield* runLoggedRequest( + "authenticate", + authenticatePayload, + acp.agent.authenticate(authenticatePayload), + ); + } let sessionId: string; let sessionSetupResult: diff --git a/apps/server/src/provider/acp/CursorAcpSupport.ts b/apps/server/src/provider/acp/CursorAcpSupport.ts index 30203ad77b1e..6d236da9999a 100644 --- a/apps/server/src/provider/acp/CursorAcpSupport.ts +++ b/apps/server/src/provider/acp/CursorAcpSupport.ts @@ -8,6 +8,7 @@ import type * as EffectAcpErrors from "effect-acp/errors"; import { CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES, + hasCursorApiKey, resolveCursorAcpBaseModelId, resolveCursorAcpConfigUpdates, } from "../Layers/CursorProvider.ts"; @@ -59,6 +60,7 @@ export const makeCursorAcpRuntime = ( ...input, spawn: buildCursorAcpSpawnInput(input.cursorSettings, input.cwd, input.environment), authMethodId: "cursor_login", + skipAuthenticate: hasCursorApiKey(input.environment), clientCapabilities: CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES, }).pipe( Layer.provide(