Repository navigation
feat: ask once per agent before it controls the Mac, and keep agent input off Desktop Tools' own UI (#24) - #120
Merged
Merged
Conversation
iamnbutler
force-pushed
the
feat/caller-approval
branch
from
October 17, 2026 12:15
af95063 to
973fc35
Compare
iamnbutler
force-pushed
the
feat/caller-approval
branch
from
October 17, 2026 12:23
2335773 to
218fc99
Compare
…use agent input to Desktop Tools' own UI (#24)
…p guard each other's UI
iamnbutler
force-pushed
the
feat/caller-approval
branch
from
October 17, 2026 12:40
218fc99 to
0d8b806
Compare
This was referenced Oct 17, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #24.
The Desktop Tools app now asks once per agent before serving it. It also refuses agent input aimed at its own UI, so only the user can answer its prompts or choose Pause.
What changes
Who is asking. The host takes the client's pid from the socket's audit token and walks up its parents to the first one that isn't
node,npx,npm, a shell,loginorenv, such as Claude Code or the terminal app.claude-code), else its file name. argv[0] is never used.Prompt. "Allow name to control this Mac?" with Allow and Don't Allow. It names the signer and path.
runModalblocked the main actor that serves the Bridge, so calls timed out.Confirmations, which Fill password fields: from the Keychain by reference, or agent-supplied text with approval #117 reuses.Unapproved callers. Every request, observation included, is refused with
CALLER_NOT_APPROVEDand a hint. The handshake refuses too, so a client learns why before asking for anything. The caller is never held while the user decides. Answers persist incallers.json(0600) in the state folder.Approved Agents submenu. It lists waiting, allowed and refused agents with their signer and path:
Recent Activity names the verified caller and its pid (
claude-code (6820)) instead ofnode.Self-target refusal. Agent input is refused with
SELF_TARGET_REFUSEDwhen:Input means the pause rule's input set plus
beginSnapshotMutation, the lease every snapshot-bound click, key, type, set-value, scroll or insert takes first. Observing the host's windows still works. This makes Pause genuinely user-only.MCP client.
hostRefusalrewrites Peekaboo'spermissionDeniedto the host's code. It also replaces Peekaboo's hint "Grant the required permission before retrying." with one telling the agent to ask the user. The server instructions explain both codes.Embedders opt in with
desktop_tools_set_caller_handleranddesktop_tools_set_callers. Without a handler nothing changes.Patch.
peekaboo-caller-admission.patchcomes last, afterpeekaboo-secure-subrole.patch. It extendsoperationAdmissionwith the client's pid and the request's target pid. The target is found from the window, the scope, a process or window identity in the request (by reflection), or the snapshot the request names. The hook is also consulted at the handshake. Details are innative/patches/README.md.For Fill password fields: from the Keychain by reference, or agent-supplied text with approval #117:
Confirmations.shared.ask(.init(key:title:message:allow:deny:)) async -> Answer?andcancel(key:);Callers.shared.caller(ofClient:) -> Caller?.Upgrade note (in the CHANGELOG): existing users are asked once per agent after updating.
Tests
npm test→ 74 pass. Newtest/caller-approval.test.mjscovers code and hint mapping, a refused listing,read_error, and the rules.npm run check→ clean.DesktopToolsCallersTests→ 9/9 pass: the ancestor walk, keys, names, save/load at 0600, refusal prefixes, the guard, the bundle family, and the prompt/menu window rule. CI now runs them.Peekaboo
PeekabooBridgeOperationRecordTests→ 6/6 pass, isolated:These ran before rebasing onto fix: mark secure text fields, keep their masks out of results, and refuse typing into them #118. After the rebase the patch applies and the signed app builds; I didn't rerun these tests.
npm run live-check -- --only 120against the dev app → 2 pass:Run under the denied perl, the harness stops at preflight with its "allow the harness's caller" message. docs/live-checks.md documents these and lists the menu checks that stay manual.
Live verification on a signed dev build (state
/tmp/approve-state), all agent-runAn unapproved Claude Code got
CALLER_NOT_APPROVEDondesktop_apps,desktop_windowsanddesktop_screen, with one prompt for three calls. No call waited; refusals came back in about 50–230 ms.An agent answered the Claude Code prompt Allow. It clicked Allow with the installed 0.13.0 tools, which predate the refusal, as the lead authorized. Listings then worked, and the approval survived a restart.
callers.jsonholdsteam:Q6L2SF6YDW:com.anthropic.claude-codeat mode 0600.desktop_activateon the dev app →SELF_TARGET_REFUSED, 119 ms, nothing dispatched.A perl-spawned client →
CALLER_NOT_APPROVEDand one prompt: "Allow perl to control this Mac?" / "Signed by Apple. /usr/bin/perl".desktop_focuson my own TextEdit test window →SELF_TARGET_REFUSED.With the lead's OK, an agent opened the dev app's status menu read-only with the installed tools. Recent Activity showed
claude-code (6820)on both refused entries, and the Approved Agents item is present.Not live-checked:
These are listed as manual in docs/live-checks.md, for the user to check.