mirror of
https://github.com/vercel-labs/agent-desktop.git
synced 2026-11-15 14:18:12 +08:00
* feat(sandbox): add the macOS VM sandbox 1. There was no reliable way for an agent to get its own macOS desktop to drive. 2. Ships the macOS VM sandbox: built and signed on the user's Mac, set up by hand once, then instant clones with agent-desktop running inside. 3. Builds on #505. ## Demo A set-up sandbox is cloned, started, driven with agent-desktop inside it, then stopped and removed. https://github.com/user-attachments/assets/1cf6a806-82ed-419f-b5ed-72bf0cf4e98f * fix(sandbox): remove the download directory before the macOS install A catalog install left its download directory in the VM bundle, and vmhost install refuses a bundle with anything but its own bookkeeping files, so fresh installs failed. The install job now removes the directory once the image is published. * fix(sandbox): escape backslashes in macOS VM provider replies on every awk json_str doubled a backslash with a gsub replacement whose meaning differs between awk implementations, so on some of them a message holding a host path with a backslash came out as invalid JSON and core hid the real error. It now escapes one character at a time with plain string concatenation, under the C locale so odd bytes cannot abort awk. * refactor(sandbox): rename the macos-vm provider to macos The provider is now named macos, so its directory, the provider table, the CI step, the docs page (now /desktop/sandboxes/macos), the skills and the provider's own messages use that name. The provider's suggestions and the docs this branch adds now point at `agent-desktop sandbox install`, since install is moving under the sandbox command on the base branch. Until that lands, the `next` hint a provider returns names a command the CLI does not have yet. * docs: list the sandbox command in the docs site command reference The site still counted 60 commands in 11 groups. sandbox is the 61st, so the counts are updated and the index links to the sandbox commands. * fix(sandbox): keep the setup time when a status probe finds the guest ready Every successful status probe rewrote setup_done and reset its mtime, and create picks the automatic clone source by that mtime. Probing an older running sandbox made the next create clone it instead of the newest set-up one. The marker is now written once, when setup first completes. * fix(sandbox): report a missing VM host, empty settings and failed boots - status and exec say the VM host is not installed when vmhost is missing, instead of reporting a running guest as still starting. - create rejects a set but empty ipsw, from or user setting instead of treating it as not passed. - start answers running only after vmhost writes vmhost.started, which it does once machine.start succeeds. A boot failure is now a failed start, and the reason stays in vm/vmhost.log. * fix(sandbox): name the install log, bound downloads and answer stop inside its budget - A failed install's status now points at vm/install.log, which rm deletes. - The image and catalog downloads time out on a stalled connection instead of holding the install job, and rm, forever. - The stop wait ends at 24 seconds so its TIMEOUT reply lands before the 30 second budget kills the script. * fix(sandbox): keep guest exit codes and macOS VM state honest - exec passes the guest's own exit status through, including 255. The guest command reports its status on stderr, so an ssh 255 without that report is still a Remote Login failure. - exec stages each copy under a per-call name and removes it when a copy fails. Concurrent execs no longer move a half-written file. - exec reports a 255 from the hash probe as Remote Login, not as a copy failure. - create rejects unknown --set keys before touching the sandbox, so a typo does not start the image download. - status reports an error when the setup record cannot be written instead of claiming guest setup is complete. - The install job runs in its own process group, and liveness checks the group. Installer children keep the bundle reserved if the job shell dies. - vmhost run takes an exclusive flock on vmhost.lock, so two hosts cannot boot the same disk image. * fix(sandbox): close four macOS VM gaps in start, status, the setup window and the address lookup - rm and stop refuse while a start is still launching the VM host, so neither acts on a sandbox whose host has not recorded its pid yet. - A set-up sandbox whose guest has an address but refuses Remote Login for two minutes reports needs_setup and names the cause, instead of answering 'still starting' forever. - Closing the setup window shuts the VM down, so the next start reopens it. - The address lookup takes the newest lease for the VM and returns none when a later lease gave that address to another machine. * fix(sandbox): give the macOS provider one explicit state machine and an explicit create source Each verb worked out the sandbox's state on its own from marker files, so every review found a verb that missed a state. One `sandbox_phase` function now derives the phase (installing, failed, stopped, starting, running) and every verb switches on it. - `create` needs exactly one source: `--set ipsw=latest`, `--set ipsw=<path>` or `--set from=<sandbox>`. The implicit download and the newest-sandbox guess are gone, and so are the shared image cache, the download resume and the size probe. `from` cannot be combined with `ipsw` or `user`. - `ipsw=latest` only takes an image whose major version the host can run. - `stop` and `exec` no longer treat an installing sandbox as stopped, a pid file left by a dead VM host reads as stopped instead of broken, and `start` answers `running` only once the guest answers. - SSH ignores the user's ssh config and every call has keepalives. - The VM host appends to install.log instead of replacing it, reports why an install failed, and ignores dot files in the bundle directory. - A failed clone reports the VM host's reason. The provider suite is now a phase-by-verb table plus the create matrix. Net: about 1,000 fewer lines. * fix(sandbox): reuse the built VM host across upgrades, and reopen the window when a guest loses its setup - The built VM host now lives in a directory keyed by its own source, so an agent-desktop upgrade that leaves the VM host unchanged keeps it. Before, every upgrade moved the provider directory and the next start reported the VM host as not installed. - A set-up sandbox that loses Remote Login or the Accessibility grant had no way back to its window, because start always ran it headless. Status now drops the setup record and says to stop and start, which opens the window again. * fix(sandbox): keep at most 64 KiB of guest stderr on the host during exec The guest decides how much it writes to stderr, and exec spooled all of it into the sandbox directory before reading it. A compromised guest could fill the host disk outside its own virtual disk. The stream now passes through a 64 KiB tail, which still carries the exit marker. * feat(sandbox): start a macOS sandbox in a window with an optional title A set-up sandbox always started headless and nothing could change that. `sandbox start <name> --window` now shows the guest desktop on the host, and `--set title=<text>` names the window. A sandbox already running headless is refused until it is stopped, and an unknown start setting is an argument error. * fix(sandbox): keep the VM host delegate alive for the machine's lifetime VZVirtualMachine holds its delegate weakly, and the delegate was a local whose last use was the assignment, so nothing guaranteed it outlived the run loop. A guest shutdown could never reach guestDidStop or didStopWithError, leaving vmhost alive and status stuck at running. The box that owns the machine now owns its delegate and attaches it at init. Verified by building providers/macos/vmhost.swift with swiftc -O. * chore: remove the sandbox probe ledger The file recorded measurements, including an unmeasured install path, that the provider code and docs no longer cite. * docs: run container desktops through agent-desktop-run Both guides made the reader start a desktop session, wait for it to write its addresses, and source those addresses in every later command. One wrapper now does it: it starts the session on first use, reuses it, and starts a fresh one when it or the container is gone, then execs the command inside it. The guides show that command and say plainly that the raw binary has no session. Verified the published wrapper against a real image with the released 1.0.2 Linux binary: launch and snapshot both return ok with ref_count 271, a cold or concurrent start leaves one desktop, a killed Xvfb is replaced, and a restarted container gets a new session on the next command. * docs: state what a sandbox does and does not isolate The pages implied isolation without saying that every sandbox on a host shares its virtual network, that clones share the image's SSH host key, or that the key in the sandbox directory is the guest credential. Say those, plus the Accessibility and hypervisor facts, in the macOS page, the overview, and the shipped desktop skill, and give the rule: one trust level per host. * fix(sandbox): give every macOS sandbox its own SSH host key A clone inherited the source VM's SSH host key, so any guest on the host's shared vmnet that answered for another sandbox's address could impersonate it, and the first connection to every sandbox was trust-on-first-use. Before a start or a clone boots the guest, the provider now writes a fresh ed25519 host key into the stopped guest disk, in place so root ownership and modes stay, keeps the public half in the bundle, and pins it in known_hosts once the address is known. SSH then uses StrictHostKeyChecking=yes for that sandbox and refuses any other key. A guest disk that cannot be opened fails the start or clone instead of falling back to the shared key. A freshly installed image has no host key until Remote Login is enabled, so its first setup connection stays trust-on-first-use. Verified on a booted macOS guest: an existing sandbox rotated to a new key, a clone got a different one, each strict connection ran commands, and a wrong pin was refused. * ci: build the docs site on every change Nothing built apps/docs in CI, so a broken page or MDX change could only surface in a Vercel deploy. Add a docs job that installs with the locked pnpm version and runs the production build. * test(sandbox): cover the per-sandbox host key in the provider suite The start path now opens the stopped guest disk to install a host key, so the suite needs a mount stub and a fake disk clone. It also asserts the new contract: start writes the key and pins it, SSH verifies the pinned key, and a disk that cannot be opened fails the start without booting. * fix(sandbox): match the whole image path when detaching a guest disk hdiutil info prints the image path after a colon, and the detach lookup compared its third whitespace field, so a state root with a space in it never matched and the guest disk stayed mounted while the VM booted. Read the path from the whole line, and cover it with a suite path that contains a space. * fix(sandbox): refuse an uninstallable image before starting the install Apple's catalog can hold a single macOS version newer than the host, so the image lookup ran inside the background job and create answered working before failing with no image. Resolve the catalog entry in create, refuse with the host version and a local-image suggestion, and pass the chosen URL and SHA-1 to the job so it only downloads and verifies. Validate the guest user before the network read, so a bad user still writes nothing.