@ai-agent-forge/plugin-desktop-macos-input 0.0.0-stage → 0.88.4

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,805 @@
1
+ /**
2
+ * macOS Accessibility driver (query + bounded input actions) and the input
3
+ * adapter facade for this package. Structurally mirrors the Windows package's
4
+ * `windows-uia-adapter.ts` + `windows-input-adapter.ts` (inlined from
5
+ * agent-forge `src/plugins/desktop/*`, D-075 S3) with these deliberate
6
+ * differences:
7
+ * - Driver boundary is `osascript -l JavaScript -e <script>` (JXA) instead of
8
+ * PowerShell UIA. The query script walks the System Events accessibility
9
+ * tree (`Application("com.apple.systemevents")`); the action script re-locates the
10
+ * observed target by its deterministic enumeration identity
11
+ * (`window-<pid>-<windowIndex>` / `<windowId>:<role>:<childIndex>`; Windows
12
+ * uses native HWND handles instead — mac AX exposes no stable handles).
13
+ * - Bounded actions: focus (frontmost + AXRaise / AXFocused), click
14
+ * (AXPress with a CGEvent coordinate-click fallback, plus screen-coordinate
15
+ * click targets — click-only by ruling), type (focus + System Events
16
+ * keystroke), key (System Events keyCode/keystroke over a minimal SendKeys
17
+ * aligned table, parsed host-side), scroll (CGEvent scroll wheel).
18
+ * - The facade exposes no replay method (this package ships no replay);
19
+ * sensitive actions stay behind the shared `requiresDesktopUserConfirmation`
20
+ * fail-closed gate.
21
+ * - Non-darwin platforms never throw at import or call time: probe reports
22
+ * `{available:false, reason:"macOS Accessibility requires darwin"}` and
23
+ * every tool returns an explicit `unsupported` result. Missing
24
+ * Accessibility (assistive access) permission is reported fail-closed as
25
+ * `accessibility-permission-required`; no degraded mode.
26
+ * This file is a deliberate copy shared (not imported) with the observe
27
+ * package's adapter; the observe copy carries the query script only.
28
+ */
29
+ import { spawnSync as nodeSpawnSync } from "node:child_process";
30
+ import { detachedFreeze } from "@agent-forge/plugin-sdk";
31
+ import { requiresDesktopUserConfirmation } from "./types.js";
32
+ const READONLY_CAPABILITIES = ["desktop-session", "desktop-uia", "desktop-input"];
33
+ const SUPPORTED_ACTIONS = ["focus", "click", "type", "key", "scroll"];
34
+ const error = (code, message, causeCode) => ({
35
+ version: 1,
36
+ code,
37
+ message,
38
+ ...(causeCode === undefined ? {} : { causeCode }),
39
+ });
40
+ const unsupported = (operationId, reason, causeCode = "platform-unavailable") => ({
41
+ version: 1,
42
+ operationId,
43
+ status: "unsupported",
44
+ summary: reason,
45
+ error: error(causeCode === "action-unsupported" ? "action_unsupported" : "platform_unavailable", reason, causeCode),
46
+ });
47
+ const rectangle = (raw) => ({
48
+ x: Number.isFinite(raw.x) ? Number(raw.x) : 0,
49
+ y: Number.isFinite(raw.y) ? Number(raw.y) : 0,
50
+ width: Number.isFinite(raw.width) ? Number(raw.width) : 0,
51
+ height: Number.isFinite(raw.height) ? Number(raw.height) : 0,
52
+ });
53
+ const isPermissionReason = (value) => /assistive access|not authorized|accessibility-permission-required/i.test(value);
54
+ /**
55
+ * JXA observation script. Enumerates every non-background System Events
56
+ * process's windows (caps: 64 windows, 256 controls total, mirroring the
57
+ * projection budgets) with role/name/position/size/enabled/value per control,
58
+ * plus the frontmost process's focused element matched by role and frame.
59
+ * Permission or automation failures are reported in-band as
60
+ * `{available:false, reason:...}` so the probe can classify them; the reason
61
+ * literal `accessibility-permission-required` is reserved for missing
62
+ * Accessibility (assistive access) grants.
63
+ */
64
+ const QUERY_SCRIPT = `
65
+ var se = Application('com.apple.systemevents');
66
+ function safe(fn) { try { return fn(); } catch (e) { return undefined; } }
67
+ function rectOf(el) {
68
+ var p = safe(function() { return el.position(); });
69
+ var z = safe(function() { return el.size(); });
70
+ if (p === undefined || z === undefined || p.length < 2 || z.length < 2) return {x: 0, y: 0, width: 0, height: 0};
71
+ return {x: Math.round(p[0]), y: Math.round(p[1]), width: Math.round(z[0]), height: Math.round(z[1])};
72
+ }
73
+ function axReason(e) {
74
+ var msg = String(e && e.message !== undefined ? e.message : e);
75
+ var num = e && typeof e.errorNumber === 'number' ? e.errorNumber : 0;
76
+ if (msg.indexOf('assistive access') !== -1 || msg.indexOf('not authorized') !== -1 ||
77
+ num === -25211 || num === -1719 || num === -1743) return 'accessibility-permission-required';
78
+ return msg;
79
+ }
80
+ try {
81
+ var out = {available: true, windows: [], controls: [], focusedControlId: null};
82
+ var procs = se.processes();
83
+ var focused = null;
84
+ for (var fi = 0; fi < procs.length; fi++) {
85
+ var fp = procs[fi];
86
+ if (safe(function() { return fp.frontmost(); }) !== true) continue;
87
+ var fe = safe(function() { return fp.attributes['AXFocusedUIElement'].value(); });
88
+ if (fe !== undefined) focused = {role: safe(function() { return fe.role(); }), rect: rectOf(fe)};
89
+ break;
90
+ }
91
+ var windowCount = 0;
92
+ var controlCount = 0;
93
+ for (var pi = 0; pi < procs.length && windowCount < 64; pi++) {
94
+ var proc = procs[pi];
95
+ if (safe(function() { return proc.backgroundOnly(); }) === true) continue;
96
+ var wins = safe(function() { return proc.windows(); });
97
+ if (wins === undefined || wins.length === 0) continue;
98
+ var pid = safe(function() { return proc.unixId(); });
99
+ var pname = safe(function() { return proc.name(); });
100
+ if (typeof pname !== 'string' || pname.length === 0) pname = 'unknown';
101
+ for (var wi = 0; wi < wins.length && windowCount < 64; wi++) {
102
+ var win = wins[wi];
103
+ var winRect = rectOf(win);
104
+ var winId = 'window-' + pid + '-' + wi;
105
+ windowCount++;
106
+ out.windows.push({id: winId, processId: pid, processName: pname,
107
+ name: safe(function() { return win.name(); }) || '', x: winRect.x, y: winRect.y,
108
+ width: winRect.width, height: winRect.height});
109
+ var contents = safe(function() { return win.entireContents(); });
110
+ if (contents === undefined) continue;
111
+ for (var ci = 0; ci < contents.length && controlCount < 256; ci++) {
112
+ var el = contents[ci];
113
+ var role = safe(function() { return el.role(); });
114
+ if (typeof role !== 'string' || role.length === 0) continue;
115
+ var r = rectOf(el);
116
+ var controlId = winId + ':' + role + ':' + ci;
117
+ var value = safe(function() { return el.value(); });
118
+ out.controls.push({windowId: winId, controlId: controlId, role: role,
119
+ name: safe(function() { return el.name(); }) || safe(function() { return el.title(); }) || '',
120
+ enabled: safe(function() { return el.enabled(); }) !== false,
121
+ value: typeof value === 'string' && value.length > 0 ? value : undefined,
122
+ x: r.x, y: r.y, width: r.width, height: r.height});
123
+ controlCount++;
124
+ if (focused !== null && out.focusedControlId === null && role === focused.role &&
125
+ r.x === focused.rect.x && r.y === focused.rect.y &&
126
+ r.width === focused.rect.width && r.height === focused.rect.height) {
127
+ out.focusedControlId = controlId;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ JSON.stringify(out);
133
+ } catch (e) {
134
+ JSON.stringify({available: false, reason: axReason(e)});
135
+ }`;
136
+ /**
137
+ * JXA bounded-input script. Reads the JSON payload from
138
+ * `AGENT_FORGE_MACOS_AX_INPUT`: `{kind, target:{windowId?, controlId?,
139
+ * coordinates?}, text?, key:{keyCode?, text?, modifiers?},
140
+ * scroll:{up, lines}}`. The target is re-located through the same
141
+ * deterministic enumeration the query script used; an unfound identity is a
142
+ * stale target. Output: `{ok:true}` or `{ok:false, code, reason}`.
143
+ */
144
+ const ACTION_SCRIPT = `
145
+ ObjC.import('CoreGraphics');
146
+ var se = Application('com.apple.systemevents');
147
+ function safe(fn) { try { return fn(); } catch (e) { return undefined; } }
148
+ function rectOf(el) {
149
+ var p = safe(function() { return el.position(); });
150
+ var z = safe(function() { return el.size(); });
151
+ if (p === undefined || z === undefined || p.length < 2 || z.length < 2) return {x: 0, y: 0, width: 0, height: 0};
152
+ return {x: Math.round(p[0]), y: Math.round(p[1]), width: Math.round(z[0]), height: Math.round(z[1])};
153
+ }
154
+ function findProcess(pid) {
155
+ var procs = se.processes();
156
+ for (var i = 0; i < procs.length; i++) {
157
+ if (safe(function() { return procs[i].unixId(); }) === pid) return procs[i];
158
+ }
159
+ return undefined;
160
+ }
161
+ function parseWindowId(windowId) {
162
+ var m = /^window-(\\d+)-(\\d+)$/.exec(String(windowId));
163
+ return m === null ? undefined : {pid: Number(m[1]), index: Number(m[2])};
164
+ }
165
+ function findWindow(windowId) {
166
+ var parsed = parseWindowId(windowId);
167
+ if (parsed === undefined) return undefined;
168
+ var proc = findProcess(parsed.pid);
169
+ if (proc === undefined) return undefined;
170
+ var wins = safe(function() { return proc.windows(); });
171
+ if (wins === undefined || parsed.index >= wins.length) return undefined;
172
+ return {proc: proc, win: wins[parsed.index]};
173
+ }
174
+ function findControl(windowId, controlId) {
175
+ var located = findWindow(windowId);
176
+ if (located === undefined) return undefined;
177
+ var parts = String(controlId).split(':');
178
+ if (parts.length < 3) return undefined;
179
+ var index = Number(parts[parts.length - 1]);
180
+ var role = parts[parts.length - 2];
181
+ var contents = safe(function() { return located.win.entireContents(); });
182
+ if (contents === undefined || index >= contents.length) return undefined;
183
+ var el = contents[index];
184
+ if (safe(function() { return el.role(); }) !== role) return undefined;
185
+ return {proc: located.proc, win: located.win, el: el};
186
+ }
187
+ function cgClick(x, y) {
188
+ var pt = {x: x, y: y};
189
+ var move = $.CGEventCreateMouseEvent($(), $.kCGEventMouseMoved, pt, $.kCGMouseButtonLeft);
190
+ $.CGEventPost($.kCGHIDEventTap, move);
191
+ var down = $.CGEventCreateMouseEvent($(), $.kCGEventLeftMouseDown, pt, $.kCGMouseButtonLeft);
192
+ $.CGEventPost($.kCGHIDEventTap, down);
193
+ var up = $.CGEventCreateMouseEvent($(), $.kCGEventLeftMouseUp, pt, $.kCGMouseButtonLeft);
194
+ $.CGEventPost($.kCGHIDEventTap, up);
195
+ }
196
+ function cgScroll(x, y, lines, up) {
197
+ var pt = {x: x, y: y};
198
+ var move = $.CGEventCreateMouseEvent($(), $.kCGEventMouseMoved, pt, $.kCGMouseButtonLeft);
199
+ $.CGEventPost($.kCGHIDEventTap, move);
200
+ var ev = $.CGEventCreateScrollWheelEvent($(), $.kCGScrollEventUnitLine, 1, up ? lines : -lines);
201
+ $.CGEventPost($.kCGHIDEventTap, ev);
202
+ }
203
+ function setFocused(el) {
204
+ try { el.focused = true; return true; } catch (e) { return false; }
205
+ }
206
+ try {
207
+ var payload = JSON.parse($.NSProcessInfo.processInfo.environment.objectForKey('AGENT_FORGE_MACOS_AX_INPUT').js);
208
+ var target = payload.target || {};
209
+ var el = undefined;
210
+ var win = undefined;
211
+ var proc = undefined;
212
+ if (target.windowId !== undefined && target.controlId !== undefined) {
213
+ var control = findControl(target.windowId, target.controlId);
214
+ if (control === undefined) throw {stale: true};
215
+ el = control.el; win = control.win; proc = control.proc;
216
+ } else if (target.windowId !== undefined) {
217
+ var located = findWindow(target.windowId);
218
+ if (located === undefined) throw {stale: true};
219
+ win = located.win; proc = located.proc;
220
+ }
221
+ if (payload.kind === 'focus') {
222
+ if (el !== undefined) {
223
+ if (!setFocused(el)) throw {reason: 'focus_failed'};
224
+ } else if (win !== undefined) {
225
+ try { proc.frontmost = true; } catch (e) {}
226
+ if (safe(function() { return win.actions['AXRaise'].perform(); }) === undefined && safe(function() { return proc.frontmost(); }) !== true) {
227
+ throw {reason: 'focus_failed'};
228
+ }
229
+ } else {
230
+ throw {reason: 'ax_action_exception'};
231
+ }
232
+ } else if (payload.kind === 'click') {
233
+ if (target.coordinates !== undefined) {
234
+ cgClick(Number(target.coordinates.x), Number(target.coordinates.y));
235
+ } else if (el !== undefined) {
236
+ var performed = false;
237
+ try { el.actions['AXPress'].perform(); performed = true; } catch (e) {}
238
+ if (!performed) {
239
+ var r = rectOf(el);
240
+ if (r.width <= 0 && r.height <= 0) throw {reason: 'press_unavailable'};
241
+ cgClick(r.x + Math.floor(r.width / 2), r.y + Math.floor(r.height / 2));
242
+ }
243
+ } else if (win !== undefined) {
244
+ var wr = rectOf(win);
245
+ cgClick(wr.x + Math.floor(wr.width / 2), wr.y + Math.floor(wr.height / 2));
246
+ } else {
247
+ throw {reason: 'ax_action_exception'};
248
+ }
249
+ } else if (payload.kind === 'type') {
250
+ if (el !== undefined && !setFocused(el)) throw {reason: 'focus_failed'};
251
+ if (el === undefined && win !== undefined) {
252
+ try { proc.frontmost = true; } catch (e) {}
253
+ }
254
+ se.keystroke(String(payload.text));
255
+ } else if (payload.kind === 'key') {
256
+ if (el !== undefined && !setFocused(el)) throw {reason: 'focus_failed'};
257
+ var using = Array.isArray(payload.key.modifiers) && payload.key.modifiers.length > 0 ? {using: payload.key.modifiers} : {};
258
+ if (typeof payload.key.keyCode === 'number') {
259
+ se.keyCode(payload.key.keyCode, using);
260
+ } else {
261
+ se.keystroke(String(payload.key.text), using);
262
+ }
263
+ } else if (payload.kind === 'scroll') {
264
+ var center = target.coordinates !== undefined
265
+ ? {x: Number(target.coordinates.x), y: Number(target.coordinates.y)}
266
+ : (el !== undefined ? rectOf(el) : (win !== undefined ? rectOf(win) : {x: 0, y: 0, width: 0, height: 0}));
267
+ cgScroll(center.x + Math.floor((center.width || 0) / 2), center.y + Math.floor((center.height || 0) / 2),
268
+ Number(payload.scroll.lines), payload.scroll.up === true);
269
+ } else {
270
+ throw {reason: 'ax_action_exception'};
271
+ }
272
+ JSON.stringify({ok: true});
273
+ } catch (e) {
274
+ if (e && e.stale === true) {
275
+ JSON.stringify({ok: false, code: 'stale_target', reason: 'ax_action_exception'});
276
+ } else {
277
+ var reason = e && typeof e.reason === 'string' ? e.reason : 'ax_action_exception';
278
+ JSON.stringify({ok: false, code: 'action_failed', reason: reason});
279
+ }
280
+ }`;
281
+ const parseJson = (stdout) => {
282
+ try {
283
+ const parsed = JSON.parse(stdout.trim());
284
+ return typeof parsed === "object" && parsed !== null ? parsed : undefined;
285
+ }
286
+ catch {
287
+ return undefined;
288
+ }
289
+ };
290
+ /**
291
+ * Minimal SendKeys-aligned key grammar, parsed host-side because JXA has no
292
+ * SendKeys equivalent. Grammar: optional modifier prefixes (`+` shift,
293
+ * `^` control, `%` option — the Windows SendKeys mapping) followed by either
294
+ * one `{NAME}` special key or exactly one literal character.
295
+ */
296
+ const MAC_KEY_CODES = Object.freeze({
297
+ RETURN: 36,
298
+ ENTER: 36,
299
+ TAB: 48,
300
+ ESC: 53,
301
+ ESCAPE: 53,
302
+ DEL: 117,
303
+ DELETE: 117,
304
+ BS: 51,
305
+ BKSP: 51,
306
+ BACKSPACE: 51,
307
+ UP: 126,
308
+ DOWN: 125,
309
+ LEFT: 123,
310
+ RIGHT: 124,
311
+ HOME: 115,
312
+ END: 119,
313
+ PGUP: 116,
314
+ PAGEUP: 116,
315
+ PGDN: 121,
316
+ PAGEDOWN: 121,
317
+ SPACE: 49,
318
+ F1: 122,
319
+ F2: 120,
320
+ F3: 99,
321
+ F4: 118,
322
+ F5: 96,
323
+ F6: 97,
324
+ F7: 98,
325
+ F8: 100,
326
+ F9: 101,
327
+ F10: 109,
328
+ F11: 103,
329
+ F12: 111,
330
+ });
331
+ const parseMacKey = (key) => {
332
+ const modifiers = [];
333
+ let index = 0;
334
+ for (; index < key.length; index += 1) {
335
+ const modifier = key[index];
336
+ if (modifier === "+")
337
+ modifiers.push("shift down");
338
+ else if (modifier === "^")
339
+ modifiers.push("control down");
340
+ else if (modifier === "%")
341
+ modifiers.push("option down");
342
+ else
343
+ break;
344
+ }
345
+ const unit = key.slice(index);
346
+ const braced = /^\{([A-Z0-9]+)\}$/.exec(unit);
347
+ if (braced !== null) {
348
+ const keyCode = MAC_KEY_CODES[braced[1] ?? ""];
349
+ if (keyCode === undefined)
350
+ return undefined;
351
+ return { keyCode, ...(modifiers.length === 0 ? {} : { modifiers }) };
352
+ }
353
+ if (unit.length !== 1)
354
+ return undefined;
355
+ return { text: unit, ...(modifiers.length === 0 ? {} : { modifiers }) };
356
+ };
357
+ const parseMacScroll = (text) => {
358
+ const match = /^(up|down)(?::(\d+))?$/.exec(text);
359
+ if (match === null)
360
+ return undefined;
361
+ const lines = match[2] === undefined ? 3 : Number(match[2]);
362
+ if (lines < 1 || lines > 100)
363
+ return undefined;
364
+ return { up: match[1] === "up", lines };
365
+ };
366
+ export class MacosAccessibilityDriver {
367
+ driverId = "macos-ax";
368
+ driverVersion = "1.0.0";
369
+ command;
370
+ timeoutMs;
371
+ platform;
372
+ spawn;
373
+ sessions = new Map();
374
+ targets = new Map();
375
+ sessionTargetKeys = new Map();
376
+ latestObservationIds = new Map();
377
+ observedSessions = new Set();
378
+ nextSessionId = 1;
379
+ nextObservationId = 1;
380
+ constructor(options = {}) {
381
+ this.command = options.command ?? "osascript";
382
+ // 60s: Apple events from an SSH (Background launchd) session run ~100x
383
+ // slower than a GUI session — one full enumeration measured 38s there;
384
+ // GUI sessions complete in seconds and stay unaffected by the raise.
385
+ this.timeoutMs = options.timeoutMs ?? 60_000;
386
+ this.platform = options.platform ?? process.platform;
387
+ this.spawn =
388
+ options.spawnSync ??
389
+ ((command, args, spawnOptions) => nodeSpawnSync(command, args, spawnOptions));
390
+ }
391
+ probe() {
392
+ if (this.platform !== "darwin") {
393
+ return {
394
+ available: false,
395
+ platform: this.platform,
396
+ driver: "macos-ax",
397
+ reason: "macOS Accessibility requires darwin",
398
+ };
399
+ }
400
+ const run = this.run(QUERY_SCRIPT);
401
+ if (run === undefined)
402
+ return {
403
+ available: false,
404
+ platform: this.platform,
405
+ driver: "macos-ax",
406
+ reason: "macOS Accessibility requires darwin",
407
+ };
408
+ if (run.failed)
409
+ return {
410
+ available: false,
411
+ platform: this.platform,
412
+ driver: "macos-ax",
413
+ reason: isPermissionReason(run.stderr) ? "accessibility-permission-required" : "osascript is unavailable",
414
+ };
415
+ const raw = parseJson(run.stdout);
416
+ if (raw === undefined)
417
+ return {
418
+ available: false,
419
+ platform: this.platform,
420
+ driver: "macos-ax",
421
+ reason: "osascript is unavailable",
422
+ };
423
+ if (raw.available === true)
424
+ return {
425
+ available: true,
426
+ platform: this.platform,
427
+ driver: "macos-ax",
428
+ capabilities: READONLY_CAPABILITIES,
429
+ };
430
+ const reason = typeof raw.reason === "string" && raw.reason.trim() !== "" ? raw.reason : "macOS Accessibility probe failed";
431
+ return { available: false, platform: this.platform, driver: "macos-ax", reason };
432
+ }
433
+ openSession(request) {
434
+ const probe = this.probe();
435
+ if (!probe.available)
436
+ return {
437
+ status: "unsupported",
438
+ error: error("platform_unavailable", probe.reason ?? "macOS Accessibility is unavailable", "platform-unavailable"),
439
+ };
440
+ const session = detachedFreeze({
441
+ version: 1,
442
+ sessionId: `desktop-macos-session-${this.nextSessionId++}`,
443
+ isolation: request.isolation,
444
+ status: "ready",
445
+ ...(request.applicationId === undefined ? {} : { applicationId: request.applicationId }),
446
+ });
447
+ this.sessions.set(session.sessionId, session);
448
+ return { status: "succeeded", session };
449
+ }
450
+ observe(sessionId) {
451
+ const session = this.sessions.get(sessionId);
452
+ if (session === undefined)
453
+ return this.failed("observe", "session_not_found", "Session does not exist");
454
+ if (session.status === "stopped")
455
+ return this.cancelled("observe", "Session has been stopped");
456
+ const run = this.run(QUERY_SCRIPT);
457
+ const raw = run === undefined || run.failed ? undefined : parseJson(run.stdout);
458
+ if (raw === undefined || raw.available !== true) {
459
+ this.invalidateObservation(sessionId);
460
+ return unsupported("observe", "macOS Accessibility observation failed");
461
+ }
462
+ return this.toObservation(session, raw);
463
+ }
464
+ execute(request) {
465
+ const session = this.sessions.get(request.sessionId);
466
+ if (session === undefined)
467
+ return this.failed(request.operationId, "session_not_found", "Session does not exist");
468
+ if (session.status === "stopped")
469
+ return this.cancelled(request.operationId, "Session has been stopped");
470
+ if (requiresDesktopUserConfirmation(request.action.kind))
471
+ return {
472
+ version: 1,
473
+ operationId: request.operationId,
474
+ status: "needs_user",
475
+ summary: `User confirmation required for ${request.action.kind}`,
476
+ error: error("needs_user", `The ${request.action.kind} action is paused for the user`, request.action.kind),
477
+ };
478
+ if (!SUPPORTED_ACTIONS.includes(request.action.kind))
479
+ return unsupported(request.operationId, "The selected macOS Accessibility adapter does not support this action", "action-unsupported");
480
+ if (request.action.target === undefined)
481
+ return this.failed(request.operationId, "target_required", "This desktop action requires an observed target");
482
+ if (request.action.target.kind === "coordinates" && request.action.kind !== "click")
483
+ return this.failed(request.operationId, "coordinates_unsupported", "Coordinate targets are supported by the click action only");
484
+ if (request.action.kind === "type" && typeof request.action.text !== "string")
485
+ return this.failed(request.operationId, "text_required", "Type action requires text");
486
+ if (request.action.kind === "key" && typeof request.action.key !== "string")
487
+ return this.failed(request.operationId, "key_required", "Key action requires a key sequence");
488
+ if (request.action.kind === "scroll" && typeof request.action.text !== "string")
489
+ return this.failed(request.operationId, "direction_required", "Scroll action requires an up/down direction");
490
+ const postconditions = request.postconditions ?? [];
491
+ if (postconditions.length > 0 && request.reobserveAfterAction === false)
492
+ return this.failed(request.operationId, "reobserve_required", "Postconditions require an action re-observation");
493
+ const payload = this.actionPayload(request);
494
+ if (payload === undefined)
495
+ return this.failed(request.operationId, payloadErrorCode(request.action), payloadErrorMessage(request.action));
496
+ const raw = this.run(ACTION_SCRIPT, payload);
497
+ if (raw === undefined || raw.failed)
498
+ return unsupported(request.operationId, "macOS Accessibility action failed");
499
+ const parsed = parseJson(raw.stdout);
500
+ if (parsed === undefined)
501
+ return unsupported(request.operationId, "macOS Accessibility action failed");
502
+ if (parsed.ok !== true) {
503
+ if (parsed.code === "stale_target") {
504
+ this.invalidateObservation(request.sessionId);
505
+ return this.failed(request.operationId, "stale_target", "Target identity is stale; reobserve the desktop before retrying");
506
+ }
507
+ const causeCode = typeof parsed.reason === "string" &&
508
+ ["focus_failed", "press_unavailable", "coordinate_click_failed", "ax_action_exception"].includes(parsed.reason)
509
+ ? parsed.reason
510
+ : "ax_action_exception";
511
+ return this.failed(request.operationId, "action_failed", "Accessibility action failed", causeCode);
512
+ }
513
+ const observation = request.reobserveAfterAction === false ? undefined : this.observe(request.sessionId);
514
+ const verification = this.verifyPostconditions(postconditions, observation);
515
+ if (postconditions.length > 0 && verification.status === "unknown")
516
+ return {
517
+ version: 1,
518
+ operationId: request.operationId,
519
+ status: "failed",
520
+ summary: "Desktop action completed but could not be re-observed",
521
+ verification,
522
+ error: error("verification_unavailable", "The requested desktop state could not be re-observed", "reobserve-failed"),
523
+ };
524
+ if (verification.status === "failed")
525
+ return {
526
+ version: 1,
527
+ operationId: request.operationId,
528
+ status: "failed",
529
+ summary: "Desktop action completed but a postcondition failed",
530
+ ...(observation !== undefined && "observationId" in observation ? { observation } : {}),
531
+ verification,
532
+ error: error("postcondition_failed", "The requested desktop state was not observed", "verification-failed"),
533
+ };
534
+ return {
535
+ version: 1,
536
+ operationId: request.operationId,
537
+ status: "succeeded",
538
+ summary: "Desktop action completed through macOS Accessibility",
539
+ ...(observation !== undefined && "observationId" in observation ? { observation } : {}),
540
+ verification,
541
+ };
542
+ }
543
+ stop(sessionId) {
544
+ const session = this.sessions.get(sessionId);
545
+ if (session === undefined)
546
+ return this.failed("stop", "session_not_found", "Session does not exist");
547
+ const stopped = detachedFreeze({ ...session, status: "stopped" });
548
+ this.sessions.set(sessionId, stopped);
549
+ this.invalidateObservation(sessionId);
550
+ return stopped;
551
+ }
552
+ actionPayload(request) {
553
+ const target = request.action.target;
554
+ if (target === undefined)
555
+ return undefined;
556
+ const resolvedTarget = target.kind === "coordinates"
557
+ ? { coordinates: { x: target.x, y: target.y } }
558
+ : (this.resolveTarget(request.sessionId, target) ?? undefined);
559
+ if (resolvedTarget === undefined)
560
+ return undefined;
561
+ if (request.action.kind === "key") {
562
+ const key = parseMacKey(request.action.key ?? "");
563
+ if (key === undefined)
564
+ return undefined;
565
+ return { kind: request.action.kind, target: resolvedTarget, key };
566
+ }
567
+ if (request.action.kind === "scroll") {
568
+ const scroll = parseMacScroll(request.action.text ?? "");
569
+ if (scroll === undefined)
570
+ return undefined;
571
+ return { kind: request.action.kind, target: resolvedTarget, scroll };
572
+ }
573
+ return {
574
+ kind: request.action.kind,
575
+ target: resolvedTarget,
576
+ ...(request.action.text === undefined ? {} : { text: request.action.text }),
577
+ };
578
+ }
579
+ run(script, payload) {
580
+ if (this.platform !== "darwin")
581
+ return undefined;
582
+ const env = {};
583
+ for (const key of ["PATH", "HOME", "TMPDIR", "USER", "LOGNAME"]) {
584
+ const value = process.env[key];
585
+ if (value !== undefined)
586
+ env[key] = value;
587
+ }
588
+ if (payload !== undefined)
589
+ env.AGENT_FORGE_MACOS_AX_INPUT = JSON.stringify(payload);
590
+ const result = this.spawn(this.command, ["-l", "JavaScript", "-e", script], {
591
+ encoding: "utf8",
592
+ timeout: this.timeoutMs,
593
+ windowsHide: true,
594
+ env,
595
+ });
596
+ if (result.error !== undefined || result.status !== 0)
597
+ return { stdout: result.stdout, stderr: result.stderr ?? "", failed: true };
598
+ return { stdout: result.stdout, stderr: result.stderr ?? "", failed: false };
599
+ }
600
+ toObservation(session, raw) {
601
+ this.invalidateObservation(session.sessionId);
602
+ const observationId = `desktop-macos-observation-${this.nextObservationId++}`;
603
+ const targetKeys = new Set();
604
+ this.sessionTargetKeys.set(session.sessionId, targetKeys);
605
+ const windows = [];
606
+ for (const item of raw.windows ?? []) {
607
+ if (typeof item.id !== "string" || item.id === "")
608
+ continue;
609
+ const windowId = item.id;
610
+ windows.push({
611
+ version: 1,
612
+ windowId,
613
+ appId: `process:${Number(item.processId ?? 0)}`,
614
+ title: item.name ?? "",
615
+ bounds: rectangle(item),
616
+ });
617
+ }
618
+ const visibleWindows = session.applicationId === undefined
619
+ ? windows
620
+ : windows.filter((window) => window.appId === session.applicationId);
621
+ for (const window of visibleWindows) {
622
+ const key = this.targetKey(session.sessionId, "window", window.windowId);
623
+ this.targets.set(key, { sessionId: session.sessionId, observationId, windowId: window.windowId });
624
+ targetKeys.add(key);
625
+ }
626
+ const controls = [];
627
+ for (const item of raw.controls ?? []) {
628
+ if (typeof item.windowId !== "string" || !visibleWindows.some((window) => window.windowId === item.windowId))
629
+ continue;
630
+ const controlId = item.controlId ?? "";
631
+ const key = this.targetKey(session.sessionId, "control", controlId);
632
+ this.targets.set(key, {
633
+ sessionId: session.sessionId,
634
+ observationId,
635
+ windowId: item.windowId,
636
+ controlId,
637
+ });
638
+ targetKeys.add(key);
639
+ controls.push({
640
+ version: 1,
641
+ controlId,
642
+ windowId: item.windowId,
643
+ role: item.role ?? "",
644
+ name: item.name ?? "",
645
+ bounds: rectangle(item),
646
+ enabled: item.enabled !== false,
647
+ ...(typeof item.value === "string" && item.value.length > 0 ? { value: item.value } : {}),
648
+ });
649
+ }
650
+ const focusedControlId = typeof raw.focusedControlId === "string" &&
651
+ controls.some((control) => control.controlId === raw.focusedControlId)
652
+ ? raw.focusedControlId
653
+ : undefined;
654
+ const observation = detachedFreeze({
655
+ version: 1,
656
+ sessionId: session.sessionId,
657
+ observationId,
658
+ source: "macos-ax",
659
+ windows: visibleWindows,
660
+ controls,
661
+ ...(focusedControlId !== undefined ? { focusedControlId } : {}),
662
+ });
663
+ this.latestObservationIds.set(session.sessionId, observationId);
664
+ this.observedSessions.add(session.sessionId);
665
+ return observation;
666
+ }
667
+ resolveTarget(sessionId, target) {
668
+ const identity = target.kind === "control" ? target.controlId : target.windowId;
669
+ const record = this.targets.get(this.targetKey(sessionId, target.kind, identity));
670
+ if (record === undefined ||
671
+ record.sessionId !== sessionId ||
672
+ record.observationId !== this.latestObservationIds.get(sessionId))
673
+ return undefined;
674
+ return {
675
+ windowId: record.windowId,
676
+ ...(record.controlId === undefined ? {} : { controlId: record.controlId }),
677
+ };
678
+ }
679
+ targetKey(sessionId, kind, identity) {
680
+ return `${sessionId}:${kind}:${identity}`;
681
+ }
682
+ invalidateObservation(sessionId) {
683
+ for (const key of this.sessionTargetKeys.get(sessionId) ?? [])
684
+ this.targets.delete(key);
685
+ this.sessionTargetKeys.delete(sessionId);
686
+ this.latestObservationIds.delete(sessionId);
687
+ }
688
+ verifyPostconditions(postconditions, observation) {
689
+ if (postconditions.length === 0)
690
+ return { status: "passed", checks: [] };
691
+ if (observation === undefined || !("observationId" in observation))
692
+ return { status: "unknown", checks: ["observation-unavailable"] };
693
+ const checks = [];
694
+ for (const condition of postconditions) {
695
+ const passed = this.checkPostcondition(condition, observation);
696
+ checks.push(`${condition.kind}:${passed ? "passed" : "failed"}`);
697
+ if (!passed)
698
+ return { status: "failed", checks };
699
+ }
700
+ return { status: "passed", checks };
701
+ }
702
+ checkPostcondition(condition, observation) {
703
+ if (condition.kind === "window-exists")
704
+ return observation.windows.some((window) => window.windowId === condition.windowId);
705
+ if (condition.kind === "control-exists")
706
+ return observation.controls.some((control) => control.controlId === condition.controlId);
707
+ if (condition.kind === "focused")
708
+ return observation.focusedControlId === condition.controlId;
709
+ const target = condition.target;
710
+ if (target.kind === "window") {
711
+ const window = observation.windows.find((item) => item.windowId === target.windowId);
712
+ if (window === undefined || condition.property !== "name")
713
+ return false;
714
+ return window.title === condition.expected;
715
+ }
716
+ // A coordinate target identifies no observed control, so it can never
717
+ // satisfy a property-equals postcondition.
718
+ if (target.kind === "coordinates")
719
+ return false;
720
+ const control = observation.controls.find((item) => {
721
+ if (item.controlId !== target.controlId)
722
+ return false;
723
+ return target.windowId === undefined || item.windowId === target.windowId;
724
+ });
725
+ if (control === undefined)
726
+ return false;
727
+ if (condition.property === "enabled")
728
+ return control.enabled === condition.expected;
729
+ if (condition.property === "name")
730
+ return control.name === condition.expected;
731
+ return control.value === condition.expected;
732
+ }
733
+ failed(operationId, code, message, causeCode) {
734
+ return { version: 1, operationId, status: "failed", summary: message, error: error(code, message, causeCode) };
735
+ }
736
+ cancelled(operationId, message) {
737
+ return {
738
+ version: 1,
739
+ operationId,
740
+ status: "cancelled",
741
+ summary: message,
742
+ error: error("cancelled", message, "stopped"),
743
+ };
744
+ }
745
+ }
746
+ const payloadErrorCode = (action) => {
747
+ if (action.kind === "key")
748
+ return "key_unsupported";
749
+ if (action.kind === "scroll")
750
+ return "direction_invalid";
751
+ return "target_not_found";
752
+ };
753
+ const payloadErrorMessage = (action) => {
754
+ if (action.kind === "key")
755
+ return "Key sequence does not match the supported macOS key grammar";
756
+ if (action.kind === "scroll")
757
+ return "Scroll direction must be up or down with optional :lines";
758
+ return "Target identity was not observed by macOS Accessibility";
759
+ };
760
+ /**
761
+ * The production input facade intentionally exposes no replay method. The
762
+ * driver remains responsible for using observed Accessibility identities
763
+ * (or explicit click coordinates) rather than global keyboard injection.
764
+ */
765
+ export class DesktopMacosInputAdapter {
766
+ adapterId = "desktop-macos-ax";
767
+ adapterVersion = "1.0.0";
768
+ driver;
769
+ constructor(driver = new MacosAccessibilityDriver()) {
770
+ this.driver = driver;
771
+ }
772
+ probe() {
773
+ return this.driver.probe();
774
+ }
775
+ openSession(request) {
776
+ return this.driver.openSession(request);
777
+ }
778
+ observe(sessionId) {
779
+ return this.driver.observe(sessionId);
780
+ }
781
+ reobserve(sessionId) {
782
+ return this.driver.observe(sessionId);
783
+ }
784
+ execute(request) {
785
+ if (requiresDesktopUserConfirmation(request.action.kind)) {
786
+ return {
787
+ version: 1,
788
+ operationId: request.operationId,
789
+ status: "needs_user",
790
+ summary: `User confirmation required for ${request.action.kind}`,
791
+ error: {
792
+ version: 1,
793
+ code: "needs_user",
794
+ message: `The ${request.action.kind} action is paused for the user`,
795
+ causeCode: request.action.kind,
796
+ },
797
+ };
798
+ }
799
+ return this.driver.execute(request);
800
+ }
801
+ stop(sessionId) {
802
+ return this.driver.stop(sessionId);
803
+ }
804
+ }
805
+ //# sourceMappingURL=macos-ax-adapter.js.map