Skip to content

fix(computer-use): truthful macOS permission probe, working CGEvent input, and frontmost-guarded keystrokes - #5928

Merged
Hmbown merged 2 commits into
mainfrom
fix/cu-darwin-permission-probe-5917
Sep 6, 2026
Merged

fix(computer-use): truthful macOS permission probe, working CGEvent input, and frontmost-guarded keystrokes#5928
Hmbown merged 2 commits into
mainfrom
fix/cu-darwin-permission-probe-5917

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #5917
Closes #5927

What was wrong (all found by driving the bundled MCP server live on macOS for the #5856 receipt)

  1. request_access lied. It checked that screencapture/osascript exist, not that the Screen Recording and Accessibility grants are held, and always answered screenshot: true.
  2. Every CGEvent input was broken. cg() took (script, timeoutMs) but keyEvent, type, scroll, and postMouseEvent passed their payload as the second argument: the script saw no P.code/P.x/P.text and the payload object coerced to a zero timeout, so key, type, hold_key, scroll, and clicks reported osascript timed out instantly.
  3. open_application { activate: true } reported launched: true before the app was in front, and key/type carried no target and no receipt of where the event went. A cleanup cmd+q from the test client went to the frontmost app and quit the terminal hosting the founder's session (computer-use macOS: key/type go to whatever is frontmost, and open_application(activate) reports launched before the app is in front — a cleanup cmd+q quit the founder's terminal #5927).
  4. A screencapture failure of could not create image from display (locked or asleep display) was indistinguishable from a permission problem.

Change

  • Probe reads AXIsProcessTrusted() and the CGWindowList name-stripping TCC applies without the Screen Recording grant (the JXA bridge has no CGPreflightScreenCaptureAccess); reports each grant granted/denied/unknown, lists missing, and names the outermost .app in the process tree in how_to_fix — the app TCC holds responsible. Input scripts fail fast with that remedy after a denied probe instead of hanging on a prompt.
  • cg(script, payload, timeoutMs).
  • open_application waits up to 3 s for the process and (when activating) for it to be frontmost; returns frontmost, pid, and a note when it could not bring the app forward.
  • key/type/hold_key accept app_ref, refuse when that app is not frontmost, and receipt frontmost_app.
  • Screenshot failure names the locked/asleep display.

Verified


Note

Medium Risk
Changes macOS raw keyboard/mouse automation and permission gating; misconfiguration could still send events to the frontmost app when app_ref is omitted, but guarded paths reduce accidental quit/key delivery (#5927).

Overview
Fixes macOS computer-use reliability around TCC permissions, CGEvent input, and keystrokes hitting the wrong app.

The permission probe (request_accessprobe) now reads real Accessibility and Screen Recording state (via AXIsProcessTrusted and window-name stripping), identifies the host .app in the process tree, and returns missing, how_to_fix, and accurate capability flags. After a denied Accessibility probe, input/a11y scripts fail fast with remediation instead of hanging on TCC prompts.

CGEvent input is repaired by changing cg() to (script, payload, timeoutMs) so keycodes, coordinates, and text reach JXA with proper timeouts (fixing instant false osascript timed out failures).

Keyboard safety: type, key, and hold_key accept optional app_ref (tool schema updated), refuse when that app is not frontmost, and always receipt frontmost_app. open_application with activate: true polls up to ~3s for the app to be frontmost and returns frontmost, pid, and a warning note when activation fails. Screenshot errors distinguish locked/asleep display from permission issues.

Nine injected darwin backend tests cover probe, fail-fast input, CGEvent payloads, and frontmost guarding.

Reviewed by Cursor Bugbot for commit 46552c6. Bugbot is set up for automated code reviews on this repo. Configure here.

…#5917)

request_access claimed screenshot/accessibility were available on every
host: the probe checked that screencapture and osascript exist, not that
the Screen Recording and Accessibility grants are held. It now reads
AXIsProcessTrusted() and the CGWindowList name-stripping TCC applies
without the Screen Recording grant, reports each grant as granted /
denied / unknown, lists what is missing, and names the outermost .app in
the process tree (the app TCC holds responsible) in a how_to_fix line.
Input scripts fail fast with that remedy after a denied probe instead of
hanging on a prompt, and a screencapture 'could not create image from
display' names the locked/asleep display instead of hinting at permissions.

Separately, every CGEvent input on macOS was broken: cg() took
(script, timeoutMs) but keyEvent, type, scroll, and postMouseEvent passed
their payload as the second argument, so the script saw no P.code / P.x /
P.text and the payload object coerced to a zero timeout — key, type,
hold_key, scroll, and clicks all reported 'osascript timed out'
instantly. cg() now takes (script, payload, timeoutMs).

Tests (injected runner, any host): denied grants surface as missing
capabilities with the host app named; a known-denied grant refuses input
without spawning osascript; granted state reports no remedy; key/type
deliver their payload and a real timeout. Live receipt on this Mac:
request_access -> host_app Ghostty, both grants granted; key cmd+q ->
ok (it reached the frontmost app, which is #5927).
npm test (crates/tui/plugins/computer-use): 51 passed, 0 failed.

Signed-off-by: CodeWhale Bot <hmbown@gmail.com>
…st app (#5927)

open_application returned launched: true as soon as `open -F` exited, before
the app was in front; a following key chord then went to whatever was
frontmost — a cleanup cmd+q quit the terminal hosting the founder's
session. It now waits up to 3 s for the process to exist and, when
activation was asked for, to be frontmost, and returns frontmost: bool,
pid, and a note naming the consequence when it could not bring the app
forward.

key, type, and hold_key accept an optional app_ref and refuse with a named
reason when that app is not frontmost at send time; every receipt carries
frontmost_app (name, pid, bundle_id) so the transcript says where the
event went. Tool schemas document app_ref.

Tests (injected runner): activation that never comes forward; guarded key
refused with no CGEvent posted; unguarded key receipts the frontmost app;
guarded type goes through when the app is in front.
npm test (crates/tui/plugins/computer-use): 55 passed, 0 failed.

Signed-off-by: CodeWhale Bot <hmbown@gmail.com>
Copilot AI lite review requested due to automatic review settings September 6, 2026 06:49
@Hmbown Hmbown added this to the v0.9.13 milestone Sep 6, 2026
@cursor

cursor Bot commented Sep 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8829a29d-3305-4b1e-9c30-dc5adc91a086)

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 5m 18s —— View job


Review: fix(computer-use): truthful macOS permission probe, working CGEvent input, and frontmost-guarded keystrokes

  • Gather context (diff, changed files, CLAUDE.md/AGENTS.md rules)
  • Understand the request (code review per repo custom instructions)
  • Review darwin.mjs for correctness bugs
  • Review tools.mjs changes
  • Review new tests in backends.test.mjs
  • Check provider/model/route safety invariant (N/A — no provider/model/routing code touched)
  • Post final review

Note: I could not execute npm test in this sandbox (Bash execution beyond git/read-only commands required approval that wasn't available), so the "55 passed, 0 failed" claim in the PR description is not independently re-verified here — my review is based on static reading of the diff and the new test file's assertions/mocks.

Summary

This is a solid, well-targeted fix for three real bugs (cg() payload/timeout swap, optimistic request_access, and un-guarded open_application/keystrokes), backed by good new coverage in backends.test.mjs that mocks the exec boundary and asserts on request shape rather than real macOS state. The hostAppName() walk-up-the-process-tree approach and the Screen Recording heuristic (via CGWindowListCopyWindowInfo name-stripping) are clever, documented workarounds for real API gaps, and the code explains why in comments rather than just what.

Two things worth a look (posted as inline comments):

  1. darwin.mjs:487guardInput's frontmostApp().catch(() => null) swallows any error from the frontmost-lookup call, not just "no frontmost app found." If that lookup fails for an unrelated reason (e.g. a transient timeout) while the subsequent findProcess(appRef) succeeds, the function reports "${target.name}" is not frontmost — a misleading refusal reason for what was actually a lookup failure. Given this PR's whole point is truthful status reporting, letting that error surface (or including its message) would be more consistent.

  2. darwin.mjs:484-495 / tools.mjs:222,226,231 — the frontmost guard is opt-in via app_ref. A key/type/hold_key call made without app_ref — which is exactly how the computer-use macOS: key/type go to whatever is frontmost, and open_application(activate) reports launched before the app is in front — a cleanup cmd+q quit the founder's terminal #5927 incident happened (a bare cmd+q) — still isn't guarded at all. Not a bug in this diff, but worth confirming the calling policy/prompt is updated to always pass app_ref for such calls, otherwise the failure mode that caused the incident is unchanged for callers that don't opt in.

Minor, non-blocking notes:

  • guardInput does two full "System Events" JXA round-trips per guarded call (frontmostApp() + findProcess(appRef)); could be merged into one script to cut latency, since both already exist as separate JXA queries.
  • The new screencapture "locked/asleep/not-at-console" remedy message (darwin.mjs ~349-353) has no test coverage in backends.test.mjs, unlike the other new behaviors.
  • As the PR body already flags, win32/linux backends silently ignore app_ref — the schema now offers it uniformly in tools.mjs, so callers on those platforms could believe they have the same protection they get on macOS. Worth tracking as the stated follow-up.

Good choices worth calling out: switching probe from "did the binary exist" to actual AXIsProcessTrusted() / TCC-enforced heuristics, returning frontmost/pid/note from open_application instead of assuming success, and the cg(script, payload, timeoutMs) signature fix with a comment explaining the exact prior failure mode.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T06:56:46.405172Z 46552c6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

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.

🟡 Changes recommended

The macOS probe/launch code still has a few confirmed contract/compatibility and bounded-wait issues (fail-closed semantics on unknown, screen_capture rename, host app detection, and open_application timing) that should be corrected before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR hardens the macOS computer-use backend by making request_access report real TCC permission state, fixing CGEvent JXA payload delivery/timeouts so raw input works, and adding optional frontmost app guards + receipts for keystroke tools to reduce “sent input to the wrong app” incidents.

Changes:

  • Replace the macOS permission probe with a real TCC-backed check that reports granted/denied/unknown, missing, and how_to_fix, and makes input fail fast after a denied probe.
  • Fix CGEvent input by changing the internal cg() helper to accept (script, payload, timeoutMs) so callers actually pass keycodes/coords/text and keep real timeouts.
  • Add app_ref support for type/key/hold_key, require the app to be frontmost when provided, and include frontmost_app in receipts; improve open_application activation truthfulness.
File summaries
File Description
crates/tui/plugins/computer-use/tests/backends.test.mjs Adds darwin-specific tests for TCC probe reporting, fail-fast input, CGEvent payload delivery, and frontmost guarding behavior.
crates/tui/plugins/computer-use/src/tools.mjs Extends tool schemas/descriptions to accept optional app_ref for keystroke tools.
crates/tui/plugins/computer-use/src/backends/darwin.mjs Implements the new TCC probe, fixes CGEvent payload plumbing, adds frontmost receipts/guards, improves open_application activation handling, and clarifies screencapture failure modes.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +513 to +516
// `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;
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 });
Comment on lines 618 to +622
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 +9 to +15
// 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).",
properties: { pid: { type: "integer" }, name: { type: "string" }, bundle_id: { type: "string" } },
additionalProperties: false,
};
// 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".

Comment on lines +484 to +495
// 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);
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;
}

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46552c6f5c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +226 to +227
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 },

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 👍 / 👎.

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 👍 / 👎.

Comment on lines +611 to +613
if (typeof d.kCGWindowName === 'string' && d.kCGWindowName.length) named++;
}
if (others > 0) out.screen_recording = named > 0;

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 👍 / 👎.

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 👍 / 👎.

Comment on lines +473 to +475
const r = await jxa(`${JXA_PRELUDE}
var se = Application('System Events');
var list = se.applicationProcesses.whose({ frontmost: true })();

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 👍 / 👎.

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 👍 / 👎.

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale review

The PR fixes several serious macOS computer-use issues: the permission probe now reports real TCC state and the responsible host app, CGEvent input scripts finally receive their payload with correct timeouts, keystroke tools support an app_ref guard with frontmost receipts, and screenshot errors distinguish locked/asleep displays. The new tests cover the injected darwin backend with host-agnostic mocks.

Findings

  • [WARNING] open_application's 3s wait is not actually bounded (crates/tui/plugins/computer-use/src/backends/darwin.mjs:519)
    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.
  • [WARNING] app_ref schema allows an empty object (crates/tui/plugins/computer-use/src/tools.mjs:12)
    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.
  • [INFO] Screen Recording detection is heuristic and can be inconclusive (crates/tui/plugins/computer-use/src/backends/darwin.mjs)
    The probe classifies screen recording as granted only when it sees at least one other window with a non-empty kCGWindowName. If there are no other visible windows, or every visible window has an empty title, it reports unknown or even denied even though screencapture may work. This is a documented best-effort check, but callers should treat screenshot/recording capabilities as uncertain when no positive evidence is available.

Suggestions

  • crates/tui/plugins/computer-use/src/tools.mjs:12 — Add minProperties: 1 to inputAppRef so an empty object is rejected by schema validation before reaching the backend.

      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,
    

Assessment

The PR fixes real and important macOS automation bugs, and the added mock-based tests are valuable. The remaining concerns are mostly edge cases around unbounded polling and schema validation; they should be tightened before relying on guarded keystrokes in production, but the changes are a clear improvement over the previous behavior.


Advisory review by Codewhale (codewhale review --pr 5928 --post, head 46552c6f5c2a1f41ce8ebfa0ef3368b1db6d8843). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

let p = null;
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.

// 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.

Comment on lines +12 to +15
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,
};

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,

@Hmbown
Hmbown merged commit a9f19d4 into main Sep 6, 2026
37 checks passed
@Hmbown
Hmbown deleted the fix/cu-darwin-permission-probe-5917 branch September 6, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants