use-computer 0.0.1__py3-none-any.whl

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.
mmini/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ from mmini.client import AsyncMmini, Mmini, RunStatus
2
+ from mmini.errors import MminiError, PlatformNotSupportedError
3
+ from mmini.models import (
4
+ ActionResult,
5
+ ActResult,
6
+ CursorPosition,
7
+ DisplayInfo,
8
+ ExecResult,
9
+ RecordingInfo,
10
+ )
11
+ from mmini.parsers import Action, parse_pyautogui, parse_xdotool
12
+ from mmini.sandbox import (
13
+ AsyncIOSSandbox,
14
+ AsyncMacOSSandbox,
15
+ AsyncSandbox,
16
+ IOSSandbox,
17
+ MacOSSandbox,
18
+ Sandbox,
19
+ SandboxType,
20
+ )
21
+ from mmini.tasks import Task, TasksClient, TaskSummary
22
+
23
+ __all__ = [
24
+ "Action",
25
+ "ActionResult",
26
+ "ActResult",
27
+ "AsyncIOSSandbox",
28
+ "AsyncMacOSSandbox",
29
+ "AsyncMmini",
30
+ "AsyncSandbox",
31
+ "CursorPosition",
32
+ "DisplayInfo",
33
+ "ExecResult",
34
+ "IOSSandbox",
35
+ "MacOSSandbox",
36
+ "Mmini",
37
+ "MminiError",
38
+ "PlatformNotSupportedError",
39
+ "RecordingInfo",
40
+ "RunStatus",
41
+ "Sandbox",
42
+ "SandboxType",
43
+ "Task",
44
+ "TaskSummary",
45
+ "TasksClient",
46
+ "parse_pyautogui",
47
+ "parse_xdotool",
48
+ ]
mmini/ax_transpile.py ADDED
@@ -0,0 +1,504 @@
1
+ """
2
+ Rewrite `osascript -e 'tell application "System Events" ...'` patterns into
3
+ shell calls that invoke `/usr/local/bin/ax_helper.py` via cua-server's
4
+ `run_command` endpoint. The helper (baked into base-macos) does the actual
5
+ Accessibility walk with `AXUIElementSetMessagingTimeout` on every element,
6
+ so a wedged target can't alarm-kill the verifier the way inline PyObjC
7
+ could.
8
+
9
+ Why route through cua-server's /cmd run_command? Because that responsibility
10
+ chain (launchd → cua-server → bash → python3.12) is what makes the system
11
+ TCC Accessibility grant on python3.12 actually apply. SSH-backed exec puts
12
+ `sshd-keygen-wrapper` in the chain and TCC denies AX with -25211.
13
+
14
+ Why not inline PyObjC? Previously we emitted base64-wrapped python snippets
15
+ at 5 call sites with no `SetMessagingTimeout`. 441 trials alarm-killed
16
+ because a wedged AX call couldn't be preempted from outside the framework.
17
+ Moving the code into a single baked helper means one place to harden.
18
+
19
+ Coverage
20
+ --------
21
+ Six AppleScript shapes (~100% of the macOSWorld AX-blocked verifier
22
+ patterns):
23
+
24
+ 1. `attribute "AX..." of <PATH>` — attr read, path walked from frontmost
25
+ or a named process.
26
+ 2. `attributes of <PATH>` — dump all attribute name=value pairs.
27
+ 3. `tell process "X" to get value of <ELEMENT> of <PATH>` — AXValue of a
28
+ leaf inside a process.
29
+ 4. `get name of first process whose frontmost is true`
30
+ 5. `name of every UI element of list 1 of application process "Dock"`
31
+ 6. `keystroke "X" [using {modifier list}]` — synthesize CGEvent.
32
+
33
+ Lines that don't match any recognized shape are passed through unchanged.
34
+
35
+ What this does NOT handle
36
+ -------------------------
37
+ * AppleScript with conditionals or repeats
38
+ * `set value of` (we only do reads)
39
+ * Multi-character `keystroke` strings (only single chars)
40
+ None of the classified macOSWorld verifiers need these.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import base64
46
+ import json
47
+ import re
48
+ import shlex
49
+ from typing import cast
50
+
51
+ # ---- Pattern parsing ------------------------------------------------------
52
+
53
+ # AppleScript element kind → AX role. Must stay in sync with
54
+ # scripts/images/ax_helper.py::_role_for_kind().
55
+ _ROLE_MAP: dict[str, str | None] = {
56
+ "window": "AXWindow",
57
+ "group": "AXGroup",
58
+ "scroll area": "AXScrollArea",
59
+ "toolbar": "AXToolbar",
60
+ "menu button": "AXMenuButton",
61
+ "button": "AXButton",
62
+ "pop up button": "AXPopUpButton",
63
+ "text field": "AXTextField",
64
+ "text area": "AXTextArea",
65
+ "checkbox": "AXCheckBox",
66
+ "radio button": "AXRadioButton",
67
+ "list": "AXList",
68
+ "row": "AXRow",
69
+ "tab": "AXTab",
70
+ "menu item": "AXMenuItem",
71
+ "static text": "AXStaticText",
72
+ "splitter group": "AXSplitGroup",
73
+ "ui element": None,
74
+ }
75
+
76
+ # Sort kinds by length descending so "menu button" wins over "button".
77
+ _KINDS_SORTED = cast(list[str], sorted(_ROLE_MAP.keys(), key=len, reverse=True))
78
+ _KIND_ALT = "|".join(re.escape(k) for k in _KINDS_SORTED)
79
+ _PATH_SEG_RE = re.compile(
80
+ r"(" + _KIND_ALT + r')\s+(?:"([^"]+)"|(\d+))',
81
+ re.IGNORECASE,
82
+ )
83
+
84
+ # Match a full osascript command: one or more `-e '...'` arguments.
85
+ # Handles both bare single quotes and the bash-escape `'\''...'\'']` form
86
+ # that test.sh files use to embed osascript inside `bash -c '...'`.
87
+ #
88
+ # Group 1 captures the FIRST script body. If there are additional `-e '...'`
89
+ # arguments (group 2 present), `transpile()` combines all bodies and uses the
90
+ # fallback timeout wrapper (specialized converters only match single-script patterns).
91
+ _OSASCRIPT_RE = re.compile(
92
+ r"""osascript(?:\s+-e\s+(?:'\\''|')(.*?)(?:'\\''|'))+""",
93
+ re.DOTALL,
94
+ )
95
+
96
+ # Used to extract ALL -e '...' bodies from a matched osascript command.
97
+ _OSASCRIPT_E_RE = re.compile(
98
+ r"""-e\s+(?:'\\''|')(.*?)(?:'\\''|')""",
99
+ re.DOTALL,
100
+ )
101
+
102
+
103
+ def _parse_path(path_str: str) -> tuple[list[dict], dict] | None:
104
+ """Parse `... of (first application process whose frontmost is true)` or
105
+ `... of process "X"` or just `... of window N`.
106
+
107
+ Returns (segments_root_first, root_spec) where root_spec is
108
+ {"root": "frontmost"} or {"root": "process", "name": "X"}. Segments are
109
+ ordered root → leaf as dicts with keys {"kind", "name"?, "index"?}.
110
+
111
+ Returns None if we can't parse the path.
112
+ """
113
+ s = path_str.strip()
114
+
115
+ m = re.search(
116
+ r"\(?\s*first\s+application\s+process\s+whose\s+frontmost\s+is\s+true\s*\)?\s*$",
117
+ s,
118
+ re.IGNORECASE,
119
+ )
120
+ if m:
121
+ root: dict = {"root": "frontmost"}
122
+ s = s[: m.start()].rstrip()
123
+ s = re.sub(r"\s+of\s*$", "", s, flags=re.IGNORECASE)
124
+ else:
125
+ m = re.search(
126
+ r'(?:application\s+)?process\s+"([^"]+)"\s*$',
127
+ s,
128
+ re.IGNORECASE,
129
+ )
130
+ if not m:
131
+ return None
132
+ root = {"root": "process", "name": m.group(1)}
133
+ s = s[: m.start()].rstrip()
134
+ s = re.sub(r"\s+of\s*$", "", s, flags=re.IGNORECASE)
135
+
136
+ # AppleScript writes segments leaf-first, separated by " of ". Reverse
137
+ # into root→leaf.
138
+ segs_leaf_first: list[dict] = []
139
+ if s:
140
+ for chunk in re.split(r"\s+of\s+", s):
141
+ chunk = chunk.strip()
142
+ if not chunk:
143
+ continue
144
+ mm = _PATH_SEG_RE.match(chunk)
145
+ if not mm:
146
+ return None
147
+ kind = mm.group(1).lower()
148
+ if kind not in _ROLE_MAP:
149
+ return None
150
+ seg: dict = {"kind": kind}
151
+ if mm.group(2) is not None:
152
+ seg["name"] = mm.group(2)
153
+ elif mm.group(3) is not None:
154
+ seg["index"] = int(mm.group(3))
155
+ segs_leaf_first.append(seg)
156
+
157
+ return list(reversed(segs_leaf_first)), root
158
+
159
+
160
+ def _parse_leaf_path(path_str: str) -> list[dict] | None:
161
+ """Parse a path inside `tell process "X" to get value of <PATH>` — no
162
+ root suffix, just a leaf-first chain of segments. Returns root→leaf."""
163
+ segs_leaf_first: list[dict] = []
164
+ for chunk in re.split(r"\s+of\s+", path_str.strip()):
165
+ chunk = chunk.strip()
166
+ if not chunk:
167
+ continue
168
+ mm = _PATH_SEG_RE.match(chunk)
169
+ if not mm:
170
+ return None
171
+ kind = mm.group(1).lower()
172
+ if kind not in _ROLE_MAP:
173
+ return None
174
+ seg: dict = {"kind": kind}
175
+ if mm.group(2) is not None:
176
+ seg["name"] = mm.group(2)
177
+ elif mm.group(3) is not None:
178
+ seg["index"] = int(mm.group(3))
179
+ segs_leaf_first.append(seg)
180
+ return list(reversed(segs_leaf_first)) if segs_leaf_first else None
181
+
182
+
183
+ # ---- Emission -------------------------------------------------------------
184
+
185
+ HELPER = "/usr/local/bin/ax_helper.py"
186
+
187
+
188
+ def _emit_helper_call(op: str, *args: str) -> str:
189
+ """Emit a shell snippet that POSTs to cua-server's /cmd run_command,
190
+ invokes ax_helper.py, and prints stdout.
191
+
192
+ The whole emission is base64-wrapped and piped through `base64 -d |
193
+ bash` so the replacement contains ZERO single/double quotes in its
194
+ payload surface. Verifier test.sh files wrap osascript calls inside
195
+ `bash -c '...'` — any raw single quote in our replacement would break
196
+ the outer quoting. Base64 alphabet (A-Z/a-z/0-9/+//=) never collides.
197
+ """
198
+ argv = [HELPER, op, *args]
199
+ cmd = " ".join(shlex.quote(a) for a in argv)
200
+ body = json.dumps({"command": "run_command", "params": {"command": cmd}})
201
+ body_b64 = base64.b64encode(body.encode()).decode()
202
+
203
+ # Payload executed by bash after decoding.
204
+ #
205
+ # Critical: do NOT use `python3 <<HEREDOC` to pass the parser source —
206
+ # heredoc input hijacks python's stdin, so the curl pipe goes nowhere
207
+ # and `sys.stdin.read()` returns empty (or the heredoc bytes). Instead
208
+ # we base64-decode the parser at runtime into `python3 -c "$P"`, which
209
+ # leaves python's stdin free for the curl pipe.
210
+ parser_py = (
211
+ "import sys, json\n"
212
+ "raw = sys.stdin.read()\n"
213
+ "for line in raw.splitlines():\n"
214
+ " line = line.strip()\n"
215
+ " if line.startswith('data:'):\n"
216
+ " line = line[5:].strip()\n"
217
+ " if not line:\n"
218
+ " continue\n"
219
+ " try:\n"
220
+ " d = json.loads(line)\n"
221
+ " except Exception:\n"
222
+ " continue\n"
223
+ " print(d.get('stdout', '').rstrip())\n"
224
+ " break\n"
225
+ )
226
+ parser_b64 = base64.b64encode(parser_py.encode()).decode()
227
+ payload = (
228
+ f"B=$(echo {body_b64} | base64 -d); "
229
+ f"P=$(echo {parser_b64} | base64 -d); "
230
+ f"curl -s -m 5 -X POST http://127.0.0.1:8000/cmd "
231
+ f"-H Content-Type:application/json "
232
+ f'--data-raw "$B" 2>/dev/null '
233
+ f'| python3 -c "$P"'
234
+ )
235
+ payload_b64 = base64.b64encode(payload.encode()).decode()
236
+ return f"echo {payload_b64} | base64 -d | bash"
237
+
238
+
239
+ # ---- Per-shape converters -------------------------------------------------
240
+
241
+
242
+ def _try_attr_of_path(script: str) -> str | None:
243
+ """get value of attribute "AX..." of <PATH>"""
244
+ m = re.match(
245
+ r'\s*tell\s+application\s+"System Events"\s+to\s+get\s+value\s+of\s+'
246
+ r'attribute\s+"(AX[A-Za-z]+)"\s+of\s+(.+)\s*$',
247
+ script,
248
+ re.IGNORECASE | re.DOTALL,
249
+ )
250
+ if not m:
251
+ return None
252
+ attr = m.group(1)
253
+ parsed = _parse_path(m.group(2))
254
+ if parsed is None:
255
+ return None
256
+ segs, root = parsed
257
+ path_json = json.dumps([root, *segs])
258
+ return _emit_helper_call("attr", attr, path_json)
259
+
260
+
261
+ def _try_attributes_of_path(script: str) -> str | None:
262
+ """get value of attributes of <PATH>"""
263
+ m = re.match(
264
+ r'\s*tell\s+application\s+"System Events"\s+to\s+get\s+value\s+of\s+'
265
+ r"attributes\s+of\s+(.+)\s*$",
266
+ script,
267
+ re.IGNORECASE | re.DOTALL,
268
+ )
269
+ if not m:
270
+ return None
271
+ parsed = _parse_path(m.group(1))
272
+ if parsed is None:
273
+ return None
274
+ segs, root = parsed
275
+ path_json = json.dumps([root, *segs])
276
+ return _emit_helper_call("attrs", path_json)
277
+
278
+
279
+ def _try_value_of_named_element(script: str) -> str | None:
280
+ """tell process "X" to get value of <LEAF> of <PATH>"""
281
+ m = re.match(
282
+ r'\s*tell\s+application\s+"System Events"\s+to\s+tell\s+process\s+'
283
+ r'"([^"]+)"\s+to\s+get\s+value\s+of\s+(.+)\s*$',
284
+ script,
285
+ re.IGNORECASE | re.DOTALL,
286
+ )
287
+ if not m:
288
+ return None
289
+ process = m.group(1)
290
+ segs = _parse_leaf_path(m.group(2))
291
+ if segs is None:
292
+ return None
293
+ return _emit_helper_call("value", process, json.dumps(segs))
294
+
295
+
296
+ def _try_front_process_name(script: str) -> str | None:
297
+ """get name of first process whose frontmost is true"""
298
+ if not re.match(
299
+ r'\s*tell\s+application\s+"System Events"\s+to\s+get\s+name\s+of\s+'
300
+ r"first\s+process\s+whose\s+frontmost\s+is\s+true\s*$",
301
+ script,
302
+ re.IGNORECASE,
303
+ ):
304
+ return None
305
+ return _emit_helper_call("frontmost_name")
306
+
307
+
308
+ def _try_dock_items(script: str) -> str | None:
309
+ """set VAR to name of every UI element of list 1 of application process "Dock" """
310
+ if not re.match(
311
+ r'\s*tell\s+application\s+"System Events"\s+to\s+set\s+\w+\s+to\s+'
312
+ r"name\s+of\s+every\s+UI\s+element\s+of\s+list\s+1\s+of\s+application\s+"
313
+ r'process\s+"Dock"\s*$',
314
+ script,
315
+ re.IGNORECASE,
316
+ ):
317
+ return None
318
+ return _emit_helper_call("dock_items")
319
+
320
+
321
+ def _try_keystroke(script: str) -> str | None:
322
+ """keystroke "X" [using {modifier list}]. Single-character only."""
323
+ m = re.match(
324
+ r'\s*tell\s+application\s+"System Events"\s+to\s+keystroke\s+'
325
+ r'"([^"]+)"(?:\s+using\s+\{([^}]+)\})?\s*$',
326
+ script,
327
+ re.IGNORECASE,
328
+ )
329
+ if not m:
330
+ return None
331
+ key = m.group(1)
332
+ if len(key) != 1:
333
+ return None
334
+ mods_raw = (m.group(2) or "").lower()
335
+ mods: list[str] = []
336
+ if "command down" in mods_raw or "cmd down" in mods_raw:
337
+ mods.append("cmd")
338
+ if "shift down" in mods_raw:
339
+ mods.append("shift")
340
+ if "option down" in mods_raw or "alt down" in mods_raw:
341
+ mods.append("alt")
342
+ if "control down" in mods_raw or "ctrl down" in mods_raw:
343
+ mods.append("ctrl")
344
+ return _emit_helper_call("keystroke", key, *mods)
345
+
346
+
347
+ # Default timeout applied to every osascript body that isn't otherwise
348
+ # specialised. 5s is well below the outer harbor alarm (28s) and well above
349
+ # the ~1s a responsive Apple Events target needs.
350
+ DEFAULT_OSASCRIPT_TIMEOUT_S = 5
351
+
352
+ # Timeout used when transpiling pre_command lines (seeding VM state). Notes and
353
+ # iWork apps cold-start in 20-30s on a fresh VM; we need state to actually be
354
+ # written, so we use a 45s budget. Fallback emissions route through exec (SSH),
355
+ # not exec_ax, so there is no cua-server 30s hard limit — only the curl
356
+ # max-time (120s) on the client side applies.
357
+ PRE_COMMAND_OSASCRIPT_TIMEOUT_S = 45
358
+
359
+
360
+ def _emit_with_timeout(applescript_body: str, timeout_s: int = DEFAULT_OSASCRIPT_TIMEOUT_S) -> str:
361
+ """Wrap an arbitrary AppleScript body in `with timeout of N seconds`.
362
+
363
+ Used as the fallback for `osascript -e '<body>'` calls that none of the
364
+ specialised converters matched — the many `tell application "Notes"` /
365
+ "Keynote" / "Contacts" / "Reminders" patterns that hang on fresh VMs
366
+ because the target app waits on iCloud, an unopened document, or a
367
+ missing UI. Live-verified: a wrapped query returns
368
+ `-1712 (AppleEvent timed out)` at exactly N seconds instead of hanging
369
+ past the outer alarm. Caller's `grep -qi 'true'` then falls through to
370
+ score 0 cleanly rather than alarm-killing the trial.
371
+
372
+ Emission is base64-wrapped so it's safe inside any bash-c wrapper; the
373
+ body goes through a single-quoted heredoc ('__AS_EOF__') so embedded
374
+ quotes in the AppleScript pass through verbatim without further escaping.
375
+ """
376
+ script = (
377
+ f"osascript <<'__AS_EOF__'\n"
378
+ f"with timeout of {timeout_s} seconds\n"
379
+ f"{applescript_body}\n"
380
+ f"end timeout\n"
381
+ f"__AS_EOF__\n"
382
+ )
383
+ script_b64 = base64.b64encode(script.encode()).decode()
384
+ return f"echo {script_b64} | base64 -d | bash"
385
+
386
+
387
+ _CONVERTERS = (
388
+ _try_attr_of_path,
389
+ _try_attributes_of_path,
390
+ _try_value_of_named_element,
391
+ _try_front_process_name,
392
+ _try_dock_items,
393
+ _try_keystroke,
394
+ )
395
+
396
+
397
+ def _applescript_to_shell(
398
+ script: str, fallback_timeout_s: int = DEFAULT_OSASCRIPT_TIMEOUT_S
399
+ ) -> str | None:
400
+ """Try each specialised converter; fall back to timeout-wrapped osascript.
401
+
402
+ `fallback_timeout_s` controls the `with timeout of N seconds` wrapper used
403
+ when no specialised converter matches. Callers transpiling pre_command lines
404
+ should pass PRE_COMMAND_OSASCRIPT_TIMEOUT_S (45s) so state-seeding calls
405
+ don't get killed before the app responds. Verifier calls use the default 5s
406
+ (fail fast = score 0, no alarm-kill).
407
+ """
408
+ for fn in _CONVERTERS:
409
+ out = fn(script)
410
+ if out is not None:
411
+ return out
412
+ # Terminal fallback: wrap with timeout so the call never hangs forever.
413
+ return _emit_with_timeout(script.strip(), timeout_s=fallback_timeout_s)
414
+
415
+
416
+ # ---- Public API -----------------------------------------------------------
417
+
418
+
419
+ def transpile(text: str, fallback_timeout_s: int = DEFAULT_OSASCRIPT_TIMEOUT_S) -> tuple[str, int]:
420
+ """Rewrite osascript+System Events lines in `text` to ax_helper calls.
421
+
422
+ Returns (rewritten_text, num_substitutions). Lines that don't match a
423
+ known pattern are left untouched.
424
+
425
+ `fallback_timeout_s` sets the `with timeout of N seconds` applied to
426
+ osascript bodies that hit the fallback converter. Pass
427
+ PRE_COMMAND_OSASCRIPT_TIMEOUT_S when transpiling pre_command lines.
428
+ """
429
+ count = 0
430
+
431
+ def _replace(m: re.Match) -> str:
432
+ nonlocal count
433
+ # Extract ALL -e '...' bodies from the matched osascript command.
434
+ bodies = [body.replace(r"'\''", "'") for body in _OSASCRIPT_E_RE.findall(m.group(0))]
435
+ if not bodies:
436
+ return m.group(0)
437
+
438
+ if len(bodies) == 1:
439
+ # Single -e: try specialized converters, fall back to timeout wrap.
440
+ replacement = _applescript_to_shell(bodies[0], fallback_timeout_s=fallback_timeout_s)
441
+ else:
442
+ # Multiple -e scripts: combine as newline-separated AppleScript and
443
+ # always use the timeout wrapper (specialized converters expect a single
444
+ # pattern and won't match a compound script).
445
+ combined = "\n".join(bodies)
446
+ replacement = _emit_with_timeout(combined, timeout_s=fallback_timeout_s)
447
+
448
+ if replacement is None:
449
+ return m.group(0)
450
+ count += 1
451
+ return replacement
452
+
453
+ return _OSASCRIPT_RE.sub(_replace, text), count
454
+
455
+
456
+ def needs_rewrite(text: str) -> bool:
457
+ """Any `osascript -e` invocation gets rewritten now (the fallback
458
+ converter wraps everything with `with timeout of N seconds`)."""
459
+ return bool(_OSASCRIPT_RE.search(text))
460
+
461
+
462
+ _B64_PAYLOAD_RE = re.compile(r"echo ([A-Za-z0-9+/=]+) \| base64 -d \| bash")
463
+
464
+
465
+ def patch_curl_timeouts(text: str) -> tuple[str, int]:
466
+ """Add -m 5 to `curl -s -X POST` calls that are buried inside base64-encoded
467
+ payloads (the ax_helper emission style). Used by mmini._run_setup to harden
468
+ already-baked test.sh files whose payloads were generated before the -m flag
469
+ was added to _emit_helper_call.
470
+
471
+ Returns (patched_text, num_patched).
472
+ """
473
+ count = 0
474
+
475
+ def _repatch(m: re.Match) -> str:
476
+ nonlocal count
477
+ try:
478
+ decoded = base64.b64decode(m.group(1)).decode()
479
+ except Exception:
480
+ return m.group(0)
481
+ if "curl -s -X POST" not in decoded:
482
+ return m.group(0)
483
+ patched = decoded.replace("curl -s -X POST", "curl -s -m 5 -X POST")
484
+ if patched == decoded:
485
+ return m.group(0)
486
+ count += 1
487
+ new_b64 = base64.b64encode(patched.encode()).decode()
488
+ return f"echo {new_b64} | base64 -d | bash"
489
+
490
+ return _B64_PAYLOAD_RE.sub(_repatch, text), count
491
+
492
+
493
+ def needs_exec_ax(text: str) -> bool:
494
+ """Return True if the (possibly transpiled) text contains ax_helper calls.
495
+
496
+ ax_helper calls (emitted by the specialised converters) use cua-server's
497
+ run_command route and MUST go through exec_ax (launchd→cua-server chain
498
+ provides the Accessibility TCC grant).
499
+
500
+ Fallback-transpiled lines (osascript heredocs wrapped with `with timeout`)
501
+ call osascript directly from bash — they do NOT need exec_ax and can run
502
+ through the regular /exec (SSH/lume) endpoint, which has no 30s hard limit.
503
+ """
504
+ return HELPER in text