返回 CodeWhale
SKILL.md
1 ---
2 name: computer-use
3 description: Desktop control that picks the right interface per step — app scripting (AppleScript/JXA), accessibility-first observation and actions, pixel fallback, screenshots, zoom, screen recording, and switching between registered computers. Qualified on macOS; Windows, Linux and HarmonyOS backends are experimental.
4 ---
5
6 # Codewhale Computer Use
7
8 ## Computers first
9
10 The plugin controls **computers**, not "the screen". `computer {action:"list"}` shows the
11 registry; one computer is always **active**, and every tool acts on the active
12 computer unless given `computer`.
13
14 A computer is an execution environment, not necessarily the user's desktop.
15 The registry holds two kinds:
16
17 - **Spawned computers are ours.** `computer {action:"spawn", id:"<id>",
18 transport:"docker"}` provisions a disposable Linux desktop container,
19 registers it, and makes it active. Every tool works on it unchanged; the
20 user's own machine is never touched. It is destroyed by `computer
21 {action:"remove"}` or when the session ends. **Prefer a spawned computer
22 for any work that does not need the user's own session** — it is the
23 isolated workspace, not a workaround for sharing theirs politely.
24 - **Registered computers are someone's.** `local` is the machine the plugin
25 runs on — the user's desktop, with their logged-in apps and their pointer.
26 `ssh` computers run the bundled remote agent (pushed automatically at
27 registration). `hdc` computers are HarmonyOS devices driven over hdc.
28 Reach for `local` only when the task genuinely needs that session — their
29 Mail, their signed-in browser, their files on screen. A spawned desktop
30 cannot replace that, and pretending otherwise is the failure mode this
31 distinction exists to prevent.
32
33 Other rules:
34
35 - Pass `computer: "<id>"` on any tool to act on (and stickily switch to) that
36 computer. `computer_switch` changes the active computer without acting.
37 - Every receipt names the computer it happened on. Read it before continuing —
38 never assume the action landed on the machine you meant.
39 - Spawned containers are task-owned: never register one as a normal computer,
40 and never treat its filesystem or state as durable — it dies with the task.
41
42 ## Human controls
43
44 When the local helper is installed, it owns the input route even when the
45 host also includes a native binary. A disconnected helper is an error, never
46 permission to bypass it with direct input. `control_paused` and
47 `control_stopped` mean the person paused or stopped Computer Use. Stop acting
48 and wait for them; do not change environment variables, restart the helper,
49 create another session or use another tool to defeat their choice. After Stop,
50 the old session remains invalid even when the person allows new sessions.
51 The helper's own setup, permission and safety controls belong to the person.
52 Do not operate them or approve the host's pending authorization yourself.
53
54 ## Consent on the user's computer
55
56 The app, not the tool, is the unit of trust on `local`. The first call that
57 targets an application — `open_application`, an `app_ref`, an element or
58 `state_id`, or an action on the bound app — refuses `consent_required`
59 until the user has decided. Ask them, then record the answer:
60
61 - `consent {action:"allow"|"deny", app:"Safari"}` — `app` accepts a name, a
62 bundle id, or `pid:`/a bare number for a pid; `name`, `bundle_id` and
63 `pid` fields work too. Decisions cover this session; `remember:true`
64 persists them for the computer until revoked.
65 - `consent {action:"status"}` — the ledger: every recorded app decision and
66 the foreground decision, each marked session or persisted.
67 `consent {action:"revoke", app:"…"}` clears a decision so the next call
68 asks again.
69 - A deny is a wall, not a hint: every spelling of the same app fails
70 `app_denied` — the ledger folds name, bundle id and pid together, and a
71 denied app cannot be opened, driven, or killed through this surface.
72 Only the user can change it; never work around it.
73
74 Foreground is a second, separate consent. `open_application
75 {activate:true}` — the shared-desktop escalation, on any platform —
76 additionally needs `consent {action:"allow", scope:"foreground"}`; a
77 refusal reads `foreground_consent_required`, a recorded denial
78 `foreground_denied`. Background control (`activate:false`, the default
79 everywhere) needs only the app consent.
80
81 Spawned computers are exempt — a task-owned desktop holds nothing of the
82 user's. Remote computers are covered by their transport's trust, not this
83 ledger. `app_script` keeps its own OS-level consent: Automation prompts
84 belong to macOS, not to this ledger.
85
86 Only in explicitly authorized foreground mode, where a shared surface is taken
87 — a front lease for window-record
88 input, a real-pointer gesture, foreground keys, an activation — the helper
89 first waits for a gap in the person's hardware input rather than cutting
90 between their keystrokes. The wait is bounded, never infinite, and every
91 successful receipt that waited reports `yield_ms`. If no quiet window
92 arrives, `user_busy` means no input was sent: wait for the person to finish
93 or use an already-authorized isolated computer; do not disable the yield
94 or loop on retries. It is turn-taking, not a lock:
95 `user_input_during_lease:true` still means the outcome is contested —
96 say so.
97
98 ## Choose the interface
99
100 Clicking is only one way to use a computer. Before each step, pick the
101 interface that finishes it verifiably with the fewest moving parts — and
102 switch freely between steps:
103
104 1. **The host's own tools** — shell, files, HTTP, git, other MCP apps.
105 A step with no reason to be on screen does not belong to this plugin:
106 never drive a terminal window to run a command the host can run itself.
107 2. **`app_script`** — AppleScript/JXA into apps that ship a scripting
108 dictionary (most native macOS apps). Deterministic, returns values,
109 needs no Accessibility grant, never touches the pointer.
110 3. **`browser`** — CDP for web work: exact selectors, no pixels.
111 4. **Accessibility actions** — the GUI loop below. The route for apps
112 with no better interface: background-safe, element-precise, verified.
113 5. **Coordinates and pixels** — last resort, when nothing else can
114 express the target.
115
116 A step that *can* be clicked still costs more than the same step
117 scripted, and a pixel click's `action_sent` proves less than a script's
118 return value or a `get_value` read-back. Prefer the interface whose
119 receipt can prove the step happened. Switching mid-task is normal —
120 script Mail for the message, process it through the host, type the
121 answer into a GUI-only editor; `get_app_state` still verifies what a
122 script changed.
123
124 ## The GUI loop
125
126 Once the GUI is the right interface: observe once, act once, then verify.
127
128 1. If readiness is unknown, call `request_access` once. It names missing
129 permissions and missing tools per platform, and never pops dialogs. Its
130 `via` field says who holds the permissions: `"app"` means the Codewhale
131 Computer Use desktop app is doing the work (grants belong to it);
132 `"direct"` means the hosting app or terminal is. Follow the actual
133 `appHint`: bundled Codewhale builds already carry their native helper.
134 `app.stale:true` means the running helper reports an older version than the
135 plugin — tell the user to restart the Codewhale Computer Use app before
136 debugging any behavior.
137 2. `list_apps` shows user-facing apps only; pass `all:true` to include
138 menu-bar helpers and background processes. If the user names an app that is
139 absent, call `open_application` once with the original user-provided name,
140 copied character-for-character — including case, spaces, punctuation, and
141 suffixes such as `app` or `.exe`. Do not translate, localize, normalize,
142 shorten, or retry with guesses.
143 3. `get_app_state` defaults to a text-first summary (macOS AX / Windows
144 UIA / Linux AT-SPI / HarmonyOS uitest) with controls, values, actions,
145 layout, element indices and a `state_id`. Start here without a screenshot,
146 whether or not the model supports vision. Pass `query`, `role`, `limit`
147 and `offset` instead of dumping the whole tree — truncated dumps hide the
148 title and search field. `detail:"compact"` is smaller (same indices,
149 shorter labels). `detail:"full"` adds nested menus and tree paths.
150 `find_elements` searches a cached `state_id` or observes now. Missing
151 labels or values mean unknown content, not something to guess. `get_value`
152 reads one field live. On macOS, browsers and Electron/webview apps expose
153 page content as `AXWebArea` descendants; the first observation may arrive
154 while the page is still populating — re-observe if the tree looks
155 suspiciously shallow or a control you can see is absent.
156 4. If the tree contains the target, act on the element: `focus` then `type`
157 or `key` for composers (or pass the element `target` straight to
158 `type`/`key` — it focuses first, in the same call), `set_value` for
159 ordinary fields, `perform_action` (AXPress/Invoke/click…) for advertised
160 actions, element click. Newlines in `type` are Return/Enter;
161 `press_enter:true` sends after the text. Never expect `\\n` to send a
162 chat message. `run_actions` batches up to 8 steps
163 (click → type → key return → get_value).
164 macOS provides background element actions; Linux AT-SPI support depends
165 on the control. Windows currently refuses scoped semantic mutations.
166 Windows and Linux are development backends: do not assume their raw
167 input is background-safe or that native Pause/Stop controls are available.
168 5. When accessibility cannot read visible text, macOS supports
169 `get_app_state({app_ref, include_ocr:true})`. Pass `ocr_region:[x,y,w,h]`
170 in screen points to recognize one rect instead of the whole window. This
171 captures locally, without a vision model. Check `ocr.status`; recognized
172 blocks include confidence, pixel bounds and ready-to-use coordinate
173 targets. OCR text is not a control role or an advertised action. Verify
174 uncertain text and observe again after changes. Other platforms return an
175 explicit unavailable status while keeping their accessibility state usable.
176 A text-only model must not infer unlabeled icons, charts or other graphical
177 meaning from OCR or a screenshot file path.
178 With vision, when accessibility cannot express the target: `screenshot`
179 (optionally `zoom` for small targets) and act with a coordinate target.
180 Default coordinates are pixels **in the latest returned raster**. Pass
181 `space:"screen"` to send absolute screen points from the AX tree and skip
182 conversion. After a new screenshot, old raster pixels are stale.
183 If the host reports an omitted or oversized image, capture a smaller app
184 window/region or zoom, then use that returned raster. Do not guess from a
185 file path or reuse coordinates from an image the model never received.
186 5b. When the UI needs time — a page loading, a dialog appearing or
187 dismissing, a spinner finishing — call `wait_for` instead of looping
188 `get_app_state` + `wait` by hand: it polls the accessibility tree until
189 a `query`/`role` match appears (`state:"present"`) or disappears
190 (`state:"absent"`), then returns the matched elements bound to a fresh
191 `state_id` you can target immediately. A `timed_out:true` receipt means
192 the condition never held — observe and reconsider rather than repeating
193 the same wait.
194 6. Verify with a fresh observation or a task oracle before claiming success.
195 `action_sent: true` means it may already have happened — never replay.
196 On macOS `type` also reports `verified`: `false` (with
197 `verification_required: "screenshot"`) means the focused control's value
198 did not reflect the text, so confirm with a screenshot before relying on
199 the input.
200
201 ## Choosing targets
202
203 - Element: `{"type":"element","index":4}` — prefer this. A bare index binds
204 that computer's latest observation; add `state_id` only to pin a specific
205 earlier snapshot (e.g. one returned by `wait_for` after newer observes).
206 Elements are revalidated against the live tree before every action: if the
207 element moved, the click lands on its fresh center and the receipt carries
208 `target_reacquired: true`; if it no longer resolves (or changed role) the
209 call fails `element_stale` — call `get_app_state` again. A `state_id` only
210 works on the computer that issued it (`state_wrong_computer`).
211 - Coordinate: `{"type":"coordinate","x":496,"y":331}` — pixels from the latest
212 raster only; submit `x`/`y` unchanged, never transform them yourself.
213 `{"type":"coordinate","x":100,"y":200,"space":"screen"}` is an absolute
214 screen point (what AX `position` uses). `zoom` returns a bindable raster of
215 its own: after zooming, raster coordinates are pixels in the zoomed image.
216 Points outside the bound raster fail `target_outside_raster` instead of
217 landing somewhere unintended.
218 - Never translate pixels into an element target; never invent `state_id`s.
219
220 ## Raw input reality (read before clicking)
221
222 - macOS: call `open_application` with `activate:false` to bind input to the
223 intended process, even when the app is already running; pass `pid` when two
224 processes share a bundle id. Then the two halves behave differently:
225 - **Keyboard and element actions are quiet.** `type`, `key`, `focus`,
226 `set_value`, `get_value`, `select_text` and `perform_action` reach the
227 bound process without moving the pointer or changing the foreground.
228 Prefer them. Text entry uses writable accessibility selection when
229 available; verify the resulting value. `get_app_state`, `list_windows`
230 and `screenshot` default to the selected app.
231 - **Background mode does not borrow keyboard focus.** Accessibility
232 click, focus, selection and scroll actions remain available. Raw pointer
233 fallbacks, modified/window-targeted keys, web value replacement and typing
234 paths that require a key-window lease refuse `background_focus_required`
235 before delivery. Use an accessibility menu/control, browser control or an
236 authorized separate computer. Do not escalate to foreground or retry the
237 same action merely because the user stopped typing briefly.
238 - Shared-desktop gestures and foreground keyboard delivery require explicit
239 user authorization for exclusive desktop use, followed by
240 `open_application(activate:true)` — which itself needs the foreground
241 consent (`consent {action:"allow", scope:"foreground"}`; see Consent on
242 the user's computer). Do not select it merely to work around a
243 background refusal. Receipts identify `input_scope: "shared-desktop"`;
244 pointer gestures use the physical cursor, even if it is restored afterward.
245 Keys and raw pointer gestures stop when another app takes focus. Never
246 keep reactivating after the user takes control; return to `activate:false`
247 when the shared-desktop step ends.
248 - Menus appear in `get_app_state`. Use the advertised action (often
249 `AXPress` to open a menu, then `AXPick` on its item), then observe again.
250 `invoke_menu {path:["File","New"]}` does that traversal in one call,
251 through accessibility alone — no key events, no focus lease. App-level
252 commands (New, Save, Quit) are reliable without a key window;
253 window-targeted items (Close) can validate against a key window the
254 background app does not have and legitimately no-op — close windows
255 through their close-button element instead. Exact titles only; a present
256 but disabled item is refused (`menu_item_disabled`) rather than pressed.
257 - A pointer gesture is refused when another application's window covers the
258 point; it names the owner. Observe again and use the selected control's
259 accessibility action, or wait for authorized exclusive desktop use. Do not
260 move or close the reported window.
261 - An accessibility press refuses to cross a modal sheet
262 (`window_blocked_by_modal_sheet`): deal with the sheet first.
263 - Virtualized lists vend collapsed placeholder rows (zero-size frames).
264 Acting on one fails `degenerate_frame` — scroll the real row into view
265 and re-observe rather than retrying the same index.
266 - `set_value` coerces numbers for `AXIncrementor`/`AXSlider`/`AXStepper`
267 and verifies the readback. Web-area direct AXValue writes are unreliable;
268 background mode refuses the focus/select-all replacement. Use browser
269 control. The replacement is available only during explicitly authorized
270 foreground control.
271 Use app-scoped screenshots (`app_ref`) to avoid capturing unrelated windows.
272 The nonactivating preview panel is on by default while an app is bound —
273 it shows the captured app window and a drawn cursor at each action's target
274 so the user can watch; the real pointer never moves. `preview(enabled:false)`
275 mutes it for the session. The preview is a local app view, not an isolated
276 desktop; watching it does not authorize shared-desktop control. Process-directed actions still
277 change the target app: do not work in an app the user is actively editing.
278 Close only disposable documents created by your task; never quit a user app.
279 - Windows/Linux: `open_application` still defaults to `activate:false` —
280 Windows launches the app minimized and Linux hands focus back to the
281 previous window — but raw input there is foreground by nature;
282 UIA/AT-SPI element actions are the precise path.
283 - HarmonyOS: `uitest` synthesizes touches; there is no hover or cursor.
284
285 ## Keyboard
286
287 - macOS uses `cmd` (`cmd+c`), Linux/Windows use `ctrl` (`ctrl+c`).
288 - `key` is the key-press tool: `return`, `enter`, `backspace`, `tab`,
289 `escape`, chords and repeats. `key {duration}` holds a key for a duration.
290 - `type` sends unicode. Newlines and `press_enter` become Return; they do
291 not insert a literal line break or U+FFFC.
292 - Prefer `set_value` on ordinary fields; prefer `focus` then `type`/`key`
293 on chat composers.
294
295 ## Recording
296
297 `recording {action:"start"}` → work → `recording {action:"stop", id}` returns the finalized file path.
298 macOS uses ScreenCaptureKit inside the signed helper — no system recorder UI
299 and no desktop dimming overlay (a receipt warning about Screen Recording
300 permission means the user must grant it once). Linux and Windows recording is
301 unavailable pending session-owned cleanup; use screenshots. HarmonyOS uses
302 snapshot-series (no native CLI recorder —
303 the receipt says so). `recording_status` / `recording_list` report bytes and
304 paths. Screenshots land in the same directory.
305
306 ## Scripting apps (macOS)
307
308 `app_script {script, language?, timeout?}` runs AppleScript (default) or
309 JXA (`language:"javascript"`) through osascript on the local computer.
310 `result` is the script's stdout; a non-zero exit fails `script_error`
311 with stderr, and `script_timeout` means the script — or a consent dialog
312 — was still open.
313
314 - A first script targeting an app may show the person an Automation
315 consent dialog; that is their choice, not your error. A declined or
316 missing consent fails `automation_denied` (-1743): name the pane
317 (System Settings → Privacy & Security → Automation) and stop — never
318 retry it away.
319 - Read the dictionary before writing: `sdef /Applications/Mail.app`
320 through the host's shell, or Script Editor's Library window. A guessed
321 property earns `script_error` (-1728/-2740) — check the dictionary,
322 don't retry with another guess.
323 - `tell application "X"` launches X if needed; no `open_application`
324 required, and the script runs while X stays in the background.
325 - `do shell script "…"` inside a script works, but prefer the host's own
326 shell for shell work — keep `app_script` for app control and the parts
327 only a dictionary exposes.
328 - ssh, docker and hdc computers refuse it (`unsupported_on_transport`):
329 remote channels stay computer-use only, never a shell — a spawned
330 desktop is no exception. Windows and Linux backends fail
331 `unsupported_on_backend` for now.
332
333 ## Browser (CDP)
334
335 `browser` drives a Chromium-family browser over the DevTools protocol in a
336 self-owned profile — the user's own browser is never attached to, typed into,
337 or closed. `start` opens (or reuses) the instance and binds this session's
338 own tab; then `navigate`, `click` (CSS selector or viewport point), `type`
339 (optional focus selector, `enter`), `screenshot`, `status`, `stop`. Elements
340 are addressed exactly, no pixels: prefer this over screen clicking for web
341 work. Page screenshots are a different space from screen captures
342 (`space: "page-viewport"`) — coordinate clicks take that space, never screen
343 points. Verify effects by observing: `status` reports the tab's live url and
344 title, and a fresh `screenshot` shows the rendered truth. One tab per
345 session; the last session out closes the shared browser. Node 22+ is needed
346 for the WebSocket transport; older runtimes refuse with `unsupported_runtime`.
347
348 ## Recording and scope
349
350 `trajectory` records every tool call this session makes into a local JSONL
351 (off until started; arguments are stored verbatim, so treat the file as
352 sensitive). `replay` re-runs a recorded file through the same pipeline —
353 grants, permissions and the kill switch still apply — and stops at the first
354 refusal; `dry_run` lists the plan first. A host may narrow the whole session
355 with `CODEWHALE_CU_GRANT` (read-only, or a tool list): tools outside it are
356 never advertised and calls fail `not_granted`. Work inside that scope; do not
357 look for a workaround. `set_window_frame` moves or resizes one window and
358 reports the app's own readback — when an app constrains or refuses part of
359 the frame the receipt says so (`verified:false`, `ax_errors`, or
360 `frame_refused`), and that is the app's answer, not a failure to retry blindly.
361
362 ## Safety
363
364 - `stop_computer_control` is the kill switch; after it, actions fail closed
365 for the session. Do not continue after it or after a denied permission.
366 - `list_sessions` shows the live sessions and the user's control mode. When
367 another model or agent is mid-task on the same machine, coordinate through
368 the person instead of fighting for the same window; `kill_app` quits an app
369 (never the helper itself) and verifies the termination in its receipt.
370 - Never retry a refused action unchanged. Re-observe, choose a fresh target.
371 - If a permission is explicitly denied, tell the user which permission in
372 which Settings pane, and end the turn. Do not promise later retries.
373
374 ## Recipes
375
376 - **Screenshot** — optionally a computer id, display index, or `[x,y,w,h]`
377 region; call `screenshot`; report path, size, computer/display. Black or
378 empty capture means Screen Recording permission is missing (macOS) for the
379 app (`via: "app"`) or the host terminal (`via: "direct"`): say which and
380 stop.
381 - **Record** — `recording {action:"start"}` (parse computer id, fps, display, duration
382 or "record for 30s" → `durationSec` on macOS), then report id, path, mode.
383 To stop, find the running id via `recording {action:"list"}` and call `recording {action:"stop", id}`.
384 - **Switch computers** — `computer {action:"list"}`; if asked to add: ssh `user@host`
385 (agent is pushed automatically) or `hdc [target]` for a HarmonyOS device;
386 otherwise show the registry and remind that any tool accepts `computer`.
387 - **Status** — `computer {action:"list"}`, then `request_access` per computer; call out
388 anything that will fail closed with the exact install hint from the receipt.
389
390 ## References
391
392 The advertised tools are merged for context economy — `click`, `pointer`,
393 `clipboard`, `recording`, `computer`, and `key {duration}` for holds. The
394 per-action wire names (`left_click`, `read_clipboard`, `recording_start`,
395 `computer_list`, `hold_key`, …) remain callable as aliases.
396
397 - `references/quick-reference.md` — every tool on one page, plus the common
398 recipes (type into a field, close a window without borrowing focus,
399 switch apps mid-task).
400 - `references/refusal-codes.md` — the fail-closed codes, what each means,
401 and the move that fixes it.
402
402 lines MARKDOWN