Skip to content

Commit e2ea424

Browse files
authored
native2: the signed client and MCP server verify the host's team (#191) (#205)
* native2: the signed client and MCP server verify the host's team before sending (#191) desktop-tools-client and desktop-tools-mcp check the process at the other end of the socket by its audit token, as the host checks them, and refuse with HOST_UNTRUSTED unless it is the same user's and signed by their team. Peer moves into its own DesktopPeer target so the client binaries can use it without linking DesktopMac. Why: native2/Sources/DesktopPeer/Peer.swift | Trust only the code-directory hash the kernel reports for the audit token's process generation, before and after reading the on-disk signature; a pid lookup can be redirected by pid reuse. Why: native2/Sources/DesktopAppLink/Trust.swift | the host check is team-only, never identifier: embedding apps host under their own bundle identifier; an unsigned client skips it, mirroring the host, so swift build dev/test setups keep working * lane: resolve notes for the Peer move * lane: sync fix-host-verify memory
1 parent f30313d commit e2ea424

17 files changed

Lines changed: 123 additions & 19 deletions

File tree

.lane/memory/native2/Sources/DesktopMac/Peer.swift/01M5BQ5C0XG3WQ4F154GYB207A-trust-only-the-code-director.md renamed to .lane/attic/native2/Sources/DesktopMac/Peer.swift/01M5BQ5C0XG3WQ4F154GYB207A-trust-only-the-code-director.md

File renamed without changes.

‎.lane/memory/native2/Package.swift/01M5CTKMEX6J2AHHKYT87JWAYE-npm-ships-native2-sources-wi.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ anchor: '@file'
44
created: 2026-10-20T14:14:23Z
55
norm: '1'
66
sig: 238929f620452290
7-
body_hash: b8a7566233c09e5a
8-
raw_hash: 9dff802b15ce1b54
7+
body_hash: d0ba78874ecd996b
8+
raw_hash: d62ee12b4c2cbcea
9+
vouched: 2026-10-24T23:32:49Z
910
lines: 1-40
1011
---
1112

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: 01M5R44YDQGE1J10KSSBE3JCH8
3+
anchor: '@file'
4+
created: 2026-10-24T23:32:47Z
5+
norm: '1'
6+
sig: 54e953840ba5fbce
7+
body_hash: 5f7dac37e9c6a8e5
8+
raw_hash: 6dd6089e22a288e7
9+
lines: 1-29
10+
---
11+
12+
the host check is team-only, never identifier: embedding apps host under their own bundle identifier; an unsigned client skips it, mirroring the host, so swift build dev/test setups keep working

‎.lane/memory/native2/Sources/DesktopHost/Server.swift/01M5CKE5M626ZCCCNEJH5Q4Y73-the-lease-is-released-when-t.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ anchor: '@file'
44
created: 2026-10-20T12:09:04Z
55
norm: '1'
66
sig: 0fd09e8458d728cc
7-
body_hash: f100374de10d3ba0
8-
raw_hash: fe34bf98f51107bc
7+
body_hash: f6b7b7b0ce3366d6
8+
raw_hash: 6c2029ec908e6351
9+
vouched: 2026-10-24T23:32:49Z
910
lines: 1-202
1011
---
1112

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: 01M5R44YDH9D16GH8XQT8S6ENG
3+
anchor: '@file'
4+
created: 2026-10-24T23:32:47Z
5+
norm: '1'
6+
sig: 54e953840ba5fbce
7+
body_hash: 5bc53acb0ee9dd58
8+
raw_hash: a965bf1f58f51cff
9+
lines: 1-114
10+
---
11+
12+
Trust only the code-directory hash the kernel reports for the audit token's process generation, before and after reading the on-disk signature; a pid lookup can be redirected by pid reuse.

‎.lane/memory/native2/Sources/desktop-tools-client/main.swift/01M5CVGAD88HCF2N16ZA1BM1W2-a-node-server-pinned-before.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: '@file'
44
created: 2026-10-20T14:30:03Z
55
norm: '1'
66
sig: 27abcba5e975f16e
7-
body_hash: 6ffe6a94a2370277
8-
raw_hash: cf2b495e6ef0e37c
9-
vouched: 2026-10-20T14:38:23Z
7+
body_hash: 238f319478760ba3
8+
raw_hash: 72ad2b60361b067a
9+
vouched: 2026-10-24T23:32:49Z
1010
lines: 1-129
1111
---
1212

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Notable changes to Desktop Tools, newest first. Each release's GitHub release no
1414
- An open menu's window, such as a SwiftUI `MenuBarExtra` status item's NSMenu or one of its submenus, can be inspected: `desktop_inspect` reads the open `AXMenu` and lists its items with `checked` for a checkmark, so they can be clicked by element. `desktop_menubar_click` on an item whose menu is open closes the menu without choosing an item (`closed_menu`), and `desktop_element_action` `AXCancel` on the menu closes it too. A window opened by a status item is `inspectable` only when it has Accessibility elements to read. ([#193](https://github.com/githubnext/desktop-tools/issues/193))
1515
- `desktop_scroll` scrolls web content it used to refuse with "no supported route". A Chrome or other Chromium page gets arrow keys, one per unit, at the window's keyboard focus when that focus is on the page and not in a field, and the result reads back that the page moved; with focus in a field it is refused with a hint to click the page or send `desktop_key` `pagedown`. A WKWebView app that loads WebKit from a library beside its executable, such as an Electrobun app, gets the background wheel Safari gets. ([#197](https://github.com/githubnext/desktop-tools/issues/197), [#201](https://github.com/githubnext/desktop-tools/issues/201))
1616
- `desktop_type` into a web field reads the field back until it settles, up to 2 seconds, so a busy page such as a React filter is confirmed instead of reported `TYPING_UNVERIFIED`. When the field still doesn't match, the error says what it read back, in `read_back`. Replacing a Chrome field's text no longer appends to it: Chrome ignores a background Command-A, so the field's text is also selected through Accessibility, and typing stops with the field's contents reported when it can't be cleared. ([#203](https://github.com/githubnext/desktop-tools/issues/203))
17+
- The signed `desktop-tools-client` and `desktop-tools-mcp` check the host again before sending anything: a process at the socket path that isn't the same user's and signed by the same team gets no requests, and the call fails with `HOST_UNTRUSTED`. The Peekaboo-based client did this, and docs/embed.md still said so. ([#191](https://github.com/githubnext/desktop-tools/issues/191))
1718

1819
## [0.17.1] - 2026-10-20
1920

‎docs/embed.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
| `void desktop_tools_log(const char *message)` | Append one line of the app's own to that log, such as when the user pauses input |
2222
| `void desktop_tools_free(void *)` | Release a status or activity string |
2323

24-
macOS grants Accessibility, Screen Recording, and event synthesis to the embedding app. Each embedding app needs its own grants on each machine. Clients must be signed by the host's team with the exact allowlisted identifier, and the client verifies the host's team. The agent cursor and window outline draw click-through panels from the GUI process, so turn them on only from an app with a running AppKit main loop. The outline's label names the agent from `DESKTOP_TOOLS_AGENT` in the requesting client's environment, which `createDesktop` sets from its `agent` option; without it the label says "Agent". For agents outside an embedding app, use the [Desktop Tools app](app.md).
24+
macOS grants Accessibility, Screen Recording, and event synthesis to the embedding app. Each embedding app needs its own grants on each machine. Clients must be signed by the host's team with the exact allowlisted identifier. In turn, a client signed with a team sends nothing, not even its hello, to a socket served by a process that isn't the same user's and signed by that team, and fails with `HOST_UNTRUSTED`; this covers `desktop-tools-client` and the app's `desktop-tools-mcp`. Both checks read the signature of the exact process at the other end of the socket, by its audit token. An unsigned build, such as `swift build` makes for development, skips them, as the host checks its client only when it is signed itself. The agent cursor and window outline draw click-through panels from the GUI process, so turn them on only from an app with a running AppKit main loop. The outline's label names the agent from `DESKTOP_TOOLS_AGENT` in the requesting client's environment, which `createDesktop` sets from its `agent` option; without it the label says "Agent". For agents outside an embedding app, use the [Desktop Tools app](app.md).
2525

2626
The caller is the client's nearest ancestor that isn't `node`, `npx`, `npm`, a shell, `login` or `env`, such as Claude Code or the terminal app an agent runs in. Its key is the team ID and signing identifier of its running code when that signature validates, so an update that moves it keeps its approval; otherwise it is its path, plus its identifier when it has one. Its name is its app's name, else the last part of a team-signed identifier (`claude-code`), else its file name. The process can't choose either one through its arguments or environment.

‎native2/Package.swift‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@ let package = Package(
2323
targets: [
2424
.target(name: "DesktopProtocol"),
2525
.target(name: "DesktopCore", dependencies: ["DesktopProtocol"]),
26-
.target(name: "DesktopMac", dependencies: ["DesktopProtocol"], linkerSettings: [.linkedLibrary("bsm")]),
27-
.target(name: "DesktopHost", dependencies: ["DesktopProtocol", "DesktopCore", "DesktopMac"]),
26+
.target(name: "DesktopMac", dependencies: ["DesktopProtocol"]),
27+
// Who is at the other end of the socket, by signature: the host checks its client, and the client its host.
28+
.target(name: "DesktopPeer", linkerSettings: [.linkedLibrary("bsm")]),
29+
.target(name: "DesktopHost", dependencies: ["DesktopProtocol", "DesktopCore", "DesktopMac", "DesktopPeer"]),
2830
.executableTarget(name: "desktop-tools-host", dependencies: ["DesktopHost"]),
29-
.target(name: "DesktopTools", dependencies: ["DesktopProtocol", "DesktopCore", "DesktopMac", "DesktopHost"]),
31+
.target(name: "DesktopTools", dependencies: ["DesktopProtocol", "DesktopCore", "DesktopMac", "DesktopHost", "DesktopPeer"]),
3032
// How the client and MCP server bundled in the app find its socket, and open the app when nothing serves it.
31-
.target(name: "DesktopAppLink"),
33+
.target(name: "DesktopAppLink", dependencies: ["DesktopPeer"]),
3234
.executableTarget(name: "desktop-tools-mcp", dependencies: ["DesktopProtocol", "DesktopAppLink"]),
3335
.executableTarget(name: "desktop-tools-client", dependencies: ["DesktopProtocol", "DesktopAppLink"]),
3436
.executableTarget(name: "desktop-tools-app", dependencies: ["DesktopCore"]),
3537
] + (tests ? [
3638
.testTarget(name: "CoreTests", dependencies: ["DesktopCore", "DesktopProtocol"]),
37-
.testTarget(name: "RuntimeTests", dependencies: ["DesktopTools", "DesktopHost", "DesktopMac", "DesktopProtocol"]),
39+
.testTarget(name: "RuntimeTests", dependencies: ["DesktopTools", "DesktopHost", "DesktopMac", "DesktopProtocol", "DesktopPeer", "DesktopAppLink"]),
3840
] : []),
3941
swiftLanguageModes: [.v6]
4042
)

‎native2/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ A Swift MCP server and host with no Peekaboo and no Node. It runs beside the shi
99
| `DesktopProtocol` | JSON, wire framing, `ToolError`, and the contract (`ContractText.swift`), which `node scripts/contract.mjs` compiles from the Node server's instructions and tools/list; `--check` fails when they differ. It is compiled in because `Bundle.module` crashes in an embedded dylib |
1010
| `DesktopCore` | Result bounding, window availability and the approved-callers store, with no macOS calls |
1111
| `DesktopMac` | Everything that touches macOS: Accessibility reads (`Elements`, `AX`), capture, apps, windows, and input (`Pointer`, `Keyboard`, `Gestures`) |
12+
| `DesktopPeer` | Who is at the other end of the socket, by the signature of its audit token's process (`Peer.swift`), and this process's own signing |
1213
| `DesktopHost` | The tools, the snapshot store, the socket server, admission (`Admission.swift`: approved callers, pause, own-UI guard) and the runtime hooks |
1314
| `desktop-tools-host` | The host, run from a terminal for now; it holds the macOS grants |
1415
| `desktop-tools-mcp` | The stdio MCP server, which forwards each call to the host |
1516
| `desktop-tools-client` | One tool call per run, for `createDesktop({ wire: 2 })` and `dist/mcp.js --wire 2` (docs/node.md) |
1617
| `DesktopTools` | `libDesktopTools.dylib`, the menu bar app's embedding runtime with native/'s C ABI (docs/embed.md): it serves the host's `Server` and draws the agent cursor, window outline and activity list through `DesktopHost/Hooks.swift` |
1718

18-
The host serves `~/.local/state/desktop-tools2/desktop.sock`, or `$DESKTOP_TOOLS2_STATE/desktop.sock`, so it runs beside the installed Desktop Tools. It accepts a connection only from the same user, and, when the host is signed, only from an MCP binary signed by the same team with the expected identifier (`DesktopMac/Peer.swift`: the socket's audit token, checked with Security and a CDHash read before and after).
19+
The host serves `~/.local/state/desktop-tools2/desktop.sock`, or `$DESKTOP_TOOLS2_STATE/desktop.sock`, so it runs beside the installed Desktop Tools. It accepts a connection only from the same user, and, when the host is signed, only from an MCP binary signed by the same team with the expected identifier (`DesktopPeer/Peer.swift`: the socket's audit token, checked with Security and a CDHash read before and after). In turn, a signed client or MCP server sends nothing to a host that isn't the same user's and signed by its team (`DesktopAppLink/Trust.swift`).
1920

2021
## How an action runs
2122

0 commit comments

Comments
 (0)