Skip to content
Merged
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
35 changes: 34 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<void> | undefined;

function primeCursorSandboxSupport(options: AgentOptions): Promise<void> {
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
Expand All @@ -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);
Expand Down
62 changes: 62 additions & 0 deletions apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
() =>
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/provider/cursorSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Loading