From e950b27138b20cc06e4d7b7a2268b9cf996a08e2 Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 09:00:12 +0900 Subject: [PATCH 1/2] refactor(adapters-cursor): isolate desktop executor type contract (split S04 L0/5) --- .../cursor/desktop-executor-contract.ts | 15 +++++++++++++++ src/adapters/cursor/native-exec-desktop.ts | 17 ++--------------- src/types/provider.ts | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 src/adapters/cursor/desktop-executor-contract.ts diff --git a/src/adapters/cursor/desktop-executor-contract.ts b/src/adapters/cursor/desktop-executor-contract.ts new file mode 100644 index 0000000000..2d89a7fa46 --- /dev/null +++ b/src/adapters/cursor/desktop-executor-contract.ts @@ -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; + /** Max time to wait for the external process. Default 30s. */ + timeoutMs?: number; +} diff --git a/src/adapters/cursor/native-exec-desktop.ts b/src/adapters/cursor/native-exec-desktop.ts index 15c6ae31b6..3a7b207ee8 100644 --- a/src/adapters/cursor/native-exec-desktop.ts +++ b/src/adapters/cursor/native-exec-desktop.ts @@ -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; - /** 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 diff --git a/src/types/provider.ts b/src/types/provider.ts index 459634fd16..6e13b5d548 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -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 From 97df51515c22ccd610665989aa940f15bc3bca24 Mon Sep 17 00:00:00 2001 From: t Date: Sat, 5 Sep 2026 09:16:31 +0900 Subject: [PATCH 2/2] test(cursor): guard the desktop executor contract edge (split S04 L0/5) --- .../cursor/cursor-desktop-exec.test.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/providers/cursor/cursor-desktop-exec.test.ts b/tests/providers/cursor/cursor-desktop-exec.test.ts index 43023799ad..9bf0870b9f 100644 --- a/tests/providers/cursor/cursor-desktop-exec.test.ts +++ b/tests/providers/cursor/cursor-desktop-exec.test.ts @@ -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>[1]["message"]) { return create(ExecServerMessageSchema, { id: 3, execId: "exec-test", message }); @@ -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({});