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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ Notable changes to Desktop Tools, newest first. Each release's GitHub release no
### Added

- The Desktop Tools app asks once per agent, such as Claude Code or a terminal app, "Allow *name* to control this Mac?", and refuses every request from an agent you haven't allowed with `CALLER_NOT_APPROVED`, listings and captures included, without making it wait. Agents are recognized by their validated signature, or by their path when unsigned, and **Approved Agents** in the menu revokes, allows or forgets each one. After updating, each agent you use is asked about once. Recent Activity names the agent, such as `claude-code`, instead of `node`. Embedding apps opt in with `desktop_tools_set_caller_handler`. ([#120](https://github.com/githubnext/desktop-tools/pull/120))
- `desktop_fill_password` fills the focused password field. It reads a Keychain item the agent names, by service or by server (an internet password fills only a web page on that server), so macOS asks you before Desktop Tools reads it and the agent never sees the password. It can also take text from the agent, which you approve fill by fill in a Desktop Tools prompt that names the agent, the app and the site, unless you turn on **Always Allow Agent-Supplied Passwords** in the menu (off by default). It is completed when the field shows the right number of characters. Results and Recent Activity never include the password or its length, and system password prompts, such as administrator authentication, are refused. Embedding apps ask the user through `desktop_tools_set_confirmation_handlers` and set the always-allow choice with `desktop_tools_set_password_autofill`. ([#124](https://github.com/githubnext/desktop-tools/pull/124))

### Changed

- `desktop_inspect` keeps containers an agent can scope into, such as a Finder window's file list after its long sidebar, when its result is cut to size, dropping other elements first. ([#126](https://github.com/githubnext/desktop-tools/pull/126))
- Agent input aimed at any Desktop Tools process, including another build of it, or sent while one of its prompts or menus is open, is refused with `SELF_TARGET_REFUSED`, so only the user can answer its prompts or choose Pause All Input. Agents can still observe them. ([#120](https://github.com/githubnext/desktop-tools/pull/120))
- **Behavior change:** while a password field has keyboard focus, `desktop_key` refuses keys that type characters and Command-V, and `desktop_insert` refuses all its routes, and `desktop_menu` refuses Paste commands (found by their Command-V shortcut, whatever the title), with `SECURE_FIELD_REQUIRES_FILL`. Nothing is sent. Return, Tab, Escape, arrows, Delete and other shortcuts still work. Passwords go in only through `desktop_fill_password` and its approval. ([#124](https://github.com/githubnext/desktop-tools/pull/124))

### Fixed

Expand Down
1 change: 1 addition & 0 deletions dist/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const patches = [
"peekaboo-layout-shortcuts.patch",
"peekaboo-focus-budget.patch",
"peekaboo-cursor-readback.patch",
"peekaboo-secure-fill.patch",
].map((name) => join(native, "patches", name));
function spawn(command, env) {
return spawnSync(command[0], command.slice(1), { encoding: "utf8", env: env ?? process.env });
Expand Down
56 changes: 36 additions & 20 deletions dist/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,17 @@ function validateAction(request) {
? "Set a date as ISO 8601: 2026-10-17, 2026-10-17T15:30 (local time) or 2026-10-17T15:30:00Z. Use desktop_type for text."
: "Set a finite number, true/false or an ISO 8601 date; use desktop_type for text.");
}
if (request.op === "fill-password") {
const named = ["service", "server", "text"].filter((key) => key in request);
const strings = ["service", "server", "account"].every((key) => request[key] === undefined || (typeof request[key] === "string" && request[key].length >= 1 && request[key].length <= 1024));
if (named.length !== 1 || !strings || (request.text !== undefined && request.account !== undefined)) {
throw new Error("Name one Keychain item by service or server, with an optional account, or supply text; not more than one.");
}
// The secret is never quoted back, so its problems are described without it.
if (request.text !== undefined && (typeof request.text !== "string" || !request.text || request.text.length > 1024)) {
throw new Error("The password text must contain 1 to 1024 UTF-16 code units.");
}
}
if (request.op === "type" && (typeof request.text !== "string" || request.text.length > 8192)) {
throw new Error("Replacement text must contain at most 8192 UTF-16 code units.");
}
Expand Down Expand Up @@ -1171,25 +1182,27 @@ async function act(native, request, signal) {
: request;
const operation = request.op === "menu"
? "menu"
: request.op === "dialog-answer"
? "dialog"
: request.op === "dock"
? "dock"
: request.op === "clipboard-write"
? "clipboard"
: request.op === "launch"
? "launch"
: request.op === "open"
? "open"
: request.op === "system-key"
? "system-key"
: request.op === "switch-space"
? "switch-space"
: request.op === "input-source"
? "input-source"
: isDesktopManagement(request)
? "management"
: "action";
: request.op === "fill-password"
? "fill"
: request.op === "dialog-answer"
? "dialog"
: request.op === "dock"
? "dock"
: request.op === "clipboard-write"
? "clipboard"
: request.op === "launch"
? "launch"
: request.op === "open"
? "open"
: request.op === "system-key"
? "system-key"
: request.op === "switch-space"
? "switch-space"
: request.op === "input-source"
? "input-source"
: isDesktopManagement(request)
? "management"
: "action";
data = await native([operation], signal, {
input: JSON.stringify(input),
onDispatch() {
Expand Down Expand Up @@ -1507,6 +1520,7 @@ async function execute(native, request, abort) {
"screen",
"click",
"type",
"fill-password",
"key",
"system-key",
"spaces",
Expand Down Expand Up @@ -1607,8 +1621,10 @@ async function execute(native, request, abort) {
act: (action, signal) => execute(native, action, signal),
}, request, abort ?? new AbortController().signal);
}
// A password fill may wait a minute for the user to answer a Keychain prompt or a confirmation.
const limit = request.op === "fill-password" ? 95 : 30;
const deadline = new AbortController();
const timer = setTimeout(() => deadline.abort(new Error("Native desktop operation timed out after 30 seconds.")), 30_000);
const timer = setTimeout(() => deadline.abort(new Error(`Native desktop operation timed out after ${limit} seconds.`)), limit * 1000);
const signal = abort ? AbortSignal.any([abort, deadline.signal]) : deadline.signal;
try {
if (request.op === "menubar-items") {
Expand Down
12 changes: 11 additions & 1 deletion dist/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,16 @@ export type DesktopRequest = DesktopManagement | DesktopLaunch | DesktopOpen | D
snapshot: string;
element: string;
text: string;
}
/** Fills the focused password field from a Keychain item named by service or server, or with text after approval. */
| {
op: "fill-password";
snapshot: string;
element: string;
service?: string;
server?: string;
account?: string;
text?: string;
} | {
op: "insert";
snapshot: string;
Expand Down Expand Up @@ -342,7 +352,7 @@ export type DesktopModifier = (typeof DESKTOP_MODIFIERS)[number];
export type DesktopSelection = (typeof DESKTOP_SELECTIONS)[number];
export type DesktopElementAction = (typeof DESKTOP_ELEMENT_ACTIONS)[number];
export type DesktopAction = Extract<DesktopRequest, {
op: "click" | "type" | "key" | "system-key" | "switch-space" | "input-source" | "insert" | "select" | "element-action" | "set-value" | "set-cell" | "scroll" | "drag" | "hover" | "press" | "activate" | "launch" | "open" | "quit" | "close" | "focus" | "minimize" | "restore" | "move" | "resize" | "clipboard-write" | "menu" | "dialog-answer" | "hide" | "unhide" | "maximize" | "fullscreen" | "menubar-click" | "dock" | "notification-action";
op: "click" | "type" | "fill-password" | "key" | "system-key" | "switch-space" | "input-source" | "insert" | "select" | "element-action" | "set-value" | "set-cell" | "scroll" | "drag" | "hover" | "press" | "activate" | "launch" | "open" | "quit" | "close" | "focus" | "minimize" | "restore" | "move" | "resize" | "clipboard-write" | "menu" | "dialog-answer" | "hide" | "unhide" | "maximize" | "fullscreen" | "menubar-click" | "dock" | "notification-action";
}>;
export type DesktopOutcome = "completed" | "refused" | "unknown";
export type DesktopImage = {
Expand Down
2 changes: 1 addition & 1 deletion dist/protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const DESKTOP_ELEMENT_ACTIONS = [
"AXScrollToVisible",
];
export function isDesktopAction(request) {
return request.op === "click" || request.op === "type" || request.op === "key"
return request.op === "click" || request.op === "type" || request.op === "fill-password" || request.op === "key"
|| request.op === "system-key" || request.op === "switch-space" || request.op === "input-source"
|| request.op === "insert" || request.op === "select" || request.op === "scroll"
|| request.op === "drag" || request.op === "hover" || request.op === "press"
Expand Down
Loading
Loading