A live check on a Mac
scripts/mac-live-check.sh: launch the app you built with its control socket and a config folder of its own, drive it through its Action List, shoot it, give its window real input with cua-driver, run an iOS build on a Simulator of its own, and quit all of it.
What it is for#
A test proves what it asserts; a live check shows the change working in
the real app, on the real window, the way a person meets it. On Linux
the fleet’s agents start a headless test desktop for that
(lab-session.sh on the build server). On a Mac,
scripts/mac-live-check.sh is the same loop:
- launch the binary you built, with
FTS_CONTROL=1(its control socket, CLI and MCP), a config folder made for the session (ARCHITECT_SANDBOX_CONFIG_DIR, so it never reads or writes the person’s own settings) and aTMPDIRof its own (so its socket and its one-instance claim are the session’s, and a second session or the person’s own copy of the app is never in the way); - act through its Action List: the same
actions runan agent uses over MCP; - shot:
APP_SHOT, the app painting its own window to a PNG (no OS permission needed), with the window as macOS shows it when that fails (cua-driver’s capture, which has its own Screen Recording grant, elsescreencapture -l); - input that has to be real (a click, a key, the menu bar, typing) through cua-driver, always naming the app’s pid and its window, so nothing else on the screen is touched;
- ios: a Simulator made for the session, the app installed and launched, a screenshot;
- stop: the app, its child processes and the Simulator, gone; the session’s folders removed.
The sandbox, end to end#
devenv shell -- cargo build -p architect-desktop
S=scripts/mac-live-check.sh
$S launch target/debug/architect-desktop
$S act THEME_SET --arg one=catppuccin-light
$S shot /tmp/lab-light.png
$S input state --png /tmp/lab-state.png # its tree and a screenshot: where things are
$S input click 22 103 # the rail's Tasks, in that screenshot's pixels
$S shot /tmp/lab-clicked.png
$S act LAB_STATE # {"tab": "tasks", …}: the click landed
$S stop
launch waits for the control socket (the app is up) and for its
window (cua-driver can see it), and prints the pid, the socket, the
window id and the config folder. status prints them again.
Commands#
launch <binary> [args…] | start it; MLC_CONFIG_ENV names another app’s config-folder variable |
act <ACTION> [--arg k=v…] [--json …] | <binary> actions --app <its socket> run … |
cli <args…> | any other actions subcommand: cli list, cli status |
shot <out.png> [--os] | APP_SHOT, a second after the last action (MLC_SHOT_SETTLE) so its frame is drawn; --os takes the window as macOS composites it instead (the title bar and what macOS draws) |
input click <x> <y> [right] [count] | a click in window-local pixels of input state’s screenshot |
input element <token> [action] | an accessibility element from input state (s0000002a:11) |
input type <text>, input key <key> [mod…], input hotkey <mod…> <key> | keys into its window |
input menu <label…> | its menu bar, input menu Edit Undo |
input scroll <dir> [n] [x y], input drag <x1> <y1> <x2> <y2> | |
input state [query] [--png out.png] | its accessibility tree, narrowed by query, and a screenshot |
input call <tool> [json] | any cua-driver tool, with pid and window_id overwritten by its own |
ios <App.app | bundle id | scheme> [args…] | a Simulator of its own, installed, launched with FTS_CONTROL=1, a screenshot after MLC_IOS_SETTLE seconds (8) to MLC_IOS_SHOT |
ios-act <ACTION> [--arg k=v…] | its Action List, over the control socket it serves on the Mac |
ios-tap <x> <y> | a tap, in points (idb) |
ios-shot <out.png> | that Simulator’s screen again |
status, stop |
MLC_SESSION names a session, so several can run side by side;
MLC_KEEP=1 makes stop keep the config folder and the app’s log.
Rules it keeps#
- Its own window only. Every input carries the launched pid and
the window id
launchfound;input calloverwrites both. A Blitz app’s window has little accessibility tree (its traffic lights and its menu bar), so most clicks are pixel clicks: read the coordinates offinput state’s screenshot, which is the space cua-driver clicks in. A pixel click reads the window first, in the session’s own cua-driver session (mlc-<session>): cua-driver refuses one on a window that session never read. - In front, for a moment. winit hears no click or key posted to a
window that is not in front, so clicks, keys, typing, scrolls and
drags go with cua-driver’s
foregrounddelivery: it fronts the app’s window, acts, and gives the front back.MLC_DELIVERY=backgroundkeeps them in the background (for an AppKit app that takes them so). The menu bar (input menu) and elements (input element) go through accessibility and never front it. - The app’s tools first. An action is exact and needs no window in
front; a shot through
APP_SHOTis what the app drew. cua-driver is for what only real input proves: a click landing where the eye says it is, the native menu bar, the title bar, typing. - Nothing of the person’s. The config folder and
TMPDIRare made per session under/tmp; the Simulator is created for the session and deleted bystop, never one already booted. - cua-driver’s permissions. It needs Accessibility and Screen
Recording for itself (
cua-driver permissions status). Without cua-driver,launch,actandshotstill work;inputand thescreencapturefallback do not.
iOS#
ios takes a built .app (installed, then launched), a bundle id
already installed, or an Xcode scheme in the current folder (built with
xcodebuild for the session’s Simulator first). The Simulator is made
for the session (the newest runtime’s newest iPhone, or
MLC_IOS_DEVICE), headless: no Simulator.app window is needed. The sandbox’s
iOS app is built without dx by apps/mobile/ios/build-app.sh, which
also puts the WASI Counter’s component in its bundle:
S=scripts/mac-live-check.sh
export MLC_SESSION=ios MLC_CLI=target/debug/architect-desktop
$S ios "$(apps/mobile/ios/build-app.sh --features wasi)" # → /tmp/mlc-ios-ios.png
$S ios-act LAB_GO --arg tab=vault
$S ios-act WORKSPACE_OPEN --json '{"view":"wasi.counter","at":{"replace":{"pane":"note-welcome"}},"focus":true}'
$S ios-tap 149 249 # "Add one", in points
$S ios-shot /tmp/ios-counter.png
$S stop
ios-actruns the app’s actions over its control socket. The app is launched withFTS_CONTROL=1andFTS_CONTROL_DIR(a short folder under/tmp): a Simulator app’s files are the Mac’s, but its own temporary folder is too deep for a socket’s path (104 bytes at most), soarchitect-controlserves in the folder it is given (features/control/src/endpoint.rsDIR_ENV). Any architect app’sactionsCLI talks to it (MLC_CLI), and MCP reaches it through the desk sandbox’s bridge:architect-desktop mcp --app <dir>/architect-mobile.sock(an iOS app has no stdio of its own;cargo xtask surfacesaccepts the phone’s// surfaces: mcp via apps/desktopfor that reason,xtask/src/surfaces.rs).ios-tap <x> <y>taps in points, a screenshot’s pixels over its scale (3 on an iPhone), through idb (idb ui tap;pip install fb-idband itsidb_companion, orMLC_IDB=<path>). Use it for what the Action List does not reach: an extension’s own buttons, gestures.ios-shotisxcrun simctl io <udid> screenshot.stopends idb’s companion for the Simulator, shuts the Simulator down and deletes it.
A test binary runs in the Simulator as it is, with no app around it:
build it with cargo test --target aarch64-apple-ios-sim --no-run and
run it with xcrun simctl spawn <udid> <binary>; environment variables
go in as SIMCTL_CHILD_<NAME>.
Linked from
- CLI and MCPEvery action on the command line, the same as over MCP: the app's control socket, the embeddable actions CLI, headless mode, the same caller and permissions, exit codes, and the parity test that keeps the two faces identical.
- Extensions and scriptsAdding to an app at runtime, without rebuilding it: data-only extensions, WASM components over a versioned WIT world drawn from the Action List, Luau scripts for macros, out-of-process plugins on a desk — with capabilities, and views as frame documents.