11 KiB
Roadmap: maximal macOS control for the computer tool
Goal: give the agent as much reliable control over macOS as the platform allows, including background control that does not disturb what the user is looking at.
This builds on the v1 computer tool (PR #345): screenshot, coordinate
mouse/keyboard, scroll, AX-tree read, cursor, permission check.
Everything below is implementable in Rust with crates that are already in the
lockfile or available on crates.io (accessibility-sys, screencapturekit,
objc2-app-kit, core-graphics). No Swift/ObjC build step.
The one hard constraint
macOS has one HID cursor and one keyboard-focus shared by the whole login session. Synthetic coordinate input (CGEvent) is therefore always visible: it moves the real cursor and types into the focused app.
Background / not-in-view control must avoid CGEvent and instead go through:
- Accessibility (AX) actions - act on a specific element by reference.
- Apple Events / scripting - drive scriptable apps with no UI.
- Per-window capture - "see" a window without raising it.
True simultaneous "you work + I work independently" needs a separate display or login session (see Tier 4).
Tier 0 - done (v1, PR #345)
screenshot(main display, point/pixel scale aware)move/click/double_click/right_click/drag/scrolltype/key(chords)ui(AX tree read via osascript)cursor,check_permissions
Tier 1 - AX semantic actions ← highest leverage for background control
Read + act on elements by reference, no cursor movement, target app need not be
frontmost. Uses accessibility-sys (AXUIElementPerformAction,
AXUIElementSetAttributeValue, AXUIElementCopyElementAtPosition,
AXUIElementCopyAttributeValue).
find_element { role?, title?, value?, pid?, app? }-> stable element handleselement_at { x, y }-> element under a point (AXUIElementCopyElementAtPosition)press { element }->AXPress(click a button in a background window)set_value { element, value }-> type into a field without focusget_value { element }perform_action { element, ax_action }-> any advertised AX actionselect_menu { app, path: ["File","Export…"] }-> drive the menu bar of any app
Handle format: pid + AX path (index chain) or a session-scoped element id cache,
so the model can act structurally instead of by pixels.
Why it matters: this is the actual "click things you're not looking at" capability.
Tier 2 - app / window / system management
Mostly objc2-app-kit (NSWorkspace, NSRunningApplication) + AX window
attributes + CoreGraphics window list.
list_apps/activate_app { app }/hide_app/quit_applist_windows { pid? }(CGWindowList) with ids, titles, bounds, on/off-screenfocus_window/move_window/resize_window/minimize_window/close_window(AX window actions - can target background windows)window_screenshot { window_id }-> capture a specific window even if occluded (CGWindowListCreateImagenow, ScreenCaptureKit later)spacesawareness (which Space an app is on; activating may switch Spaces - visible)
Tier 3 - clipboard, input fidelity, observation
get_clipboard/set_clipboard { text }(NSPasteboardvia objc2-app-kit)key_down/key_up(hold modifiers, game-style input)type_into { element, text }(AX set value + confirm) for reliability over blind typingwait_for { element|condition, timeout }usingAXObserver*notifications (e.g. wait for a sheet to appear) instead of sleep-and-pollpaste_type { text }- set clipboard + Cmd-V for fast/large text entry
Tier 4 - true background / parallel operation (advanced)
These give genuinely off-screen, non-interfering control. Higher setup cost.
- Apple Events scripting bridge:
run_applescript { script }/run_jxa. Fully headless for scriptable apps (Mail, Notes, Safari, Finder, Music, System Settings panes, Terminal, many pro apps). No cursor, no focus. Per-app Automation permission (prompts on first use). - Virtual / headless display: route the agent's cursor+windows to a second
(virtual) display the user isn't looking at. Options: a virtual display driver
(e.g. BetterDisplay/
CGVirtualDisplayprivate API) or a real unused monitor. Lets the agent move windows there and use coordinate input without touching the user's screen. - Separate login / Screen Sharing session: a second macOS session has its own cursor and focus; the agent drives that one. Strongest isolation, most setup.
- Shortcuts integration: invoke the user's
Shortcutsautomations (shortcuts run …) as high-level, sanctioned actions.
Tier 5 - sensors / extras (optional, opt-in)
ocr { region|window }via Vision framework (read text in images / non-AX apps).screen_record { seconds }short clips via ScreenCaptureKit.- Audio in/out control, notifications,
do_not_disturbtoggling via scripting. - Camera/mic are separate TCC permissions; keep strictly opt-in.
Permissions (TCC) - the gatekeeping reality
| Permission | Unlocks | Auto-grantable? |
|---|---|---|
| Accessibility | CGEvent input, all AX read/act, window control | No - user toggles once (we can prompt + deep-link) |
| Screen Recording | screenshots, window/ocr capture | Request API exists (CGRequestScreenCaptureAccess) |
| Automation (Apple Events) | scripting each app | Prompts per target app on first send |
| Input Monitoring | reading global input stream (only if we add capture) | Request API exists |
Plan: a request_permissions action that calls
AXIsProcessTrustedWithOptions(prompt=true) (adds jcode to the list + shows the
dialog) and deep-links to the exact System Settings pane, then polls
AXIsProcessTrusted(). One prompt + one toggle; never zero-touch for Accessibility
(Apple's anti-malware boundary).
Important: the permission attaches to the host binary/terminal running jcode. For a stable experience we likely want a signed jcode.app with a fixed bundle id so the grant persists across updates (otherwise each new binary path re-prompts).
Safety model (high blast radius)
- Gated like
bash: refuses early if required permission missing. dry_runon mutating actions: resolve + report target without acting.- Prefer AX semantic actions over blind coordinate clicks (auditable, robust).
- Screenshot/element echo on destructive coordinate clicks.
- No global input capture unless explicitly enabled (keeps us out of Input Monitoring by default).
- Per-action audit log; optional allowlist/denylist of target apps.
Suggested build order
- Tier 1 (AX actions) - biggest capability jump, enables background control.
- Tier 2 window mgmt + per-window screenshot - "see and act on hidden windows".
- Tier 3 clipboard + AXObserver waits - reliability.
run_applescript/JXA bridge (Tier 4) - headless scripting for many apps.- Virtual-display / second-session (Tier 4) - true parallel, non-interfering.
- Signed jcode.app bundle for durable permissions.
- Vision OCR (Tier 5) as needed.
Crates
accessibility-sys0.2 (AX read/act/observe) - on crates.ioscreencapturekit7 (modern capture) - on crates.io;core-graphicswindow list as fallbackobjc2-app-kit/objc2-foundation0.3 - already in lockfile (NSWorkspace, NSPasteboard)core-graphics0.23 - already a direct dep (CGEvent, CGWindowList, CGDisplay)
Tool interface design (decided)
Single tool, progressive disclosure
One computer tool, action-dispatched (like browser). To keep always-on
context flat regardless of how many tiers exist, the schema uses progressive
disclosure:
- Always-on core (~370 tokens, measured with tiktoken cl100k_base):
screenshot, ui, ocr, click, type, key, press, set_value, run_applescript, setup, discover. discover { category }returns full specs for advanced actions on demand (mouse|keyboard|ax|windows|apps|clipboard|scripting|displays|system|all), ~130 tokens per category, paid only when used.- Shared handle types (
element,window_id,region) defined once and reused, so params do not multiply with actions.
Measured always-on cost:
| Design | Actions visible | Always-on tokens |
|---|---|---|
| Current v1 tool | 12 | ~720 |
| Flat, all tiers (~46 actions) | 46 | ~1,020 |
| Progressive core | 11 | ~370 |
Background control is a property of the mechanism, not the tier: CGEvent = visible; AX actions (press/set_value/select_menu) + Apple Events = background.
setup / check_permissions action
A first-class setup action that:
- Reports status of every requirement: Accessibility (
AXIsProcessTrusted), Screen Recording (CGPreflightScreenCaptureAccess), Automation (per-app, via first Apple Event), plus install/bundle health. - Requests what it can programmatically:
AXIsProcessTrustedWithOptions(prompt=true)— shows the Accessibility dialog and pre-adds jcode to the list (toggled off).CGRequestScreenCaptureAccess()— prompts for Screen Recording.- First Apple Event to a target app — triggers its Automation prompt.
- Deep-links to the exact System Settings pane for anything still missing:
x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility…?Privacy_ScreenCapture…?Privacy_Automation
- Polls
AXIsProcessTrusted()until granted, then reports "ready".
Hard limit: the Accessibility toggle itself cannot be flipped by any API
(Apple anti-malware boundary). tccutil can only reset, not grant. So the best
achievable UX is "one or two prompts + one toggle," never zero-touch.
Durable permissions: signed app bundle
TCC permissions attach to the running binary's identity. A bare dev/cli binary changes path/signature across updates, so macOS re-prompts every time. To make a grant stick:
- Ship/install jcode as a signed
.appbundle with a stable bundle id (e.g.com.jcode.app) and a Designated Requirement, so the Accessibility / Screen Recording grant persists across updates. setupshould detect "running from an unstable/unsigned path" and offer to install the proper bundle, so the user grants once.
Build order (updated)
- Progressive-disclosure refactor of the v1 tool (core +
discover). setupaction (check + request + deep-link + poll).- Tier 1 AX actions (background control).
- Tier 2 window/app management + per-window screenshot.
- Tier 3 clipboard + AXObserver waits.
run_applescript/JXA bridge (Tier 4 headless scripting).- Signed app bundle for durable permissions.
- Tier 5 OCR (Vision). (Camera/audio intentionally excluded.)
- Virtual-display / second-session for true parallel work (advanced).