diff --git a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.i18n.yaml index 7383dc7a71..e9ab8aeda1 100644 --- a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.i18n.yaml @@ -9,8 +9,8 @@ en: a22ddcfb339e503d zh: a1f4fadb46fce1e7 /agent-note-native-cua-objects-independent-of-evaluator-ownership/decision: - en: dce5ac6347fe50a8 - zh: 84df50be936e16dc + en: e0a96e9dc96f3995 + zh: 1de83da57be4c779 /agent-note-native-cua-objects-independent-of-evaluator-ownership/alternatives-considered: en: 62dfcec71ed2a058 zh: 0f3c6edfe19fef1f diff --git a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md index 6b67f2b33d..96728a1d60 100644 --- a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md +++ b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md @@ -10,6 +10,8 @@ Computer-use programs need consistent app and window operations. Coupling those ## Decision +The shared `cua` library owns application/window objects, consent, and logged dispatch. `cua-macos` owns the Swift helper, its build, and its local connection lifecycle. Providers pass a prepared executable and a connection factory to the shared installer, keeping platform code out of object consumers without adding another runtime registry. + The experimental CUA package owns a native-app subset of the Codex-style object interface and selected MIT-licensed macOS behavior adapted from Cua Driver in a Swift helper. A source manifest records the adapted upstream files and local implementation. The helper is locally compiled; the package has no external Cua Driver runtime dependency. The existing [provider registration](2026-09-12-computer-use-provider-registration.md) prevents competing computer-use providers within one composition. The facade accepts an operation dispatcher. Its tool adapter resolves the current tool binding for each operation, preserving normal authorization, logging and image admission. The execution host owns variable persistence and completion serialization. CUA defines no persistence directive, worker or run_code replacement. diff --git a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.zh.md b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.zh.md index 5645b47bdb..7120f4d824 100644 --- a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.zh.md @@ -10,6 +10,8 @@ Computer-use 程序需要一致的应用和窗口操作。将这些操作与 CUA ## 决策 +共享的 `cua` 库负责应用与窗口对象、授权和已记录的派发。`cua-macos` 负责 Swift helper、构建和本地连接生命周期。提供方将准备好的可执行文件和连接工厂交给共享安装函数,使对象调用方无需包含平台代码,也无需增加另一套运行时注册表。 + 实验 CUA 包提供 Codex 风格的原生应用对象接口,并在 Swift 辅助进程中改写 Cua Driver 中选取的 MIT 许可 macOS 行为。源码清单记录改写的上游文件和本地实现。辅助进程在本地编译,本包不依赖外部 Cua Driver 运行时。已有的 [provider 注册机制](2026-09-12-computer-use-provider-registration.zh.md)防止同一组合中出现竞争 provider。 对象接口接收操作分派器。工具适配模块每次操作都获取当前工具绑定,保留标准授权、日志和图片接入。执行宿主负责变量持久化和最终表达式值的序列化。CUA 不定义持久化指令、执行 worker 或 run_code 替代实现。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 087c2d0012..b755400d20 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -790,72 +790,6 @@ export interface Config { ``` - - - -## `@deepseek-ai/dsh-cua` - -- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents` -- `refs`: [`Branded`](../packages/util/brand/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/experimental/cua/src/index.ts:32`](../packages/experimental/cua/src/index.ts) - -```ts config-catalog -/** Native executable and per-operation resource bounds. */ -export interface Config { - /** Signed applications explicitly approved for future tasks in this profile. */ - allowedApps: Volatile & { - /** Unbranded digest at the profile configuration boundary; admitted by the Config schema. */ - id: string - }>> - /** Absolute native helper override; omission prepares the package-owned executable. */ - helperPath?: string - /** Maximum time in milliseconds for native component preparation. */ - buildTimeoutMs: number - /** Deadline for one native operation, including capture and AX traversal. */ - timeoutMs: number - /** Maximum time to connect to or start the shared native helper. */ - connectTimeoutMs: number - /** Screenshot deadline including time queued behind other native operations. */ - screenshotTimeoutMs: number - /** Deadline for each individual Accessibility system call. */ - axTimeoutMs: number - /** Maximum bytes retained for one helper response frame. */ - maxResponseBytes: number - /** Maximum installed application matches returned for one discovery query. */ - maxAppMatches: number - /** Maximum bytes preserved from the system clipboard during one paste. */ - maxClipboardBytes: number - /** Maximum UTF-8 bytes admitted for one helper request frame. */ - maxRequestBytes: number - /** Duration in milliseconds of one same-window drag. */ - dragDurationMs: number - /** Number of movement segments dispatched during a drag. */ - dragSteps: number - /** Delay in milliseconds after each Unicode key-down or key-up event. */ - textEventIntervalMs: number - /** Native connection cleanup, socket-write and launcher termination budget in milliseconds. */ - graceMs: number - /** Maximum nodes admitted by a native accessibility snapshot. */ - maxElements: number - /** Maximum depth traversed by a native accessibility snapshot. */ - maxDepth: number -} - -/** Identity shown to the human before application access is granted. */ -export interface CuaApplicationGrant { - /** Native identity digest admitted from the helper or validated profile configuration. */ - id: CuaApplicationId - /** Application label presented when the decision was made. */ - name: string - /** Canonical executable path presented with the application label. */ - executablePath: string -} - -/** Native digest of an executable path and its signing requirement, process lifetime or pending executable identity. */ -export type CuaApplicationId = Branded<'CuaApplicationId'> -``` - - @@ -1090,6 +1024,21 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-experimental-cua-macos` + +- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents` +- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts) +- `source`: [`packages/experimental/cua-macos/src/index.ts:16`](../packages/experimental/cua-macos/src/index.ts) + +```ts config-catalog +/** Native preparation, operation bounds, and profile-local application grants. */ +export type Config = CuaConfig +``` + + @@ -4715,6 +4664,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-client-ui-slots` | — | [`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts) | | `@deepseek-ai/dsh-client-web` | — | [`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts) | | `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) | +| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) | | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | | `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | | `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index ee898ff635..1a2d24b439 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -792,72 +792,6 @@ export interface Config { ``` - - - -## `@deepseek-ai/dsh-cua` - -- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents` -- `refs`: [`Branded`](../packages/util/brand/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/experimental/cua/src/index.ts:32`](../packages/experimental/cua/src/index.ts) - -```ts config-catalog -/** Native executable and per-operation resource bounds. */ -export interface Config { - /** Signed applications explicitly approved for future tasks in this profile. */ - allowedApps: Volatile & { - /** Unbranded digest at the profile configuration boundary; admitted by the Config schema. */ - id: string - }>> - /** Absolute native helper override; omission prepares the package-owned executable. */ - helperPath?: string - /** Maximum time in milliseconds for native component preparation. */ - buildTimeoutMs: number - /** Deadline for one native operation, including capture and AX traversal. */ - timeoutMs: number - /** Maximum time to connect to or start the shared native helper. */ - connectTimeoutMs: number - /** Screenshot deadline including time queued behind other native operations. */ - screenshotTimeoutMs: number - /** Deadline for each individual Accessibility system call. */ - axTimeoutMs: number - /** Maximum bytes retained for one helper response frame. */ - maxResponseBytes: number - /** Maximum installed application matches returned for one discovery query. */ - maxAppMatches: number - /** Maximum bytes preserved from the system clipboard during one paste. */ - maxClipboardBytes: number - /** Maximum UTF-8 bytes admitted for one helper request frame. */ - maxRequestBytes: number - /** Duration in milliseconds of one same-window drag. */ - dragDurationMs: number - /** Number of movement segments dispatched during a drag. */ - dragSteps: number - /** Delay in milliseconds after each Unicode key-down or key-up event. */ - textEventIntervalMs: number - /** Native connection cleanup, socket-write and launcher termination budget in milliseconds. */ - graceMs: number - /** Maximum nodes admitted by a native accessibility snapshot. */ - maxElements: number - /** Maximum depth traversed by a native accessibility snapshot. */ - maxDepth: number -} - -/** Identity shown to the human before application access is granted. */ -export interface CuaApplicationGrant { - /** Native identity digest admitted from the helper or validated profile configuration. */ - id: CuaApplicationId - /** Application label presented when the decision was made. */ - name: string - /** Canonical executable path presented with the application label. */ - executablePath: string -} - -/** Native digest of an executable path and its signing requirement, process lifetime or pending executable identity. */ -export type CuaApplicationId = Branded<'CuaApplicationId'> -``` - - @@ -1092,6 +1026,21 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-experimental-cua-macos` + +- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents` +- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts) +- `source`: [`packages/experimental/cua-macos/src/index.ts:16`](../packages/experimental/cua-macos/src/index.ts) + +```ts config-catalog +/** Native preparation, operation bounds, and profile-local application grants. */ +export type Config = CuaConfig +``` + + @@ -4717,6 +4666,7 @@ export interface Config { | `@deepseek-ai/dsh-client-ui-slots` | — | [`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts) | | `@deepseek-ai/dsh-client-web` | — | [`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts) | | `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) | +| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) | | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | | `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | | `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) | diff --git a/docs/module-graph.md b/docs/module-graph.md index a3721b01b4..29aa8dffcb 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -256,6 +256,7 @@ flowchart TD pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"] pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"] pkg_experimental_cot_translation_bundle["experimental-cot-translation-bundle"] + pkg_experimental_cua_macos["experimental-cua-macos"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] @@ -912,6 +913,12 @@ flowchart TD pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt pkg_experimental_computer_use_cua_driver_native --> pkg_tools + pkg_experimental_cua_macos --> pkg_agent + pkg_experimental_cua_macos --> pkg_computer_use + pkg_experimental_cua_macos --> pkg_subprocess + pkg_experimental_cua_macos --> pkg_system_prompt + pkg_experimental_cua_macos --> pkg_tools + pkg_experimental_cua_macos --> pkg_user_questions pkg_experimental_tool_worktree --> pkg_experimental_worktree pkg_experimental_tool_worktree --> pkg_tools pkg_hooks_codex --> pkg_agent @@ -1696,6 +1703,7 @@ flowchart TD | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`experimental-cua-macos`](../packages/experimental/cua-macos) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) | | [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index f2e9fd2823..b8f2f2c091 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -258,6 +258,7 @@ flowchart TD pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"] pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"] pkg_experimental_cot_translation_bundle["experimental-cot-translation-bundle"] + pkg_experimental_cua_macos["experimental-cua-macos"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] @@ -914,6 +915,12 @@ flowchart TD pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt pkg_experimental_computer_use_cua_driver_native --> pkg_tools + pkg_experimental_cua_macos --> pkg_agent + pkg_experimental_cua_macos --> pkg_computer_use + pkg_experimental_cua_macos --> pkg_subprocess + pkg_experimental_cua_macos --> pkg_system_prompt + pkg_experimental_cua_macos --> pkg_tools + pkg_experimental_cua_macos --> pkg_user_questions pkg_experimental_tool_worktree --> pkg_experimental_worktree pkg_experimental_tool_worktree --> pkg_tools pkg_hooks_codex --> pkg_agent @@ -1698,6 +1705,7 @@ flowchart TD | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`experimental-cua-macos`](../packages/experimental/cua-macos) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) | | [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | diff --git a/docs/subsystems/computer-use.i18n.yaml b/docs/subsystems/computer-use.i18n.yaml index 862d0fe917..a0cf7675d0 100644 --- a/docs/subsystems/computer-use.i18n.yaml +++ b/docs/subsystems/computer-use.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/computer-use.md /computer-use: - en: f7410eb6db7e1636 - zh: 5adc97516d1d6c6f + en: a82a6d1ec3fa9724 + zh: 17483872a54d6899 /computer-use/choose-a-provider: en: fa2e0d7b41c0450b zh: af9a799711f901e6 diff --git a/docs/subsystems/computer-use.md b/docs/subsystems/computer-use.md index 6f9962a108..6331c0f492 100644 --- a/docs/subsystems/computer-use.md +++ b/docs/subsystems/computer-use.md @@ -5,7 +5,7 @@ English | [中文](computer-use.zh.md) Computer use lets a model observe and operate the local desktop through a configured provider. The shared DSH capability is called **computer use**; **Cua Driver** names the upstream implementation. -The native [`dsh-cua`](../../packages/experimental/cua/README.md) provider supports explicit source composition and enforces application consent through User Questions and profile configuration. +The [`macOS CUA provider`](../../packages/experimental/cua-macos/README.md) uses the [shared CUA objects and executor](../../packages/experimental/cua/README.md), which enforce application consent through User Questions and profile configuration. ## Choose a provider diff --git a/docs/subsystems/computer-use.zh.md b/docs/subsystems/computer-use.zh.md index aa1fc3ecdf..cd4901bd1a 100644 --- a/docs/subsystems/computer-use.zh.md +++ b/docs/subsystems/computer-use.zh.md @@ -5,7 +5,7 @@ 计算机操作让模型通过配置的提供方观察并操作本地桌面。DSH 的共享能力称为 **computer use(计算机操作)**;**Cua Driver** 是上游实现的名称。 -原生 [`dsh-cua`](../../packages/experimental/cua/README.zh.md) 提供方支持显式源码组合,通过 User Questions 和配置文件执行应用授权规则。 +[macOS CUA 提供方](../../packages/experimental/cua-macos/README.zh.md)使用[共享 CUA 对象与执行器](../../packages/experimental/cua/README.zh.md),通过 User Questions 和配置档设置执行应用授权。 ## 选择提供方 diff --git a/package.json b/package.json index fc55ddc23f..2f07ead44f 100644 --- a/package.json +++ b/package.json @@ -282,6 +282,7 @@ "@deepseek-ai/dsh-tool-working-directory": "workspace:*", "@deepseek-ai/dsh-working-directory": "workspace:*", "@deepseek-ai/dsh-experimental-worktree": "workspace:*", - "@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*" + "@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*", + "@deepseek-ai/dsh-experimental-cua-macos": "workspace:*" } } diff --git a/packages/experimental/README.i18n.yaml b/packages/experimental/README.i18n.yaml index 1e49ece9e9..0a584c5480 100644 --- a/packages/experimental/README.i18n.yaml +++ b/packages/experimental/README.i18n.yaml @@ -18,8 +18,8 @@ en: b55aeb927737b90d zh: 0b215fcb5c2c7908 /packages-experimental/packages: - en: b404c79bddc92273 - zh: d6b9afb8777a8126 + en: 4e7d0a6ceafedf97 + zh: 681e72965bac1f95 /packages-experimental/related-documentation: en: 1cb9d3448d44e48d zh: cd326d70a746b896 diff --git a/packages/experimental/README.md b/packages/experimental/README.md index b6ef6ced1c..6b38fa850a 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -66,7 +66,8 @@ Promotion requires a named product role, documented public behavior and limitati | [`claude-code-mods`](claude-code-mods/README.md) | Run Claude Code mods as plugins: their hook chains on harness extension points and a band above the prompt | `ctx.claudeCodeMods` | | [`client-ui-claude-code-mods`](client-ui-claude-code-mods/README.md) | The Web band that draws a mod's tree above the prompt and sends button clicks back | — | | [`ptc-runtime-python`](ptc-runtime-python/README.md) | CPython subprocess backend for the PTC execution seam | `ctx.ptcRuntime` | -| [`cua`](cua/README.md) | Native macOS application and window operations | `ctx.computerUse` | +| [CUA](cua/README.md) | Shared application/window objects, consent, and logged execution. | Library | +| [macOS CUA](cua-macos/README.md) | Swift native execution and platform preparation. | `ctx.computerUse` | | [`computer-use-cua-driver-mcp`](computer-use-cua-driver-mcp/README.md) | Use an installed Cua Driver through MCP | `ctx.computerUse` | | [`computer-use-cua-driver-native`](computer-use-cua-driver-native/README.md) | Embed the Cua Driver native npm runtime | `ctx.computerUse` | | [`browser-use-playwright-mcp`](browser-use-playwright-mcp/README.md) | Playwright browser tools over MCP | `ctx.browserUse` | diff --git a/packages/experimental/README.zh.md b/packages/experimental/README.zh.md index 1042d2c981..b6d63e526c 100644 --- a/packages/experimental/README.zh.md +++ b/packages/experimental/README.zh.md @@ -66,7 +66,8 @@ kind: "package-group" | [`claude-code-mods`](claude-code-mods/README.zh.md) | 把 Claude Code 模组作为插件运行:钩子链落在 harness 扩展点上,并在提示框上方绘制横幅 | `ctx.claudeCodeMods` | | [`client-ui-claude-code-mods`](client-ui-claude-code-mods/README.zh.md) | 在提示框上方绘制模组树并把按钮点击发回的 Web 横幅 | — | | [`ptc-runtime-python`](ptc-runtime-python/README.zh.md) | PTC 执行 seam 的 CPython 子进程后端 | `ctx.ptcRuntime` | -| [`cua`](cua/README.zh.md) | 原生 macOS 应用与窗口操作 | `ctx.computerUse` | +| [CUA](cua/README.zh.md) | 共享应用与窗口对象、授权和已记录的执行。 | 库 | +| [macOS CUA](cua-macos/README.zh.md) | Swift 原生执行与平台组件准备。 | `ctx.computerUse` | | [`computer-use-cua-driver-mcp`](computer-use-cua-driver-mcp/README.zh.md) | 通过 MCP 使用已安装的 Cua Driver | `ctx.computerUse` | | [`computer-use-cua-driver-native`](computer-use-cua-driver-native/README.zh.md) | 嵌入 Cua Driver 原生 npm 运行时 | `ctx.computerUse` | | [`browser-use-playwright-mcp`](browser-use-playwright-mcp/README.zh.md) | 通过 MCP 提供 Playwright 浏览器工具 | `ctx.browserUse` | diff --git a/packages/experimental/cua-macos/README.i18n.yaml b/packages/experimental/cua-macos/README.i18n.yaml new file mode 100644 index 0000000000..db504c10b9 --- /dev/null +++ b/packages/experimental/cua-macos/README.i18n.yaml @@ -0,0 +1,52 @@ +# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/experimental/cua-macos/README.md +/: + en: a1503331d3f35516 + zh: fcb8e5220ff732e1 +/deepseek-ai-dsh-experimental-cua-macos: + en: 022ca213425e11ee + zh: d64631748ed91cc2 +/deepseek-ai-dsh-experimental-cua-macos/summary: + en: 4549f02fc53854a5 + zh: 6f74f1130b6623a8 +/deepseek-ai-dsh-experimental-cua-macos/table-of-contents: + en: d152484eb41ac6b4 + zh: 7b30b973a849247d +/deepseek-ai-dsh-experimental-cua-macos/use-this-package: + en: b80fbe57355aefad + zh: 6ae84f80ad3f7829 +/deepseek-ai-dsh-experimental-cua-macos/use-this-package/connect-an-external-execution-environment: + en: 110fda1de696160b + zh: b3d20cf50d984505 +/deepseek-ai-dsh-experimental-cua-macos/understand-the-implementation: + en: 8b9ec208ca057a4a + zh: e8dbb45891ad15b8 +/deepseek-ai-dsh-experimental-cua-macos/further-exploration: + en: ff9fac614001a98f + zh: 329678941154aaea +/deepseek-ai-dsh-experimental-cua-macos/model-experience: + en: 215e7ba838619b7b + zh: a311da3843709f90 +/deepseek-ai-dsh-experimental-cua-macos/model-experience/native-application-operations: + en: ab7cba4a42334ab9 + zh: f77cd9d84624b6c2 +/deepseek-ai-dsh-experimental-cua-macos/model-experience/native-application-operations/what-the-model-sees: + en: c221fa5ce647b1dd + zh: 9388c91a9f89e029 +/deepseek-ai-dsh-experimental-cua-macos/model-experience/native-application-operations/what-the-model-sees/operation-guidance-verbatim: + en: 2dbc22ffd1f8a717 + zh: d4458872e75305d9 +/deepseek-ai-dsh-experimental-cua-macos/model-experience/native-application-operations/token-effect: + en: ab52b46d874fa8b9 + zh: e87d3620ef77cb06 +/deepseek-ai-dsh-experimental-cua-macos/model-experience/native-application-operations/kv-cache-effect: + en: a2c9acb5a4320dda + zh: e8d1586af60215c4 +/deepseek-ai-dsh-experimental-cua-macos/known-limitations-and-deferred-work: + en: 3a86880cfb4ec1fb + zh: 40fd3715d610d32e +/deepseek-ai-dsh-experimental-cua-macos/known-limitations-and-deferred-work/dev-note: + en: b836f01bd0bceb4a + zh: 8122fa08b2aa95fa diff --git a/packages/experimental/cua-macos/README.md b/packages/experimental/cua-macos/README.md new file mode 100644 index 0000000000..957a47df87 --- /dev/null +++ b/packages/experimental/cua-macos/README.md @@ -0,0 +1,218 @@ +--- +description: "Inspect and operate native macOS applications through scoped app and window objects." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-experimental-cua-macos + +English | [中文](README.zh.md) + +## Summary + +Inspect macOS permissions, select running applications and windows, read accessibility trees, capture screenshots and send input. The object API can be retained by an external persistent execution environment. The Swift helper uses macOS frameworks directly and adapts selected MIT-licensed Cua Driver behavior. This package owns neither a PTC runtime nor variable persistence. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Enable this experimental provider through explicit source composition on macOS 14 or later. Windows support is in development. Shipped profiles do not mount it. Prepare a native helper and load the registry/provider overlay: + +```sh +swift build --package-path packages/experimental/cua-macos/native --configuration release +cua_native_root="$PWD/packages/experimental/cua-macos/native" +export DSH_CUA_HELPER="$cua_native_root/.build/release/dsh-cua-native" +pnpm dsh headless --patch packages/experimental/cua-macos/examples/macos.cordis.yml --dump-config +``` + +The dump command checks profile composition without a model request. For a real task, replace `--dump-config` with a prompt and configure the profile’s model credentials. The overlay mounts the computer-use registry and this provider; remove an existing competing provider before using it. The responsible host/helper needs Accessibility permission for AX/input and Screen Recording permission for capture. `getCapabilities` reports permissions and session lock state without opening a system dialog. Locked or unavailable desktop sessions reject UI observations and input before dispatch; the user must unlock macOS before resuming. `listApps` and `cua.getState` list running applications. `findApps(query)` searches standard macOS application directories by name, bundle ID or absolute path. `getApp` selects a unique name, bundle ID, path or running PID, obtains application consent, and launches a stopped app in the background when needed. A stopped executable is identified before approval and checked again before and after launch; unsigned code only receives task access. The returned object reads initial AX without activating the app. Launch cancellation may leave the application running. Model guidance starts with background observation and input, without activation during initialization or before each action. For hidden or minimized windows, try `app.show()` or `window.show()` and read fresh state. This requests unhide and unminimize without activation, raising or switching Spaces; app-level restoration selects the focused, main or sole AX window. The result names the selected `windowId`, or null when no window was selected, and reports `foregroundChanged` by comparing the front app and top window before and after the request. Null means foreground evidence was unavailable; later focus changes remain possible. If restoration fails or the window remains unusable, explain the needed foreground step, call `app.activate()` or an observed window’s `raise` action, then observe again. Necessary foreground access takes priority over preserving focus. Applications may change focus after input; background delivery does not guarantee an undisturbed user session. + +| Field | Default | Meaning | +| --- | --- | --- | +| `allowedApps` | `[]` | Live profile-local application consent, keyed by executable path and verified signing requirement. | +| `helperPath` | package-owned executable | Optional absolute native executable override. | +| `buildTimeoutMs` | `300000` | Maximum native preparation time. | +| `timeoutMs` | `30000` | Per-operation deadline; cancellation can leave partial input. | +| `connectTimeoutMs` | `5000` | Deadline for connecting to or starting the shared helper. | +| `screenshotTimeoutMs` | `5000` | Screenshot deadline including queue time, capped by `timeoutMs`. | +| `axTimeoutMs` | `3000` | Deadline per Accessibility system call, configured independently of the complete operation. | +| `maxRequestBytes` / `maxResponseBytes` | `1048576` / `33554432` | UTF-8 request and response limits. | +| `maxClipboardBytes` | `33554432` | Maximum original clipboard bytes preserved during a paste; oversized contents reject before replacement. | +| `maxAppMatches` | `20` | Maximum installed-app search matches; broader queries reject and require a narrower name. | +| `maxElements` / `maxDepth` | `2000` / `25` | Accessibility traversal limits. | +| `dragDurationMs` / `dragSteps` | `400` / `20` | Duration and movement segments for a drag. | +| `textEventIntervalMs` | `0.25` | Delay after each Unicode down/up event, from 0 to 1000 ms. Chord timing is independent. | + +The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-cua-macos) lists all fields. Native tool calls use `{method, params}`; the complete method-specific schema is exposed to the model. For example, `getApp` returns the `appHandle` needed by `listWindows`, `getState` and input operations. + +Keypad names (`KP_0` through `KP_9`, `KP_Enter`, and keypad operators) retain their physical key codes. Key chords use `+` between modifiers and the key. Use `+` for the plus key, `cmd++` for Command-plus, or the key name `plus`; an unfinished chord such as `cmd+` is rejected. + +Agent disposal and plugin unload cancel and await the complete tool call, including image admission and attachment storage. An image write already in progress must settle before disposal returns; a canceled call cannot return a successful screenshot. + +Application consent is enforced before AX reads, window-title reads, screenshots, activation, and input through the CUA tool. User Questions offers task access or denial, plus saved access when the application has a verifiable signature and the composition provides settings persistence. Task access is bound to the selected process handle; saved access is bound to its canonical executable path and designated code-signing requirement. Denial prevents repeated prompts in that task. Removing a saved `allowedApps` entry cancels affected operations and invalidates their helper handles; reauthorization requires fresh selection. Other handles on the same Agent connection also expire, while saved grants remain reusable. Agent/provider disposal joins cleanup of the corresponding native connection. See the [application consent decision](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md). These checks govern CUA calls; persistent PTC and separately authorized shell tools retain their own execution authority. + +Raw `getState` exposes validated state fields and the complete text for the requested disclosure options. The native helper also returns structured element diagnostics for native checks; the tool projection omits that duplicate array from model text and structured tool values. + +AX observations show menu-bar entries by default. Use `app.getAXState({ includeMenus: true })` to expand menu contents and `app.getAXState({ details: true })` for identifiers, global-point bounds and complete action names. These options also apply to raw `getState` and combined AX/screenshot observations. Every observation replaces that scope's indices; obtain fresh indices from the expanded output. Details do not bypass the configured node and depth limits. Truncation identifies the reached limit. + +Values and placeholders are separate. Named, stateful or actionable groups and scroll areas retain their own rows. Repeated names, descriptions and help text on one node are shown once. Values, placeholders and URLs keep their distinct meanings even when their text matches; body text shared by distinct nodes remains intact. Writability is explicit on relevant controls, including `AXIncrementor`; `unknown` means the system did not report it. Missing optional state fields do not imply false. UTF-16 ranges outside the observed value are marked unavailable. Custom action aliases omit AppKit target/selector diagnostics; ambiguous aliases require a complete name from `details:true`. + +Observations emit by default and also return their value. In persistent PTC, use `void await app.getAXState()` for one emitted copy, or `emit:false` and one explicit print. Capturing a screenshot after an emitted tree can fail independently; retain the successful AX output. + +Screenshot diagnostics use `[cua-screenshot]` records keyed by the tool request ID. Native phase records return over the originating Agent connection and cover window/AX geometry checks, system capture, PNG/Base64 encoding and response writing. Host debug logs add dispatch, reply and cleanup timings. Each request retains at most 8192 bytes of complete diagnostic records, with explicit truncation; screenshot failures append these records to the error for Session export. Records contain stages, timings and error codes, not window titles, AX text or image bytes. + +Native preparation serializes compiler checks and publication with the shared file lock. It updates the package payload and runs a content-addressed copy under native/.build/prepared, preserving executable files used by earlier source-runtime processes. Identical builds reuse the same file; corrupted retained copies fail validation. Cancellation and preparation failure finish managed process cleanup before releasing the lock. + +### Connect an external execution environment + +To run through persistent PTC, add `--patch packages/experimental/cua-macos/examples/persistent-ptc.cordis.yml` after the macOS overlay. This provider keeps variables for the live Agent and applies the selected file sandbox policy. The caller imports the bridge and supplies the current authorized `tools.cua` binding. Persistent PTC receives an installation-owned bridge file URL in its CUA instructions, so the project does not need to install this package. The example below is for an external host that resolves the package from its own installation and supplies `tools`; the package does not install a global object or keep variables alive: + +```javascript +import { connectCua } from '@deepseek-ai/dsh-cua/bridge' +const cua = connectCua(() => tools.cua, text => console.log(text)) +const app = await cua.getApp("com.apple.TextEdit") +void await app.getScreenshot() +``` + +Use app methods directly for single-window tasks. `getApp` reads initial AX state, so a screenshot needs no window enumeration or additional AX read. Use `app.getWindows()` and `app.getWindow(windowId)` when the task needs a particular window or default selection is ambiguous; choose by the observed title and bounds. + +`getWindow` emits its initial AX tree. An owned window without an AX surface returns an empty snapshot and remains available for screenshots and coordinate input. Window objects retain separate snapshots, so reading one window does not invalidate another. Re-reading the same scope invalidates old indices. An AX node removed during observation rejects the read with `STALE_SNAPSHOT` and publishes no partial tree; read again without repeating the preceding input. App-scoped screenshots and coordinates reuse the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not query current focus again; read `app.getAXState()` to follow a focus change. Explicit window objects retain their selected target. Keyboard methods stay on `app` because input follows the app’s current focus. + +`paste(text, { format: "text" | "html" })` temporarily publishes the requested clipboard format and invokes the application’s enabled standard Paste menu command. It keeps the temporary format until a pasteboard reader requests its data, then restores every captured original format while it still owns the clipboard, preserving a later copy. Paste operations share a cross-process lock and cancellation joins helper cleanup. A forced process exit can prevent restoration. Unsupported Paste commands fail without typing fallback; `typeText` remains separate key-event input. + +`selectText` selects a unique literal occurrence or places the cursor before/after it through writable `AXSelectedTextRange`. Adjacent prefix/suffix disambiguate repeats. Selection preserves the remaining text and formatting, and verifies the resulting UTF-16 range; unsupported, missing, ambiguous or changed text fails without keyboard fallback. AX text includes `selectionWritable`, selection ranges and selected text when available. + +Screenshot data must use canonical Base64. Response byte limits apply before parsing, and attachment admission validates the raster and its configured capacity. + +Coordinate clicks, scrolling and dragging require a successful AX read or screenshot of that window. A width or height change since the latest observation returns `WINDOW_CHANGED` before input, including raw calls without a snapshot ID; read fresh AX or take a new screenshot before proceeding. Window movement alone preserves window-local coordinates. Capture and pointer input reject known disagreement between the selected window’s accessibility frame and displayed bounds with `WINDOW_CHANGED`, before posting pointer events. This includes Stage Manager transformations when AX retains the full window frame. Read fresh state; any activation or window raise remains explicit. Advertised AX actions remain available. Missing AX geometry retains the AXless path and does not establish that a transformed window can be operated. + +Retained objects resolve the current tool binding on every call. The external runtime owns persistence, imports, cancellation and completion-value serialization. Screenshots return PNG bytes; the standard tool pipeline admits the image attachment exactly once. `emit: false` suppresses facade output only. A host that uses another dispatcher can build objects through `createCuaApi` from `./api`. + +AppKit sheets are selectable by their window IDs and remain visible in their document window’s tree. Keyboard input follows a system Open/Save panel only when the application’s focused sheet identifies one Apple panel-service window and that service still focuses the same panel. Unrelated or ambiguous remote recipients fail before dispatch. + +Indexed clicks support a single right/middle click or a single/double left click. Click and drag options accept a button and modifier keys; a modified click uses pointer input. Drags hold that button and carry its modifiers through every movement and release. AX text marks disabled controls, `valueWritable=true`, and available `selected` state. The helper uses an advertised accessibility action for unmodified clicks when available; pointer delivery resolves the element’s current bounds and verifies its owning window. Actions report dispatch, not application success. A visual pointer or click marker can be rendered independently of input; AX actions do not require moving the system cursor. Window-directed mouse input can still change application focus, selection or dialogs. Read state after input. `STALE_APP` requires acquiring the application again; `STALE_SNAPSHOT` requires a fresh AX read. Request timeout/cancellation waits for native acknowledgement and preserves usable handles. Missing cleanup acknowledgement closes that Agent’s connection. A screenshot RPC still awaiting its system callback occupies the single capture slot; further screenshots return `CAPTURE_BUSY`, while AX and input remain available. A late capture result is discarded, and its real completion releases that slot. Only an unresponsive operation or failed cleanup can invalidate the shared process and all its handles. Never retry uncertain input automatically. Closing an Agent releases its connection and AX references, without undoing changes to applications. + +----- + + +## Understand the implementation + +
+Dispatch and resource ownership + +The object facade translates app/window methods into JSON operations. The bridge routes them through the normal tool pipeline, preserving authorization, nested logs and image admission. Agents and Hosts belonging to one user share a helper for the same native build. Each connection retains independent handles, snapshots, configuration and cancellation state; desktop operations use a global queue. The managed launcher transfers service lifetime to these connections, and the helper exits after the last connection finishes cleanup. Process locks cover both build identity and the real executable path, preventing duplicate startup and path collisions after a running executable is replaced. Native handles bind a process identity to its kernel start time, and snapshots retain native AX references only inside that helper. The same operation protocol can serve a future platform provider without adding platform logic to the object API. + +[Zod operation definitions](../cua/src/protocol.ts) own model parameters and separately declare Host resource fields and private continuations. Public data types derive from those definitions. `pnpm exec tsx packages/experimental/cua-macos/native/generate-protocol.ts` generates the native JSON Schema and Swift field/enum admission tables; the package test lane checks freshness and rejects Host-only fields at model admission. Native target validation, resource limits and operation behavior remain enforced at execution. + +[AX text formatting](native/Sources/CuaNative/AXTextFormatter.swift) consumes already-read node data. The AX walker alone assigns indices and retains native references, so formatting owns no second snapshot or target registry. + +Native request IDs match the logged tool call IDs. Successful activation and window-raise results retain foreground request details and immediate before/after focus observations in the raw tool result. An already-frontmost app skips redundant activation; a window raise is skipped only when the exact window is also focused, main, unminimized and first among visible normal windows. Unknown observations retain the requested operation. These observations do not establish who caused an unrelated focus change or verify completion. + +Node roles and metadata share one AX batch per observation. Group and scroll-area nodes contribute only children; unused metadata errors do not reject them. Unsupported batching retains individual attribute reads. AX traversal visits each native identity once, including proxy references that compare equal, so cyclic accessibility children cannot repeat whole subtrees. The [source manifest](native/upstream.json) records adapted upstream behavior and local Swift modules. AppKit runs on the main event loop; a separate serial executor owns AX references and synchronous input. A request retains exclusive execution through capture and response delivery. No invariant companion is published: there is no independently projected registry or durable evaluator state to reconcile. Protocol validation, target identity and resource cleanup are checked by their owning implementations. + +
+ +----- + + +## Further Exploration + +- [Computer-use registration](../../computer-use/computer-use/README.md) +- [Native protocol](native/protocol.schema.json) +- [Object interfaces](../cua/src/types.ts) + +----- + + +## Model Experience + +### Native application operations + +#### What the model sees + +The plugin contributes one `cua` operation tool with method-specific parameters. Standard PTC presentation exposes it as `tools.cua` through `run_code`. The following package-owned guidance is emitted verbatim: + +##### Operation guidance (verbatim) + +```markdown +Use cua with method getCapabilities and params {} to inspect permissions and sessionLocked. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI attempts and ask the user to unlock macOS. A null sessionLocked means no usable console session was verified. After unlocking, read fresh state; screenshots from a locked desktop are not current observations. Use listApps or cua.getState to discover running applications, and findApps(query) to search installed applications by name, bundle ID or absolute path. getApp requests user consent before launching a stopped application in the background, or before observations and input on a running one. If access is denied or revoked, stop using that application until the user explicitly grants access again. Never switch tools to bypass a refusal. Keep the returned appHandle; every app operation requires it. listWindows returns window IDs and point bounds. Raw getState returns AX text and snapshotId. Menu-bar entries are shown by default; includeMenus=true expands their contents. details=true adds identifiers, global-point bounds and complete action names. Both options also apply to getAXState. Each read replaces the indices for that app/window scope. Use its element indices only with that snapshotId. Raw screenshot and coordinate operations require a windowId. Coordinates are window-local macOS points, which may differ from screenshot pixels. A verified window can expose no AX elements. Its empty snapshot permits screenshots and coordinate input; element-index actions require an element from a fresh AX observation. + +Initialize the object API once in persistent PTC. Reuse its variables in later calls; put temporary declarations inside { ... } blocks to avoid redeclaration errors. All object methods are async. getAXState returns text, not a structured state object. Its text separates values and placeholders, and reports subroles, help, URLs, disabled/focused/selected/expanded state and UTF-16 selection ranges when available. Writability is true, false or unknown on relevant controls; omitted state fields do not assert false. selection=unavailable means the reported range exceeds the observed text. Compact output omits ordinary Press/Pick (click), ShowMenu (right click) and ScrollToVisible actions; details=true lists every available action. Use the displayed action name with performSecondaryAction; ambiguous aliases require the complete name from details=true. Truncation reports the reached provider limit. Narrow the window or increase that provider limit before reading again. + +Use app methods directly for single-window tasks; getApp already reads initial AX state. Call app.getWindows() and await app.getWindow(windowId) only to target a particular window or resolve ambiguous window selection. App screenshots and coordinate actions use the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not refresh focus automatically. Read app.getAXState() to follow changed focus. + +Object API signatures (options default to { emit: true }): + +- cua.getCapabilities(options?) -> { platform, permissions, sessionLocked, methods, coordinateSpace, browser } +- cua.listApps(options?) -> Array<{ id, displayName, isRunning: true, pid }> +- cua.findApps(query: string, options?) -> Array<{ id, displayName, path, isRunning, pid? }> +- cua.getState(options?) -> { apps, browsers: [] } +- cua.getApp(selector: string) -> App +- app.activate() -> { dispatched: true, verified: false } +- app.getWindows(options?) -> Array<{ windowId, pid, appName, title, bounds, isOnScreen, onCurrentSpace }> +- app.getWindow(windowId: number) -> Window +- app.typeText(text: string), app.pressKey(chord: string) -> action result +- app.paste(text: string, options?: { format?: "text" | "html" }) -> action result + +App and Window methods: + +- show() -> { dispatched: true, verified: false, windowId: number | null, foregroundChanged: boolean | null } +- getAXState(options?: { emit?: boolean, disableDiffing?: boolean, includeMenus?: boolean, details?: boolean }) -> string +- getScreenshot(options?: { emit?: boolean }) -> Uint8Array +- getAXStateAndScreenshot(options?) -> { state: string, screenshot: Uint8Array } +- click(target: number | [x, y], options?: { mouseButton?: "left" | "right" | "middle", clickCount?: 1 | 2, modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result +- setValue(elementIndex: number, value: string) -> action result +- selectText(elementIndex: number, text: string, options?: { prefix?: string, suffix?: string, selectionType?: "text" | "cursor_before" | "cursor_after" }) -> action result +- performSecondaryAction(elementIndex: number, action: string) -> action result +- scroll({ x, y, deltaLinesX, deltaLinesY }) -> action result +- drag(from: [x, y], to: [x, y], options?: { mouseButton?: "left" | "right" | "middle", modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result + +Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input. + +Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Coordinate clicks, scrolling and dragging require a successful AX observation or screenshot of that window. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input. + +Use selectText only on controls advertising selectionWritable. It selects a unique literal occurrence, or places the cursor before/after it; prefix and suffix must be immediately adjacent. AMBIGUOUS_TEXT requires more context; TEXT_NOT_FOUND or SELECTION_CHANGED requires a fresh observation before deciding on input. Left clicks support single/double clicks; right and middle clicks support single clicks. Modified clicks use pointer input. Keypad names such as KP_0 and KP_Enter retain their physical key identity. Window objects expose observation and pointer/AX actions; keyboard input remains app-scoped and follows current focus. Scroll deltas are integer lines from -10 to 10; positive values reveal content above or left. Paste delivers plain text or HTML through the application's enabled standard Paste menu command. Original clipboard formats are restored if the clipboard still belongs to this paste; a later copy is preserved. Unsupported Paste commands fail without typing fallback. CLIPBOARD_CHANGED means another writer intervened; observe before deciding on further input. Browser applications can be controlled with getApp like other native apps; browser tabs are unavailable. Installed-app search covers standard macOS application directories; use an absolute application path for another location. Launch does not restore a window or document, and cancellation may leave an application already started. The execution host must install the object API and own variable persistence; this plugin does not preload a cua global. In persistent PTC, use the host-provided initialization snippet below once. Its installation-owned file URL works independently of the project directory. Retain app objects between calls. Observation methods emit by default and also return their value. Use void await app.getAXState() to avoid a duplicate REPL completion, or pass emit:false and print the returned text once. End screenshot calls with void await app.getScreenshot() or void await app.getAXStateAndScreenshot() so image bytes are not serialized as the program completion value. +``` + +#### Token effect + +Enabling the plugin adds a fixed schema and guidance. AX observations, operation results and admitted images add model input. Retained objects are not serialized into prompts by this package. + +#### KV Cache effect + +Tool results append to Session history. Changing plugin configuration or tool presentation can change the request prefix; retaining an app object does not change existing history. + +## Known Limitations and Deferred Work + +The queue orders individual native requests. Holding an app object does not reserve its application across a Session; application-level ownership spanning several requests remains separate work. Capture RPCs that never return occupy the single capture slot; creating new app objects does not reset that system connection. + + + +- Only the macOS native implementation is provided. Windows support is in development; this package remains in `experimental` until that support is complete, then moves to the `packages/computer-use/cua-macos` directory. Browser tabs, AX diffs, unassociated system panels and accessory-only apps are unsupported. +- Coordinates use window-local points including the title bar. PNG pixel dimensions can differ. Window movement, application changes and other desktop users can race observations and input. +- macOS Stage Manager can reduce background window bounds and screenshots to thumbnails. Full-window observation and coordinate input in this state are not guaranteed; model guidance requires explaining foreground needs and avoiding repeated focus reclamation. +- Background input depends on private SkyLight symbols; macOS updates can make them unavailable. Validate each target OS version. +- Observed failures include dispatched Unicode text not appearing in Blender and background Command+A not selecting all text in AppKit. Inspect application state after input; a successful tool result confirms dispatch only. +- Native binaries require a local build. Signing, distribution, and system-permission onboarding remain deferred. +- Persistent PTC is an external dependency assumption, not an implementation in this package. Real-model task success requires separate evaluation beyond native and protocol fixtures. + +----- + + +### Dev Note + +
+Working context for maintainers + +None. + +
diff --git a/packages/experimental/cua-macos/README.zh.md b/packages/experimental/cua-macos/README.zh.md new file mode 100644 index 0000000000..0ddc097c19 --- /dev/null +++ b/packages/experimental/cua-macos/README.zh.md @@ -0,0 +1,218 @@ +--- +description: "通过按应用和窗口限定的对象,观察和操作原生 macOS 应用。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-experimental-cua-macos + +[English](README.md) | 中文 + +## 概述 + +检查 macOS 权限,选择运行中的应用和窗口,读取辅助功能树、截图并发送输入。外部持久执行环境可以保留这些对象。Swift 辅助进程直接使用 macOS 框架,并改写选取的 MIT 许可 Cua Driver 行为。本包不提供 PTC 运行时或变量持久化。 + +## 目录 + +- [使用本包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步阅读](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与后续工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +该实验性提供方需要在 macOS 14 及以上通过源码配置显式启用。Windows 支持正在开发中。默认配置不加载它。先准备原生组件,再加载注册表和提供方覆盖层: + +```sh +swift build --package-path packages/experimental/cua-macos/native --configuration release +cua_native_root="$PWD/packages/experimental/cua-macos/native" +export DSH_CUA_HELPER="$cua_native_root/.build/release/dsh-cua-native" +pnpm dsh headless --patch packages/experimental/cua-macos/examples/macos.cordis.yml --dump-config +``` + +配置输出命令不发起模型请求。执行真实任务时,将 `--dump-config` 替换为任务提示,并配置该 profile 的模型凭据。配置挂载 computer-use 注册表和本 provider;使用前需移除已有的竞争 provider。负责执行的宿主或辅助进程需要辅助功能权限来读取 AX 和输入,需要屏幕录制权限来截图。`getCapabilities` 查询权限和锁屏状态,不弹出系统授权窗口。桌面会话锁定或不可用时,界面观察与输入会在分派前拒绝,需由用户解锁 macOS 后继续。`listApps` 和 `cua.getState` 列出运行中的应用。`findApps(query)` 按名称、bundle ID 或绝对路径搜索 macOS 标准应用目录。`getApp` 按唯一名称、bundle ID、路径或运行中的 PID 选择应用,取得应用许可后,在需要时后台启动尚未运行的应用。启动前先确认可执行文件身份,授权后在启动前后再次核对;未签名代码只取得任务许可。返回的对象读取初始 AX,不自动激活应用。启动被取消时,应用可能已经运行。模型指引要求先在后台观察和输入,不在初始化或每次动作前激活应用。窗口隐藏或最小化时,先尝试 `app.show()` 或 `window.show()`,再重新观察。它请求取消隐藏和恢复最小化,不主动激活、抬起窗口或切换桌面空间;应用级恢复选择焦点窗口、主窗口或唯一 AX 窗口。结果通过 `windowId` 指明选中的窗口,未选中时为 null;`foregroundChanged` 比较操作前后的前台应用和最上层窗口,无法取得完整观察时为 null,返回后仍可能发生焦点变化。恢复失败或窗口仍不可用时,说明需要前台的步骤,再调用 `app.activate()` 或窗口的 `raise` 动作并重新观察。必须使用前台时,以完成任务为先。应用收到输入后仍可能改变焦点,后台投递不保证用户操作始终不受干扰。 + +| 字段 | 默认值 | 含义 | +| --- | --- | --- | +| `allowedApps` | `[]` | 当前配置档的动态应用授权,绑定可执行文件路径和已验证的代码签名要求。 | +| `helperPath` | 包内可执行文件 | 可选的原生可执行文件绝对路径覆盖。 | +| `buildTimeoutMs` | `300000` | 原生组件准备的最长时间。 | +| `timeoutMs` | `30000` | 单次操作截止时间,取消后可能已发生部分输入。 | +| `connectTimeoutMs` | `5000` | 连接或启动共享 helper 的截止时间。 | +| `screenshotTimeoutMs` | `5000` | 截图总期限,包含排队时间,且不超过 `timeoutMs`。 | +| `axTimeoutMs` | `3000` | 每次 Accessibility 系统调用的截止时间,与完整操作截止独立配置。 | +| `maxRequestBytes` / `maxResponseBytes` | `1048576` / `33554432` | UTF-8 请求与响应字节限制。 | +| `maxClipboardBytes` | `33554432` | 粘贴时保存原剪贴板内容的字节上限;超限会在替换前拒绝。 | +| `maxAppMatches` | `20` | 已安装应用搜索的结果上限;范围过大的查询会被拒绝,需要缩小名称范围。 | +| `maxElements` / `maxDepth` | `2000` / `25` | 辅助功能树遍历限制。 | +| `dragDurationMs` / `dragSteps` | `400` / `20` | 拖动时长和移动分段数。 | +| `textEventIntervalMs` | `0.25` | 每个 Unicode 按下或抬起事件后的间隔,范围为 0 至 1000 毫秒。组合键的节奏独立。 | + +[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-cua-macos)列出全部字段。原生工具调用使用 `{method, params}`,模型可以看到每个方法的完整参数定义。例如,`getApp` 返回 `listWindows`、`getState` 和输入操作所需的 `appHandle`。 + +数字键盘名称(`KP_0` 到 `KP_9`、`KP_Enter` 和数字键盘运算键)保留独立的物理键码。组合键用 `+` 分隔修饰键与按键。加号键写作 `+`,Command 加号写作 `cmd++`,也可以使用键名 `plus`;`cmd+` 这类缺少按键的输入会被拒绝。 + +Agent 销毁和插件卸载会取消并等待完整工具调用,包括图像接入检查和附件存储。已经开始的图像写入必须结束后,销毁才会返回;已取消的调用不会返回成功截图。 + +CUA 工具在读取 AX、窗口标题、截图、激活和输入之前检查应用授权。User Questions 提供任务级允许和拒绝选项;应用签名可验证且组合支持配置持久化时,也可保存授权。任务授权绑定所选进程句柄,长期授权绑定真实可执行路径和指定代码签名要求。拒绝后,同一任务不再反复询问。删除保存的 `allowedApps` 条目会取消相关操作并使原生组件中的句柄失效;重新授权需要重新选择应用。同一 Agent 连接中的其他句柄也会失效,已保存的授权仍可复用。Agent 或提供方销毁时等待对应连接的原生状态清理。设计理由见[应用授权决策](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.zh.md)。这些检查约束 CUA 调用;持久 PTC 和另行授权的 shell 工具保留各自的执行权限。 + +原始 `getState` 操作返回经过校验的状态字段,以及按本次披露选项生成的完整文本。原生 helper 还返回供原生检查使用的结构化元素诊断数组;工具投影会从模型文本和结构化工具值中移除这份重复数组。 + +AX 观察默认只展示菜单栏入口。使用 `app.getAXState({ includeMenus: true })` 展开菜单内容,使用 `app.getAXState({ details: true })` 读取标识符、全局 point 坐标和完整动作名。原始 `getState` 和 AX/截图组合观察也接受这两个选项。每次观察都会替换该范围的元素编号,展开后应使用新输出中的编号。详情不会绕过配置的节点数和深度上限;截断提示会指明触发的上限。 + +实际值与占位文字分别展示。有名称、状态或可用动作的分组和滚动区域保留独立行。同一节点中重复的名称、描述和帮助只显示一次。实际值、占位文字和 URL 即使文字相同也保留各自含义,不删除不同节点共有的正文。相关控件明确展示可写性,包括 `AXIncrementor`;`unknown` 表示系统未报告。缺少可选状态字段不代表 false。超出当前值长度的 UTF-16 选区标为不可用。自定义动作简称省略 AppKit 的 target/selector 诊断信息;同名动作需要使用 `details:true` 返回的完整名称。 + +观察方法默认自动输出,同时返回结果。在 persistent PTC 中使用 `void await app.getAXState()` 输出一次,或指定 `emit:false` 后自行打印一次。AX 输出后的截图可能单独失败,已成功取得的 AX 输出仍保留。 + +截图诊断使用带工具请求 ID 的 `[cua-screenshot]` 记录。原生阶段记录经本次 Agent 连接返回,覆盖窗口及 AX 几何检查、系统截图、PNG/Base64 编码和响应写出。Host 调试日志补充发送、响应和清理耗时。每次请求最多保留 8192 字节完整诊断记录,超限会明确标记;截图失败时附在错误中,保留到 Session trace。记录只含阶段、耗时和错误码,不含窗口标题、AX 正文或图片内容。 + +原生准备复用文件锁,串行检查编译和发布。构建更新包内产物,并从 native/.build/prepared 下按内容摘要区分的副本运行,保留旧源码进程使用的可执行文件。相同构建复用同一文件;保留的副本损坏时拒绝使用。取消或准备失败后,先完成受管进程清理再释放锁。 + +### 接入外部执行环境 + +使用 persistent PTC 时,在 macOS 配置后追加 `--patch packages/experimental/cua-macos/examples/persistent-ptc.cordis.yml`。该 provider 为存活的 Agent 保留变量,并遵守当前选择的文件沙箱策略。调用方导入适配模块,并提供当前执行中已授权的 `tools.cua` 绑定。persistent PTC 会在 CUA 指引中收到安装目录内 bridge 的文件 URL,项目无须安装本包。下面的例子用于能从自身安装目录解析本包、且已提供 `tools` 的外部宿主;本包不安装全局对象,也不负责保留变量: + +```javascript +import { connectCua } from '@deepseek-ai/dsh-cua/bridge' +const cua = connectCua(() => tools.cua, text => console.log(text)) +const app = await cua.getApp("com.apple.TextEdit") +void await app.getScreenshot() +``` + +单窗口任务直接使用 app 方法。`getApp` 已读取初始 AX,截图不需要枚举窗口或额外读取 AX。任务需要指定窗口或默认选窗存在歧义时,再调用 `app.getWindows()` 和 `app.getWindow(windowId)`,根据观察到的标题与位置选择目标。 + +`getWindow` 会输出初始 AX 树。已确认归属但没有 AX 界面的窗口返回空快照,仍可截图和发送坐标输入。不同窗口分别保留快照,读取一个窗口不会使另一个窗口失效;重新读取同一范围会使旧索引失效。观察期间节点被移除时,该次读取返回 `STALE_SNAPSHOT`,不发布部分树;可以重新读取,但不要重复此前的输入。应用级截图和坐标操作复用最近一次 AX 观察:优先使用其中在屏幕上且尺寸非零的焦点窗口,否则要求只有一个符合条件的窗口。它们不会重新查询即时焦点;需要跟随焦点变化时,先读取 `app.getAXState()`。显式窗口对象保持选定的目标。键盘方法保留在 `app` 上,因为输入跟随应用的当前焦点。 + +`paste(text, { format: "text" | "html" })` 临时提供指定格式的剪贴板内容,并调用应用已启用的标准 Paste 菜单命令。临时格式保留到剪贴板读取方请求其数据,然后只在仍持有剪贴板时恢复保存的全部原格式,保留期间产生的新复制内容。粘贴操作共用跨进程锁,取消会等待辅助进程清理。强制终止进程可能阻止恢复。应用不支持 Paste 命令时明确失败,不自动改成逐字输入;`typeText` 仍单独提供按键事件输入。 + +`selectText` 通过可写的 `AXSelectedTextRange` 选中唯一的字面文本,或把光标放在它的前后。紧邻的前后文用于区分重复文本。选区操作保留其余文字和格式,并核对实际 UTF-16 范围;控件不支持、文本不存在、匹配不唯一或文字变化时明确失败,不自动改用键盘输入。AX 文本包含可用的 `selectionWritable`、选区范围和选中文字。 + +截图数据必须使用规范 Base64。响应字节上限在解析前执行,附件接入校验图像数据及配置的容量。 + +坐标点击、滚动和拖动要求先成功读取该窗口的 AX 或截图。窗口宽高与最近一次观察不同时,在输入前返回 `WINDOW_CHANGED`,原始调用即使不传快照 ID 也会检查;重新读取 AX 或截图后再操作。单纯移动窗口不使窗口内坐标失效。截图和鼠标输入会核对所选窗口的辅助功能框与显示边界;已知两者不一致时,在发送任何鼠标事件前返回 `WINDOW_CHANGED`。当 AX 保留完整窗口框时,这也会拦住 Stage Manager 变换后的窗口。需要重新读取状态;激活应用或抬起窗口仍须显式调用。元素声明的 AX 动作保持可用。缺少 AX 几何时保留无 AX 路径,不代表变换后的窗口一定可以操作。 + +保留的对象会在每次调用时获取当前工具绑定。外部运行时负责持久化、导入、取消和最终表达式值的序列化。截图返回 PNG 字节,标准工具链负责将图片作为附件传递一次。`emit: false` 只控制对象层的输出。使用其他分派器的宿主可以通过 `./api` 的 `createCuaApi` 创建对象。 + +AppKit sheet 可以通过其窗口 ID 单独选择,也会出现在所属文档窗口的控件树中。只有应用当前获得焦点的 sheet 指向唯一的 Apple 面板服务窗口,且该服务仍聚焦于同一面板时,键盘输入才转交系统打开或保存面板。无法确认或存在歧义的远端接收方会在输入前报错。 + +元素索引支持右键或中键单击以及左键单击、双击。点击和拖动选项可指定按键与修饰键;带修饰键的点击使用指针输入。拖动期间保持指定按键,并在移动和释放事件中携带修饰键。AX 文本标明禁用控件、`valueWritable=true` 和可读取的 `selected` 状态。不带修饰键的点击优先使用元素声明的辅助功能动作;需要发送鼠标事件时,会重新读取元素位置并核实所属窗口。动作返回只表示输入已分派。可视指针或点击标记可以独立绘制;AX 动作无需移动系统指针。窗口定向鼠标输入仍可能改变应用焦点、选区或对话框。输入后需要读取状态确认结果。`STALE_APP` 需要重新获取应用,`STALE_SNAPSHOT` 需要重新读取 AX。请求超时或取消会等待原生停止确认,保留仍可用的句柄;无法确认清理完成时关闭该 Agent 的连接。未返回系统回调的截图 RPC 占用唯一捕获槽位,新截图立即返回 `CAPTURE_BUSY`,AX 和输入仍可使用。迟到图片被丢弃,真实回调结束后释放槽位。只有操作无法停止或清理失败时,才可能使共享进程及所有旧句柄失效。不能自动重试结果不确定的输入。关闭 Agent 会释放其连接和 AX 引用,但不会撤销应用中的修改。 + +----- + + +## 理解实现 + +
+操作分派与资源归属 + +对象接口将应用和窗口方法转换为 JSON 操作。适配模块通过标准工具链保留授权、嵌套日志和图片接入。同一用户的相同 helper 构建由多个 Agent 和 Host 共享。每条连接独立保存句柄、快照、配置和取消状态,桌面操作通过全局队列执行。受管启动器将服务生命期交给这些连接,最后一条连接清理完成后服务退出。进程锁覆盖构建身份及真实可执行路径,防止重复启动或覆盖运行版本后产生路径冲突。原生句柄绑定进程身份及内核启动时间,快照中的 AX 引用只保存在辅助进程内。未来平台可以实现相同操作协议,无需在对象接口中加入平台逻辑。 + +[Zod 操作定义](../cua/src/protocol.ts)负责模型参数,并单独声明 Host 资源字段和内部续接操作。公共数据类型从这些定义推导。`pnpm exec tsx packages/experimental/cua-macos/native/generate-protocol.ts` 生成原生 JSON Schema 和 Swift 字段、枚举校验表;包测试检查产物是否同步,并验证模型不能提交 Host 专属字段。原生目标验证、资源上限和操作行为仍在执行处检查。 + +[AX 文本格式化](native/Sources/CuaNative/AXTextFormatter.swift)只接收已读取的节点数据。AX 遍历器独自分配索引并保存原生引用,格式化不维护另一套快照或目标注册表。 + +原生请求 ID 与工具日志中的调用 ID 一致。成功的激活和窗口抬起操作会在原始工具结果中保留前台操作信息,以及操作前后的即时焦点观察。应用已在前台时跳过重复激活;窗口抬起还要求该窗口正是焦点主窗口、未最小化且位于可见普通窗口的最前方,才会跳过。无法确认时保留原请求。这些观察不能确认其他焦点切换的发起者,也不代表操作已经完成。 + +每次观察在同一批 AX 请求中读取节点角色和元数据。组节点和滚动区域节点只提供子节点,未使用的元数据错误不会导致它们被拒绝。不支持批量读取时保留逐个属性读取。AX 遍历对每个原生身份只访问一次,包括表示同一元素的不同代理引用,避免循环子节点重复展开整个子树。[源码清单](native/upstream.json)记录改写的上游行为和本地 Swift 模块。AppKit 使用主事件循环,独立串行执行器持有 AX 引用并发送同步输入。一条请求在截图和结果发送期间仍独占执行。本包不发布 invariant 入口,因为没有需要对照的独立注册表投影或持久执行器状态。协议验证、目标身份和资源清理由对应实现负责。 + +
+ +----- + + +## 进一步阅读 + +- [Computer-use 注册](../../computer-use/computer-use/README.zh.md) +- [原生协议](native/protocol.schema.json) +- [对象接口](../cua/src/types.ts) + +----- + + +## 模型体验 + +### 原生应用操作 + +#### 模型可见内容 + +插件提供一个 `cua` 操作工具,定义各方法的参数。标准 PTC 呈现通过 `run_code` 将它暴露为 `tools.cua`。以下是本包逐字输出的指导文本: + +##### 操作指导文本(原文) + +```markdown +Use cua with method getCapabilities and params {} to inspect permissions and sessionLocked. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI attempts and ask the user to unlock macOS. A null sessionLocked means no usable console session was verified. After unlocking, read fresh state; screenshots from a locked desktop are not current observations. Use listApps or cua.getState to discover running applications, and findApps(query) to search installed applications by name, bundle ID or absolute path. getApp requests user consent before launching a stopped application in the background, or before observations and input on a running one. If access is denied or revoked, stop using that application until the user explicitly grants access again. Never switch tools to bypass a refusal. Keep the returned appHandle; every app operation requires it. listWindows returns window IDs and point bounds. Raw getState returns AX text and snapshotId. Menu-bar entries are shown by default; includeMenus=true expands their contents. details=true adds identifiers, global-point bounds and complete action names. Both options also apply to getAXState. Each read replaces the indices for that app/window scope. Use its element indices only with that snapshotId. Raw screenshot and coordinate operations require a windowId. Coordinates are window-local macOS points, which may differ from screenshot pixels. A verified window can expose no AX elements. Its empty snapshot permits screenshots and coordinate input; element-index actions require an element from a fresh AX observation. + +Initialize the object API once in persistent PTC. Reuse its variables in later calls; put temporary declarations inside { ... } blocks to avoid redeclaration errors. All object methods are async. getAXState returns text, not a structured state object. Its text separates values and placeholders, and reports subroles, help, URLs, disabled/focused/selected/expanded state and UTF-16 selection ranges when available. Writability is true, false or unknown on relevant controls; omitted state fields do not assert false. selection=unavailable means the reported range exceeds the observed text. Compact output omits ordinary Press/Pick (click), ShowMenu (right click) and ScrollToVisible actions; details=true lists every available action. Use the displayed action name with performSecondaryAction; ambiguous aliases require the complete name from details=true. Truncation reports the reached provider limit. Narrow the window or increase that provider limit before reading again. + +Use app methods directly for single-window tasks; getApp already reads initial AX state. Call app.getWindows() and await app.getWindow(windowId) only to target a particular window or resolve ambiguous window selection. App screenshots and coordinate actions use the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not refresh focus automatically. Read app.getAXState() to follow changed focus. + +Object API signatures (options default to { emit: true }): + +- cua.getCapabilities(options?) -> { platform, permissions, sessionLocked, methods, coordinateSpace, browser } +- cua.listApps(options?) -> Array<{ id, displayName, isRunning: true, pid }> +- cua.findApps(query: string, options?) -> Array<{ id, displayName, path, isRunning, pid? }> +- cua.getState(options?) -> { apps, browsers: [] } +- cua.getApp(selector: string) -> App +- app.activate() -> { dispatched: true, verified: false } +- app.getWindows(options?) -> Array<{ windowId, pid, appName, title, bounds, isOnScreen, onCurrentSpace }> +- app.getWindow(windowId: number) -> Window +- app.typeText(text: string), app.pressKey(chord: string) -> action result +- app.paste(text: string, options?: { format?: "text" | "html" }) -> action result + +App and Window methods: + +- show() -> { dispatched: true, verified: false, windowId: number | null, foregroundChanged: boolean | null } +- getAXState(options?: { emit?: boolean, disableDiffing?: boolean, includeMenus?: boolean, details?: boolean }) -> string +- getScreenshot(options?: { emit?: boolean }) -> Uint8Array +- getAXStateAndScreenshot(options?) -> { state: string, screenshot: Uint8Array } +- click(target: number | [x, y], options?: { mouseButton?: "left" | "right" | "middle", clickCount?: 1 | 2, modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result +- setValue(elementIndex: number, value: string) -> action result +- selectText(elementIndex: number, text: string, options?: { prefix?: string, suffix?: string, selectionType?: "text" | "cursor_before" | "cursor_after" }) -> action result +- performSecondaryAction(elementIndex: number, action: string) -> action result +- scroll({ x, y, deltaLinesX, deltaLinesY }) -> action result +- drag(from: [x, y], to: [x, y], options?: { mouseButton?: "left" | "right" | "middle", modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result + +Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input. + +Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Coordinate clicks, scrolling and dragging require a successful AX observation or screenshot of that window. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input. + +Use selectText only on controls advertising selectionWritable. It selects a unique literal occurrence, or places the cursor before/after it; prefix and suffix must be immediately adjacent. AMBIGUOUS_TEXT requires more context; TEXT_NOT_FOUND or SELECTION_CHANGED requires a fresh observation before deciding on input. Left clicks support single/double clicks; right and middle clicks support single clicks. Modified clicks use pointer input. Keypad names such as KP_0 and KP_Enter retain their physical key identity. Window objects expose observation and pointer/AX actions; keyboard input remains app-scoped and follows current focus. Scroll deltas are integer lines from -10 to 10; positive values reveal content above or left. Paste delivers plain text or HTML through the application's enabled standard Paste menu command. Original clipboard formats are restored if the clipboard still belongs to this paste; a later copy is preserved. Unsupported Paste commands fail without typing fallback. CLIPBOARD_CHANGED means another writer intervened; observe before deciding on further input. Browser applications can be controlled with getApp like other native apps; browser tabs are unavailable. Installed-app search covers standard macOS application directories; use an absolute application path for another location. Launch does not restore a window or document, and cancellation may leave an application already started. The execution host must install the object API and own variable persistence; this plugin does not preload a cua global. In persistent PTC, use the host-provided initialization snippet below once. Its installation-owned file URL works independently of the project directory. Retain app objects between calls. Observation methods emit by default and also return their value. Use void await app.getAXState() to avoid a duplicate REPL completion, or pass emit:false and print the returned text once. End screenshot calls with void await app.getScreenshot() or void await app.getAXStateAndScreenshot() so image bytes are not serialized as the program completion value. +``` + +#### Token 影响 + +启用插件会增加固定参数定义和指导文本。AX 观察、操作结果和已接入的图片会增加模型输入。本包不将保留的对象序列化进提示。 + +#### KV Cache 影响 + +工具结果追加到 Session 历史。修改插件配置或工具呈现方式可能改变请求前缀;保留应用对象不会改变已有历史。 + +## 已知限制与后续工作 + +队列只排序单次原生请求。持有 app 对象不会为 Session 保留应用,跨多个请求的应用级占用需要单独设计。一直不返回的系统截图 RPC 会占用唯一捕获槽位,重新创建 app 对象不会重置该系统连接。 + + + +- 只提供 macOS 原生实现,Windows 支持正在开发中。本包暂放在 `experimental`,待 Windows 支持完成后迁回 `packages/computer-use/cua-macos` 目录。不支持浏览器标签、启动应用、AX 差分、无法确认应用归属的系统面板,以及仅后台辅助类型的应用。 +- 坐标使用包含标题栏的窗口内部 point,可能不同于 PNG 像素尺寸。窗口移动、应用变化及其他桌面使用者的操作可能与观察和输入交错。 +- macOS 台前调度可能将后台窗口的尺寸与截图变为缩略图,当前无法保证这种状态下的完整画面与坐标输入;模型指引要求说明前台需求并避免反复抢回焦点。 +- 后台输入依赖私有 SkyLight 符号,macOS 升级可能使其不可用,需在目标系统版本验证。 +- 实测 Blender Unicode 输入曾出现分派成功但界面未显示文本,后台 AppKit 的 Command+A 曾未选中全文。输入后必须检查应用状态;工具返回成功只表示已分派。 +- 原生二进制需要本地构建。签名、分发和系统权限引导留待后续。 +- 持久 PTC 是对外部运行环境的假设,本包不实现它。真实模型任务成功率需要在原生测试和协议测试之外单独评估。 + +----- + + +### 开发备注 + +
+维护者工作背景 + +无。 + +
diff --git a/packages/experimental/cua/examples/macos.cordis.yml b/packages/experimental/cua-macos/examples/macos.cordis.yml similarity index 71% rename from packages/experimental/cua/examples/macos.cordis.yml rename to packages/experimental/cua-macos/examples/macos.cordis.yml index af6b19ce4c..94942836e0 100644 --- a/packages/experimental/cua/examples/macos.cordis.yml +++ b/packages/experimental/cua-macos/examples/macos.cordis.yml @@ -1,6 +1,6 @@ - insert: - id: cua-provider-registry - name: '../../computer-use/src/index.ts' + name: '../../../computer-use/computer-use/src/index.ts' - id: cua name: '../src/index.ts' config: diff --git a/packages/experimental/cua/examples/persistent-ptc.cordis.yml b/packages/experimental/cua-macos/examples/persistent-ptc.cordis.yml similarity index 100% rename from packages/experimental/cua/examples/persistent-ptc.cordis.yml rename to packages/experimental/cua-macos/examples/persistent-ptc.cordis.yml diff --git a/packages/experimental/cua/native/.gitignore b/packages/experimental/cua-macos/native/.gitignore similarity index 100% rename from packages/experimental/cua/native/.gitignore rename to packages/experimental/cua-macos/native/.gitignore diff --git a/packages/experimental/cua/native/LICENSE.cua-driver b/packages/experimental/cua-macos/native/LICENSE.cua-driver similarity index 100% rename from packages/experimental/cua/native/LICENSE.cua-driver rename to packages/experimental/cua-macos/native/LICENSE.cua-driver diff --git a/packages/experimental/cua/native/Package.swift b/packages/experimental/cua-macos/native/Package.swift similarity index 100% rename from packages/experimental/cua/native/Package.swift rename to packages/experimental/cua-macos/native/Package.swift diff --git a/packages/experimental/cua/native/README.txt b/packages/experimental/cua-macos/native/README.txt similarity index 100% rename from packages/experimental/cua/native/README.txt rename to packages/experimental/cua-macos/native/README.txt diff --git a/packages/experimental/cua/native/Sources/CuaNative/AXAccess.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/AXAccess.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/AXAccess.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/AXAccess.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/AXTextFormatter.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/AXTextFormatter.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/AXTextFormatter.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/AXTextFormatter.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/AXWorker.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/AXWorker.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/AXWorker.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/AXWorker.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/ApplicationAuthorization.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/ApplicationAuthorization.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/ApplicationAuthorization.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/ApplicationAuthorization.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/ApplicationDiscovery.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/ApplicationDiscovery.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/ApplicationDiscovery.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/ApplicationDiscovery.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/ClipboardPaste.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/ClipboardPaste.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/ClipboardPaste.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/ClipboardPaste.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/KeyboardInput.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/KeyboardInput.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/KeyboardInput.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/KeyboardInput.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/Main.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/Main.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/Main.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/Main.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/NativePlatform.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/NativePlatform.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/NativePlatform.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/NativePlatform.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/NativeProtocol.generated.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/NativeProtocol.generated.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/NativeProtocol.generated.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/NativeProtocol.generated.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/PIDEventPoster.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/PIDEventPoster.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/PIDEventPoster.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/PIDEventPoster.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/Protocol.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/Protocol.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/Protocol.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/Protocol.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/RequestAdmission.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/RequestAdmission.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/RequestAdmission.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/RequestAdmission.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/RequestCoordinator.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/RequestCoordinator.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/RequestCoordinator.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/RequestCoordinator.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/RequestScheduler.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/RequestScheduler.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/RequestScheduler.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/RequestScheduler.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/Runtime.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/Runtime.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/Runtime.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/Runtime.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/ScreenshotTrace.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/ScreenshotTrace.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/ScreenshotTrace.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/ScreenshotTrace.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/SharedHelperProxy.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperProxy.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/SharedHelperProxy.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperProxy.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/SharedHelperRuntime.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperRuntime.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/SharedHelperRuntime.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperRuntime.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/SharedHelperServer.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperServer.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/SharedHelperServer.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperServer.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/TextSelection.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/TextSelection.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/TextSelection.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/TextSelection.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/WindowCapture.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/WindowCapture.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/WindowCapture.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/WindowCapture.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/WindowGeometry.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/WindowGeometry.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/WindowGeometry.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/WindowGeometry.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/WindowPointerInput.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/WindowPointerInput.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/WindowPointerInput.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/WindowPointerInput.swift diff --git a/packages/experimental/cua/native/Sources/CuaNative/WindowSpaces.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/WindowSpaces.swift similarity index 100% rename from packages/experimental/cua/native/Sources/CuaNative/WindowSpaces.swift rename to packages/experimental/cua-macos/native/Sources/CuaNative/WindowSpaces.swift diff --git a/packages/experimental/cua/native/generate-protocol.ts b/packages/experimental/cua-macos/native/generate-protocol.ts similarity index 99% rename from packages/experimental/cua/native/generate-protocol.ts rename to packages/experimental/cua-macos/native/generate-protocol.ts index 0b4a71a1b9..cc6e9628fc 100644 --- a/packages/experimental/cua/native/generate-protocol.ts +++ b/packages/experimental/cua-macos/native/generate-protocol.ts @@ -2,7 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises' import { z } from 'zod' import type { JSONSchema } from 'zod/v4/core' -import { NativeOnlyOperations, NativeResourceParameters, Request } from '../src/protocol.ts' +import { NativeOnlyOperations, NativeResourceParameters, Request } from '@deepseek-ai/dsh-cua/protocol' type Schema = JSONSchema.JSONSchema diff --git a/packages/experimental/cua/native/protocol.schema.json b/packages/experimental/cua-macos/native/protocol.schema.json similarity index 100% rename from packages/experimental/cua/native/protocol.schema.json rename to packages/experimental/cua-macos/native/protocol.schema.json diff --git a/packages/experimental/cua/native/tests/AxlessFixture.swift b/packages/experimental/cua-macos/native/tests/AxlessFixture.swift similarity index 100% rename from packages/experimental/cua/native/tests/AxlessFixture.swift rename to packages/experimental/cua-macos/native/tests/AxlessFixture.swift diff --git a/packages/experimental/cua/native/tests/CapabilitiesFixture.swift b/packages/experimental/cua-macos/native/tests/CapabilitiesFixture.swift similarity index 100% rename from packages/experimental/cua/native/tests/CapabilitiesFixture.swift rename to packages/experimental/cua-macos/native/tests/CapabilitiesFixture.swift diff --git a/packages/experimental/cua/native/tests/Fixture.swift b/packages/experimental/cua-macos/native/tests/Fixture.swift similarity index 100% rename from packages/experimental/cua/native/tests/Fixture.swift rename to packages/experimental/cua-macos/native/tests/Fixture.swift diff --git a/packages/experimental/cua/native/tests/axless_e2e.py b/packages/experimental/cua-macos/native/tests/axless_e2e.py similarity index 100% rename from packages/experimental/cua/native/tests/axless_e2e.py rename to packages/experimental/cua-macos/native/tests/axless_e2e.py diff --git a/packages/experimental/cua/native/tests/blender_keyboard_e2e.py b/packages/experimental/cua-macos/native/tests/blender_keyboard_e2e.py similarity index 100% rename from packages/experimental/cua/native/tests/blender_keyboard_e2e.py rename to packages/experimental/cua-macos/native/tests/blender_keyboard_e2e.py diff --git a/packages/experimental/cua/native/tests/blender_keyboard_fixture.py b/packages/experimental/cua-macos/native/tests/blender_keyboard_fixture.py similarity index 100% rename from packages/experimental/cua/native/tests/blender_keyboard_fixture.py rename to packages/experimental/cua-macos/native/tests/blender_keyboard_fixture.py diff --git a/packages/experimental/cua/native/tests/capabilities_e2e.py b/packages/experimental/cua-macos/native/tests/capabilities_e2e.py similarity index 100% rename from packages/experimental/cua/native/tests/capabilities_e2e.py rename to packages/experimental/cua-macos/native/tests/capabilities_e2e.py diff --git a/packages/experimental/cua/native/tests/e2e.py b/packages/experimental/cua-macos/native/tests/e2e.py similarity index 100% rename from packages/experimental/cua/native/tests/e2e.py rename to packages/experimental/cua-macos/native/tests/e2e.py diff --git a/packages/experimental/cua/native/tests/electron-fixture.html b/packages/experimental/cua-macos/native/tests/electron-fixture.html similarity index 100% rename from packages/experimental/cua/native/tests/electron-fixture.html rename to packages/experimental/cua-macos/native/tests/electron-fixture.html diff --git a/packages/experimental/cua/native/tests/electron-fixture.mjs b/packages/experimental/cua-macos/native/tests/electron-fixture.mjs similarity index 100% rename from packages/experimental/cua/native/tests/electron-fixture.mjs rename to packages/experimental/cua-macos/native/tests/electron-fixture.mjs diff --git a/packages/experimental/cua/native/tests/electron_e2e.py b/packages/experimental/cua-macos/native/tests/electron_e2e.py similarity index 100% rename from packages/experimental/cua/native/tests/electron_e2e.py rename to packages/experimental/cua-macos/native/tests/electron_e2e.py diff --git a/packages/experimental/cua/native/tests/native_test_support.py b/packages/experimental/cua-macos/native/tests/native_test_support.py similarity index 100% rename from packages/experimental/cua/native/tests/native_test_support.py rename to packages/experimental/cua-macos/native/tests/native_test_support.py diff --git a/packages/experimental/cua/native/tests/observation_benchmark.py b/packages/experimental/cua-macos/native/tests/observation_benchmark.py similarity index 100% rename from packages/experimental/cua/native/tests/observation_benchmark.py rename to packages/experimental/cua-macos/native/tests/observation_benchmark.py diff --git a/packages/experimental/cua/native/tests/protocol_e2e.py b/packages/experimental/cua-macos/native/tests/protocol_e2e.py similarity index 100% rename from packages/experimental/cua/native/tests/protocol_e2e.py rename to packages/experimental/cua-macos/native/tests/protocol_e2e.py diff --git a/packages/experimental/cua/native/tests/shared_helper_e2e.py b/packages/experimental/cua-macos/native/tests/shared_helper_e2e.py similarity index 100% rename from packages/experimental/cua/native/tests/shared_helper_e2e.py rename to packages/experimental/cua-macos/native/tests/shared_helper_e2e.py diff --git a/packages/experimental/cua/native/tests/swift/AXFailureTests.swift b/packages/experimental/cua-macos/native/tests/swift/AXFailureTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/AXFailureTests.swift rename to packages/experimental/cua-macos/native/tests/swift/AXFailureTests.swift diff --git a/packages/experimental/cua/native/tests/swift/AXTextFormatterTests.swift b/packages/experimental/cua-macos/native/tests/swift/AXTextFormatterTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/AXTextFormatterTests.swift rename to packages/experimental/cua-macos/native/tests/swift/AXTextFormatterTests.swift diff --git a/packages/experimental/cua/native/tests/swift/ApplicationDiscoveryTests.swift b/packages/experimental/cua-macos/native/tests/swift/ApplicationDiscoveryTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/ApplicationDiscoveryTests.swift rename to packages/experimental/cua-macos/native/tests/swift/ApplicationDiscoveryTests.swift diff --git a/packages/experimental/cua/native/tests/swift/ClipboardPasteTests.swift b/packages/experimental/cua-macos/native/tests/swift/ClipboardPasteTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/ClipboardPasteTests.swift rename to packages/experimental/cua-macos/native/tests/swift/ClipboardPasteTests.swift diff --git a/packages/experimental/cua/native/tests/swift/ForegroundTests.swift b/packages/experimental/cua-macos/native/tests/swift/ForegroundTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/ForegroundTests.swift rename to packages/experimental/cua-macos/native/tests/swift/ForegroundTests.swift diff --git a/packages/experimental/cua/native/tests/swift/InputMappingTests.swift b/packages/experimental/cua-macos/native/tests/swift/InputMappingTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/InputMappingTests.swift rename to packages/experimental/cua-macos/native/tests/swift/InputMappingTests.swift diff --git a/packages/experimental/cua/native/tests/swift/PermissionIdentityTests.swift b/packages/experimental/cua-macos/native/tests/swift/PermissionIdentityTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/PermissionIdentityTests.swift rename to packages/experimental/cua-macos/native/tests/swift/PermissionIdentityTests.swift diff --git a/packages/experimental/cua/native/tests/swift/PointerTimingTests.swift b/packages/experimental/cua-macos/native/tests/swift/PointerTimingTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/PointerTimingTests.swift rename to packages/experimental/cua-macos/native/tests/swift/PointerTimingTests.swift diff --git a/packages/experimental/cua/native/tests/swift/ProtocolTests.swift b/packages/experimental/cua-macos/native/tests/swift/ProtocolTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/ProtocolTests.swift rename to packages/experimental/cua-macos/native/tests/swift/ProtocolTests.swift diff --git a/packages/experimental/cua/native/tests/swift/ScreenshotTraceTests.swift b/packages/experimental/cua-macos/native/tests/swift/ScreenshotTraceTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/ScreenshotTraceTests.swift rename to packages/experimental/cua-macos/native/tests/swift/ScreenshotTraceTests.swift diff --git a/packages/experimental/cua/native/tests/swift/SharedHelperTests.swift b/packages/experimental/cua-macos/native/tests/swift/SharedHelperTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/SharedHelperTests.swift rename to packages/experimental/cua-macos/native/tests/swift/SharedHelperTests.swift diff --git a/packages/experimental/cua/native/tests/swift/SharedSocketTests.swift b/packages/experimental/cua-macos/native/tests/swift/SharedSocketTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/SharedSocketTests.swift rename to packages/experimental/cua-macos/native/tests/swift/SharedSocketTests.swift diff --git a/packages/experimental/cua/native/tests/swift/TextSelectionTests.swift b/packages/experimental/cua-macos/native/tests/swift/TextSelectionTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/TextSelectionTests.swift rename to packages/experimental/cua-macos/native/tests/swift/TextSelectionTests.swift diff --git a/packages/experimental/cua/native/tests/swift/WindowCaptureTests.swift b/packages/experimental/cua-macos/native/tests/swift/WindowCaptureTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/WindowCaptureTests.swift rename to packages/experimental/cua-macos/native/tests/swift/WindowCaptureTests.swift diff --git a/packages/experimental/cua/native/tests/swift/WindowGeometryTests.swift b/packages/experimental/cua-macos/native/tests/swift/WindowGeometryTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/WindowGeometryTests.swift rename to packages/experimental/cua-macos/native/tests/swift/WindowGeometryTests.swift diff --git a/packages/experimental/cua/native/tests/swift/WindowSpacesTests.swift b/packages/experimental/cua-macos/native/tests/swift/WindowSpacesTests.swift similarity index 100% rename from packages/experimental/cua/native/tests/swift/WindowSpacesTests.swift rename to packages/experimental/cua-macos/native/tests/swift/WindowSpacesTests.swift diff --git a/packages/experimental/cua/native/tests/test_cleanup.py b/packages/experimental/cua-macos/native/tests/test_cleanup.py similarity index 100% rename from packages/experimental/cua/native/tests/test_cleanup.py rename to packages/experimental/cua-macos/native/tests/test_cleanup.py diff --git a/packages/experimental/cua/native/tests/test_swift.py b/packages/experimental/cua-macos/native/tests/test_swift.py similarity index 100% rename from packages/experimental/cua/native/tests/test_swift.py rename to packages/experimental/cua-macos/native/tests/test_swift.py diff --git a/packages/experimental/cua/native/tests/text_input_benchmark.py b/packages/experimental/cua-macos/native/tests/text_input_benchmark.py similarity index 100% rename from packages/experimental/cua/native/tests/text_input_benchmark.py rename to packages/experimental/cua-macos/native/tests/text_input_benchmark.py diff --git a/packages/experimental/cua/native/upstream.json b/packages/experimental/cua-macos/native/upstream.json similarity index 100% rename from packages/experimental/cua/native/upstream.json rename to packages/experimental/cua-macos/native/upstream.json diff --git a/packages/experimental/cua-macos/package.json b/packages/experimental/cua-macos/package.json new file mode 100644 index 0000000000..18533796bd --- /dev/null +++ b/packages/experimental/cua-macos/package.json @@ -0,0 +1,87 @@ +{ + "name": "@deepseek-ai/dsh-experimental-cua-macos", + "description": "macOS native provider for shared Computer Use application and window objects", + "version": "0.2.1-alpha.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/experimental/cua-macos" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "native/bin", + "native/Package.swift", + "native/Sources", + "native/tests/swift", + "native/tests/test_swift.py", + "native/LICENSE.cua-driver", + "native/upstream.json", + "native/README.txt", + "native/protocol.schema.json", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-computer-use": "workspace:*", + "@deepseek-ai/dsh-system-prompt": "workspace:*", + "@deepseek-ai/dsh-tools": "workspace:*", + "@deepseek-ai/dsh-agent": "workspace:*", + "@deepseek-ai/dsh-subprocess": "workspace:*", + "@deepseek-ai/dsh-user-questions": "workspace:*" + }, + "dependencies": { + "zod": "^4.4.3", + "@deepseek-ai/dsh-atomic-write": "workspace:*", + "@deepseek-ai/dsh-cua": "workspace:*" + }, + "devDependencies": { + "@deepseek-ai/dsh-working-directory": "workspace:*", + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/cordis-plugin-include": "workspace:~", + "@deepseek-ai/cordis-plugin-loader": "workspace:~", + "@deepseek-ai/dsh-agent": "workspace:*", + "@deepseek-ai/dsh-agent-loop": "workspace:*", + "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*", + "@deepseek-ai/dsh-attachment": "workspace:*", + "@deepseek-ai/dsh-attachment-local": "workspace:*", + "@deepseek-ai/dsh-computer-use": "workspace:*", + "@deepseek-ai/dsh-llm": "workspace:*", + "@deepseek-ai/dsh-session": "workspace:*", + "@deepseek-ai/dsh-session-projection": "workspace:*", + "@deepseek-ai/dsh-system-prompt": "workspace:*", + "@deepseek-ai/dsh-tools": "workspace:*", + "@deepseek-ai/dsh-subprocess": "workspace:*", + "@deepseek-ai/dsh-subprocess-local": "workspace:*", + "@deepseek-ai/dsh-fs-local": "workspace:*", + "@deepseek-ai/dsh-llm-deepseek": "workspace:*", + "@deepseek-ai/dsh-ptc-runtime-node": "workspace:*", + "@deepseek-ai/dsh-sandbox-local": "workspace:*", + "@deepseek-ai/dsh-sandbox-policy": "workspace:*", + "sharp": "^0.35.3", + "@deepseek-ai/dsh-ptc-runtime": "workspace:*", + "@deepseek-ai/dsh-user-questions": "workspace:*", + "@deepseek-ai/dsh-settings": "workspace:*", + "@deepseek-ai/dsh-app-boot": "workspace:*", + "@deepseek-ai/dsh-config-editor": "workspace:*", + "@deepseek-ai/dsh-hmr": "workspace:*", + "@deepseek-ai/cordis-plugin-timer": "workspace:~" + }, + "scripts": { + "build:native": "swift build --package-path native --configuration release" + } +} diff --git a/packages/experimental/cua-macos/src/guidance.ts b/packages/experimental/cua-macos/src/guidance.ts new file mode 100644 index 0000000000..823809c1ab --- /dev/null +++ b/packages/experimental/cua-macos/src/guidance.ts @@ -0,0 +1,39 @@ +/** macOS operation guidance supplied with the shared CUA object API. */ +/** Instructions for raw operation calls and optional object-API bindings. */ +export const GUIDANCE = `Use cua with method getCapabilities and params {} to inspect permissions and sessionLocked. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI attempts and ask the user to unlock macOS. A null sessionLocked means no usable console session was verified. After unlocking, read fresh state; screenshots from a locked desktop are not current observations. Use listApps or cua.getState to discover running applications, and findApps(query) to search installed applications by name, bundle ID or absolute path. getApp requests user consent before launching a stopped application in the background, or before observations and input on a running one. If access is denied or revoked, stop using that application until the user explicitly grants access again. Never switch tools to bypass a refusal. Keep the returned appHandle; every app operation requires it. listWindows returns window IDs and point bounds. Raw getState returns AX text and snapshotId. Menu-bar entries are shown by default; includeMenus=true expands their contents. details=true adds identifiers, global-point bounds and complete action names. Both options also apply to getAXState. Each read replaces the indices for that app/window scope. Use its element indices only with that snapshotId. Raw screenshot and coordinate operations require a windowId. Coordinates are window-local macOS points, which may differ from screenshot pixels. A verified window can expose no AX elements. Its empty snapshot permits screenshots and coordinate input; element-index actions require an element from a fresh AX observation. + +Initialize the object API once in persistent PTC. Reuse its variables in later calls; put temporary declarations inside { ... } blocks to avoid redeclaration errors. All object methods are async. getAXState returns text, not a structured state object. Its text separates values and placeholders, and reports subroles, help, URLs, disabled/focused/selected/expanded state and UTF-16 selection ranges when available. Writability is true, false or unknown on relevant controls; omitted state fields do not assert false. selection=unavailable means the reported range exceeds the observed text. Compact output omits ordinary Press/Pick (click), ShowMenu (right click) and ScrollToVisible actions; details=true lists every available action. Use the displayed action name with performSecondaryAction; ambiguous aliases require the complete name from details=true. Truncation reports the reached provider limit. Narrow the window or increase that provider limit before reading again. + +Use app methods directly for single-window tasks; getApp already reads initial AX state. Call app.getWindows() and await app.getWindow(windowId) only to target a particular window or resolve ambiguous window selection. App screenshots and coordinate actions use the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not refresh focus automatically. Read app.getAXState() to follow changed focus. + +Object API signatures (options default to { emit: true }): + +- cua.getCapabilities(options?) -> { platform, permissions, sessionLocked, methods, coordinateSpace, browser } +- cua.listApps(options?) -> Array<{ id, displayName, isRunning: true, pid }> +- cua.findApps(query: string, options?) -> Array<{ id, displayName, path, isRunning, pid? }> +- cua.getState(options?) -> { apps, browsers: [] } +- cua.getApp(selector: string) -> App +- app.activate() -> { dispatched: true, verified: false } +- app.getWindows(options?) -> Array<{ windowId, pid, appName, title, bounds, isOnScreen, onCurrentSpace }> +- app.getWindow(windowId: number) -> Window +- app.typeText(text: string), app.pressKey(chord: string) -> action result +- app.paste(text: string, options?: { format?: "text" | "html" }) -> action result + +App and Window methods: + +- show() -> { dispatched: true, verified: false, windowId: number | null, foregroundChanged: boolean | null } +- getAXState(options?: { emit?: boolean, disableDiffing?: boolean, includeMenus?: boolean, details?: boolean }) -> string +- getScreenshot(options?: { emit?: boolean }) -> Uint8Array +- getAXStateAndScreenshot(options?) -> { state: string, screenshot: Uint8Array } +- click(target: number | [x, y], options?: { mouseButton?: "left" | "right" | "middle", clickCount?: 1 | 2, modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result +- setValue(elementIndex: number, value: string) -> action result +- selectText(elementIndex: number, text: string, options?: { prefix?: string, suffix?: string, selectionType?: "text" | "cursor_before" | "cursor_after" }) -> action result +- performSecondaryAction(elementIndex: number, action: string) -> action result +- scroll({ x, y, deltaLinesX, deltaLinesY }) -> action result +- drag(from: [x, y], to: [x, y], options?: { mouseButton?: "left" | "right" | "middle", modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result + +Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input. + +Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Coordinate clicks, scrolling and dragging require a successful AX observation or screenshot of that window. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input. + +Use selectText only on controls advertising selectionWritable. It selects a unique literal occurrence, or places the cursor before/after it; prefix and suffix must be immediately adjacent. AMBIGUOUS_TEXT requires more context; TEXT_NOT_FOUND or SELECTION_CHANGED requires a fresh observation before deciding on input. Left clicks support single/double clicks; right and middle clicks support single clicks. Modified clicks use pointer input. Keypad names such as KP_0 and KP_Enter retain their physical key identity. Window objects expose observation and pointer/AX actions; keyboard input remains app-scoped and follows current focus. Scroll deltas are integer lines from -10 to 10; positive values reveal content above or left. Paste delivers plain text or HTML through the application's enabled standard Paste menu command. Original clipboard formats are restored if the clipboard still belongs to this paste; a later copy is preserved. Unsupported Paste commands fail without typing fallback. CLIPBOARD_CHANGED means another writer intervened; observe before deciding on further input. Browser applications can be controlled with getApp like other native apps; browser tabs are unavailable. Installed-app search covers standard macOS application directories; use an absolute application path for another location. Launch does not restore a window or document, and cancellation may leave an application already started. The execution host must install the object API and own variable persistence; this plugin does not preload a cua global. In persistent PTC, use the host-provided initialization snippet below once. Its installation-owned file URL works independently of the project directory. Retain app objects between calls. Observation methods emit by default and also return their value. Use void await app.getAXState() to avoid a duplicate REPL completion, or pass emit:false and print the returned text once. End screenshot calls with void await app.getScreenshot() or void await app.getAXStateAndScreenshot() so image bytes are not serialized as the program completion value.` diff --git a/packages/experimental/cua-macos/src/index.ts b/packages/experimental/cua-macos/src/index.ts new file mode 100644 index 0000000000..b351d8be8f --- /dev/null +++ b/packages/experimental/cua-macos/src/index.ts @@ -0,0 +1,49 @@ +/** macOS native CUA provider using the shared application and window API. */ +import { isAbsolute, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Context } from '@deepseek-ai/cordis' +import { CuaConfig, installCua } from '@deepseek-ai/dsh-cua' +import { nativeHostArchitecture, prepareNativeExecutable } from './native-build.ts' +import { createSharedNativeChannel } from './shared-native.ts' +import { GUIDANCE } from './guidance.ts' +export { GUIDANCE } from './guidance.ts' + +/** Loader plugin identity. */ +export const name = 'experimental-cua-macos' +/** Services required by the shared CUA executor and the native launcher. */ +export const inject = ['computerUse', 'tools', 'systemPrompt', 'subprocess', 'agents'] +/** Native preparation, operation bounds, and profile-local application grants. */ +export type Config = CuaConfig +/** Validate the shared configuration before preparing the macOS helper. */ +export const Config: typeof CuaConfig = CuaConfig + +const description = 'Inspect macOS permissions, select an application and window, read AX state, capture screenshots or dispatch input. Use appHandle from getApp; verify actions by reading fresh state.' + +/** + * Mount logged native operations with separate Agent connections to the shared helper. + * @param ctx - Host context with computer-use, subprocess and tool services. + * @param config - Validated native preparation and operation bounds. + * @returns Completion after native preparation and tool registration. + */ +export async function apply(ctx: Context, config: Config): Promise { + if (config.helperPath !== undefined) { + if (!isAbsolute(config.helperPath)) throw new Error('dsh-cua helperPath must be absolute') + installCua(ctx, config, { helperPath: config.helperPath, createChannel: createSharedNativeChannel, guidance: GUIDANCE, description }) + return + } + const architecture = nativeHostArchitecture(process.platform, process.arch) + const controller = new AbortController() + const prepared = prepareNativeExecutable({ + packageRoot: resolve(fileURLToPath(new URL('..', import.meta.url))), + architecture, subprocess: ctx.subprocess, + timeoutMs: config.buildTimeoutMs, graceMs: config.graceMs, signal: controller.signal, + }) + ctx.effect(() => async () => { + controller.abort() + // The Loader receives preparation failures through apply; disposal only awaits quiescence. + await prepared.catch(() => {}) + }, 'dsh-cua.native-preparation') + const helperPath = await prepared + controller.signal.throwIfAborted() + installCua(ctx, config, { helperPath, createChannel: createSharedNativeChannel, guidance: GUIDANCE, description }) +} diff --git a/packages/experimental/cua/src/native-build.ts b/packages/experimental/cua-macos/src/native-build.ts similarity index 98% rename from packages/experimental/cua/src/native-build.ts rename to packages/experimental/cua-macos/src/native-build.ts index 1b0eaf8894..1d79c4b402 100644 --- a/packages/experimental/cua/src/native-build.ts +++ b/packages/experimental/cua-macos/src/native-build.ts @@ -48,7 +48,7 @@ export function nativeExecutablePath(packageRoot: string, architecture: NativeAr */ export async function isSourcePackage(packageRoot: string): Promise { const root = resolve(packageRoot, '../../..') - if (join(root, 'packages/experimental/cua') !== packageRoot) return false + if (join(root, 'packages/experimental/cua-macos') !== packageRoot) return false try { const manifest: unknown = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) return typeof manifest === 'object' && manifest !== null && 'name' in manifest && manifest.name === '@deepseek-ai/dsh-root' diff --git a/packages/experimental/cua/src/shared-native.ts b/packages/experimental/cua-macos/src/shared-native.ts similarity index 95% rename from packages/experimental/cua/src/shared-native.ts rename to packages/experimental/cua-macos/src/shared-native.ts index 8b742348c2..3f59c02b93 100644 --- a/packages/experimental/cua/src/shared-native.ts +++ b/packages/experimental/cua-macos/src/shared-native.ts @@ -4,11 +4,12 @@ import { lstat, mkdir, readFile, realpath } from 'node:fs/promises' import { Socket } from 'node:net' import { userInfo } from 'node:os' import { join } from 'node:path' -import { PassThrough, Writable, type Readable } from 'node:stream' +import { PassThrough, Writable } from 'node:stream' import { setTimeout as delay } from 'node:timers/promises' import type { SubprocessHandle, SubprocessOutcome, SubprocessRuntime } from '@deepseek-ai/dsh-subprocess' import { z } from 'zod' -import type { NativeClientConfig } from './native-client.ts' +import type { NativeClientConfig, NativeChannel } from '@deepseek-ai/dsh-cua/native-client' +export type { NativeChannel } from '@deepseek-ai/dsh-cua/native-client' const Ready = z.object({ type: z.literal('ready'), protocol: z.literal(1), runtimeKey: z.string(), @@ -19,16 +20,6 @@ const handshakeByteLimit = 8192 type LauncherExit = [PromiseSettledResult, PromiseSettledResult] interface RuntimeLocation { helperPath: string; runtimeKey: string; directory: string } -/** One Agent's ordered native protocol stream and its cleanup completion. */ -export interface NativeChannel { - readonly stdin: Writable - readonly stdout: Readable - readonly done: Promise - /** Stop writes and release this connection's native state without terminating other clients. */ - close(): void - /** @returns Completion after this socket and its managed startup process have closed. */ - waitForClose(): Promise -} function transportError(code: string, message: string, cause?: unknown): Error & { code: string } { return Object.assign(new Error(message, { cause }), { code }) diff --git a/packages/experimental/cua/tests/large-image-ptc.spec.ts b/packages/experimental/cua-macos/tests/large-image-ptc.spec.ts similarity index 100% rename from packages/experimental/cua/tests/large-image-ptc.spec.ts rename to packages/experimental/cua-macos/tests/large-image-ptc.spec.ts diff --git a/packages/experimental/cua/tests/loader-composition.spec.ts b/packages/experimental/cua-macos/tests/loader-composition.spec.ts similarity index 99% rename from packages/experimental/cua/tests/loader-composition.spec.ts rename to packages/experimental/cua-macos/tests/loader-composition.spec.ts index 2cf00ed99a..da9da2a07f 100644 --- a/packages/experimental/cua/tests/loader-composition.spec.ts +++ b/packages/experimental/cua-macos/tests/loader-composition.spec.ts @@ -20,7 +20,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools' import SubprocessLocal from '@deepseek-ai/dsh-subprocess-local' import { z } from 'zod' import * as NativeProvider from '../src/index.ts' -import { createCuaApi } from '../src/api.ts' +import { createCuaApi } from '@deepseek-ai/dsh-cua/api' import { installNativeFixture } from './native-fixture.ts' const png = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADElEQVQImWNgZGIGAAAOAAeCcsnOAAAAAElFTkSuQmCC' diff --git a/packages/experimental/cua/tests/native-build.spec.ts b/packages/experimental/cua-macos/tests/native-build.spec.ts similarity index 99% rename from packages/experimental/cua/tests/native-build.spec.ts rename to packages/experimental/cua-macos/tests/native-build.spec.ts index cd912568c3..178e656656 100644 --- a/packages/experimental/cua/tests/native-build.spec.ts +++ b/packages/experimental/cua-macos/tests/native-build.spec.ts @@ -9,7 +9,7 @@ import { buildNativeExecutable, isSourcePackage, nativeExecutablePath, nativeHos async function fixture(diagnostics = true) { const root = await mkdtemp(join(tmpdir(), 'cua-native-build-')) onTestFinished(() => rm(root, { recursive: true, force: true })) - const packageRoot = join(root, 'packages/experimental/cua') + const packageRoot = join(root, 'packages/experimental/cua-macos') await mkdir(packageRoot, { recursive: true }) const controller = new AbortController() let output: Buffer = Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0, 255, 128]) diff --git a/packages/experimental/cua/tests/native-fixture.ts b/packages/experimental/cua-macos/tests/native-fixture.ts similarity index 100% rename from packages/experimental/cua/tests/native-fixture.ts rename to packages/experimental/cua-macos/tests/native-fixture.ts diff --git a/packages/experimental/cua/tests/persistent-bridge.built.e2e.ts b/packages/experimental/cua-macos/tests/persistent-bridge.built.e2e.ts similarity index 97% rename from packages/experimental/cua/tests/persistent-bridge.built.e2e.ts rename to packages/experimental/cua-macos/tests/persistent-bridge.built.e2e.ts index 66227b76e5..8885a39503 100644 --- a/packages/experimental/cua/tests/persistent-bridge.built.e2e.ts +++ b/packages/experimental/cua-macos/tests/persistent-bridge.built.e2e.ts @@ -19,9 +19,9 @@ import Subprocess from '@deepseek-ai/dsh-subprocess-local' import { expect, it } from 'vitest' import * as Cua from '../src/index.ts' import { installNativeFixture } from './native-fixture.ts' -import { Request } from '../src/protocol.ts' +import { Request } from '@deepseek-ai/dsh-cua/protocol' -const bridge = fileURLToPath(new URL('../lib/bridge.js', import.meta.url)) +const bridge = fileURLToPath(new URL('../../cua/lib/bridge.js', import.meta.url)) const bootstrap = join(dirname(fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-ptc-runtime-node/package.json'))), 'lib/persistent-process.js') class VisualAdapter extends LlmAdapter { diff --git a/packages/experimental/cua/tests/protocol-generation.spec.ts b/packages/experimental/cua-macos/tests/protocol-generation.spec.ts similarity index 95% rename from packages/experimental/cua/tests/protocol-generation.spec.ts rename to packages/experimental/cua-macos/tests/protocol-generation.spec.ts index 996bde8923..4c9710368a 100644 --- a/packages/experimental/cua/tests/protocol-generation.spec.ts +++ b/packages/experimental/cua-macos/tests/protocol-generation.spec.ts @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises' import { describe, expect, it } from 'vitest' import { renderNativeProtocol } from '../native/generate-protocol.ts' -import { Request } from '../src/protocol.ts' +import { Request } from '@deepseek-ai/dsh-cua/protocol' describe('native protocol generation', () => { it('keeps native JSON Schema and Swift admission tables current', async () => { diff --git a/packages/experimental/cua/tests/provider-lifecycle.spec.ts b/packages/experimental/cua-macos/tests/provider-lifecycle.spec.ts similarity index 98% rename from packages/experimental/cua/tests/provider-lifecycle.spec.ts rename to packages/experimental/cua-macos/tests/provider-lifecycle.spec.ts index dec37bd68b..601544fa09 100644 --- a/packages/experimental/cua/tests/provider-lifecycle.spec.ts +++ b/packages/experimental/cua-macos/tests/provider-lifecycle.spec.ts @@ -24,7 +24,7 @@ import { SessionId } from '@deepseek-ai/dsh-session' import Subprocess from '@deepseek-ai/dsh-subprocess' import { expect, it, onTestFinished, vi } from 'vitest' import * as NativeProvider from '../src/index.ts' -import type { NativeClient } from '../src/native-client.ts' +import type { NativeClient } from '@deepseek-ai/dsh-cua/native-client' import type { NativeBuildSpec } from '../src/native-build.ts' const native = vi.hoisted(() => ({ @@ -37,8 +37,8 @@ vi.mock('../src/native-build.ts', async importOriginal => ({ nativeHostArchitecture: native.architecture, prepareNativeExecutable: native.prepare, })) -vi.mock('../src/native-client.ts', async importOriginal => ({ - ...await importOriginal(), +vi.mock('@deepseek-ai/dsh-cua/native-client', async importOriginal => ({ + ...await importOriginal(), NativeClient: class { disposed = false constructor() { native.instances.push(this) } @@ -435,7 +435,7 @@ it('adds an installation-owned bridge URL when persistent PTC is available', asy await f.ctx.plugin(Sandbox) await f.ctx.plugin(SandboxPolicy) await f.ctx.plugin(NodeRuntime, { executionMode: 'persistent-in-memory' }) - expect(await text()).toContain(new URL('../lib/bridge.js', import.meta.url).href) + expect(await text()).toContain(new URL('../../cua/lib/bridge.js', import.meta.url).href) }) it('keeps Cordis loading until preparation succeeds and reports preparation failure through the fiber', async () => { diff --git a/packages/experimental/cua/tests/shared-native.spec.ts b/packages/experimental/cua-macos/tests/shared-native.spec.ts similarity index 99% rename from packages/experimental/cua/tests/shared-native.spec.ts rename to packages/experimental/cua-macos/tests/shared-native.spec.ts index 052a7199d2..32538d1b1f 100644 --- a/packages/experimental/cua/tests/shared-native.spec.ts +++ b/packages/experimental/cua-macos/tests/shared-native.spec.ts @@ -6,7 +6,7 @@ import { userInfo } from 'node:os' import { join } from 'node:path' import type { SubprocessHandle, SubprocessOutcome, SubprocessRuntime } from '@deepseek-ai/dsh-subprocess' import { describe, expect, it, onTestFinished, vi } from 'vitest' -import type { NativeClientConfig } from '../src/native-client.ts' +import type { NativeClientConfig } from '@deepseek-ai/dsh-cua/native-client' import { createSharedNativeChannel, type NativeChannel } from '../src/shared-native.ts' const runtime = vi.hoisted(() => ({ source: '', directory: '', foreignOwner: false })) diff --git a/packages/experimental/cua-macos/tsconfig.json b/packages/experimental/cua-macos/tsconfig.json new file mode 100644 index 0000000000..05a6c912a2 --- /dev/null +++ b/packages/experimental/cua-macos/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../cua" + }, + { + "path": "../../subprocess/subprocess" + }, + { + "path": "../../util/atomic-write" + } + ] +} diff --git a/packages/experimental/cua-windows/README.i18n.yaml b/packages/experimental/cua-windows/README.i18n.yaml new file mode 100644 index 0000000000..60e544ea1f --- /dev/null +++ b/packages/experimental/cua-windows/README.i18n.yaml @@ -0,0 +1,7 @@ +# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/experimental/cua-windows/README.md +/windows-computer-use: + en: dfaa27a02a433ba7 + zh: 6ef0e8fd15e277da diff --git a/packages/experimental/cua-windows/README.md b/packages/experimental/cua-windows/README.md new file mode 100644 index 0000000000..734745d757 --- /dev/null +++ b/packages/experimental/cua-windows/README.md @@ -0,0 +1,7 @@ +# Windows Computer Use + +English | [中文](README.zh.md) + +Windows native execution is reserved for a separate C# helper. This directory contains no loadable provider yet. The implementation will consume the [shared CUA object API](../cua/README.md) and preserve its app/window methods, authorization, snapshot validity, and action-result semantics. macOS implementation and build files belong to [cua-macos](../cua-macos/README.md). + +The Windows implementation must document system-specific limitations and provide cross-compilation evidence separately from Windows desktop validation. diff --git a/packages/experimental/cua-windows/README.zh.md b/packages/experimental/cua-windows/README.zh.md new file mode 100644 index 0000000000..6c5419f795 --- /dev/null +++ b/packages/experimental/cua-windows/README.zh.md @@ -0,0 +1,7 @@ +# Windows Computer Use + +[English](README.md) | 中文 + +Windows 原生执行将使用独立的 C# helper。本目录尚无可加载的提供方。实现将使用[共享 CUA 对象 API](../cua/README.zh.md),保持其应用与窗口方法、授权、快照有效性和动作结果语义。macOS 实现和构建文件归 [cua-macos](../cua-macos/README.zh.md) 所有。 + +Windows 实现须说明系统特有的限制,并分别提供交叉编译和 Windows 桌面验证的证据。 diff --git a/packages/experimental/cua/README.i18n.yaml b/packages/experimental/cua/README.i18n.yaml index 24c69d9753..4a04827c51 100644 --- a/packages/experimental/cua/README.i18n.yaml +++ b/packages/experimental/cua/README.i18n.yaml @@ -3,50 +3,44 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/cua/README.md /: - en: a1503331d3f35516 - zh: fcb8e5220ff732e1 + en: aa3d316ef17feae7 + zh: 5a30d743ce257d5b /deepseek-ai-dsh-cua: en: 0ef7ee882840cf1b zh: 01b9e768eff2d302 /deepseek-ai-dsh-cua/summary: - en: 4549f02fc53854a5 - zh: 6f74f1130b6623a8 + en: f4d9cc48587d62a2 + zh: d1bac6c1fe7eb29e /deepseek-ai-dsh-cua/table-of-contents: en: d152484eb41ac6b4 - zh: 7b30b973a849247d + zh: 1db86285abf6c499 /deepseek-ai-dsh-cua/use-this-package: - en: 760c77bce6472262 - zh: a8806a04335dd6e5 -/deepseek-ai-dsh-cua/use-this-package/connect-an-external-execution-environment: - en: 98a598ea10daad8c - zh: 62ea7d5a1d14d0c4 + en: f7ede2d68d593294 + zh: b3419cfc17db7043 /deepseek-ai-dsh-cua/understand-the-implementation: - en: 66c97ed581583ba3 - zh: 35bb9ee599415a9b + en: effcee57d8854c32 + zh: d02c6b76e7249051 /deepseek-ai-dsh-cua/further-exploration: - en: b1047318879cd6f4 - zh: e4ce4a5ad2a9c4dc + en: 9cb2956020aadfa5 + zh: d72f89022f337469 /deepseek-ai-dsh-cua/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 -/deepseek-ai-dsh-cua/model-experience/native-application-operations: - en: ab7cba4a42334ab9 - zh: f77cd9d84624b6c2 -/deepseek-ai-dsh-cua/model-experience/native-application-operations/what-the-model-sees: - en: c221fa5ce647b1dd - zh: 9388c91a9f89e029 -/deepseek-ai-dsh-cua/model-experience/native-application-operations/what-the-model-sees/operation-guidance-verbatim: - en: 2dbc22ffd1f8a717 - zh: d4458872e75305d9 -/deepseek-ai-dsh-cua/model-experience/native-application-operations/token-effect: - en: ab52b46d874fa8b9 - zh: e87d3620ef77cb06 -/deepseek-ai-dsh-cua/model-experience/native-application-operations/kv-cache-effect: - en: a2c9acb5a4320dda - zh: e8d1586af60215c4 +/deepseek-ai-dsh-cua/model-experience/authorized-application-operations: + en: 0faf1e4d409d61aa + zh: 1866dd0b702f2a26 +/deepseek-ai-dsh-cua/model-experience/authorized-application-operations/what-the-model-sees: + en: 4f90c4a6b121bce0 + zh: c36ab4b80985c76e +/deepseek-ai-dsh-cua/model-experience/authorized-application-operations/token-effect: + en: 88463f0304c207bd + zh: 6d3ae2f1b8e45747 +/deepseek-ai-dsh-cua/model-experience/authorized-application-operations/kv-cache-effect: + en: 2d5df52dafa1d940 + zh: 06e815759b9150c3 /deepseek-ai-dsh-cua/known-limitations-and-deferred-work: - en: 1687063bfc1e8476 - zh: ae06137ce64e6807 + en: c727217a98fd1ce4 + zh: fc94acd27639d7d5 /deepseek-ai-dsh-cua/known-limitations-and-deferred-work/dev-note: - en: b836f01bd0bceb4a - zh: 8122fa08b2aa95fa + en: 0a1253c6f7b5e8b9 + zh: 9f56528562c7c914 diff --git a/packages/experimental/cua/README.md b/packages/experimental/cua/README.md index eb82ed8b95..58a2657df8 100644 --- a/packages/experimental/cua/README.md +++ b/packages/experimental/cua/README.md @@ -1,6 +1,6 @@ --- -description: "Inspect and operate native macOS applications through scoped app and window objects." -kind: "package-reference" +description: "Reuse Computer Use application and window objects with authorized, logged platform operations." +kind: "package-library" --- # @deepseek-ai/dsh-cua @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Inspect macOS permissions, select running applications and windows, read accessibility trees, capture screenshots and send input. The object API can be retained by an external persistent execution environment. The Swift helper uses macOS frameworks directly and adapts selected MIT-licensed Cua Driver behavior. This package owns neither a PTC runtime nor variable persistence. +Inspect applications, retain window objects across PTC calls, and send actions through the normal tool pipeline. Platform providers supply native execution; this library owns the object API, request validation, application consent, and image admission. The external PTC runtime owns variable persistence. ## Table of Contents @@ -25,81 +25,15 @@ Inspect macOS permissions, select running applications and windows, read accessi ## Use this package -Enable this experimental provider through explicit source composition on macOS 14 or later. Windows support is in development. Shipped profiles do not mount it. Prepare a native helper and load the registry/provider overlay: +Mount the [macOS provider](../cua-macos/README.md) to enable native operations. External PTC hosts import `connectCua` from `@deepseek-ai/dsh-cua/bridge` and supply the current `tools.cua` binding. Source compositions and native preparation belong to the provider. -```sh -swift build --package-path packages/experimental/cua/native --configuration release -cua_native_root="$PWD/packages/experimental/cua/native" -export DSH_CUA_HELPER="$cua_native_root/.build/release/dsh-cua-native" -pnpm dsh headless --patch packages/experimental/cua/examples/macos.cordis.yml --dump-config -``` +Use App methods for a single-window task. `getApp` reads initial accessibility state; `getWindows` and `getWindow` select an explicit window when needed. Re-reading one app/window scope replaces its element indices without invalidating another window's snapshot. Keyboard input follows the application's focus. -The dump command checks profile composition without a model request. For a real task, replace `--dump-config` with a prompt and configure the profile’s model credentials. The overlay mounts the computer-use registry and this provider; remove an existing competing provider before using it. The responsible host/helper needs Accessibility permission for AX/input and Screen Recording permission for capture. `getCapabilities` reports permissions and session lock state without opening a system dialog. Locked or unavailable desktop sessions reject UI observations and input before dispatch; the user must unlock macOS before resuming. `listApps` and `cua.getState` list running applications. `findApps(query)` searches standard macOS application directories by name, bundle ID or absolute path. `getApp` selects a unique name, bundle ID, path or running PID, obtains application consent, and launches a stopped app in the background when needed. A stopped executable is identified before approval and checked again before and after launch; unsigned code only receives task access. The returned object reads initial AX without activating the app. Launch cancellation may leave the application running. Model guidance starts with background observation and input, without activation during initialization or before each action. For hidden or minimized windows, try `app.show()` or `window.show()` and read fresh state. This requests unhide and unminimize without activation, raising or switching Spaces; app-level restoration selects the focused, main or sole AX window. The result names the selected `windowId`, or null when no window was selected, and reports `foregroundChanged` by comparing the front app and top window before and after the request. Null means foreground evidence was unavailable; later focus changes remain possible. If restoration fails or the window remains unusable, explain the needed foreground step, call `app.activate()` or an observed window’s `raise` action, then observe again. Necessary foreground access takes priority over preserving focus. Applications may change focus after input; background delivery does not guarantee an undisturbed user session. +Observation methods emit and return their result. Use `void await app.getAXState()` for one visible copy, or use `emit:false` and print the returned text once. Screenshots return PNG bytes and enter the attachment pipeline once; `emit:false` only suppresses the object sink. -| Field | Default | Meaning | -| --- | --- | --- | -| `allowedApps` | `[]` | Live profile-local application consent, keyed by executable path and verified signing requirement. | -| `helperPath` | package-owned executable | Optional absolute native executable override. | -| `buildTimeoutMs` | `300000` | Maximum native preparation time. | -| `timeoutMs` | `30000` | Per-operation deadline; cancellation can leave partial input. | -| `connectTimeoutMs` | `5000` | Deadline for connecting to or starting the shared helper. | -| `screenshotTimeoutMs` | `5000` | Screenshot deadline including queue time, capped by `timeoutMs`. | -| `axTimeoutMs` | `3000` | Deadline per Accessibility system call, configured independently of the complete operation. | -| `maxRequestBytes` / `maxResponseBytes` | `1048576` / `33554432` | UTF-8 request and response limits. | -| `maxClipboardBytes` | `33554432` | Maximum original clipboard bytes preserved during a paste; oversized contents reject before replacement. | -| `maxAppMatches` | `20` | Maximum installed-app search matches; broader queries reject and require a narrower name. | -| `maxElements` / `maxDepth` | `2000` / `25` | Accessibility traversal limits. | -| `dragDurationMs` / `dragSteps` | `400` / `20` | Duration and movement segments for a drag. | -| `textEventIntervalMs` | `0.25` | Delay after each Unicode down/up event, from 0 to 1000 ms. Chord timing is independent. | +Application consent precedes launch, observation, capture, and input. Task access belongs to the selected instance; saved grants require platform-verified executable identity. Revocation closes affected Agent connections, so their old objects require fresh selection. No input is replayed after cancellation, timeout, or reconnect. -The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-cua) lists all fields. Native tool calls use `{method, params}`; the complete method-specific schema is exposed to the model. For example, `getApp` returns the `appHandle` needed by `listWindows`, `getState` and input operations. - -Keypad names (`KP_0` through `KP_9`, `KP_Enter`, and keypad operators) retain their physical key codes. Key chords use `+` between modifiers and the key. Use `+` for the plus key, `cmd++` for Command-plus, or the key name `plus`; an unfinished chord such as `cmd+` is rejected. - -Agent disposal and plugin unload cancel and await the complete tool call, including image admission and attachment storage. An image write already in progress must settle before disposal returns; a canceled call cannot return a successful screenshot. - -Application consent is enforced before AX reads, window-title reads, screenshots, activation, and input through the CUA tool. User Questions offers task access or denial, plus saved access when the application has a verifiable signature and the composition provides settings persistence. Task access is bound to the selected process handle; saved access is bound to its canonical executable path and designated code-signing requirement. Denial prevents repeated prompts in that task. Removing a saved `allowedApps` entry cancels affected operations and invalidates their helper handles; reauthorization requires fresh selection. Other handles on the same Agent connection also expire, while saved grants remain reusable. Agent/provider disposal joins cleanup of the corresponding native connection. See the [application consent decision](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md). These checks govern CUA calls; persistent PTC and separately authorized shell tools retain their own execution authority. - -Raw `getState` exposes validated state fields and the complete text for the requested disclosure options. The native helper also returns structured element diagnostics for native checks; the tool projection omits that duplicate array from model text and structured tool values. - -AX observations show menu-bar entries by default. Use `app.getAXState({ includeMenus: true })` to expand menu contents and `app.getAXState({ details: true })` for identifiers, global-point bounds and complete action names. These options also apply to raw `getState` and combined AX/screenshot observations. Every observation replaces that scope's indices; obtain fresh indices from the expanded output. Details do not bypass the configured node and depth limits. Truncation identifies the reached limit. - -Values and placeholders are separate. Named, stateful or actionable groups and scroll areas retain their own rows. Repeated names, descriptions and help text on one node are shown once. Values, placeholders and URLs keep their distinct meanings even when their text matches; body text shared by distinct nodes remains intact. Writability is explicit on relevant controls, including `AXIncrementor`; `unknown` means the system did not report it. Missing optional state fields do not imply false. UTF-16 ranges outside the observed value are marked unavailable. Custom action aliases omit AppKit target/selector diagnostics; ambiguous aliases require a complete name from `details:true`. - -Observations emit by default and also return their value. In persistent PTC, use `void await app.getAXState()` for one emitted copy, or `emit:false` and one explicit print. Capturing a screenshot after an emitted tree can fail independently; retain the successful AX output. - -Screenshot diagnostics use `[cua-screenshot]` records keyed by the tool request ID. Native phase records return over the originating Agent connection and cover window/AX geometry checks, system capture, PNG/Base64 encoding and response writing. Host debug logs add dispatch, reply and cleanup timings. Each request retains at most 8192 bytes of complete diagnostic records, with explicit truncation; screenshot failures append these records to the error for Session export. Records contain stages, timings and error codes, not window titles, AX text or image bytes. - -Native preparation serializes compiler checks and publication with the shared file lock. It updates the package payload and runs a content-addressed copy under native/.build/prepared, preserving executable files used by earlier source-runtime processes. Identical builds reuse the same file; corrupted retained copies fail validation. Cancellation and preparation failure finish managed process cleanup before releasing the lock. - -### Connect an external execution environment - -To run through persistent PTC, add `--patch packages/experimental/cua/examples/persistent-ptc.cordis.yml` after the macOS overlay. This provider keeps variables for the live Agent and applies the selected file sandbox policy. The caller imports the bridge and supplies the current authorized `tools.cua` binding. Persistent PTC receives an installation-owned bridge file URL in its CUA instructions, so the project does not need to install this package. The example below is for an external host that resolves the package from its own installation and supplies `tools`; the package does not install a global object or keep variables alive: - -```javascript -import { connectCua } from '@deepseek-ai/dsh-cua/bridge' -const cua = connectCua(() => tools.cua, text => console.log(text)) -const app = await cua.getApp("com.apple.TextEdit") -void await app.getScreenshot() -``` - -Use app methods directly for single-window tasks. `getApp` reads initial AX state, so a screenshot needs no window enumeration or additional AX read. Use `app.getWindows()` and `app.getWindow(windowId)` when the task needs a particular window or default selection is ambiguous; choose by the observed title and bounds. - -`getWindow` emits its initial AX tree. An owned window without an AX surface returns an empty snapshot and remains available for screenshots and coordinate input. Window objects retain separate snapshots, so reading one window does not invalidate another. Re-reading the same scope invalidates old indices. An AX node removed during observation rejects the read with `STALE_SNAPSHOT` and publishes no partial tree; read again without repeating the preceding input. App-scoped screenshots and coordinates reuse the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not query current focus again; read `app.getAXState()` to follow a focus change. Explicit window objects retain their selected target. Keyboard methods stay on `app` because input follows the app’s current focus. - -`paste(text, { format: "text" | "html" })` temporarily publishes the requested clipboard format and invokes the application’s enabled standard Paste menu command. It keeps the temporary format until a pasteboard reader requests its data, then restores every captured original format while it still owns the clipboard, preserving a later copy. Paste operations share a cross-process lock and cancellation joins helper cleanup. A forced process exit can prevent restoration. Unsupported Paste commands fail without typing fallback; `typeText` remains separate key-event input. - -`selectText` selects a unique literal occurrence or places the cursor before/after it through writable `AXSelectedTextRange`. Adjacent prefix/suffix disambiguate repeats. Selection preserves the remaining text and formatting, and verifies the resulting UTF-16 range; unsupported, missing, ambiguous or changed text fails without keyboard fallback. AX text includes `selectionWritable`, selection ranges and selected text when available. - -Screenshot data must use canonical Base64. Response byte limits apply before parsing, and attachment admission validates the raster and its configured capacity. - -Coordinate clicks, scrolling and dragging require a successful AX read or screenshot of that window. A width or height change since the latest observation returns `WINDOW_CHANGED` before input, including raw calls without a snapshot ID; read fresh AX or take a new screenshot before proceeding. Window movement alone preserves window-local coordinates. Capture and pointer input reject known disagreement between the selected window’s accessibility frame and displayed bounds with `WINDOW_CHANGED`, before posting pointer events. This includes Stage Manager transformations when AX retains the full window frame. Read fresh state; any activation or window raise remains explicit. Advertised AX actions remain available. Missing AX geometry retains the AXless path and does not establish that a transformed window can be operated. - -Retained objects resolve the current tool binding on every call. The external runtime owns persistence, imports, cancellation and completion-value serialization. Screenshots return PNG bytes; the standard tool pipeline admits the image attachment exactly once. `emit: false` suppresses facade output only. A host that uses another dispatcher can build objects through `createCuaApi` from `./api`. - -AppKit sheets are selectable by their window IDs and remain visible in their document window’s tree. Keyboard input follows a system Open/Save panel only when the application’s focused sheet identifies one Apple panel-service window and that service still focuses the same panel. Unrelated or ambiguous remote recipients fail before dispatch. - -Indexed clicks support a single right/middle click or a single/double left click. Click and drag options accept a button and modifier keys; a modified click uses pointer input. Drags hold that button and carry its modifiers through every movement and release. AX text marks disabled controls, `valueWritable=true`, and available `selected` state. The helper uses an advertised accessibility action for unmodified clicks when available; pointer delivery resolves the element’s current bounds and verifies its owning window. Actions report dispatch, not application success. A visual pointer or click marker can be rendered independently of input; AX actions do not require moving the system cursor. Window-directed mouse input can still change application focus, selection or dialogs. Read state after input. `STALE_APP` requires acquiring the application again; `STALE_SNAPSHOT` requires a fresh AX read. Request timeout/cancellation waits for native acknowledgement and preserves usable handles. Missing cleanup acknowledgement closes that Agent’s connection. A screenshot RPC still awaiting its system callback occupies the single capture slot; further screenshots return `CAPTURE_BUSY`, while AX and input remain available. A late capture result is discarded, and its real completion releases that slot. Only an unresponsive operation or failed cleanup can invalidate the shared process and all its handles. Never retry uncertain input automatically. Closing an Agent releases its connection and AX references, without undoing changes to applications. +Actions return `dispatched:true, verified:false`; read fresh state to verify the result. Coordinate actions require a successful observation of the target window. Resizing rejects old coordinates with `WINDOW_CHANGED`; movement alone preserves window-local coordinates. ----- @@ -107,17 +41,11 @@ Indexed clicks support a single right/middle click or a single/double left click ## Understand the implementation
-Dispatch and resource ownership +Shared execution and platform ownership -The object facade translates app/window methods into JSON operations. The bridge routes them through the normal tool pipeline, preserving authorization, nested logs and image admission. Agents and Hosts belonging to one user share a helper for the same native build. Each connection retains independent handles, snapshots, configuration and cancellation state; desktop operations use a global queue. The managed launcher transfers service lifetime to these connections, and the helper exits after the last connection finishes cleanup. Process locks cover both build identity and the real executable path, preventing duplicate startup and path collisions after a running executable is replaced. Native handles bind a process identity to its kernel start time, and snapshots retain native AX references only inside that helper. The same operation protocol can serve a future platform provider without adding platform logic to the object API. +The provider calls [installCua](src/index.ts) with its prepared helper, connection factory, and guidance. [NativeClient](src/native-client.ts) owns request correlation, byte limits, cancellation acknowledgement, and failure settlement. The platform connector owns startup and connection cleanup. The shared executor retains one connection per Agent and joins in-flight image writes during disposal. -[Zod operation definitions](src/protocol.ts) own model parameters and separately declare Host resource fields and private continuations. Public data types derive from those definitions. `pnpm exec tsx packages/experimental/cua/native/generate-protocol.ts` generates the native JSON Schema and Swift field/enum admission tables; the package test lane checks freshness and rejects Host-only fields at model admission. Native target validation, resource limits and operation behavior remain enforced at execution. - -[AX text formatting](native/Sources/CuaNative/AXTextFormatter.swift) consumes already-read node data. The AX walker alone assigns indices and retains native references, so formatting owns no second snapshot or target registry. - -Native request IDs match the logged tool call IDs. Successful activation and window-raise results retain foreground request details and immediate before/after focus observations in the raw tool result. An already-frontmost app skips redundant activation; a window raise is skipped only when the exact window is also focused, main, unminimized and first among visible normal windows. Unknown observations retain the requested operation. These observations do not establish who caused an unrelated focus change or verify completion. - -Node roles and metadata share one AX batch per observation. Group and scroll-area nodes contribute only children; unused metadata errors do not reject them. Unsupported batching retains individual attribute reads. AX traversal visits each native identity once, including proxy references that compare equal, so cyclic accessibility children cannot repeat whole subtrees. The [source manifest](native/upstream.json) records adapted upstream behavior and local Swift modules. AppKit runs on the main event loop; a separate serial executor owns AX references and synchronous input. A request retains exclusive execution through capture and response delivery. No invariant companion is published: there is no independently projected registry or durable evaluator state to reconcile. Protocol validation, target identity and resource cleanup are checked by their owning implementations. +[The operation schema](src/protocol.ts) is the authority for model parameters and separately declared Host-only fields. Native implementations enforce target identity, window geometry, resource limits, and operating-system access at execution. No invariant companion is published because this library has no separately projected registry or durable state.
@@ -126,8 +54,8 @@ Node roles and metadata share one AX batch per observation. Group and scroll-are ## Further Exploration -- [Computer-use registration](../../computer-use/computer-use/README.md) -- [Native protocol](native/protocol.schema.json) +- [macOS provider](../cua-macos/README.md) +- [Windows implementation scope](../cua-windows/README.md) - [Object interfaces](src/types.ts) ----- @@ -135,84 +63,31 @@ Node roles and metadata share one AX batch per observation. Group and scroll-are ## Model Experience -### Native application operations +### Authorized application operations #### What the model sees -The plugin contributes one `cua` operation tool with method-specific parameters. Standard PTC presentation exposes it as `tools.cua` through `run_code`. The following package-owned guidance is emitted verbatim: - -##### Operation guidance (verbatim) - -```markdown -Use cua with method getCapabilities and params {} to inspect permissions and sessionLocked. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI attempts and ask the user to unlock macOS. A null sessionLocked means no usable console session was verified. After unlocking, read fresh state; screenshots from a locked desktop are not current observations. Use listApps or cua.getState to discover running applications, and findApps(query) to search installed applications by name, bundle ID or absolute path. getApp requests user consent before launching a stopped application in the background, or before observations and input on a running one. If access is denied or revoked, stop using that application until the user explicitly grants access again. Never switch tools to bypass a refusal. Keep the returned appHandle; every app operation requires it. listWindows returns window IDs and point bounds. Raw getState returns AX text and snapshotId. Menu-bar entries are shown by default; includeMenus=true expands their contents. details=true adds identifiers, global-point bounds and complete action names. Both options also apply to getAXState. Each read replaces the indices for that app/window scope. Use its element indices only with that snapshotId. Raw screenshot and coordinate operations require a windowId. Coordinates are window-local macOS points, which may differ from screenshot pixels. A verified window can expose no AX elements. Its empty snapshot permits screenshots and coordinate input; element-index actions require an element from a fresh AX observation. - -Initialize the object API once in persistent PTC. Reuse its variables in later calls; put temporary declarations inside { ... } blocks to avoid redeclaration errors. All object methods are async. getAXState returns text, not a structured state object. Its text separates values and placeholders, and reports subroles, help, URLs, disabled/focused/selected/expanded state and UTF-16 selection ranges when available. Writability is true, false or unknown on relevant controls; omitted state fields do not assert false. selection=unavailable means the reported range exceeds the observed text. Compact output omits ordinary Press/Pick (click), ShowMenu (right click) and ScrollToVisible actions; details=true lists every available action. Use the displayed action name with performSecondaryAction; ambiguous aliases require the complete name from details=true. Truncation reports the reached provider limit. Narrow the window or increase that provider limit before reading again. - -Use app methods directly for single-window tasks; getApp already reads initial AX state. Call app.getWindows() and await app.getWindow(windowId) only to target a particular window or resolve ambiguous window selection. App screenshots and coordinate actions use the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not refresh focus automatically. Read app.getAXState() to follow changed focus. - -Object API signatures (options default to { emit: true }): - -- cua.getCapabilities(options?) -> { platform, permissions, sessionLocked, methods, coordinateSpace, browser } -- cua.listApps(options?) -> Array<{ id, displayName, isRunning: true, pid }> -- cua.findApps(query: string, options?) -> Array<{ id, displayName, path, isRunning, pid? }> -- cua.getState(options?) -> { apps, browsers: [] } -- cua.getApp(selector: string) -> App -- app.activate() -> { dispatched: true, verified: false } -- app.getWindows(options?) -> Array<{ windowId, pid, appName, title, bounds, isOnScreen, onCurrentSpace }> -- app.getWindow(windowId: number) -> Window -- app.typeText(text: string), app.pressKey(chord: string) -> action result -- app.paste(text: string, options?: { format?: "text" | "html" }) -> action result - -App and Window methods: - -- show() -> { dispatched: true, verified: false, windowId: number | null, foregroundChanged: boolean | null } -- getAXState(options?: { emit?: boolean, disableDiffing?: boolean, includeMenus?: boolean, details?: boolean }) -> string -- getScreenshot(options?: { emit?: boolean }) -> Uint8Array -- getAXStateAndScreenshot(options?) -> { state: string, screenshot: Uint8Array } -- click(target: number | [x, y], options?: { mouseButton?: "left" | "right" | "middle", clickCount?: 1 | 2, modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result -- setValue(elementIndex: number, value: string) -> action result -- selectText(elementIndex: number, text: string, options?: { prefix?: string, suffix?: string, selectionType?: "text" | "cursor_before" | "cursor_after" }) -> action result -- performSecondaryAction(elementIndex: number, action: string) -> action result -- scroll({ x, y, deltaLinesX, deltaLinesY }) -> action result -- drag(from: [x, y], to: [x, y], options?: { mouseButton?: "left" | "right" | "middle", modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result - -Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input. - -Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Coordinate clicks, scrolling and dragging require a successful AX observation or screenshot of that window. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input. - -Use selectText only on controls advertising selectionWritable. It selects a unique literal occurrence, or places the cursor before/after it; prefix and suffix must be immediately adjacent. AMBIGUOUS_TEXT requires more context; TEXT_NOT_FOUND or SELECTION_CHANGED requires a fresh observation before deciding on input. Left clicks support single/double clicks; right and middle clicks support single clicks. Modified clicks use pointer input. Keypad names such as KP_0 and KP_Enter retain their physical key identity. Window objects expose observation and pointer/AX actions; keyboard input remains app-scoped and follows current focus. Scroll deltas are integer lines from -10 to 10; positive values reveal content above or left. Paste delivers plain text or HTML through the application's enabled standard Paste menu command. Original clipboard formats are restored if the clipboard still belongs to this paste; a later copy is preserved. Unsupported Paste commands fail without typing fallback. CLIPBOARD_CHANGED means another writer intervened; observe before deciding on further input. Browser applications can be controlled with getApp like other native apps; browser tabs are unavailable. Installed-app search covers standard macOS application directories; use an absolute application path for another location. Launch does not restore a window or document, and cancellation may leave an application already started. The execution host must install the object API and own variable persistence; this plugin does not preload a cua global. In persistent PTC, use the host-provided initialization snippet below once. Its installation-owned file URL works independently of the project directory. Retain app objects between calls. Observation methods emit by default and also return their value. Use void await app.getAXState() to avoid a duplicate REPL completion, or pass emit:false and print the returned text once. End screenshot calls with void await app.getScreenshot() or void await app.getAXStateAndScreenshot() so image bytes are not serialized as the program completion value. -``` +Platform providers mount the shared `cua` tool and supply operating instructions. Calls and observations use the same app/window interface and enter normal Session history. Platform guidance is documented by the [macOS provider](../cua-macos/README.md#model-experience). #### Token effect -Enabling the plugin adds a fixed schema and guidance. AX observations, operation results and admitted images add model input. Retained objects are not serialized into prompts by this package. +Operation results and admitted images add model input. Retained JavaScript objects are not serialized into prompts. Duplicate caller printing can repeat observation text. #### KV Cache effect -Tool results append to Session history. Changing plugin configuration or tool presentation can change the request prefix; retaining an app object does not change existing history. +Results append to history. Retaining objects does not rewrite previous messages; changing the mounted tools or guidance can change the request prefix. ## Known Limitations and Deferred Work -The queue orders individual native requests. Holding an app object does not reserve its application across a Session; application-level ownership spanning several requests remains separate work. Capture RPCs that never return occupy the single capture slot; creating new app objects does not reset that system connection. - -- Only the macOS native implementation is provided. Windows support is in development; this package remains in `experimental` until that support is complete, then moves to the `computer-use` group. Browser tabs, AX diffs, unassociated system panels and accessory-only apps are unsupported. -- Coordinates use window-local points including the title bar. PNG pixel dimensions can differ. Window movement, application changes and other desktop users can race observations and input. -- macOS Stage Manager can reduce background window bounds and screenshots to thumbnails. Full-window observation and coordinate input in this state are not guaranteed; model guidance requires explaining foreground needs and avoiding repeated focus reclamation. -- Background input depends on private SkyLight symbols; macOS updates can make them unavailable. Validate each target OS version. -- Observed failures include dispatched Unicode text not appearing in Blender and background Command+A not selecting all text in AppKit. Inspect application state after input; a successful tool result confirms dispatch only. -- Native binaries require a local build. Signing, distribution, and system-permission onboarding remain deferred. -- Persistent PTC is an external dependency assumption, not an implementation in this package. Real-model task success requires separate evaluation beyond native and protocol fixtures. +- Native execution currently uses the macOS provider. Windows support is in development in a separate directory. +- Persistent variables do not reserve an application across a sequence of operations. Other Agents and the user can change its state. +- The package stays in `experimental` until Windows support and platform validation permit promotion to `packages/computer-use/cua`. ----- ### Dev Note -
-Working context for maintainers - None. - -
diff --git a/packages/experimental/cua/README.zh.md b/packages/experimental/cua/README.zh.md index 7c143ceabe..2434c38bc5 100644 --- a/packages/experimental/cua/README.zh.md +++ b/packages/experimental/cua/README.zh.md @@ -1,6 +1,6 @@ --- -description: "通过按应用和窗口限定的对象,观察和操作原生 macOS 应用。" -kind: "package-reference" +description: "复用 Computer Use 应用和窗口对象,通过已授权并记录日志的平台操作完成任务。" +kind: "package-library" --- # @deepseek-ai/dsh-cua @@ -9,13 +9,13 @@ kind: "package-reference" ## 概述 -检查 macOS 权限,选择运行中的应用和窗口,读取辅助功能树、截图并发送输入。外部持久执行环境可以保留这些对象。Swift 辅助进程直接使用 macOS 框架,并改写选取的 MIT 许可 Cua Driver 行为。本包不提供 PTC 运行时或变量持久化。 +检查应用、跨 PTC 调用保留窗口对象,并通过正常工具执行链发送动作。平台提供方负责原生执行;本库负责对象 API、请求校验、应用授权和图像接纳。变量持久性由外部 PTC 运行时负责。 ## 目录 - [使用本包](#use-this-package) - [理解实现](#understand-the-implementation) -- [进一步阅读](#further-exploration) +- [进一步探索](#further-exploration) - [模型体验](#model-experience) - [已知限制与后续工作](#known-limitations-and-deferred-work) - [开发备注](#dev-note) @@ -25,81 +25,15 @@ kind: "package-reference" ## 使用本包 -该实验性提供方需要在 macOS 14 及以上通过源码配置显式启用。Windows 支持正在开发中。默认配置不加载它。先准备原生组件,再加载注册表和提供方覆盖层: +挂载 [macOS 提供方](../cua-macos/README.zh.md)以启用原生操作。外部 PTC 宿主从 `@deepseek-ai/dsh-cua/bridge` 导入 `connectCua`,并提供当前 `tools.cua` 绑定。源码组合与原生组件准备由提供方负责。 -```sh -swift build --package-path packages/experimental/cua/native --configuration release -cua_native_root="$PWD/packages/experimental/cua/native" -export DSH_CUA_HELPER="$cua_native_root/.build/release/dsh-cua-native" -pnpm dsh headless --patch packages/experimental/cua/examples/macos.cordis.yml --dump-config -``` +单窗口任务直接使用 App 方法。`getApp` 读取初始辅助功能状态;需要指定窗口时使用 `getWindows` 和 `getWindow`。重新读取一个应用或窗口范围会替换其中的元素编号,不使其他窗口的快照失效。键盘输入跟随应用焦点。 -配置输出命令不发起模型请求。执行真实任务时,将 `--dump-config` 替换为任务提示,并配置该 profile 的模型凭据。配置挂载 computer-use 注册表和本 provider;使用前需移除已有的竞争 provider。负责执行的宿主或辅助进程需要辅助功能权限来读取 AX 和输入,需要屏幕录制权限来截图。`getCapabilities` 查询权限和锁屏状态,不弹出系统授权窗口。桌面会话锁定或不可用时,界面观察与输入会在分派前拒绝,需由用户解锁 macOS 后继续。`listApps` 和 `cua.getState` 列出运行中的应用。`findApps(query)` 按名称、bundle ID 或绝对路径搜索 macOS 标准应用目录。`getApp` 按唯一名称、bundle ID、路径或运行中的 PID 选择应用,取得应用许可后,在需要时后台启动尚未运行的应用。启动前先确认可执行文件身份,授权后在启动前后再次核对;未签名代码只取得任务许可。返回的对象读取初始 AX,不自动激活应用。启动被取消时,应用可能已经运行。模型指引要求先在后台观察和输入,不在初始化或每次动作前激活应用。窗口隐藏或最小化时,先尝试 `app.show()` 或 `window.show()`,再重新观察。它请求取消隐藏和恢复最小化,不主动激活、抬起窗口或切换桌面空间;应用级恢复选择焦点窗口、主窗口或唯一 AX 窗口。结果通过 `windowId` 指明选中的窗口,未选中时为 null;`foregroundChanged` 比较操作前后的前台应用和最上层窗口,无法取得完整观察时为 null,返回后仍可能发生焦点变化。恢复失败或窗口仍不可用时,说明需要前台的步骤,再调用 `app.activate()` 或窗口的 `raise` 动作并重新观察。必须使用前台时,以完成任务为先。应用收到输入后仍可能改变焦点,后台投递不保证用户操作始终不受干扰。 +观察方法会输出并返回结果。使用 `void await app.getAXState()` 只展示一份内容,或使用 `emit:false` 后打印一次返回文本。截图返回 PNG 字节,并进入一次附件处理链;`emit:false` 只抑制对象层的输出。 -| 字段 | 默认值 | 含义 | -| --- | --- | --- | -| `allowedApps` | `[]` | 当前配置档的动态应用授权,绑定可执行文件路径和已验证的代码签名要求。 | -| `helperPath` | 包内可执行文件 | 可选的原生可执行文件绝对路径覆盖。 | -| `buildTimeoutMs` | `300000` | 原生组件准备的最长时间。 | -| `timeoutMs` | `30000` | 单次操作截止时间,取消后可能已发生部分输入。 | -| `connectTimeoutMs` | `5000` | 连接或启动共享 helper 的截止时间。 | -| `screenshotTimeoutMs` | `5000` | 截图总期限,包含排队时间,且不超过 `timeoutMs`。 | -| `axTimeoutMs` | `3000` | 每次 Accessibility 系统调用的截止时间,与完整操作截止独立配置。 | -| `maxRequestBytes` / `maxResponseBytes` | `1048576` / `33554432` | UTF-8 请求与响应字节限制。 | -| `maxClipboardBytes` | `33554432` | 粘贴时保存原剪贴板内容的字节上限;超限会在替换前拒绝。 | -| `maxAppMatches` | `20` | 已安装应用搜索的结果上限;范围过大的查询会被拒绝,需要缩小名称范围。 | -| `maxElements` / `maxDepth` | `2000` / `25` | 辅助功能树遍历限制。 | -| `dragDurationMs` / `dragSteps` | `400` / `20` | 拖动时长和移动分段数。 | -| `textEventIntervalMs` | `0.25` | 每个 Unicode 按下或抬起事件后的间隔,范围为 0 至 1000 毫秒。组合键的节奏独立。 | +应用授权发生在启动、观察、截图和输入之前。任务许可绑定已选择的应用实例;长期许可要求平台验证可执行文件身份。撤销会关闭受影响的 Agent 连接,因此旧对象必须重新选择目标。取消、超时或重新连接后均不重放输入。 -[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-cua)列出全部字段。原生工具调用使用 `{method, params}`,模型可以看到每个方法的完整参数定义。例如,`getApp` 返回 `listWindows`、`getState` 和输入操作所需的 `appHandle`。 - -数字键盘名称(`KP_0` 到 `KP_9`、`KP_Enter` 和数字键盘运算键)保留独立的物理键码。组合键用 `+` 分隔修饰键与按键。加号键写作 `+`,Command 加号写作 `cmd++`,也可以使用键名 `plus`;`cmd+` 这类缺少按键的输入会被拒绝。 - -Agent 销毁和插件卸载会取消并等待完整工具调用,包括图像接入检查和附件存储。已经开始的图像写入必须结束后,销毁才会返回;已取消的调用不会返回成功截图。 - -CUA 工具在读取 AX、窗口标题、截图、激活和输入之前检查应用授权。User Questions 提供任务级允许和拒绝选项;应用签名可验证且组合支持配置持久化时,也可保存授权。任务授权绑定所选进程句柄,长期授权绑定真实可执行路径和指定代码签名要求。拒绝后,同一任务不再反复询问。删除保存的 `allowedApps` 条目会取消相关操作并使原生组件中的句柄失效;重新授权需要重新选择应用。同一 Agent 连接中的其他句柄也会失效,已保存的授权仍可复用。Agent 或提供方销毁时等待对应连接的原生状态清理。设计理由见[应用授权决策](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.zh.md)。这些检查约束 CUA 调用;持久 PTC 和另行授权的 shell 工具保留各自的执行权限。 - -原始 `getState` 操作返回经过校验的状态字段,以及按本次披露选项生成的完整文本。原生 helper 还返回供原生检查使用的结构化元素诊断数组;工具投影会从模型文本和结构化工具值中移除这份重复数组。 - -AX 观察默认只展示菜单栏入口。使用 `app.getAXState({ includeMenus: true })` 展开菜单内容,使用 `app.getAXState({ details: true })` 读取标识符、全局 point 坐标和完整动作名。原始 `getState` 和 AX/截图组合观察也接受这两个选项。每次观察都会替换该范围的元素编号,展开后应使用新输出中的编号。详情不会绕过配置的节点数和深度上限;截断提示会指明触发的上限。 - -实际值与占位文字分别展示。有名称、状态或可用动作的分组和滚动区域保留独立行。同一节点中重复的名称、描述和帮助只显示一次。实际值、占位文字和 URL 即使文字相同也保留各自含义,不删除不同节点共有的正文。相关控件明确展示可写性,包括 `AXIncrementor`;`unknown` 表示系统未报告。缺少可选状态字段不代表 false。超出当前值长度的 UTF-16 选区标为不可用。自定义动作简称省略 AppKit 的 target/selector 诊断信息;同名动作需要使用 `details:true` 返回的完整名称。 - -观察方法默认自动输出,同时返回结果。在 persistent PTC 中使用 `void await app.getAXState()` 输出一次,或指定 `emit:false` 后自行打印一次。AX 输出后的截图可能单独失败,已成功取得的 AX 输出仍保留。 - -截图诊断使用带工具请求 ID 的 `[cua-screenshot]` 记录。原生阶段记录经本次 Agent 连接返回,覆盖窗口及 AX 几何检查、系统截图、PNG/Base64 编码和响应写出。Host 调试日志补充发送、响应和清理耗时。每次请求最多保留 8192 字节完整诊断记录,超限会明确标记;截图失败时附在错误中,保留到 Session trace。记录只含阶段、耗时和错误码,不含窗口标题、AX 正文或图片内容。 - -原生准备复用文件锁,串行检查编译和发布。构建更新包内产物,并从 native/.build/prepared 下按内容摘要区分的副本运行,保留旧源码进程使用的可执行文件。相同构建复用同一文件;保留的副本损坏时拒绝使用。取消或准备失败后,先完成受管进程清理再释放锁。 - -### 接入外部执行环境 - -使用 persistent PTC 时,在 macOS 配置后追加 `--patch packages/experimental/cua/examples/persistent-ptc.cordis.yml`。该 provider 为存活的 Agent 保留变量,并遵守当前选择的文件沙箱策略。调用方导入适配模块,并提供当前执行中已授权的 `tools.cua` 绑定。persistent PTC 会在 CUA 指引中收到安装目录内 bridge 的文件 URL,项目无须安装本包。下面的例子用于能从自身安装目录解析本包、且已提供 `tools` 的外部宿主;本包不安装全局对象,也不负责保留变量: - -```javascript -import { connectCua } from '@deepseek-ai/dsh-cua/bridge' -const cua = connectCua(() => tools.cua, text => console.log(text)) -const app = await cua.getApp("com.apple.TextEdit") -void await app.getScreenshot() -``` - -单窗口任务直接使用 app 方法。`getApp` 已读取初始 AX,截图不需要枚举窗口或额外读取 AX。任务需要指定窗口或默认选窗存在歧义时,再调用 `app.getWindows()` 和 `app.getWindow(windowId)`,根据观察到的标题与位置选择目标。 - -`getWindow` 会输出初始 AX 树。已确认归属但没有 AX 界面的窗口返回空快照,仍可截图和发送坐标输入。不同窗口分别保留快照,读取一个窗口不会使另一个窗口失效;重新读取同一范围会使旧索引失效。观察期间节点被移除时,该次读取返回 `STALE_SNAPSHOT`,不发布部分树;可以重新读取,但不要重复此前的输入。应用级截图和坐标操作复用最近一次 AX 观察:优先使用其中在屏幕上且尺寸非零的焦点窗口,否则要求只有一个符合条件的窗口。它们不会重新查询即时焦点;需要跟随焦点变化时,先读取 `app.getAXState()`。显式窗口对象保持选定的目标。键盘方法保留在 `app` 上,因为输入跟随应用的当前焦点。 - -`paste(text, { format: "text" | "html" })` 临时提供指定格式的剪贴板内容,并调用应用已启用的标准 Paste 菜单命令。临时格式保留到剪贴板读取方请求其数据,然后只在仍持有剪贴板时恢复保存的全部原格式,保留期间产生的新复制内容。粘贴操作共用跨进程锁,取消会等待辅助进程清理。强制终止进程可能阻止恢复。应用不支持 Paste 命令时明确失败,不自动改成逐字输入;`typeText` 仍单独提供按键事件输入。 - -`selectText` 通过可写的 `AXSelectedTextRange` 选中唯一的字面文本,或把光标放在它的前后。紧邻的前后文用于区分重复文本。选区操作保留其余文字和格式,并核对实际 UTF-16 范围;控件不支持、文本不存在、匹配不唯一或文字变化时明确失败,不自动改用键盘输入。AX 文本包含可用的 `selectionWritable`、选区范围和选中文字。 - -截图数据必须使用规范 Base64。响应字节上限在解析前执行,附件接入校验图像数据及配置的容量。 - -坐标点击、滚动和拖动要求先成功读取该窗口的 AX 或截图。窗口宽高与最近一次观察不同时,在输入前返回 `WINDOW_CHANGED`,原始调用即使不传快照 ID 也会检查;重新读取 AX 或截图后再操作。单纯移动窗口不使窗口内坐标失效。截图和鼠标输入会核对所选窗口的辅助功能框与显示边界;已知两者不一致时,在发送任何鼠标事件前返回 `WINDOW_CHANGED`。当 AX 保留完整窗口框时,这也会拦住 Stage Manager 变换后的窗口。需要重新读取状态;激活应用或抬起窗口仍须显式调用。元素声明的 AX 动作保持可用。缺少 AX 几何时保留无 AX 路径,不代表变换后的窗口一定可以操作。 - -保留的对象会在每次调用时获取当前工具绑定。外部运行时负责持久化、导入、取消和最终表达式值的序列化。截图返回 PNG 字节,标准工具链负责将图片作为附件传递一次。`emit: false` 只控制对象层的输出。使用其他分派器的宿主可以通过 `./api` 的 `createCuaApi` 创建对象。 - -AppKit sheet 可以通过其窗口 ID 单独选择,也会出现在所属文档窗口的控件树中。只有应用当前获得焦点的 sheet 指向唯一的 Apple 面板服务窗口,且该服务仍聚焦于同一面板时,键盘输入才转交系统打开或保存面板。无法确认或存在歧义的远端接收方会在输入前报错。 - -元素索引支持右键或中键单击以及左键单击、双击。点击和拖动选项可指定按键与修饰键;带修饰键的点击使用指针输入。拖动期间保持指定按键,并在移动和释放事件中携带修饰键。AX 文本标明禁用控件、`valueWritable=true` 和可读取的 `selected` 状态。不带修饰键的点击优先使用元素声明的辅助功能动作;需要发送鼠标事件时,会重新读取元素位置并核实所属窗口。动作返回只表示输入已分派。可视指针或点击标记可以独立绘制;AX 动作无需移动系统指针。窗口定向鼠标输入仍可能改变应用焦点、选区或对话框。输入后需要读取状态确认结果。`STALE_APP` 需要重新获取应用,`STALE_SNAPSHOT` 需要重新读取 AX。请求超时或取消会等待原生停止确认,保留仍可用的句柄;无法确认清理完成时关闭该 Agent 的连接。未返回系统回调的截图 RPC 占用唯一捕获槽位,新截图立即返回 `CAPTURE_BUSY`,AX 和输入仍可使用。迟到图片被丢弃,真实回调结束后释放槽位。只有操作无法停止或清理失败时,才可能使共享进程及所有旧句柄失效。不能自动重试结果不确定的输入。关闭 Agent 会释放其连接和 AX 引用,但不会撤销应用中的修改。 +动作返回 `dispatched:true, verified:false`;请读取新状态以验证结果。坐标操作要求先成功观察目标窗口。窗口缩放后以 `WINDOW_CHANGED` 拒绝旧坐标;单纯移动窗口不改变窗口内坐标。 ----- @@ -107,27 +41,21 @@ AppKit sheet 可以通过其窗口 ID 单独选择,也会出现在所属文档 ## 理解实现
-操作分派与资源归属 +共享执行与平台职责 -对象接口将应用和窗口方法转换为 JSON 操作。适配模块通过标准工具链保留授权、嵌套日志和图片接入。同一用户的相同 helper 构建由多个 Agent 和 Host 共享。每条连接独立保存句柄、快照、配置和取消状态,桌面操作通过全局队列执行。受管启动器将服务生命期交给这些连接,最后一条连接清理完成后服务退出。进程锁覆盖构建身份及真实可执行路径,防止重复启动或覆盖运行版本后产生路径冲突。原生句柄绑定进程身份及内核启动时间,快照中的 AX 引用只保存在辅助进程内。未来平台可以实现相同操作协议,无需在对象接口中加入平台逻辑。 +提供方将准备好的 helper、连接工厂和指引传给 [installCua](src/index.ts)。[NativeClient](src/native-client.ts) 负责请求关联、字节上限、取消确认和失败处理。平台连接器负责启动和连接清理。共享执行器为每个 Agent 保留一个连接,并在销毁时等待进行中的图像写入。 -[Zod 操作定义](src/protocol.ts)负责模型参数,并单独声明 Host 资源字段和内部续接操作。公共数据类型从这些定义推导。`pnpm exec tsx packages/experimental/cua/native/generate-protocol.ts` 生成原生 JSON Schema 和 Swift 字段、枚举校验表;包测试检查产物是否同步,并验证模型不能提交 Host 专属字段。原生目标验证、资源上限和操作行为仍在执行处检查。 - -[AX 文本格式化](native/Sources/CuaNative/AXTextFormatter.swift)只接收已读取的节点数据。AX 遍历器独自分配索引并保存原生引用,格式化不维护另一套快照或目标注册表。 - -原生请求 ID 与工具日志中的调用 ID 一致。成功的激活和窗口抬起操作会在原始工具结果中保留前台操作信息,以及操作前后的即时焦点观察。应用已在前台时跳过重复激活;窗口抬起还要求该窗口正是焦点主窗口、未最小化且位于可见普通窗口的最前方,才会跳过。无法确认时保留原请求。这些观察不能确认其他焦点切换的发起者,也不代表操作已经完成。 - -每次观察在同一批 AX 请求中读取节点角色和元数据。组节点和滚动区域节点只提供子节点,未使用的元数据错误不会导致它们被拒绝。不支持批量读取时保留逐个属性读取。AX 遍历对每个原生身份只访问一次,包括表示同一元素的不同代理引用,避免循环子节点重复展开整个子树。[源码清单](native/upstream.json)记录改写的上游行为和本地 Swift 模块。AppKit 使用主事件循环,独立串行执行器持有 AX 引用并发送同步输入。一条请求在截图和结果发送期间仍独占执行。本包不发布 invariant 入口,因为没有需要对照的独立注册表投影或持久执行器状态。协议验证、目标身份和资源清理由对应实现负责。 +[操作 schema](src/protocol.ts) 定义模型参数,并单独声明 Host 专用字段。原生实现在执行时检查目标身份、窗口几何、资源上限和操作系统访问条件。本库没有独立投影的注册表或持久状态,因此不发布 invariant 模块。
----- -## 进一步阅读 +## 进一步探索 -- [Computer-use 注册](../../computer-use/computer-use/README.zh.md) -- [原生协议](native/protocol.schema.json) +- [macOS 提供方](../cua-macos/README.zh.md) +- [Windows 实现范围](../cua-windows/README.zh.md) - [对象接口](src/types.ts) ----- @@ -135,84 +63,31 @@ AppKit sheet 可以通过其窗口 ID 单独选择,也会出现在所属文档 ## 模型体验 -### 原生应用操作 +### 已授权的应用操作 #### 模型可见内容 -插件提供一个 `cua` 操作工具,定义各方法的参数。标准 PTC 呈现通过 `run_code` 将它暴露为 `tools.cua`。以下是本包逐字输出的指导文本: - -##### 操作指导文本(原文) - -```markdown -Use cua with method getCapabilities and params {} to inspect permissions and sessionLocked. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI attempts and ask the user to unlock macOS. A null sessionLocked means no usable console session was verified. After unlocking, read fresh state; screenshots from a locked desktop are not current observations. Use listApps or cua.getState to discover running applications, and findApps(query) to search installed applications by name, bundle ID or absolute path. getApp requests user consent before launching a stopped application in the background, or before observations and input on a running one. If access is denied or revoked, stop using that application until the user explicitly grants access again. Never switch tools to bypass a refusal. Keep the returned appHandle; every app operation requires it. listWindows returns window IDs and point bounds. Raw getState returns AX text and snapshotId. Menu-bar entries are shown by default; includeMenus=true expands their contents. details=true adds identifiers, global-point bounds and complete action names. Both options also apply to getAXState. Each read replaces the indices for that app/window scope. Use its element indices only with that snapshotId. Raw screenshot and coordinate operations require a windowId. Coordinates are window-local macOS points, which may differ from screenshot pixels. A verified window can expose no AX elements. Its empty snapshot permits screenshots and coordinate input; element-index actions require an element from a fresh AX observation. - -Initialize the object API once in persistent PTC. Reuse its variables in later calls; put temporary declarations inside { ... } blocks to avoid redeclaration errors. All object methods are async. getAXState returns text, not a structured state object. Its text separates values and placeholders, and reports subroles, help, URLs, disabled/focused/selected/expanded state and UTF-16 selection ranges when available. Writability is true, false or unknown on relevant controls; omitted state fields do not assert false. selection=unavailable means the reported range exceeds the observed text. Compact output omits ordinary Press/Pick (click), ShowMenu (right click) and ScrollToVisible actions; details=true lists every available action. Use the displayed action name with performSecondaryAction; ambiguous aliases require the complete name from details=true. Truncation reports the reached provider limit. Narrow the window or increase that provider limit before reading again. - -Use app methods directly for single-window tasks; getApp already reads initial AX state. Call app.getWindows() and await app.getWindow(windowId) only to target a particular window or resolve ambiguous window selection. App screenshots and coordinate actions use the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not refresh focus automatically. Read app.getAXState() to follow changed focus. - -Object API signatures (options default to { emit: true }): - -- cua.getCapabilities(options?) -> { platform, permissions, sessionLocked, methods, coordinateSpace, browser } -- cua.listApps(options?) -> Array<{ id, displayName, isRunning: true, pid }> -- cua.findApps(query: string, options?) -> Array<{ id, displayName, path, isRunning, pid? }> -- cua.getState(options?) -> { apps, browsers: [] } -- cua.getApp(selector: string) -> App -- app.activate() -> { dispatched: true, verified: false } -- app.getWindows(options?) -> Array<{ windowId, pid, appName, title, bounds, isOnScreen, onCurrentSpace }> -- app.getWindow(windowId: number) -> Window -- app.typeText(text: string), app.pressKey(chord: string) -> action result -- app.paste(text: string, options?: { format?: "text" | "html" }) -> action result - -App and Window methods: - -- show() -> { dispatched: true, verified: false, windowId: number | null, foregroundChanged: boolean | null } -- getAXState(options?: { emit?: boolean, disableDiffing?: boolean, includeMenus?: boolean, details?: boolean }) -> string -- getScreenshot(options?: { emit?: boolean }) -> Uint8Array -- getAXStateAndScreenshot(options?) -> { state: string, screenshot: Uint8Array } -- click(target: number | [x, y], options?: { mouseButton?: "left" | "right" | "middle", clickCount?: 1 | 2, modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result -- setValue(elementIndex: number, value: string) -> action result -- selectText(elementIndex: number, text: string, options?: { prefix?: string, suffix?: string, selectionType?: "text" | "cursor_before" | "cursor_after" }) -> action result -- performSecondaryAction(elementIndex: number, action: string) -> action result -- scroll({ x, y, deltaLinesX, deltaLinesY }) -> action result -- drag(from: [x, y], to: [x, y], options?: { mouseButton?: "left" | "right" | "middle", modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result - -Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input. - -Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Coordinate clicks, scrolling and dragging require a successful AX observation or screenshot of that window. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input. - -Use selectText only on controls advertising selectionWritable. It selects a unique literal occurrence, or places the cursor before/after it; prefix and suffix must be immediately adjacent. AMBIGUOUS_TEXT requires more context; TEXT_NOT_FOUND or SELECTION_CHANGED requires a fresh observation before deciding on input. Left clicks support single/double clicks; right and middle clicks support single clicks. Modified clicks use pointer input. Keypad names such as KP_0 and KP_Enter retain their physical key identity. Window objects expose observation and pointer/AX actions; keyboard input remains app-scoped and follows current focus. Scroll deltas are integer lines from -10 to 10; positive values reveal content above or left. Paste delivers plain text or HTML through the application's enabled standard Paste menu command. Original clipboard formats are restored if the clipboard still belongs to this paste; a later copy is preserved. Unsupported Paste commands fail without typing fallback. CLIPBOARD_CHANGED means another writer intervened; observe before deciding on further input. Browser applications can be controlled with getApp like other native apps; browser tabs are unavailable. Installed-app search covers standard macOS application directories; use an absolute application path for another location. Launch does not restore a window or document, and cancellation may leave an application already started. The execution host must install the object API and own variable persistence; this plugin does not preload a cua global. In persistent PTC, use the host-provided initialization snippet below once. Its installation-owned file URL works independently of the project directory. Retain app objects between calls. Observation methods emit by default and also return their value. Use void await app.getAXState() to avoid a duplicate REPL completion, or pass emit:false and print the returned text once. End screenshot calls with void await app.getScreenshot() or void await app.getAXStateAndScreenshot() so image bytes are not serialized as the program completion value. -``` +平台提供方挂载共享的 `cua` 工具并提供操作指引。调用和观察使用同一套应用与窗口接口,进入正常 Session 历史。平台指引见 [macOS 提供方](../cua-macos/README.zh.md#model-experience)。 #### Token 影响 -启用插件会增加固定参数定义和指导文本。AX 观察、操作结果和已接入的图片会增加模型输入。本包不将保留的对象序列化进提示。 +操作结果和接纳的图像增加模型输入。保留的 JavaScript 对象不会序列化进提示词。调用方重复打印会重复观察文本。 #### KV Cache 影响 -工具结果追加到 Session 历史。修改插件配置或工具呈现方式可能改变请求前缀;保留应用对象不会改变已有历史。 +结果追加到历史。保留对象不重写已有消息;更改挂载的工具或指引可能改变请求前缀。 ## 已知限制与后续工作 -队列只排序单次原生请求。持有 app 对象不会为 Session 保留应用,跨多个请求的应用级占用需要单独设计。一直不返回的系统截图 RPC 会占用唯一捕获槽位,重新创建 app 对象不会重置该系统连接。 - -- 只提供 macOS 原生实现,Windows 支持正在开发中。本包暂放在 `experimental`,待 Windows 支持完成后迁回 `computer-use` 组。不支持浏览器标签、启动应用、AX 差分、无法确认应用归属的系统面板,以及仅后台辅助类型的应用。 -- 坐标使用包含标题栏的窗口内部 point,可能不同于 PNG 像素尺寸。窗口移动、应用变化及其他桌面使用者的操作可能与观察和输入交错。 -- macOS 台前调度可能将后台窗口的尺寸与截图变为缩略图,当前无法保证这种状态下的完整画面与坐标输入;模型指引要求说明前台需求并避免反复抢回焦点。 -- 后台输入依赖私有 SkyLight 符号,macOS 升级可能使其不可用,需在目标系统版本验证。 -- 实测 Blender Unicode 输入曾出现分派成功但界面未显示文本,后台 AppKit 的 Command+A 曾未选中全文。输入后必须检查应用状态;工具返回成功只表示已分派。 -- 原生二进制需要本地构建。签名、分发和系统权限引导留待后续。 -- 持久 PTC 是对外部运行环境的假设,本包不实现它。真实模型任务成功率需要在原生测试和协议测试之外单独评估。 +- 当前原生执行使用 macOS 提供方。Windows 支持在独立目录中开发。 +- 持久变量不会在一组连续操作期间独占应用。其他 Agent 和用户可能改变其状态。 +- 本包保留在 `experimental`,Windows 支持和平台验证完成后可迁至 `packages/computer-use/cua`。 ----- ### 开发备注 -
-维护者工作背景 - 无。 - -
diff --git a/packages/experimental/cua/package.json b/packages/experimental/cua/package.json index e241e86b86..a5bdf4bcca 100644 --- a/packages/experimental/cua/package.json +++ b/packages/experimental/cua/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-cua", - "description": "macOS computer use through application and window objects", + "description": "Shared Computer Use application objects, authorization, and logged tool execution", "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" @@ -27,21 +27,22 @@ "./bridge": { "types": "./lib/types/bridge.d.ts", "default": "./lib/bridge.js" + }, + "./native-client": { + "types": "./lib/types/native-client.d.ts", + "default": "./lib/native-client.js" + }, + "./protocol": { + "types": "./lib/types/protocol.d.ts", + "default": "./lib/protocol.js" } }, "files": [ "lib/index.js", - "native/bin", "lib/api.js", "lib/bridge.js", - "native/Package.swift", - "native/Sources", - "native/tests/swift", - "native/tests/test_swift.py", - "native/LICENSE.cua-driver", - "native/upstream.json", - "native/README.txt", - "native/protocol.schema.json", + "lib/native-client.js", + "lib/protocol.js", "lib/types/**/*.d.ts" ], "license": "MIT", @@ -93,8 +94,5 @@ "@deepseek-ai/dsh-config-editor": "workspace:*", "@deepseek-ai/dsh-hmr": "workspace:*", "@deepseek-ai/cordis-plugin-timer": "workspace:~" - }, - "scripts": { - "build:native": "swift build --package-path native --configuration release" } } diff --git a/packages/experimental/cua/src/index.ts b/packages/experimental/cua/src/index.ts index 581ae0e996..7e5230c626 100644 --- a/packages/experimental/cua/src/index.ts +++ b/packages/experimental/cua/src/index.ts @@ -1,19 +1,17 @@ -/** macOS computer use through Agent-scoped connections to a shared native helper. */ -import { isAbsolute, join, resolve } from 'node:path' +/** Shared CUA tool execution, application consent, and object-runtime integration. */ +import { join } from 'node:path' import { tmpdir } from 'node:os' import { withFileLock } from '@deepseek-ai/dsh-atomic-write' -import { fileURLToPath } from 'node:url' import type { Context, Volatile } from '@deepseek-ai/cordis' import Schema from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' import { ComputerUseProviderName } from '@deepseek-ai/dsh-computer-use/brand' import { createMcpToolDefinition } from '@deepseek-ai/dsh-mcp-client' import { z } from 'zod' -import { NativeClient, NativeError, type NativeClientConfig } from './native-client.ts' +import { NativeClient, NativeError, type NativeClientConfig, type NativeChannelFactory } from './native-client.ts' import { ApplicationAuthorizationResult, ApplicationAccess } from './application-access.ts' import type { CuaApplicationGrant, CuaApplicationId } from './application-types.ts' import type { CuaAppHandle } from './types.ts' -import { nativeHostArchitecture, prepareNativeExecutable } from './native-build.ts' import { AppResult, Request, ScreenshotResult, StateResult } from './protocol.ts' import type {} from '@deepseek-ai/dsh-system-prompt' import type { ToolExecution } from '@deepseek-ai/dsh-tools' @@ -23,13 +21,9 @@ import type {} from '@deepseek-ai/dsh-ptc-runtime' import type {} from '@deepseek-ai/dsh-settings' import type {} from '@deepseek-ai/cordis-plugin-loader' -/** Loader plugin identity. */ -export const name = 'cua' -/** Services required for logged tools and native launcher ownership. */ -export const inject = ['computerUse', 'tools', 'systemPrompt', 'subprocess', 'agents'] /** Native executable and per-operation resource bounds. */ -export interface Config { +export interface CuaConfig { /** Signed applications explicitly approved for future tasks in this profile. */ allowedApps: Volatile & { /** Unbranded digest at the profile configuration boundary; admitted by the Config schema. */ @@ -70,7 +64,7 @@ export interface Config { } /** Validated host configuration; no driver is downloaded at runtime. */ -export const Config = Schema.object({ +export const CuaConfig = Schema.object({ allowedApps: Schema.array(Schema.object({ id: Schema.string().pattern(/^[a-f0-9]{64}$/).required(), name: Schema.string().required(), executablePath: Schema.string().min(1).required(), @@ -91,46 +85,8 @@ export const Config = Schema.object({ graceMs: Schema.number().step(1).min(1).max(2 ** 31 - 1).default(3000), maxElements: Schema.number().step(1).min(1).max(10000).default(2000), maxDepth: Schema.number().step(1).min(1).max(100).default(25), -}) satisfies Schema, Config> +}) satisfies Schema, CuaConfig> -/** Instructions for raw operation calls and optional object-API bindings. */ -export const GUIDANCE = `Use cua with method getCapabilities and params {} to inspect permissions and sessionLocked. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI attempts and ask the user to unlock macOS. A null sessionLocked means no usable console session was verified. After unlocking, read fresh state; screenshots from a locked desktop are not current observations. Use listApps or cua.getState to discover running applications, and findApps(query) to search installed applications by name, bundle ID or absolute path. getApp requests user consent before launching a stopped application in the background, or before observations and input on a running one. If access is denied or revoked, stop using that application until the user explicitly grants access again. Never switch tools to bypass a refusal. Keep the returned appHandle; every app operation requires it. listWindows returns window IDs and point bounds. Raw getState returns AX text and snapshotId. Menu-bar entries are shown by default; includeMenus=true expands their contents. details=true adds identifiers, global-point bounds and complete action names. Both options also apply to getAXState. Each read replaces the indices for that app/window scope. Use its element indices only with that snapshotId. Raw screenshot and coordinate operations require a windowId. Coordinates are window-local macOS points, which may differ from screenshot pixels. A verified window can expose no AX elements. Its empty snapshot permits screenshots and coordinate input; element-index actions require an element from a fresh AX observation. - -Initialize the object API once in persistent PTC. Reuse its variables in later calls; put temporary declarations inside { ... } blocks to avoid redeclaration errors. All object methods are async. getAXState returns text, not a structured state object. Its text separates values and placeholders, and reports subroles, help, URLs, disabled/focused/selected/expanded state and UTF-16 selection ranges when available. Writability is true, false or unknown on relevant controls; omitted state fields do not assert false. selection=unavailable means the reported range exceeds the observed text. Compact output omits ordinary Press/Pick (click), ShowMenu (right click) and ScrollToVisible actions; details=true lists every available action. Use the displayed action name with performSecondaryAction; ambiguous aliases require the complete name from details=true. Truncation reports the reached provider limit. Narrow the window or increase that provider limit before reading again. - -Use app methods directly for single-window tasks; getApp already reads initial AX state. Call app.getWindows() and await app.getWindow(windowId) only to target a particular window or resolve ambiguous window selection. App screenshots and coordinate actions use the latest AX observation: its focused on-screen, nonzero-size window, otherwise the sole such window. They do not refresh focus automatically. Read app.getAXState() to follow changed focus. - -Object API signatures (options default to { emit: true }): - -- cua.getCapabilities(options?) -> { platform, permissions, sessionLocked, methods, coordinateSpace, browser } -- cua.listApps(options?) -> Array<{ id, displayName, isRunning: true, pid }> -- cua.findApps(query: string, options?) -> Array<{ id, displayName, path, isRunning, pid? }> -- cua.getState(options?) -> { apps, browsers: [] } -- cua.getApp(selector: string) -> App -- app.activate() -> { dispatched: true, verified: false } -- app.getWindows(options?) -> Array<{ windowId, pid, appName, title, bounds, isOnScreen, onCurrentSpace }> -- app.getWindow(windowId: number) -> Window -- app.typeText(text: string), app.pressKey(chord: string) -> action result -- app.paste(text: string, options?: { format?: "text" | "html" }) -> action result - -App and Window methods: - -- show() -> { dispatched: true, verified: false, windowId: number | null, foregroundChanged: boolean | null } -- getAXState(options?: { emit?: boolean, disableDiffing?: boolean, includeMenus?: boolean, details?: boolean }) -> string -- getScreenshot(options?: { emit?: boolean }) -> Uint8Array -- getAXStateAndScreenshot(options?) -> { state: string, screenshot: Uint8Array } -- click(target: number | [x, y], options?: { mouseButton?: "left" | "right" | "middle", clickCount?: 1 | 2, modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result -- setValue(elementIndex: number, value: string) -> action result -- selectText(elementIndex: number, text: string, options?: { prefix?: string, suffix?: string, selectionType?: "text" | "cursor_before" | "cursor_after" }) -> action result -- performSecondaryAction(elementIndex: number, action: string) -> action result -- scroll({ x, y, deltaLinesX, deltaLinesY }) -> action result -- drag(from: [x, y], to: [x, y], options?: { mouseButton?: "left" | "right" | "middle", modifiers?: Array<"command" | "control" | "option" | "shift" | "fn"> }) -> action result - -Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input. - -Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Coordinate clicks, scrolling and dragging require a successful AX observation or screenshot of that window. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input. - -Use selectText only on controls advertising selectionWritable. It selects a unique literal occurrence, or places the cursor before/after it; prefix and suffix must be immediately adjacent. AMBIGUOUS_TEXT requires more context; TEXT_NOT_FOUND or SELECTION_CHANGED requires a fresh observation before deciding on input. Left clicks support single/double clicks; right and middle clicks support single clicks. Modified clicks use pointer input. Keypad names such as KP_0 and KP_Enter retain their physical key identity. Window objects expose observation and pointer/AX actions; keyboard input remains app-scoped and follows current focus. Scroll deltas are integer lines from -10 to 10; positive values reveal content above or left. Paste delivers plain text or HTML through the application's enabled standard Paste menu command. Original clipboard formats are restored if the clipboard still belongs to this paste; a later copy is preserved. Unsupported Paste commands fail without typing fallback. CLIPBOARD_CHANGED means another writer intervened; observe before deciding on further input. Browser applications can be controlled with getApp like other native apps; browser tabs are unavailable. Installed-app search covers standard macOS application directories; use an absolute application path for another location. Launch does not restore a window or document, and cancellation may leave an application already started. The execution host must install the object API and own variable persistence; this plugin does not preload a cua global. In persistent PTC, use the host-provided initialization snippet below once. Its installation-owned file URL works independently of the project directory. Retain app objects between calls. Observation methods emit by default and also return their value. Use void await app.getAXState() to avoid a duplicate REPL completion, or pass emit:false and print the returned text once. End screenshot calls with void await app.getScreenshot() or void await app.getAXStateAndScreenshot() so image bytes are not serialized as the program completion value.` function executionAgent(execution: Readonly): Agent { if (!execution.agent) throw new Error('dsh-cua requires an agent-owned execution') @@ -141,36 +97,23 @@ interface CuaExecution extends ToolExecution { nativeClient: NativeClient } -/** - * Mount logged native operations with separate Agent connections to the shared helper. - * @param ctx - Host context with computer-use, subprocess and tool services. - * @param config - Validated native preparation and operation bounds. - * @returns Completion after native preparation and tool registration. - */ -export async function apply(ctx: Context, config: Config): Promise { - if (config.helperPath !== undefined) { - if (!isAbsolute(config.helperPath)) throw new Error('dsh-cua helperPath must be absolute') - mount(ctx, config, config.helperPath) - return - } - const architecture = nativeHostArchitecture(process.platform, process.arch) - const controller = new AbortController() - const prepared = prepareNativeExecutable({ - packageRoot: resolve(fileURLToPath(new URL('..', import.meta.url))), - architecture, subprocess: ctx.subprocess, - timeoutMs: config.buildTimeoutMs, graceMs: config.graceMs, signal: controller.signal, - }) - ctx.effect(() => async () => { - controller.abort() - // The Loader receives preparation failures through apply; disposal only awaits quiescence. - await prepared.catch(() => {}) - }, 'dsh-cua.native-preparation') - const helperPath = await prepared - controller.signal.throwIfAborted() - mount(ctx, config, helperPath) + +/** Prepared platform executable, transport, and platform-specific operating instructions. */ +export interface CuaBackend { + helperPath: string + createChannel: NativeChannelFactory + guidance: string + description: string } -function mount(ctx: Context, config: Config, helperPath: string): void { +/** + * Install authorized CUA calls and Agent-owned native connections. + * @param ctx - Provider context with computer-use, subprocess, tools, prompt, and Agent services. + * @param config - Validated resource limits and live application grants. + * @param backend - Platform-owned executable, connection factory, and instructions. + */ +export function installCua(ctx: Context, config: CuaConfig, backend: CuaBackend): void { + const { helperPath } = backend const native: NativeClientConfig = { helperPath, cwd: process.cwd(), timeoutMs: config.timeoutMs, axTimeoutMs: config.axTimeoutMs, connectTimeoutMs: config.connectTimeoutMs, screenshotTimeoutMs: config.screenshotTimeoutMs, @@ -237,7 +180,7 @@ function mount(ctx: Context, config: Config, helperPath: string): void { } const definition = createMcpToolDefinition(ctx, { name: 'cua', rawName: 'cua', - description: 'Inspect macOS permissions, select an application and window, read AX state, capture screenshots or dispatch input. Use appHandle from getApp; verify actions by reading fresh state.', + description: backend.description, inputSchema: { ...z.record(z.string(), z.json()).parse(z.toJSONSchema(Request)), type: 'object' }, async call(args, execution) { // The owning execute callback admits the request before entering this private MCP adapter. @@ -299,7 +242,7 @@ function mount(ctx: Context, config: Config, helperPath: string): void { throw new NativeError('APP_ACCESS_REQUIRED', 'Select the application with getApp before reading or controlling it') } if (!client || (client.disposed && !('appHandle' in request.params))) { - client = new NativeClient(ctx, native, (message) => { ctx.logger.debug(message) }) + client = new NativeClient(ctx, native, backend.createChannel, (message) => { ctx.logger.debug(message) }) clients.set(agent, client) } const nativeClient = client @@ -332,7 +275,7 @@ function mount(ctx: Context, config: Config, helperPath: string): void { }) yield ctx.systemPrompt.section({ name: 'computer-use:dsh-cua', order: ctx.systemPrompt.getSectionOrder('TOOL_COMPUTER_USE'), - text: () => ctx.get('ptcRuntime')?.programFormat !== 'repl' ? GUIDANCE : `${GUIDANCE} + text: () => ctx.get('ptcRuntime')?.programFormat !== 'repl' ? backend.guidance : `${backend.guidance} Persistent PTC initialization: var { connectCua } = await import(${JSON.stringify(new URL('../lib/bridge.js', import.meta.url).href)}); diff --git a/packages/experimental/cua/src/native-client.ts b/packages/experimental/cua/src/native-client.ts index fcb2c1e244..a61ff18bf1 100644 --- a/packages/experimental/cua/src/native-client.ts +++ b/packages/experimental/cua/src/native-client.ts @@ -5,7 +5,7 @@ import type { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess' import type { ToolExecution } from '@deepseek-ai/dsh-tools' import type { JsonValue } from '@deepseek-ai/dsh-util-values' import { z } from 'zod' -import { createSharedNativeChannel, type NativeChannel } from './shared-native.ts' +import type { Readable, Writable } from 'node:stream' const Reply = z.union([ z.object({ id: z.string(), result: z.json() }).strict(), @@ -45,6 +45,26 @@ export interface NativeClientConfig { textEventIntervalMs: number } +/** One Agent's ordered connection to native operations and cleanup acknowledgement. */ +export interface NativeChannel { + readonly stdin: Writable + readonly stdout: Readable + readonly done: Promise + /** Stop writes and release this connection's native state. */ + close(): void + /** @returns Completion after the connection and its owned startup processes close. */ + waitForClose(): Promise +} +/** + * Open the platform connection with the configured resource bounds. + * @param owner - Managed subprocess provider for startup and cleanup. + * @param config - Executable, operation limits, and deadlines. + * @returns Agent-owned native byte stream and its cleanup acknowledgement. + */ +export type NativeChannelFactory = ( + owner: { subprocess: Pick }, config: NativeClientConfig, +) => NativeChannel + /** One Agent owns this connection and its native handles; other Agents use independent connections. */ export class NativeClient { private readonly handle: NativeChannel @@ -61,12 +81,13 @@ export class NativeClient { /** * @param owner - Managed subprocess provider. * @param config - Executable, transport bounds and deadlines. + * @param createChannel - Platform-owned connection and process lifecycle. * @param reportDiagnostic - Optional Host diagnostic sink; never receives captured content. */ - constructor(owner: { subprocess: Pick }, config: NativeClientConfig, + constructor(owner: { subprocess: Pick }, config: NativeClientConfig, createChannel: NativeChannelFactory, private readonly reportDiagnostic?: (message: string) => void) { this.config = config - this.handle = createSharedNativeChannel(owner, config) + this.handle = createChannel(owner, config) this.handle.stdout.on('data', (chunk: Buffer) => { this.receive(chunk) }) this.handle.stdout.on('error', (error: Error) => { this.fail(new NativeError('NATIVE_IO', `Native connection failed: ${error.message}`, { cause: error })) }) this.handle.stdout.on('end', () => { this.fail(new NativeError('NATIVE_EOF', 'Native connection ended; acquire fresh application handles')) }) diff --git a/packages/experimental/cua/tests/application-access.spec.ts b/packages/experimental/cua/tests/application-access.spec.ts index 31b423eed9..fc77988a11 100644 --- a/packages/experimental/cua/tests/application-access.spec.ts +++ b/packages/experimental/cua/tests/application-access.spec.ts @@ -28,6 +28,8 @@ vi.mock('../src/native-client.ts', async original => ({ }, })) +const unusedConnector = () => { throw new Error('Mocked native client does not connect') } + const nativeConfig = { helperPath: '/fixture', cwd: '/', timeoutMs: 1000, connectTimeoutMs: 5000, screenshotTimeoutMs: 5000, axTimeoutMs: 100, maxRequestBytes: 4096, maxResponseBytes: 4096, graceMs: 100, textEventIntervalMs: 1 } const identity = ApplicationAuthorizationResult.parse({ id: 'a'.repeat(64), name: 'Test application', executablePath: '/Applications/Fixture.app/Contents/MacOS/Fixture', persistable: true }) @@ -48,7 +50,7 @@ async function setup(choice = 'Allow for this task', options: { writable?: boole const save = vi.fn(async (next: readonly CuaApplicationGrant[]) => { grants = next }) let writable = options.writable !== false const access = new ApplicationAccess(ctx, { grants: () => grants, get save() { return writable ? save : undefined } }) - const client = new NativeClient(ctx, nativeConfig) + const client = new NativeClient(ctx, nativeConfig, unusedConnector) return { ctx, owner, agent: owner.agent, answer, save, access, client, grants: () => grants, setWritable(value: boolean) { writable = value }, } @@ -68,7 +70,7 @@ it('retains task consent for the exact helper handle without saving it', async ( await f.access.select(f.agent, f.client, handle, identity, signal) expect(f.answer).toHaveBeenCalledOnce() expect(f.save).not.toHaveBeenCalled() - const replacement = new NativeClient(f.ctx, nativeConfig) + const replacement = new NativeClient(f.ctx, nativeConfig, unusedConnector) await expect(f.access.run(replacement, handle, signal, async () => null)).rejects.toThrow('APP_ACCESS_REQUIRED') }) @@ -107,7 +109,7 @@ it('reuses only saved signed identities when a task or helper is replaced', asyn const { persistable: _persistable, ...grant } = identity const f = await setup('Deny', { initial: [grant] }) await f.access.select(f.agent, f.client, handle, identity, signal) - const replacement = new NativeClient(f.ctx, nativeConfig) + const replacement = new NativeClient(f.ctx, nativeConfig, unusedConnector) await f.access.select(f.agent, replacement, handle, identity, signal) expect(f.answer).not.toHaveBeenCalled() await expect(f.access.run(f.client, handle, signal, async () => null)).rejects.toThrow('APP_ACCESS_REQUIRED') @@ -215,7 +217,7 @@ it('keeps another task allowed when the same application is denied in a new task const f = await setup() await f.access.select(f.agent, f.client, handle, identity, signal) const owner = await f.ctx.agents.create({ sessionId: SessionId('cua-task-denial') }) - const client = new NativeClient(f.ctx, nativeConfig) + const client = new NativeClient(f.ctx, nativeConfig, unusedConnector) f.answer.mockResolvedValueOnce({ answers: [{ id: 'cua-app-access', selected: ['Deny'] }] }) await expect(f.access.select(owner.agent, client, handle, identity, signal)).rejects.toThrow('APP_ACCESS_DENIED') expect(f.client.disposed).toBe(false) @@ -251,7 +253,7 @@ it('cancels native work when saved consent is removed from configuration', async expect(f.client.disposed).toBe(true) await expect(f.access.run(f.client, handle, signal, async () => null)).rejects.toThrow('APP_ACCESS_REVOKED') await f.save([{ id: identity.id, name: identity.name, executablePath: identity.executablePath }]) - const replacement = new NativeClient(f.ctx, nativeConfig) + const replacement = new NativeClient(f.ctx, nativeConfig, unusedConnector) await f.access.select(f.agent, replacement, handle, identity, signal) await expect(f.access.run(f.client, handle, signal, async () => null)).rejects.toThrow('APP_ACCESS_REQUIRED') await expect(f.access.run(replacement, handle, signal, async () => 'fresh')).resolves.toBe('fresh') @@ -285,7 +287,7 @@ it('refuses closed helpers and forgets one task without removing another task', const f = await setup() await f.access.select(f.agent, f.client, handle, identity, signal) const owner = await f.ctx.agents.create({ sessionId: SessionId('other-consented-task') }) - const other = new NativeClient(f.ctx, nativeConfig) + const other = new NativeClient(f.ctx, nativeConfig, unusedConnector) await f.access.select(owner.agent, other, handle, identity, signal) f.ctx.emit('loader/volatile-update', [['allowedApps']]) f.access.forget(f.agent) @@ -307,7 +309,7 @@ it('keeps an active task operation running when a different task denies the same try { const operationSignal = await entered.promise const owner = await f.ctx.agents.create({ sessionId: SessionId('denying-task') }) - const other = new NativeClient(f.ctx, nativeConfig) + const other = new NativeClient(f.ctx, nativeConfig, unusedConnector) f.answer.mockResolvedValueOnce({ answers: [{ id: 'cua-app-access', selected: ['Deny'] }] }) await expect(f.access.select(owner.agent, other, handle, identity, signal)).rejects.toThrow('APP_ACCESS_DENIED') expect(operationSignal.aborted).toBe(false) diff --git a/packages/experimental/cua/tests/native-client.spec.ts b/packages/experimental/cua/tests/native-client.spec.ts index 2becbec8d3..83c35d481a 100644 --- a/packages/experimental/cua/tests/native-client.spec.ts +++ b/packages/experimental/cua/tests/native-client.spec.ts @@ -1,13 +1,12 @@ /** Controlled connection streams exercise the platform-independent native protocol client. */ import { PassThrough, Writable } from 'node:stream' import { ToolCallId } from '@deepseek-ai/dsh-llm' -import type { NativeChannel } from '../src/shared-native.ts' +import type { NativeChannel } from '../src/native-client.ts' import { describe, expect, it, onTestFinished, vi } from 'vitest' import { z } from 'zod' import { NativeClient, NativeError } from '../src/native-client.ts' const shared = vi.hoisted(() => ({ create: vi.fn() })) -vi.mock('../src/shared-native.ts', () => ({ createSharedNativeChannel: shared.create })) const Request = z.object({ id: z.string(), method: z.string(), params: z.record(z.string(), z.json()) }).strict() const Cancel = z.object({ type: z.literal('cancel'), id: z.string() }).strict() @@ -45,7 +44,7 @@ function fixture(options: { maxRequestBytes: options.maxRequestBytes ?? 1024, maxResponseBytes: options.maxResponseBytes ?? 1024, graceMs: options.graceMs ?? 10, textEventIntervalMs: 0.5, } - const client = new NativeClient(owner, config, options.reportDiagnostic) + const client = new NativeClient(owner, config, shared.create, options.reportDiagnostic) function finish() { done.resolve(); gone.resolve() } function reply(value: unknown) { stdout.write(Buffer.from(JSON.stringify(value) + '\n')) } function id(index = requests.length - 1) { diff --git a/packages/experimental/cua/tsdown.config.ts b/packages/experimental/cua/tsdown.config.ts index 6f91243efb..ccc53ca43d 100644 --- a/packages/experimental/cua/tsdown.config.ts +++ b/packages/experimental/cua/tsdown.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'tsdown' export default defineConfig(Object.entries({ index: 'lib/types/index.js', api: 'lib/types/api.js', bridge: 'lib/types/bridge.js', + 'native-client': 'lib/types/native-client.js', protocol: 'lib/types/protocol.js', }).map(([name, path]) => ({ entry: { [name]: path }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a423e1cd4f..b14a33f7f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: '@deepseek-ai/dsh-experimental-computer-use-cua-driver-native': specifier: workspace:* version: link:packages/experimental/computer-use-cua-driver-native + '@deepseek-ai/dsh-experimental-cua-macos': + specifier: workspace:* + version: link:packages/experimental/cua-macos '@deepseek-ai/dsh-experimental-tool-worktree': specifier: workspace:* version: link:packages/experimental/tool-worktree @@ -118,12 +121,12 @@ importers: '@types/node': specifier: ^22.20.1 version: 22.20.1 - '@types/semver': - specifier: ^7.8.0 - version: 7.8.0 '@types/papaparse': specifier: ^5.5.2 version: 5.5.2 + '@types/semver': + specifier: ^7.8.0 + version: 7.8.0 '@types/spdx-expression-parse': specifier: ^4.0.0 version: 4.0.0 @@ -8443,6 +8446,109 @@ importers: specifier: ^0.35.3 version: 0.35.5(@types/node@22.20.1) + packages/experimental/cua-macos: + dependencies: + '@deepseek-ai/dsh-atomic-write': + specifier: workspace:* + version: link:../../util/atomic-write + '@deepseek-ai/dsh-cua': + specifier: workspace:* + version: link:../cua + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:~ + version: link:../../../vendor/cordis + '@deepseek-ai/cordis-plugin-include': + specifier: workspace:~ + version: link:../../../vendor/include + '@deepseek-ai/cordis-plugin-loader': + specifier: workspace:~ + version: link:../../../vendor/loader + '@deepseek-ai/cordis-plugin-timer': + specifier: workspace:~ + version: link:../../../vendor/timer + '@deepseek-ai/dsh-agent': + specifier: workspace:* + version: link:../../core/agent + '@deepseek-ai/dsh-agent-loop': + specifier: workspace:* + version: link:../../core/agent-loop + '@deepseek-ai/dsh-agent-loop-testkit': + specifier: workspace:* + version: link:../../test-support/agent-loop-testkit + '@deepseek-ai/dsh-app-boot': + specifier: workspace:* + version: link:../../boot/app-boot + '@deepseek-ai/dsh-attachment': + specifier: workspace:* + version: link:../../attachment/attachment + '@deepseek-ai/dsh-attachment-local': + specifier: workspace:* + version: link:../../attachment/attachment-local + '@deepseek-ai/dsh-computer-use': + specifier: workspace:* + version: link:../../computer-use/computer-use + '@deepseek-ai/dsh-config-editor': + specifier: workspace:* + version: link:../../boot/config-editor + '@deepseek-ai/dsh-fs-local': + specifier: workspace:* + version: link:../../fs/fs-local + '@deepseek-ai/dsh-hmr': + specifier: workspace:* + version: link:../../boot/hmr + '@deepseek-ai/dsh-llm': + specifier: workspace:* + version: link:../../llm/llm + '@deepseek-ai/dsh-llm-deepseek': + specifier: workspace:* + version: link:../../llm/llm-deepseek + '@deepseek-ai/dsh-ptc-runtime': + specifier: workspace:* + version: link:../../ptc-runtime/ptc-runtime + '@deepseek-ai/dsh-ptc-runtime-node': + specifier: workspace:* + version: link:../../ptc-runtime/ptc-runtime-node + '@deepseek-ai/dsh-sandbox-local': + specifier: workspace:* + version: link:../../sandbox/sandbox-local + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:* + version: link:../../sandbox/sandbox-policy + '@deepseek-ai/dsh-session': + specifier: workspace:* + version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:* + version: link:../../session/session-projection + '@deepseek-ai/dsh-settings': + specifier: workspace:* + version: link:../../settings/settings + '@deepseek-ai/dsh-subprocess': + specifier: workspace:* + version: link:../../subprocess/subprocess + '@deepseek-ai/dsh-subprocess-local': + specifier: workspace:* + version: link:../../subprocess/subprocess-local + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:* + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:* + version: link:../../core/tools + '@deepseek-ai/dsh-user-questions': + specifier: workspace:* + version: link:../../interaction/user-questions + '@deepseek-ai/dsh-working-directory': + specifier: workspace:* + version: link:../../session/working-directory + sharp: + specifier: ^0.35.3 + version: 0.35.5(@types/node@22.20.1) + packages/experimental/hook-protocol: devDependencies: '@deepseek-ai/cordis': @@ -15780,10 +15886,6 @@ importers: packages: - papaparse@5.5.3: - resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} - - '@agentclientprotocol/sdk@1.4.0': resolution: {integrity: sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg==} peerDependencies: @@ -21840,6 +21942,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + papaparse@5.7.0: resolution: {integrity: sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==} @@ -23366,9 +23471,6 @@ packages: snapshots: - papaparse@5.5.3: {} - - '@agentclientprotocol/sdk@1.4.0(zod@4.4.3)': dependencies: zod: 4.4.3 @@ -29582,6 +29684,8 @@ snapshots: pako@1.0.11: {} + papaparse@5.5.3: {} + papaparse@5.7.0: {} parse-ms@4.0.0: {} diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index fcba5c87af..3b56fd4f0a 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -171,7 +171,8 @@ export function readWorkspaceManifests(repositoryRoot: string): WorkspaceManifes const packageFileExtras: Readonly> = { // CUA publishes its client API and attributed native source for local macOS builds. - '@deepseek-ai/dsh-cua': ['native/bin', 'lib/api.js', 'lib/bridge.js', 'native/Package.swift', 'native/Sources', 'native/tests/swift', 'native/tests/test_swift.py', 'native/LICENSE.cua-driver', 'native/upstream.json', 'native/README.txt', 'native/protocol.schema.json'], + '@deepseek-ai/dsh-cua': ['lib/api.js', 'lib/bridge.js', 'lib/native-client.js', 'lib/protocol.js'], + '@deepseek-ai/dsh-experimental-cua-macos': ['native/bin', 'native/Package.swift', 'native/Sources', 'native/tests/swift', 'native/tests/test_swift.py', 'native/LICENSE.cua-driver', 'native/upstream.json', 'native/README.txt', 'native/protocol.schema.json'], '@deepseek-ai/dsh-webhook-github': ['examples/github-review/cordis.yml', 'examples/github-review/github-ready-review-rule.mjs'], // Owned Worker bundles import this public bootstrap before their business entry. '@deepseek-ai/dsh-app-boot': ['lib/worker/profile-resolution-bootstrap.js'], diff --git a/scripts/release/families.spec.ts b/scripts/release/families.spec.ts index 90ecdca54d..876e9a4bc0 100644 --- a/scripts/release/families.spec.ts +++ b/scripts/release/families.spec.ts @@ -67,6 +67,7 @@ describe('release families', () => { '@deepseek-ai/dsh-experimental-computer-use-cua-driver-native', '@deepseek-ai/dsh-experimental-cot-translation-bundle', '@deepseek-ai/dsh-cua', + '@deepseek-ai/dsh-experimental-cua-macos', '@deepseek-ai/dsh-hook-protocol', '@deepseek-ai/dsh-hooks-claude-code', '@deepseek-ai/dsh-hooks-codex', diff --git a/snapshots/session/cua-native/cordis.snapshot.yml b/snapshots/session/cua-native/cordis.snapshot.yml index 12a22397cf..f6708af93d 100644 --- a/snapshots/session/cua-native/cordis.snapshot.yml +++ b/snapshots/session/cua-native/cordis.snapshot.yml @@ -48,7 +48,7 @@ - id: computer-use name: '@deepseek-ai/dsh-computer-use' - id: cua-native - name: '@deepseek-ai/dsh-cua' + name: '@deepseek-ai/dsh-experimental-cua-macos' config: helperPath: !!js process.cwd() + '/native-helper.mjs' diff --git a/snapshots/session/cua-native/cordis.yml b/snapshots/session/cua-native/cordis.yml index a1c6a1db18..b60d7872d4 100644 --- a/snapshots/session/cua-native/cordis.yml +++ b/snapshots/session/cua-native/cordis.yml @@ -30,7 +30,7 @@ - id: computer-use name: '@deepseek-ai/dsh-computer-use' - id: cua-native - name: '@deepseek-ai/dsh-cua' + name: '@deepseek-ai/dsh-experimental-cua-macos' config: helperPath: !!js process.cwd() + '/native-helper.mjs' diff --git a/tsconfig.base.json b/tsconfig.base.json index 2cd4a3a614..c121467084 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -89,6 +89,9 @@ "@deepseek-ai/dsh-computer-use": ["./packages/computer-use/computer-use/src/index.ts"], "@deepseek-ai/dsh-computer-use/brand": ["./packages/computer-use/computer-use/src/brand.ts"], "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": ["./packages/experimental/computer-use-cua-driver-mcp/src/index.ts"], + "@deepseek-ai/dsh-experimental-cua-macos": ["./packages/experimental/cua-macos/src/index.ts"], + "@deepseek-ai/dsh-cua/native-client": ["./packages/experimental/cua/src/native-client.ts"], + "@deepseek-ai/dsh-cua/protocol": ["./packages/experimental/cua/src/protocol.ts"], "@deepseek-ai/dsh-cua": ["./packages/experimental/cua/src/index.ts"], "@deepseek-ai/dsh-cua/api": ["./packages/experimental/cua/src/api.ts"], "@deepseek-ai/dsh-cua/bridge": ["./packages/experimental/cua/src/bridge.ts"], diff --git a/tsconfig.host.json b/tsconfig.host.json index f18e30d8f0..6084b66d1f 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -196,7 +196,7 @@ "apps/desktop-host/tests/**/*.ts", "benchmarks/**/*.ts", "packages/*/*/tests/**/*.ts", - "packages/experimental/cua/native/generate-protocol.ts", + "packages/experimental/cua-macos/native/generate-protocol.ts", "packages/experimental/claude-code-mods/examples/*/index.ts", "packages/experimental/inspector/scripts/**/*.ts", "scripts/**/*.ts", @@ -259,6 +259,7 @@ { "path": "./packages/experimental/computer-use-cua-driver-mcp" }, { "path": "./packages/experimental/computer-use-cua-driver-native" }, { "path": "./packages/experimental/cua" }, + { "path": "./packages/experimental/cua-macos" }, { "path": "./packages/client/ui-sidebar-documentpreview/tsconfig.host.json" }, { "path": "./packages/client/ui-sidebar-browser/tsconfig.host.json" }, { "path": "./packages/client/ui-settings-general/tsconfig.host.json" },