# Backend contract and Lima driver

> The four-role backend.Backend interface (Host, Images, Machines, Housekeeping), capability requirement checks (Caps), and the Lima driver implementation.

- Repository: devvmhq/devvm
- GitHub: https://github.com/devvmhq/devvm
- Human docs: https://grok-wiki.com/public/docs/devvmhq-devvm-4242986d2853
- Complete Markdown: https://grok-wiki.com/public/docs/devvmhq-devvm-4242986d2853/llms-full.txt

## Source Files

- `internal/backend/backend.go`
- `internal/backend/caps.go`
- `internal/backend/guard.go`
- `internal/backend/lima/lima.go`
- `internal/backend/lima/start.go`
- `internal/backend/lima/seatbelt.go`

---

---
title: "Backend contract and Lima driver"
description: "The four-role backend.Backend interface (Host, Images, Machines, Housekeeping), capability requirement checks (Caps), and the Lima driver implementation."
---

`internal/backend` defines the hypervisor abstraction that separates VM lifecycle orchestration, image recipes, and network policy from underlying virtualization tools. Every backend implements the `backend.Backend` interface—composed of four required role interfaces (`Host`, `Images`, `Machines`, and `Housekeeping`) plus an explicit capability map (`Caps`)—and is wrapped at registration time by a `guarded` decorator that enforces feature requirements and specification invariants before any driver method executes. The default `lima` driver in `internal/backend/lima` implements this contract over `limactl`, using Apple Virtualization.framework (`vz`) with Seatbelt (`sandbox-exec`) confinement on macOS and QEMU/KVM with `restrict=on` user-mode network confinement (`devvm _qemu`) on Linux.

## Four-role backend contract

`backend.Backend` (`internal/backend/backend.go`) combines identification and capability reporting (`Name() string`, `Caps() Caps`) with four role interfaces. Backends that store per-VM host state outside the VM instance itself can also implement the optional `Forgetter` interface (`internal/backend/leftovers.go`), which callers discover through `backend.As[backend.Forgetter](b)` so the registry's `guarded` wrapper does not hide optional interfaces.

```mermaid
classDiagram
    class Backend {
        <<interface>>
        +Name() string
        +Caps() Caps
    }
    class Host {
        <<interface>>
        +Problems(ctx) []Problem
        +NameMax() int
        +Location() (where string, isDefault bool)
        +DiskDirs() []string
        +Floors() Floors
    }
    class Images {
        <<interface>>
        +BuildImage(ctx, ImageSpec) (Built, error)
        +Images(ctx) ([]string, error)
        +ImageSize(ctx, key string) (int64, error)
        +DeleteImage(ctx, key string) error
        +BuildTools(ctx, ToolsSpec) error
        +ToolFormat() string
        +Origin(f *From) ([]Step, error)
        +PinOrigin(ctx, f *From, how Pin) (string, error)
    }
    class Machines {
        <<interface>>
        +CreateVM(ctx, image string, spec VMSpec) error
        +Start(ctx, vm string, w Want, c Confinement) error
        +Running(ctx, vm string) (Run, error)
        +ProxyURL(c Confinement) string
        +Stop(ctx, vm string) error
        +Delete(ctx, vm string) error
        +Rename(ctx, vm, to string) error
        +List(ctx) ([]Instance, error)
        +Inspect(ctx, vm string) (Instance, error)
        +Exec(ctx, vm string, argv []string, o ExecOpts) error
        +Shell(ctx, vm string, o ShellOpts) (Command, error)
        +SSH(ctx, vm string) (SSHTarget, error)
    }
    class Housekeeping {
        <<interface>>
        +Usage(ctx) (Usage, error)
        +Strays(ctx) ([]Stray, error)
        +DevvmCache(ctx) ([]CacheEntry, error)
        +DeleteCache(ctx, entries []CacheEntry) error
    }
    class Forgetter {
        <<interface>>
        +Forget(ctx, vm string) error
    }
    class guarded {
        -Backend
        -r *Registry
        +BuildImage(ctx, ImageSpec) (Built, error)
        +BuildTools(ctx, ToolsSpec) error
        +CreateVM(ctx, string, VMSpec) error
        +Start(ctx, string, Want, Confinement) error
    }
    class Lima {
        +host
        +images
        +machines
        +housekeeping
        +Dirs paths.Dirs
        +Runner Runner
        +OS string
        +Arch string
    }

    Backend --|> Host
    Backend --|> Images
    Backend --|> Machines
    Backend --|> Housekeeping
    guarded ..|> Backend : wraps & validates
    Lima ..|> Backend : implements via embedded roles
```

### Role breakdown

| Role | Responsibility | Key Methods and Return Types |
| --- | --- | --- |
| `Host` | Reports missing host dependencies (`devvm doctor`), maximum VM name length, storage root location, and minimum free disk thresholds (`backend.Space`). | `Problems(ctx) []Problem`, `NameMax() int`, `Location() (string, bool)`, `DiskDirs() []string`, `Floors() Floors` |
| `Images` | Pins upstream base origins, emits backend-specific recipe steps for image key hashing, builds base/layer/shared-tool images, and manages stored images. | `Origin(*From) ([]image.Step, error)`, `PinOrigin(ctx, *From, Pin) (string, error)`, `BuildImage(ctx, ImageSpec) (Built, error)`, `BuildTools(ctx, ToolsSpec) error`, `ToolFormat() string`, `Images(ctx) ([]string, error)`, `ImageSize(ctx, string) (int64, error)`, `DeleteImage(ctx, string) error` |
| `Machines` | Clones VMs from images, applies `Want` and `Confinement` deltas on stopped VMs at `Start`, inspects live confinement lifelines, and executes guest commands or shells. | `CreateVM(ctx, string, VMSpec) error`, `Start(ctx, string, Want, Confinement) error`, `Running(ctx, string) (Run, error)`, `ProxyURL(Confinement) string`, `Stop`, `Delete`, `Rename`, `List`, `Inspect`, `Exec`, `Shell`, `SSH` |
| `Housekeeping` | Calculates VM and cache disk footprints for `devvm space`, finds unlabeled/orphaned `Stray` VMs, and prunes devvm-owned entries in backend caches for `devvm gc --caches`. | `Usage(ctx) (Usage, error)`, `Strays(ctx) ([]Stray, error)`, `DevvmCache(ctx) ([]CacheEntry, error)`, `DeleteCache(ctx, []CacheEntry) error` |
| `Forgetter` *(optional)* | Removes auxiliary per-VM backend files after a VM is already gone (`devvm gc --orphans`). Discovered with `backend.As[backend.Forgetter](b)`. | `Forget(ctx, vm string) error` (not implemented by `lima`, which stores all VM state inside the Lima instance folder) |

### Core specification and state types

 Callers pass declarative specifications to `Images` and `Machines` methods rather than mutating backend configuration files directly:

<ParamField body="Want" type="backend.Want">
  Mutable VM properties applied at `CreateVM` and re-evaluated on every `Start` of a stopped VM:
  - `Size` (`CPUs int`, `MemoryGiB float64`, `DiskGiB float64`): zero fields leave existing values unchanged. `backend.Validate(w, in)` rejects any `DiskGiB` smaller than `in.Size.DiskGiB` with `disk_shrink`.
  - `Project` (`string`) and `ProjectID` (`string`): host path of the read-write project mount and its recorded device/inode identity (`platform.FolderID`). Empty for `--clone` VMs.
  - `AgentDirs` (`[]string`): host paths mounted read-only at identical paths in the guest.
  - `AgentForwarding` (`bool`): whether SSH agent forwarding is enabled in the guest configuration.
</ParamField>

<ParamField body="VMSpec" type="backend.VMSpec">
  Embeds `Want` and adds creation-time parameters for `CreateVM`: `Name`, `CloneAt` (guest repo path for `--clone` VMs), `Tools` and `ToolsAt` (read-only shared tools mount at `/mnt/devvm-tools`), `AgentToggle` (`bool`), `SSHPort` (`int`), and base guest `Env` (`map[string]string`).
</ParamField>

<ParamField body="Confinement" type="backend.Confinement">
  Runtime network and credential confinement applied at `Start`:
  - `Net` (`backend.Net`): one of `NetDirect` (0, open tier using the backend's user-mode network), `NetProxy` (1, confined tiers reaching only `devvm _proxy` on host loopback), `NetInternet` (2, image/tool builds reaching the internet but blocked from host loopback where supported), or `NetNone` (3, offline builds/VMs).
  - `ProxyPort` (`int`) and `ProxyPassword` (`string`): host loopback port (`127.0.0.1`) and 64-hex-character random password (`backend.NewProxyPassword`) for `devvm _proxy`. `Confinement.GuestProxy()` returns `true` when `Net == NetProxy` or when `Net == NetDirect && ProxyPort != 0`.
  - `Env` (`map[string]string`): guest environment variables written on boot (proxy CA variables, `NO_PROXY`).
  - `SSHPort` (`int`), `SSH` (`bool`), `AgentSocket` (`string`), and `NoAgent` (`bool`).
</ParamField>

<ParamField body="Run" type="backend.Run">
  Returned by `Machines.Running(ctx, vm)`:
  - `ID` (`string`): unique identifier for the current boot session (`"<pid> <mtime_rfc3339nano>"` on Lima, or `""` when stopped).
  - `Confined` (`bool`): `true` only if the VM is currently running under the exact host process started by `Start` with `NetProxy`.
  - `Lifeline` (`string`): path to the host hypervisor/hostagent PID file that `devvm _proxy` and `vmssh.Forwards` watch to terminate automatically when the VM exits.
</ParamField>

---

## Capability matrix and requirement checks

Every feature that `devvm` can ask of a backend is represented by a `backend.Feature` constant (`internal/backend/caps.go`). A backend declares its support level for all 19 features in `Caps() map[Feature]Support`:

- `backend.Native` (`rank = 3`): supported directly by the backend.
- `backend.Emulated(true)` (`rank = 2`, string `"emulated"`): built by `devvm` on top of other backend primitives and approved after a design note in `docs/content/docs/internals/design.md`, a threat review, and end-to-end tests.
- `backend.Emulated(false)` (`rank = 1`, string `"unreviewed"`): implemented as an emulation but refused at runtime unless explicitly whitelisted for testing via `DEVVM_UNREVIEWED=<backend>:<feature>`.
- `backend.None` (`rank = 0`, string `"none"`): unsupported.

### Feature dependencies and Lima support

Three composite features depend on a base feature via `Feature.BuiltOn()`. `Caps.Check()` rejects any backend registration where a dependent feature declares a higher support rank than its base feature, or where `len(c) != len(Features)`.

| Feature | `BuiltOn` Base | Required By | Lima Support | Lima Implementation Details |
| --- | --- | --- | --- | --- |
| `build-image` | — | `ImageSpec.Features()`, `ToolsSpec.Features()` | `Native` | Builds `devvm-img-<key>` instances from `base.yaml` + recipe scripts |
| `build-confinement` | — | `NetInternet`, `NetNone` | `Native` | macOS Seatbelt denies `localhost:*` or all outbound; Linux QEMU `restrict=on` confines `NetNone` |
| `from` | — | `ImageSpec` with `From != nil` | `Native` | Converts OCI/Docker image into raw disk + kernel/initrd in a conversion VM |
| `clone` | — | `VMSpec.Features()` | `Native` | `limactl clone`: APFS CoW clone on macOS (`vz`), `qcow2` backing overlay on Linux (`qemu`) |
| `project-mount` | — | `VMSpec` with `Project != ""` | `Native` | Read-write mount (`virtiofs` via `/.vol/<dev>/<ino>` on macOS `vz`; `9p` by path on Linux) |
| `agent-folders` | — | `Want` with `len(AgentDirs) > 0` | `Native` | Read-only host folder mounts (`writable: false`) |
| `tools-share` | — | `VMSpec` with `Tools != ""` | `Native` | Read-only mount at `/mnt/devvm-tools` (`writable: false`) |
| `egress-proxy` | — | `NetProxy` | `Native` | macOS Seatbelt profile; Linux QEMU `restrict=on` + `guestfwd` (`192.168.5.4:3128`) |
| `ssh` | — | `devvm code`, `devvm ssh-agent`, links | `Native` | Lima's driver SSH forward on `127.0.0.1` + `_config/user` identity |
| `ports` | — | `Ports` with `len(Ranges) > 0` or `All` | `Emulated(true)` | `vmssh.Forwards` over `SSH()` with Lima's own `portForwards` set to `ignore: true` |
| `ports-all` | `ports` | `Ports` with `All: true` | `Emulated(true)` | `vmssh.Forwards` polling guest listeners `>= 1024` via `assets/guest/ports.sh` |
| `agent-forwarding` | — | `Want` with `AgentForwarding: true` | `Native` | Sets `.ssh.forwardAgent=true` and passes `SSH_AUTH_SOCK` to `limactl` |
| `agent-toggle` | `agent-forwarding` | `VMSpec` with `AgentToggle: true`, `devvm ssh-agent` | `Native` | Updates `.ssh.forwardAgent` via `limactl edit` before `Start` |
| `cpus` | — | `Size` with `CPUs > 0` | `Native` | `--cpus` in `limactl create` / `limactl edit` |
| `memory` | — | `Size` / `ImageSpec` with `MemoryGiB > 0` | `Native` | `--memory` in `limactl create` / `limactl edit` |
| `disk` | — | `Size` with `DiskGiB > 0` | `Native` | `--disk` in `limactl create` / `limactl edit` (grow-only) |
| `docker-layer` | — | `ImageFeatures("...docker...")` | `Native` | Runs Docker Engine inside the guest |
| `nix-layer` | — | `ImageFeatures("...nix...")`, `ImageSpec` with `nix` | `Native` | Runs Nix daemon and sandboxed builds inside the guest |
| `link` | `ssh` | `devvm link`, `devvm group` | `Native` | Host-mediated SSH tunnel binding `127.77.0.0/16` in client VMs |

### Dual-layer guard enforcement

Feature checks happen at two boundaries (`internal/backend/caps.go` and `internal/backend/guard.go`):

1. **Caller pre-check (`Review.Require`):** Before building an image, creating or starting a VM, or mutating settings, `internal/vm` calls `env.Backends.Require(b, vmName, features...)`. If a feature is `None`, `Require` returns `msg.BackendLacks(b.Name(), feature, vm)`. If it is `Emulated(false)` and not listed in `Review.Unreviewed` (parsed from `DEVVM_UNREVIEWED`), `Require` returns `msg.BackendUnreviewed(b.Name(), feature, vm)`. If `DEVVM_UNREVIEWED` lets an unreviewed feature through, `Require` emits a warning via `msg.BackendUnreviewedUsed`.
2. **Registry guard (`guarded`):** `Registry.Register` wraps every registered backend in `*guarded`. Calling `BuildImage`, `BuildTools`, `CreateVM`, or `Start` on a registered backend runs `spec.Validate()` and `r.refusal(g.Backend, name, nil, features)` before delegating to the driver. A caller that omits `Require` still cannot invoke a backend operation for an unsupported or unreviewed feature.

```go title="internal/backend/guard.go"
func (g *guarded) BuildImage(ctx context.Context, spec ImageSpec) (Built, error) {
	if err := spec.Validate(); err != nil {
		return Built{}, err
	}
	if err := g.r.refusal(g.Backend, spec.Image, nil, spec.Features()); err != nil {
		return Built{}, err
	}
	return g.Backend.BuildImage(ctx, spec)
}

func (g *guarded) Start(ctx context.Context, vm string, w Want, c Confinement) error {
	if err := g.r.refusal(g.Backend, vm, nil, slices.Concat(w.Features(), c.Features())); err != nil {
		return err
	}
	return g.Backend.Start(ctx, vm, w, c)
}
```

<Note>
The `open` network tier uses `NetDirect` and requires no `egress-proxy` capability (`NetDirect.Features()` returns `nil`). Every backend must still route `NetDirect` guest traffic to `127.0.0.1:<ProxyPort>` on the host when `Confinement.GuestProxy()` is true so `devvm _proxy` can inject bound secrets and log requests.
</Note>

---

## Lima driver implementation

`lima.Lima` (`internal/backend/lima/lima.go`) embeds four single-field role structs—`host{l}`, `images{l}`, `machines{l}`, and `housekeeping{l}`—wired together by `withRoles`. All external commands execute through `Lima.Runner` (`logRunner` wrapping the UI's `LoggedCmd`).

### Isolated `LIMA_HOME` and `limactl` invocation

Lima merges `_config/default.yaml` and `_config/override.yaml` from `LIMA_HOME` into every instance it creates or starts. To prevent user-configured Lima networks or mounts from leaking into `devvm` VMs, `Lima.limactl` (`internal/backend/lima/limactl.go`) enforces strict process isolation on every `limactl` call:

- **Dedicated home (`Dirs.Lima`):** Sets `LIMA_HOME` to `~/.local/share/devvm/lima` (or `DEVVM_LIMA_HOME`), ignoring any ambient `LIMA_HOME` in the user's shell.
- **Global config check:** `checkGlobalConfigs()` checks for `_config/default.yaml` and `_config/override.yaml` inside `Dirs.Lima` using `os.Lstat` and refuses to start VMs (`lima_global_config`) if either exists. `Start` also inspects `in.Config.Networks` and fails with `vm_has_networks` if any extra network is configured on the instance.
- **Working directory (`Dirs.Data`):** Runs `limactl` with `cmd.Dir = l.Dirs.Data` (`~/.local/share/devvm`). When `limactl` rejects an invalid YAML config, it writes `lima.REJECTED.yaml` into its current working directory; running inside `Dirs.Data` prevents a compromised guest from planting a symlink named `lima.REJECTED.yaml` inside a writable project folder.
- **Environment scrubbing:** Strips all ambient proxy variables (`HTTP_PROXY`, `HTTPS_PROXY`, `FTP_PROXY`, `NO_PROXY`, `ALL_PROXY` in upper and lower case) and `SSH_AUTH_SOCK` from `limactl`'s environment. Because Lima shares a single OpenSSH `ControlMaster` connection per VM, an ambient `SSH_AUTH_SOCK` inherited by the first `limactl shell` would expose the host's SSH agent to subsequent sessions even when the VM is configured for no agent or `devvm`'s isolated agent.
- **Socket path length limit (`NameMax`):** Lima creates `<Dirs.Lima>/<vm>/ssh.sock`, and OpenSSH appends a 17-byte temporary suffix (`sshSocketExtra = 17`) while binding. Because macOS caps `sockaddr_un.sun_path` at 104 bytes (`socketMax = 104`), `NameMax()` returns:
  $$\text{NameMax} = \max\bigl(0,\; 104 - 17 - \text{len}(\text{Dirs.Lima}) - \text{len}(\text{"/"}) - \text{len}(\text{"/ssh.sock"})\bigr)$$

### Image origins, builds, and disk overlays

In `internal/backend/lima`, an image is a stopped Lima instance named `devvm-img-<key>` (`ImagePrefix = "devvm-img-"`). `imageKey(name)` distinguishes image instances from project VMs for folders named `img-...` by verifying `!vmname.IsVM(name)` (project VMs end with a `-<14-hex-hash>` suffix).

<Steps>
  <Step title="Pin and record the base origin">
    Before computing `base`'s image key, the builder calls `PinOrigin(ctx, f, how)` (`internal/backend/lima/pin.go`) under `Dirs.UbuntuPinLock()`. For default Ubuntu builds, `PinOrigin` queries `ubuntu.Finder` for the newest dated Ubuntu 24.04 cloud image (`PinFirst` or `PinNewest`) or the newest release prior to the current pin (`PinOlder` fallback on `ErrBoot`), writing `~/.local/share/devvm/images/ubuntu.json`. `Origin(f)` then returns the initial `image.Step` slice hashed into `base`'s key:
    1. `ubuntu-image` with the pinned release and SHA-256 argument.
    2. For `--from` builds (`f != nil`), the `oci/*` conversion scripts (`convertSteps`).
    3. `disk` with `backend.BaseDiskGiB` (`"100"`).
    4. `base.yaml` containing the embedded Lima template from `assets/base.yaml`.
  </Step>
  <Step title="Fit base resources and create the instance">
    `buildSize` fits `BaseCPUs` (`4`) and `BaseMemoryGiB` (`8` GiB) to the host via `backend.FitCPUs` and `backend.FitMemory` (capping memory at `hostGiB / 2` if `8 GiB` would leave the host with less than `2 GiB`). `create` invokes `limactl create --tty=false --mount-none --name=devvm-img-<key>` and pipes `base.yaml` over `stdin` (`-`) so no shared template file is written to disk. Layer builds instead clone their stopped parent instance with `limactl clone --tty=false --mount-none <parent> <inst>`.
  </Step>
  <Step title="Execute recipe steps under build confinement">
    `BuildImage` starts `devvm-img-<key>` confined to `spec.Net` (`NetInternet` or `NetNone`). If a corporate proxy is active on a `NetInternet` build (`buildProxy` in `internal/backend/lima/buildproxy.go`), `devvm` spawns a per-build `devvm _proxy` forwarding to the upstream proxy and exports temporary proxy variables on each script's `stdin` (preserving them across `sudo` via `/etc/sudoers.d/devvm-build-proxy` and removing the sudoers file afterward) so corporate proxy credentials are never written to `/etc/environment` inside the image. If any step exits with code `75` (`backend.ExitUnreachable` from `assets/layers/apt.sh`), `runStep` returns `msg.BuildSourcesUnreachable`.
  </Step>
  <Step title="Sync, stop, and seal the image disk">
    After all recipe steps complete and `spec.Trust.Remove` removes temporary build CAs, `BuildImage` runs `sync` inside the guest, stops the instance, deletes its runtime directory (`Dirs.VM(inst).Dir`), and on Linux removes write bits (`mode &^ 0o222`) from `<LimaHome>/devvm-img-<key>/disk`.
  </Step>
</Steps>

#### OS-specific VM cloning (`CreateVM`)

When `CreateVM` clones `devvm-img-<key>` into a project VM:
- **macOS (`vz`):** `limactl clone` creates an APFS copy-on-write clone of the raw disk image, sharing underlying blocks automatically.
- **Linux (`qemu`):** `limactl clone` copies the entire `qcow2` file. To avoid duplicating multi-gigabyte disks, `Lima.overlay` (`internal/backend/lima/lima.go`) verifies the cloned disk is `qcow2` via `qemu-img info -U --output=json`, deletes the full copy, and replaces it with a thin `qcow2` overlay backed by the read-only image disk (`qemu-img create -q -f qcow2 -b <img_disk> -F qcow2 <vm_disk>`), setting mode `0600`. `DeleteImage` checks `overlayUsers` via `qemu-img info -U` and refuses to delete an image whose disk backs any existing VM (`image_in_use`).

### Idempotent configuration diffing on `Start`

Any invocation of `limactl edit` causes Lima's cloud-init iso generator to assign a new instance ID on the next boot, which regenerates the guest's SSH host keys. Consequently, `Start` (`internal/backend/lima/start.go`) inspects the stopped VM via `limactl list --json` and computes the minimal set of `--set` flags through `wantEdits` and `startSets`. If `len(opts) == 0`, `Start` skips `limactl edit` altogether.

| Lima YAML Path / Flag | Condition | Value Set by `configure` | Purpose |
| --- | --- | --- | --- |
| `--cpus`, `--memory`, `--disk` | `w.Size` differs from `in.size()` (`DiskGiB` only when larger) | Requested CPU count, GiB memory, or GiB disk | Resizes stopped VM (`Validate` blocks disk shrink first) |
| `.mounts` | Project path/inode or `w.AgentDirs` changed | Project mount + read-only `AgentDirs` + existing tools share | On macOS `vz`, mounts project as `location: /.vol/<dev>/<ino>` (`platform.VolPath`) with `mountPoint: <Project>` so host symlinks replacing the project path are never followed; on Linux `9p`, mounts by path |
| `.ssh.forwardAgent` | `w.AgentForwarding != in.AgentForwarding` | `true` or `false` | Toggles SSH agent forwarding |
| `.ssh.localPort` | `sshPort != 0 && cfg.SSH.LocalPort != sshPort` | Allocated host loopback port | Pins the VM's SSH port across restarts |
| `.env` | `guestEnv(c)` differs from `cfg.Env` | JSON map of `c.Env` + `HTTP_PROXY`/`HTTPS_PROXY` (upper & lower) | Written to `/etc/environment` by Lima; uses `devvm-proxy-password` stand-in during `limactl edit` |
| `.propagateProxyEnv` | `(c.Net == NetDirect && !c.GuestProxy()) != propagates` | `false` whenever `devvm _proxy` is used or during builds; `true` otherwise | Prevents Lima from injecting host proxy credentials into images or proxy-confined VMs |
| `.hostResolver.enabled` | `c.Net == NetProxy` vs. other modes | `false` for `NetProxy`; `del(.hostResolver)` otherwise | Disables Lima's built-in DNS resolver (which binds ephemeral loopback ports denied by Seatbelt) when names resolve in `devvm _proxy` |
| `.dns` | `c.Net == NetProxy` vs. other modes | `["127.0.0.1"]` for `NetProxy`; `del(.dns)` otherwise | Makes direct guest DNS lookups fail fast on confined tiers |
| `.portForwards` | `!forwardsNone(cfg.PortForwards)` | 3 ignore rules (`127.0.0.1`, `0.0.0.0`, `::` for ports `1..65535`, `proto: "any"`, `ignore: true`) | Disables Lima's native port forwarder (`SO_REUSEADDR`) so `vmssh.Forwards` owns all port forwarding |

#### Proxy password stand-in and log redaction

Because process arguments to `limactl edit` are visible to other local processes in `ps`, `startSets` substitutes `const passwordStandIn = "devvm-proxy-password"` into the proxy URL passed to `--set .env = ...`. Immediately after `limactl edit` returns, `fillPassword` reads `<LimaHome>/<vm>/lima.yaml`, validates that the real proxy URL contains only safe YAML scalar characters (`a-zA-Z0-9-._~%:/@`), replaces `devvm-proxy-password` with `c.ProxyPassword`, and writes `lima.yaml` atomically with mode `0600` (`state.WriteFile`).

When `limactl start` runs, Lima's `hostagent` logs parsed environment variables to `<LimaHome>/<vm>/ha.stderr.log`. `Start` registers `defer l.redactLog(vm)`, which calls `redact.File` to overwrite any proxy password in `ha.stderr.log` with `*` bytes of the exact same length in-place so the running `hostagent`'s file offset remains valid.

---

## Host process confinement: Seatbelt and QEMU wrapper

For confined VMs (`NetProxy`, `NetInternet`, `NetNone`), `Lima.confine` (`internal/backend/lima/start.go`) restricts the hypervisor/network process at the OS level so even `root` inside the guest cannot bypass `devvm _proxy` or reach arbitrary host loopback services.

```mermaid
flowchart LR
    subgraph macOS["macOS (darwin) — internal/backend/lima/seatbelt.go"]
        SB["/usr/bin/sandbox-exec\n-f vms/&lt;hash&gt;/run/sandbox.sb"] --> HA["Lima hostagent\n(vz driver + usernet)"]
        HA -- "allow localhost:&lt;SSHPort&gt;" --> SSHD1["VM sshd forward"]
        HA -- "allow localhost:&lt;ProxyPort&gt;\n(192.168.5.2 in guest)" --> Proxy1["devvm _proxy\n(127.0.0.1:&lt;ProxyPort&gt;)"]
        HA -- "allow subpath LimaDir\n&amp; AgentSocket" --> Socks["ssh.sock / vz sock\n/ SSH_AUTH_SOCK"]
        HA -. "deny network-outbound\ndeny network-bind" .-> Blocked1["Host LAN, internet,\nDocker socket, other ports"]
    end

    subgraph Linux["Linux (linux) — internal/backend/lima/qemu.go"]
        Wrap["QEMU_SYSTEM_&lt;ARCH&gt; wrapper\ndevvm _qemu exec"] --> QEMU["qemu-system-&lt;arch&gt;\n-netdev user,id=net0,...,restrict=on"]
        QEMU -- "guestfwd=tcp:192.168.5.4:3128\n-cmd: devvm _qemu connect" --> Proxy2["devvm _proxy\n(127.0.0.1:&lt;ProxyPort&gt;)"]
        QEMU -. "restrict=on drops\nall other traffic" .-> Blocked2["Host loopback,\nLAN &amp; direct internet"]
    end
```

<Tabs>
  <Tab title="macOS Seatbelt (seatbelt.go)">
    On macOS, `confine` renders a Seatbelt profile (`~/.local/share/devvm/vms/<hash>/run/sandbox.sb`) and wraps `limactl start` with `/usr/bin/sandbox-exec -f <profile>`. Because the Lima `hostagent` owns the `vz` user-mode network stack, confining `limactl start` confines all guest network traffic:

    ```scheme title="internal/backend/lima/seatbelt.go (profileTmpl)"
    (version 1)
    (allow default)
    {{- if .Internet}}
    (deny network-outbound (remote ip "localhost:*"))
    {{- else}}
    (deny network-outbound)
    (allow network-outbound (remote unix-socket (subpath {{sb .LimaDir}})))
    {{- with .AgentSocket}}
    (allow network-outbound (remote unix-socket (path-literal {{sb .}})))
    {{- end}}
    (deny mach-lookup (global-name "com.apple.dnssd.service"))
    {{- end}}
    (allow network-outbound (remote ip "localhost:{{.SSHPort}}"))
    {{- with .ProxyPort}}
    (allow network-outbound (remote ip "localhost:{{.}}"))
    {{- end}}
    {{- if .NoBinds}}
    (deny network-bind)
    (allow network-bind (local tcp "localhost:{{.SSHPort}}"))
    (allow network-bind (local unix-socket (subpath {{sb .LimaDir}})))
    {{- end}}
    ```

    - **`NetProxy`:** Sets `ProxyPort = c.ProxyPort` and `NoBinds = true`. Denies all outbound IP traffic except `localhost:<SSHPort>` and `localhost:<ProxyPort>`, denies `com.apple.dnssd.service` Mach lookups, restricts Unix domain socket connections to `LimaDir` (resolved with `paths.PhysicalParent`) and `AgentSocket` (only when `forwarding` is enabled), and denies all TCP/Unix binds except `localhost:<SSHPort>` and sockets inside `LimaDir`. Guest processes reach `devvm _proxy` at `http://devvm:<password>@192.168.5.2:<ProxyPort>`.
    - **`NetInternet` (builds):** Sets `Internet = true` (and `ProxyPort` when behind a corporate build proxy). Allows outbound internet connections while denying `localhost:*` except `SSHPort` and `ProxyPort`, preventing build scripts from probing host loopback services.
    - **`NetNone`:** Leaves `Internet = false` and `ProxyPort = 0`, blocking all outbound IP traffic except `localhost:<SSHPort>`.
  </Tab>
  <Tab title="Linux QEMU wrapper (qemu.go)">
    On Linux, `confine` writes an executable wrapper script at `~/.local/share/devvm/run/qemu/qemu-system-<arch>` (mode `0700`) and sets `QEMU_SYSTEM_AARCH64` or `QEMU_SYSTEM_X86_64` in `limactl start`'s environment:

    ```sh title="~/.local/share/devvm/run/qemu/qemu-system-<arch>"
    #!/bin/sh
    # Written by devvm: QEMU with devvm's network restrictions.
    exec '/path/to/devvm' _qemu exec "$0" "$@"
    ```

    When Lima spawns QEMU, `devvm _qemu exec` runs `lima.RunQEMU`, which rewrites the argument list with `RestrictNetdev(args, os.Getenv("DEVVM_GUESTFWD"))` before `exec`-ing the real `qemu-system-<arch>` binary on `PATH` (skipping relative `PATH` entries and the wrapper's own directory via `paths.Physical`):
    - **Single netdev enforcement:** `RestrictNetdev` scans for `-netdev` (or `--netdev`). It appends `,restrict=on` to the single `user,id=net0,...` argument. If `DEVVM_GUESTFWD` is set (`NetProxy`), it also appends `,guestfwd=tcp:192.168.5.4:3128-cmd:<cmd>` (escaping commas as `,,`), where `<cmd>` is `'/path/to/devvm' _qemu connect 127.0.0.1:<ProxyPort>`.Guest processes reach the proxy at `http://devvm:<password>@192.168.5.4:3128`.
    - **Fail-closed argument validation:** `RestrictNetdev` leaves non-network probe calls (such as `-accel help` or `-netdev help` lacking `id=`) untouched, but returns `msg.QEMURefused()` if there is more than one `-netdev`, if `-netdev` does not start with `user,id=net0,` or already contains `restrict=`, if `-netdev`'s value starts with `-`, if `-nic` or `-net` is anything other than `none`, or if `-readconfig` is passed.
    - **`NetInternet` on Linux:** Because QEMU SLIRP `restrict=on` blocks all external internet access alongside host loopback, `NetInternet` builds on Linux run without the QEMU wrapper (`NetProxy` and `NetNone` use the wrapper).
  </Tab>
</Tabs>

### Lifeline tracking, degraded boots, and broken-state recovery

- **Confinement verification (`recordStart` and `Running`):** Once `limactl start` succeeds, `recordStart` reads the hostagent PID from `<LimaHome>/<vm>/ha.pid` and writes `"<pid> <c.Net>\n"` to `~/.local/share/devvm/vms/<hash>/run/started`. `Running()` compares the live PID inside `ha.pid` (and checks `daemon.Alive(pid)`) against `started`: `Run.Confined` is `true` only if the PID matches and was started with `NetProxy`. If a user starts the VM manually via `limactl start`, `ha.pid` receives a new PID, `Run.Confined` evaluates to `false`, and `devvm ls` reports the VM as `unenforced`.
- **Degraded boot check (`degraded`):** `limactl start` exits non-zero when a guest boots and answers SSH but a cloud-init or guest readiness script reports a warning. When `limactl start` fails, `degraded` inspects the VM: if Lima reports `Status == "Running"`, `devvm` runs `limactl shell --workdir / <vm> sh -c "cd '/' && exec 'true'"` with a `60s` timeout (`sshTimeout`). If SSH succeeds, `Start` emits `msg.StartedWithWarnings(vm)` and continues; if SSH times out, `Start` stops the VM with `limactl stop --force` and returns `msg.StartNoSSH`.
- **Settling interrupted QEMU starts (`settled`):** On slow or nested-virtualization Linux hosts, an interrupted start can leave `hostagent` running while QEMU has not yet written its PID file (`Status == "Broken"`, `HostAgentPID != 0`, `DriverPID == 0`). Killing `hostagent` with `--force` in that window orphans the starting QEMU process, holding the disk lock. `Stop` calls `settled`, polling every `100ms` up to `30s` (`driverWait`) until `DriverPID != 0` before running `limactl stop --force`. Conversely, if `Start` finds `Status == "Broken"` with `DriverPID != 0 && HostAgentPID == 0`, it runs `limactl stop --force` first to release the orphaned QEMU lock.

---

## Host diagnostics and cache housekeeping

### Host problem detection (`Problems`)

`lima.Problems(ctx)` (`internal/backend/lima/check.go`) implements `Host.Problems` for `devvm doctor` and pre-launch checks (`backend.Check`):

| Problem Key | Condition Checked | Auto-Fix Command (`Problem.Fix`) |
| --- | --- | --- |
| `os` | `l.OS != "darwin" && l.OS != "linux"` | None (`lima_host_unsupported`) |
| `arch` | `l.Arch != "arm64" && l.Arch != "amd64"` | None (`lima_arch_unsupported`) |
| `macos` | macOS major version `< 13` (`minMacOS = "13"`) | None (`program_old`) |
| `lima` | `limactl` missing, fails `--version`, or `< 2.2` (`minLima = "2.2"`) | `brew install lima`, `brew reinstall lima`, or `brew upgrade lima` (when `brew` is on `PATH`) |
| `qemu` | On Linux, `qemu-system-<arch>` or `qemu-img` missing from `PATH` | First detected package manager (`apt-get`, `dnf`, `pacman`, `zypper`) command |
| `kvm` | On Linux, `os.Stat("/dev/kvm")` fails | None (`no_kvm`) |
| `kvm-access` | On Linux, `/dev/kvm` exists but `os.OpenFile(l.KVM, os.O_RDWR, 0)` fails | `sudo usermod -aG kvm <USER>` |
| `lima-config` | `<Dirs.Lima>/_config/default.yaml` or `override.yaml` exists | None (`lima_global_config`) |

### Disk space floors and cache pruning

`host.Floors()` (`internal/backend/lima/lima.go`) defines the minimum free space required on `Dirs.Lima` before disk-writing operations proceed (`backend.Space.Check` rounds each threshold up to whole GiB):

| Operation | macOS (`vz` / APFS) | Linux (`qemu` / `qcow2` overlays) |
| --- | --- | --- |
| `Floors.Base` (building `base`) | `8 GiB` | `8 GiB` |
| `Floors.Image` (building a layer image) | `2 GiB` | `8 GiB` (`+6 GiB` for full parent `qcow2` copy during build) |
| `Floors.VM` (creating and starting a VM) | `2 GiB` | `2 GiB` |
| `Floors.ForTools(n)` (building `n` shared tools) | `3 GiB + n * 512 MiB` | `9 GiB + n * 512 MiB` (`+6 GiB` for throwaway `devvm-tools-build` VM disk) |

For `devvm gc --caches`, `housekeeping.DevvmCache` (`internal/backend/lima/caches.go`) inspects Lima's shared download cache at `<UserCache>/lima/download/by-url-sha256/`. Because that cache is shared with the user's own Lima instances, `devvmEntry` only selects entries that meet all four conditions:
1. The entry is a directory whose `ModTime` is older than 1 hour (`cacheRecent = time.Hour`), avoiding active downloads.
2. `<entry>/url` hashes (SHA-256 hex) to the directory's basename.
3. The URL matches `Lima`'s Ubuntu cloud image regex (`^<ubuntuURL>/release-[0-9]{8}/ubuntu-24\.04-server-cloudimg-(arm64|amd64)\.img$`).
4. The URL is not referenced by any existing image record in `state.Images(dirs)` (`recordedURLs`).

`DeleteCache` re-verifies all four conditions on each entry before calling `os.RemoveAll`.

## Related pages

<CardGroup cols={2}>
  <Card title="VM lifecycle and state" href="/vm-lifecycle-and-state">
    How `internal/vm` maps project folders to VM names, persists `vm.json`, and orchestrates `Start` and remakes.
  </Card>
  <Card title="Images, layers, and content keys" href="/image-and-layer-model">
    How `Origin` steps, layer scripts, and shared tool formats combine into deterministic SHA-256 image keys.
  </Card>
  <Card title="Network tiers and host proxy" href="/network-tiers-and-proxy">
    How `devvm _proxy` enforces allowlist, internet, open, and offline policies for confined VMs.
  </Card>
  <Card title="Connect VMs with links, groups, and port forwards" href="/vm-links-groups-and-ports">
    How `vmssh.Forwards` and VM links operate over `Machines.SSH` and follow the backend `Lifeline`.
  </Card>
  <Card title="Diagnose host, boot, and SSH failures" href="/host-and-vm-diagnostics">
    Troubleshooting `devvm doctor` checks, `PinOlder` cloud image fallbacks, and SSH host-key pinning.
  </Card>
</CardGroup>
