{"id":51022779,"url":"https://github.com/cocoonstack/cocoon-macos","last_synced_at":"2026-06-21T17:01:30.829Z","repository":{"id":361546468,"uuid":"1254870029","full_name":"cocoonstack/cocoon-macos","owner":"cocoonstack","description":"Run full macOS (Tahoe 26) as a QEMU/KVM guest on x86 Linux — image-build automation + thin cocoon-style CLI","archived":false,"fork":false,"pushed_at":"2026-05-31T07:08:02.000Z","size":18,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-31T07:12:25.715Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cocoonstack.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T05:14:59.000Z","updated_at":"2026-05-31T07:08:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cocoonstack/cocoon-macos","commit_stats":null,"previous_names":["cocoonstack/cocoon-macos"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/cocoonstack/cocoon-macos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cocoonstack%2Fcocoon-macos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cocoonstack%2Fcocoon-macos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cocoonstack%2Fcocoon-macos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cocoonstack%2Fcocoon-macos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cocoonstack","download_url":"https://codeload.github.com/cocoonstack/cocoon-macos/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cocoonstack%2Fcocoon-macos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34618484,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-21T17:01:25.695Z","updated_at":"2026-06-21T17:01:30.824Z","avatar_url":"https://github.com/cocoonstack.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cocoon-macos\n\nRun **full macOS (Tahoe 26)** as a fully-virtualized **QEMU/KVM** guest on **x86 Linux** —\nbuilt entirely by CI. A GitHub Action installs macOS from scratch and publishes a golden\ndisk image to ghcr; a thin Go CLI clones that image and boots VMs from it.\n\n## What works (proven in CI on GitHub Actions `ubuntu-latest`)\n\n- **Fully-automated macOS Tahoe 26 install** (`stage=install`, ~50 min): OpenCore boot →\n  `diskutil` erase (APFS) → OCR/keyboard-driven installer click-through → ~15 GB download +\n  install → `RequestBootVarRouting` makes the install reboots auto-continue → Setup Assistant.\n- **Golden images on ghcr:**\n  - `ghcr.io/cocoonstack/cocoon-macos/tahoe:26-base` — installed macOS Tahoe 26 at first-run Setup Assistant.\n  - `ghcr.io/cocoonstack/cocoon-macos/tahoe:26` — **SSH-ready**: provisioned via Recovery Terminal\n    (admin user `cocoon`/`cocoon`, a complete home, Remote Login/SSH on first boot). SSH + VNC login\n    work (verify stage confirms `cocoon@…`, macOS 26.5). **Known limitation:** the GUI first boot\n    still lands at the macOS 26 system Setup Assistant — see *Boot-to-desktop (WIP)* below.\n- **CLI** (`cocoon-macos vm …`) clones a golden image (copy-on-write qcow2 overlay) and launches QEMU.\n- **Per-VM identity** (`--random-smbios`, testbed-verified): injects a unique Apple SMBIOS\n  (serial/MLB/UUID/ROM, with the guest NIC MAC set to the ROM) into a per-VM OpenCore so clones\n  don't all boot as the shipped placeholder serial. Confirmed in-guest via `system_profiler` —\n  two clones get two distinct serials, each matching what was injected.\n- **Image management** (`cocoon-macos image pull|list|inspect|rm`): pulls the golden qcow2 from\n  ghcr into a content-addressed local store — cocoon's `cloudimg` backend imported directly — so\n  `vm run \u003cref\u003e` bakes a CoW overlay on the immutable shared base. **qcow2-only** (no OCI image\n  layers; `oras` is just the ghcr transport for the qcow2 blob).\n- **Networking** (`--net user|tap|cni|bridge`): user-mode SLIRP + `--ssh-port` hostfwd by default;\n  `tap`/`bridge`/`cni` auto-create a host TAP via cocoon's network plane (imported `network/bridge`\n  + `network/cni`), so macOS VMs join the **same** bridge/CNI forwarding plane as cocoon's CH/FC\n  VMs on the node. The guest MAC stays = SMBIOS ROM. Auto-create is Linux-only (CAP_NET_ADMIN);\n  user-mode + a pre-created `--tap` work everywhere.\n- **Snapshot / restore / clone** (`vm snapshot|restore|clone`): offline qcow2-internal snapshots\n  (`qemu-img snapshot`, VM stopped — `-cpu +invtsc` blocks live RAM snapshot, so this is disk-state\n  only) and CoW clones that cold-boot a **fresh** Apple identity, so two clones never share a serial/MAC.\n\n## CLI\n\n```bash\ngo build -o cocoon-macos .\n\n# pull the golden qcow2 from ghcr into the local store (cocoon cloudimg; /var/lib/cocoon-macos)\ncocoon-macos image pull ghcr.io/cocoonstack/cocoon-macos/tahoe:26\ncocoon-macos image list        # also: inspect, rm\n\n# install the shared OpenCore loader + OVMF firmware ONCE into \u003cstate-dir\u003e/firmware (reused by\n# every VM); afterwards vm create/run default to these, so --opencore/--ovmf-* become optional\ncocoon-macos firmware install --opencore OpenCore.qcow2 --ovmf-code OVMF_CODE_4M.fd --ovmf-vars OVMF_VARS.fd\ncocoon-macos firmware list\n\n# clone the golden image into a per-VM overlay and boot it (x86 Linux + /dev/kvm).\n# IMAGE is a store ref (above) or a direct qcow2 path; firmware defaults to the install above.\ncocoon-macos vm run ghcr.io/cocoonstack/cocoon-macos/tahoe:26 \\\n  --name m1 --cpus 4 --memory 8192 --ssh-port 2222 --vnc 1 --random-smbios\n\ncocoon-macos vm list           # JSON of all VMs\ncocoon-macos vm inspect m1\ncocoon-macos vm stop m1\ncocoon-macos vm rm m1\n# also: create (no boot), start, console\n\n# networking — join the host's bridge/CNI plane instead of user-mode SLIRP (Linux):\ncocoon-macos vm run \u003cIMAGE\u003e --net bridge --bridge br0 --random-smbios  …   # auto-creates a TAP on br0\ncocoon-macos vm run \u003cIMAGE\u003e --net cni    --random-smbios  …               # CNI: TAP in a netns\ncocoon-macos vm run \u003cIMAGE\u003e --net tap    --tap tap0       …               # use a pre-created TAP verbatim\n\n# snapshot / restore / clone (VM stopped for snapshot/restore; clone gets a unique identity):\ncocoon-macos vm snapshot m1 --tag clean\ncocoon-macos vm restore  m1 --tag clean          # --force to stop+restore+relaunch a running VM\ncocoon-macos vm clone    m1 -n m2 --ssh-port 2223 --random-smbios\n```\n\n`vm run` does: `qemu-img create -b \u003cgolden\u003e overlay.qcow2` (instant CoW clone) → copy a\nper-VM `OVMF_VARS` → launch `qemu-system-x86_64` (validated OSX-KVM recipe in `qemu/launch.go`:\nSkylake-Client CPU spoofing GenuineIntel + `isa-applesmc` OSK + OVMF + OpenCore + the macOS\nqcow2) daemonized, recording state under `--state-dir` / `$COCOON_MACOS_HOME` (default `/var/lib/cocoon-macos`, mirroring cocoon's `/var/lib/cocoon`).\n\nWith `--random-smbios`, `create` also copies OpenCore per-VM and injects a generated identity\ninto its `config.plist` `PlatformInfo/Generic` (via `qemu-nbd` mount); the model stays `iMac19,1`\n(proven to boot Tahoe) and only serial/MLB/UUID/ROM are randomized. The assigned identity is\nrecorded and shown by `vm inspect`.\n\nQEMU's VNC is loopback-only (`127.0.0.1:590\u003cvnc\u003e`) and offers `None` auth, which **macOS\nScreen Sharing hangs on**. Pass `--vnc-password \u003c≤8 chars\u003e` to start QEMU with `password=on`\n(set via the monitor post-launch) so Screen Sharing prompts and connects; tunnel first with\n`ssh -L 5901:127.0.0.1:5901 \u003chost\u003e`. Plain VNC clients (RealVNC/TigerVNC) work without a password.\n\n**Display sleep blanks VNC.** macOS only repaints the emulated framebuffer while the display is\nawake; once it sleeps (~idle), VNC shows a blank **white/black** screen with just the cursor even\nthough the guest is healthy (SSH works, WindowServer is up). It is *not* a GPU/driver problem — a\nmouse move repaints it, and the full Tahoe desktop renders fine (Finder/Dock/menu bar). The golden\nimage's first-boot daemon now runs `pmset -a displaysleep 0 disablesleep 1` system-wide (covers the\npre-login loginwindow) so the framebuffer stays painted; older images need a `setup`-stage rebuild.\n\n## CI image pipeline (`.github/workflows/build-macos-image.yml`, `scripts/build-qemu-macos.sh`)\n\n`workflow_dispatch` with a `macos` version + a `stage`. **`macos`** selects which OS to build and\nwhere it lands on ghcr (the build script derives `MACOS_SHORTNAME`/`GHCR_REPO`/`GHCR_TAG` from it):\n\n| `macos` | fetch-macOS shortname | ghcr repo:tag |\n|---------|-----------------------|---------------|\n| `tahoe` (default) | `tahoe` | `…/tahoe:26` — macOS 26, the **last Intel** macOS |\n| `sequoia` | `sequoia` | `…/sequoia:15` — macOS 15 (n-1, still security-supported) |\n\n`stage` controls how far the build goes (shown for tahoe:26; the actual repo:tag follows `macos`):\n\n| stage | what |\n|-------|------|\n| `boot` | smoke: boot OpenCore → macOS Recovery (proves KVM + OpenCore + recovery) |\n| `install` | full install from scratch → capture → push `\u003crepo\u003e:\u003ctag\u003e-base` (~65 min) |\n| `setup` | pull `\u003crepo\u003e:\u003ctag\u003e-base` → boot Recovery → `provision-macos.sh` (SA-skip recipe + user + SSH) → push `\u003crepo\u003e:\u003ctag\u003e` |\n| `slim` | pull `\u003crepo\u003e:\u003ctag\u003e` → boot → reclaim stale clusters → re-push `\u003crepo\u003e:\u003ctag\u003e` (smaller) |\n| `verify` | pull `\u003crepo\u003e:\u003ctag\u003e` → boot → confirm login + SSH (`cocoon@localhost`) |\n\nSame OSX-KVM (multi-version OpenCore) + the same provision recipe build either OS — only the\nrecovery shortname + tag differ. This pipeline is **image-only** (no Go); the CLI end-to-end\n(`vm run` + `--random-smbios`) is exercised on a KVM testbed, keeping image and Go CI separate.\n\nAutomation primitives (`scripts/qmp-input.py`): QMP absolute mouse click/move, keyboard\ntype/chord, **tesseract+PIL OCR-click and title routing** (drives the macOS GUI installer where\nbuttons can't be reached by keyboard), HMP `screendump`. Provisioning (`scripts/provision-macos.sh`)\nruns in the Recovery Terminal against the installed Data volume (`dscl -f` offline user,\n`.AppleSetupDone`, first-boot LaunchDaemon for Remote Login).\n\nKey host facts: GitHub `ubuntu-latest` exposes `/dev/kvm` (needs `chmod 666`); macOS Tahoe 26\nis the last Intel-supporting macOS, so this x86 path has a finite shelf life.\n\n## Boot-to-desktop (GUI renders; auto-skip of the Setup Assistant is the remaining WIP)\n\nThe full Tahoe desktop **does render over VNC** — testbed-verified: boot → login window (the `cocoon`\nuser) → type `cocoon` → Finder + Dock + menu bar + desktop widgets, all repainting normally. The\nearlier \"white/black VNC\" was purely **display sleep** (see the VNC note above), now fixed at the\nimage level. What's left for a *fully unattended* boot-to-desktop is auto-skipping the first-run\nSetup Assistant + auto-login.\n\nThe `desktop` build stage + `provision-macos.sh` aim to make `:26` boot straight to `cocoon`'s\ndesktop (auto-login, no Setup Assistant). The **post-SA recipe is validated** (proven on a testbed\nVM): complete home + `com.apple.SetupAssistant` `DidSee*`/`LastSeen*` (Buddy=build `25F71`,\nCloud=product `26.5`) + auto-login (`autoLoginUser` + `/etc/kcpassword` written via `perl pack` —\nmacOS `/bin/bash` 3.2 has no `printf \\xHH`) + keyboard-wizard suppress + `pmset` no-sleep.\n\n**The blocker:** a fresh macOS 26 Tahoe clone boots to the *system* Setup Assistant\n(`_mbsetupuser` / `SetupAssistantSpringboard`) and it resists every marker-based skip we tried\n(`.AppleSetupDone`, complete home from User Template, `.skipbuddy`, `DidSee*`, auto-login, a\ncorrectly-named killsa daemon, removing `/var/db/ConfigurationProfiles` [SIP-blocked]). macOS 14+\nbroke the classic `.AppleSetupDone` skip. The keyboard does not register at the SA, so the only\nreliable automated skip is a **mouse/OCR click-through of the SA wizard** (the install-stage OCR\nmachinery) — not yet implemented. Until then, `:26` is SSH/VNC-login-usable but the GUI lands at SA.\n\n## Why QEMU (not Apple VZ)\n\nVZ on Apple Silicon caps ~2 macOS VMs/host and can't use the App Store; QEMU + OpenCore on x86\nhas neither limit (at the cost of per-VM identity + Apple-ID ban risk at fleet scale). See the\ndeep-research notes that motivated this project.\n\n## E2E regression (`scripts/e2e.sh`)\n\nA testbed lifecycle regression (modeled on cocoon's), two tiers:\n\n- **`[DUMMY]`** — file-level lifecycle on a tiny throwaway qcow2, **no macOS boot**: image store\n  (`pull`-less `list`/`rm`), `vm create` overlay-on-shared-base, `--random-smbios` serial/MAC\n  uniqueness + MAC==ROM, `snapshot`/`restore` (qcow2-internal tag rollback), `clone` (CoW on the\n  shared base + distinct identity + clone-of-clone backing), `stop` PID-reuse-safe teardown,\n  `--net bridge` auto-TAP create + `tap_owned` + teardown-on-`rm` + user-`--tap` preservation +\n  negative paths — with post-conditions asserting no leaked TAPs/procs and backing-chain integrity.\n  Runs on any x86 Linux/KVM host; the `--random-smbios` rows need `root` + `nbd` + a real\n  `CM_OPENCORE`, the `--net` rows need `root` + a test bridge (they `SKIP`, not `FAIL`, otherwise).\n- **`[REAL]`** (`--real`) — boots `tahoe:26` from the store, passes the OpenCore picker over the HMP\n  monitor, asserts SSH-ready (`sw_vers` 26.x) + in-guest serial == injected. Testbed-only.\n\n```bash\nsudo CM_BIN=./cocoon-macos CM_OPENCORE=.../OpenCore.qcow2 ./scripts/e2e.sh           # [DUMMY] (30 rows)\nsudo CM_HOME=~/cm-demo CM_OPENCORE=... CM_OVMF_CODE=... CM_OVMF_VARS=... ./scripts/e2e.sh --real-only\n```\n\nThe `[DUMMY]` tier is the behavioral gate today's CI structurally can't give (CI is pure `go\ntest`/`vet`/build — nothing opens `/dev/kvm` or mutates netlink); it belongs on a privileged\nself-hosted KVM runner. `[REAL]` stays a manual/nightly testbed run (~15GB + cold boot).\n\n## Roadmap\n\n- **Port cocoon's bypass/passthrough surface.** cocoon exposes extra VM hardware that cocoon-macos\n  doesn't yet wire to QEMU; port it the same way as networking (Linux-tagged, reusing cocoon types):\n  - **Multi-disk / data disks** — cocoon's `types.DataDiskSpec` + `StorageRole` (`data`/`cow`/…) and\n    `--data-disk`. Maps to extra QEMU `-drive`/`-device virtio-blk` per disk (attach/detach lifecycle).\n  - **Hardware passthrough (VFIO)** — cocoon's `extend/vfio` (`vfio.Spec`/`Attacher`, BDF/sysfs) and\n    `cmd/vm attach`. Maps to QEMU `-device vfio-pci,host=\u003cBDF\u003e` for GPU/NIC/etc. passthrough.\n- **GUI boot-to-desktop** — finish the SA click-through (below).\n- **Live snapshot** is intentionally *not* on the roadmap: `-cpu +invtsc` makes the macOS guest\n  non-migratable, so resume-from-RAM can't work; snapshot stays offline/disk-only + cold clone.\n\n## Out of scope (v0.x)\n\nA `qemu` Hypervisor backend *inside* cocoon (so cocoon's own `cmd/core` dispatches macOS VMs) is a\nseparate later phase — cocoon-macos currently **imports** cocoon's libraries (`cloudimg`, `network`,\nstorage/CoW conventions) rather than the reverse. Per-VM SMBIOS injection (`--random-smbios`) is\nimplemented + testbed-verified, but registering those identities with iServices/App Store is the\nconsumer's policy concern — it needs validated (not just unique) serials and carries Apple-ID ban\nrisk at fleet scale — so it is not done here.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcocoonstack%2Fcocoon-macos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcocoonstack%2Fcocoon-macos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcocoonstack%2Fcocoon-macos/lists"}