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
11 changes: 11 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@ jobs:
for i in $(seq 1 100); do [ -f "$DATA2/port.json" ] && break; sleep 0.1; done
kill $PID2 || true
test -f "$DATA2/port.json"
# TD-5228: the pinned agent-desktop CLI and its helper sit beside
# tstd, signed along with the app (ad-hoc here, run-tauri.mjs), and
# the CLI runs from there. Signing changes the bytes, so the release
# digest is not checked here; the daemon trusts the packaged copy
# (TD-5222).
AD="$APP/Contents/MacOS/agent-desktop"
test -x "$APP/Contents/MacOS/agent-desktop-macos-helper"
codesign -v "$AD"
codesign -v "$APP/Contents/MacOS/agent-desktop-macos-helper"
"$AD" version | grep -q '"version":"1.0.2"'
ls "$APP/Contents/Resources/licenses/LICENSE" "$APP/Contents/Resources/licenses/NOTICE"

- name: Smoke the bundled sidecar (Linux)
if: runner.os == 'Linux'
Expand Down
54 changes: 54 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19663,3 +19663,57 @@ as two agents); hiding the ring after a fixed time inside an episode (the
TD-3407 reason stands: a long think between actions is still the agent
driving); a stop-file watch inside the AppKit helper (it would need the
config path and the mode as well; the sidecar already has both).

## 2026-11-03 — TD-5228: the Mac app carries agent-desktop (Class B)

**Decision:** The macOS app (Apple silicon and Intel) ships the pinned
`agent-desktop` 1.0.2 CLI and its `agent-desktop-macos-helper` beside
`tstd`, the same way TD-5221 step 1 does on Linux, so the TD-5220 tools
work with `binary` empty. Windows stays unbundled. The tools stay off by
default.

1. **The same fetch, two more targets.** `fetch_agent_desktop.py` pins
both macOS archives (digests copied from the release's `checksums.txt`)
and both helpers (hashed from the checked archives). The CLI digests
were already in `ax_cli.PINNED_SHA256`. The helper's name now depends
on the platform. `build_sidecar.py` fetches on macOS hosts too, in both
modes.
2. **A macOS platform config.** `shell/tauri.macos.conf.json` repeats
`binaries/tstd` (the merge patch replaces the list) and adds the two
files and the licence resources. A test checks that each platform config
keeps every shared entry and never names the other platform's helper,
which is not fetched there and would fail the build.
3. **Signed by the bundler, under their own names.** Tauri's macOS bundler
(CLI 2.11.4) copies every `externalBin` into `Contents/MacOS` with the
triple dropped, then signs each one before the app, with the app's
identity, the hardened runtime and the app's entitlements. So the CLI
and helper carry the same `tst-desk-dev` certificate signature as the
rest of the bundle, with identifiers `agent-desktop` and
`agent-desktop-macos-helper`. Unlike `tstd`, they are not re-signed
with the bundle id. TCC charges a command-line tool's Accessibility use
to the app that started its process tree, here the `tst-desk` host:
the 2026-09-02 entry saw exactly that for `tstd`'s own input events,
and the CLI's own permission guide relies on the same rule.
`install-macos.sh` refuses a bundle where either file is not
certificate-signed.
4. **The packaged copy is not the release's bytes.** Signing rewrites the
binary, so the bundled CLI never matches the release digest. TD-5222's
rule (the copy beside the packaged daemon is trusted as part of the
package) already covers it. The files in `shell/binaries/` stay as
released, so the build's pin check still holds and a rebuild needs no
download.
5. **The helper is for the clipboard only.** In 1.0.2 the CLI starts
`agent-desktop-macos-helper` only for its clipboard commands, which none
of the four tools use. It ships so the bundle matches the upstream
package. The CLI accepts it only beside itself under that exact name,
owned by the user running the CLI, with no folder above it writable by
others. `/Applications` is group-writable (admin), so from there the
clipboard commands would refuse; nothing in TST Desk calls them.

**Rejected:** re-signing the two files with the bundle id, as
`install-macos.sh` does for `tstd` (attribution follows the responsible
process, and another identity rewrite is one more step that can drift);
signing them in `build_sidecar.py` before bundling (the bundler re-signs
every `externalBin` anyway, and the files in `shell/binaries/` would stop
matching their pins); bumping to agent-desktop 1.1.0, released the same
day (the tools were built and tested against 1.0.2).
8 changes: 5 additions & 3 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ The accessibility desktop tools (computer_use.accessibility, TD-5220) run
the unmodified agent-desktop command-line tool, release 1.0.2, as a separate
process. This repository does not include its source or binaries. The Linux
packages (AppImage, deb) ship the unmodified release binaries agent-desktop
and agent-desktop-linux-clipboard-owner, downloaded at build time and
checked against pinned SHA-256 digests (TD-5221), together with this notice
and a copy of the Apache License, Version 2.0 (licenses/LICENSE and
and agent-desktop-linux-clipboard-owner, and the macOS app ships
agent-desktop and agent-desktop-macos-helper, re-signed with the app's own
code signature. All are downloaded at build time and checked against pinned
SHA-256 digests (TD-5221, TD-5228), and ship together with this notice and a
copy of the Apache License, Version 2.0 (licenses/LICENSE and
licenses/NOTICE in the installed app's resources).
14 changes: 10 additions & 4 deletions core/scripts/build_sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
tauri.conf.json's beforeDevCommand uses it so `tauri dev` pays the build
once instead of on every start.

On Linux it also puts the pinned agent-desktop CLI and its helper beside
the sidecar (TD-5221, ``fetch_agent_desktop.py``); tauri.linux.conf.json
bundles them. Both modes do this, and a checksum mismatch fails the build.
On Linux and macOS it also puts the pinned agent-desktop CLI and its helper
beside the sidecar (TD-5221, TD-5228, ``fetch_agent_desktop.py``);
tauri.linux.conf.json and tauri.macos.conf.json bundle them. Both modes do
this, and a checksum mismatch fails the build.

Run from anywhere; paths resolve from this file's location.
"""
Expand Down Expand Up @@ -58,6 +59,11 @@ def host_triple() -> str:
raise RuntimeError("could not parse host triple from rustc -vV")


def bundles_agent_desktop() -> bool:
"""True on the hosts whose package carries the agent-desktop CLI."""
return sys.platform.startswith("linux") or sys.platform == "darwin"


def target_path(triple: str) -> Path:
"""Where this host's sidecar lands; Tauri's externalBin convention."""
# PyInstaller appends .exe on Windows, and Tauri's externalBin
Expand Down Expand Up @@ -347,7 +353,7 @@ def main() -> int:
)
args = parser.parse_args()
triple = host_triple()
if sys.platform.startswith("linux"):
if bundles_agent_desktop():
try:
for path in fetch_agent_desktop.ensure(triple):
print(f"agent-desktop {fetch_agent_desktop.PINNED_RELEASE}: {path}")
Expand Down
42 changes: 28 additions & 14 deletions core/scripts/fetch_agent_desktop.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
"""Fetch the pinned agent-desktop CLI for the Linux package (TD-5221).
"""Fetch the pinned agent-desktop CLI for the Linux and macOS packages (TD-5221, TD-5228).

uv run python scripts/fetch_agent_desktop.py [--triple T] [--archive FILE]

Downloads the ``agent-desktop`` release archive for one Linux target, checks
the archive and both files inside it against pinned SHA-256 digests, and
writes them as Tauri ``externalBin`` sidecars:
Downloads the ``agent-desktop`` release archive for one Linux or macOS
target, checks the archive and both files inside it against pinned SHA-256
digests, and writes them as Tauri ``externalBin`` sidecars:

shell/binaries/agent-desktop-<triple>
shell/binaries/agent-desktop-linux-clipboard-owner-<triple>
shell/binaries/<helper>-<triple>

The bundler drops the triple, so both land beside ``tstd`` in the package,
which is where the CLI looks for its helper and where the daemon looks for
the CLI. Any digest mismatch fails the build. Nothing here runs in the app:
the packaged app never downloads.
where the helper is ``agent-desktop-linux-clipboard-owner`` on Linux and
``agent-desktop-macos-helper`` on macOS. The bundler drops the triple, so
both land beside ``tstd`` in the package, which is where the CLI looks for
its helper and where the daemon looks for the CLI. On macOS the bundler
signs both with the app's identity, and they run as children of TST Desk,
so their Accessibility use is TST Desk's own; the files written here stay
as released. Any digest mismatch fails the build. Nothing here runs in the
app: the packaged app never downloads.

Files already in place with the pinned digests are kept, so a rebuild needs
no network. ``--archive`` uses a local copy of the archive instead of
downloading it; it is checked the same way.

Only Linux is bundled for now. macOS needs the helper signed with the app
and its Accessibility grant (TD-4823); Windows has no live test yet.
Windows is not bundled yet: it has no live test.
"""

from __future__ import annotations
Expand All @@ -41,7 +44,9 @@
BINARIES = CORE.parent / "shell" / "binaries"

CLI = "agent-desktop"
HELPER = "agent-desktop-linux-clipboard-owner"
LINUX_HELPER = "agent-desktop-linux-clipboard-owner"
#: The CLI accepts this helper only under exactly this name, beside itself.
MACOS_HELPER = "agent-desktop-macos-helper"

URL = (
"https://github.com/vercel-labs/agent-desktop/releases/download/"
Expand All @@ -52,13 +57,17 @@
ARCHIVE_SHA256: Mapping[str, str] = {
"x86_64-unknown-linux-gnu": "ad661b7a3fd8f990a769243f4889a240ca0e2c8dab3ef5bddd6019086c71e0b4",
"aarch64-unknown-linux-gnu": "864cb782e0afc8bf0df19622016b24bdbd1a83fc7749f784ee82aa49a7f845eb",
"aarch64-apple-darwin": "7bee385196d7b478de80cf05ca4304aaa38f0eba593b3406ad3c247c1773be19",
"x86_64-apple-darwin": "8edda6df17eafc042395502a823dbb8415bc8ac14835a6731e4d8becc9cb4121",
}

#: SHA-256 of the clipboard helper inside each archive above. The CLI's own
#: digest comes from ``ax_cli.PINNED_SHA256``, the table the daemon checks.
HELPER_SHA256: Mapping[str, str] = {
"x86_64-unknown-linux-gnu": "bf17de139902724e86b3616970b7b7f16f5f1fb4143910b532a38dd2182720fa",
"aarch64-unknown-linux-gnu": "dcd6f2ce9a0e92e42f4d2ee9161ea1b9f2b11c0e4b1878119a19c0a2db3630a6",
"aarch64-apple-darwin": "522829116cb006b5770c6a82b361f466a4775f794d6af4083e75231a2d6bb551",
"x86_64-apple-darwin": "ce52467818ac601ae1145a0b9ecaa14aaf04d2e72d24cf6d29d26757340f9c6b",
}

#: The archives are about 3.4 MB; anything far larger is not the release.
Expand All @@ -77,16 +86,21 @@ def bundled(triple: str) -> bool:
return triple in ARCHIVE_SHA256


def helper_name(triple: str) -> str:
"""The helper the CLI starts on *triple*'s platform."""
return MACOS_HELPER if triple.endswith("-apple-darwin") else LINUX_HELPER


def expected(triple: str) -> dict[str, str]:
"""File name inside the archive → its pinned SHA-256."""
if not bundled(triple):
raise FetchError(f"no pinned agent-desktop {PINNED_RELEASE} archive for {triple}")
return {CLI: PINNED_SHA256[triple], HELPER: HELPER_SHA256[triple]}
return {CLI: PINNED_SHA256[triple], helper_name(triple): HELPER_SHA256[triple]}


def sidecar_paths(triple: str, binaries: Path = BINARIES) -> dict[str, Path]:
"""Where each file lands: Tauri's ``externalBin`` name-triple convention."""
return {name: binaries / f"{name}-{triple}" for name in (CLI, HELPER)}
return {name: binaries / f"{name}-{triple}" for name in (CLI, helper_name(triple))}


def _sha256(data: bytes) -> str:
Expand Down
17 changes: 15 additions & 2 deletions core/scripts/smoke_macos_bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Packaged-sidecar smoke on a clean macOS guest (TD-4906).
#
# Run on macOS after `npm run tauri:build` in ui/. Proves the bundled sidecar
# serves with no Python on PATH, then runs the protocol stranger loop
# serves with no Python on PATH, that agent-desktop and its helper ship signed
# beside it (TD-5228), then runs the protocol stranger loop
# (mock OpenAI → local preset → fs_write → reply) and a keychain probe.
#
# Usage: smoke_macos_bundle.sh [path-to-.app]
Expand Down Expand Up @@ -46,6 +47,18 @@ kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
echo "sidecar ok: $(cat "$DATA/d/port.json")"

echo "== agent-desktop beside tstd (TD-5228) =="
AD="$APP/Contents/MacOS/agent-desktop"
AD_HELPER="$APP/Contents/MacOS/agent-desktop-macos-helper"
test -x "$AD"
test -x "$AD_HELPER"
codesign -v "$AD"
codesign -v "$AD_HELPER"
"$AD" version | grep -q '"version":"1.0.2"'
test -f "$APP/Contents/Resources/licenses/LICENSE"
test -f "$APP/Contents/Resources/licenses/NOTICE"
echo "agent-desktop ok: $("$AD" version)"

echo "== protocol E2E (bundled tstd + loopback mock) =="
WS=$(mktemp -d)
export HOME="$DATA/home"
Expand Down Expand Up @@ -79,4 +92,4 @@ kill "$DAEMON" 2>/dev/null || true
wait "$DAEMON" 2>/dev/null || true
kill "$MOCK" 2>/dev/null || true
wait "$MOCK" 2>/dev/null || true
echo "clean macOS guest: sidecar + protocol E2E + keychain probe ok"
echo "clean macOS guest: sidecar + agent-desktop + protocol E2E + keychain probe ok"
Loading
Loading