030: A Mac runs desktop commands through launchctl asuser
Status: accepted, 1 October 2026. Extended by 039: Windows runs desktop commands through a one-off scheduled task. Extended by 047: on a pool’s Mac, the helper writes the sudoers rule for each box’s user and the reset removes it.
Context
The run tool’s session: "desktop" starts a command in the box user’s
logged-in desktop session (011). Core does this on
Linux only. On macOS it refuses, and the architecture planned a LaunchAgent
for it (issue #57).
SSH on macOS runs in the Background session, apart from the user’s Aqua
session, and each session has its own way to the window server, the
pasteboard and the login keychain. Apple advises a LaunchAgent for work in
the desktop session, and calls launchctl asuser, which moves a command
into a user’s session, legacy. A LaunchAgent must be installed in every
image and home skeleton, stays resident, and would have to carry stdin,
output, the exit status and stopping across FIFOs itself.
Measured on macOS 27.0 in a Tart box, over bb-box’s SSH (the macOS desktop session):
- A command wrapped in
launchctl asuserruns inAqua, with the PATH from ~/.zshenv. Its exit status, stdin and output pass through as for any SSH command, and output streams. - The wrapper costs about 21 ms per run.
- The login keychain works only in a wrapped command. The pasteboard works with or without it.
- From plain SSH on 27.0,
open -astarts an app frontmost, and an app’s binary gets a window that takes typing through Accessibility. - Privacy grants (TCC) follow the process macOS holds responsible, not the session, so the wrapper neither gains nor loses them.
Decision
-
On macOS, core runs a desktop command through the box’s own transport as:
Terminal window sudo -n /bin/launchctl asuser "$uid" /usr/bin/sudo -n -u "$user" -- "$SHELL" -c '<script>'The outer sudo gives
asuserthe root it needs, and the inner one runs the command as the box user again. The full paths make the command match the sudoers rule below exactly. -
Every Mac gets it:
-
a box whose user is an administrator, such as Tart’s, needs nothing more;
-
on a Mac whose box user is a standard user, such as an SSH host pool’s Mac or a physical Mac, Mac setup writes
/etc/sudoers.d/bb-box-desktop, mode 0440, checked withvisudo -cf:<user> ALL=(root) NOPASSWD: /bin/launchctl asuser <uid> /usr/bin/sudo -n -u <user> -- *It allows the wrapper and nothing else, and gives no root shell. A home reset leaves
/etcalone, so the rule survives it.
-
-
The inner sudo resets the environment. So
command()puts the env file’s sourcing, thecdand the script inside the wrapper. Writing the env file stays outside it. -
Before it runs, the command checks two things, and each failure has its own exit status and message:
- the box user owns the console (
stat -f %Su /dev/console), or there is no desktop session to join; - sudo runs a no-op,
/usr/bin/true, through the wrapper without a password, or the message gives the sudoers rule to add.sudo -n -lis not enough: it allows a command whose rule needs a password (E13 in the research note).
- the box user owns the console (
-
canRunInDesktop("macos")is true. The interface does not change, and core never branches on the backend: on a Mac where sudo refuses, the run fails with the message above. -
The conformance suite runs a
session: "desktop"command on macOS desktop kinds, and checks thatlaunchctl managernameprintsAquaand that stderr stays apart from stdout. -
If a later macOS breaks
asuser, that check fails, and a LaunchAgent that runs requests from a FIFO replaces the wrapper, as the research note describes.
Consequences
- An agent can start an app, or run a test that needs the window server, in a Mac box’s desktop, with output and the exit status as for any command.
- Only a desktop command reaches the login keychain, so git’s keychain helper and Xcode’s signing work there and not in a plain run.
- Nothing is installed in Mac images, and no process stays resident.
- A run’s time limit works as for other macOS commands: core stops the
local
ssh, and the command may keep running in the box. - The SSH host pool’s Mac setup, when it is written, and the physical Mac’s setup add the sudoers rule.
- Windows still needs a scheduled task with
/ITfor its desktop session (issue #46). - Core makes the change in
src/core/in-box.tsafter reading BB’s variables on macOS.