ov-vms/skills/vms/SKILL.md
Authoring reference for kind:vm entities in vms.yml. Parallel to /ov-build:layer and /ov-build:image. Covers the VmSpec schema, source.kind discriminator (cloud_image vs bootc), base_user adopt pattern, and step-by-step recipes for both source kinds. MUST be invoked before authoring or editing vms.yml entries.
npx skillsauth add overthinkos/overthink-plugins vmsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
vms.yml is the authoring surface for kind: vm entities — VM primitives that pair with either a remote cloud-image URL (source.kind: cloud_image) or an in-repo bootc container image (source.kind: bootc). Loaded through overthink.yml includes: alongside image.yml. Entries are resolved by LoadUnified into VmSpec Go types (ov/vm_spec.go) and consumed by ov vm build, ov vm create, and ov deploy add vm:<name>.
The VM surface parallels the kind: image surface: one YAML entry per entity, kind-keyed, discovered through includes. The Go types that back it live in /ov-dev:vm-spec; the rendering paths in /ov-dev:libvirt-renderer and /ov-dev:cloud-init-renderer.
# vms.yml
vms:
<name>:
source:
kind: cloud_image | bootc
# cloud_image branch:
url: https://…
checksum: { type: sha256, value?: <hex> }
base_user: arch # adopt this account (see Adopt pattern below)
cache: ~/.cache/ov/vm-images/ # optional override
# bootc branch:
image: <kind:image entry name>
transport: registry | containers-storage | oci | oci-archive
rootfs: ext4 | xfs | btrfs
root_size: 10G # optional cap; rest of disk stays unpartitioned
kernel_args: "…"
# Hardware (both branches):
disk_size: 20G | "10 GiB"
ram: 4G | "8192M"
cpus: 4
machine: q35 | virt | i440fx # default: host-native
firmware: bios | uefi-insecure | uefi-secure # default: bios
# Network:
network:
mode: user | bridge | nat
bridge: br0 # only when mode=bridge
mac: "52:54:…" # optional pin; default stable-from-name
port_forwards: ["8080:80", …] # additive to SSH forward
# SSH + key injection:
ssh:
user: arch | root | … # defaults: cloud_image→"ov", bootc→"root"
port: 2222 # host port → guest :22
key_source: auto | generate | none | /abs/path.pub
key_injection:
smbios: auto | enabled | disabled
cloud_init: auto | enabled | disabled
# Cloud-init (structured intent):
cloud_init:
timezone: UTC
packages: [sudo, spice-vdagent, …]
runcmd: ["…", …]
ov_install:
strategy: auto | none
# Libvirt XML knobs (see /ov-dev:libvirt-renderer):
libvirt:
devices:
channels: […]
graphics: […]
video: [{model: virtio, vram: 65536, heads: 1, accel3d: false}]
rng: [{model: virtio, backend: /dev/urandom}]
memballoon: {model: virtio}
listen: field — three accepted shapesgraphics[].listen is how you control the <listen> children of a
<graphics> element. Three equivalent shapes, all unmarshal to the same
internal list:
# (1) Scalar address (shorthand for one TCP listener):
listen: 127.0.0.1
# (2) Single map (explicit type control — socket | address | network):
listen:
type: socket # libvirt auto-allocates the UNIX socket path
# or:
listen:
type: address
address: 127.0.0.1
# (3) List of maps (multiple listeners on one <graphics>):
listen:
- type: socket
- type: address
address: 127.0.0.1
Prefer type: socket for ov-managed VMs. virt-manager and
remote-viewer --connect qemu+ssh://… auto-forward UNIX sockets over
the libvirt RPC channel — GUI clients work out of the box against a
remote libvirt with zero ssh -L setup. TCP loopback listeners are
never auto-tunneled, by design. See /ov-vms:arch
"Connecting from a remote workstation".
Use when the VM is built from an externally published qcow2 (Arch cloud image from pkgbuild.com, Fedora Cloud, Ubuntu Cloud, Debian Cloud, CentOS Cloud, etc.). The build pipeline fetches the URL, integrity-checks it via sha256 (sidecar auto-resolved when checksum.value is empty), creates a qcow2 overlay at spec.disk_size, renders a NoCloud seed ISO, and hands off to libvirt/QEMU.
Canonical example: /ov-vms:arch. Only existing cloud_image VM in the repo — read it before authoring another one. It documents the non-obvious decisions learned the hard way:
/etc/default/grub (Arch's upstream issue with fbcon=nodefer). BIOS boot reads /boot/grub/grub.cfg directly from the root fs, which is always current./ov-dev:libvirt-renderer "video model choice" — virtio-gpu is the modern default for Linux guests.pacman -S spice-vdagent pulls in GTK3 + X11 (~200 MB download, ~1 GB installed); running at 2 GiB RAM stalls cloud-init. Size for the workload: 8 GiB / 4 cpus is reasonable for a workstation-class dev VM.arch, ubuntu, fedora, debian, cloud-user, etc.). This becomes source.base_user: — triggers the adopt pattern described below./ov-vms:arch as a template. Change url, base_user, distro-specific cloud_init packages: and runcmd:.bios unless the upstream image explicitly requires UEFI (e.g., secure boot lock-in).ov vm build <name> — observe the fetched qcow2 sha256 + rendered seed ISO path.ov vm create <name> + ov vm ssh <name> to verify cloud-init completed.Use when the VM is built from an in-repo bootc container image (a kind: image entry with bootc: true). ov vm build runs bootc install to-disk --via-loopback inside a privileged container to produce the qcow2/raw disk.
The 4 bootc VMs currently shipped (each with a dedicated thin skill):
| VM entity | Paired container image | Skill |
|---|---|---|
| aurora-bootc | /ov-foundation:aurora | /ov-vms:aurora-bootc |
| bazzite-ai-bootc | /ov-foundation:bazzite-ai | /ov-vms:bazzite-ai-bootc |
| openclaw-browser-bootc-bootc | /ov-openclaw:openclaw-browser-bootc | /ov-vms:openclaw-browser-bootc-bootc |
| selkies-desktop-bootc-bootc | /ov-selkies:selkies-desktop-bootc | /ov-vms:selkies-desktop-bootc-bootc |
The -bootc suffix is doubled on some entries because the paired container image already ends in -bootc (the VM is distinguished from an equivalent container-form deploy by the vms: namespacing, not by the name).
bootc: true declared and builds cleanly.vms: entry with source.kind: bootc + source.image: <entry-name>./ov-foundation:<name> skill's VM Configuration section for the authoritative numbers).ov vm build <vm-name>. See /ov-advanced:vm known-caveats section for bootc-specific gotchas (rootful storage split, nested-container --transport containers-storage, loopback device mount namespace).Mirrors the container-side base_user: + user_policy: adopt pattern documented in /ov-build:image "user_policy". The key insight: don't recreate accounts cloud-init already shipped — just append the SSH pubkey and move on.
When source.base_user: is set, the cloud-init renderer (/ov-dev:cloud-init-renderer::composeUsers) emits a merge-by-name entry:
# Rendered user-data
users:
- default
- name: <base_user>
ssh_authorized_keys:
- ssh-ed25519 AAAA…
cloud-init interprets users: [default, {name: <base_user>}] as "keep the distro's default account untouched, append SSH key to the named account". Result: no useradd, no sudoers rewrite, no shell change, no home-directory relocation — just the pubkey lands in ~<user>/.ssh/authorized_keys on first boot.
spec.ssh.user defaults to source.base_user, so ov vm ssh <name> connects as the adopted account without extra declaration.
Leave base_user: empty only when the upstream has no default account — in which case author a full custom user entry in cloud_init.users: with sudo/groups/shell fields. Don't do this when adopting works; useradd-at-first-boot races with other cloud-init modules and is harder to reason about.
ov_install.strategy: auto in cloud_init: wires ov's in-guest installer (/ov-dev:cloud-init-renderer → emitted runcmd: entries) so the provisioned VM comes up with ov already installed. Lets ov deploy add vm:<name> apply host-deploy-style layer recipes inside the VM over SSH without a bootstrap round-trip. See /ov-dev:cloud-init-renderer for the emission + handshake.
strategy: none skips the step entirely — useful when the VM will be managed by something other than ov after provisioning.
Load-time errors raised by ValidateVmSpec (ov/libvirt_validate.go, see /ov-dev:vm-spec):
source.kind must be one of cloud_image, bootc.cloud_image branch requires url: populated.bootc branch requires image: populated and pointing at a resolvable kind:image entry.firmware: must be one of bios, uefi-insecure, uefi-secure.network.mode: must be one of user, bridge, nat.ssh.key_source: must parse as auto, generate, none, or an absolute path.ssh.key_injection.smbios / .cloud_init must be one of auto, enabled, disabled.libvirt: structure is schema-validated via ValidateLibvirtConfig — invalid snippets fail fast.Projects predating this schema had three coupled fields on kind: image entries: bootc: true, vm: {...}, libvirt: [...]. All three were deleted in the hard cutover. Conversion is one-shot:
ov migrate vm-spec
Idempotent. Harvests the legacy fields into vms: entries, preserving any pre-existing vms: keys. See /ov-build:migrate for the full command reference and /ov-dev:cutover-policy for why hard-cutover was the chosen policy.
/ov-advanced:vm — the ov vm build/create/start/stop/ssh/console command family/ov-build:migrate — ov migrate vm-spec conversion from legacy/ov-core:deploy — ov deploy add vm:<name> for in-guest layer application/ov-vms:arch — canonical cloud_image VM/ov-vms:aurora-bootc, /ov-vms:bazzite-ai-bootc, /ov-vms:openclaw-browser-bootc-bootc, /ov-vms:selkies-desktop-bootc-bootc — bootc VMs/ov-dev:vm-spec — Go type reference/ov-dev:libvirt-renderer — libvirt XML emission/ov-dev:cloud-init-renderer — NoCloud seed ISO + user-data emission/ov-dev:ovmf — UEFI firmware path resolution (when firmware: ≠ bios)/ov-dev:vm-deploy-target — VmDeployTarget in the InstallPlan pipeline/ov-dev:cutover-policy — Hard Cutover by Default policy/ov-foundation:cloud-init — guest-side cloud-init layer (pairs with host-side cloud_init: emission)/ov-foundation:qemu-guest-agent — virtio-serial channel for host↔guest comms/ov-build:eval 10 standards)Changes that touch this verb's output must reach a healthy deployment on a target explicitly marked disposable: true (see /ov-dev:disposable). Use ov update <name> to destroy + rebuild unattended on any disposable target. Never experiment on a non-disposable deploy — set up a disposable one first with ov deploy add <name> <ref> --disposable or mark a VM in vms.yml.
After committing the source-level fix, ov update the disposable target ONCE MORE from clean and re-run the full verification. A fix that passes only on a hand-patched target is not a real fix — it's a regression waiting for the next unrelated rebuild. Paste BOTH the exploratory-pass output and the fresh-rebuild-pass output into the conversation.
Unit tests + a clean compile are necessary but not sufficient. See CLAUDE.md R1–R10.
development
Claude Code multi-agent support in Overthink — sub-agents, dynamic workflows, and agent teams, and how each drives the existing `ov eval` disposable beds to test and verify. MUST be invoked before authoring or invoking an ov sub-agent / dynamic workflow / agent team, wiring agent-lifecycle hooks, or asking "which primitive should drive the R10 beds?".
tools
Mounts a virtiofs share tagged `workspace` at /workspace inside a VM guest via a systemd .mount unit. Use when a kind:vm entity shares a host directory into the guest and you need it auto-mounted (and re-mounted at every boot).
development
MUST be invoked before any work involving: the `kind: android` schema kind, a `target: android` deploy, the `apk:` layer package format (installing Android apps declaratively), AndroidDeployTarget, an in-pod emulator OR a remote/physical adb-endpoint device, or nested `pod → android` deployment. The first-class Android device + app surface that sits above `ov eval adb`/`appium`.
tools
Use when committing, branching, pushing, merging, tagging, creating PRs, or approving/merging PRs with gh — the feat/-branch, R10-gated, never-force-push landing workflow across the main repo + the plugins submodule + image/<distro> submodules. Covers sync-to-upstream, branch/worktree pruning, the fork+PR path for contributors without write access, and cross-repo @github landing order.