agent-vm

module
v0.0.0-...-fb8325a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT

README

agent-vm

Isolated Linux development VMs for AI-assisted work on macOS, one VM per domain of work, via Lima. A VM is described by its own folder on the host: the config lives there, along with the keys, certificates and agent settings delivered into the guest. Driven by a single Go binary, avm.

Install

curl -fsSL https://raw.githubusercontent.com/MikD1/agent-vm/main/install.sh | sh

The installer supports macOS. It installs avm from the latest GitHub Release and checks for Lima, the only runtime dependency. If Lima is missing and Homebrew is already installed, the script runs brew install lima; if Homebrew is also missing, it exits with instructions for installing Homebrew or Lima manually. The installer does not install Homebrew itself.

Set AVM_INSTALL_DIR to choose the install directory, or AVM_VERSION to pin a release tag:

curl -fsSL https://raw.githubusercontent.com/MikD1/agent-vm/main/install.sh | AVM_INSTALL_DIR=/usr/local/bin AVM_VERSION=v0.1.0 sh

The installer finds the latest version through github.com's own /releases/latest redirect, so it needs no call to api.github.com — a host corporate proxies often block outright, and one GitHub rate-limits at 60 requests/hour for an unauthenticated IP. Either produces a 403 on a release that is public and downloads fine. The API is still tried as a fallback, and if both are unreachable the installer prints what each host answered; pinning AVM_VERSION skips release discovery altogether.

Install from source

For development, or when you already have Go installed:

go install github.com/MikD1/agent-vm/cmd/avm@latest
go install ./cmd/avm

Usage

A VM is a folder. Put its config there, along with anything the guest needs, and list the projects it works on:

mkdir -p ~/vms/work && cd ~/vms/work
avm init                 # write agent-vm.yaml, then edit it
avm create               # create + provision the VM (Record + VM)
avm shell work           # open a shell in the VM (lands in the guest home)

Each project in mounts is mounted read/write at ~/<name> in the guest; you edit on the host and the VM sees the changes. Commit/diff/branch inside the VM; push/pull on the host where credentials live.

Mounting a project at runtime

A project can be attached to an existing VM without editing the config by hand:

cd ~/projects/web
avm mount work           # mounts the current folder into the VM named `work`
avm unmount work web     # detach it again, by guest name or host path

Both commands update the VM's agent-vm.yaml, its Record, and the VM itself. Lima attaches folders at boot and will not rewrite a running instance's config, so a running VM has to be stopped and started again for the change to take effect — avm mount asks first. Declining leaves agent-vm.yaml and the Record updated but the VM as it was; a plain avm restart does not apply them. To apply them later, stop the VM and run the command again, or avm recreate <name>. Everything inside the guest survives the stop/start.

A VM's own directory cannot be mounted into it: it is already there, read-only, at /mnt/host/vm.

Two projects with the same folder name collide at ~/<name>; pass --name <name> to place one of them elsewhere.

Commands

Command Description
avm init [path] Write an agent-vm.yaml template. --force overwrites.
avm create [path] Create + provision the VM from a VM directory.
avm recreate <name> Pristine rebuild from the VM's current agent-vm.yaml (from the record when that folder is not on this host).
avm list [name] List VMs with registry status (managed / orphaned / unmanaged) and Lima runtime state. With a name, show that VM's mounts and tools.
avm mount <vm> [path] Mount a project folder into a VM. --name overrides the guest directory name.
avm unmount <vm> <path|name> Detach a project folder from a VM.
avm shell [name] Open a shell in the VM, at the guest home (~), with each mounted project one cd away.
avm start/stop/restart [name] Lifecycle controls.
avm delete <name> Stop + delete the VM and remove its record. --force skips confirmation.
avm prune [name] Remove orphaned records (record without a VM).

[name] defaults to the VM whose directory you are in (from name: in agent-vm.yaml, or the folder's basename). avm mount and avm unmount always take the name explicitly: they run from a project folder, not from the VM's.

Global flags
Flag Description
--verbose Show the full Lima log. By default only avm's own ==> progress plus Lima warnings and errors are shown; with --verbose every Lima line is shown. Either way the time=…level=… prefix and trailing fields are stripped to plain text. Colors honor NO_COLOR.

VM config — agent-vm.yaml

name: work           # optional; defaults to this folder's basename

modules:             # mise tools; a name with no version installs the latest
  - node: lts
  - go: "1.24"
  - claude

resources:
  cpus: 8            # default 4
  memory: 16GiB      # default 4GiB
  disk: 200GiB       # default 120GiB

# base: { image: corp-ubuntu }   # optional; default template:_images/ubuntu

files:               # host source → guest destination
  claude-settings.json: ~/.claude/settings.json
  codex-auth.json:
    to: ~/.codex/auth.json
    mode: "0600"

scripts:             # bash, run last, as root
  - provision/postgres.sh

mounts:              # the projects this VM works on
  - ~/projects/api   #   → ~/api in the guest
  - ~/projects/web   #   → ~/web
  - path: ~/other/api
    name: api-legacy #   → ~/api-legacy

Mount paths must be absolute or start with ~/, so a VM's contents never depend on where its folder happens to sit. Each mounts read/write at ~/<name>, where name defaults to the folder's basename.

Modules

A module is a mise tool: a name, optionally with a version. Anything in mise's registry works, as do backend references such as npm:@openai/codex or aqua:owner/repo — avm needs no change to support a new tool.

modules:
  - node: lts        # a pinned alias
  - go: "1.24"       # a pinned version
  - claude           # no version → the latest release
  - python: "3.12"

Tools install system-wide under /usr/local/share/mise, and their shim directory is on PATH in login shells, in the shell avm shell opens, and under sudo. avm records the versions mise actually resolved in the VM Record.

Every VM also gets, without being asked: the host CA certificates in the system trust store, a sanitized .gitconfig, git, curl, jq, ripgrep, fd, build-essential, Docker, and mise itself. The .gitconfig is read from <vm-dir>/.gitconfig if that file exists — put the VM's git identity there, per VM, next to agent-vm.yaml; every credential.* section is stripped on the way in, which is what makes it sanitized.

Config files

Tool configuration and credentials are declared in files, as host source → guest destination. Sources are relative to agent-vm.yaml, or absolute, and must sit inside the VM directory: the copy runs inside the guest, and the VM directory is the only host folder a files source can be read from. Keeping them there is also what keeps them out of any project repository.

files:
  claude-settings.json: ~/.claude/settings.json
  codex-config.toml: ~/.codex/config.toml
  tmux.conf: ~/.tmux.conf
  codex-auth.json:
    to: ~/.codex/auth.json
    mode: "0600"

The destination may be absolute or start with ~/. mode defaults to 0644; set it to 0600 for anything holding credentials. A directory source is copied recursively and keeps its own permissions, so mode is not allowed on one.

Files are copied at provision time. Edit them and run avm recreate <name> to apply changes to an existing VM.

Provisioning scripts

For anything neither a tool nor a file — a database, a service, a corporate setup step — list bash scripts in scripts. They run last, in order, as root, with VM_USER, VM_HOME, and VM_CONFIG set, and a non-zero exit fails provisioning.

scripts:
  - provision/postgres.sh

Root's PATH carries no mise shims during provisioning, so to use an installed tool, drop to the VM user's login shell:

sudo -u "$VM_USER" -H bash -lc 'claude plugin install some-plugin'

How it works

avm is a Go orchestrator over three layers with narrow interfaces:

  1. Go CLI parses config, owns the registry, and plans provisioning. It is the only thing that reasons in domain terms.
  2. Lima virtualizes: avm shells out to limactl (a stable CLI contract) to create/start/shell/delete VMs.
  3. Bash provisioning runs inside the guest in a fixed phase sequence: system layer (certificates, trust) → platform (apt packages, Docker, mise) → tools (one mise install) → config files → user scripts → restart. Only the tools phase depends on the project; everything else is the same on every VM.
Two config artifacts
  • VM Spec (agent-vm.yaml, in the VM's folder) — portable intent: modules, resources, mounted projects, files, scripts, optional base image.
  • VM Record (~/.config/agent-vm/vms/<name>.yaml, host-local) — the tool's materialization of one Lima VM (resolved spec + create-time facts). avm reconciles the registry against Lima on every list and labels each VM managed, orphaned (record without VM), or unmanaged (VM without record). create writes the record first; if provisioning fails the VM is rolled back and the record is kept as orphaned, recoverable via recreate/prune.
Provisioning phases
Phase 0  create + start the VM from the base image
Phase 1  system layer — install host CA certs into the trust store, export trust
         env globally (tools never touch certificates)
Phase 2  platform — apt packages, Docker, mise itself; always installed, never
         selected by a project
Phase 3  tools — one `mise install` for every module in the spec, retried up
         to three times so a stalled download does not cost the whole VM
Phase 4  config files — copy each `files` entry from the VM directory to its
         guest destination
Phase 5  user scripts — run each `scripts` entry, in order, as root
Phase 6  restart — applies group membership (docker) and anything else a live
         session holds

Each script runs as root with a small env contract: VM_USER, VM_HOME, VM_CONFIG (/mnt/host/vm, the VM directory, read-only).

A phase is quiet while it runs: avm buffers the guest's stdout and streams only its stderr. Phase 2 downloads mise from GitHub's release CDN — a host none of the earlier phases touch — so a network path that blocks or throttles it fails there and nowhere else. That download takes the compressed archive (~22 MB, not the ~95 MB bare binary), is bounded (connect timeout, stall guard, retries), resumes a partial file instead of restarting it, and prints its progress every 20s. A failure names the URL to measure by hand.

Custom CA certificates

Drop the root CAs into <vm-dir>/ca-certificates/; the Phase 1 system layer installs them into the VM trust store and exports the trust env globally, so node/git/python/curl/mise all inherit it with no per-tool configuration.

Any regular file in that folder is read, whatever its extension, and both PEM and DER encodings are accepted — a corporate root CA exported from a Windows trust store as corp-root.crt works as-is. Put the whole chain in if the proxy presents one (root plus any subordinate CAs), one file per certificate or several in one PEM.

On macOS, a CA already trusted by the host can be exported straight out of the keychain:

security find-certificate -a -c "<certificate name>" -p \
  /Library/Keychains/System.keychain > ~/vms/work/ca-certificates/corp-root.pem

Phase 1 says on screen what it found — one system: trusting … line per certificate with its subject, the installed count, and a warning naming any file that was not a certificate. It then checks that TLS to github.com and api.github.com actually verifies, so a network that inspects TLS fails there, named, rather than in Phase 3 as invalid peer certificate: UnknownIssuer against a tool name.

If you see that error from mise in Phase 3, the VM does not trust the CA your network re-signs with: add it here and run avm recreate <name>. Check what the VM sees from inside a working VM with openssl s_client -connect api.github.com:443 < /dev/null — Verify return code: 0 (ok) means the trust store is right.

Security

  • Each domain of work is isolated in its own VM.
  • The VM directory is mounted read-only; the guest cannot write back to it.
  • Git credentials stay on the host — they never leave it.

Directories

Path Synopsis
cmd
avm command
internal
cli
config
Package config defines the VM Spec, its validation, and the resolution funnel (flags > VM Spec > built-in defaults) that produces a Resolved config feeding both the Lima template and the VM Record.
Package config defines the VM Spec, its validation, and the resolution funnel (flags > VM Spec > built-in defaults) that produces a Resolved config feeding both the Lima template and the VM Record.
lima
Package lima is the only package that shells out to limactl.
Package lima is the only package that shells out to limactl.
provision
Package provision drives the fixed phase sequence (create/start → system → platform → tools → files → scripts → restart) via a lima.Client.
Package provision drives the fixed phase sequence (create/start → system → platform → tools → files → scripts → restart) via a lima.Client.
registry
Package registry stores one VM Record per managed VM under <root>/vms and reconciles the registry against Lima's view of existence.
Package registry stores one VM Record per managed VM under <root>/vms and reconciles the registry against Lima's view of existence.
specedit
Package specedit edits an existing agent-vm.yaml in place, preserving the explanatory comments and key order the `avm init` template ships with.
Package specedit edits an existing agent-vm.yaml in place, preserving the explanatory comments and key order the `avm init` template ships with.
templates
Package templates provides the embedded Lima base template and the spec template written by `avm init`.
Package templates provides the embedded Lima base template and the spec template written by `avm init`.
vmname
Package vmname normalizes and validates VM names as Lima DNS labels.
Package vmname normalizes and validates VM names as Lima DNS labels.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL