Skip to content

fix(cu): bind Maka Computer actions to observed window captures - #699

Closed
hqhq1025 wants to merge 73 commits into
apache:mainfrom
hqhq1025:feat/cu-runtime-helper
Closed

fix(cu): bind Maka Computer actions to observed window captures#699
hqhq1025 wants to merge 73 commits into
apache:mainfrom
hqhq1025:feat/cu-runtime-helper

Conversation

@hqhq1025

@hqhq1025hqhq1025 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Host-level Computer Use execution for macOS, keeping cua-driver as the sole native executor and binding every stateful action to the exact observed frame, window, display transform, and Electron page identity that grounded it.

P0 execution contract

A coordinate is never reinterpreted against the current highest-z window at dispatch time.

observe
-> frameId / epoch
-> bind normalized action to screenshot-time target
-> claim canonical fingerprint
-> validate exact live target
-> background dispatch
-> fresh postcondition observation
-> verify effect
-> invalidate old frame / advance epoch

Model-facing coordinate, type, key, scroll, drag, mouse-move, and zoom calls must echo the latest frame_id + frame_epoch. The runtime creates the action fingerprint before executor dispatch; replay is rejected.

Observation identity

Each screenshot produces a one-shot observation containing:

  • unique frame ID and epoch;
  • capture time and screenshot dimensions;
  • display logical bounds, source-pixel atlas bounds, and scale;
  • layer-0 window PID/window ID/app/title/bounds/z-order;
  • privacy-safe SHA-256 AX content fingerprint;
  • Electron cdpPort + pageTargetId + pageUrl + targetUrlContains;
  • privacy-safe SHA-256 DOM freshness fingerprint.

Equal-scale multi-display screenshots support negative display origins. Mixed-scale or ambiguous screenshot atlases expose only a dimension-proven primary display; unproven displays are unavailable rather than guessed.

Bound action identity

Before entering cua-driver, runtime resolves:

  • source screenshot coordinate;
  • display logical coordinate;
  • window-local coordinate;
  • exact target window identity;
  • exact display transform;
  • exact Electron page identity when applicable;
  • canonical action fingerprint.

Drag and zoom endpoints must bind to the same window.

Dispatch validation

Fresh state is used only to validate the bound target, never to choose another target.

Stable fail-closed errors cover:

  • no_active_frame
  • stale_frame
  • stale_epoch
  • target_missing
  • target_changed
  • target_occluded
  • page_target_changed
  • duplicate_action
  • user_intervened

Validation rejects missing/reused/moved/resized windows, changed display transforms, occlusion, changed AX/DOM content, changed/non-unique page targets, stale epochs, and replay. None of these paths falls back to an unbound pixel or foreground action.

Keyboard ownership

Keyboard ownership is runtime-owned, not inferred from current system focus or backend history. It contains:

session + turn + frame/epoch + bound click target

Only a successful, verified, text-editable bound click followed by a fresh observation establishes ownership. It is revoked by new observation, failed/stale action, changed target/page, turn/session end, clearSession, or abort. Type validates the same bound window/page again and never activates it.

Effect verification and postconditions

Successful mutating actions consume the old frame. A fresh observation is mandatory before a new frame is issued. If a high-impact action cannot produce a fresh postcondition, it becomes capture_failed even if the underlying dispatch reported success.

Semantic success still requires observable effect:

  • editable click: DOM focus;
  • checkbox: checked-state change;
  • button/double/triple click: downstream mutation;
  • middle click: handled auxclick plus mutation;
  • right click: consumed context menu or mutation;
  • range drag: persisted value;
  • native text: fresh AX value readback;
  • Electron text: fresh DOM value readback.

supported:true, ok:false is terminal. Only side-effect-free supported:false may fall back once.

User concurrency and focus

  • User pointer movement and frontmost-app changes are allowed by the monitor.
  • If user activity invalidates the bound target, the action fails stale/occluded/user-intervened.
  • Agent-caused real pointer movement remains a P0 failure.
  • Agent focus stealing, temporary activation, foreground CGEvent fallback, and window-less desktop clicks remain prohibited.

Verification

  • Core: 812/812.
  • Runtime frame/tool focused tests: 32/32.
  • Computer Use: 87/87.
  • Script/coverage/sequence tests: 20/20.
  • Desktop typecheck: passed.
  • cua-driver bundle/provenance gate: passed.
  • Added direct tests proving:
    • screenshot-time target dispatch;
    • decoy/occluder zero interaction;
    • missing/reused/moved window rejection;
    • display-transform rejection;
    • AX/DOM user-intervention rejection;
    • CDP target ID/URL change rejection;
    • duplicate action rejection across epoch advancement;
    • negative-origin and 1x/2x display transforms;
    • no pixel/foreground fallback on validation failure.
  • A real E2E run proved the first frame-bound Electron click reached the exact captured window. Later reruns under a full-screen ChatGPT window stopped before input because all fixture points were occluded, which is the required fail-closed result.

Scope

This PR is execution-layer only. Provider/model harnesses and realRunEnabled scenario policy remain in the model-loop/UX PR and are not added here.

Remaining release gap

Production Electron packaging, Developer ID signing, notarization, and post-package verification remain separate release-pipeline work. The pinned cua-driver-rs-v0.7.1-maka.2 compatibility Mach-O is universal and ad-hoc signed.

hqhq1025 added 29 commits July 8, 2026 02:53
Zero-dependency type foundation for host-level computer use, locking the
Path 18 (smoke.md S12-S18) invariants into shared vocabulary before any
runner/overlay lands:
- ComputerUseErrorCode: the closed S17 fail-closed error enum (7 codes)
- CuAction: normalized action union adapting Anthropic computer_20251124
(coordinate/text-modifier/scroll/zoom), leaving room for other adapters
- ComputerUseScreenFrame + 2MB cap: the S15b typed provider-frame boundary
- ComputerUseDispatchTier (ax | coordinate-background | foreground-visible):
the capability-probed ladder so degradation is reported, never silent
- ComputerUseActionOutcome: typed success(tier,verified)/failure(S17 code)
Pins the model tool contract (computer_20251124 + computer-use-2025-11-24)
for Opus 4.8 via coproxy. Pure additive types; 9 unit tests; core 694/694.
… wip)
Native NDJSON-over-stdio helper the main process spawns for Tier-1,
public-API, background computer use on macOS: Accessibility action
dispatch (AXPress / AXSetValue) + capture. No private SkyLight SPI, no
global CGEventPost HID-tap — never moves the real cursor or steals focus
(measured: frontmost + cursor unchanged across every op).
Ops: preflight (live TCC) / screenshot (fail-closed on missing Screen
Recording, 2MB S15b cap) / click / type / key. Findings baked in from
on-device validation (macOS 26.5):
- AXPress can return success while doing nothing → every mutating op
reports honest `verified` via readback; window-controls re-read state.
- Background coordinate clicks must be pid-scoped hit-tests (occlusion-
independent, app-scoped refs dispatch reliably); global element-at-
position hits the occluding window and its ref can silently no-op.
- Background AX traversal is transiently flaky → op-level retries.
Typed S17 error enum on every failure; no swallowed catch, no faked
success. README documents the signing/notarization + ScreenCaptureKit
productionization TODO (the biggest new-infra item).
…ispatch seam (PR-RUNTIME-CU)
- permission: add `computer_use` ToolCategory (block in explore, prompt in
ask+execute like browser — host control of real apps is irreversible, always
prompt; the overlay + per-turn approval are the safety net; bypass allows).
Single turn-scope so one approval carries the screenshot→click→type loop.
New 'computer_use' permission reason (permission.ts + events.ts).
- @maka/runtime `computer` MakaTool (the name Anthropic's model emits) +
CuDispatchBackend seam (desktop spawns the signed helper behind it) +
adaptToCuAction mapping the flat computer_20251124 grammar → CuAction.
Owns the OS-independent Path 18 duties: per-action TCC re-check (S12,
fail-closed), coordinate authority stays runtime-side (S15), typed errors
(S17), AbortSignal short-circuit (S18). Honest summaries: verified=false
tells the model to re-screenshot — never a silent success.
Tests: runtime computer-use-tools 14/14; full runtime typecheck clean; core
694/694. (2 unrelated Bash-streaming/shell-exec suites are pre-existing
sandbox timing flakiness, not touched by this change.)
Next increment (provider-tool wiring, spec'd): register
anthropic.tools.computer_20251124 under map key 'computer' in ai-sdk-backend,
execute=wrapToolExecute(computerTool), toModelOutput→image-data for screenshots.
…JSON (PR-RUNTIME-CU)
Concrete Tier-1 backend injected into buildComputerUseTools({backend}). Per-
request spawn of maka-cu-helper (stateless; inherits Electron's TCC grants, no
2nd prompt), one NDJSON request line → one response line → typed
ComputerUseActionOutcome. Maps CuAction → helper op (screenshot writes a temp
PNG read back to base64 with the 2MB S15b cap; click/type/key; wait sleeps;
unimplemented actions fail closed honestly, never faked). AbortSignal + timeout
kill the child.
Verified against the real helper binary: preflight maps
{accessibility:true,screenRecording:false}; screenshot fail-closes to
permission_missing (no capture — privacy-safe); invalid coordinate round-trips
to a typed invalid_coordinate. Desktop main typecheck: 0 errors.
…ed_action code
Wire the model-facing `computer` tool's vision + error surface, and add the
S17 code the cua-driver backend needs to fail closed on keyboard:
- tool-runtime: optional MakaTool.toModelOutput (input/output typed unknown so
MakaTool stays covariant/assignable); ai-sdk-backend forwards it. Lets a tool
return a native image block — a screenshot the vision model can SEE — while
session history keeps only the text summary (coerceResultContent drops the
screenshot field, so the <=2MB frame never bloats history).
- computer-use-tools: structured { text, screenshot } result feeding
toModelOutput; redact backend-supplied outcome.message via redactSecrets at
the runtime chokepoint (S16 — cua-driver does not redact upstream).
- core: add 'unsupported_action' to the closed S17 enum (+ smoke.md S17 +
enum lock test) — a backend refusing an action it cannot do SAFELY, e.g.
cua-driver keyboard whose only target is the user's frontmost window.
…eview-hardened
Adds the Tier-2 coordinate-background backend (trycua/cua-driver v0.7.1, MIT,
embedded MCP over stdio) behind CuDispatchBackend, selected by select-backend
(fails closed off macOS / missing binary → zero tools, capability unadvertised),
resolved via cua-driver-path (<Resources>/bin, dev-repo fallback), and disposed
on before-quit.
Hardened against an adversarial review (12 confirmed findings):
- KEYBOARD FAILS CLOSED. Removed the frontmostPid() masking shim: cua-driver
keyboard needs a pid and the only one resolvable is the OS-frontmost app =
the user's active window. type/key now return unsupported_action instead of
injecting keystrokes into whatever the user is using (non-negotiable safety).
- No startup deadlock: the initialize/set_config handshake is per-request
timeout-bounded and abort-aware; any handshake failure SIGKILLs the child so
the next action retries fresh (was: raw request() with no timeout → a silent
child wedged every future action forever).
- set_config failure fails CLOSED (rejects start) instead of warn-and-continue
against an unconfigured desktop scope.
- Drain stderr (bounded tail) so a chatty child can't fill the OS pipe and hang.
- stdin 'error' listener routes EPIPE into orderly teardown instead of crashing
the Electron main process.
- Bounded stdout transport buffer (32MB) — tear down a runaway/garbage stream.
- select-backend logs a genuine construction failure (distinct from the
legitimate binary-absent path) while still failing closed.
Tests: 8 backend (incl. keyboard fail-closed, hung-handshake timeout+kill,
set_config fail-closed) + 2 path resolver — all green against a mock driver.
…e e2e)
Ran a read-only protocol probe against the REAL cua-driver v0.7.1 binary
(handshake + describe + check_permissions, no capture/click/type). Findings:
- Embedded TCC inheritance CONFIRMED: check_permissions.source =
{attribution:'host', embedded:true, note:'reflect the HOST app's TCC grant,
child in host's responsibility chain'} — validates the whole embedded design.
- check_permissions structuredContent keys {accessibility, screen_recording,
screen_recording_capturable, source} match the backend's preflight mapping.
- Keyboard: type_text/press_key are background-safe (delivery_mode:'background',
no focus steal) and target an explicit pid — the mechanism is NOT frontmost-
only as the prior comment claimed. The real limiter is the flat computer
grammar carries no target pid; guessing = frontmost = the user's window. So
fail-closed stays (safe), but the rationale + upgrade path are now accurate.
- No overlay in embedded+no-daemon mode (get_agent_cursor_state → 0 instances).
Behavior unchanged (backend test still 8/8); comment-only correction.
…ing + palette)
Faithful TypeScript port of trycua/cua's cursor-overlay Rust crate — the pure,
backend-agnostic motion + visual core of the Codex-style agent cursor:
- palette.ts — 10 palettes × 5 colours + for_instance(id) stable hash + gradient
- dubins.ts — minimum-turning-radius arc–straight–arc path planner (6 solvers)
- cursor-engine.ts — tick_swift_constants port: smootherstep speed profile
(300→900→200 pts/s along the Dubins path) → spring settle (K=400, C=17,
overshoot 0.8), + MoveTo (16px click-offset, off-screen sentinel), + Canvas
paint (bloom r22, procedural arrow with tip→tail gradient 0/0.53/1, click pulse).
Pure visual layer — never touches the real cursor (empirically 0px move). Backend-
agnostic: driven purely by (x,y) per action, so it works for cua-driver Tier-2 AND
the AX-helper Tier-1. Verified: 6/6 engine tests (Dubins endpoints/continuity,
speed-profile peak, glide+spring convergence to target in ~0.55s, click-pulse
timing, palette determinism) + standalone canvas demo.
…gine
A transparent, always-on-top, click-through BrowserWindow that renders the
agent cursor over the real desktop, driven by MAIN with per-action coordinates.
Proven over the real desktop via scripts/cursor-overlay-demo.mjs (12 scripted
moves/clicks; focus + real cursor untouched).
- src/overlay/{cursor-overlay.ts,-preload.ts,.html}: Canvas host running the
CursorEngine on a rAF loop that blocks on idle; receive-only preload
(ipcRenderer.on only → cannot send/inject back, S15) exposing onMove/onReset.
- main/computer-use/cursor-overlay-window.ts: createCursorOverlayController with
the S14 window options (focusable:false, setIgnoreMouseEvents(true,{forward})
armed BEFORE showInactive, screen-saver level, all-workspaces). Persistent per
session — move() sends 'overlay:move' window-local coords over IPC instead of
recreating the window; supersede-no-orphan; synchronous destroy() teardown (S18).
- engine: click/drag actions glide to the target then pulse ON ARRIVAL.
- scripts/build-cursor-overlay.mjs: esbuild bundle (renderer IIFE + CJS preload).
Tests: 5 window-contract (S14 flags, arm-before-show, persistence/no-recreate,
window-local coords, teardown/supersede/fail-closed) — all green.
…actions
Wires the Maka-owned cursor overlay to the runtime so it follows every computer
action, backend-agnostically:
- runtime: buildComputerUseTools gains an optional `overlay` hook (CuOverlayHook);
fires onActionBegin(action, {sessionId,toolCallId}) at the coordinate-authority
point (after adaptToCuAction, before backend.run) and onActionEnd in a finally.
Best-effort — a throwing overlay can never break dispatch. Sits above
backend.run, so it fires identically for cua-driver Tier-2 and ax-helper Tier-1.
- desktop: computer-use-overlay-hook.ts maps CuAction → controller.move, doing the
S15 declared-px → logical-screen transform in MAIN (÷scaleFactor + display origin,
getPrimaryDisplay). Non-coordinate actions (type/key/screenshot/wait) keep the
cursor present without moving it. select-backend threads the hook through.
- main.ts: creates the controller + hook, passes to selectComputerUseBackend, tears
the cursor down per-session on turn-end (streamEvents complete/abort/error) and
unconditionally at before-quit.
Tests: 4 hook (transform 1×/2×/origin, click/scroll/drag/move kind mapping,
non-coord ensure-without-move); runtime CU 14/14 + desktop CU 23/23 unchanged.
- build-cursor-overlay.mjs exports buildCursorOverlay() (still runnable directly).
- package.json: build:overlay script + folded into the build chain.
- dev.mjs: builds the overlay bundle in parallel with preload so `npm run dev`
produces dist/overlay (the controller loads dist/overlay/cursor-overlay.html).
…PRESETS
The computer_use ToolCategory/reason (added with the CU foundation) left the
exhaustive Record<ReasonKind, ReasonPreset> map incomplete — latent since main-only
builds skip @maka/ui; the full `tsc --build tsconfig.lib.json` (npm run dev) failed
TS2741. Adds a MousePointer2 'caution' preset. Surfaced by the real-runtime e2e.
…p + e2e harness
Real agent-driven e2e (real connection + runtime + tools) surfaced why the
computer tool never reached the model, and it works end-to-end after these fixes:
- cua-driver-path: in an unpackaged dev run process.resourcesPath points to
Electron's OWN Resources dir, so a resourcesPath-only check looked in the wrong
place and the binary was 'not found' → selectComputerUseBackend returned NONE →
the computer tool was silently absent. resolveCuaDriverBinaryPath now tries BOTH
the packaged path AND the dev repo path. devBinaryPath() also made layout-robust
(walks to the dist root) so the esbuild dev bundle (dist/main/main.js) resolves
the same as the tsc layout.
- cursor-overlay-window: same import.meta.url-in-bundle hazard for the overlay dist
dir — walk to the dist root instead of a fixed ../../ depth.
- main.ts: dev-only MAKA_CU_E2E_PROMPT harness (auto-runs one real NL turn, auto-
approves permissions, logs tool activity) + a [cu-startup] backend/tools diag line.
Verified live: [cu-startup] backend=cua-driver tools=1; opus-4-6 called
computer{screenshot} (real 3024x1964 capture) + left_click (dispatched) + key
(fail-closed 'unsupported_action', keyboard safety holds in the real app).
…WARPS the real cursor
RED LINE: a live e2e run confirmed cua-driver's scope:'desktop' (no-pid) click
synthesizes a GLOBAL CGEvent that moves the user's REAL cursor. The earlier '0px
cursor move' finding was for the click{pid,window_id,x,y} (CGEventPostToPid) path,
NOT the desktop-scope path the backend was using. Stealing the cursor violates the
non-negotiable invariant this whole feature exists to uphold.
- click (all variants) + scroll now fail closed with unsupported_action rather than
warp the cursor. The no-warp path (window-at-point → pid+window_id → CGEventPostToPid,
or the AX element path) needs window/pid resolution — a careful follow-up, not a
speculative live test that could steal the cursor again.
- mouse_move now succeeds as a pure VISUAL agent-cursor glide (no real input, no cua
call) — matches Codex's move_cursor; the overlay hook already animates it.
- computer tool description reframed: no REAL cursor movement (a visual agent-cursor
shows attention); prefer over shelling to cliclick; keyboard unavailable.
Tests updated: click/scroll never reach cua-driver; mouse_move injects nothing.
…ay coord logging
- MAKA_CU_E2E_PROMPT accepts a ';;'-delimited scenario list, each run as its own
session sequentially (broad suite on one app boot) with a per-scenario summary.
- overlay hook logs the declared→screen coordinate transform + kind under the e2e
env, which is how the desktop-scope cursor-warp was localized to left_click.
…res safe clicking)
Replaces the fail-closed stub with the real no-warp click. Resolves the window
under the click point (list_windows, screen-point space, frontmost layer-0 —
which also excludes Maka's always-on-top overlay) and clicks via pid+window_id.
That forces cua-driver's click_at_xy_with_window_local → SLEventPostToPid/
post_to_pid, which — confirmed at the cua source (mouse.rs) AND empirically on the
real binary (0px real-cursor movement across 3 clicks on a scratch window) — does
NOT warp the cursor. Fails closed ONLY on empty desktop (no window under the
point), where cua-driver's sole path (click_at_xy_desktop) CGWarps the real cursor.
- getScale() caches get_screen_size scale_factor to convert the model's device-px
coordinate ↔ logical window bounds; window-local device px = coord − origin*scale.
- Safety is guaranteed by construction: click ONLY ever sends pid+window_id (never
scope:desktop) or fails closed — no warp is possible.
Tests: click on a window → pid+window_id, no scope:desktop; empty desktop → fail
closed, no click sent; scroll still fail-closed (its desktop-scope also warps).
…ina screenshots
Two issues surfaced by the live e2e (user saw TWO agent cursors + blocked screenshots):
- Duplicate cursor: the backend spawned cua-driver with --embedded but WITHOUT
--no-daemon-relaunch, so cua relaunched its daemon which drew its OWN agent-cursor
overlay ON TOP of Maka's. Maka owns the overlay; cua's must not render. Added
--no-daemon-relaunch + CUA_DRIVER_RS_MCP_NO_RELAUNCH=1 (verified: 0 cua cursor
instances; capture still works in-process).
- Screenshot cap: a native Retina full-display PNG (3024x1964) runs 4-6 MB, so the
2 MB S15b cap blocked real screenshots as sensitivity_blocked. Raised to 8 MB —
keeping NATIVE resolution so the model's coordinate space stays device px (which
the no-warp click resolver depends on). FOLLOW-UP: JPEG-compress at native res
instead of a large cap (small payload, same coordinates).
Tests updated (spawn argv, cap value); 34/34 CU sweep green.
…and cursor color
- Two cursors persisted because --no-daemon-relaunch alone did NOT suppress cua's
agent-cursor overlay. Add --no-overlay ('Disable the cursor overlay entirely')
to the spawn — the definitive kill switch. Maka owns the overlay.
- Cursor colour: replace the per-session palette hash with a fixed Maka brand
palette derived from the app's primary token --action = oklch(0.62 0.19 264),
so the agent cursor reads as 'Maka' (per 昊卿: match the app theme). setSession
keeps its id param for future multi-agent hue differentiation.
Scroll requires a pid and posts via scroll_wheel_at_xy → post_to_pid (confirmed at
cua source: no CGWarp — the cursor warp only exists in the empty-desktop click
path). So scroll gets the same treatment as click: resolve the window under the
point, scroll it window-locally via pid+window_id (no cursor warp); fail closed on
empty desktop. Test: scroll on a window → pid+window_id, no scope:desktop; empty → fail closed.
The 8 MB cap was a stopgap; a Retina full-display capture can still approach the
provider's ~5 MB image limit and bloats every turn. Compress frames >1.5 MB to
JPEG (quality 82) at NATIVE resolution via Electron nativeImage — coordinates are
unchanged (the no-warp click resolver depends on native px), payload drops ~5-10×.
Small crisp PNGs (simple screens) pass through untouched. Injected as an optional
compressFrame(base64,mime) so the backend stays testable under node --test.
Test: large frame → compressFrame applied, mimeType image/jpeg; small frame →
compressor not called, stays PNG. 12/12 backend tests green.
…an instant pop)
On a short turn (one click) the cursor used to snap to the target + a quick pulse
and vanish — near-invisible. Now the first appearance enters from up-and-left of
the target so it visibly GLIDES in via the Dubins path. Verified in the ~3s minimal
overlay demo (scripts/cursor-overlay-demo.mjs) rather than the full app.
… scale_factor) + fast CLI harness
- getScale trusted get_screen_size.scale_factor, which was observed reporting 1 on a
Retina display in a CLI context (the app happened to get 2) → clicks mapped
off-screen → 'no app window'. Now compute the TRUE ratio: device screenshot width ÷
logical screen width (cached from the last capture), falling back to scale_factor
only before the first frame. Robust across contexts.
- scripts/cu-cli.mjs: fast (~5s) backend smoke — drives the real createCuaDriverBackend
against the binary (no Electron, no LLM) through screenshot/mouse_move/click/scroll/
key and prints outcomes + frame size. This is how backend logic should be iterated
(the overlay RENDER uses the ~3s cursor-overlay-demo.mjs; a full agent turn uses the
app). The CLI found this very scale bug on its first run.
Backend unit tests 12/12 green.
…ision)
Replicates the backend's window-resolution + device-px→window-local transform
against the real cua-driver on a scratch TextEdit, using debug_image_out to draw a
crosshair where the click LANDED, then reads it back (PIL) via the crosshair's
line-intersection (peak red column × row — robust vs a centroid). Result: clicks
land within 1px of intent at center / upper-left-quarter / lower-right of the
window. Also surfaced that list_windows returns chrome windows (the menu bar, owned
by the frontmost app) — the probe now filters to a real document window; the
backend's containment + z-index sort handles this for content-area clicks. ~6s, no
Electron/LLM — the kind of iteration the CLI is for.
…ol path
Closes the fidelity gap: cu-tool-cli.mjs imports the REAL buildComputerUseTools
(runtime), the REAL cua-driver backend, and the REAL overlay hook, then calls the
tool impl with model-shaped action args — so it exercises S12 TCC recheck +
adaptToCuAction (flat grammar → CuAction) + the overlay hook's declared-px→screen
transform + the backend's window resolution/no-warp click, exactly as the app does.
Only the Electron overlay BrowserWindow (visual — covered by the ~3s overlay demo)
and the LLM (covered by an occasional full-app run) are stubbed. Verified live: the
hook maps left_click [1500,1000] → overlay screen (750,500) at scale 2; key fails
closed. ~6s.
…ire headless CLI
Move the OS-independent computer-use backend out of apps/desktop and into a new
zero-Electron package so both the GUI and the CLI can drive it:
packages/computer-use/ (new @maka/computer-use)
- cua-driver-backend / cua-driver-path / helper-backend / select-backend
- computer-use-overlay-hook (decoupled: OverlayCursorSink + CursorMoveInput
types now live here, not in the Electron overlay window)
Electron-bound pieces stay in apps/desktop: cursor-overlay-window.ts (the
BrowserWindow that renders the agent-cursor). main.ts now imports the backend +
hook from @maka/computer-use and feeds it the Electron overlay + JPEG compressor.
CLI wiring (the point of the move): runtime-bootstrap builds the headless
backend via selectComputerUseBackend() with NO overlay — the visual agent-cursor
is Electron-only, so the CLI runs computer-use blind. Because that on-screen
visibility is what makes GUI computer-use safe to watch, the CLI keeps the
capability OPT-IN behind MAKA_CLI_COMPUTER_USE=1; every action still routes
through the permission engine ('ask' mode). The cua-driver child process is
disposed via a new context.dispose() called in cli.ts's finally.
Path resolver hardened for the shared location: devBinaryPath() walks up to the
dir containing apps/desktop instead of a fixed relative depth. Binary itself is
gitignored (fetched by scripts/prepare-cua-driver.mjs, packaged via
extraResources).
Build graph: root workspaces + tsconfig.lib.json + apps/desktop & packages/cli
deps updated. Tests: 18/18 (moved package) + desktop overlay/engine green + 74/74
CLI. tsc --build tsconfig.lib.json clean.
…t entry
Activate the two packaging scripts that were written but never wired:
bundled-tools.json +cuaDriver { repo trycua/cua, version v0.7.1,
tag cua-driver-rs-v0.7.1, asset cua-driver-rs-0.7.1-darwin-universal-binary.tar.gz,
binaryName cua-driver, sha256 43a78c17… } — single source of the version pin.
package.json +prepare:cua-driver (download→verify→extract→marker),
+check:cua-driver-bundle (release gate), and check:release now runs the gate.
Mirrors the officecli prepare/check pattern. Fail-closed: prepare throws if the
sha256 isn't pinned or mismatches; check throws if the binary is absent / not
executable / marker-mismatched. The binary stays gitignored (fetched, not source).
Verified end-to-end: prepare downloaded the 10,270,955-byte tarball, sha256
matched the manifest pin (== official checksums.txt), extracted + wrote the
marker; check passed; a second prepare reported up-to-date (idempotent).
…, no-warp)
The core CuAction (startCoordinate + coordinate) and the runtime adapter already
carried left_click_drag; only the backend dispatch was missing (it fell through
to unsupported_action). Wire it to cua-driver's `drag` tool.
Safety — source-verified against cua-driver-rs v0.7.1 (commit 7caf72b): `drag`
sends its whole down→(interpolated moves)→up sequence through the SAME window-local
post_mouse_event → SLEventPostToPid/CGEventPostToPid path as click. The only
CGWarpMouseCursorPosition in the entire crate is click's pid-less scope:'desktop'
branch, and drag has no such branch (its pid is required). So a pid+window_id drag
never moves the user's REAL cursor — the non-negotiable red line holds.
Design:
- Resolve BOTH endpoints (reuse resolveWindowAt) and require the SAME window.
Cross-window drag can't be one window-local gesture, and cross-app
drag-and-drop needs a real NSDraggingSession this synthetic post_to_pid path
can't establish (cua-driver marks the result unverifiable). Fail closed on
empty desktop (no target window ⇒ no required pid) or cross-window.
- delivery_mode left DEFAULT (Background). Never 'foreground', which would
briefly reorder window z-order/frontmost (a focus disturbance).
- Tool description now advertises drag + its single-window constraint.
Tests: +3 (same-window no-warp coords, endpoint-on-empty-desktop fail-closed,
cross-window fail-closed); second mock window added for the cross-window case
without perturbing existing click/scroll probe points. @maka/computer-use 21/21,
runtime adapter green, desktop bundle + lib graph typecheck clean.
Bring 134 commits of main into the CUA branch so the draft PR is based on current
main (not the 7-05 fork point). Resolved 5 conflicts, keeping both sides' intent:
- runtime/index.ts — main's shell/workspace exports + CUA computer-use exports
- apps/desktop/package.json — build keeps build:overlay (CUA) AND build:test (main)
- cli.ts — took main's startup error-handling + context.close() shape;
the CU child-process teardown is folded into close()
- runtime-bootstrap — MakaCliRuntimeContext keeps main's close(); headless CU
dispose runs inside it; CU tools push onto allTools (the set
handed to the backend), not the already-spread `tools`
- main.ts — kept main's runBeforeQuitCleanup() teardown; the CU
backend.dispose() + overlay.destroyAll() are folded in
Verified post-merge: lib-graph typecheck clean, desktop main bundle clean,
@maka/computer-use 21/21, maka-agent CLI 185/185, runtime 1136/1139 (the one fail
is the pre-existing shell-exec process-tree-kill flaky, unrelated to this merge).
Keyboard (type/key) now works on the cua-driver backend, delivered ONLY to
the window the agent last clicked/scrolled/dragged (lastTarget -> type_text /
press_key, delivery_mode default Background). No established target -> fail
closed; never guesses the user's frontmost window. Anthropic key chords
(cmd+a, ctrl+shift+t) parse to cua-driver key + mac modifiers. hold_key stays
unsupported (no cua-driver hold-duration primitive).
Drop the half-baked, self-built ax-helper (maka-cu-helper Swift) backend
entirely -- cua-driver is now the sole backend. This removes the confirmed
red-line hole where ax-helper type/key fell back to NSWorkspace.frontmost
(= the user's active window). Deleted helper-backend.ts + native/maka-cu-helper/,
simplified select-backend to cua-driver only.
Audit fixes (from the PR apache#699 multi-dimension review):
- packaging: check:release now RE-HASHES the actual cua-driver binary bytes
against the pinned sha256 (was: trusted the sidecar marker only); prepare
self-heals a swapped binary; unpinned/placeholder sha256 fails closed; temp
extraction dir cleaned on the error path; contract test pins the gate into
check:release.
- overlay: removed dead idle-fade machinery (idleAlpha was provably always 1;
the fade branch is unreachable since the rAF loop halts when not moving).
- tests: lock the primary device/logical scale path (screenshot_width/
logical_width, not scale_factor), the z-order tiebreak + layer!=0/off-screen
exclusion, and delivery_mode != foreground on click/scroll.
Tests: computer-use cua-driver-backend 22/22, runtime cu-tools 14/14, desktop
cursor-engine + overlay-window + build-hygiene 13/13; lib + desktop typecheck
clean.
@hqhq1025hqhq1025 changed the title feat(cu): host computer-use — shared @maka/computer-use pkg + cua-driver backend + agent-cursor overlay + dragfeat(cu): host computer-use — cua-driver backend (click/scroll/drag/type/key) + agent-cursor overlayJul 9, 2026
@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

Execution-layer P0 found by the real OpenAI model-loop harness:

  • The model correctly grounded the owned Electron fixture around (1240,681).
  • While the user was working, ChatGPT covered that coordinate.
  • cua-driver-backend.resolveWindowAt() re-resolved by highest z-order at action time, selected pid=94159,title=ChatGPT, and dispatched the pixel path (path=cgevent, verified=false).
  • This disturbed the user's focus and repeated because the intended fixture never changed.

This belongs in #699, not the provider/model harness. Required execution contract, matching Codex/Sky window2:

  1. Observation returns a stable target window identity plus screenshot/frame id.
  2. Pointer actions carry that target identity/frame id.
  3. Backend rehydrates the same pid + window_id and maps the coordinate into that window, regardless of occlusion.
  4. If the target/window/frame is stale or missing, fail closed. Never reselect the highest-z window under the same global coordinate.
  5. Add a regression with an owned target window covered by a second window: zero events to the covering window, no frontmost PID change, no real cursor movement.

Codex/Sky evidence: window2 click/drag/scroll accept Window {app,id} and optional screenshotId; screenshot IDs must remain cached for that target window.

The model-loop branch now has only a harness guard (target_occluded) to prevent further accidental clicks; no permanent backend workaround is being added there.

@hqhq1025
hqhq1025 marked this pull request as ready for review July 12, 2026 11:10
@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

#699 Window / Frame Binding 验收要求

P0 核心原则

模型基于哪一帧截图产生动作,动作就必须绑定那一帧以及那一帧中的目标窗口。

禁止在 dispatch 时重新按当前最高 z-order 窗口解释裸坐标。之前点击落到 ChatGPT,就是因为 observation 和 dispatch 之间重新选了窗口。

1. Observation 身份

每次截图必须生成不可复用的 observation:

interfaceCuaObservation{frameId: string;epoch: number;capturedAt: number;screenshotWidthPx: number;screenshotHeightPx: number;displays: DisplaySnapshot[];windows: WindowSnapshot[];}

每个窗口至少包含:

interfaceWindowIdentity{pid: number;windowId: number;bundleId?: string;bounds: Rect;zIndex: number;}

Electron/CDP 目标还需要绑定:

{cdpPort;pageTargetId;pageUrl;}

不能只依赖可重复的 window title。

2. Action 身份

模型动作在进入 executor 前必须转成:

interfaceBoundAction{frameId: string;epoch: number;target: WindowIdentity;sourceCoordinate: Point;windowCoordinate: Point;actionFingerprint: string;}

target 必须从生成截图时的 windows snapshot 解析,不能在执行时调用“重新找坐标下最高窗口”。

3. Dispatch 前验证

以下任一情况必须 fail closed:

  • frameId 不是当前有效 frame;
  • epoch 已变化;
  • pid/windowId 不存在;
  • windowId 被复用到其他窗口;
  • 窗口移动、缩放、换屏导致 transform 变化;
  • Retina/display scale 变化;
  • 原目标被其他窗口遮挡;
  • CDP page target 变化或不再唯一;
  • 用户操作改变了目标状态;
  • 同一 bound action 被重复提交。

建议稳定错误码:

no_active_frame
stale_frame
stale_epoch
target_missing
target_changed
target_occluded
page_target_changed
duplicate_action
user_intervened

任何错误都不能退回裸 pixel click。

4. 坐标空间

必须保存完整变换链:

model coordinate
-> source screenshot coordinate
-> display logical coordinate
-> window-local coordinate

要求覆盖:

  • 1x/2x Retina;
  • 负 display origin;
  • 多显示器;
  • 窗口跨屏;
  • 截图裁剪;
  • 窗口移动;
  • drag 两端必须属于同一 bound window。

执行层接收的是已经绑定的 source/window 坐标,不能为了前端光标动画重新缩放。

5. 用户并发与焦点

用户正常使用电脑不应自动视为 agent 违规。

但需要区分:

  • 用户移动鼠标、切换自己的前台窗口:允许;
  • 用户行为导致目标 frame/window 失效:当前动作返回 stale,重新观察;
  • agent 改变真实鼠标位置:P0 失败;
  • agent 抢前台焦点:P0 失败;
  • 为了坐标点击临时 activate 窗口:不允许;
  • 后台投递失败时,不允许自动降级为 foreground CGEvent。

6. Keyboard ownership

键盘目标只能来源于一次成功并验证过的 bound click:

sessionId + turnId + pid + windowId + frame/epoch

以下情况立即撤销 ownership:

  • 新 observation;
  • stale/failed click;
  • 用户改变目标;
  • window/page identity 变化;
  • turn/session 结束;
  • action abort。

type 不能依赖当前系统焦点,也不能主动激活目标窗口。

7. Action 生命周期

推荐流程:

observe
-> 创建 frameId/epoch
-> 模型输出 action
-> bind action
-> claim 去重
-> validate exact target
-> background dispatch
-> UI settle
-> fresh observation
-> verify effect
-> invalidate old frame/advance epoch

动作成功必须携带:

{
path: 'ax'|'cdp'|'cgevent';
effect: 'confirmed'|'unverifiable'|'suspected_noop';
verified: boolean;}

高影响动作没有 fresh postcondition,不得报告 verified:true

8. 必须通过的测试

  • L1 单窗口精确点击,禁止重复点击。
  • L2 type/drag/scroll,全部绑定同一窗口。
  • L3 两个相似窗口,decoy 必须零交互。
  • L3 截图后替换窗口,旧 action 必须 stale_frame
  • L3 action 前加入 occluder,必须 target_occluded,不得点穿。
  • 窗口在 observation 后移动,必须拒绝。
  • 1x/2x、负 origin、多显示器坐标测试。
  • 用户同时移动鼠标/切焦点,agent 不得移动真实光标或抢焦点。
  • 同一 action 重放必须拒绝。
  • CDP target 变化或不唯一必须拒绝。
  • 失败路径必须证明没有调用 pixel/foreground fallback。

Done 标准

完成后才能把这些场景设置为 realRunEnabled: true

l1-single-click
l2-multi-control
l3-two-window
l3-stale-window

并用真实 Maka 模型链跑,不只是 mock backend。

现有 CuaFrameState 已经提供 frameId/epoch/duplicate 状态机,可以复用;但关键是必须贯穿:

screenshot -> provider action -> executor -> postcondition

不能只停留在独立类型和单元测试里。

@hqhq1025
hqhq1025force-pushed the feat/cu-runtime-helper branch from 14b7fb6 to 675433dCompareJuly 12, 2026 14:34
@hqhq1025hqhq1025 changed the title fix(cu): exact background computer use via cua-driverfix(cu): bind Maka Computer actions to observed window capturesJul 12, 2026
hqhq1025 added a commit to hqhq1025/maka-agent that referenced this pull request Jul 12, 2026
@Astro-Han

Astro-Han commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Thanks for putting this together. The overall direction makes sense to me, especially binding actions to the exact observation instead of resolving the target again at dispatch time.

I spent some time tracing the production paths, and I’m finding this difficult to review confidently at the current size. The PR is about 22k added lines across 122 files, and it includes several fairly different changes: the core/runtime contract, cua-driver execution, Electron page targeting, desktop wiring, the cursor overlay, CLI support, provider adapters, OpenAI transport, and multiple E2E harnesses.

I also found a number of concrete issues while reading it. A few examples:

  • the observed frame does not currently include or validate the AX/DOM freshness fingerprints described in the PR body, so content inside the same window can change while an old coordinate remains valid;
  • semantic element matching can fall back from a changed token to role + label, which may accept a replacement control;
  • type, wait, and standalone screenshot do not appear to participate consistently in frame invalidation;
  • screenshot capture, Electron page resolution, validation, and final dispatch happen across separate operations, leaving some TOCTOU gaps;
  • several overlay assumptions do not hold for semantic actions, secondary displays, or overlay load failures;
  • the provider adapters and OpenAI backend appear to be separate work, and some of them are not connected to the current desktop path.

There are more cases in the same areas, but I’m not sure posting a very long list of inline comments on this PR would be useful. Several findings seem to come from the same missing ownership boundaries, so fixing them one at a time here may make the diff even harder to reason about.

Would you consider splitting this along the existing dependency seams?

  1. Core types, permissions, runtime frame state, and the backend interface.
  2. The macOS cua-driver backend, including window/page identity, freshness checks, dispatch, and keyboard ownership.
  3. Desktop integration, binary packaging, lifecycle cleanup, and the required overlay behavior.
  4. A small deterministic cross-layer E2E suite.

CLI host control, provider-specific adapters, and the OpenAI Responses loop could then be separate follow-ups. Unit tests should stay with the implementation they cover.

This is a suggestion, not a blocking request from me. Please push back if the dependency boundaries make a different split more practical. My main concern is that the current PR mixes enough independent behavior that it is hard to tell whether the safety contract actually holds across all layers. Smaller PRs would make it much easier to give each part a useful review.


中文补充

整体方向我是认可的,尤其是让动作绑定观察时的窗口和页面,而不是执行时再临时寻找目标。这条主线值得继续做。

目前比较实际的困难是,这个 PR 同时改了太多层。两万多行里既有 runtime 的帧状态,也有 cua-driver、Electron page 定位、桌面接线、overlay、CLI、provider adapter 和多套 E2E。每一部分单独看都需要不少上下文,放在一起以后,很难确认从观察、校验、派发到后置验证的安全约束是否真的闭环。

阅读过程中也确实发现了一些具体问题。例如,同一窗口内部内容变化后,旧坐标仍可能有效;AX token 变化后仍可能按角色和标签接受新的控件;typewaitscreenshot 与帧失效的关系不一致;Electron 截图、page identity 校验和最终派发之间也存在时间窗口。overlay 在语义动作、副屏和加载失败等情况下,也不一定能准确反映真正执行的动作。

这些问题分布在 observation、backend validation、dispatch、keyboard ownership 和 overlay 几个环节,逐条修补很容易顾此失彼。继续在当前 PR 里改下去,review 范围可能还会进一步扩大。

所以想建议按依赖边界拆一下:

  1. 先提交 core/runtime contract,包括类型、权限、frame/epoch 状态和 backend interface。
  2. 再提交 macOS cua-driver backend,把窗口、页面、内容 freshness、派发和 keyboard ownership 放在同一个审查范围内。
  3. 然后接 desktop integration,包括 binary、生命周期和 overlay。
  4. 最后补一个小而确定的跨层 E2E,验证代表性的 observe、action、fresh observation 流程和关键失败路径。

CLI host control、provider adapter 和 OpenAI Responses loop 可以各自作为后续工作。它们和当前 native execution 主线有关,但不是证明这条主线正确所必需的部分。

这不是阻塞要求,只是基于目前 review 体验给出的建议。如果实际依赖关系更适合另一种拆法,也欢迎直接 push back。主要希望每个 PR 都能形成一个可以单独理解、验证和回滚的完整意图,这样大家更容易给出有效 review。

@hqhq1025

Copy link
Copy Markdown
ContributorAuthor

@astrohan This implementation has been superseded by the rebuilt, review-split Computer Use stack #857 -> #892 -> #893 -> #894 -> #895 -> #896 -> #897 -> #898. Closing this older execution PR to keep review on one canonical implementation.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hqhq1025@Astro-Han