Skip to content
Closed
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
15 changes: 15 additions & 0 deletions src/adapters/cursor/desktop-executor-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Opt-in external executor for computer-use / record-screen. opencodex is a headless proxy and
* cannot drive a screen itself; set these commands only when running on a host that can. Each
* command receives the request as JSON on stdin and must print a JSON result on stdout.
*/
export interface DesktopExecutorConfig {
/** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */
computerUseCommand?: string;
/** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */
recordScreenCommand?: string;
cwd?: string;
env?: Record<string, string>;
/** Max time to wait for the external process. Default 30s. */
timeoutMs?: number;
}
17 changes: 2 additions & 15 deletions src/adapters/cursor/native-exec-desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,11 @@ import {
} from "./gen/agent_pb";
import { errorText } from "./native-exec-common";
import type { CursorNativeToolDeps } from "./native-exec-tools";
import type { DesktopExecutorConfig } from "./desktop-executor-contract";

const DEFAULT_DESKTOP_TIMEOUT_MS = 30_000;

/**
* Opt-in external executor for computer-use / record-screen. opencodex is a headless proxy and
* cannot drive a screen itself; set these commands only when running on a host that can. Each
* command receives the request as JSON on stdin and must print a JSON result on stdout.
*/
export interface DesktopExecutorConfig {
/** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */
computerUseCommand?: string;
/** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */
recordScreenCommand?: string;
cwd?: string;
env?: Record<string, string>;
/** Max time to wait for the external process. Default 30s. */
timeoutMs?: number;
}
export type { DesktopExecutorConfig } from "./desktop-executor-contract";

/**
* Build `computerUse` / `recordScreen` deps from external executor commands. Returns `{}` when no
Expand Down
2 changes: 1 addition & 1 deletion src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ export interface OcxProviderConfig {
* headless and cannot control a screen itself; provide commands here only when running on a host
* that can. With no executor, these tools honestly report "not supported".
*/
desktopExecutor?: import("../adapters/cursor/native-exec-desktop").DesktopExecutorConfig;
desktopExecutor?: import("../adapters/cursor/desktop-executor-contract").DesktopExecutorConfig;
/**
* Cursor adapter only: unsafe opt-in escape hatch for Cursor server-driven built-in local
* read/write/delete/ls/grep/shell/fetch execution. Prefer `nativeLocalExec: "on"` for new
Expand Down
24 changes: 23 additions & 1 deletion tests/providers/cursor/cursor-desktop-exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import {
RecordScreenArgsSchema,
} from "../../../src/adapters/cursor/gen/agent_pb";
import { handleCursorNativeExec } from "../../../src/adapters/cursor/native-exec";
import { desktopDepsFromConfig } from "../../../src/adapters/cursor/native-exec-desktop";
import {
desktopDepsFromConfig,
type DesktopExecutorConfig as DesktopExecutorConfigViaImplementation,
} from "../../../src/adapters/cursor/native-exec-desktop";
import type { DesktopExecutorConfig } from "../../../src/adapters/cursor/desktop-executor-contract";
import { shellInvocation } from "../../../src/lib/win-exec";
import { readFileSync } from "node:fs";
import { repoPath } from "../../helpers/repo-root";

function execMessage(message: Parameters<typeof create<typeof ExecServerMessageSchema>>[1]["message"]) {
return create(ExecServerMessageSchema, { id: 3, execId: "exec-test", message });
Expand All @@ -27,6 +33,22 @@ function echoJson(json: string, platform: NodeJS.Platform = process.platform): s
}

describe("Cursor desktop executor hooks", () => {
test("DesktopExecutorConfig is one contract reachable from both paths, and provider types no longer import the implementation", () => {
// Type-level parity: the historical export and the contract leaf must be the same shape.
const viaContract: DesktopExecutorConfig = { computerUseCommand: "x", timeoutMs: 1 };
const viaImplementation: DesktopExecutorConfigViaImplementation = viaContract;
expect(desktopDepsFromConfig(viaImplementation)).toHaveProperty("computerUse");

// Graph guard: the contract is dependency-free and src/types/provider.ts points at it,
// not at native-exec-desktop.ts (that edge closed a type cycle through tool-definitions).
const contract = readFileSync(repoPath("src", "adapters", "cursor", "desktop-executor-contract.ts"), "utf8");
expect(contract).not.toMatch(/^\s*import\s/m);
expect(contract).toMatch(/^export interface DesktopExecutorConfig \{/m);
const provider = readFileSync(repoPath("src", "types", "provider.ts"), "utf8");
expect(provider).toContain('import("../adapters/cursor/desktop-executor-contract").DesktopExecutorConfig');
expect(provider).not.toContain("native-exec-desktop");
});

test("desktopDepsFromConfig returns empty deps when nothing configured", () => {
expect(desktopDepsFromConfig(undefined)).toEqual({});
expect(desktopDepsFromConfig({})).toEqual({});
Expand Down
Loading