check/skills/libvirt/SKILL.md
the `libvirt:` libvirt-RPC check verb for VMs — domain info, framebuffer screenshots, send-key, passwd, QMP, qemu-guest-agent client, snapshots, events — served out-of-process by candy/plugin-vm.
npx skillsauth add overthinkos/overthink-plugins libvirtInstall 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.
libvirt: is a DECLARATIVE check verb — authored as libvirt: <method> inside
a candy/box plan check:/run: step. It is NOT a host charly check
subcommand — a declarative check verb served out-of-process by
candy/plugin-vm (nested under charly check at runtime), parallel to the
kube:/adb:/appium: plugin verbs. The VM/libvirt-API implementation (and
the go-libvirt + kata-containers/govmm + libvirt.org/go/libvirtxml
dependencies) lives entirely in that external plugin, NOT in charly's core; at
check time the host dispatches libvirt: through the provider registry to the
out-of-process plugin (the same path a bed's checks take via charly check live / charly check run).
The verb maps every libvirt-RPC management/introspection surface a VM needs
beyond what charly vm (lifecycle) provides — domain info, framebuffer
capture, keyboard injection, live graphics-password patching, QMP passthrough,
the qemu-guest-agent client, snapshots, and lifecycle events. VM-only — it
needs a running libvirt domain.
charly core owns NO go-libvirt. The host pre-resolves the vm.yml entity →
libvirt domain target host-side and passes the resolved VM-entity name
(Runner.vmTargetName()) to the plugin, so the verb addresses the live domain
charly-<vm-entity> even when the deploy/bed name differs. The plugin opens the
libvirtd session connection and runs the method. Authoring is unchanged from a
built-in verb: you write libvirt: info, never plugin: libvirt.
libvirt: stepThe method name becomes the verb's YAML value (libvirt: <method>); the former
CLI positional args become sibling Op modifier fields. A query/probe is a
check: step; an action that changes domain state (send-key, passwd, snapshot
create/revert) is a run: step. Shared matchers (stdout:, stderr:,
exit_status:) and the artifact validators (artifact_min_bytes:,
artifact_min_dimensions:, artifact_not_uniform:) work like every other
verb. context: [deploy] — the verb needs a running VM; under charly check box (no running VM) it skips. Run a candy's baked libvirt: steps against a
live VM deployment with charly check live <vm> --filter libvirt.
Every method below is the libvirt: value; the former CLI flags are sibling
modifier fields on the same step.
Top-level:
| libvirt: value | Modifiers | Description |
|---|---|---|
| list | — | all domains on the session |
| info | — | state + graphics + uptime + agent |
| screenshot | artifact: (+ artifact_* validators) | libvirt DomainScreenshot → PNG |
| send-key | key: | keyboard injection (Linux keycode set); chord notation (key: "ctrl+alt+F2") |
| passwd | text: | live graphics password patch |
| qmp | text: (QMP command) + input: (JSON args) | QMP passthrough |
| domain-xml | — | dump the live domain XML |
| events | — | poll lifecycle state transitions |
| console | — | (stub) serial console — use charly vm console <vm> |
guest/* — qemu-guest-agent client:
| libvirt: value | Modifiers | Description |
|---|---|---|
| guest/ping | — | reach the agent |
| guest/info | — | agent capability list |
| guest/os-info | — | distro/kernel |
| guest/time | — | guest clock + host delta |
| guest/hostname | — | guest hostname |
| guest/users | — | logged-in users |
| guest/interfaces | — | IPs + MACs + stats |
| guest/disks | — | block devices |
| guest/fsinfo | — | mounted filesystems |
| guest/vcpus | — | guest-side online state |
| guest/exec | command: | run a command via guest-exec |
| guest/fstrim | — | run TRIM on guest filesystems |
snapshot/*:
| libvirt: value | Modifiers | Description |
|---|---|---|
| snapshot/list | — | list snapshots |
| snapshot/create | target: (snapshot name) | create a snapshot |
| snapshot/info | target: | show snapshot XML |
| snapshot/revert | target: | revert to a snapshot |
| snapshot/delete | target: | delete a snapshot |
The libvirt: <method> value validates against this method set (the
#LibvirtMethod enum on charly's core closed #Op) at charly box validate
time.
A handful of qemu-guest-agent ops carry no declarative method — guest file read/guest file write and guest fsfreeze status/freeze/thaw, plus the
ad-hoc flags --screen N (screenshot), --hold (send-key), --type spice|vnc / --persistent (passwd), --config (domain-xml), and --duration
(events/console). Because the verb is also surfaced under charly check at
runtime (attachNestedCheckPlugins, like kube/adb/appium), reach those
through the nested out-of-process command charly check libvirt guest file … /
charly check libvirt guest fsfreeze … for interactive operator use. They are
NOT part of the declarative libvirt: step vocabulary.
Every method maps to one go-libvirt RPC (plus, for guest/*, the
QEMUDomainAgentCommand passthrough). The method name mirrors the
libvirt/virsh vocabulary so skills translate cleanly. Design choices:
spice: verb's host-side
pre-resolution: vm.yml entity name → libvirt domain → live XML via
libvirtxml.Domain. Errors are the same across both.libvirt: screenshot) goes through
DomainScreenshot — QEMU's VNC framebuffer capture, returned as PPM, decoded
in-process and re-encoded as PNG. Independent of whichever graphics protocol
(SPICE or VNC) the VM exposes on the wire.DomainSendKey. A friendly
keyname map in candy/plugin-vm/libvirt_ops.go covers
letters/digits/modifiers/arrows/function keys. Supports chord notation
(key: "ctrl+alt+F2").<graphics type="spice|vnc" passwd="…"> attribute via
libvirtxml, then calls DomainUpdateDeviceFlags(LIVE).text: = the QMP method name,
input: = the JSON args). Flag 0 = QMP; HMP not exposed.guest-exec + polls guest-exec-status until exit
(command: carries the argv). stdout/stderr are base64-decoded from the agent
reply.Each step is its own child node of the candy/box, named by its id:; a
probe is a check: step, a state-changing action is a run: step. Run with
charly check live arch --filter libvirt.
# Sanity — the domain is up and the agent is reachable.
libvirt-domains-listed:
check: the session lists the arch domain
id: libvirt-domains-listed
libvirt: list
context: [deploy]
stdout:
contains: arch
libvirt-agent-reachable:
check: the domain reports a reachable guest agent
id: libvirt-agent-reachable
libvirt: info
context: [deploy]
stdout:
contains: "Agent: true"
# Framebuffer capture (independent of SPICE wire state).
libvirt-framebuffer:
check: a non-uniform framebuffer screenshot is captured
id: libvirt-framebuffer
libvirt: screenshot
artifact: /tmp/fb.png
context: [deploy]
artifact_not_uniform: true
# Keyboard injection via libvirt (alternate path to the `spice: key` verb).
libvirt-send-chord:
run: switch to VT2 via a libvirt key chord
id: libvirt-send-chord
libvirt: send-key
key: "ctrl+alt+F2"
context: [deploy]
# QMP escape hatch.
libvirt-qmp-status:
check: QMP reports the running domain status
id: libvirt-qmp-status
libvirt: qmp
text: query-status
context: [deploy]
stdout:
contains: running
# qemu-guest-agent (only if the guest has qemu-guest-agent installed).
libvirt-guest-uname:
check: the guest agent runs uname
id: libvirt-guest-uname
libvirt: guest/exec
command: uname -a
context: [deploy]
stdout:
contains: Linux
# Transactional testing: snapshot → … → revert → delete.
libvirt-snapshot-create:
run: snapshot the domain before a destructive step
id: libvirt-snapshot-create
libvirt: snapshot/create
target: pre-exp
context: [deploy]
libvirt-snapshot-revert:
run: revert to the pre-experiment snapshot
id: libvirt-snapshot-revert
libvirt: snapshot/revert
target: pre-exp
context: [deploy]
libvirt-snapshot-delete:
run: delete the snapshot
id: libvirt-snapshot-delete
libvirt: snapshot/delete
target: pre-exp
context: [deploy]
A libvirt: step can target a VM on a remote libvirt host. Set
CHARLY_LIBVIRT_URI=qemu+ssh://[user@]host/session: the host-side pre-resolver
opens the SSH connection, discovers the remote virtqemud session socket (via
id -u), and forwards it over the SSH channel — so DomainScreenshot,
DomainSendKey, QMP, etc. all work against the remote hypervisor, with the
artifact written by the artifact: modifier locally. Example:
CHARLY_LIBVIRT_URI=qemu+ssh://o.example.org/session \
charly check live arch --filter libvirt
guest/* methods require qemu-guest-agent installed and running in the guest
AND a <channel type='unix' ... name='org.qemu.guest_agent.0'/> in the domain
XML. The opencharly layer qemu-guest-agent drops this in for bootc VMs;
cloud-image VMs need to add it to cloud_init.packages: explicitly.
Use a libvirt: info step — if it reports Agent: false, the agent is not
reachable.
When a cloud-image VM uses charly_install.strategy: auto (the modern
default), the vm deploy's lifecycle hook (PrepareVenue → EnsureCharlyInGuest)
scps the host charly binary into the guest post-boot. The core charly binary is cgo-free for audio — the SPICE audio
channels (the sole opus/portaudio cgo consumers) were dep-shed out of core
into the out-of-tree candy/plugin-spice, gated behind -tags spice_audio.
ldd $(command -v charly) shows no libportaudio/libopus, so the scp'd binary
runs in the guest with NO portaudio/opusfile installed.
Minimum cloud-image package list for full check-live coverage:
cloud_init:
packages:
- sudo
- spice-vdagent # for spice-vdagentd
- qemu-guest-agent # for the `libvirt: guest/*` probes
The libvirt: verb (and every spice: check verb) routes through the libvirt
user-session daemon socket. The daemon must be running as a user-service:
systemctl --user enable --now virtqemud.service # libvirt ≥ 8 (modular)
# OR (older monolithic):
systemctl --user enable --now libvirtd.service
charly vm create best-effort starts these units before resolving the VM
backend (see /charly-vm:vm "Prereq"). The charly check run <bed> path for
VM-targeting beds (e.g. check-k3s-vm) invokes the same hook. If the unit
doesn't exist (libvirt not installed), the explicit-backend gate in
resolveVmBackend() surfaces a clear error pointing at the remediation.
spice: verb)The two VM verbs are single-protocol by design — both served out-of-process by
the candy/plugin-vm/candy/plugin-spice plugins, neither a host CLI command:
libvirt: — libvirt RPC only. Every call goes through libvirtd. Works
regardless of the VM's graphics protocol. Use when the thing under test is
"is the VM working?" (framebuffer capture, keyboard injection, snapshots,
domain state).spice: — SPICE wire only. Proves the SPICE protocol path is alive
end-to-end. Use when the thing under test is "is SPICE itself healthy?".For ambiguous display tests, run both and diff: compare libvirt: screenshot
against spice: screenshot — if both render the same pixels, the SPICE server
and the guest framebuffer agree.
The libvirt: verb is served OUT-OF-PROCESS by candy/plugin-vm (the
go-libvirt shed moved it out of charly core). The host dispatches it through
the provider registry, passing the resolved VM-entity name
(Runner.vmTargetName()) so the verb addresses the live domain
charly-<vm-entity>, not charly-<deploy-name>.
candy/plugin-vm/libvirt_cmd.go — the libvirt command tree + all method implementations.candy/plugin-vm/libvirt_methods.go — the method allowlist (op → subcommand-path + positional-args dispatch data).candy/plugin-vm/libvirt_ops.go — shared helpers (screenshot PPM→PNG decode, key-name → Linux keycode map).candy/plugin-vm/libvirt_guest_agent.go — typed client over QEMUDomainAgentCommand with methods for every QGA command in use.candy/plugin-vm/vm_target.go — VM target resolution; VmTarget.XML gives you the live libvirtxml.Domain for fast field lookups.These live in candy/plugin-vm (the out-of-process plugin), NOT charly core:
github.com/digitalocean/go-libvirt — pure-Go libvirt RPC client.libvirt.org/go/libvirtxml — typed domain XML parsing/generation.libvirt Arch package (libvirtd, session socket) is already a hard dep of the opencharly-git PKGBUILD./charly-check:check — parent router; the libvirt: verb catalog entry, the artifact-validation modifiers, and charly check live <vm> --filter libvirt./charly-check:spice — the sibling spice: SPICE-wire check verb, also served out-of-process by candy/plugin-vm/candy/plugin-spice./charly-internals:plugin — the out-of-process provider model that serves libvirt (candy/plugin-vm)./charly-vm:vm — the charly vm VM lifecycle command family (create/start/stop/destroy/ssh/console/gpu) — separate from this introspection verb.MUST be invoked before any work involving: the libvirt: check verb,
libvirt RPC-based VM inspection/management, qemu-guest-agent probing, VM
snapshots, domain event watching, or any VM-introspection surface beyond what
charly vm (lifecycle) provides.
tools
Use when authoring or modifying a charly PLUGIN — a candy with a `plugin:` block that contributes Providers (verbs/kinds/deploy-targets/steps/builders/commands), its own CUE schema, builtin (compiled-in) or external (out-of-tree git repo). Covers the unified Provider model, the per-plugin CUE-schema contract (single source → Go params for dev + schema-over-Describe RPC for runtime), the SDK, and the loader.
tools
The CUE data-validation / configuration CLI (cue), pinned to v0.16.1. Use when working with the cue candy, installing the cue binary into a box or onto a target:local dev host, or running the offline schema-vendoring pipeline that feeds charly's egress validation.
tools
CUE EGRESS validation — validating (and, where it adds value, generating) the config files charly WRITES to a system BEFORE the bytes hit disk. MUST be invoked before working on charly/egress.go, the vendored schemas under candy/plugin-egress/egress-schemas/vendor/, the ValidateEgress / registerVendoredEgressKind path, the offline `task cue:vendor` pipeline, or adding an egress schema for any written artifact (cloud-init, k8s manifests, traefik routes, runtime config, install ledger, systemd/quadlet units, ssh_config, libvirt XML).
tools
Kubernetes cluster-probe declarative check verb — the `kube:` check verb (nodes, pods, ingress, storage class, addon health, apply/delete, and arbitrary resource GETs) served out-of-process by the candy/plugin-kube plugin (vendored client-go; no external kubectl required).