diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts b/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts index b060148fe6f9..96027ebe98be 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts @@ -16,7 +16,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; -import { Agent } from "../../provider/cursorSdk.ts"; +import { Agent, createAgentPlatform } from "../../provider/cursorSdk.ts"; import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; import { ProviderEventLoggers } from "../../provider/Layers/ProviderEventLoggers.ts"; @@ -295,6 +295,36 @@ function makeCursorAgentSdkProtocolLogger(input: { .pipe(Effect.ignore); } +/** + * The Cursor SDK decides once per process whether local sandboxing works, and + * caches the answer the first time any run starts. Only sandboxed runs point + * it at its `cursorsandbox` helper first, so after an unsandboxed (Full access) + * run it caches "unsupported" and rejects every later sandboxed run until the + * server restarts. Warming a bare sandboxed executor before the first + * unsandboxed agent opens lets the SDK find the helper and cache the real + * answer. Warming is best effort: on a machine without sandbox support it + * fails, the SDK caches "unsupported", and sandboxed runs report that as + * before. + */ +let cursorSandboxSupportPrime: Promise | undefined; + +function primeCursorSandboxSupport(options: AgentOptions): Promise { + cursorSandboxSupportPrime ??= (async () => { + const cwd = typeof options.local?.cwd === "string" ? options.local.cwd : undefined; + const platform = await createAgentPlatform(cwd === undefined ? {} : { workspaceRef: cwd }); + const release = await platform.prewarmLocalWorkspace({ + ...(options.apiKey === undefined ? {} : { apiKey: options.apiKey }), + local: { + ...(cwd === undefined ? {} : { cwd }), + settingSources: [], + sandboxOptions: { enabled: true }, + }, + }); + await release(); + })().catch(() => undefined); + return cursorSandboxSupportPrime; +} + /** * Runs agents through the Cursor SDK, logging every frame to the protocol * logger chosen for each opened agent. The live layer writes the native @@ -306,6 +336,9 @@ export function makeCursorAgentSdkRunner( ): CursorAgentSdkRunnerShape { return CursorAgentSdkRunner.of({ open: Effect.fn("CursorAgentSdkRunner.open")(function* (input) { + if (input.options.local?.sandboxOptions?.enabled === false) { + yield* Effect.promise(() => primeCursorSandboxSupport(input.options)); + } const protocolLogger = protocolLoggerFor(input); const log = (event: CursorAgentSdkProtocolLogEvent) => protocolLogger === undefined ? Effect.void : protocolLogger(event); diff --git a/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts b/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts index b2971990d12b..8a008a585126 100644 --- a/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts +++ b/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts @@ -220,6 +220,68 @@ describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")( 360_000, ); + it.live( + "runs a sandboxed thread after a full access thread in the same server", + () => + Effect.gen(function* () { + yield* runEffectWorkerDaemonWithOptions({ concurrency: 2 }).pipe(Effect.forkScoped); + const orchestrator = yield* OrchestratorV2; + const projectId = ProjectId.make("project:cursor-live-sandbox-after-full-access"); + + const runThread = Effect.fn("CursorOrchestratorV2Live.runThread")(function* (input: { + readonly name: string; + readonly runtimeMode: "full-access" | "approval-required"; + }) { + const threadId = ThreadId.make(`thread:cursor-live-sandbox:${input.name}`); + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`command:cursor-live-sandbox:${input.name}:create`), + threadId, + projectId, + title: `Cursor live sandbox ${input.name}`, + modelSelection: CURSOR_MODEL_SELECTION, + runtimeMode: input.runtimeMode, + interactionMode: "default", + branch: null, + worktreePath: process.cwd(), + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`command:cursor-live-sandbox:${input.name}:message`), + threadId, + messageId: MessageId.make(`message:cursor-live-sandbox:${input.name}`), + text: "Respond with exactly: OK. Do not use any tools.", + attachments: [], + modelSelection: CURSOR_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }); + return yield* waitForIdle(threadId); + }); + + // The SDK decides once per process whether local sandboxing works. + // The unsandboxed thread must run first to catch a wrong verdict. + const fullAccess = yield* runThread({ name: "full-access", runtimeMode: "full-access" }); + const supervised = yield* runThread({ + name: "supervised", + runtimeMode: "approval-required", + }); + + assert.deepEqual( + fullAccess.runs.map((run) => run.status), + ["completed"], + ); + assert.deepEqual( + supervised.runs.map((run) => run.status), + ["completed"], + ); + }).pipe(Effect.provide(liveLayer), Effect.scoped), + 360_000, + ); + it.live( "spawns native subagents with child thread lineage", () => diff --git a/apps/server/src/provider/cursorSdk.ts b/apps/server/src/provider/cursorSdk.ts index 28593b4eee9d..abe231629b13 100644 --- a/apps/server/src/provider/cursorSdk.ts +++ b/apps/server/src/provider/cursorSdk.ts @@ -4,5 +4,11 @@ import * as NodeModule from "node:module"; // Cursor's Webpack chunks and local helpers must stay beside the SDK entry. // createRequire also loads that disk-backed package from a Node SEA executable. const requireCursorSdk = NodeModule.createRequire(import.meta.url); -export const { Agent, AuthenticationError, Cursor, CursorSdkError, InMemoryCredentialStore } = - requireCursorSdk("@cursor/sdk") as typeof import("@cursor/sdk"); +export const { + Agent, + AuthenticationError, + createAgentPlatform, + Cursor, + CursorSdkError, + InMemoryCredentialStore, +} = requireCursorSdk("@cursor/sdk") as typeof import("@cursor/sdk");