# smolmachines as a VM backend for bot-bottle Evaluation of whether [smolmachines](https://smolmachines.com/) would simplify the macOS agent-VM-isolation work spelled out in [`agent-vm-isolation.md`](agent-vm-isolation.md). Research conducted 2026-05-11. ## Summary smolmachines replaces **four of the six subsystems** in `agent-vm-isolation.md` cleanly, including the two hardest ones — the `VZFileHandleNetworkDeviceAttachment` + gvproxy wiring and the PyObjC lifecycle wrapper. Pipelock stays. The disk image story changes from "sealed `.img`" to "OCI image + writable overlay," which is fine for the isolation goal as long as `-v` host mounts are forbidden in any bottle that maps to a smolmachine. Recommendation: adopt smolmachines as the macOS VM backend; keep pipelock DIY; wire the two via `--outbound-localhost-only` plus `HTTPS_PROXY` in the Smolfile, after smoke-testing that TSI passes through 127.0.0.1 traffic to a host-side pipelock. ## What smolmachines actually is - libkrun VMM linked as a library (no daemon); rides directly on Apple Hypervisor.framework on macOS and KVM on Linux. - Custom kernel is **not** supported — you get libkrunfw only. Day-to-day knobs are `command` and `env` in a TOML Smolfile. - Networking model: libkrun **TSI** ("Transport Socket Interface") — userspace socket hijacking inside the VMM library itself. DNS filtering is built in via vsock port 6002 — the guest's `/etc/resolv.conf` points at `127.0.0.1` and a guest-side DNS proxy tunnels queries over vsock to the host, which returns NXDOMAIN for anything not allow-listed. - vsock control plane is fully implemented with well-known ports: 5000 workload control, 5001 log streaming, 6000 agent OCI ops, 6001 SSH agent, 6002 DNS filter. - External integration is the CLI (`smolvm machine create/start/stop/exec`) or the HTTP API (`smolvm serve`). No Python SDK yet; Node.js embedded SDK exists but has a known bug where machines aren't visible to the CLI. ## Subsystem-by-subsystem comparison | # | Subsystem | DIY recipe (today) | smolmachines | Verdict | Caveats | |---|---|---|---|---|---| | 1 | MicroVM runtime | vfkit or PyObjC + Virtualization.framework, minimal device model | libkrun (library, no daemon) over Hypervisor.framework / KVM. libkrunfw kernel only. | Replaces | No custom kernel/initrd. | | 2 | Network attachment | `VZFileHandleNetworkDeviceAttachment` + unixgram socket → gvproxy userspace stack; DNS NXDOMAIN by default | libkrun TSI — userspace socket hijacking inside the VMM. CIDR allowlist enforced at the VMM layer; guest cannot bypass by dialing IPs. DNS filter via vsock port 6002. | Replaces | TSI is enabled when `--allow-cidr` / `--allow-host` is used; the alternative `virtio-net` backend does not support policy. | | 3 | Egress proxy (pipelock) | pipelock at `127.0.0.1:8888`, HTTPS MITM + DLP + allowlist | No analogue. Integration: `--outbound-localhost-only` + `env = ["HTTPS_PROXY=http://127.0.0.1:8888"]` in the Smolfile. | Irrelevant — keep pipelock | Whether TSI passes 127.0.0.1 traffic through to a host-side proxy is *unverified*; smoke test required. | | 4 | Control plane (vsock) | `VZVirtioSocketDeviceConfiguration` + `AF_VSOCK` in guest, Unix socket on host | Full vsock plane built in. External use via `smolvm machine exec` or the `smolvm serve` HTTP API. | Replaces | The well-known vsock ports are internal to smolmachines. Custom task protocols must use the HTTP API or open a fresh vsock port inside the guest. | | 5 | Disk image | Sealed virtio-blk raw image, no host mounts | OCI image + writable overlay (default 2 GiB, `--overlay` to tune). `-v HOST:GUEST` mounts use virtiofs. `.smolmachine` packs the whole rootfs. | Partial | Overlay is writable and lives on the host. For "no host filesystem visible to the guest," forbid `-v` mounts in bottles that map to smolmachines. | | 6 | Lifecycle wrapper | ~100 lines PyObjC + `subprocess.Popen` for gvproxy | CLI or `smolvm serve` HTTP API. | Replaces | No Python SDK yet. Drive via `httpx` to the HTTP API, or shell out to the CLI. Embedded Node.js SDK has a known bug (machines invisible to CLI) — avoid for now. | ## Caveats worth flagging before commitment - **No custom kernel.** If the agent-vm-isolation work assumed a hand-rolled kernel cmdline, that flexibility goes away. Smolfile `env` and `command` cover the everyday cases. - **`--allow-host` semantics.** Hostnames are resolved at VM start time and stored as `/32` CIDRs. All ports on resolved IPs are permitted — there is no destination-port filtering at the smolmachines layer. For the pipelock integration path this is acceptable because the right flag is `--outbound-localhost-only`, not `--allow-host`. - **TSI passthrough to `127.0.0.1`.** The TSI code path for localhost isn't explicitly documented. Validate with a pipelock instance *before* building around it: curl-from-guest → pipelock-on-host should succeed; curl to any other host should be blocked. - **Embedded SDK bug.** Machines created via the Node.js embedded SDK are currently invisible to the CLI. Use the HTTP API instead. - **Volume policy.** "No host filesystem visible to the guest" needs to be a manifest-validation rule (no `-v` mounts in microvm-backed bottles), not just a documentation note. ## Recommendation **Adopt smolmachines as the bottle VM backend on macOS; keep pipelock DIY.** The work in `agent-vm-isolation.md` is mostly the network-attachment plumbing and the PyObjC wrapper — exactly the parts smolmachines eliminates. What remains (pipelock integration, picking the right networking flag, deciding on volumes vs. sealed overlay) is the work that needs doing regardless of the VM backend. This aligns with the borrowable idea identified in [`agent-sandbox-landscape.md`](agent-sandbox-landscape.md) — a `"runtime": "microvm"` opt-in field on a bottle. smolmachines is the most plausible concrete implementation of that field on macOS today. ### Prereqs before this becomes more than a research note 1. **Smoke-test TSI → pipelock on localhost.** Confirm the guest can reach `127.0.0.1:8888` on the host through TSI when launched with `--outbound-localhost-only`, and that all other hosts are blocked. 2. **Decide volume policy.** Add a manifest-validation rule disallowing `-v` mounts in any bottle with `"runtime": "microvm"`. 3. **Decide control-plane shape.** Either drive smolmachines via the HTTP API (`smolvm serve` as a long-running sidecar) or via CLI subprocess invocation per bottle. The HTTP API is the cleaner long-term path; CLI subprocesses are the lower-overhead first iteration.