6.7 KiB
Proposal: native computer tool for macOS computer use
Summary
Add a single native tool, computer, that lets the agent observe and control
the macOS GUI — screenshots, the accessibility (AX) tree, mouse/keyboard input,
window/app management, and clipboard — through one action-dispatched interface.
This mirrors the existing browser tool (crates/jcode-app-core/src/tool/browser.rs):
one registered tool, an action: String that selects a sub-operation, with optional
typed params. It gives jcode a closed control loop (see screen → decide → act)
without depending on a browser or external automation tooling.
Motivation
- The agent can already drive a browser; it cannot drive native macOS apps, system UI, or anything outside the browser sandbox.
- "Computer use" agents need exactly three primitives: read the screen, read UI structure, and synthesize input. macOS exposes all three through the Accessibility / Quartz Event Services / ScreenCaptureKit stack.
- A single, well-scoped tool keeps the tool surface small and the permission story in one place.
Architecture
crates/jcode-macos-control/ (new) cfg(target_os = "macos") platform crate
└─ AX (accessibility-sys), CGEvent (core-graphics),
CoreFoundation (core-foundation), screenshots (ScreenCaptureKit / CGDisplay),
app/window control (objc2 + objc2-app-kit), clipboard (objc2 NSPasteboard)
crates/jcode-app-core/src/tool/computer.rs (new) ComputerTool
└─ thin dispatch layer: parse input -> call jcode-macos-control -> ToolOutput
└─ registered in crates/jcode-app-core/src/tool/mod.rs base_tools()
- All native APIs are reached through existing Rust bindings (
objc2,accessibility-sys,core-graphics,core-foundation) — no Swift/ObjC build step. - On non-macOS targets the tool still registers but every action returns a clean
unsupported on this platformerror, so the tool list stays stable across OSes. screenshotreturns its image viaToolOutput::with_image(base64), matching howbrowserreturns screenshots today.
Permissions (the important part)
macOS splits this across four TCC permissions. Programmatic request support differs per permission:
| Permission | Used for | Programmatic request |
|---|---|---|
| Accessibility | drive other apps' UI, inject CGEvent input |
⚠️ prompt + deep-link only; user must toggle |
| Screen Recording | screenshots / get_ui_tree of some apps |
✅ CGRequestScreenCaptureAccess() |
| Input Monitoring | reading the global input stream | ✅ IOHIDRequestAccess(...) |
| Automation (Apple Events) | scripting cooperating apps | ✅ prompts on first send, per target app |
Accessibility is the one that cannot be auto-granted (Apple's anti-malware
boundary), and it is required for input injection. Best achievable flow, exposed via
the request_permissions action:
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt: true])— shows the system dialog and auto-adds jcode to the Accessibility list (toggled off).- Deep-link to the exact pane:
open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility". - Poll
AXIsProcessTrusted()until granted, then report ready.
So the experience becomes one prompt + one toggle, not "go hunt in Settings" — but never zero-touch for Accessibility.
check_permissions reports the current state of all four so the agent can tell the
user precisely what is missing before attempting control.
Actions
action (required) selects the operation. Params below are optional and validated
per action.
Permissions
check_permissions→ status of accessibility / screen-recording / input-monitoringrequest_permissions→ prompt + deep-link flow above
Observe
screenshot—{ display?, window_id?, region? }→ imageget_ui_tree—{ pid? | frontmost, max_depth? }→ serialized AX tree (role, title, value, position, size, actions)find_element—{ role?, title?, value?, pid? }→ matching elements + their identifierselement_at—{ x, y }→ element under the point
Mouse
move—{ x, y }click/double_click/right_click—{ x?, y? }(current position if omitted)drag—{ from: [x,y], to: [x,y] }scroll—{ x?, y?, dx, dy }
Keyboard
type—{ text }key—{ keys: "cmd+shift+4" }(chord)key_down/key_up—{ key }
Semantic AX (preferred over raw input when available)
press—{ element }(AXPress)set_value—{ element, value }get_value—{ element }perform_action—{ element, ax_action }select_menu—{ app, path: ["File", "Export…"] }
Window / app
list_apps,activate_app{ app }list_windows{ pid? },focus_window{ window }move_window{ window, x, y },resize_window{ window, w, h }minimize_window/close_window{ window }
Clipboard
get_clipboard,set_clipboard{ text }
Element identifiers:
find_element/get_ui_treereturn stable-enough handles (e.g.pid+ AX path or a session-scoped element id) that semantic actions accept, so the agent can act structurally instead of by pixel coordinates when possible.
Safety
Computer use is high-blast-radius, so:
- Permission-gated like other powerful tools; refuses early with a clear message if Accessibility/Screen Recording is missing.
dry_runparam on mutating actions — resolves and reports the target without acting.- Screenshot-assisted confirmation for destructive coordinate clicks (return the region/element being targeted).
- No global input capture in v1 (we synthesize input but do not log the user's keystrokes), keeping us out of Input Monitoring unless a future feature needs it.
- Prefer semantic AX actions over blind coordinate input wherever the element is resolvable — more robust and more auditable.
Implementation plan
jcode-macos-controlcrate: permissions, screenshot, AX read, AX action, CGEvent input, window/app control, clipboard. Unit-test the pure parts (input parsing, chord parsing, tree serialization).ComputerToolintool/computer.rs: input struct +actiondispatch + schema + description; register"computer"intool/mod.rsbase_tools().- Default-off / gated rollout + docs in
docs/. - Follow-up: Windows/Linux backends behind the same tool surface.
Open questions
- Element handle format —
pid+AX-path vs an opaque session-scoped id cache? - Should
request_permissionsblock-and-poll, or return immediately with status and let the agent re-check? - Default enablement: opt-in flag vs always-registered-but-gated?