diff --git a/CHANGELOG.md b/CHANGELOG.md index 825c32f..db3db01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,10 @@ # Changelog + +## Unreleased + +### Added + +- Optional Lume macOS backend with identity-pinned clones, durable sandbox + metadata, snapshot recovery, SSH guest execution, and SFTP file transfer. +- Mac platform dispatch and the Tahoe desktop seed catalog entry. Deployment + qualification remains pending the server VM's source-restricted SSH setup. diff --git a/README.md b/README.md index de86221..aff232e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ images_file: images/catalog.yaml OVN requires the fleet-managed central, chassis TLS configuration, and physical uplink. To use the bridge fallback, set `default_network_kind: bridge` and configure `incus.host`. -Pass its path with `--config` or `AGENTCOMPUTE_CONFIG`. YAML and TOML are strict: unknown keys fail startup. Catalog and certificate paths resolve relative to the configuration file. +Pass its path with `--config` or `AGENTCOMPUTE_CONFIG`. YAML and TOML are strict: unknown keys fail startup. Catalog and credential paths resolve relative to the configuration file. Run the local STDIO transport: @@ -57,7 +57,9 @@ Run Streamable HTTP on its loopback default: go run ./cmd/agentcompute http --config agentcompute.yaml --addr localhost:8080 ``` -Both commands build shared Incus, catalog, compute, and CodeMode dependencies once. HTTP reuses them across sessions. Startup reconciles digest-pinned images; upstream `remote:alias` images are fetched on first use. +Both commands build shared backend, catalog, compute, and CodeMode dependencies once. HTTP reuses them across sessions. Startup reconciles digest-pinned Incus images; upstream `remote:alias` images are fetched on first use. Mac `seed:` entries remain host-local and bypass Incus reconciliation. + +For Mac guests, configure the optional Lume backend using the [macOS runbook](images/macos/README.md#configure-the-server). Create a sandbox with `platform="mac"` and select `macos/tahoe/desktop`. Mac guests use Lume's NAT network; cluster placement and network overrides are unsupported. For a local MCP client, build the binary and configure its absolute path: @@ -97,9 +99,9 @@ OVN instances may use an explicit online `host`. Without one, placement prefers An OVN network with `nat=false` is isolated: it has no direct outside path and consumes no external address. Attach a router instance or use `net.peer` for reachability. Isolated networks reject external forwards. Operator-management and OOB denies are immutable, including against broader user allow rules. -Restoring a snapshot stages a copy before deleting the current instance, then recreates it under the same agent-visible name and ownership. The Incus identity and NIC MAC can change, so the DHCP lease can change too; the original instance's snapshots are consumed. Low-level Incus access remains blocked. +On Incus, restoring a snapshot stages a copy before deleting the current instance, then recreates it under the same agent-visible name and ownership. The Incus identity and NIC MAC can change, so the DHCP lease can change too; the original instance's snapshots are consumed. Low-level Incus access remains blocked. Lume snapshots are stopped host-local clones: creating one temporarily stops a running guest, and restoring one retains the named snapshot for reuse. -Exec retains 64 KiB per stream while draining the rest. `stdout_truncated` and `stderr_truncated` report overflow. An exec-only timeout returns `timed_out=true`; caller cancellation remains an error. `user` accepts a numeric UID or `root`. macOS is not available. +Exec retains 64 KiB per stream while draining the rest. `stdout_truncated` and `stderr_truncated` report overflow. An exec-only timeout returns `timed_out=true`; caller cancellation remains an error. Linux `user` accepts a numeric UID or `root`. Mac exec defaults to `lume` (UID 501); other numeric UIDs and `root` require guest sudo access. Windows exec uses the Incus agent service identity and does not accept `user`. Only `main()`'s final converted value is returned. Intermediate capability results remain inside the worker and do not enter the model's context. diff --git a/go.mod b/go.mod index cd19f9a..2769425 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,12 @@ require ( github.com/meigma/codemode v0.2.1 github.com/modelcontextprotocol/go-sdk v1.7.0 github.com/pelletier/go-toml/v2 v2.4.3 + github.com/pkg/sftp v1.13.11 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.12.1 go.yaml.in/yaml/v3 v3.0.5 + golang.org/x/crypto v0.55.0 golang.org/x/image v0.31.0 ) @@ -51,7 +53,6 @@ require ( github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/opencontainers/umoci v0.6.1-0.20251213054154-70fc5ee1f4df // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/sftp v1.13.11 // indirect github.com/pkg/xattr v0.4.12 // indirect github.com/rogpeppe/go-internal v1.15.0 // indirect github.com/rootless-containers/proto/go-proto v0.0.0-20260207013450-f6ee952d53d9 // indirect @@ -77,7 +78,6 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.starlark.net v0.0.0-20260708150628-5395d018f003 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/crypto v0.55.0 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect diff --git a/images/catalog.yaml b/images/catalog.yaml index 981cf31..43799da 100644 --- a/images/catalog.yaml +++ b/images/catalog.yaml @@ -80,3 +80,17 @@ images: memory_mb: 4096 disk_gb: 100 alias: windows/server-2025-26100.32230 +# macOS images are host-local Lume seeds, not registry digests. The seed name +# is the stopped VM on the Mac host; clones inherit operator TCC consent. +- name: macos/tahoe/desktop + os: macos + version: '26.6.2' + platform: mac + kinds: + - vm + kind: vm + desktop: true + cpus: 4 + memory_mb: 8192 + disk_gb: 100 + seed: ac-seed-macos-tahoe-desktop diff --git a/images/macos/README.md b/images/macos/README.md index 81d427a..0e746d4 100644 --- a/images/macos/README.md +++ b/images/macos/README.md @@ -9,7 +9,7 @@ desktop-control credential. `provision.sh`, `verify.sh`, and `lib.sh` are shared by every train; each train directory holds only `image.yaml` and `unattended.yaml`. -## Status: built and qualified +## Seed qualification `macos/tahoe/desktop` exists and passes the gate on `studio-1` (Mac Studio M2 Max, 64 GB, macOS 26.6.2 / 25G83). The host is the owner's amendment to @@ -18,6 +18,10 @@ mini, with the backend confined to a dedicated `agentcompute` account. Migrating to a mini is moving that account's `~/.lume` directory and re-issuing one SSH key. +First-boot clones require the identity-pinning step below. The clone gate now +rejects Setup Assistant rather than skipping the desktop check. The local +host procedure is qualified; it is not evidence of a deployed MCP backend. + `macos/sequoia/desktop` is recorded but **not** the qualified train. Sequoia's last full restore image is 15.6.1 (24G90, 2025-08-20) — a year behind that train's security level — and its unattended install behaves no better than @@ -32,21 +36,226 @@ current, matches the host's own build, and has a publisher-independent digest. | Backend account | `agentcompute`, standard (not admin), hidden, member of `com.apple.access_ssh` | | VM store | `/Users/agentcompute/.lume` | | API | `lume serve --port 7777`, `/Library/LaunchDaemons/io.gilman.agentcompute.lume-serve.plist`, bound to `127.0.0.1` only | -| Server key | authorized only for `agentcompute`, `restrict,port-forwarding,permitopen="127.0.0.1:7777",command=/usr/local/libexec/agentcompute-no-shell` | -| Root-owned duplicate of those limits | `/etc/ssh/sshd_config.d/110-agentcompute.conf` (`Match User agentcompute`) | -| Guest key | `/Users/agentcompute/.ssh/guest_ed25519`; only the public half enters a guest | +| Approved server key policy | `from="",no-agent-forwarding,no-X11-forwarding`; normal shell and local TCP forwarding | +| Root-owned account policy | `/etc/ssh/sshd_config.d/110-agentcompute.conf` (`Match User agentcompute`) | +| Guest key | Per-image key on the server; the seed's provisioned key is `/Users/agentcompute/.ssh/guest_ed25519` on Studio | + +The dedicated account is the containment boundary, not a forwarding-only key. +It must remain hidden, standard, and unable to use sudo. The owner's home must +deny traversal by this account; a `staff`-readable home is insufficient because +both accounts belong to `staff`. + +The approved SSH policy below is **not installed yet**: the existing server +VM's identity and source IP are still needed. The old forced-command policy +remains active. The account's hidden/non-admin status, failed `sudo -n true`, +and owner-home denial were verified; same-key rejection from a different source +has not been exercised. + +## Authorize the remote transport + +First verify the agentcompute server VM's tailnet IPv4 address. Restrict its +public key in `~agentcompute/.ssh/authorized_keys`: + +```text +from="",no-agent-forwarding,no-X11-forwarding ssh-ed25519 agentcompute server +``` + +Replace the forwarding-only root drop-in with the following policy, substituting +that same verified address. Keep it owned by `root:wheel`, mode `0644`. + +```text +Match User agentcompute + AllowUsers agentcompute@ + AuthenticationMethods publickey + PasswordAuthentication no + KbdInteractiveAuthentication no + AllowTcpForwarding local + PermitTunnel no + X11Forwarding no + AllowAgentForwarding no + GatewayPorts no +Match all +``` + +Remove `ForceCommand` and `PermitOpen`; do not retain `restrict` on the key. +The root-owned source and forwarding restrictions prevent the account from +widening its access by editing `authorized_keys`. Password and keyboard-interactive +authentication must stay disabled for this account. + +Before opening a new connection, run `sudo sshd -t` and inspect +`sudo sshd -T -C user=agentcompute,host=studio-1,addr=`. +Keep an operator session available during the change. From the server, verify +normal shell access and that `sudo -n true` fails. Check that the account cannot +read or traverse subdirectories of `/Users/josh`; on Studio the home directory +is owner-only (`0700`). Attempt authentication with the **same key** from a +different source address and require refusal. A failure caused by the tailnet +ACL alone does not prove the SSH source restriction. + +Lifecycle remains HTTP over the SSH connection to `127.0.0.1:7777`. Guest exec +and file transfer use system SSH through `ssh -J agentcompute@studio-1` to +the NAT lease, with a separate per-image guest key. Files use the SFTP subsystem +over that connection. Verify both SSH host keys; +do not forward an agent or install private keys in a guest. Host metadata uses +same-directory temporary files followed by `mv`; identity pinning edits a +stopped clone's configuration before its first run. + +Lume 0.5.3 has no HTTP routes for guest exec, sidecar I/O, or setting a clone's +machine identifier. Its clone request always regenerates the identifier. + +Source: Lume 0.5.3's [HTTP routes](https://github.com/trycua/cua/blob/754eec754991e1760100621e9bfe7ec1395cc7db/libs/lume/src/Server/Server.swift#L203-L401), +[clone implementation](https://github.com/trycua/cua/blob/754eec754991e1760100621e9bfe7ec1395cc7db/libs/lume/src/LumeController.swift#L296-L386), +and [host-status handler](https://github.com/trycua/cua/blob/754eec754991e1760100621e9bfe7ec1395cc7db/libs/lume/src/Server/Handlers.swift#L901-L940). + +## Configure the server + +Add this section to the server's existing Incus configuration. Use Studio's +tailnet **IPv4** address so the connection uses the source family authorized +above; do not use either LAN address. Replace the example credential paths +with files owned by the server's service account. + +```yaml +lume: + host: + identity_file: /etc/agentcompute/ssh/studio_ed25519 + known_hosts_file: /etc/agentcompute/ssh/known_hosts + guest_known_hosts_file: /etc/agentcompute/ssh/guest_known_hosts + guest_keys: + macos/tahoe/desktop: /etc/agentcompute/ssh/tahoe_ed25519 +``` + +Private keys must be readable only by the service account. The host key and +guest key are distinct. Key-map entries use catalog image names, not VM names. +Relative paths resolve beside the server configuration file. + +Verify Studio's SSH host key through operator access, and the guest host key +through the qualified seed. The host known-hosts file identifies Studio's +tailnet address. The guest known-hosts file identifies the **seed name**: + +```text +ac-seed-macos-tahoe-desktop ssh-ed25519 +``` + +Clones retain that SSH host key while their NAT leases change. Do not enroll +each new lease with `StrictHostKeyChecking=no` or trust an unverified +`ssh-keyscan` result. The generated jump configuration selects the host and +guest identities separately; neither needs an SSH agent. + +The catalog's `seed:` value refers to the stopped VM in this account's Lume +store. Mac entries bypass Incus image reconciliation. Omitting the `lume` +section leaves the Incus-only runtime available. + +Keep Lume's LaunchDaemon stdout and stderr at +`/Users/agentcompute/lume-serve.out.log` and +`/Users/agentcompute/lume-serve.err.log`. The capacity failure check reads only +new log bytes from the attempted run. Keep sandbox sidecars under +`/Users/agentcompute/.agentcompute/sandboxes`; do not discard them when +restarting or upgrading the server, since they are also the reaper's ownership +records. + +## Verify the backend + +Local checks pass with Go 1.26.6: repository lint, `go test -race ./...`, +`go build ./...`, a Linux/amd64 build with `CGO_ENABLED=0`, and the compiled +artifact's offline startup smoke check. Transport tests use real system SSH +with ProxyJump and a native SFTP server; the Lume HTTP service is a fixture. +These checks do not qualify a deployed MCP service. -`restrict` alone does **not** stop command execution — a key with -`restrict,port-forwarding,permitopen=…` and no forced command still runs -`ssh host `. The forced command is what makes it forwarding-only, and -the `sshd_config.d` drop-in repeats every limit somewhere the account cannot -edit (it owns its own `authorized_keys`, and `lume serve` runs as it). +After installing and proving the source-restricted SSH policy, run the live +backend lane from the authorized server VM. Set `STUDIO_TAILNET_IP` to Studio's +verified tailnet IPv4 address and adjust the credential paths to match its +configuration: + +```sh +AGENTCOMPUTE_TEST_LUME=1 \ +AGENTCOMPUTE_TEST_LUME_HOST="$STUDIO_TAILNET_IP" \ +AGENTCOMPUTE_TEST_LUME_IDENTITY_FILE=/etc/agentcompute/ssh/studio_ed25519 \ +AGENTCOMPUTE_TEST_LUME_KNOWN_HOSTS=/etc/agentcompute/ssh/known_hosts \ +AGENTCOMPUTE_TEST_LUME_GUEST_KNOWN_HOSTS=/etc/agentcompute/ssh/guest_known_hosts \ +AGENTCOMPUTE_TEST_LUME_GUEST_KEY=/etc/agentcompute/ssh/tahoe_ed25519 \ +go test ./internal/lume -run '^TestLiveLumeLifecycle$' -count=1 -timeout=25m +``` + +The lane creates a uniquely named sandbox and guest, waits for desktop/Driver +readiness, checks guest execution, and deletes the sandbox on cleanup. It is +a backend API check, not a substitute for deployed MCP acceptance. + +Deployed qualification remains pending: create without Setup Assistant, +`sw_vers`, app listing, screenshot retrieval, snapshot restore, restart +rediscovery and TTL deletion, unsupported-network AgentErrors, two concurrent +guests and third-guest refusal. Creation and screenshot service timings have +not been measured. PF and tailnet ACLs have not been changed. + +## Restrict VNC before serving workers + +Lume's VNC listener uses an ephemeral TCP port, not port 5900. The observed +host range is 49152–65535 (`sysctl net.inet.ip.portrange.first +net.inet.ip.portrange.last`). `pf.anchor` admits that range only through +loopback and the current Tailscale interface. + +**Impact:** this range also contains non-Lume applications. Review +`sudo lsof -nP -iTCP -sTCP:LISTEN` before applying it. Do not apply it if +another application requires LAN access in this range; resolve that conflict +with the owner first. These rules were syntax-checked, not installed during +the identity experiment. + +1. Stop all worker VMs through the HTTP API so no pre-existing VNC states + survive the policy change. Leave the seed stopped. +2. Find the current Tailscale interface with + `route -n get `; use its `interface` value below. + It was `utun9` during the experiment, but the number can change. +3. Validate and install only the anchor file: + + ```sh + tailnet_if=utun9 # replace with the observed interface + sudo pfctl -n -D "tailnet_if=$tailnet_if" -f images/macos/pf.anchor + sudo install -o root -g wheel -m 0644 images/macos/pf.anchor \ + /etc/pf.anchors/io.gilman.agentcompute + ``` + +4. Back up `/etc/pf.conf`. Add `anchor "io.gilman.agentcompute" quick` + immediately before its existing `anchor "com.apple/*"` filter anchor. + Preserve every existing scrub, NAT, redirect, and Apple anchor. Compare + the live root rules (`sudo pfctl -sr`, `sudo pfctl -sn`) with the file: + reloading it can discard dynamically inserted root rules. If they differ, + stop and reconcile with the owner rather than losing another service's + rules. Check `sudo pfctl -nf /etc/pf.conf` before the maintenance-window + reload with `sudo pfctl -f /etc/pf.conf`. Never use a global flush. +5. Load the child rules: + + ```sh + sudo pfctl -a io.gilman.agentcompute \ + -D "tailnet_if=$tailnet_if" -f /etc/pf.anchors/io.gilman.agentcompute + sudo pfctl -a io.gilman.agentcompute -sr + sudo pfctl -s info + ``` + + If PF is disabled, enable it with `sudo pfctl -E` and retain its reference + token. Never use `pfctl -d`: other host services also depend on PF. +6. Start one disposable worker. Read its actual `vncUrl` port and verify a + fresh TCP connection from loopback and an allowed tailnet peer succeeds, + while fresh connections to both LAN addresses fail. Also test IPv6 if a + wildcard IPv6 listener exists. Stop the worker after checking. + +The child rules must be reloaded after a reboot or Tailscale interface +change, **before** starting workers. The main anchor declaration alone does +not load them. An unattended host boot loader is not installed by this +procedure. If the interface cannot be identified or the anchor is absent, +do not serve workers. + +Rollback: stop workers, then unload only this anchor with +`sudo pfctl -a io.gilman.agentcompute -F rules`. Release only the PF reference +token acquired by this procedure with `sudo pfctl -X `. Restore the +saved main configuration if its declaration must also be removed. + +Upstream bind-address request: +[trycua/cua#3878](https://github.com/trycua/cua/issues/3878). ## Build a seed -All commands run on the Mac host from a checkout of this repository. The pin -reader is the recipe's own, so a runbook command cannot disagree with -`pins.lock.yaml`: +Run the seed procedures on the Mac from a checkout readable by `agentcompute`, +for example under `/Users/agentcompute/src/agentcompute`, not the owner's +protected home. All relative paths below refer to that checkout. The pin reader +is the recipe's own, so a command cannot disagree with `pins.lock.yaml`: ```sh pins=images/macos/pins.lock.yaml @@ -182,9 +391,10 @@ sudo -u agentcompute -H images/macos/verify.sh \ --identity /Users/agentcompute/.ssh/guest_ed25519 ``` -All ten checks must pass (6 s measured). `--identity` exercises the scp pull -the server will use; without it that leg is skipped and the run is not a full -gate. Reboot the seed once and re-run it before declaring the seed done, then +All ten checks must pass (6 s measured). `--identity` exercises an independent +SCP pull of the screenshot; the backend uses SFTP over the same SSH jump path. +Without it that leg is skipped and the run is not a full gate. Reboot the seed +once and re-run it before declaring the seed done, then stop the VM and leave it stopped. ### 8. Clone smoke @@ -192,17 +402,103 @@ stop the VM and leave it stopped. ```sh curl -sS -X POST http://127.0.0.1:7777/lume/vms/clone -H 'Content-Type: application/json' \ -d '{"name":"ac-seed-macos-tahoe-desktop","newName":"ac-smoke-1"}' +``` + +Before the clone's **first boot**, copy only `machineIdentifier` from the +stopped seed. Keep the clone's generated MAC; copying the MAC would create a +network collision. Run this as the `agentcompute` account, either locally or +through its source-restricted SSH shell: + +```sh +sudo -u agentcompute -H /bin/bash -euo pipefail <<'PIN' +store="$HOME/.lume" +seed="$store/ac-seed-macos-tahoe-desktop/config.json" +clone="$store/ac-smoke-1/config.json" +identifier=$(/usr/bin/plutil -extract machineIdentifier raw -o - "$seed") +tmp=$(/usr/bin/mktemp "$store/ac-smoke-1/.config.XXXXXX") +trap 'rm -f "$tmp"' EXIT +cp "$clone" "$tmp" +/usr/bin/plutil -replace machineIdentifier -string "$identifier" "$tmp" +jq -e --arg id "$identifier" '.machineIdentifier == $id' "$tmp" >/dev/null +mv -f "$tmp" "$clone" +PIN +``` + +The rename replaces the configuration atomically on the same filesystem. +Never run it while the clone is running. Then start through HTTP: + +```sh curl -sS -X POST http://127.0.0.1:7777/lume/vms/ac-smoke-1/run -H 'Content-Type: application/json' \ -d '{"noDisplay":true}' +``` + +Wait until `GET /lume/vms/ac-smoke-1` reports a NAT address and SSH accepts +connections. Start the already-installed Driver LaunchAgent if it is pending; +this does not grant or alter TCC permissions: + +```sh +ip=$(curl -fsS http://127.0.0.1:7777/lume/vms/ac-smoke-1 | jq -er .ipAddress) +sudo -u agentcompute ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ + -i /Users/agentcompute/.ssh/guest_ed25519 "lume@$ip" \ + 'launchctl kickstart gui/$(id -u)/com.trycua.cua_driver_daemon' sudo -u agentcompute -H images/macos/verify.sh --vm ac-smoke-1 --clone \ --recipe images/macos/tahoe/desktop/image.yaml \ --identity /Users/agentcompute/.ssh/guest_ed25519 ``` -Measured over four clone cycles: clone 2–3 s, boot to NAT address 10–11 s, -Driver answering with both grants about 30 s, `verify.sh --clone` 6–7 s, host -allocation ~26.5 GB per running clone (APFS copy-on-write; the seed's own -allocation does not change). No clone ever asked for consent again. +The earlier unpinned measurements were clone 2–3 s, NAT address 10–11 s, +and host allocation about 26.5 GB per clone. Those runs skipped the wizard +check and are not desktop-readiness measurements. + +On 2026-09-15, a fresh pre-boot-pinned clone and a second pinned clone both +ran concurrently, kept their distinct MACs, retained both TCC grants, and +passed the strict gate after the Driver kickstart. Screenshots showed the +desktop, not Setup Assistant. A fresh unpinned control still showed the +wizard: the old gate exited 0, while the corrected gate exited 1. +The comparison and gate output are retained in +[`spikes/lume/identity.json`](../../spikes/lume/identity.json). + +The pinned UUID matched the seed. System loginwindow and system/user Setup +Assistant plists were identical in the initial seed/control comparison; +the clone had acquired an additional UUID-named ByHost loginwindow plist. +Changing only `machineIdentifier` restored the seed's guest UUID and serial. +No dismissal agent or TCC database modification was used. Rebooting an +already-booted unpinned clone also cleared the wizard in one experiment: +use **fresh first-boot controls**, not a reboot alone, to qualify pinning. + +Both pinned and unpinned clones sometimes left the Driver LaunchAgent in +`pended nondemand spawn = speculative`, with zero runs. `launchctl kickstart` +started the existing app-owned daemon; both grants then remained true. + +A subsequent disposable-clone investigation found a second seed LaunchAgent, +`io.gilman.agentcompute.cua-driver`, which runs `open -g -a`. The canonical job +sometimes exits with `daemon already running`, while the daemon belongs to an +`application.com.trycua.driver.*` launchd job. Removing the stale launcher from +that clone and rebooting did not eliminate this behavior. Both grants remained +true after kickstart. The cold-boot scheduling cause remains unresolved; this +experiment did not modify the seed or grant new permissions. + +## Host-wide guest-count rule + +Before a create/start request makes any Lume HTTP call, inspect the dedicated +account's inventory with `/usr/local/bin/lume ls --format json`. Count running +**macOS** guests regardless of name, including a running seed, and serialize +pending starts. Refuse a third Lume-owned running macOS guest: + +```text +macOS guest limit reached (2 per host); other accounts may also occupy slots +``` + +This inventory is not a host-wide authority: another account's VM is invisible +to it. Apple enforces the host-wide limit. If HTTP accepts a run but the VM +stays stopped and the daemon's new log output reports the limit, return an +explicit error naming Apple's two-guest limit and the possibility that another +account holds a slot. Do not mistake an old log entry for this run's failure, +or report Running from HTTP's `202` response alone. + +Start and restart require the same gate. Reaper rediscovery must not start a +stopped guest merely because its sidecar exists. +See [trycua/cua#3880](https://github.com/trycua/cua/issues/3880). ## Measurements @@ -222,15 +518,10 @@ allocation does not change). No clone ever asked for consent again. ## Findings -1. **A clone re-runs Setup Assistant.** The seed stays clean across its own - reboots once `AccountInfo:FirstLogins:lume` is raised in - `/Library/Preferences/com.apple.loginwindow.plist`, but every clone boots - into the Apple Account step again, and killing it logs the session out. The - Driver is unaffected — grants, accessibility tree and capture all pass — so - `verify.sh --clone` skips only the desktop-session check. An agent handed - such a clone sees the wizard, so this is a real defect for the desktop - image, not a cosmetic one. The documented `com.apple.SetupAssistant` - `DidSee*` keys do not suppress it on 26.6.2. +1. **Clone identity triggers first-boot Setup Assistant.** Pinning the seed's + `machineIdentifier` before first boot removed the wizard while preserving + the clone's generated MAC. The strict `verify.sh --clone` gate always + checks the desktop. See the clone smoke procedure and its control evidence. 2. **Lume's API accepts a third macOS guest and silently does nothing.** `POST /lume/vms//run` returns `202 {"status":"pending"}`; the VM stays `stopped` and only `lume serve`'s log carries `The number of virtual @@ -240,7 +531,7 @@ allocation does not change). No clone ever asked for consent again. and the guest re-parses the result, so `lume ssh vm /bin/bash -c '