@vashp2029/glovebox 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ # Backend protocol
2
+
3
+ A backend is the native piece for one OS. The scripting layer talks to it with
4
+ JSON lines: one request `{"id", "method", "params"}` per line, one reply
5
+ `{"id", "result"}` or `{"id", "error": {"message", "code"}}`, and unsolicited
6
+ events `{"event": "escape"}`. On macOS the transport is the helper's Unix
7
+ socket; a new backend may use a socket, a named pipe, or stdio.
8
+
9
+ All coordinates are global screen points with a top-left origin. The
10
+ scripting layer owns the image-pixel mapping: it picks each window's image
11
+ size and converts targets to screen points before calling the backend. The
12
+ one place a backend sees image dimensions is `screenshot`, which receives the
13
+ `width` and `height` to capture at and must return an image of exactly that
14
+ size. A backend does not choose a scale of its own.
15
+
16
+ The backend enforces approval: every method that names a `pid` (or launches
17
+ an app) must pass the policy in `config.json` before it acts, and refuses with
18
+ a coded error otherwise. Methods that only read the app list or the backend's
19
+ own state are not gated: `hello`, `ping`, `permissions`, `listApps`,
20
+ `resolveApp`, `forget`, `cursor`. See `design.md` and `SECURITY.md`.
21
+
22
+ | Method | Params | Result |
23
+ |---|---|---|
24
+ | `hello` | `client` (name shown in dialogs and the menu) | `{version, platform, pid}` |
25
+ | `ping` | | `{version, platform, pid, background}` |
26
+ | `permissions` | `prompt`, `inputMonitoring` | `{accessibility, screenRecording, escapeWatcher, inputMonitoring}` |
27
+ | `listApps` | | `[{name, bundleId, pid?, path?, running, active?}]` |
28
+ | `resolveApp` | `query` (name, bundle id, `pid:N`, path) | one app |
29
+ | `launch` | `bundleId`, `path`, `name`, `activate` | the running app |
30
+ | `activate` | `pid` | |
31
+ | `isActive` | `pid` | `{active}` |
32
+ | `windows` | `pid` | `[{index, title, frame: [x,y,w,h], scale, focused, main, minimized, subrole?, windowId?}]` |
33
+ | `axTree` | `pid`, `window`, `maxNodes`, `maxDepth`, `maxText`, `menus` | `{window, nodes, truncated}` |
34
+ | `hitTest` | `pid`, `x`, `y` | `{index, role, actions, frame}` or `{}` |
35
+ | `elementFrame` | `pid`, `index` | `{frame}` |
36
+ | `actionNames` | `pid`, `index` | `{actions}` |
37
+ | `performAction` | `pid`, `index`, `action` | |
38
+ | `setValue` | `pid`, `index`, `value`, `attribute?` | |
39
+ | `focusElement` | `pid`, `index` | |
40
+ | `selectText` | `pid`, `index`, `start`, `length` | |
41
+ | `keys` | `pid`, `chords: [{key, mods}]`, input options | input result |
42
+ | `type` | `pid`, `text`, input options | input result |
43
+ | `click` | `pid`, `x`, `y`, `button`, `count`, `mods`, input options | input result |
44
+ | `scroll` | `pid`, `x`, `y`, `dy`, `dx`, input options | input result |
45
+ | `drag` | `pid`, `path: [[x, y], ...]`, `button`, input options | input result |
46
+ | `move` | `pid` (for the gate), `x`, `y` | |
47
+ | `cursor` | `enabled`, `hide`, `x`, `y`, `windowId` | `{enabled}` |
48
+ | `makeKey` | `pid`, `windowId` | `{keyed}` |
49
+ | `screenshot` | `pid`, `windowId`, `width`, `height`, `format`, `quality` | `{data (base64), width, height, mimeType}` |
50
+ | `forget` | `pid` | |
51
+
52
+ `cursor` draws the agent cursor (a decoration; clients ignore its failures).
53
+ `move` moves the real pointer. `makeKey` makes a window key inside its app
54
+ without activating the app. `forget` drops the element table kept for `pid`.
55
+ The scripting layer does not call `move`, `makeKey`, or `forget` today; a
56
+ port may leave them out. `format` is `png` or `jpeg`; `quality` is 0 to 1.
57
+
58
+ `ping.background` reports which private functions the macOS helper found, as
59
+ booleans: `postEventRecord`, `postToPid`, `setWindowLocation`,
60
+ `setIntegerField`, `processSerialNumber`. Background input needs
61
+ `postEventRecord`, `setIntegerField`, and `processSerialNumber`.
62
+ `glovebox doctor` prints this; it is informational. Clients decide to fall
63
+ back from the error codes below, not from `ping`.
64
+
65
+ `permissions` returns `inputMonitoring` (listening for key events, which
66
+ Escape-to-stop needs on macOS) next to `accessibility` and `screenRecording`.
67
+ `prompt: true` asks the OS to prompt; with `inputMonitoring: true` it also
68
+ asks for Input Monitoring. `escapeWatcher` says whether an Escape watcher is
69
+ running.
70
+
71
+ ## Background input
72
+
73
+ `keys`, `type`, `click`, `scroll`, and `drag` take `mode: "background"` with
74
+ the target `windowId` (from `windows`) to deliver input without activating
75
+ the app or moving the user's pointer. `windowLocation: "screen"` switches the
76
+ event location from window-local to screen coordinates. Without `mode`, input
77
+ goes out on the foreground route: the client activates the app first. A
78
+ background result is `{route, focusRestored}`, where `route` is `background`
79
+ or `background-skylight` (Chromium and Electron apps), and `focusRestored` is
80
+ true if the target took focus and the backend gave it back to the user's app.
81
+
82
+ A backend with no background route should answer `mode: "background"` with
83
+ `background_unavailable`; the client then falls back to foreground.
84
+
85
+ ## Errors
86
+
87
+ Replies carry `{message, code}`. Codes the macOS helper uses:
88
+
89
+ | Code | Meaning |
90
+ |---|---|
91
+ | `blocked` | The app is blocked by default (terminals, password managers, ...). |
92
+ | `denied` | On the `deny` list, or the user chose Deny. |
93
+ | `not_allowed` | `approval` is `allowlist` and the app is not on `allow`. |
94
+ | `background_unavailable` | The private functions background input needs are missing. |
95
+ | `background_unconfirmed` | The window could not be made key in the background. |
96
+ | `no_window` | No such window, or it is gone or not capturable. |
97
+ | `no_frame` | The element has no on-screen frame. |
98
+ | `stale_index` | The element index is not in the table; read the tree again. |
99
+ | `action_failed` | An accessibility action or attribute write failed. |
100
+ | `activate_failed` | The app would not come forward. |
101
+ | `not_found` | No app matches the query, or it cannot be located to launch. |
102
+ | `not_running` | No running app has that `pid`. |
103
+ | `permission` | A needed OS permission (Screen Recording) is missing. |
104
+ | `bad_key` | A key name has no key code. |
105
+ | `bad_request` | A parameter is missing or invalid, or the method is unknown. |
106
+ | `failed` | Anything else. |
107
+
108
+ Clients act on `background_unavailable` and `background_unconfirmed` by
109
+ falling back to the foreground route; every other code is an error.
110
+
111
+ Node shape in `axTree`: `{i, d, role, subrole?, title?, value?, desc?, help?,
112
+ placeholder?, id?, frame?, actions?, focused?, selected?, disabled?,
113
+ settable?}`. `i` must be stable for the same element for as long as the
114
+ backend runs. `role` uses the macOS AX names (`AXButton`, `AXTextField`, ...);
115
+ other backends map their native roles onto those.
116
+
117
+ Key names are xdotool keysyms after parsing (`Return`, `Tab`, `Page_Down`,
118
+ single characters); modifiers are `ctrl`, `shift`, `alt`, `super`, `fn`.
package/docs/cli.md ADDED
@@ -0,0 +1,150 @@
1
+ # CLI reference
2
+
3
+ `glovebox` is one executable (`bin/glovebox.mjs`). This page lists every
4
+ subcommand and flag, the environment variables, the config file, the two MCP
5
+ tools, and how the helper and daemon start and stop. For attaching an agent
6
+ see `agents.md`; for the scripting API see `api.md`.
7
+
8
+ Run `glovebox` with no arguments, `help`, `-h`, or `--help` to print usage.
9
+ A failing command exits with status 1.
10
+
11
+ ## Subcommands
12
+
13
+ | Command | What it does |
14
+ |---|---|
15
+ | `glovebox setup [--rebuild] [--restart]` | Builds Glovebox Helper if it is missing or its source changed, then walks through Accessibility and Screen Recording, one at a time, and offers Input Monitoring. Exits 1 if the first two are not granted. macOS only. |
16
+ | `glovebox signing [--create]` | Reports whether the local signing certificate exists. With `--create`, makes it in the login keychain. Run `setup --rebuild` afterwards to re-sign. macOS only. |
17
+ | `glovebox doctor` | Checks Node (20.10+), the config, the helper build, that the helper answers, background input, Accessibility, Screen Recording, and Input Monitoring. Exits 1 on any `FAIL`. |
18
+ | `glovebox mcp` | Runs the stdio MCP server (tools `js`, `js_reset`). |
19
+ | `glovebox run [code] [-f file] [-s name] [-t title] [--timeout ms]` | Runs a script in a persistent CLI session and prints the result. |
20
+ | `glovebox reset [-s name]` | Drops a CLI session's bindings. |
21
+ | `glovebox stop` | Stops the CLI daemon and every session in it. |
22
+ | `glovebox smoke [--full]` | End-to-end test on Calculator. See `testing.md`. |
23
+ | `glovebox allow <app>...` | Adds apps to `allow`, and removes them from `deny`. |
24
+ | `glovebox deny <app>...` | Adds apps to `deny`, and removes them from `allow`. |
25
+ | `glovebox revoke <app>...` | Removes apps from both lists. |
26
+ | `glovebox approval <ask\|allowlist\|allow-all>` | Sets `approval` in the config. |
27
+ | `glovebox config` | Prints the effective configuration as JSON: defaults, then the config file, then `GLOVEBOX_APPROVAL`. |
28
+ | `glovebox log [-n N]` | Prints the last N (default 20) lines of the action log. |
29
+ | `glovebox version` | Prints the version. Also `--version` and `-v`. |
30
+ | `glovebox daemon` | Internal. Runs the CLI daemon; `run` starts it when needed. Do not call it directly. |
31
+
32
+ ### Flags
33
+
34
+ | Flag | Applies to | Meaning |
35
+ |---|---|---|
36
+ | `-f`, `--file path` | `run` | Read the script from a file. |
37
+ | `-s`, `--session name` | `run`, `reset` | Session name. Default `default`. Each name has its own bindings. `stop` ignores it. |
38
+ | `-t`, `--title text` | `run` | A few words naming the step, written to the action log as `step`. |
39
+ | `--timeout ms` | `run` | Time limit for the script. Default is `timeoutMs` from the config (120000). |
40
+ | `--rebuild` | `setup` | Rebuild the helper even if its source is unchanged. Quits a running helper first. |
41
+ | `--restart` | `setup` | Quit the running helper so it relaunches with new grants (needed after enabling Input Monitoring). |
42
+ | `--create` | `signing` | Create the certificate `glovebox local signing` if it does not exist. |
43
+ | `--full` | `smoke` | Also run the background click, drag, and scroll checks in TextEdit. |
44
+ | `-n N` | `log` | Number of lines. |
45
+
46
+ Notes:
47
+
48
+ - `run` takes code from the arguments (joined with spaces), else from
49
+ `-f file`, else from stdin. With none of them it exits with an error. Text
50
+ results print to stdout; screenshots are saved under
51
+ `~/.glovebox/screenshots/` and the path is printed. Exit status is 1 if the
52
+ script failed or was stopped.
53
+ - `run` starts the daemon if none is listening. `reset` and `stop` do not; with
54
+ no daemon they print `No daemon running.`
55
+ - `allow`, `deny`, and `revoke` match names and bundle ids case-insensitively,
56
+ rewrite `config.json`, and do not affect approvals a running session already
57
+ holds. Use `glovebox reset` or `js_reset` to make a session re-check.
58
+ - `approval allow-all` prints a warning: every app except the blocked ones is
59
+ then controllable without asking.
60
+ - `setup` and `signing` are macOS only; elsewhere they exit with an error.
61
+
62
+ ## Environment variables
63
+
64
+ | Variable | Effect |
65
+ |---|---|
66
+ | `GLOVEBOX_HOME` | State directory. Default `~/.glovebox`. Holds `config.json`, `actions.log`, `screenshots/`, `daemon.sock`, `helper.sock`, and `Glovebox Helper.app`. The helper receives it when glovebox launches it. |
67
+ | `GLOVEBOX_APPROVAL` | Overrides `approval` (`ask`, `allowlist`, `allow-all`) without editing the file. glovebox passes it to the helper when it launches it; a helper that is already running keeps the value it started with. |
68
+ | `GLOVEBOX_SOCKET` | Path of the helper's socket. Default `$GLOVEBOX_HOME/helper.sock`. Also passed to the helper at launch. It does not move the CLI daemon's socket. |
69
+ | `GLOVEBOX_CLIENT` | Name of the agent, shown in the approval dialog and menu-bar item and written to the log as `client`. `glovebox run` sends it when it creates a session; the default is `glovebox CLI (<session>)`. Over MCP the name is the client's own `clientInfo.name`. |
70
+ | `GLOVEBOX_DEBUG` | When set, the session process's stderr is copied to the host's stderr. Useful with `glovebox mcp`; the CLI daemon runs detached with no stderr. |
71
+
72
+ glovebox also sets `GLOVEBOX_IMAGE_MODE` (`inline` over MCP, `file` over the
73
+ CLI) on its session processes. It is internal.
74
+
75
+ ## Config file
76
+
77
+ Location: `$GLOVEBOX_HOME/config.json` (default `~/.glovebox/config.json`),
78
+ created with mode 0600 by `allow`, `deny`, `revoke`, `approval`, or the
79
+ "Always Allow" button. A missing file means all defaults; a file that is not
80
+ valid JSON is an error. Any subset of keys may be set. Nested objects are
81
+ merged key by key; arrays and scalars replace the default.
82
+
83
+ | Key | Default | Meaning |
84
+ |---|---|---|
85
+ | `approval` | `"ask"` | `ask`: native dialog on first use of each app per session. `allowlist`: never prompt; only apps in `allow` work. `allow-all`: no prompts; blocked apps still need `allow`. |
86
+ | `allow` | `[]` | App names or bundle ids that are always approved. Also unlocks blocked apps. |
87
+ | `deny` | `[]` | App names or bundle ids that are always refused. Wins over `allow`. |
88
+ | `input` | `"background"` | `background`: input reaches apps without focusing them, with a foreground fallback. `foreground`: always bring the app forward. |
89
+ | `foregroundApps` | `[]` | App names or bundle ids that always use the foreground route. |
90
+ | `cursor` | `true` | Draw the agent cursor where actions land. `false` turns it off. |
91
+ | `screenshot.maxLongSide` | `1568` | Longest side of a window image, in pixels. |
92
+ | `screenshot.maxPixels` | `1150000` | Pixel budget of a window image. |
93
+ | `screenshot.format` | `"png"` | `png` or `jpeg`. |
94
+ | `screenshot.quality` | `80` | JPEG quality as a percentage. Ignored for PNG. |
95
+ | `ax.maxNodes` | `800` | Elements per `getAXState` read. |
96
+ | `ax.maxDepth` | `40` | Tree depth per read. |
97
+ | `ax.maxTextLength` | `160` | Longest label or value kept, in characters. |
98
+ | `log.enabled` | `true` | Write `actions.log`. |
99
+ | `log.text` | `false` | Log typed text instead of only its length. |
100
+ | `daemon.idleMinutes` | `60` | The CLI daemon exits after this long without requests. `0` never exits. |
101
+ | `timeoutMs` | `120000` | Default script time limit for `js` and `run`. |
102
+ | `settleMs` | `80` | Pause after each action, in milliseconds. |
103
+
104
+ `screenshot.maxLongSide` and `maxPixels` set the image scale; see "Coordinates
105
+ and images" in `design.md`. Check order for each app: `deny`, `allow`, the
106
+ blocked list (`SECURITY.md`), then `approval`.
107
+
108
+ When changes apply: the session process reloads the config before every call,
109
+ and the helper reads it on every approval check. The CLI daemon reads it once
110
+ at startup, so `timeoutMs` and `daemon.idleMinutes` need `glovebox stop`.
111
+
112
+ ## MCP tools
113
+
114
+ `glovebox mcp` speaks MCP over stdio and exposes two tools. The session starts
115
+ on the first `js` call and lives until `js_reset` or the connection closes.
116
+
117
+ | Tool | Parameters | Notes |
118
+ |---|---|---|
119
+ | `js` | `code` (string, required), `title` (string), `timeout_ms` (integer) | Runs `code` in the persistent session. `title` names the step in the action log. `timeout_ms` defaults to `timeoutMs`; a value that is not a positive integer is ignored. Result content is text, plus inline images for screenshots; `isError` is true if the script threw or was stopped. |
120
+ | `js_reset` | none | Restarts the session and drops every binding. |
121
+
122
+ On timeout the script is asked to stop at its next action and the result says
123
+ `Stopped: the script ran longer than N ms.` If it has not stopped five seconds
124
+ later, the session process is killed and the bindings are lost.
125
+
126
+ ## Helper and daemon lifecycle
127
+
128
+ Glovebox Helper (macOS):
129
+
130
+ - `setup` builds it into `$GLOVEBOX_HOME/Glovebox Helper.app`. It is rebuilt
131
+ only when its source changes; until then, or after an upgrade, clients fail
132
+ with a message to run `glovebox setup`.
133
+ - The first client request launches it through LaunchServices; no agent has to
134
+ start it. It listens on `helper.sock`, mode 0600, and refuses other users.
135
+ - While at least one client is connected it shows a menu-bar item with the
136
+ connected sessions and their approved apps, and the entries Stop Running
137
+ Scripts, Revoke Session Approvals, Hide Agent Cursor, and Quit Glovebox
138
+ Helper.
139
+ - It quits by itself after 30 minutes with no connected clients, checked once a
140
+ minute. The next client relaunches it. Quit Glovebox Helper, `setup
141
+ --restart`, and `setup --rebuild` also stop it.
142
+
143
+ CLI daemon:
144
+
145
+ - `glovebox run` starts it on first use, detached, listening on
146
+ `$GLOVEBOX_HOME/daemon.sock` (mode 0600).
147
+ - Each session name is its own session process, created by the first `run`
148
+ that names it.
149
+ - It exits after `daemon.idleMinutes` of no requests, or on `glovebox stop`.
150
+ Its sessions go with it.
package/docs/design.md ADDED
@@ -0,0 +1,212 @@
1
+ # Design
2
+
3
+ This is the design glovebox follows. It records what we chose and why, so
4
+ changes can argue with the reasons rather than the code.
5
+
6
+ ## The bar
7
+
8
+ Measured against a real Codex computer-use trace (786 calls in one session,
9
+ median script about 130 characters), the properties that made that loop cheap:
10
+
11
+ 1. One tool call holds a whole step: several actions, then one read.
12
+ 2. The session persists. A handle bound in one call exists in the next, and
13
+ the model can define its own helpers.
14
+ 3. The default read is accessibility text that diffs after the first read.
15
+ 4. Screenshots are an explicit choice, never a side effect of an action.
16
+ 5. Clicks land in the same pixel space as the image the model saw.
17
+ 6. Pixel actions and element actions are both cheap, because some apps
18
+ (Citrix, canvases) expose a single accessibility node.
19
+ 7. No documentation dump in tool results, and no idle teardown that throws
20
+ away bindings.
21
+
22
+ The shapes we compared against:
23
+
24
+ | | Codex (`@oai/sky`) | Claude computer use | Orca | glovebox |
25
+ |---|---|---|---|---|
26
+ | Calls per step | 1 script | 1 per action, or a fixed batch list | 1 CLI call per action | 1 script |
27
+ | Persistent bindings | yes | no | no | yes |
28
+ | Default read | AX text, diffed | screenshot plus AX summary | AX text plus screenshot | AX text, diffed |
29
+ | Model-defined helpers | yes | no | no | yes |
30
+ | Pixel space | screenshot pixels | screenshot frame | window coordinates | window image pixels |
31
+ | Background input | yes | yes | partial | yes (keys, clicks, drag, scroll; foreground fallback) |
32
+ | Drawn agent cursor | yes | yes | no | yes |
33
+ | Works without vendor app | no | no | no | yes |
34
+
35
+ Claude's tools and Orca's CLI both make the model pay a round trip per
36
+ action, and every state read carries an image or a full tree. Codex's REPL is
37
+ the right shape but is tied to OpenAI's signed binaries and service.
38
+
39
+ ## Interface: one persistent JavaScript session
40
+
41
+ We expose a JavaScript session, not a set of action tools, because it is the
42
+ only shape that gets properties 1, 2, and 6 at once. Every agent we target
43
+ writes JavaScript fluently, and the surface stays close to the one Codex
44
+ models already know (`getApp`, `click`, `pressKey`, `typeText`,
45
+ `getAXState`), with `cua` kept as an alias for `gb`.
46
+
47
+ The same session is reachable two ways:
48
+
49
+ - **MCP** (`glovebox mcp`): two tools, `js` and `js_reset`. The tool
50
+ description is the cheat sheet (about 1.8 KB, sent once with `tools/list`).
51
+ `help()` prints the full reference only when asked.
52
+ - **CLI** (`glovebox run`): a background daemon keeps named sessions alive,
53
+ so agents that can only run shell commands get the same persistence.
54
+ Images go to files and the path is printed.
55
+
56
+ ### Persistence
57
+
58
+ A cell is rewritten (with acorn) so that top-level `const`, `let`, `class`,
59
+ and `import` become assignments to context-level `var`s, function
60
+ declarations are hoisted, and the body runs in an async function. The last
61
+ expression is the result. This is what Node's REPL does, without its
62
+ problem that a rejected top-level `await` is printed and swallowed instead of
63
+ failing the call.
64
+
65
+ The session runs in its own process (the kernel), so the host can stop a
66
+ runaway script. A timeout first sets an abort flag that every action checks;
67
+ if the script still has not stopped after five seconds, the kernel is killed
68
+ and the result says the bindings were lost. MCP sessions live as long as the
69
+ connection. CLI sessions live until `glovebox reset`, `glovebox stop`, or an
70
+ idle limit (an hour by default).
71
+
72
+ ## Reads and output
73
+
74
+ `getAXState` prints one line per element: index, role, label, value, id,
75
+ flags, and non-obvious actions, indented by depth. The format is close to
76
+ Codex's on purpose. Element indices are stable for the session, because the
77
+ backend keeps a table of the elements it has seen (`CFEqual` on macOS), so a
78
+ diff only shows real changes: `~` changed, `+` added, then a list of removed
79
+ indices. Frames are off by default (they are tokens most steps do not need)
80
+ and on with `{frames: true}`.
81
+
82
+ Reads emit into the result by default and return their value; `{emit:
83
+ false}` returns only. Actions emit nothing.
84
+
85
+ ## Coordinates and images
86
+
87
+ Image space is per window: `(0, 0)` is the window's top-left corner, and one
88
+ image pixel is `1 / s` screen points, where
89
+
90
+ s = min(backing scale, maxLongSide / max(w, h), sqrt(maxPixels / (w * h)))
91
+
92
+ with defaults `maxLongSide = 1568` and `maxPixels = 1.15 M`. The scale depends
93
+ only on the window size and display, so screenshots, `frames`, `center()`, and
94
+ `click([x, y])` agree even when no screenshot was taken. The screenshot is
95
+ captured at exactly that size, so there is no resize without a matching
96
+ transform. The header of every read prints the image size.
97
+
98
+ ## Input delivery
99
+
100
+ Input goes to apps in the background, the way Codex computer use does it:
101
+ the user's frontmost app keeps focus and the real cursor never moves.
102
+
103
+ - A plain left click on a pressable element (button, checkbox, link, menu
104
+ item) is sent as an accessibility press.
105
+ - Keys and typing: the helper makes the target window key *inside its app*
106
+ without activating the app, using two private SkyLight event records
107
+ (`SLPSPostEventRecordTo`: a focus record and a make-key pair, 248 bytes
108
+ each). It confirms through Accessibility that the window is key, then posts
109
+ the key events to the process (`SLEventPostToPid`). Plain
110
+ `CGEventPostToPid` is not enough: Calculator dropped those keys when another
111
+ app was frontmost, because a background app has no key window.
112
+ - Mouse clicks, drags, and scrolls: a focus record for the target window only,
113
+ then a `mouseMoved` and the button events posted to the process, each
114
+ stamped with the target pid and window (event fields 40, 51, 58, 91, 92)
115
+ and a window-local location (`CGEventSetWindowLocation`). AppKit apps get
116
+ the public `CGEventPostToPid`; Chromium/Electron apps get the SkyLight route
117
+ plus an off-screen primer click. Scroll events go through both routes.
118
+ Background events carry no click count unless one is set, and AppKit
119
+ ignores a mouse-down without it.
120
+ - A focus guard notes the frontmost app before each background action and
121
+ gives it focus back if the target took it anyway.
122
+ - Each private function is looked up at runtime. If one is missing (a future
123
+ macOS may remove it), the helper says so and glovebox falls back to the
124
+ foreground route: bring the app forward once per call, then post events.
125
+ On that route, switching to another app stops the script. `glovebox doctor`
126
+ reports whether background input is available, and `app.lastRoute` shows
127
+ which route the last action took.
128
+ - The record layouts and event fields come from open-source tools that do the
129
+ same: yabai, trycua's cua-driver, and Peekaboo (all MIT). Codex's service
130
+ links only public frameworks and resolves these calls by hashed name at
131
+ runtime, and Claude desktop's helper references the same calls, so this is
132
+ the established approach, not a novel hack. It is still private API.
133
+ - A drawn agent cursor (a borderless, click-through window ordered just above
134
+ the target window, not above everything) animates to each action's point.
135
+ It is included in screen recordings and hides after a few seconds idle.
136
+ - Pixel targets outside the window image are refused, so a bad coordinate
137
+ cannot click into another app.
138
+ - While any agent is connected the helper shows a menu-bar item listing the
139
+ sessions and their approved apps, with Stop Running Scripts, Revoke Session
140
+ Approvals, Hide Agent Cursor, and Quit.
141
+
142
+ Measured on macOS 27 with Firefox frontmost throughout (`glovebox smoke
143
+ --full`): background keys in Calculator, a pixel click that placed TextEdit's
144
+ caret at the start of the text, a drag that selected a line, and a scroll
145
+ that moved the scroll bar, with the frontmost app unchanged.
146
+
147
+ ## Approval and safety
148
+
149
+ Computer use can click anything, so approval is part of the product:
150
+
151
+ - The gate lives in the helper, the process that holds the macOS grants, not
152
+ in the scripting layer. A client cannot skip it by talking to the socket.
153
+ - Default `approval: "ask"`: the first action in each app shows a native
154
+ dialog naming the agent (from the MCP `clientInfo`) and the app: Allow for
155
+ This Session, Always Allow (persisted to `config.json`), or Deny.
156
+ - `allowlist` mode never prompts. `allow-all` is an explicit opt-in.
157
+ - Terminals, password managers, Keychain Access, System Settings, Script
158
+ Editor, Automator, and the helper itself are blocked by default and never
159
+ offered in a dialog. Only an explicit `glovebox allow` unlocks one. The
160
+ full list is in `SECURITY.md`.
161
+ - Switching away stops a script on the foreground route; background scripts
162
+ keep running. A script brings an app forward at most once per call; if the app is no longer in front later in
163
+ the same call, the user took over, and the script stops rather than taking
164
+ focus back. (An earlier version re-activated before every key and fought
165
+ the user for focus.)
166
+ - With Input Monitoring granted, Escape pressed by the user (glovebox marks
167
+ its own events) stops the running script. In testing on macOS 27, an
168
+ Accessibility-only global key monitor saw no key events, so Escape is
169
+ documented as needing Input Monitoring.
170
+ - Every action is appended to `~/.glovebox/actions.log` as one JSON object:
171
+ `ts`, `client`, `step` (the call's title, when given), then the action's own
172
+ fields (`app`, `action`, and for example `target`, `keys`, or `length`).
173
+ Typed text is logged as a length unless `log.text` is true.
174
+ - The helper's socket is mode 0600 and checks the peer's uid.
175
+
176
+ ## Signing and permissions
177
+
178
+ macOS ties Accessibility, Screen Recording, and Input Monitoring grants to the
179
+ helper's designated requirement. An ad-hoc signature's requirement is the
180
+ binary's hash, so every rebuild lost the grants (and stale entries kept
181
+ showing as on). `glovebox signing --create` makes a self-signed code-signing
182
+ certificate in the login keychain; signed with it, the requirement becomes
183
+ "identifier + certificate", and grants survive rebuilds. The certificate
184
+ does not need to be trusted. Verified by rebuilding twice without re-granting.
185
+
186
+ ## The macOS backend
187
+
188
+ Glovebox Helper is a small Swift app (`native/macos/GloveboxHelper.swift`)
189
+ built locally by `glovebox setup` with the Xcode command line tools. It is
190
+ signed with the local certificate from `glovebox signing --create` when one
191
+ exists, and ad hoc otherwise. It runs as its own app, launched through LaunchServices, so
192
+ macOS attributes Accessibility and Screen Recording to "Glovebox Helper"
193
+ rather than to whatever terminal or agent started it. One grant covers every
194
+ agent. (The first prototype ran the native code as a child process; macOS
195
+ then charges the permission to the parent, and some hosts disclaim
196
+ responsibility for their children, so the grant never applies.)
197
+
198
+ It uses AXUIElement for the tree and actions, CGEvent for input, and
199
+ ScreenCaptureKit for window capture, all public APIs. Background input
200
+ (see Input delivery) also uses private SkyLight functions, looked up at
201
+ runtime, with a foreground fallback when they are missing. It speaks JSON
202
+ lines over `~/.glovebox/helper.sock`. See `backend-protocol.md`.
203
+
204
+ ## What we did not do
205
+
206
+ - **Wrap OpenAI's Sky service.** It works on a Mac with ChatGPT installed, but
207
+ only by launching OpenAI's signed binaries to satisfy the service's
208
+ sender check. That is fragile across updates, depends on a vendor install,
209
+ and works around an access control, which is a poor fit for a public tool.
210
+ - **One tool per action.** Every action becomes a model round trip.
211
+ - **Screenshots after every action.** Images are the most expensive thing in
212
+ the context; the model asks when it needs one.
@@ -0,0 +1,162 @@
1
+ # Recipes
2
+
3
+ Short scripts an agent can paste into one `js` call (or `glovebox run`). They
4
+ use only the API in `api.md`.
5
+
6
+ Only recipes 1 and 2, the Calculator and TextEdit flows, match what `glovebox
7
+ smoke` has exercised on a real machine (see `testing.md`). Every other recipe
8
+ is marked "not yet run on a real machine": it follows the documented API and
9
+ the code, but nobody has run it. Treat the details, such as app names and menu
10
+ labels, as things to check against the tree.
11
+
12
+ ## 1. Calculator: one batched step
13
+
14
+ Bind the app, press keys, read once. This is the loop `glovebox smoke` runs.
15
+
16
+ ```js
17
+ const calc = await gb.getApp("Calculator");
18
+ await calc.pressKey("Escape");
19
+ await calc.typeText("6*7=");
20
+ await calc.getAXState(); // the display line shows value="42"
21
+ ```
22
+
23
+ Clicking by element index works in a later call, because `calc` persists. Find
24
+ the index in the tree (`14 button "7"` means index 14), then:
25
+
26
+ ```js
27
+ await calc.click(14);
28
+ await calc.getAXState(); // prints only what changed
29
+ ```
30
+
31
+ ## 2. TextEdit: click into the text and type
32
+
33
+ Uses the tree for the text area's box, a pixel click inside it, then typing.
34
+ Assumes TextEdit already has a document window open. This follows the
35
+ `glovebox smoke --full` flow.
36
+
37
+ ```js
38
+ const te = await gb.getApp("TextEdit");
39
+ const tree = await te.getAXState({ frames: true, disableDiffing: true, emit: false });
40
+ const [, x, y] = tree.match(/text area[^\n]*@(\d+),(\d+)/);
41
+ await te.click([Number(x) + 8, Number(y) + 10]); // inside the text area
42
+ await te.typeText("Hello from glovebox\n");
43
+ await te.getAXState();
44
+ ```
45
+
46
+ ## 3. Open a menu item through the AX tree
47
+
48
+ Not yet run on a real machine. There is no menu API. `menus: true` adds the
49
+ menu bar to the tree, and menu bar items and menu items are pressed through
50
+ accessibility like buttons. This clicks the bar item, reads again, then
51
+ clicks the item.
52
+
53
+ ```js
54
+ const app = await gb.getApp("TextEdit");
55
+ const find = (tree, role, label) => {
56
+ const m = tree.match(new RegExp(`^\\s*(\\d+) ${role} "${label}"`, "m"));
57
+ if (!m) throw new Error(`No ${role} "${label}" in the tree`);
58
+ return Number(m[1]);
59
+ };
60
+ const clickMenu = async (app, menu, item) => {
61
+ let tree = await app.getAXState({ menus: true, disableDiffing: true, emit: false });
62
+ await app.click(find(tree, "menu bar item", menu));
63
+ tree = await app.getAXState({ menus: true, disableDiffing: true, emit: false });
64
+ await app.click(find(tree, "menu item", item));
65
+ };
66
+ await clickMenu(app, "Edit", "Select All");
67
+ await app.getAXState();
68
+ ```
69
+
70
+ ## 4. Pixel fallback for Citrix and canvases
71
+
72
+ Not yet run on a real machine. When the tree is one node, take a screenshot,
73
+ read coordinates off the image, and click `[x, y]`. Pixels are the
74
+ screenshot's own: `(0, 0)` is the window's top-left corner.
75
+
76
+ ```js
77
+ const app = await gb.getApp("Citrix Viewer"); // any app with a one-node tree
78
+ const shot = await app.getScreenshot(); // the image comes back in the result
79
+ console.log(shot.width, shot.height);
80
+ await app.click([412, 230]); // a point you read off the image
81
+ await app.typeText("hello");
82
+ await app.getScreenshot(); // look again before the next step
83
+ ```
84
+
85
+ Re-take the screenshot if the window was resized: a click outside the window
86
+ image throws instead of landing in another app.
87
+
88
+ ## 5. Define a helper once, reuse it
89
+
90
+ Not yet run on a real machine. Functions and constants persist across calls
91
+ until `js_reset`, so a repeated lookup becomes one name.
92
+
93
+ ```js
94
+ const clickLabel = async (app, label) => {
95
+ const tree = await app.getAXState({ emit: false, disableDiffing: true });
96
+ const m = tree.match(new RegExp(`^\\s*(\\d+) button "${label}"`, "m"));
97
+ if (!m) throw new Error(`No button "${label}"`);
98
+ await app.click(Number(m[1]));
99
+ };
100
+ ```
101
+
102
+ Later calls:
103
+
104
+ ```js
105
+ const calc = await gb.getApp("Calculator");
106
+ for (const label of ["7", "Add", "1", "Equals"]) await clickLabel(calc, label);
107
+ await calc.getAXState();
108
+ ```
109
+
110
+ ## 6. Set a field's value directly
111
+
112
+ Not yet run on a real machine. A line with `settable` in its flags can take
113
+ `setValue`, which is faster and more exact than typing. Read it back to check.
114
+
115
+ ```js
116
+ const tree = await app.getAXState({ disableDiffing: true });
117
+ // e.g. 9 text field "Name" (settable)
118
+ await app.setValue(9, "Ada Lovelace");
119
+ await app.getAXState();
120
+ ```
121
+
122
+ If the app ignores the new value, fall back to `await app.focus(9)` then
123
+ `await app.typeText("Ada Lovelace")`. `help("setValue")` prints the reference.
124
+
125
+ ## 7. Handle a "Stopped:" result
126
+
127
+ Not yet run on a real machine. A result that starts with `Stopped:` means the
128
+ user took over (switched apps, pressed Escape with Input Monitoring granted,
129
+ or the script hit its time limit). Do not retry or continue; tell the user and
130
+ ask. Inside a script, never swallow it (`clickLabel` is from recipe 5):
131
+
132
+ ```js
133
+ try {
134
+ for (const label of ["7", "Add", "1", "Equals"]) await clickLabel(calc, label);
135
+ } catch (error) {
136
+ if (error.name === "Stopped") throw error; // let the result say Stopped:
137
+ console.log(`click failed: ${error.message}`);
138
+ }
139
+ ```
140
+
141
+ Continue only after the user asks, and start with a fresh read:
142
+ `await calc.getAXState({ disableDiffing: true })`.
143
+
144
+ ## 8. Force foreground for an app that ignores background input
145
+
146
+ Not yet run on a real machine. If background clicks or keys do nothing (some
147
+ Java, Tk, and game windows), bring the app forward for those actions. A plain
148
+ click on a button is pressed through accessibility and never needs this; it
149
+ matters for mouse clicks, keys, typing, scroll, and drag.
150
+
151
+ ```js
152
+ const app = await gb.getApp("Some Java App");
153
+ await app.click([120, 80], { foreground: true });
154
+ await app.typeText("hello", { foreground: true });
155
+ app.lastRoute; // "foreground"
156
+ ```
157
+
158
+ To make every action on an app use the foreground route, pass
159
+ `gb.getApp(name, {foreground: true})` on the call that first binds it, or ask
160
+ the user to add the app to `foregroundApps` in the config (`cli.md`). In
161
+ foreground mode a script brings the app forward once per call and stops if
162
+ the user switches away.