Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions apps/server/src/provider/Layers/CursorProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
discoverCursorModelsViaAcp,
getCursorFallbackModels,
getCursorParameterizedModelPickerUnsupportedMessage,
applyCursorApiKeyAuth,
parseCursorAboutOutput,
parseCursorCliConfigChannel,
parseCursorVersionDate,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
});
});

Expand All @@ -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);
Expand Down
40 changes: 36 additions & 4 deletions apps/server/src/provider/Layers/CursorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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))),
);
Expand Down Expand Up @@ -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.",
};
}

Expand Down Expand Up @@ -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.",
};
}

Expand Down Expand Up @@ -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.",
};
}

Expand All @@ -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",
},
};
Comment thread
cursor[bot] marked this conversation as resolved.
}

const runCursorCommand = (
cursorSettings: CursorSettings,
args: ReadonlyArray<string>,
Expand Down Expand Up @@ -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({
Expand Down
19 changes: 11 additions & 8 deletions apps/server/src/provider/acp/AcpSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface AcpSessionRuntimeOptions {
readonly version: string;
};
readonly authMethodId: string;
readonly skipAuthenticate?: boolean;
readonly mcpServers?: ReadonlyArray<EffectAcpSchema.McpServer>;
readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect<void, never>;
readonly protocolLogging?: {
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/provider/acp/CursorAcpSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down
Loading