# Linux VMs

The VM service (`Sources/NucleicCore/MacVM/`, see [MACOS_VM.md](MACOS_VM.md)) boots **Linux** guests as
well as macOS ones, directly on Apple's Virtualization framework. A Linux guest is a full GUI VM the
agent can both `exec` into (`mac_vm_exec`) and **see + drive** (`mac_vm_computer`) — a real Linux
desktop, isolated per session, the counterpart to the macOS guest.

It is a *sibling* of the macOS path, not a fork: `MacVMEngine` / `MacVMManager` / `MacVMSpec` are one
service that branches on a guest-OS kind (`GuestOS.macOS` / `.linux`). The whole control plane — vsock
exec (`MacVMExecChannel`), the host-side computer-use surface (`MacVMSurfaceHost` /
`VZVirtualMachineView`), NAT networking, clone-on-boot, the idle/concurrency policy, and the
`awaitAgentReady` vsock gate — is shared unchanged. (The types keep their `MacVM*` names for now; a
neutral `VMEngine` rename is a planned follow-up.)

## Why Linux is smoother than a Mac guest

Linux under the Virtualization framework exposes flexible virtio/USB devices the macOS guest can't use,
and we lean into exactly the rough spots the Mac path fights (`MacVMEngine+LinuxConfig.swift`):

| Concern | macOS guest | Linux guest |
|---|---|---|
| Output | `VZMacGraphicsDeviceConfiguration` (fixed; capture can come back blank) | **`VZVirtioGraphicsDeviceConfiguration`** (virtio-gpu scanout — reliable framebuffer capture) |
| Pointer | `VZMacTrackpadConfiguration` (relative) | **`VZUSBScreenCoordinatePointingDeviceConfiguration`** (absolute — injected clicks land 1:1 with screenshot pixels) |
| Keyboard | `VZMacKeyboardConfiguration` | **`VZUSBKeyboardConfiguration`** |
| Permissions | Screen Recording + Accessibility TCC (fallback path); SIP for the AX agent | **none** — computer-use is ordinary virtual hardware |
| Console | — | **`VZVirtioConsoleDeviceSerialPortConfiguration`** → a per-clone `console.log` for boot diagnostics |
| Platform / boot | `VZMacPlatformConfiguration` + `VZMacOSBootLoader` + restore-image install | `VZGenericPlatformConfiguration` + `VZLinuxBootLoader` (external kernel + initrd) |

Because capture + input are 100% host-side over virtio-gpu + USB HID, a Linux guest needs **no in-guest
agent for computer-use at all** — only for `exec`.

## Control plane — the Linux vsock agent

`guest/nucleic-linux-agent/` is a small, dependency-free **static C binary** that mirrors the macOS
agent's wire protocol (`VMAgentCore.AgentWire` / `MacVMAgentWire`) byte-for-byte: NDJSON over
`AF_VSOCK` port **2035**. It implements just the two ops the host reaches in for:

- `ping` → `{ok, version:2, os:"linux", displays, readiness}` (readiness is always-true — Linux
  computer-use needs no in-guest permission).
- `exec` → the streaming shell-exec sub-protocol `MacVMExecChannel` speaks: run `/bin/sh -c <command>`,
  stream base64 `o`/`e`/`x` frames, honor host `i`/`eof`/`sig` frames.

The AX-semantic ops (`ax_dump`, `ax_press`, …) are macOS-only and have no Linux equivalent; the engine
short-circuits them with a "use screenshot + pixel actions" message for Linux guests.

Build + test (in the Linux build container / CI):

```sh
guest/nucleic-linux-agent/build.sh                                   # → static arm64 binary
cc -O2 -pthread -o /tmp/t guest/nucleic-linux-agent/test/loopback_test.c && /tmp/t   # protocol round-trip
```

## Base image — the fully-automated two-phase build

macOS has no Linux filesystem tooling, so the ext4 root disk is assembled **inside the guest** across
two in-framework boots (`MacVMEngine+LinuxBase.swift`, `MacVMEngine+LinuxProvision.swift`). The host
only downloads (and version-caches) three artifacts: an uncompressed arm64 kernel `Image` (gunzipped
host-side — modern Ubuntu arm64 `vmlinuz` is an EFI-**zboot** self-decompressing PE, unwrapped to a raw
`Image` the way a gzip `vmlinuz` is gunzipped, since `VZLinuxBootLoader` needs a raw Image), a matching
initrd, and the Ubuntu root filesystem tarball. Pinned **Ubuntu 26.04 LTS (resolute)** arm64 defaults
ship in `MacVMSettings`, so it builds with zero configuration — the latest LTS, and it ships **GNOME 50
/ Mutter**, the Wayland compositor the in-guest semantic agent targets
(docs/LINUX_VM_SEMANTIC_AGENT.md); a GNOME bump is a deliberate port.

1. **Bootstrap boot** — the kernel + a tiny **busybox initramfs** (`guest/linux-base/bootstrap/init`,
   built by `scripts/build-linux-bootstrap.sh`) boot with the empty root disk attached and the
   provisioning payload (rootfs tarball, the agent binary, provisioning script + systemd units) handed
   in as a **raw tar on a second virtio-blk disk** (`/dev/vdb`). `/init` auto-detects the two disks (the
   payload disk carries a `ustar` magic; the other is root), `tar -x`'s the payload, formats the root
   disk, unpacks the rootfs, installs the agent + a first-boot unit + fstab + DHCP, and powers off. (The
   payload rides a block device, not virtiofs, because modern Ubuntu kernels ship `virtiofs` as a module
   that isn't present in the busybox initramfs; `virtio_blk` is built-in.)
2. **Provision boot** — the installed rootfs boots; `nucleic-firstboot.service` runs
   `provision-linux-guest.sh` once: `apt` installs a **GNOME Wayland session** (`gnome-shell` /
   Mutter + `gdm3` auto-login + GTK4 apps) so the surface has a live screen, turns the **AT-SPI
   accessibility bus on** system-wide (`toolkit-accessibility`, the semantic agent's precondition), and
   installs the dev toolchain (build tools, git, node, python + the AT-SPI GI bindings) plus **Firefox**
   (the real `.deb` from Mozilla's APT repo, not the Ubuntu snap) as a reference browser. It writes
   `/var/lib/nucleic/provisioned`; the host waits on that over the agent, then powers off to seal the base.

Session VMs are instant copy-on-write clones of the sealed base (`clonefile(2)` on the rootfs disk),
exactly like the macOS path.

```
storageRoot/
  linux-base/            golden Linux base bundle (vmlinux, initrd.img, cmdline, Disk.img, bundle.json)
  linux-artifacts/       version-keyed cache: kernel, initrd, rootfs tarball, bootstrap.cpio.gz
  instances/<name>/      per-session clones (nucleic-lin-<short>)
```

### Prebuilt artifacts

- The **bootstrap initramfs** is built on-demand on the Mac (`scripts/build-linux-bootstrap.sh` needs
  only a busybox download + `ar`/`tar`/`gzip`/`python3`, all present with the Xcode command line tools).
- The **agent binaries** can't be compiled on macOS (they're Linux arm64 binaries), so they're
  build/CI artifacts: the static-C `nucleic-linux-agent` (`guest/nucleic-linux-agent/build.sh`) and the
  Rust AT-SPI agent `nucleic-a11y-agent` (`guest/nucleic-a11y-agent/build.sh`, docs/LINUX_VM_SEMANTIC_AGENT.md).
  The engine resolves each from `guest/<agent>/build/`, a bundled `LinuxAssets` resource, or
  `NUCLEIC_LINUX_ASSETS`; the a11y agent is optional (absent → host falls back to pixels).
- **CI:** `.github/workflows/linux-vm-agents.yml` builds both agents on an arm64 runner (+ unwraps the
  pinned kernel via `scripts/publish-linux-vm-kernel.sh`), uploads them as workflow artifacts, and
  publishes them to GHCR as single-blob OCI artifacts (`nucleic-linux-agent`, `nucleic-a11y-agent`,
  `nucleic-linux-vm-kernel`) — the same distribution channel as `nucleic-kernel`/`nucleic-sandbox`.
  Commit the built binaries into `guest/<agent>/build/` (the path the base build reads).

## File sharing + host-path re-creation

The session's repo is a **virtiofs** share (tag `nucleicshare`). Unlike macOS there is no automount:
the exec path mounts it on demand at `/mnt/nucleic`, and each share is a subdirectory
`/mnt/nucleic/<name>` (the run's worktree is `workspace`, so the default workdir is
`/mnt/nucleic/workspace`).

Because that guest path differs from the host path, build artifacts that hardcode an absolute path
(SwiftPM `.build`, `compile_commands.json`, `.o` debug maps, caches) would break. So once the agent is
ready the engine re-creates each share's **original host path** inside the guest by **bind-mounting**
the share there — a real directory backed by the same virtiofs inode, matching the container's
identical-path model — falling back to a symlink if the bind can't be made
(`MacVMEngine+HostPaths.swift`, `MacVMSpec.Mount.recreateHostPath`, default on). The step is
idempotent and best-effort; a share that can't be reconstructed is logged and skipped. macOS uses a
symlink instead — see [MACOS_VM.md §6.1](MACOS_VM.md).

## Lifecycle control — `linux_vm_control`

The Linux sibling of `mac_vm_control`: a non-gated MCP tool that lets the agent manage the VM it
booted — `op` one of `status` / `stop` / `suspend` / `resume` / `restart` / `kill`, acting on the
session's own disposable Linux VM. Advertised (and pre-allowed) whenever the Linux VM is usable
(`allowLinuxVMExec` **or** `allowLinuxVMComputer`). Shares
`MCPApprovalServer.VMControlCall`/`VMControlReply` and `ClaudeCodeBackend.performVMControl` with the
macOS tool — see [MACOS_VM.md §8.3](MACOS_VM.md) for the full op table and rationale.

## Settings

`MacVMSettings` (in `Project.swift`), all off/pinned by default:

- `linuxServiceEnabled` — master switch (sibling of `serviceEnabled`).
- `linuxExposeByDefault`, `linuxComputerUseByDefault` — per-session exposure.
- `linuxKernelURL` / `linuxInitrdURL` / `linuxRootfsURL` — artifact sources (default: Ubuntu 26.04 LTS arm64).
- `linuxBasePrebuiltPath` — clone a prebuilt base bundle instead of building.

## Try it (macvm-spike)

The spike drives the real engine end-to-end (Apple silicon, signed with the virtualization entitlement):

```sh
swift build --product macvm-spike
codesign --force --sign - --entitlements signing/spike.entitlements .build/.../macvm-spike
NUCLEIC_LINUXVM=1 NUCLEIC_LINUXVM_BUILD_BASE=1 .build/.../macvm-spike   # one-time base build
NUCLEIC_LINUXVM=1 .build/.../macvm-spike                               # clone → boot → exec → screenshot → teardown
```

## Status / remaining

Landed: the guest-OS kind + Linux VZ config, the vsock agent (protocol-tested), the two-phase base
build + bootstrap, computer-use reuse, and `MacVMManager` plumbing. The end-to-end base build (booting
real VMs + the `apt` desktop install) requires validation on a real Mac and is expected to need
iteration on the desktop/session specifics. The app-surface wiring — the SettingsView "Build Linux
base image" panel and per-session OS selection that exposes `mac_vm_exec`/`mac_vm_computer` against a
Linux VM — mirrors the existing macOS wiring and is the remaining integration step.
