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.
Overview
Section titled “Overview”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"]Isolation model
Section titled “Isolation model”Each agent runs inside a QEMU MicroVM (microvm.hypervisor = "qemu",
optimize.enable = true). The containment boundary per VM is:
- Private network segment — its own
tapinterface (vm-*) bridged tovmbr0, a static IP in10.200.0.0/24, and a unique MAC. - Read-only Nix store —
/nix/storeis shared in read-only via virtiofs (ro-storetag, 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) andhome.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(tagproject). - VSOCK SSH — a unique VSOCK CID per VM enables
microvm -s <name>from the host; root login isprohibit-password(key only). - Resource QoS — every agent VM runs in
agent.slicewith 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.
VM inventory
Section titled “VM inventory”| 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.
Agent types
Section titled “Agent types”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:
pi—llm-agentsflake (agent-jadorey).hermes— officialNousResearch/hermes-agentflake (agent-finalform), with themessaging,voice, andexadependency groups enabled, plus extra tooling (pandoc, imagemagick, ffmpeg, jq, bat) and rootless Podman withdockerCompat. 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.
Host networking (@luxus/microvm-host)
Section titled “Host networking (@luxus/microvm-host)”services/microvm-host/default.nix (role server, on vanessa) provides the
shared infrastructure:
- Imports
inputs.microvm.nixosModules.host. - Bridge
vmbr0— systemd-networkdnetdev/networkwith host IP10.200.0.1/24. Tap interfaces matchingvm-*are attached to the bridge. - NAT —
networking.natwithexternalInterface = "br0"andinternalInterfaces = [ "vmbr0" ](IPv4 only). This gives guests internet access out through the LAN-facing bridge. - DNS —
dnsmasqbound tovmbr0+lo, forwarding upstream to192.168.1.1. Guests point their resolver at the host gateway10.200.0.1. dnsmasq is orderedafter/requiresthe bridge device. - Firewall —
vmbr0is a trusted interface; UDP/TCP 53 are opened for DNS. agent.slice— a systemd slice for all agent MicroVMs with accounting on and fairCPUWeight = 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 thebr0bridge itself (enp4s0 bridged intobr0, shared with the Home Assistant VM) and trustsbr0. It deliberately does not import the microvm host module — that belongs to@luxus/microvm-host.
Guest networking
Section titled “Guest networking”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.
Port forwarding
Section titled “Port forwarding”@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.
Per-VM orchestration (@luxus/agent-vm)
Section titled “Per-VM orchestration (@luxus/agent-vm)”services/agent-vm/default.nix (role host) turns one instance into:
microvm.vms.<vmName>—restartIfChanged = true,autostart, and the guest config built bymodules/nixos/agent-vm-guest.nixwith the instance settings.agent-vm-prep-<vmName>.service— aoneshot(RemainAfterExit) prep unit that builds the secret bundle and ensures the project root exists. It runsbeforebothmicrovm@<vmName>.serviceandmicrovm-virtiofsd@<vmName>.service.microvm@<vmName>.serviceoverrides —requires/afterthe prep unit, placed inagent.slice, with the QoS limits applied.- NAT port forwards — only when
forwardPortsis non-empty.
Secret flow
Section titled “Secret flow”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/<name>/{key,token,value}"] prep["agent-vm-prep-<vm>.service<br/>(oneshot, before microvm)"] bundle["/var/lib/microvm-bundles/<hostName>/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 --> hermesHost side: building the bundle
Section titled “Host side: building the bundle”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 as0444 root:rootnamed 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 as0444 root:rootunder<secretName>. - Ensures
projectRootexists (0755 luxus:users).
Guest side: mount and export
Section titled “Guest side: mount and export”modules/nixos/agent-vm-guest.nix mounts the bundle read-only via virtiofs:
source = /var/lib/microvm-bundles/<hostName>/secretsmountPoint = /run/host-secrets (readOnly)The zsh shellInit then exports variables three ways:
agent.secrets(per agent type) —export NAME=$(cat /run/host-secrets/NAME).- A scan of
/run/host-secrets(and~/.config/agent-secrets) that exports only files whose entire name is a validUPPER_SNAKEenv identifier — a stray*.sig/lowercase/dotfile can’t silently become a variable. - Declarative
environmentmappings — reads/run/host-secrets/<secretName>/{key,token,value}and exports under the chosenenvName.
Hermes service env (tmpfs)
Section titled “Hermes service env (tmpfs)”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.
Secrets per VM (from flake.nix)
Section titled “Secrets per VM (from flake.nix)”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 fromhermes-telegram-bot-token); plussecretFiles.HERMES_TELEGRAM_BOT_TOKENfor the name that doesn’t match 1:1.
QoS & systemd ordering
Section titled “QoS & systemd ordering”Ordering enforced by @luxus/agent-vm and @luxus/microvm-host:
agent-vm-prep-<vm>.service (oneshot) before: microvm@<vm>.service, microvm-virtiofsd@<vm>.servicemicrovm@<vm>.service requires/after: agent-vm-prep-<vm>.service Slice: agent.slicednsmasq after/requires: sys-subsystem-net-devices-vmbr0.devicePer-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.
Source of truth
Section titled “Source of truth”| 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 |
Operations
Section titled “Operations”Run on vanessa:
sudo systemctl status microvm@agent-jadoreysudo systemctl status microvm@agent-finalformsudo systemctl status agent-vm-prep-agent-finalformsudo journalctl -u microvm@agent-finalform -fssh luxus@10.200.0.5 # via the bridgeScripted runtime verification (run from emily after deploy):
scripts/verify-agent-vms.sh