check/skills/spice/SKILL.md
the `spice:` SPICE-wire check verb for VMs — handshake, native-SPICE display screenshots, and input injection, served out-of-process by candy/plugin-spice (the vendored Shells-com/spice library).
npx skillsauth add overthinkos/overthink-plugins spiceInstall 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.
spice: is a DECLARATIVE check verb — authored as spice: <method> inside a
candy/box plan check:/run: step. There is no host charly check spice
command. The SPICE-wire implementation (and the upstream SPICE client
library + its cgo opus/portaudio audio transitives) was dep-shed into the
out-of-tree candy/plugin-spice plugin module; at check time the host
dispatches spice: through the provider registry to that out-of-process
plugin (the same path a bed's checks take via charly check live / charly check run). This mirrors the adb: / appium: / kube: externalizations
(see charly/check_cmd.go).
The verb proves the SPICE server is speaking the protocol correctly on the
wire — not just that the TCP port is open: main-channel handshake, auth,
channel enumeration, native display-channel image decode, and input injection.
VM-only — it needs a running libvirt VM that exposes a <graphics type='spice'>
device.
charly core owns NO go-libvirt. The host (preresolveSpiceEndpoint,
charly/spice_preresolve.go) DELEGATES the vm.yml → libvirt-domain → live-XML →
<graphics type='spice'> resolution to the out-of-process vm plugin
(invokeVmPlugin("resolve-spice", …) → candy/plugin-vm's ResolveVmTarget /
SpiceEndpoint, where the go-libvirt deps live), passing the resolved VM-entity
name (Runner.vmTargetName()) as the domain target. It opens any qemu+ssh:// side
tunnel itself from the returned endpoint and hands the spice plugin a plain
DIALABLE endpoint via the CheckEnv. The spice plugin just dials it and runs the
method.
spice: verb in a plan stepThe verb is one inline Op carried by a step node in the candy/box plan (a
display/handshake probe is a check: step; an input action that changes guest
state is a run: step). The method name becomes the verb's YAML value
(spice: <method>); the former CLI positional args become sibling Op modifier
fields:
| Method | Declarative form | Modifiers | Description |
|---|---|---|---|
| status | spice: status | — | handshake + channel enumeration (first line SPICE: ok) |
| screenshot | spice: screenshot + artifact: | artifact: | native SPICE display-channel decode → PNG |
| cursor | spice: cursor + artifact: | artifact: | capture cursor bitmap + position → PNG |
| click | spice: click + x:/y: | x:, y:, button: | mouse press/release via the inputs channel |
| mouse | spice: mouse + x:/y: | x:, y: | pointer move (no click) |
| type | spice: type + text: | text: | type text as PC-AT scancodes |
| key | spice: key + key: | key: | press one named key (Return, Escape, F2, …) |
The 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, and a SPICE-less deployment (e.g. a GPU desktop with no <graphics type='spice'> device) is reported N/A SKIP.
The method-name vocabulary (status/screenshot/cursor/click/mouse/
type/key) and every modifier stay on charly's core closed #Op — so
authoring is UNCHANGED from a built-in verb (spice: status, not plugin: spice); the plugin advertises the verb with no plugin_input.
Example — each step is its own child node of the candy/box, named by its id::
spice-handshake:
check: the SPICE server completes the handshake and enumerates channels
id: spice-handshake
spice: status
context: [deploy]
stdout:
- contains: ok
- contains: "inputs: ready"
desktop-rendered:
check: the SPICE display channel decodes a non-uniform framebuffer
id: desktop-rendered
spice: screenshot
artifact: /tmp/spice-shot.png
context: [deploy]
artifact_not_uniform: true
Input injection is authored as run: steps (each one mutates guest state) — a
console login sequence walks the form with spice: key / spice: type steps:
drive-login:
run: type the username and password at the console
id: drive-login
spice: key
key: return
context: [deploy]
# … followed by sibling `spice: type` (text: arch) and `spice: key` (key: tab)
# steps to walk the login form.
A spice: step can target a VM on a remote libvirt host. Set
CHARLY_LIBVIRT_URI=qemu+ssh://[user@]host/session (the former --uri flag
carried this same env): the host-side pre-resolver runs locally, discovers the
remote SPICE endpoint, and opens the side tunnel transparently — libvirt RPC
rides the SSH control channel; the SPICE display channel gets a dedicated
forward.
<listen type='socket'/> (the arch default after the
socket-listen cutover), the host forwards the UNIX socket.127.0.0.1:<random> forward.GUI clients (virt-manager, remote-viewer --connect qemu+ssh://…) don't need
any charly involvement for socket listeners — they auto-forward via libvirt
RPC fd-passing. See /charly-vm:arch "Connecting from a remote workstation"
for the complete story.
libvirtxml.Domain, and extracts the
SPICE host/port/passwd from the <graphics type='spice'> element (honoring
autoport='yes') before handing the plugin a dialable endpoint. The operator
escape-hatches the former CLI exposed (--address, --socket, --password)
are NOT part of the declarative verb — the endpoint comes from the bed's VM
context.spice: screenshot is a native SPICE
display-channel decode (NOT libvirt DomainScreenshot) — it proves the SPICE
display path renders pixels end-to-end. type/key emit PC-AT scancodes
(the friendly-keyname → scancode table lives in the plugin).-tags spice_audio, so the upstream library's cgo
opus/portaudio audio channels are not linked. Complex agent-channel
operations (clipboard, resolution change); the upstream library exposes them
but the verb doesn't wrap them yet — use the libvirt: verb (libvirt: passwd, libvirt: guest/exec) for the corresponding management ops.libvirt: verb)The two verbs are single-protocol by design:
spice: — every byte flows through the SPICE wire. Use when the thing
under test is "is SPICE itself healthy?".libvirt: — every call goes through libvirtd RPC. Use when the thing
under test is "is the VM working?" (framebuffer capture, keyboard injection,
snapshots, domain state). The libvirt: verb is likewise a declarative check
verb served out-of-process — by candy/plugin-vm (see /charly-check:libvirt).For input testing, prefer spice: type/key/click to prove the SPICE wire
delivers input to the guest. For display testing, compare spice: screenshot
against libvirt: screenshot — if both render the same pixels, the SPICE
server + the guest framebuffer agree.
The spice: verb and its SPICE-wire client live in the out-of-tree
candy/plugin-spice plugin module (an external-charly-verb plugin), NOT in
charly's core (which carries no SPICE library and no opus/portaudio cgo deps).
candy/plugin-spice/provider.go — the out-of-process verb provider: it
dials the host-pre-resolved endpoint, dispatches the method, then
self-evaluates the stdout/stderr/exit_status matchers + the artifact
validators itself.candy/plugin-spice/session.go / methods.go — the connection wrapper over
the SPICE library's Connector/Driver interfaces and the per-method
implementations.candy/plugin-spice/third_party/spice — the vendored Shells-com/spice
library, built without the audio tag (ch-audio-stub.go).candy/plugin-spice/schema/spice.cue — the plugin's served CUE schema (the
verb keeps its discriminator + modifiers on core #Op, so the schema carries
no plugin_input).Host side:
charly/spice_preresolve.go — preresolveSpiceEndpoint: delegates vm.yml →
libvirt domain → live XML → SPICE endpoint to the vm plugin, opens any
qemu+ssh:// side tunnel, and hands the spice plugin a dialable SpiceEnv via
the CheckEnv. Stays in core (it owns no go-libvirt).candy/plugin-vm/vm_target.go — the OUT-OF-PROCESS VM target resolution
(ResolveVmTarget / SpiceEndpoint, go-libvirt); VmTarget.XML gives the live
libvirtxml.Domain. The host reaches it via invokeVmPlugin("resolve-spice", …),
passing Runner.vmTargetName() (the resolved VM-entity name, not the deploy name).providerRegistry.ResolveVerb("spice") → the
out-of-process grpcProvider → invokeVerbProvider, which hands the plugin
the full #Op as params after the host pre-resolves the endpoint.These now live in candy/plugin-spice, NOT in charly's core:
github.com/Shells-com/spice (MIT) — SPICE client library, vendored under
third_party/spice.github.com/hraban/opus + github.com/gordonklaus/portaudio — cgo audio
transitives, indirect and NOT linked (the plugin is built without
-tags spice_audio).The VM target resolution (go-libvirt / libvirtxml) runs OUT-OF-PROCESS in
candy/plugin-vm/vm_target.go — the host reaches it via
invokeVmPlugin("resolve-spice", …), not a direct core call, and passes the
resolved VM-entity name (Runner.vmTargetName()) so the plugin addresses the live
domain charly-<vm-entity> even when the deploy/bed name differs.
/charly-check:libvirt — the sibling declarative libvirt: check verb,
served out-of-process by candy/plugin-vm (libvirt RPC: framebuffer
screenshot, send-key, QMP, snapshots, guest agent)./charly-check:check — the unified check system and the Op (a plan step)
that holds every verb discriminator + modifier./charly-vm:arch — the arch VM that ships SPICE by default; "Connecting from
a remote workstation"./charly-internals:plugin — the external-charly-verb plugin model the spice
verb follows.MUST be invoked for any task involving the spice: declarative check verb,
SPICE protocol debugging, or proving a libvirt VM's SPICE display path is alive
end-to-end. Invoke this skill BEFORE reading the plugin's Go source.
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).