@@ -20,22 +20,53 @@ disk image to ghcr; a thin Go CLI clones that image and boots VMs from it.
2020 (serial/MLB/UUID/ROM, with the guest NIC MAC set to the ROM) into a per-VM OpenCore so clones
2121 don't all boot as the shipped placeholder serial. Confirmed in-guest via ` system_profiler ` —
2222 two clones get two distinct serials, each matching what was injected.
23+ - ** Image management** (` cocoon-macos image pull|list|inspect|rm ` ): pulls the golden qcow2 from
24+ ghcr into a content-addressed local store — cocoon's ` cloudimg ` backend imported directly — so
25+ ` vm run <ref> ` bakes a CoW overlay on the immutable shared base. ** qcow2-only** (no OCI image
26+ layers; ` oras ` is just the ghcr transport for the qcow2 blob).
27+ - ** Networking** (` --net user|tap|cni|bridge ` ): user-mode SLIRP + ` --ssh-port ` hostfwd by default;
28+ ` tap ` /` bridge ` /` cni ` auto-create a host TAP via cocoon's network plane (imported ` network/bridge `
29+ + ` network/cni ` ), so macOS VMs join the ** same** bridge/CNI forwarding plane as cocoon's CH/FC
30+ VMs on the node. The guest MAC stays = SMBIOS ROM. Auto-create is Linux-only (CAP_NET_ADMIN);
31+ user-mode + a pre-created ` --tap ` work everywhere.
32+ - ** Snapshot / restore / clone** (` vm snapshot|restore|clone ` ): offline qcow2-internal snapshots
33+ (` qemu-img snapshot ` , VM stopped — ` -cpu +invtsc ` blocks live RAM snapshot, so this is disk-state
34+ only) and CoW clones that cold-boot a ** fresh** Apple identity, so two clones never share a serial/MAC.
2335
2436## CLI
2537
2638``` bash
2739go build -o cocoon-macos .
2840
29- # clone the golden image into a per-VM overlay and boot it (x86 Linux + /dev/kvm)
30- cocoon-macos vm run ghcr-pulled-tahoe.qcow2 \
31- --name m1 --cpus 4 --memory 8192 --ssh-port 2222 --vnc 1 --random-smbios \
32- --opencore OpenCore.qcow2 --ovmf-code OVMF_CODE_4M.fd --ovmf-vars OVMF_VARS.fd
41+ # pull the golden qcow2 from ghcr into the local store (cocoon cloudimg; ~/.cocoon-macos)
42+ cocoon-macos image pull ghcr.io/cocoonstack/cocoon-macos/tahoe:26
43+ cocoon-macos image list # also: inspect, rm
44+
45+ # install the shared OpenCore loader + OVMF firmware ONCE into <state-dir>/firmware (reused by
46+ # every VM); afterwards vm create/run default to these, so --opencore/--ovmf-* become optional
47+ cocoon-macos firmware install --opencore OpenCore.qcow2 --ovmf-code OVMF_CODE_4M.fd --ovmf-vars OVMF_VARS.fd
48+ cocoon-macos firmware list
49+
50+ # clone the golden image into a per-VM overlay and boot it (x86 Linux + /dev/kvm).
51+ # IMAGE is a store ref (above) or a direct qcow2 path; firmware defaults to the install above.
52+ cocoon-macos vm run ghcr.io/cocoonstack/cocoon-macos/tahoe:26 \
53+ --name m1 --cpus 4 --memory 8192 --ssh-port 2222 --vnc 1 --random-smbios
3354
3455cocoon-macos vm list # JSON of all VMs
3556cocoon-macos vm inspect m1
3657cocoon-macos vm stop m1
3758cocoon-macos vm rm m1
3859# also: create (no boot), start, console
60+
61+ # networking — join the host's bridge/CNI plane instead of user-mode SLIRP (Linux):
62+ cocoon-macos vm run < IMAGE> --net bridge --bridge br0 --random-smbios … # auto-creates a TAP on br0
63+ cocoon-macos vm run < IMAGE> --net cni --random-smbios … # CNI: TAP in a netns
64+ cocoon-macos vm run < IMAGE> --net tap --tap tap0 … # use a pre-created TAP verbatim
65+
66+ # snapshot / restore / clone (VM stopped for snapshot/restore; clone gets a unique identity):
67+ cocoon-macos vm snapshot m1 --tag clean
68+ cocoon-macos vm restore m1 --tag clean # --force to stop+restore+relaunch a running VM
69+ cocoon-macos vm clone m1 -n m2 --ssh-port 2223 --random-smbios
3970```
4071
4172` vm run ` does: ` qemu-img create -b <golden> overlay.qcow2 ` (instant CoW clone) → copy a
@@ -53,6 +84,13 @@ Screen Sharing hangs on**. Pass `--vnc-password <≤8 chars>` to start QEMU with
5384(set via the monitor post-launch) so Screen Sharing prompts and connects; tunnel first with
5485` ssh -L 5901:127.0.0.1:5901 <host> ` . Plain VNC clients (RealVNC/TigerVNC) work without a password.
5586
87+ ** Display sleep blanks VNC.** macOS only repaints the emulated framebuffer while the display is
88+ awake; once it sleeps (~ idle), VNC shows a blank ** white/black** screen with just the cursor even
89+ though the guest is healthy (SSH works, WindowServer is up). It is * not* a GPU/driver problem — a
90+ mouse move repaints it, and the full Tahoe desktop renders fine (Finder/Dock/menu bar). The golden
91+ image's first-boot daemon now runs ` pmset -a displaysleep 0 disablesleep 1 ` system-wide (covers the
92+ pre-login loginwindow) so the framebuffer stays painted; older images need a ` setup ` -stage rebuild.
93+
5694## CI image pipeline (` .github/workflows/build-macos-image.yml ` , ` scripts/build-qemu-macos.sh ` )
5795
5896` workflow_dispatch ` with ` stage ` :
@@ -76,7 +114,13 @@ runs in the Recovery Terminal against the installed Data volume (`dscl -f` offli
76114Key host facts: GitHub ` ubuntu-latest ` exposes ` /dev/kvm ` (needs ` chmod 666 ` ); macOS Tahoe 26
77115is the last Intel-supporting macOS, so this x86 path has a finite shelf life.
78116
79- ## Boot-to-desktop (WIP — blocked on the macOS 26 Setup Assistant)
117+ ## Boot-to-desktop (GUI renders; auto-skip of the Setup Assistant is the remaining WIP)
118+
119+ The full Tahoe desktop ** does render over VNC** — testbed-verified: boot → login window (the ` cocoon `
120+ user) → type ` cocoon ` → Finder + Dock + menu bar + desktop widgets, all repainting normally. The
121+ earlier "white/black VNC" was purely ** display sleep** (see the VNC note above), now fixed at the
122+ image level. What's left for a * fully unattended* boot-to-desktop is auto-skipping the first-run
123+ Setup Assistant + auto-login.
80124
81125The ` desktop ` build stage + ` provision-macos.sh ` aim to make ` :26 ` boot straight to ` cocoon ` 's
82126desktop (auto-login, no Setup Assistant). The ** post-SA recipe is validated** (proven on a testbed
@@ -98,9 +142,47 @@ VZ on Apple Silicon caps ~2 macOS VMs/host and can't use the App Store; QEMU + O
98142has neither limit (at the cost of per-VM identity + Apple-ID ban risk at fleet scale). See the
99143deep-research notes that motivated this project.
100144
145+ ## E2E regression (` scripts/e2e.sh ` )
146+
147+ A testbed lifecycle regression (modeled on cocoon's), two tiers:
148+
149+ - ** ` [DUMMY] ` ** — file-level lifecycle on a tiny throwaway qcow2, ** no macOS boot** : image store
150+ (` pull ` -less ` list ` /` rm ` ), ` vm create ` overlay-on-shared-base, ` --random-smbios ` serial/MAC
151+ uniqueness + MAC==ROM, ` snapshot ` /` restore ` (qcow2-internal tag rollback), ` clone ` (CoW on the
152+ shared base + distinct identity + clone-of-clone backing), ` stop ` PID-reuse-safe teardown,
153+ ` --net bridge ` auto-TAP create + ` tap_owned ` + teardown-on-` rm ` + user-` --tap ` preservation +
154+ negative paths — with post-conditions asserting no leaked TAPs/procs and backing-chain integrity.
155+ Runs on any x86 Linux/KVM host; the ` --random-smbios ` rows need ` root ` + ` nbd ` + a real
156+ ` CM_OPENCORE ` , the ` --net ` rows need ` root ` + a test bridge (they ` SKIP ` , not ` FAIL ` , otherwise).
157+ - ** ` [REAL] ` ** (` --real ` ) — boots ` tahoe:26 ` from the store, passes the OpenCore picker over the HMP
158+ monitor, asserts SSH-ready (` sw_vers ` 26.x) + in-guest serial == injected. Testbed-only.
159+
160+ ``` bash
161+ sudo CM_BIN=./cocoon-macos CM_OPENCORE=.../OpenCore.qcow2 ./scripts/e2e.sh # [DUMMY] (30 rows)
162+ sudo CM_HOME=~ /cm-demo CM_OPENCORE=... CM_OVMF_CODE=... CM_OVMF_VARS=... ./scripts/e2e.sh --real-only
163+ ```
164+
165+ The ` [DUMMY] ` tier is the behavioral gate today's CI structurally can't give (CI is pure `go
166+ test` / ` vet` /build — nothing opens ` /dev/kvm` or mutates netlink); it belongs on a privileged
167+ self-hosted KVM runner. ` [REAL] ` stays a manual/nightly testbed run (~ 15GB + cold boot).
168+
169+ ## Roadmap
170+
171+ - ** Port cocoon's bypass/passthrough surface.** cocoon exposes extra VM hardware that cocoon-macos
172+ doesn't yet wire to QEMU; port it the same way as networking (Linux-tagged, reusing cocoon types):
173+ - ** Multi-disk / data disks** — cocoon's ` types.DataDiskSpec ` + ` StorageRole ` (` data ` /` cow ` /…) and
174+ ` --data-disk ` . Maps to extra QEMU ` -drive ` /` -device virtio-blk ` per disk (attach/detach lifecycle).
175+ - ** Hardware passthrough (VFIO)** — cocoon's ` extend/vfio ` (` vfio.Spec ` /` Attacher ` , BDF/sysfs) and
176+ ` cmd/vm attach ` . Maps to QEMU ` -device vfio-pci,host=<BDF> ` for GPU/NIC/etc. passthrough.
177+ - ** GUI boot-to-desktop** — finish the SA click-through (below).
178+ - ** Live snapshot** is intentionally * not* on the roadmap: ` -cpu +invtsc ` makes the macOS guest
179+ non-migratable, so resume-from-RAM can't work; snapshot stays offline/disk-only + cold clone.
180+
101181## Out of scope (v0.x)
102182
103- cocoon engine integration (a ` qemu ` Hypervisor backend in cocoon) is a separate later phase.
104- Per-VM SMBIOS injection (` --random-smbios ` ) is implemented + testbed-verified, but registering
105- those identities with iServices/App Store is the consumer's policy concern — it needs validated
106- (not just unique) serials and carries Apple-ID ban risk at fleet scale — so it is not done here.
183+ A ` qemu ` Hypervisor backend * inside* cocoon (so cocoon's own ` cmd/core ` dispatches macOS VMs) is a
184+ separate later phase — cocoon-macos currently ** imports** cocoon's libraries (` cloudimg ` , ` network ` ,
185+ storage/CoW conventions) rather than the reverse. Per-VM SMBIOS injection (` --random-smbios ` ) is
186+ implemented + testbed-verified, but registering those identities with iServices/App Store is the
187+ consumer's policy concern — it needs validated (not just unique) serials and carries Apple-ID ban
188+ risk at fleet scale — so it is not done here.
0 commit comments