Validate CUA response targets and stabilize platform permission loading

This commit is contained in:
creatixchu committed 2026-11-06 12:15:41 +08:00
1 parent f5c1a31125
commit 96a6b779fe
39 files changed
+164 -77

No files matched your search

@@ -9,8 +9,8 @@
en: a22ddcfb339e503d
zh: a1f4fadb46fce1e7
/agent-note-native-cua-objects-independent-of-evaluator-ownership/decision:
en: 2780d1b0f5ce9ea8
zh: ff986f8c1c90300f
en: 176a9c2f6040f2a1
zh: 2617ec13d25de998
/agent-note-native-cua-objects-independent-of-evaluator-ownership/alternatives-considered:
en: 3d0bc77e3c432cb9
zh: 50a20eda1811f16c
@@ -16,13 +16,13 @@ The experimental CUA package owns a native-app subset of the Codex-style object
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.
Native process ownership follows the [shared helper decision](2026-10-09-cua-shared-native-helper.md), with isolated Agent connections. App handles bind process identity and kernel start time; replacing the process or helper never revives old handles. AX snapshots are scoped to an app and optional window, so independent window observations remain usable. Coordinate actions select an owned window; the Host maps [screenshot coordinates](2026-10-13-cua-hover-routes-and-coordinates.md) to native window-local points. Keyboard input remains app-scoped because macOS delivers it to the app's current focus.
Native process ownership follows the [shared helper decision](2026-10-09-cua-shared-native-helper.md), with isolated Agent connections. App handles bind process identity and kernel start time; replacing the process or helper never revives old handles. The native helper retains per-app and per-window snapshots. The [object API](2026-11-05-cua-scoped-surfaces-and-input.md) keeps the latest successful observation per application handle. Coordinate actions select an owned window; the Host maps [screenshot coordinates](2026-10-13-cua-hover-routes-and-coordinates.md) to native window-local points. Providers declare keyboard scopes in `keyboard`. macOS supports keyboard input at the application's current focus.
The operation schema owns shared data declarations. Host-only limits and launch continuations remain separate from model parameters, and generated native field/enum tables preserve independent native admission without hand-maintained copies. Native code continues to own system calls, permissions and cancellation. AX formatting consumes immutable observations; the walker retains sole ownership of indices and AX references.
Clipboard paste is distinct from Unicode key input: Return and Tab key events can trigger actions instead of inserting clipboard content. Pasteboard access runs on the AppKit main actor and retains promised data until a reader requests it. AX command completion alone does not establish that the application has fetched the data. Restoration checks ownership before replacing contents; the caller still verifies the application result. After successful command dispatch, `pasteReadTimeoutMs` bounds waiting for a data request, with a default of 1000 ms. Expiry returns `PASTE_NOT_CONSUMED`, closes the data provider against late delivery, and restores the clipboard only while still owned. The failure reports no read within that interval; it does not prove the application will never process its queued Paste command. A delayed command may read restored clipboard contents, so callers inspect the target and never retry automatically. Foreground-dependent consumption was observed in the installed ChatGPT/Codex application (`com.openai.codex`); other applications need their own validation. Guidance presents this as an example and applies recovery instructions to any `PASTE_NOT_CONSUMED` result.
CUA registers its own Harness tool and returns operation values directly. Screenshot capture requires a resolved image-capable model and the shared attachment service. The tool admits each screenshot once and renders its durable image reference; the object API, combined observation and image sink retain the same `CuaScreenshot` containing encoded `bytes`, `mimeType`, `pixelWidth`, `pixelHeight` and `windowId` from the validated screenshot result. The bridge preserves this metadata without another upload or image emission. Callers use the reported format and dimensions when saving or declaring images.
CUA registers its own Harness tool and returns operation values directly. Screenshot capture requires a resolved image-capable model and the shared attachment service. The tool admits each screenshot once and renders its durable image reference; the window object and image sink retain the same `CuaScreenshot` containing encoded `bytes`, `mimeType`, `pixelWidth`, `pixelHeight` and `windowId` from the validated screenshot result. The bridge preserves this metadata without another upload or image emission. Callers use the reported format and dimensions when saving or declaring images.
## Alternatives considered
@@ -16,13 +16,13 @@ Computer-use 程序需要一致的应用和窗口操作。将这些操作与 CUA
对象接口接收操作分派器。工具适配模块每次操作都获取当前工具绑定,保留标准授权、日志和图片接入。执行宿主负责变量持久化和最终表达式值的序列化。CUA 不定义持久化指令、执行 worker 或 run_code 替代实现。
原生进程归属遵循[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md),Agent 连接保持隔离。应用句柄绑定进程身份及内核启动时间,替换进程或辅助进程不会使旧句柄重新有效。AX 快照按应用和可选窗口保存,不同窗口的观察可以分别使用。坐标操作选择属于该应用的窗口;Host 按[截图坐标规则](2026-10-13-cua-hover-routes-and-coordinates.zh.md)换算为原生窗口内 point。键盘输入保持应用范围,因为 macOS 将其送往应用的当前焦点。
原生进程归属遵循[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md),Agent 连接保持隔离。应用句柄绑定进程身份及内核启动时间,替换进程或辅助进程不会使旧句柄重新有效。原生 helper 按应用和可选窗口保存快照。[对象 API](2026-11-05-cua-scoped-surfaces-and-input.zh.md) 为每个应用句柄保留最近一次成功读取的快照。坐标操作选择属于该应用的窗口;Host 按[截图坐标规则](2026-10-13-cua-hover-routes-and-coordinates.zh.md)换算为原生窗口内 point。提供方通过 `keyboard` 声明可用范围。macOS 仅支持向应用当前焦点发送键盘输入。
操作 schema 负责共享数据声明。Host 专属限制和启动续接与模型参数分开,生成的原生字段、枚举表保留独立原生校验,同时避免手工维护副本。系统调用、权限和取消仍由原生代码负责。AX 格式化只消费不可变观察数据,遍历器独自负责索引与 AX 引用。
剪贴板粘贴与 Unicode 按键输入分别实现,因为 Return 和 Tab 按键可能触发动作,而不是插入剪贴板内容。剪贴板访问使用 AppKit 主线程,保留延迟提供的数据直到读取方请求。AX 命令完成本身不能证明应用已取得数据。恢复前检查剪贴板归属;调用方仍需验证应用中的结果。 命令成功分派后,`pasteReadTimeoutMs` 限制等待数据请求的时间,默认 1000 毫秒。到期返回 `PASTE_NOT_CONSUMED`,关闭数据提供方以阻止迟到的数据交付,并仅在仍持有剪贴板时恢复原内容。该失败只说明这段时间内没有读取,不证明应用以后不会处理已排队的 Paste 命令。迟到的命令可能读取恢复后的剪贴板内容,因此调用方必须检查目标,不能自动重试。已安装的 ChatGPT/Codex 应用(`com.openai.codex`)已观察到依赖前台才能读取的行为,其他应用需要分别验证。指引将其写为例子,并对任何 `PASTE_NOT_CONSUMED` 结果提供恢复步骤。
CUA 注册自己的 Harness 工具,直接返回操作结果。截图要求解析到支持图片输入的模型,并提供共享附件服务。工具只接入一次截图,再渲染持久图片引用;对象 API、组合观察和图片输出回调保留同一个 `CuaScreenshot`,其中的编码 `bytes`、`mimeType`、`pixelWidth`、`pixelHeight` 和 `windowId` 来自已校验的截图结果。桥接保留这些元数据,不再次上传或输出图片。调用方落盘或声明图片时使用返回的格式和尺寸。
CUA 注册自己的 Harness 工具,直接返回操作结果。截图要求解析到支持图片输入的模型,并提供共享附件服务。工具只接入一次截图,再渲染持久图片引用;窗口对象和图片输出回调保留同一个 `CuaScreenshot`,其中的编码 `bytes`、`mimeType`、`pixelWidth`、`pixelHeight` 和 `windowId` 来自已校验的截图结果。桥接保留这些元数据,不再次上传或输出图片。调用方落盘或声明图片时使用返回的格式和尺寸。
## 考虑过的替代方案
@@ -9,8 +9,8 @@
en: cdc04139c8d69415
zh: a3fe3e78ada08d95
/agent-note-continuous-cua-hover-without-cross-call-ownership/decision:
en: a25e8c472c084852
zh: 12250574113e110a
en: d1ea8fdc05927896
zh: d3039bbb82bc949c
/agent-note-continuous-cua-hover-without-cross-call-ownership/alternatives-considered:
en: 18d917ee2e909b93
zh: 5b5ad0530f138cda
@@ -10,7 +10,7 @@ Hover interactions span movement, delayed content, observation and input inside
## Decision
App and Window expose `hover` for an observed AX element or an explicit coordinate target. It never presses a mouse button; disabled controls can use system-pointer movement. Completion, AX reads and screenshots do not restore a previous pointer position. Successive calls can visit intermediate points before entering a popup. Clicks deliver button events at their target without an off-window priming click. The [input-routing decision](2026-10-13-cua-hover-routes-and-coordinates.md) selects AX alternate UI before falling back to system movement with foreground and mouse-hit checks. It never activates or raises the app implicitly. Other input or window changes can race that check.
An explicit Window exposes `mouseMove` for coordinates or an observed element ID. The result reports `hoverRoute` and `systemPointerMoved`. It never presses a mouse button; disabled controls can use system-pointer movement. Completion, AX reads and screenshots do not restore a previous pointer position. Successive calls can visit intermediate points before entering a popup. Clicks deliver button events at their target without an off-window priming click. The [input-routing decision](2026-10-13-cua-hover-routes-and-coordinates.md) selects AX alternate UI before falling back to system movement with foreground and mouse-hit checks. It never activates or raises the app implicitly. Other input or window changes can race that check.
The [shared helper](2026-10-09-cua-shared-native-helper.md) serializes complete individual operations. It grants no application ownership across calls. Earlier arrival, a retained app object and Session lifetime do not establish task priority. Waiting for the model does not reserve the application. Explicit task coordination determines any required execution order.
@@ -10,7 +10,7 @@ Status: implemented
## 决策
App 和 Window 提供 `hover`,接受已观察的 AX 元素或显式坐标目标。悬停不按下鼠标按钮,禁用控件可使用系统鼠标移动路径。调用结束、AX 读取和截图不恢复先前的鼠标位置。进入浮层前可连续调用并经过中间位置。点击只在目标位置发送按键事件,不先在窗口外点击。 [输入路径决定](2026-10-13-cua-hover-routes-and-coordinates.zh.md)优先选择 AX 替代界面,再回退到经过前台和鼠标命中检查的系统移动,不隐式激活或抬起应用。其他输入或窗口变化仍可能与检查交错。
显式窗口提供 `mouseMove`,接受窗口坐标或已观察元素的字符串 ID,结果用 `hoverRoute` 和 `systemPointerMoved` 报告执行路径。悬停不按下鼠标按钮,禁用控件可使用系统鼠标移动路径。调用结束、AX 读取和截图不恢复先前的鼠标位置。进入浮层前可连续调用并经过中间位置。点击只在目标位置发送按键事件,不先在窗口外点击。 [输入路径决定](2026-10-13-cua-hover-routes-and-coordinates.zh.md)优先选择 AX 替代界面,再回退到经过前台和鼠标命中检查的系统移动,不隐式激活或抬起应用。其他输入或窗口变化仍可能与检查交错。
[共享 helper](2026-10-09-cua-shared-native-helper.zh.md)串行执行完整的单次操作,不分配跨调用的应用所有权。先发起请求、持有 app 对象和 Session 寿命均不代表任务优先级。等待模型期间不占用应用。需要确定执行顺序时,由明确的任务协调决定。
@@ -9,8 +9,8 @@
en: f96bf1ebc0719fb0
zh: e9a1144f0e76572d
/agent-note-background-hover-and-screenshot-coordinates/decision:
en: 0f45528ea6c5988c
zh: 9bde9df417ddb1ce
en: 6ac0b77bea25fcf7
zh: e50fec9049926fdc
/agent-note-background-hover-and-screenshot-coordinates/alternatives-considered:
en: 5fc4beaf79761c50
zh: 4948333b0020be28
@@ -12,7 +12,7 @@ Hover-only controls can sometimes be revealed without moving the user's pointer.
Native hover first uses a same-window element or ancestor advertising both `AXShowAlternateUI` and `AXShowDefaultUI`. Entering its revealed descendants preserves the parent UI; leaving restores it. An unavailable route falls back to the real system pointer. An AX action failure propagates because received effects may be uncertain. The result records the route and whether the system pointer moved. System movement requires the selected foreground app and AppKit's mouse hit test, which excludes input-transparent overlays. No route grants [cross-call ownership](2026-10-13-cua-continuous-hover.md).
Model-facing coordinate input defaults to pixels in the returned screenshot. `relative: true` accepts integer thousandths from 0 to 999 for click, hover, scroll and drag; it does not apply to AX indices. The Host owns the mapping to native window points. An application-wide observation refreshes its inventory; a scoped observation preserves other windows' image mappings. Window resizing invalidates the old image, while movement preserves local coordinates. Relative endpoints remain inside the window and do not depend on screenshot resolution.
Model-facing coordinate input defaults to pixels in the returned screenshot. `relative: true` accepts integer thousandths from 0 to 999 for `click`, `mouseMove`, `scroll` and `drag`; it does not apply to opaque element IDs. The Host owns the mapping to native window points. An application-wide observation refreshes its inventory; a scoped observation preserves other windows' image mappings. Window resizing invalidates the old image, while movement preserves local coordinates. Relative endpoints remain inside the window and do not depend on screenshot resolution.
`LlmAdapter.imageRequestTarget` exposes the same dimensions and byte target used by image serialization. DeepSeek and pi-ai share their existing projection functions with this method. Other adapters preserve normalized images unless they declare resizing. Screenshot bytes returned to programs match the request image; its durable source is admitted and stored once. The native PNG metadata is checked against decoded source dimensions before any input mapping is published.
@@ -12,7 +12,7 @@ Status: implemented
原生悬停优先寻找同一窗口内同时声明 `AXShowAlternateUI` 和 `AXShowDefaultUI` 的元素或祖先。进入其展开区域的子控件时保持父控件展开,离开后恢复。没有可用路径时回退到真实系统鼠标。AX 动作失败直接返回,因为已产生的效果可能不确定。结果记录实际路径及是否移动系统鼠标。系统移动要求所选应用处于前台,并使用 AppKit 的鼠标命中查询排除不接收输入的覆盖窗口。所有路径都遵守[跨调用不分配所有权](2026-10-13-cua-continuous-hover.zh.md)的决定。
面向模型的坐标默认使用返回截图中的像素。click、hover、scroll 和 drag 传入 `relative: true` 时接受 0~999 的千分比整数,不适用于 AX 索引。Host 负责换算原生窗口 point。应用级观察刷新窗口清单,单窗口观察保留其他窗口的图片映射。窗口尺寸变化使旧图片失效,移动窗口保留局部坐标。相对坐标的端点保持在窗口内,不随截图分辨率改变。
面向模型的坐标默认使用返回截图中的像素。`click`、`mouseMove`、`scroll` 和 `drag` 传入 `relative: true` 时接受 0~999 的千分比整数,仅用于坐标目标。Host 负责换算原生窗口 point。应用级观察刷新窗口清单,单窗口观察保留其他窗口的图片映射。窗口尺寸变化使旧图片失效,移动窗口保留局部坐标。相对坐标的端点保持在窗口内,不随截图分辨率改变。
`LlmAdapter.imageRequestTarget` 提供与图片序列化相同的尺寸和字节目标。DeepSeek 和 pi-ai 在该方法中复用已有投影函数。其他适配器除非声明缩放,否则保留归一化图片。返回程序的截图字节与模型请求图片一致,持久原图只接入并保存一次。发布输入映射前,原生 PNG 元数据必须与解码后的原图尺寸一致。
@@ -12,5 +12,5 @@
en: e5d1027faa55adba
zh: 4b88ad6a70977fb3
/native-cua-application-and-window-api/migration:
en: ecbd85a36241873d
zh: 0a0e126a012a919f
en: 6cabae699a340845
zh: f2256751518c5e36
@@ -17,7 +17,7 @@ Providers supply startup capabilities alongside their platform guidance. The nat
1. After `cua.getApp(selector)`, select the target from `app.getWindows()` with `app.getWindow(windowId)`. Use `window.getScreenshot()`, `window.click()`, `window.scroll()`, and `window.drag()`. Replace `hover` with `window.mouseMove()`.
2. Replace `getAXState()` with `getAccessibilityTree()`. Replace `getAXStateAndScreenshot()` with separate tree and screenshot reads. Selection reads the initial tree silently for a window, and for an app when `appLevelAccessibility` is true. Read the tree explicitly to display it.
3. Pass the opaque string ID from the latest tree wherever you passed a numeric element index. In raw requests use `elementId` with `snapshotId`. Replace `performSecondaryAction` with `listActions(id)` and `performAction(id, exactName, argument?)`, or use `invoke(id)` for the default action. `setValue` accepts the provider’s supported JSON values.
3. Pass the opaque string ID from the latest tree wherever you passed a numeric element index. In raw requests use `elementId` with `snapshotId`. Replace `performSecondaryAction` with `listActions(id)` and `performAction(id, exactName, argument?)`, or use `invoke(id)` for the default action. `setValue` accepts the provider’s supported JSON values. On macOS, `performAction(id, 'AXRaise')` performs the advertised raise action. Call `app.activate()` first when the application must become active.
4. Replace a packed chord such as `pressKey('shift+Tab')` with `pressKey('Tab', ['shift'])`. Check `(await cua.getCapabilities()).methods` before using `sendKeyEvent`: macOS supports complete keystrokes and rejects directional events with `NOT_IMPLEMENTED`.
5. Read `dispatched` from action results, then observe the application to verify the outcome. `invoke` returns `dispatched: false` when the element has no default action. Remove reads of the old `verified` field and of `pid` on selected objects or window records. Use `listApps()` or running `findApps()` matches when you need a PID.
6. For a custom provider, replace `CuaBackend.description` with `capabilities` and pass `guidance` and `permissionParameters` to `installCua`. Rebuild any custom `helperPath` against the [operation schema](../../../../packages/experimental/cua/src/protocol.ts) and [tagged permission response](../../../../packages/experimental/cua/src/access-types.ts). Restart the provider, inspect its advertised methods and keyboard scopes, and open the Computer Use permission page to verify its platform controls.
@@ -17,7 +17,7 @@ CUA 脚本在指定窗口上截图和操作鼠标。App 和 Window 对象共享
1. 调用 `cua.getApp(selector)` 后,从 `app.getWindows()` 返回的列表选定目标,再调用 `app.getWindow(windowId)`。在窗口上调用 `getScreenshot()`、`click()`、`scroll()` 和 `drag()`,将原有的 `hover` 调用改为 `window.mouseMove()`。
2. 将 `getAXState()` 改为 `getAccessibilityTree()`,将 `getAXStateAndScreenshot()` 拆成读取树和截图两次调用。选择窗口时会静默读取初始树。选择应用时,只有 `appLevelAccessibility` 为真才读取。要显示树,请显式读取。
3. 将数字元素索引改为从最新树中取得的字符串 ID。直接发送请求时使用 `elementId` 和 `snapshotId`。原先调用 `performSecondaryAction` 的地方,先用 `listActions(id)` 取得完整动作名,再调用 `performAction(id, exactName, argument?)`。要执行默认动作,可调用 `invoke(id)`。`setValue` 接受提供方支持的 JSON 值。
3. 将数字元素索引改为从最新树中取得的字符串 ID。直接发送请求时使用 `elementId` 和 `snapshotId`。原先调用 `performSecondaryAction` 的地方,先用 `listActions(id)` 取得完整动作名,再调用 `performAction(id, exactName, argument?)`。要执行默认动作,可调用 `invoke(id)`。`setValue` 接受提供方支持的 JSON 值。 在 macOS 上,`performAction(id, 'AXRaise')` 只执行元素声明的抬起窗口动作。需要把应用切到前台时先调用 `app.activate()`。
4. 将 `pressKey('shift+Tab')` 之类的组合键写法改为 `pressKey('Tab', ['shift'])`。调用 `sendKeyEvent` 前检查 `(await cua.getCapabilities()).methods`。macOS 支持完整按键,单独发送按下或松开事件会返回 `NOT_IMPLEMENTED`。
5. 从动作结果读取 `dispatched`,再观察应用确认效果。元素没有默认动作时,`invoke` 返回 `dispatched: false`。移除读取旧字段 `verified` 的代码,以及对已选对象和窗口记录中 `pid` 的读取。需要 PID 时,使用 `listApps()` 或 `findApps()` 返回的运行中应用记录。
6. 自定义提供方需将 `CuaBackend.description` 改为 `capabilities`,并向 `installCua` 传入 `guidance` 和 `permissionParameters`。如配置了 `helperPath`,请按[操作协议](../../../../packages/experimental/cua/src/protocol.ts)和[带平台标记的权限响应](../../../../packages/experimental/cua/src/access-types.ts)重新构建 helper。重启提供方,检查它声明的方法和键盘输入范围,再打开 Computer Use 权限页确认平台控件。
+4 -4
View File
@@ -173,7 +173,7 @@ The plugin contributes one `cua` operation tool with method-specific parameters.
##### Operation guidance (verbatim)
```markdown
macOS: accessibility tree operations are available at app or window level. Simulated keyboard input is available at app level. Simulated mouse input is available at window level. Use window coordinates for click and mouseMove on macOS. The helper rejects element-targeted pointer requests carrying the window ID supplied by these object methods. For an element’s default action, use invoke with its latest ID. getApp also accepts bundle IDs and .app paths, and findApps searches /Applications, /System/Applications, and ~/Applications. Use the Computer Use permission page to grant the helper application Accessibility and Screen Recording permissions in System Settings. Native window and AX bounds use macOS points, while delivered screenshots carry their own pixel dimensions. show does not switch Spaces, and Stage Manager can present reduced window geometry; explain any required foreground step before activate or raise.
macOS: accessibility tree operations are available at app or window level. Simulated keyboard input is available at app level. Simulated mouse input is available at window level. Use window coordinates for click and mouseMove on macOS. The helper rejects element-targeted pointer requests carrying the window ID supplied by these object methods. For an element’s default action, use invoke with its latest ID. getApp also accepts bundle IDs and .app paths, and findApps searches /Applications, /System/Applications, and ~/Applications. Use the Computer Use permission page to grant the helper application Accessibility and Screen Recording permissions in System Settings. Native window and AX bounds use macOS points, while delivered screenshots carry their own pixel dimensions. show does not switch Spaces, and Stage Manager can present reduced window geometry; explain any required foreground step before calling app.activate() or performing an advertised AXRaise action.
Use cua to observe and operate native applications. Always use cua.getCapabilities() to confirm whether a specific API is available on the current platform; its reported capabilities take precedence over availability statements in these instructions. All methods are async; observation options default to { emit: true }, and emit: false suppresses sink output only, leaving returned values unchanged. Actions report dispatch evidence, not verified completion. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI actions and ask the user to unlock the desktop session; null or unknown means verify a usable unlocked desktop session first. cua.getApp requests user consent; if access is denied or revoked, stop using that application until the user explicitly permits another request, and never bypass a refusal or switch tools to avoid it. Never replay uncertain input without a fresh observation.
@@ -197,7 +197,7 @@ Setup and discovery:
Available at app or window level depending on the platform.
Element ids are opaque strings from the latest successful tree; the object facade keeps one current snapshot per app across its repeated app and window objects, a new read replaces it, and a failed read invalidates the previous one. Repeated strings can appear for different elements, so do not reuse an old id blindly, and element operations do not fall back to pointer input or typing.
- getAccessibilityTree(options?: { emit?: boolean, includeMenus?: boolean, details?: boolean, disableDiffing?: boolean }) -> string — read the full tree text; includeMenus expands menu contents and details adds identifiers, native bounds, and complete action names.
- getAccessibilityTree(options?: { emit?: boolean, includeMenus?: boolean, details?: boolean }) -> string — read the full tree text; includeMenus expands menu contents and details adds identifiers, native bounds, and complete action names.
- listActions(elementId: string) -> string[] — list the actions an element advertises.
- invoke(elementId: string) -> ActionResult — invoke the element's default action; dispatched is false when it has none.
- performAction(elementId: string, action: string, argument?: JsonValue) -> ActionResult — perform an observed action with an optional direct JSON argument.
@@ -212,7 +212,7 @@ Use an explicit window for screenshots and pointer input.
- scroll(options: { x: number, y: number, deltaLinesX: number, deltaLinesY: number, relative?: boolean }) -> ActionResult — scroll at a point; deltas are integers from -10 to 10 with at least one nonzero, and positive values reveal content above or left.
- drag(from: [number, number], to: [number, number], options?: { mouseButton?: 'left' | 'right' | 'middle', modifiers?: ('command' | 'control' | 'option' | 'shift' | 'fn')[], relative?: boolean }) -> ActionResult — drag between two points of one window.
A string target is an element id; coordinate targets are [x, y] and are pixels in the latest returned screenshot by default. With relative: true they are integer thousandths of the window width and height from 0 to 999, based on the current AX observation or screenshot. Left clicks support one or two presses; right and middle clicks support one. FOREGROUND_REQUIRED means explain the need, then explicitly activate or raise and observe again; activation is never automatic. Read fresh state after actions, and treat WINDOW_CHANGED, STALE_SNAPSHOT, and SCREENSHOT_REQUIRED as requiring the stated fresh observation or capture before any retry.
A string target is an element id; coordinate targets are [x, y] and are pixels in the latest returned screenshot by default. With relative: true they are integer thousandths of the window width and height from 0 to 999, based on the current AX observation or screenshot. Left clicks support one or two presses; right and middle clicks support one. FOREGROUND_REQUIRED means explain the need, then call app.activate() and observe again; activation is never automatic. Read fresh state after actions, and treat WINDOW_CHANGED, STALE_SNAPSHOT, and SCREENSHOT_REQUIRED as requiring the stated fresh observation or capture before any retry.
## Simulated keyboard
Available at app or window level depending on the platform.
@@ -220,7 +220,7 @@ Available at app or window level depending on the platform.
- paste(text: string, options?: { format?: 'text' | 'html' }) -> ActionResult — deliver text or HTML through the app's Paste command, restoring prior clipboard contents unless another writer changes them; a Paste failure is surfaced without substituting typeText, and PASTE_NOT_CONSUMED means inspect before any retry.
- pressKey(key: string, modifiers?: string[]) -> ActionResult — press and release one complete keystroke. For example pressKey('Tab', ['shift']) or pressKey('+'). Key and modifier names are platform-defined strings. Pass key and modifiers separately; do not pack them into a chord string.
Raw tools use { method, params }: include appHandle for app calls and windowId for window calls, and use snapshotId with elementId for element routes. Object options such as emit and disableDiffing are facade-only: emit is local output control and neither is a raw RPC parameter, so raw callers must follow the raw operation schema for admitted params. getAccessibilityTree and getWindow read through raw getState; getWindows maps to listWindows; getScreenshot maps to screenshot; cua.getState maps to listApps; every other named operation uses the same method name.
Raw tools use { method, params }: include appHandle for app calls and windowId for window calls, and use snapshotId with elementId for element routes. The emit option is facade-only: emit is local output control, so raw callers must follow the raw operation schema for admitted params. getAccessibilityTree and getWindow read through raw getState; getWindows maps to listWindows; getScreenshot maps to screenshot; cua.getState maps to listApps; every other named operation uses the same method name.
```
#### Token effect
+4 -4
View File
@@ -173,7 +173,7 @@ AppKit sheet 可以通过其窗口 ID 单独选择,也会出现在所属文档
##### 操作指导文本(原文)
```markdown
macOS: accessibility tree operations are available at app or window level. Simulated keyboard input is available at app level. Simulated mouse input is available at window level. Use window coordinates for click and mouseMove on macOS. The helper rejects element-targeted pointer requests carrying the window ID supplied by these object methods. For an element’s default action, use invoke with its latest ID. getApp also accepts bundle IDs and .app paths, and findApps searches /Applications, /System/Applications, and ~/Applications. Use the Computer Use permission page to grant the helper application Accessibility and Screen Recording permissions in System Settings. Native window and AX bounds use macOS points, while delivered screenshots carry their own pixel dimensions. show does not switch Spaces, and Stage Manager can present reduced window geometry; explain any required foreground step before activate or raise.
macOS: accessibility tree operations are available at app or window level. Simulated keyboard input is available at app level. Simulated mouse input is available at window level. Use window coordinates for click and mouseMove on macOS. The helper rejects element-targeted pointer requests carrying the window ID supplied by these object methods. For an element’s default action, use invoke with its latest ID. getApp also accepts bundle IDs and .app paths, and findApps searches /Applications, /System/Applications, and ~/Applications. Use the Computer Use permission page to grant the helper application Accessibility and Screen Recording permissions in System Settings. Native window and AX bounds use macOS points, while delivered screenshots carry their own pixel dimensions. show does not switch Spaces, and Stage Manager can present reduced window geometry; explain any required foreground step before calling app.activate() or performing an advertised AXRaise action.
Use cua to observe and operate native applications. Always use cua.getCapabilities() to confirm whether a specific API is available on the current platform; its reported capabilities take precedence over availability statements in these instructions. All methods are async; observation options default to { emit: true }, and emit: false suppresses sink output only, leaving returned values unchanged. Actions report dispatch evidence, not verified completion. If sessionLocked is true or an operation reports SESSION_LOCKED, stop UI actions and ask the user to unlock the desktop session; null or unknown means verify a usable unlocked desktop session first. cua.getApp requests user consent; if access is denied or revoked, stop using that application until the user explicitly permits another request, and never bypass a refusal or switch tools to avoid it. Never replay uncertain input without a fresh observation.
@@ -197,7 +197,7 @@ Setup and discovery:
Available at app or window level depending on the platform.
Element ids are opaque strings from the latest successful tree; the object facade keeps one current snapshot per app across its repeated app and window objects, a new read replaces it, and a failed read invalidates the previous one. Repeated strings can appear for different elements, so do not reuse an old id blindly, and element operations do not fall back to pointer input or typing.
- getAccessibilityTree(options?: { emit?: boolean, includeMenus?: boolean, details?: boolean, disableDiffing?: boolean }) -> string — read the full tree text; includeMenus expands menu contents and details adds identifiers, native bounds, and complete action names.
- getAccessibilityTree(options?: { emit?: boolean, includeMenus?: boolean, details?: boolean }) -> string — read the full tree text; includeMenus expands menu contents and details adds identifiers, native bounds, and complete action names.
- listActions(elementId: string) -> string[] — list the actions an element advertises.
- invoke(elementId: string) -> ActionResult — invoke the element's default action; dispatched is false when it has none.
- performAction(elementId: string, action: string, argument?: JsonValue) -> ActionResult — perform an observed action with an optional direct JSON argument.
@@ -212,7 +212,7 @@ Use an explicit window for screenshots and pointer input.
- scroll(options: { x: number, y: number, deltaLinesX: number, deltaLinesY: number, relative?: boolean }) -> ActionResult — scroll at a point; deltas are integers from -10 to 10 with at least one nonzero, and positive values reveal content above or left.
- drag(from: [number, number], to: [number, number], options?: { mouseButton?: 'left' | 'right' | 'middle', modifiers?: ('command' | 'control' | 'option' | 'shift' | 'fn')[], relative?: boolean }) -> ActionResult — drag between two points of one window.
A string target is an element id; coordinate targets are [x, y] and are pixels in the latest returned screenshot by default. With relative: true they are integer thousandths of the window width and height from 0 to 999, based on the current AX observation or screenshot. Left clicks support one or two presses; right and middle clicks support one. FOREGROUND_REQUIRED means explain the need, then explicitly activate or raise and observe again; activation is never automatic. Read fresh state after actions, and treat WINDOW_CHANGED, STALE_SNAPSHOT, and SCREENSHOT_REQUIRED as requiring the stated fresh observation or capture before any retry.
A string target is an element id; coordinate targets are [x, y] and are pixels in the latest returned screenshot by default. With relative: true they are integer thousandths of the window width and height from 0 to 999, based on the current AX observation or screenshot. Left clicks support one or two presses; right and middle clicks support one. FOREGROUND_REQUIRED means explain the need, then call app.activate() and observe again; activation is never automatic. Read fresh state after actions, and treat WINDOW_CHANGED, STALE_SNAPSHOT, and SCREENSHOT_REQUIRED as requiring the stated fresh observation or capture before any retry.
## Simulated keyboard
Available at app or window level depending on the platform.
@@ -220,7 +220,7 @@ Available at app or window level depending on the platform.
- paste(text: string, options?: { format?: 'text' | 'html' }) -> ActionResult — deliver text or HTML through the app's Paste command, restoring prior clipboard contents unless another writer changes them; a Paste failure is surfaced without substituting typeText, and PASTE_NOT_CONSUMED means inspect before any retry.
- pressKey(key: string, modifiers?: string[]) -> ActionResult — press and release one complete keystroke. For example pressKey('Tab', ['shift']) or pressKey('+'). Key and modifier names are platform-defined strings. Pass key and modifiers separately; do not pack them into a chord string.
Raw tools use { method, params }: include appHandle for app calls and windowId for window calls, and use snapshotId with elementId for element routes. Object options such as emit and disableDiffing are facade-only: emit is local output control and neither is a raw RPC parameter, so raw callers must follow the raw operation schema for admitted params. getAccessibilityTree and getWindow read through raw getState; getWindows maps to listWindows; getScreenshot maps to screenshot; cua.getState maps to listApps; every other named operation uses the same method name.
Raw tools use { method, params }: include appHandle for app calls and windowId for window calls, and use snapshotId with elementId for element routes. The emit option is facade-only: emit is local output control, so raw callers must follow the raw operation schema for admitted params. getAccessibilityTree and getWindow read through raw getState; getWindows maps to listWindows; getScreenshot maps to screenshot; cua.getState maps to listApps; every other named operation uses the same method name.
```
#### Token 影响
@@ -18,11 +18,11 @@
en: 1435672513328c05
zh: 11e7a1e672410c10
/dsh-cua-native-helper/request-protocol:
en: 3df5f05c01ba39cb
zh: d580288c0d65bdaa
en: 20be674c0592d66d
zh: e26112b908187acc
/dsh-cua-native-helper/observation-and-input:
en: c633136e2dae9333
zh: be54a0f98705a066
en: bf1fa9dbe1cb6dfe
zh: 14097def08a240a4
/dsh-cua-native-helper/verification:
en: 77b266136d5e3631
zh: a5f135e1b12f7a52
@@ -65,7 +65,7 @@ Operation frames remain `{ "id": "opaque request id", "method": "method name", "
Screenshot diagnostics use `type: "diagnostic"` frames on the originating connection. Records contain requestId, stage, elapsedMs since receipt and optional errorCode, without window titles, AX text or image bytes. The stdin/stdout proxy forwards them to stderr with `[cua-screenshot]`; operation replies remain on stdout. The Host retains a bounded diagnostic buffer and includes it in screenshot failures.
Successful `activate` results include `foreground` alongside `{dispatched: true}`. It records `requestId`, `operation`, `targetPid`, nullable `targetWindowId`, `activationRequested`, `raiseRequested`, and immediate `before`/`after` observations. `frontPid` comes from WindowServer; `focusedWindowId` is the target app's AX-focused window; `frontWindowId` and `frontWindowPid` identify the first visible normal window. Unknown values are null. Observations are sequential, not atomic or proof of causality. Tool-owned requests use the logged tool call ID. Failed operations retain the existing error response and may have partial effects.
Successful `activate` returns `dispatched` and `foregroundChanged`. A known change of front process or window yields true, confirmed equality of both yields false, and incomplete evidence yields null. The raw `foreground` record includes the request ID, target PID, whether activation was requested, and the sampled processes and windows before and after the request. Other desktop activity can occur between samples.
| Method | Result |
| --- | --- |
@@ -115,6 +115,8 @@ System file-panel keyboard routing resolves the remote recipient through the sel
Keyboard modifier names are plain strings. The shared object API passes the names through unchanged; the macOS helper accepts `command`/`cmd`, `shift`, `option`/`alt`, `control`/`ctrl`, and `fn`, and rejects an unknown name with `INVALID_ARGUMENT`. It does not parse chord strings.
Tree observation and action discovery treat a generic AX failure reading one element’s action names as missing metadata. Discovery skips attributes whose writability cannot be determined. `invoke` requires a successful or unsupported-action query before reporting that no default action exists. `setValue` attempts a write only when the element confirms that `AXValue` is writable.
`selectText` requires a fresh element snapshot and writable AXSelectedTextRange. It matches one literal occurrence, narrowed by immediately adjacent prefix/suffix, and selects text or places the cursor before/after it. Matching and reported ranges use UTF-16 offsets. Ambiguous or missing text fails; the helper rechecks the value and selected range without rewriting content. AX text includes available writable, disabled, selected and selection-range information; AXAccess owns element IDs and native references while AXTextFormatter renders the observed values.
`paste` publishes one plain-text or HTML clipboard format, dispatches the application's enabled standard Paste menu command, and waits for a clipboard data request. It then restores the preserved formats only while its ownership marker and change count still match. The Host serializes its pastes across helpers with a file lock. maxClipboardBytes bounds both input and preserved contents; unsupported menus and oversized data fail before dispatch. A later copy is preserved. Cancellation restores owned clipboard data when possible; forced termination cannot guarantee restoration, and a data request does not prove the target completed editing. After dispatch, `pasteReadTimeoutMs` bounds waiting for clipboard consumption. Expiry returns `PASTE_NOT_CONSUMED`, closes the promised data provider, and restores the original clipboard only while still owned. The failure does not prove the app will never process its queued Paste command; inspect the target before deciding whether to retry.
@@ -65,7 +65,7 @@ AppKit 在主事件循环上运行。阻塞式 socket 接收与握手使用后
截图诊断通过原始连接发送 `type: "diagnostic"` 帧。记录包含 requestId、stage、从接收开始计算的 elapsedMs 和可选的 errorCode,不含窗口标题、AX 文本或图片字节。stdin/stdout 代理在 stderr 上以 `[cua-screenshot]` 前缀转发诊断,操作回复仍写入 stdout。Host 保留有上限的诊断缓冲区,并将其附在截图失败中。
成功的 `activate` 结果在 `{dispatched: true}` 之外包含 `foreground`,记录 `requestId`、`operation`、`targetPid`、可空的 `targetWindowId`、`activationRequested`、`raiseRequested` 和即时的 `before`/`after` 观察。`frontPid` 来自 WindowServer,`focusedWindowId` 为目标应用的 AX 焦点窗口,`frontWindowId` 和 `frontWindowPid` 标识最前面的可见普通窗口。未知值为 null。观察依次取得,不具备原子性,也不证明因果关系。工具发起的请求使用日志中的工具调用 ID。失败操作保留原有错误响应,可能已产生部分效果。
成功的 `activate` 返回 `dispatched` 和 `foregroundChanged`。观察到前台进程或窗口变化时,后者为 true,两者均确认相同时为 false,证据不足时为 null。原生结果还包含 `foreground`,记录请求 ID、目标 PID、是否请求激活以及操作前后的进程和窗口。这些采样可能与其他桌面操作交错。
| 方法 | 结果 |
| --- | --- |
@@ -115,6 +115,8 @@ AppKit 在主事件循环上运行。阻塞式 socket 接收与握手使用后
键盘修饰键名称是普通字符串。共享对象 API 原样传递名称;macOS helper 接受 `command`/`cmd`、`shift`、`option`/`alt`、`control`/`ctrl` 和 `fn`,未知名称返回 `INVALID_ARGUMENT`。它不解析组合键字符串。
读取树或枚举动作时,某个元素的动作名查询返回一般性 AX 失败,helper 将其视为缺少动作元数据。枚举时跳过无法确认可写性的属性。`invoke` 需要成功查询元素动作才能报告没有默认动作。`setValue` 仅在元素确认 `AXValue` 可写时尝试写入。
`selectText` 要求新近元素快照和可写 AXSelectedTextRange。它匹配唯一的字面文本,可用紧邻的 prefix/suffix 缩小范围,然后选中文本或将光标放在前后。匹配和返回范围采用 UTF-16 偏移。歧义或缺失文本会失败,helper 不改写内容,并重新核实值和选区。AX 文本包含可用的可写性、禁用状态、选中状态和选区信息;AXAccess 持有元素 ID 和原生引用,AXTextFormatter 渲染已观察的值。
`paste` 提供一种纯文本或 HTML 剪贴板格式,调用应用已启用的标准 Paste 菜单命令,并等待剪贴板数据请求。随后仅在所有权标记和修改计数仍匹配时恢复原有格式。Host 通过文件锁串行化不同 helper 的粘贴。maxClipboardBytes 同时限制输入和保留内容,不支持的菜单和过大的数据在分派前失败。后续复制会被保留。取消会尽可能恢复仍持有的剪贴板数据,强制结束不保证恢复,数据请求也不证明目标已完成编辑。 分派完成后,`pasteReadTimeoutMs` 限制等待剪贴板读取的时间。到期返回 `PASTE_NOT_CONSUMED`,关闭延迟数据提供方,并仅在仍持有剪贴板时恢复原内容。该失败不能证明应用以后不会处理已排队的 Paste 命令;决定是否重试前先检查目标内容。
@@ -33,6 +33,12 @@ struct ForegroundObservation: Sendable {
let focusedWindowID: UInt32?
let frontWindowID: UInt32?
let frontWindowPID: Int32?
static func change(beforePID: Int32?, beforeWindow: UInt32?, afterPID: Int32?, afterWindow: UInt32?) -> JSONValue {
if let beforePID, let afterPID, beforePID != afterPID { return .bool(true) }
if let beforeWindow, let afterWindow, beforeWindow != afterWindow { return .bool(true) }
guard beforePID != nil, afterPID != nil, beforeWindow != nil, afterWindow != nil else { return .null }
return .bool(false)
}
var json: JSONValue {
.object(["frontPid": frontPID.map { .number(Double($0)) } ?? .null,
"focusedWindowId": focusedWindowID.map { .number(Double($0)) } ?? .null,
@@ -266,10 +272,8 @@ actor AXWorker {
try NativePlatform.requireUnlocked(observeSession())
let afterPID = NativePlatform.frontPID()
let afterWindow = NativePlatform.windows().first { $0.isOnScreen }?.windowID
let changed: JSONValue
if let beforePID, let afterPID, let beforeWindow, let afterWindow {
changed = .bool(beforePID != afterPID || beforeWindow != afterWindow)
} else { changed = .null }
let changed = ForegroundObservation.change(beforePID: beforePID, beforeWindow: beforeWindow,
afterPID: afterPID, afterWindow: afterWindow)
return .result(.object(["dispatched": .bool(true),
"windowId": restored.map { .number(Double($0)) } ?? .null, "foregroundChanged": changed]))
case "activate":
@@ -7,6 +7,9 @@ struct RequestParameters: Sendable {
init(_ request: NativeRequest) throws {
guard let values = request.params.object else { throw Self.invalid("params must be an object") }
guard let allowed = Self.fields[request.method] else { throw NativeFailure(code: "UNSUPPORTED", message: "Unsupported native operation: \(request.method)") }
if ["typeText", "pressKey", "paste"].contains(request.method), values["windowId"] != nil {
throw Self.invalid("macOS keyboard input follows application focus; use the App object")
}
guard Set(values.keys).isSubset(of: allowed) else { throw Self.invalid("Unknown operation parameter") }
for (field, allowed) in NativeProtocol.stringEnums[request.method] ?? [:] {
guard let value = values[field] else { continue }
@@ -237,10 +237,11 @@ import Darwin
activationRequested = true
}
let after = try await worker.foregroundObservation(plan)
return .object(["dispatched": .bool(true), "foreground": .object([
"requestId": .string(requestID), "operation": .string("activate"),
"targetPid": .number(Double(plan.identity.pid)), "targetWindowId": .null,
"before": before.json, "after": after.json, "activationRequested": .bool(activationRequested), "raiseRequested": .bool(false),
let changed = ForegroundObservation.change(beforePID: before.frontPID, beforeWindow: before.frontWindowID,
afterPID: after.frontPID, afterWindow: after.frontWindowID)
return .object(["dispatched": .bool(true), "foregroundChanged": changed, "foreground": .object([
"requestId": .string(requestID), "targetPid": .number(Double(plan.identity.pid)),
"before": before.json, "after": after.json, "activationRequested": .bool(activationRequested),
])])
}
private func transportFailed(_ failure: NativeFailure) {
@@ -53,7 +53,7 @@ final class WindowPointerInput {
/// Input-transparent windows do not obscure a target; AppKit owns mouse hit testing.
static func requireSystemPointerTarget(_ route: WindowRoute, frontPID: Int32?, hitWindowID: Int) throws {
guard frontPID == route.owner.pid, hitWindowID == Int(route.windowID) else {
throw NativeFailure(code: "FOREGROUND_REQUIRED", message: "System-pointer hover requires foreground PID \(route.owner.pid) and mouse-hit window \(route.windowID); observed foreground PID \(frontPID.map(String.init) ?? "unknown") and mouse-hit window \(hitWindowID). Activate or raise the target explicitly, then observe again")
throw NativeFailure(code: "FOREGROUND_REQUIRED", message: "System-pointer hover requires foreground PID \(route.owner.pid) and mouse-hit window \(route.windowID); observed foreground PID \(frontPID.map(String.init) ?? "unknown") and mouse-hit window \(hitWindowID). Call app.activate() or use performAction with an advertised AXRaise, then observe again")
}
}
@@ -406,7 +406,8 @@
}
]
}
}
},
"$id": "urn:deepseek-harness:cua:request:performAction"
},
{
"type": "object",
@@ -783,7 +784,8 @@
}
]
}
}
},
"$id": "urn:deepseek-harness:cua:request:setValue"
},
{
"type": "object",
@@ -56,6 +56,11 @@ def cases():
]
result.append((request("hover-button", "mouseMove", {"mouseButton": "left"}), "hover-button", "INVALID_ARGUMENT",
"Unknown operation parameter"))
for method in ("typeText", "pressKey", "paste"):
identifier = "window-keyboard-" + method
argument = {"key": "Return"} if method == "pressKey" else {"text": "do not dispatch"}
result.append((request(identifier, method, {"appHandle": "never-selected", "windowId": 1, **argument}),
identifier, "INVALID_ARGUMENT", "macOS keyboard input follows application focus; use the App object"))
for index in range(24):
identifier = f"ordered-{index:02d}"
method = f"protocol-absent-{index:02d}"
@@ -6,3 +6,10 @@ import Testing
#expect(observed.json == .object(["frontPid": .null, "focusedWindowId": .null, "frontWindowId": .null, "frontWindowPid": .null]))
#expect(AXWorker.actionResult == .object(["dispatched": .bool(true)]))
}
@Test func reportsOnlyObservableForegroundChanges() {
#expect(ForegroundObservation.change(beforePID: 1, beforeWindow: nil, afterPID: 2, afterWindow: nil) == .bool(true))
#expect(ForegroundObservation.change(beforePID: nil, beforeWindow: 3, afterPID: nil, afterWindow: 4) == .bool(true))
#expect(ForegroundObservation.change(beforePID: 1, beforeWindow: 3, afterPID: 1, afterWindow: 3) == .bool(false))
#expect(ForegroundObservation.change(beforePID: 1, beforeWindow: nil, afterPID: 1, afterWindow: 3) == .null)
}
@@ -61,6 +61,7 @@
"@deepseek-ai/dsh-cua": "workspace:*"
},
"devDependencies": {
"ajv": "^8.20.0",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cordis-plugin-include": "workspace:~",
@@ -2,7 +2,7 @@
import { COMMON_GUIDANCE } from '@deepseek-ai/dsh-cua/guidance'
/** macOS operation levels, permissions, selectors, and native measurement units. */
export const PLATFORM_GUIDANCE = 'macOS: accessibility tree operations are available at app or window level. Simulated keyboard input is available at app level. Simulated mouse input is available at window level. Use window coordinates for click and mouseMove on macOS. The helper rejects element-targeted pointer requests carrying the window ID supplied by these object methods. For an element’s default action, use invoke with its latest ID. getApp also accepts bundle IDs and .app paths, and findApps searches /Applications, /System/Applications, and ~/Applications. Use the Computer Use permission page to grant the helper application Accessibility and Screen Recording permissions in System Settings. Native window and AX bounds use macOS points, while delivered screenshots carry their own pixel dimensions. show does not switch Spaces, and Stage Manager can present reduced window geometry; explain any required foreground step before activate or raise.'
export const PLATFORM_GUIDANCE = 'macOS: accessibility tree operations are available at app or window level. Simulated keyboard input is available at app level. Simulated mouse input is available at window level. Use window coordinates for click and mouseMove on macOS. The helper rejects element-targeted pointer requests carrying the window ID supplied by these object methods. For an element’s default action, use invoke with its latest ID. getApp also accepts bundle IDs and .app paths, and findApps searches /Applications, /System/Applications, and ~/Applications. Use the Computer Use permission page to grant the helper application Accessibility and Screen Recording permissions in System Settings. Native window and AX bounds use macOS points, while delivered screenshots carry their own pixel dimensions. show does not switch Spaces, and Stage Manager can present reduced window geometry; explain any required foreground step before calling app.activate() or performing an advertised AXRaise action.'
/** Complete model instructions for this provider. */
export const GUIDANCE = `${PLATFORM_GUIDANCE}
@@ -46,7 +46,7 @@ it('enumerates every available method once in its catalog entry', () => {
})
it('documents option fields and return fields', () => {
for (const token of ['emit', 'includeMenus', 'details', 'disableDiffing', 'relative', 'mouseButton', 'clickCount', 'holdMs', 'modifiers', 'format', 'prefix', 'suffix', 'selectionType']) expect(COMMON_GUIDANCE).toContain(token)
for (const token of ['emit', 'includeMenus', 'details', 'relative', 'mouseButton', 'clickCount', 'holdMs', 'modifiers', 'format', 'prefix', 'suffix', 'selectionType']) expect(COMMON_GUIDANCE).toContain(token)
for (const token of ['dispatched', 'foregroundChanged', 'hoverRoute', 'systemPointerMoved', 'bytes', 'mimeType', 'pixelWidth', 'pixelHeight', 'windowId']) expect(COMMON_GUIDANCE).toContain(token)
})
@@ -124,5 +124,5 @@ it('documents facade initialization, emitted observations, and separate key argu
})
it('omits stale names and unavailable platform operations', () => {
for (const stale of ['getAXState', 'performSecondaryAction', 'hover(', 'keyChord', 'sendKeyEvent', 'window.typeText', 'window.pressKey', 'elementIndex', 'verified: false', 'browser tabs', 'not implemented']) expect(GUIDANCE).not.toContain(stale)
for (const stale of ['getAXState', 'disableDiffing', 'performSecondaryAction', 'hover(', 'keyChord', 'sendKeyEvent', 'window.typeText', 'window.pressKey', 'elementIndex', 'verified: false', 'browser tabs', 'not implemented']) expect(GUIDANCE).not.toContain(stale)
})
@@ -1,5 +1,6 @@
/** Native admission artifacts follow the model schema without exposing Host-only operations. */
import { readFile } from 'node:fs/promises'
import { Ajv2020 } from 'ajv/dist/2020.js'
import { describe, expect, it } from 'vitest'
import { renderNativeProtocol } from '../native/generate-protocol.ts'
import { Request } from '@deepseek-ai/dsh-cua/protocol'
@@ -11,6 +12,15 @@ describe('native protocol generation', () => {
expect(await readFile(new URL('../native/Sources/CuaNative/NativeProtocol.generated.swift', import.meta.url), 'utf8')).toBe(generated.swift)
})
it('resolves recursive JSON arguments in the complete native request schema', () => {
const validate = new Ajv2020().compile(JSON.parse(renderNativeProtocol().json))
const target = { appHandle: 'app', snapshotId: 'snapshot', elementId: '0' }
const value = { values: [null, { nested: [false, 7] }] }
expect(validate({ id: 'value', method: 'setValue', params: { ...target, value } })).toBe(true)
expect(validate({ id: 'action', method: 'performAction', params: { ...target, action: 'AXTextArea.AXValue.Set', argument: value } })).toBe(true)
expect(validate({ id: 'invalid', method: 'setValue', params: target })).toBe(false)
})
it.each([
{ method: 'getState', params: { appHandle: 'app', maxElements: 1 } },
{ method: 'drag', params: { appHandle: 'app', windowId: 1, fromX: 1, fromY: 1, toX: 2, toY: 2, steps: 1 } },
@@ -493,6 +493,33 @@ it('maps the actual model-request image pixels back to native points without sto
expect(native.request).toHaveBeenLastCalledWith('click', { appHandle: 'app-one', windowId: 7, x: 399, y: 199 }, expect.any(AbortSignal), expect.any(String))
})
it.each([{ appHandle: 'foreign', windowId: 7 }, { appHandle: 'app-one', windowId: null }])(
'rejects a helper observation for the wrong target before releasing its contents: %j', async (identity) => {
const f = await setup()
await selectApplication(f)
native.request.mockResolvedValueOnce({ ...identity, snapshotId: 'other', tree: 'foreign content', truncated: false, windows: [] })
const result = await f.execute('getState', { appHandle: 'app-one', windowId: 7 })
expect(result.isError).toBe(true)
expect(JSON.stringify(result.content)).toContain('NATIVE_PROTOCOL')
expect(JSON.stringify(result.content)).not.toContain('foreign content')
},
)
it('rejects a screenshot from another window before attachment admission', async () => {
const f = await setup({ provider: 'image-fixture', model: 'vision' })
await selectApplication(f)
f.ctx.llm.registerAdapter(['image-fixture'], new ImageAdapter())
const root = await mkdtemp(join(tmpdir(), 'dsh-cua-wrong-window-'))
onTestFinished(async () => { try { await f.ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) } })
await f.ctx.plugin(LocalAttachmentStore, { dshHome: root })
const save = vi.spyOn(f.ctx.attachments, 'saveImages')
native.request.mockResolvedValueOnce({ ...imageMetadata, windowId: 8, mimeType: 'image/png', data: imageData })
const result = await f.execute('screenshot', { appHandle: 'app-one', windowId: 7 })
expect(result.isError).toBe(true)
expect(JSON.stringify(result.content)).toContain('NATIVE_PROTOCOL')
expect(save).not.toHaveBeenCalled()
})
it.each([{ pixelWidth: 2 }, { pixelHeight: 2 }])('rejects native screenshot dimensions that disagree with decoded bytes: %j', async (invalid) => {
const f = await setup({ provider: 'image-fixture', model: 'vision' })
await selectApplication(f)
+4
View File
@@ -123,6 +123,9 @@ function createCuaObjects(call: ReturnType<typeof validatedCall>, sink: CuaObser
...options?.includeMenus === undefined ? {} : { includeMenus: options.includeMenus },
...options?.details === undefined ? {} : { details: options.details },
}))
if (state.appHandle !== target.appHandle || state.windowId !== (target.windowId ?? null)) {
throw new Error('Native observation belongs to another application or window')
}
observed.set(target.appHandle, { snapshotId: state.snapshotId, windowId: target.windowId })
if (options?.emit !== false) await sink?.text(state.tree)
return state.tree
@@ -185,6 +188,7 @@ function createCuaObjects(call: ReturnType<typeof validatedCall>, sink: CuaObser
...base.surface, windowId,
async getScreenshot(options) {
const image = ScreenshotResult.parse(await base.call('screenshot', {}))
if (image.windowId !== windowId) throw new Error('Native screenshot belongs to another window')
const screenshot: CuaScreenshot = {
bytes: Uint8Array.from(Buffer.from(image.data, 'base64')),
mimeType: image.mimeType,
@@ -33,6 +33,14 @@
}
.applicationBadge img { flex-shrink: 0; object-fit: contain; }
.applicationBadge > span { min-width: 0; overflow-wrap: anywhere; }
.skeletonCopy strong, .skeletonCopy .applicationBadge {
color: transparent;
background: var(--dsw-alias-bg-skeleton);
border-radius: var(--dsw-radius-xs);
}
.skeletonCopy p { color: transparent; }
.appPlaceholder { width: 160px; height: 1.6em; }
.pendingHint { visibility: hidden; }
.permissionAction, .buttonPlaceholder { width: 168px; flex-shrink: 0; }
.permissionAction[data-configured]:disabled { opacity: 1; color: var(--dsw-alias-label-primary); cursor: default; }
.buttonPlaceholder, .appsPlaceholder {
@@ -96,17 +96,20 @@ function Applications({ title, apps, icons, onAction, label, disabled }: {
</section>
}
function PermissionDescription({ application, permission, t }: Pick<PermissionProps, 't'> & {
function PermissionDescription({ application, permission, loading = false, t }: Pick<PermissionProps, 't'> & {
application: Extract<CuaSystemPermissionState, { platform: 'darwin' }>['launchApplication']
permission: 'accessibility' | 'screenRecording'
loading?: boolean
}) {
if (application === undefined) return <p>{t(permission === 'accessibility' ? 'accessibilityUse' : 'screenRecordingUse')}</p>
if (application === undefined && !loading) return <p>{t(permission === 'accessibility' ? 'accessibilityUse' : 'screenRecordingUse')}</p>
const [before, after] = t(permission === 'accessibility' ? 'accessibilityPermission' : 'screenRecordingPermission').split('{application}')
return <p>
{before}
<span className={css.applicationBadge}>
{application.icon !== undefined && <img src={application.icon} alt="" width={20} height={20} draggable={false} />}
<span>{application.name}</span>
{application === undefined ? <span className={css.appPlaceholder} /> : <>
{application.icon !== undefined && <img src={application.icon} alt="" width={20} height={20} draggable={false} />}
<span>{application.name}</span>
</>}
</span>
{after}
</p>
@@ -126,14 +129,16 @@ function PermissionAction({ permission, granted, busy, request, openSettings, t
}
function UnknownStatus({ loading, t }: Pick<PermissionProps, 't'> & { loading: boolean }) {
return (['interaction', 'capture'] as const).map(kind => <div className={css.permission} key={kind}
role="group" aria-label={t(kind === 'interaction' ? 'applicationInteraction' : 'windowsCapture')}>
<div>
<strong>{t(kind === 'interaction' ? 'applicationInteraction' : 'windowsCapture')}</strong>
<p>{t(kind === 'interaction' ? 'accessibilityUse' : 'screenRecordingUse')}</p>
return (['accessibility', 'screenRecording'] as const).map((permission) => {
const use = t(permission === 'accessibility' ? 'accessibilityUse' : 'screenRecordingUse')
return <div className={css.permission} key={permission} role="group" aria-label={use}>
{loading ? <div className={css.skeletonCopy} aria-hidden="true">
<strong>{t(permission)}</strong>
<PermissionDescription application={undefined} permission={permission} loading t={t} />
</div> : <div><strong>{use}</strong></div>}
{loading ? <span className={css.buttonPlaceholder} aria-hidden /> : <span>{t('unknown')}</span>}
</div>
{loading ? <span className={css.buttonPlaceholder} aria-hidden /> : <span>{t('unknown')}</span>}
</div>)
})
}
function WindowsStatus({ system, uncertain, t }: Pick<PermissionProps, 't'> & {
@@ -202,9 +207,9 @@ export function PermissionPanel(props: PermissionPanelProps) {
busy={busy} request={props.request} openSettings={props.openSettings} t={t} />
</div>)}
</div>
{observed !== null && <p>{observed.platform === 'win32'
<p className={observed === null ? css.pendingHint : undefined} aria-hidden={observed === null || undefined}>{observed?.platform === 'win32'
? <>{t('windowsInstructions')} {t(observed.elevated ? 'elevated' : 'standardPrivileges')}</>
: t('permissionHint')}</p>}
: t('permissionHint')}</p>
</section>
<section className={css.panel} aria-label={t('applicationsTitle')}>
<h3>{t('applicationsTitle')}</h3>
+3 -3
View File
@@ -22,7 +22,7 @@ Setup and discovery:
Available at app or window level depending on the platform.
Element ids are opaque strings from the latest successful tree; the object facade keeps one current snapshot per app across its repeated app and window objects, a new read replaces it, and a failed read invalidates the previous one. Repeated strings can appear for different elements, so do not reuse an old id blindly, and element operations do not fall back to pointer input or typing.
- getAccessibilityTree(options?: { emit?: boolean, includeMenus?: boolean, details?: boolean, disableDiffing?: boolean }) -> string — read the full tree text; includeMenus expands menu contents and details adds identifiers, native bounds, and complete action names.
- getAccessibilityTree(options?: { emit?: boolean, includeMenus?: boolean, details?: boolean }) -> string — read the full tree text; includeMenus expands menu contents and details adds identifiers, native bounds, and complete action names.
- listActions(elementId: string) -> string[] — list the actions an element advertises.
- invoke(elementId: string) -> ActionResult — invoke the element's default action; dispatched is false when it has none.
- performAction(elementId: string, action: string, argument?: JsonValue) -> ActionResult — perform an observed action with an optional direct JSON argument.
@@ -37,7 +37,7 @@ Use an explicit window for screenshots and pointer input.
- scroll(options: { x: number, y: number, deltaLinesX: number, deltaLinesY: number, relative?: boolean }) -> ActionResult — scroll at a point; deltas are integers from -10 to 10 with at least one nonzero, and positive values reveal content above or left.
- drag(from: [number, number], to: [number, number], options?: { mouseButton?: 'left' | 'right' | 'middle', modifiers?: ('command' | 'control' | 'option' | 'shift' | 'fn')[], relative?: boolean }) -> ActionResult — drag between two points of one window.
A string target is an element id; coordinate targets are [x, y] and are pixels in the latest returned screenshot by default. With relative: true they are integer thousandths of the window width and height from 0 to 999, based on the current AX observation or screenshot. Left clicks support one or two presses; right and middle clicks support one. FOREGROUND_REQUIRED means explain the need, then explicitly activate or raise and observe again; activation is never automatic. Read fresh state after actions, and treat WINDOW_CHANGED, STALE_SNAPSHOT, and SCREENSHOT_REQUIRED as requiring the stated fresh observation or capture before any retry.
A string target is an element id; coordinate targets are [x, y] and are pixels in the latest returned screenshot by default. With relative: true they are integer thousandths of the window width and height from 0 to 999, based on the current AX observation or screenshot. Left clicks support one or two presses; right and middle clicks support one. FOREGROUND_REQUIRED means explain the need, then call app.activate() and observe again; activation is never automatic. Read fresh state after actions, and treat WINDOW_CHANGED, STALE_SNAPSHOT, and SCREENSHOT_REQUIRED as requiring the stated fresh observation or capture before any retry.
## Simulated keyboard
Available at app or window level depending on the platform.
@@ -45,4 +45,4 @@ Available at app or window level depending on the platform.
- paste(text: string, options?: { format?: 'text' | 'html' }) -> ActionResult — deliver text or HTML through the app's Paste command, restoring prior clipboard contents unless another writer changes them; a Paste failure is surfaced without substituting typeText, and PASTE_NOT_CONSUMED means inspect before any retry.
- pressKey(key: string, modifiers?: string[]) -> ActionResult — press and release one complete keystroke. For example pressKey('Tab', ['shift']) or pressKey('+'). Key and modifier names are platform-defined strings. Pass key and modifiers separately; do not pack them into a chord string.
Raw tools use { method, params }: include appHandle for app calls and windowId for window calls, and use snapshotId with elementId for element routes. Object options such as emit and disableDiffing are facade-only: emit is local output control and neither is a raw RPC parameter, so raw callers must follow the raw operation schema for admitted params. getAccessibilityTree and getWindow read through raw getState; getWindows maps to listWindows; getScreenshot maps to screenshot; cua.getState maps to listApps; every other named operation uses the same method name.`
Raw tools use { method, params }: include appHandle for app calls and windowId for window calls, and use snapshotId with elementId for element routes. The emit option is facade-only: emit is local output control, so raw callers must follow the raw operation schema for admitted params. getAccessibilityTree and getWindow read through raw getState; getWindows maps to listWindows; getScreenshot maps to screenshot; cua.getState maps to listApps; every other named operation uses the same method name.`
+4
View File
@@ -224,6 +224,7 @@ export function installCua(ctx: Context, config: CuaConfig, backend: CuaBackend)
if (method === 'screenshot') {
const { attachments, target } = await screenshotStore(ctx, execution)
const screenshot = NativeScreenshotResult.parse(await requestNative())
if (screenshot.windowId !== parsedParams.windowId) throw new NativeError('NATIVE_PROTOCOL', 'Native screenshot belongs to another window')
signal.throwIfAborted()
const stage = async <T>(name: string, operation: () => Promise<T>): Promise<T> => {
try { return await operation() } catch (error) {
@@ -282,6 +283,9 @@ export function installCua(ctx: Context, config: CuaConfig, backend: CuaBackend)
}
if (method === 'getState') {
const state = StateResult.parse(result)
if (state.appHandle !== parsedParams.appHandle || state.windowId !== (parsedParams.windowId ?? null)) {
throw new NativeError('NATIVE_PROTOCOL', 'Native observation belongs to another application or window')
}
execution.coordinates.observe(state.appHandle, state.windows, state.windowId ?? undefined)
return state
}
@@ -49,6 +49,8 @@ export function nativeProtocol(permissionFields: Partial<Record<'requestPermissi
}
schema.properties = { id: { type: 'string' }, ...schema.properties }
schema.required = ['id', 'method', 'params']
// Each embedded resource resolves its recursive JSON value references against its own definitions.
if (schema.$defs) schema.$id = `urn:deepseek-harness:cua:request:${method}`
delete schema.$schema
const names = new Set<string>()
const values = new Map<string, Set<string>>()
+7 -9
View File
@@ -459,15 +459,13 @@ describe('application and window observations', () => {
const providerIdentities: Record<string, JsonValue>[] = [
{ appHandle: 'another-app' }, { windowId: 24 },
]
it.each(providerIdentities)('accepts provider state without revalidating identity: %j', async (identity) => {
it.each(providerIdentities)('rejects provider state from another target and discards the old snapshot: %j', async (identity) => {
const f = fixture()
const app = await f.api.getApp('Editor')
f.dispatch.mockResolvedValueOnce(state(identity))
await expect(app.getAccessibilityTree()).resolves.toBe('Changed tree')
await app.setValue(element('0'), 'stale')
expect(f.dispatch).toHaveBeenLastCalledWith({ method: 'setValue', params: {
appHandle: 'app-17', snapshotId: 'changed', elementId: '0', value: 'stale',
} })
await expect(app.getAccessibilityTree()).rejects.toThrow('belongs to another application or window')
expect(f.sink.text).not.toHaveBeenCalled()
await expect(app.setValue(element('0'), 'stale')).rejects.toThrow('Read getAccessibilityTree')
})
it('returns provider window records without process filtering', async () => {
@@ -596,13 +594,13 @@ describe('screenshots', () => {
expect(f.sink.image).toHaveBeenCalledTimes(1)
})
it('returns a screenshot captured from another window without rejecting it', async () => {
it('rejects a screenshot from another window before emitting its image', async () => {
const f = fixture()
const app = await f.api.getApp('Editor')
const window = await app.getWindow(23)
f.dispatch.mockResolvedValueOnce(screenshot({ windowId: 24 }))
await expect(window.getScreenshot()).resolves.toMatchObject({ windowId: 24 })
expect(f.sink.image).toHaveBeenCalledWith(expect.objectContaining({ windowId: 24 }))
await expect(window.getScreenshot()).rejects.toThrow('belongs to another window')
expect(f.sink.image).not.toHaveBeenCalled()
})
it.each(['image/jpeg', 'image/webp', 'image/gif'])('preserves Host-admitted %s bytes and metadata in the return value and image sink', async (mimeType) => {
@@ -146,13 +146,13 @@ it.each([en, zh])('waits for platform identification before offering system perm
const f = fixture()
render(<PermissionPanel {...f.props} t={makeTranslate(dictionary)} />)
expect(screen.queryByRole('group', { name: dictionary.accessibility })).toBeNull()
expect(screen.queryByText(dictionary.permissionHint)).toBeNull()
expect(screen.getByText(dictionary.permissionHint).getAttribute('aria-hidden')).toBe('true')
f.publish({ system: { ...f.state.getSnapshot().system, busy: false, error: 'connection lost' }, busy: false })
expect(screen.getAllByText(dictionary.unknown)).toHaveLength(2)
expect(screen.queryByText(dictionary.granted)).toBeNull()
expect(screen.queryByRole('button', { name: dictionary.request })).toBeNull()
expect(screen.queryByRole('button', { name: dictionary.unknown })).toBeNull()
expect(screen.queryByText(dictionary.permissionHint)).toBeNull()
expect(screen.getByText(dictionary.permissionHint).getAttribute('aria-hidden')).toBe('true')
f.publish({ ...observe({ ...value, system: { ...value.system, accessibility: true, screenRecording: false } }) })
fireEvent.click(screen.getByRole('button', { name: dictionary.request }))
expect(f.props.request).toHaveBeenCalledWith('screenRecording')
@@ -286,7 +286,7 @@ it('keeps all four requirements visible before the first permission response', (
expect(within(requirements).getAllByRole('group')).toHaveLength(4)
expect(screen.queryByRole('alert')).toBeNull()
expect(screen.queryByRole('button', { name: en.request })).toBeNull()
for (const permission of [en.applicationInteraction, en.windowsCapture]) expect(within(screen.getByRole('group', { name: permission })).queryByRole('button')).toBeNull()
for (const permission of [en.accessibilityUse, en.screenRecordingUse]) expect(within(screen.getByRole('group', { name: permission })).queryByRole('button')).toBeNull()
})
it('keeps content mounted and disables actions while management changes are pending', () => {
+3
View File
@@ -10086,6 +10086,9 @@ importers:
'@deepseek-ai/dsh-working-directory':
specifier: workspace:*
version: link:../../session/working-directory
ajv:
specifier: ^8.20.0
version: 8.20.0
sharp:
specifier: ^0.35.3
version: 0.35.5(@types/[email protected])
@@ -153,9 +153,8 @@ function createApplicationState(socket, record, application, captureBroker, conn
application.frontWindowId = window.windowId
}
const after = { frontPid: application.frontPid, focusedWindowId: window.windowId, frontWindowId: application.frontWindowId, frontWindowPid: application.frontPid }
return { dispatched: true, foreground: {
requestId, operation: 'activate', targetPid: app.pid, targetWindowId: null,
before, after, activationRequested, raiseRequested: false,
return { dispatched: true, foregroundChanged: before.frontPid !== after.frontPid || before.frontWindowId !== after.frontWindowId, foreground: {
requestId, targetPid: app.pid, before, after, activationRequested,
} }
}
if (method === 'listWindows') return [window, axlessWindow]