Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 166 additions & 24 deletions crates/tui/plugins/computer-use/src/backends/darwin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,19 @@ export function create({ exec }) {
fs.writeFileSync(file, script);
// Payload travels as a plain argv string: osascript is spawned without a
// shell, so JSON content can never become script syntax.
// Input and accessibility scripts need the Accessibility grant. When the
// last probe saw it denied, refuse here with the remedy instead of
// letting osascript hang on a TCC prompt nobody can answer (#5917).
if (state.tcc?.accessibility === false && /CGEventPost|System Events/.test(script)) {
throw new ExecError(`accessibility permission is not granted to ${await grantTarget()}: ${TCC_FIX.accessibility}`);
}
const r = await runL("osascript", ["-l", "JavaScript", file, JSON.stringify(payload)], { timeoutMs });
if (r.timedOut) throw new ExecError("osascript timed out", r);
if (r.timedOut) {
const hint = state.tcc?.accessibility === true
? ""
: ` (if macOS is showing a permission prompt, grant Accessibility to ${await grantTarget()}: ${TCC_FIX.accessibility})`;
throw new ExecError(`osascript timed out${hint}`, r);
}
if (r.code !== 0) {
const msg = (r.stderr || r.stdout).trim().split("\n")[0] || "osascript failed";
throw new ExecError(/(not allowed assistive|assistive access|250)/i.test(r.stderr || "") || /(-25211|-1719|not allowed)/i.test(msg)
Expand Down Expand Up @@ -223,11 +234,16 @@ export function create({ exec }) {
}

// ---------- raw input via CGEvent ----------
async function cg(script, timeoutMs = 10_000) {
// Every caller hands its values as `payload`; the script reads them as `P`.
// The old `(script, timeoutMs)` shape silently swallowed the payload (so
// `P.code`, `P.x`, `P.text` were undefined) and coerced the object to a
// zero timeout, which is why every CGEvent input on macOS reported
// "osascript timed out" instantly (#5917).
async function cg(script, payload = {}, timeoutMs = 10_000) {
return jxa(`ObjC.import('CoreGraphics');
function run(argv){ var P = JSON.parse(argv[0]);
${script}
}`, {}, timeoutMs);
}`, payload, timeoutMs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit the correct CoreGraphics drag event

Forwarding the payload here makes left_click_drag execute for the first time, but that path supplies MOUSE.left.dragged, currently numeric event type 7. CoreGraphics defines 6 as kCGEventLeftMouseDragged and 7 as kCGEventRightMouseDragged, so a left-button drag now posts right-drag events between its left-button down and up and can fail or invoke the wrong interaction; correct the event constants before enabling this path.

Useful? React with 👍 / 👎.

}

async function postMouseEvent(type, x, y, button, clickState) {
Expand Down Expand Up @@ -326,7 +342,16 @@ export function create({ exec }) {
}
args.push(file);
const r = await runL("screencapture", args, { timeoutMs: 20_000 });
if (r.code !== 0) throw new ExecError(`screencapture exited ${r.code}: ${r.stderr.trim().slice(0, 300)}`, r);
if (r.code !== 0) {
const detail = r.stderr.trim().slice(0, 300);
// This is what screencapture says when the display is locked, asleep, or
// the session is not the console user — not a permission problem
// (without the Screen Recording grant it exits 0 and omits windows).
const remedy = /could not create image/i.test(detail)
? " — the display is locked, asleep, or this session is not at the console; unlock or wake it and retry"
: "";
throw new ExecError(`screencapture exited ${r.code}: ${detail}${remedy}`, r);
}
const stat = fs.statSync(file);
const displays = await displayInfo();
const d = displays.find((x) => x.index === (disp === "all" ? 1 : disp)) ?? displays[0];
Expand Down Expand Up @@ -442,6 +467,33 @@ export function create({ exec }) {
}`, {}, 25_000);
}

// Which app owns the keyboard right now. Raw CGEvents go to it no matter
// what the caller meant (#5927), so every input receipt names it.
async function frontmostApp() {
const r = await jxa(`${JXA_PRELUDE}
var se = Application('System Events');
var list = se.applicationProcesses.whose({ frontmost: true })();
Comment on lines +473 to +475

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid requiring unprobed Automation access for app_ref

On a Mac where the host has the reported Accessibility grant but has not also authorized Apple Events automation of System Events, this new lookup can prompt, time out, or fail even though raw CGEvent input is permitted. Guarded input then cannot succeed, while request_access reports no missing permission because it probes only AX trust and Screen Recording. Read the frontmost/running application through a non-Apple-Events API such as NSWorkspace, or explicitly probe and report the additional Automation boundary.

AGENTS.md reference: AGENTS.md:L28-L29

Useful? React with 👍 / 👎.

if (!list.length) return JSON.stringify({ found: false });
var p = list[0];
return JSON.stringify({ found: true, name: g(function(){ return String(p.name()); }), pid: num(function(){ return p.unixId(); }),
bundle_id: g(function(){ var b = p.bundleIdentifier(); return b ? String(b) : null; }) });
}`, { probe: "frontmost-app" }, 8_000);
return r && r.found ? { name: r.name, pid: r.pid, bundle_id: r.bundle_id } : null;
}

// Refuse to post keystrokes when the app the caller named is not the one
// that would receive them. Returns the frontmost app for the receipt.
async function guardInput(appRef) {
const front = await frontmostApp().catch(() => null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guardInput swallows any failure from frontmostApp() into front = null, not just the "no frontmost app" case:

const front = await frontmostApp().catch(() => null);

If this JXA call fails for a reason unrelated to the actual frontmost state (a transient osascript timeout, an unrelated JXA error, etc.) while the subsequent findProcess(appRef) call on line 489 happens to succeed, the function falls through to:

if (!front || front.pid !== target.pid) {
  throw new ExecError(`refusing to send input: "${target.name}" is not frontmost...`);
}

front is null, so this unconditionally reports "${target.name}" is not frontmost — even though the real cause was an unrelated lookup failure, not the named app actually being backgrounded. That's a false negative: a legitimate, safe keystroke request gets refused with a misleading reason, which cuts against this PR's own goal of truthful status reporting (vs. the old request_access that just lied optimistically — this direction, pessimistically, but still not truthful about why).

Consider letting the frontmostApp() error propagate (or at least surface its message) instead of unconditionally collapsing it to "not frontmost".

if (!appRef) return front;
const target = await findProcess(appRef);
if (!target.found) throw new ExecError("application not found — call list_apps for exact names/pids");
if (!front || front.pid !== target.pid) {
throw new ExecError(`refusing to send input: "${target.name}" is not frontmost${front ? ` ("${front.name}" is)` : ""}; bring it forward first with open_application { activate: true } or click into it`);
}
return front;
}
Comment on lines +484 to +495

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix for the timeout/payload bug and the truthful probe. One residual gap worth calling out explicitly (not necessarily a blocker): guardInput only refuses when the caller passes app_ref (tools.mjs lines 222/226/231 make it optional). A key/type/hold_key call made the way the #5927 incident happened — no app_ref — still goes straight through to whatever is frontmost, exactly as before this PR. That's fine as a backend capability (the guard can't force the caller to use it), but since the PR title frames this as fixing "frontmost-guarded keystrokes," it may be worth confirming the tool-calling policy/prompt actually always supplies app_ref for destructive-ish keys (e.g. cmd+q), otherwise the guard added here doesn't change the failure mode that caused the incident.


async function listWindows(appRef) {
const p = await findProcess(appRef ?? {});
if (!p.found) throw new ExecError("application not found — call list_apps for exact names/pids");
Expand All @@ -456,11 +508,25 @@ export function create({ exec }) {
if (activate) args.unshift("-F");
const r = await runL("open", args, { timeoutMs: 25_000 });
if (r.code !== 0) throw new ExecError(`open failed: ${r.stderr.trim().slice(0, 200)}`, r);
await new Promise((res) => setTimeout(res, 600));
const find = {};
if (bid) find.bundle_id = bid; else if (pid) find.pid = pid; else find.name = String(name).replace(/\.app$/, "");
const p = await findProcess(find).catch(() => null);
return { launched: true, activate, url: urlArg ?? null, resolved: p?.found ? { name: p.name, pid: p.pid, bundle_id: p.bundle_id, frontmost: p.frontmost } : null };
// `open -F` returns before the app is in front. Wait for the process to
// exist and, when activation was asked for, to actually be frontmost;
// otherwise the next keystroke lands in whatever app is (#5927).
let p = null;
Comment on lines +513 to +516
for (let attempt = 0; attempt < 10; attempt++) {
await new Promise((res) => setTimeout(res, 300));
p = await findProcess(find).catch(() => null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] open_application's 3s wait is not actually bounded

The polling loop sleeps 300ms per attempt but each findProcess(find) call runs to its own timeout (potentially 8s or more when a TCC prompt appears and Accessibility state is unknown). If the app is slow to appear or System Events hangs, open_application can block far longer than the documented 3 seconds. Consider using a deadline or Promise.race around findProcess so total wait is bounded.

if (p?.found && (!activate || p.frontmost)) break;
}
const resolved = p?.found ? { name: p.name, pid: p.pid, bundle_id: p.bundle_id, frontmost: !!p.frontmost } : null;
const frontmost = !!resolved?.frontmost;
const note = activate && !frontmost
? (resolved
? `"${resolved.name}" is running but did not come to the front within 3 s; keystrokes would go to another app — retry activation or click into its window before typing`
: `the app did not appear within 3 s of \`open\`; call list_apps to see what is running`)
: undefined;
return { launched: true, activate, frontmost, pid: resolved?.pid ?? null, url: urlArg ?? null, resolved, ...(note ? { note } : {}) };
}

// ---------- clipboard / cursor / waits ----------
Expand Down Expand Up @@ -493,21 +559,94 @@ print('{\"x\": %d, \"y\": %d}' % (l.x, l.y))`;
}

// ---------- probe ----------
const TCC_FIX = {
accessibility: "System Settings → Privacy & Security → Accessibility → enable the host app, then relaunch it",
screen_recording: "System Settings → Privacy & Security → Screen & System Audio Recording → enable the host app, then relaunch it",
};
// TCC attributes grants to the .app that owns this process tree (the
// terminal or IDE hosting the engine), never to node or osascript. Name it so
// the remedy says which row to flip.
async function hostAppName() {
if (state.hostApp !== undefined) return state.hostApp;
// Keep the outermost bundle: framework binaries also live inside an .app
// (python3 runs from Python.app), but TCC holds the launching app
// responsible for everything under it.
let pid = process.ppid;
let found = null;
for (let depth = 0; depth < 12 && pid > 1; depth++) {
const r = await runL("ps", ["-o", "ppid=,comm=", "-p", String(pid)], { timeoutMs: 4_000 });
if (r.code !== 0) break;
const m = /^\s*(\d+)\s+(.*)$/.exec(r.stdout.trim());
if (!m) break;
const app = /([^/]+)\.app\//.exec(m[2]);
if (app) found = app[1];
pid = Number(m[1]);
}
state.hostApp = found;
return found;
}
async function grantTarget() {
const host = await hostAppName().catch(() => null);
return host ? `"${host}"` : "the app hosting the Codewhale engine (your terminal)";
}
// Ask TCC instead of guessing from tool presence: screencapture exits 0
// without the grant (it just omits windows) and osascript hangs on the
// prompt, so probing by running them proves nothing.
// The JXA bridge does not expose CGPreflightScreenCaptureAccess, so the
// Screen Recording state is read the way TCC enforces it: without the grant,
// CGWindowListCopyWindowInfo strips kCGWindowName from every other
// process's window. No other windows on screen means the answer is unknown.
async function tccState() {
const r = await jxa(`ObjC.import('ApplicationServices'); ObjC.import('CoreGraphics'); ObjC.import('Foundation');
function run(){
const out = { accessibility: !!$.AXIsProcessTrusted(), screen_recording: null };
const me = $.NSProcessInfo.processInfo.processIdentifier;
const list = $.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly | $.kCGWindowListExcludeDesktopElements, $.kCGNullWindowID);
const n = Number($.CFArrayGetCount(list));
let others = 0, named = 0;
for (let i = 0; i < n; i++) {
const d = ObjC.deepUnwrap(ObjC.castRefToObject($.CFArrayGetValueAtIndex(list, i)));
if (!d || d.kCGWindowOwnerPID === me || d.kCGWindowLayer !== 0) continue;
others++;
if (typeof d.kCGWindowName === 'string' && d.kCGWindowName.length) named++;
}
if (others > 0) out.screen_recording = named > 0;
Comment on lines +611 to +613

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat unnamed windows as an unknown permission state

When at least one other layer-0 window exists but every such window has an absent or empty kCGWindowName, this sets screen_recording to false. Window names are optional even when Screen Recording access is granted, so an untitled-window-only desktop is reported as denied, disables screenshot and recording capabilities, and directs the user to change a grant they already hold. The absence of a name can prove neither state here; preserve null unless a named window positively proves access.

AGENTS.md reference: AGENTS.md:L28-L29

Useful? React with 👍 / 👎.

return JSON.stringify(out);
}`, {}, 8_000);
return r && typeof r === "object" ? r : {};
}
async function probe() {
const caps = { screenshot: true, recording: true, accessibility_tree: true, clipboard: true, displays: true };
const caps = { screenshot: true, recording: true, accessibility_tree: true, raw_input: true, clipboard: true, displays: true };
const perms = {};
try { await jxa(`function run(argv){ return JSON.stringify({n: Application('System Events').applicationProcesses.length}); }`, {}, 8_000); perms.accessibility = "granted"; }
catch (e) { perms.accessibility = "denied_or_unavailable"; caps.accessibility_tree = false; caps.raw_input = "unreliable"; }
try {
const t = os.tmpdir() + `/cu-probe-${crypto.randomBytes(3).toString("hex")}.png`;
const r = await runL("screencapture", ["-x", "-R0,0,2,2", "-t", "png", t], { timeoutMs: 8_000 });
perms.screen_capture = r.code === 0 ? "ok" : "failed";
try { fs.rmSync(t, { force: true }); } catch {}
} catch { perms.screen_capture = "failed"; }
const hasRecording = fs.existsSync("/usr/sbin/screencapture");
return { platform: "darwin", capabilities: caps, permissions: perms, note: "macOS does not expose Screen-Recording TCC state to CLI; a black/empty screenshot means Screen Recording permission is missing. Raw pointer/keyboard events go to whatever is frontmost at the target point — activate the app first for click-type actions." };
const missing = [];
let tcc = {};
Comment on lines 618 to +622
try { tcc = await tccState(); } catch (e) { perms.probe_error = String(e?.message || e).slice(0, 200); }
state.tcc = tcc;
const target = await grantTarget();
if (tcc.accessibility === false) {
perms.accessibility = "denied";
caps.accessibility_tree = false;
caps.raw_input = false;
missing.push("accessibility");
} else {
perms.accessibility = tcc.accessibility === true ? "granted" : "unknown";
}
if (tcc.screen_recording === false) {
perms.screen_recording = "denied";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the smoke harness for the renamed permission field

The probe now emits permissions.screen_recording, but the bundled live smoke check in scripts/smoke.mjs:97 still reads permissions.screen_capture and declares the row passing based only on top-level ok. Consequently the macOS smoke run prints capture=undefined and passes whether the new probe reports granted, denied, or unknown, so it cannot validate the central permission fix; update the consumer and assert the returned state, or preserve a compatible alias.

AGENTS.md reference: AGENTS.md:L120-L122

Useful? React with 👍 / 👎.

caps.screenshot = false;
caps.recording = false;
missing.push("screen_recording");
} else {
perms.screen_recording = tcc.screen_recording === true ? "granted" : "unknown";
}
const how_to_fix = Object.fromEntries(missing.map((m) => [m, `${TCC_FIX[m]} — grant it to ${target}`]));
const note = missing.length
? `Missing: ${missing.join(", ")}. Grants belong to ${target}, not to node or osascript. ${Object.values(how_to_fix).join(" ")}`
: "Raw pointer/keyboard events go to whatever is frontmost at the target point — activate the app first for click-type actions.";
return { platform: "darwin", capabilities: caps, permissions: perms, missing, how_to_fix, host_app: state.hostApp ?? null, note };
}


return {
platform: "darwin",
probe,
Expand Down Expand Up @@ -565,33 +704,36 @@ print('{\"x\": %d, \"y\": %d}' % (l.x, l.y))`;
$.CGEventPost($.kCGHIDEventTap, ev);
return JSON.stringify({ ok: true });`, { dx, dy }).then(() => ({ action_sent: true, direction, amount }));
},
type: async ({ text }) => {
type: async ({ text, app_ref }) => {
if (!text) return { action_sent: false, note: "empty text" };
const frontmost_app = await guardInput(app_ref);
const r = await cg(`var ev = $.CGEventCreateKeyboardEvent($(), 0, true);
$.CGEventKeyboardSetUnicodeString(ev, P.text.length, P.text);
$.CGEventPost($.kCGHIDEventTap, ev);
var ev2 = $.CGEventCreateKeyboardEvent($(), 0, false);
$.CGEventKeyboardSetUnicodeString(ev2, P.text.length, P.text);
$.CGEventPost($.kCGHIDEventTap, ev2);
return JSON.stringify({ ok: true, chars: P.text.length });`, { text }, 15_000);
return { action_sent: true, chars: text.length, strategy: "unicode-events" };
return { action_sent: true, chars: text.length, strategy: "unicode-events", frontmost_app };
},
key: async ({ text, repeat = 1 }) => {
key: async ({ text, repeat = 1, app_ref }) => {
const { flags, code, key } = parseChord(text);
const frontmost_app = await guardInput(app_ref);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck app_ref before every repeated keypress

When repeat > 1, this checks the foreground app only once before the loop even though each key event is later posted globally through a separate osascript invocation. If the first shortcut causes the target to quit or lose focus—for example cmd+q—a subsequent repetition can land in the newly frontmost app despite the supplied guard; revalidate the target before each repetition or post the events specifically to the target process.

AGENTS.md reference: AGENTS.md:L28-L29

Useful? React with 👍 / 👎.

for (let i = 0; i < Math.max(1, Math.min(100, repeat)); i++) {
await keyEvent(code, flags, true);
await keyEvent(code, flags, false);
if (i < repeat - 1) await new Promise((r) => setTimeout(r, 30));
}
return { action_sent: true, key, code, repeat: Math.max(1, Math.min(100, repeat)) };
return { action_sent: true, key, code, repeat: Math.max(1, Math.min(100, repeat)), frontmost_app };
},
hold_key: async ({ text, duration }) => {
hold_key: async ({ text, duration, app_ref }) => {
const { flags, code, key } = parseChord(text);
const frontmost_app = await guardInput(app_ref);
const d = Math.max(0.05, Math.min(30, Number(duration) || 1));
await keyEvent(code, flags, true);
await new Promise((r) => setTimeout(r, d * 1000));
await keyEvent(code, flags, false);
return { action_sent: true, key, heldSec: d };
return { action_sent: true, key, heldSec: d, frontmost_app };
},
set_value: async ({ target, value }) => {
const el = await elementAction(target.app_ref, target.windowIndex, target.path, { kind: "set_value", value });
Expand Down
18 changes: 13 additions & 5 deletions crates/tui/plugins/computer-use/src/tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ const computerParam = {
description: "Computer id to act on. Defaults to the active computer. Providing a different registered id switches to it first (sticky).",
};

// Optional guard for keystroke tools: the app that must be frontmost.
const inputAppRef = {
type: "object",
description: "Refuse to send the keystrokes unless this app is frontmost (name, bundle_id, or pid).",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] app_ref schema allows an empty object

inputAppRef has no minProperties or required, so app_ref: {} is valid input and reaches the backend. The backend treats the empty object as truthy, calls findProcess({}), and returns a confusing 'application not found' instead of rejecting the invalid schema. Requiring at least one app identifier prevents this.

properties: { pid: { type: "integer" }, name: { type: "string" }, bundle_id: { type: "string" } },
additionalProperties: false,
};
Comment on lines +9 to +15
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add minProperties: 1 to inputAppRef so an empty object is rejected by schema validation before reaching the backend.

Suggested change
description: "Refuse to send the keystrokes unless this app is frontmost (name, bundle_id, or pid).",
properties: { pid: { type: "integer" }, name: { type: "string" }, bundle_id: { type: "string" } },
additionalProperties: false,
};
type: "object",
minProperties: 1,
description: "Refuse to send the keystrokes unless this app is frontmost (name, bundle_id, or pid).",
properties: { pid: { type: "integer" }, name: { type: "string" }, bundle_id: { type: "string" } },
additionalProperties: false,


const targetSchema = {
oneOf: [
{
Expand Down Expand Up @@ -211,16 +219,16 @@ export const TOOLS = [
},
// ---- text & keyboard ----
{
name: "type", description: "Type text into the focused control (unicode). Focus the field first (click/element action).",
inputSchema: { type: "object", required: ["text"], properties: { text: { type: "string" }, computer: computerParam }, additionalProperties: false },
name: "type", description: "Type text into the focused control (unicode). Focus the field first (click/element action). Pass app_ref to refuse unless that app is frontmost; the receipt names frontmost_app either way.",
inputSchema: { type: "object", required: ["text"], properties: { text: { type: "string" }, app_ref: inputAppRef, computer: computerParam }, additionalProperties: false },
},
{
name: "key", description: "Press a key or chord, e.g. 'return', 'cmd+c' (macOS), 'ctrl+c' (Linux/Windows). Repeat with `repeat`.",
inputSchema: { type: "object", required: ["text"], properties: { text: { type: "string" }, repeat: { type: "integer", minimum: 1, maximum: 100 }, computer: computerParam }, additionalProperties: false },
name: "key", description: "Press a key or chord, e.g. 'return', 'cmd+c' (macOS), 'ctrl+c' (Linux/Windows). Repeat with `repeat`. Pass app_ref to refuse unless that app is frontmost; the receipt names frontmost_app either way.",
inputSchema: { type: "object", required: ["text"], properties: { text: { type: "string" }, repeat: { type: "integer", minimum: 1, maximum: 100 }, app_ref: inputAppRef, computer: computerParam }, additionalProperties: false },
Comment on lines +226 to +227

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce app_ref across the advertised backends

On Linux, Windows, and HarmonyOS—including those platforms reached over SSH—this schema accepts and forwards app_ref, but their type, key, and hold_key implementations destructure only the original arguments and silently send input to the current foreground application. A caller relying on the advertised refusal guard can therefore still send a destructive shortcut to the wrong app; implement the guard on every backend or reject/remove app_ref where it is unsupported.

AGENTS.md reference: AGENTS.md:L38-L40

Useful? React with 👍 / 👎.

},
{
name: "hold_key", description: "Hold a key for `duration` seconds (0.05..30).",
inputSchema: { type: "object", required: ["text", "duration"], properties: { text: { type: "string" }, duration: { type: "number", minimum: 0.05, maximum: 30 }, computer: computerParam }, additionalProperties: false },
inputSchema: { type: "object", required: ["text", "duration"], properties: { text: { type: "string" }, duration: { type: "number", minimum: 0.05, maximum: 30 }, app_ref: inputAppRef, computer: computerParam }, additionalProperties: false },
},
{
name: "set_value", description: "Set an editable element's value through the accessibility layer (background-safe, no keystrokes). Element targets only.",
Expand Down
Loading
Loading