Skip to content

Agent Vm

How the isolated agent MicroVMs on vanessa are built from Clan services: the host networking layer, the per-VM orchestration, the guest OS, and the secret flow that delivers API keys from host Clan vars into the guest.

This is reference documentation for the running system. The source of truth is always the code; see the table below.

Agent VMs are Clan service instances, not inventory.machines entries. They are declared in flake.nix and composed onto vanessa:

flake.nix inventory.instances
-> @luxus/microvm-host on vanessa
-> imports inputs.microvm.nixosModules.host
-> creates bridge vmbr0 (10.200.0.1/24)
-> provides NAT + DNS (dnsmasq) for guests
-> @luxus/agent-vm:agent-jadorey on vanessa
-> microvm.vms.agent-jadorey
-> modules/nixos/agent-vm-guest.nix (agentType = pi)
-> @luxus/agent-vm:agent-finalform on vanessa
-> microvm.vms.agent-finalform
-> modules/nixos/agent-vm-guest.nix (agentType = hermes)

vanessa is the only deploy target. Each @luxus/agent-vm instance defines exactly one microvm.vms.<vmName> on the host.

graph LR
ext["external iface (br0)<br/>NAT"] --- host["@luxus/microvm-host<br/>vmbr0 · 10.200.0.1/24<br/>dnsmasq DNS"]
host --- jadorey["agent-jadorey<br/>jadorey · 10.200.0.4<br/>tap vm-jadorey · pi"]
host --- finalform["agent-finalform<br/>finalform · 10.200.0.5<br/>tap vm-finalform · hermes"]

Each agent runs inside a QEMU MicroVM (microvm.hypervisor = "qemu", optimize.enable = true). The containment boundary per VM is:

  • Private network segment — its own tap interface (vm-*) bridged to vmbr0, a static IP in 10.200.0.0/24, and a unique MAC.
  • Read-only Nix store/nix/store is shared in read-only via virtiofs (ro-store tag, mounted at /nix/.ro-store).
  • Dedicated secret bundle — a per-VM virtiofs share mounted read-only at /run/host-secrets (see Secret flow).
  • Persistent volumes — two raw images per VM: persist.img/persist (4096 MB) and home.img/home/luxus (4096 MB). The home image keeps agent state, ~/.pi/~/.hermes, npm/pnpm globals, zellij/shell history, and dotfiles across reboots.
  • Project share — the host project directory is virtiofs-mounted read-write at /work (tag project).
  • VSOCK SSH — a unique VSOCK CID per VM enables microvm -s <name> from the host; root login is prohibit-password (key only).
  • Resource QoS — every agent VM runs in agent.slice with systemd CPU/Memory limits (see QoS & systemd ordering).

Guests authorize two SSH keys (the emily local key and the vanessa root / management key) for both luxus and root. The guest SSH host key is read directly from the persistent path /persist/ssh/ssh_host_ed25519_key, so ssh-keyscan stays stable across VM reboots.

Instance VM name Hostname IP Tap MAC VSOCK CID Agent type vCPU Mem
agent-jadorey agent-jadorey jadorey 10.200.0.4 vm-jadorey 02:00:00:00:00:03 3 pi 2 1024 MB
agent-finalform agent-finalform finalform 10.200.0.5 vm-finalform 02:00:00:00:00:04 4 hermes 4 4096 MB

Both VMs use /srv/agent-projects/<name> on the host, mounted at /work in the guest. Do not place new project shares under /home/luxus.

agentType (defined in services/agent-vm/default.nix) selects the CLI, default environment, expected secret names, and packages in modules/nixos/agent-vm-guest.nix. Active VMs today:

  • pillm-agents flake (agent-jadorey).
  • hermes — official NousResearch/hermes-agent flake (agent-finalform), with the messaging, voice, and exa dependency groups enabled, plus extra tooling (pandoc, imagemagick, ffmpeg, jq, bat) and rootless Podman with dockerCompat. Container storage is persisted on /persist/containers/storage.

Legacy types (opencode, claude) remain in agent-vm-guest.nix for reuse but have no running instances.

services/microvm-host/default.nix (role server, on vanessa) provides the shared infrastructure:

  • Imports inputs.microvm.nixosModules.host.
  • Bridge vmbr0 — systemd-networkd netdev/network with host IP 10.200.0.1/24. Tap interfaces matching vm-* are attached to the bridge.
  • NATnetworking.nat with externalInterface = "br0" and internalInterfaces = [ "vmbr0" ] (IPv4 only). This gives guests internet access out through the LAN-facing bridge.
  • DNSdnsmasq bound to vmbr0 + lo, forwarding upstream to 192.168.1.1. Guests point their resolver at the host gateway 10.200.0.1. dnsmasq is ordered after/requires the bridge device.
  • Firewallvmbr0 is a trusted interface; UDP/TCP 53 are opened for DNS.
  • agent.slice — a systemd slice for all agent MicroVMs with accounting on and fair CPUWeight = 100, so runaway agents can’t starve the rest of the host (Home Assistant VM, Bambuddy, etc.).

The settings in flake.nix override the module defaults: externalInterface = "br0", bridgeName = "vmbr0", hostIP = "10.200.0.1", subnet = "10.200.0.0/24".

The physical host (modules/hosts/vanessa/default.nix) owns the br0 bridge itself (enp4s0 bridged into br0, shared with the Home Assistant VM) and trusts br0. It deliberately does not import the microvm host module — that belongs to @luxus/microvm-host.

In agent-vm-guest.nix, each guest configures a static uplink via systemd-networkd (20-uplink): Address = <ip>/24, Gateway = 10.200.0.1, DNS = 10.200.0.1, DHCP off, wait-online disabled.

@luxus/agent-vm exposes a forwardPorts option that maps to networking.nat.forwardPorts (host port → <guestIP>:guestPort). Both current VMs set forwardPorts = [ ] (no host-side forwards); access is via the bridge IP or VSOCK SSH.

services/agent-vm/default.nix (role host) turns one instance into:

  1. microvm.vms.<vmName>restartIfChanged = true, autostart, and the guest config built by modules/nixos/agent-vm-guest.nix with the instance settings.
  2. agent-vm-prep-<vmName>.service — a oneshot (RemainAfterExit) prep unit that builds the secret bundle and ensures the project root exists. It runs before both microvm@<vmName>.service and microvm-virtiofsd@<vmName>.service.
  3. microvm@<vmName>.service overridesrequires/after the prep unit, placed in agent.slice, with the QoS limits applied.
  4. NAT port forwards — only when forwardPorts is non-empty.

Secrets never reach the guest by mounting Clan paths directly — the host UID/GID/mode of /run/secrets/... may not map correctly to the guest user over virtiofs. Instead, the prep service materializes a VM-readable bundle.

flowchart LR
vars["host Clan vars<br/>/run/secrets/vars/&lt;name&gt;/{key,token,value}"]
prep["agent-vm-prep-&lt;vm&gt;.service<br/>(oneshot, before microvm)"]
bundle["/var/lib/microvm-bundles/&lt;hostName&gt;/secrets<br/>0444 root:root"]
viofs["virtiofs share (tag: secrets, read-only)"]
guest["/run/host-secrets in guest"]
env["exported env vars<br/>(zsh shellInit)"]
hermes["/run/hermes/.env (tmpfs)<br/>hermes-gateway EnvironmentFile"]
vars --> prep --> bundle --> viofs --> guest
guest --> env
guest --> hermes

agent-vm-prep-<vmName>.service (in services/agent-vm/default.nix):

  • Clears and recreates /var/lib/microvm-bundles/<hostName>/secrets (0755 root:root).
  • For secretFiles (logical name → known host path): installs each file as 0444 root:root named after the logical key.
  • For environment (envVarName → logical secret name): probes at runtime for /run/secrets/vars/<secretName>/{key,token,value} (these paths only exist on the booted host, not at Nix eval time), and installs the first match as 0444 root:root under <secretName>.
  • Ensures projectRoot exists (0755 luxus:users).

modules/nixos/agent-vm-guest.nix mounts the bundle read-only via virtiofs:

source = /var/lib/microvm-bundles/<hostName>/secrets
mountPoint = /run/host-secrets (readOnly)

The zsh shellInit then exports variables three ways:

  1. agent.secrets (per agent type) — export NAME=$(cat /run/host-secrets/NAME).
  2. A scan of /run/host-secrets (and ~/.config/agent-secrets) that exports only files whose entire name is a valid UPPER_SNAKE env identifier — a stray *.sig/lowercase/dotfile can’t silently become a variable.
  3. Declarative environment mappings — reads /run/host-secrets/<secretName>/{key,token,value} and exports under the chosen envName.

For agentType = "hermes", the gateway is a persistent user service (systemd.user.services.hermes-gateway, with users.users.luxus.linger = true):

  • ExecStart = hermes gateway run, Restart = always, WorkingDirectory = /home/luxus.
  • EnvironmentFile = /run/hermes/.env.

A system.activationScripts.hermes-env-merge rebuilds /run/hermes/.env from scratch on every boot/activation: it lives on tmpfs (/run, mode 0600, owned by luxus) and is populated by iterating /run/host-secrets/* and writing NAME=<value> lines. Keeping it off the persistent home.img ensures plaintext secrets are never captured in backups/copies of the home image.

  • agent-jadorey (environment): OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENCODE_API_KEY, FIREWORKS_API_KEY, XAI_API_KEY, TELEGRAM_BOT_TOKEN, COHERE_API_KEY.
  • agent-finalform (environment): ANTHROPIC_API_KEY, EXA_API_KEY, TELEGRAM_BOT_TOKEN (mapped from hermes-telegram-bot-token); plus secretFiles.HERMES_TELEGRAM_BOT_TOKEN for the name that doesn’t match 1:1.

Ordering enforced by @luxus/agent-vm and @luxus/microvm-host:

agent-vm-prep-<vm>.service (oneshot)
before: microvm@<vm>.service, microvm-virtiofsd@<vm>.service
microvm@<vm>.service
requires/after: agent-vm-prep-<vm>.service
Slice: agent.slice
dnsmasq
after/requires: sys-subsystem-net-devices-vmbr0.device

Per-VM limits (systemd, accounting enabled):

VM CPUQuota MemoryHigh MemoryMax
agent-jadorey 250% 1152M 1280M
agent-finalform 350% 4096M 4608M

Hermes gets a more generous budget because of Podman + subagents + Playwright.

Concern File
VM inventory and identity flake.nix
Host bridge, NAT, DNS, microvm host import services/microvm-host/default.nix
Per-VM host orchestration, prep, secret bundle, port forwards services/agent-vm/default.nix
Guest OS config modules/nixos/agent-vm-guest.nix
Physical host config modules/hosts/vanessa/default.nix
Current ops notes machines/vanessa/README.md

Run on vanessa:

Terminal window
sudo systemctl status microvm@agent-jadorey
sudo systemctl status microvm@agent-finalform
sudo systemctl status agent-vm-prep-agent-finalform
sudo journalctl -u microvm@agent-finalform -f
ssh luxus@10.200.0.5 # via the bridge

Scripted runtime verification (run from emily after deploy):

Terminal window
scripts/verify-agent-vms.sh