jetkvm
Recipe card from the charly-check plugin (Commands — runtime CLI verbs).
JetKVM — IP-KVM control from a jetkvm: check verb
Section titled “JetKVM — IP-KVM control from a jetkvm: check verb”Overview
Section titled “Overview”jetkvm: is a DECLARATIVE check/control verb — authored as jetkvm: <method>
inside a candy/box plan check:/run: step. It is NOT a host charly check
subcommand: there is no charly check jetkvm. The verb’s implementation and its
WebRTC/HID client dependency live in the out-of-tree
candy/plugin-jetkvm plugin module; at check time the host dispatches jetkvm:
through the provider registry to that out-of-process plugin, exactly like
vnc:/adb:/cdp:.
It drives a JetKVM (https://jetkvm.com) with no browser, over the device’s own
control plane: local login (POST /auth/login-local) → the signaling websocket
(/webrtc/signaling/client) → the rpc JSON-RPC 2.0 data channel and the binary
hidrpc input channel, with the H.264 video track decoded to a PNG by ffmpeg
(required on the host).
Authoring a jetkvm: step
Section titled “Authoring a jetkvm: step”The method name is the scalar value for a bare-method step (jetkvm: status), or
the method: key of the jetkvm: map when the step carries jetkvm-exclusive
fields (host:, x:, y:, text:, key:, artifact:, …) — those live INSIDE
the jetkvm: map. Only the shared matchers (stdout:, stderr:,
exit_status:) and context:/id:/timeout: stay siblings. All jetkvm: steps
are deploy-context only (they need a reachable device), so author them with
context: [runtime].
- check: the JetKVM is reachable and its control channel answers context: [runtime] jetkvm: method: status host: jk.example.ts.net stdout: - contains: "rpc: true"
- check: a fresh framebuffer is captured as a real PNG context: [runtime] jetkvm: method: screenshot host: jk.example.ts.net artifact: /tmp/kvm.png artifact_min_bytes: 20000READ-ONLY BY DEFAULT — the device-safety gate
Section titled “READ-ONLY BY DEFAULT — the device-safety gate”A JetKVM is a physical appliance wired into a real machine’s keyboard, video
and power. Unlike a container it is NOT disposable, and a stray keystroke or power
action has real consequences. So every mutating method requires
allow_control: true; without it the step reports a documented skip naming
the gate rather than acting. factory-reset and update are refused even WITH
allow_control, because no unattended plan may wipe or reimage a device.
The classification is an allowlist, so a method added later is mutating-by-default and cannot touch a device until it is deliberately classified.
Methods
Section titled “Methods”Read-only (no gate): status, screenshot, ocr, version, video-state,
usb-state, atx-state, dc-state, virtual-media-state, storage-files,
wol-devices, macros, keyboard-layout, timezones, cloud-state,
network-state, network-settings, tailscale-status, update-status,
devmode-state, ssh-key, tls-state, extensions, public-ip,
diagnostics, check-media-url, usb-config.
Mutating (need allow_control: true): key, type, key-combo, macro,
click, mouse, move, scroll, drag, install, power, dc-power,
reboot, wol, wake-host, virtual-media, usb-device, usb-emulation,
set-settings,
set-edid, set-video, set-display, set-audio, set-network,
set-tailscale, set-devmode, set-ssh-key, set-tls,
set-keyboard-layout, set-macros, set-jiggler, set-extension,
set-wol-devices, set-log-level, renew-dhcp, rpc.
Never autonomous: factory-reset, update.
wake-host vs wol: wake-host sends the device’s own USB HID wake report
(upstream’s wakeHost RPC) and wakes a host that is display-asleep
(DPMS); wol sends a magic packet and only helps a host that is powered
off. A screenshot whose diagnostics boundary is “zero RTP”/“RTP stalled”
(the device sent no media — a host with no HDMI signal) now appends a hint
naming these two methods instead of a bare frame-wait timeout.
Screen reading — ocr and artifact_contains_text
Section titled “Screen reading — ocr and artifact_contains_text”Two wait-for-screen primitives, both running tesseract on the HOST (the plugin process is host-side, so the captured bytes are local — no venue round-trip):
ocr(read-only) captures a frame, OCRs it, and assertstextis present (case-insensitive). The method form: no artifact required.- check: the installer greeter is showingjetkvm: {method: ocr, text: "Press Return to Start Install"}context: [runtime]eventually: 120sretry_interval: 5sartifact_contains_text(onscreenshot) runs the SDK’s shared artifact validator over the pulled PNG — the same OCR assertion inscreenshotform, composable withartifact_min_bytes/artifact_not_uniform.
Failing to RUN is not failing to MATCH: a missing tesseract (or missing
tesseract-data-eng) returns a named error, never a vacuous “not found”.
The configurable console-installer driver — install + kind: jetkvm
Section titled “The configurable console-installer driver — install + kind: jetkvm”install (mutating) drives a text-console OS installer end to end with nobody at
the keyboard: it connects ONCE and walks an ordered steps: recipe, OCR-waiting
for each screen’s anchor before sending that screen’s key/type/combo input.
WHY ONE CONNECTION: the verb opens a fresh WebRTC session per dispatch call
(0.8–1.6 s connect, a 1.5–6.7 s /webrtc/signaling/client handshake per capture).
A ~15-screen wizard as a chain of check: steps pays that per screen; the driver
pays it once.
The recipe is DATA, supplied by a kind: jetkvm device entity — the plugin is
installer-agnostic (R3), so one plugin drives any text console. Example:
omarchy-kvm: jetkvm: installer: answer_secrets: password: OMARCHY_KVM_PASSWORD # credential store — never plaintext answers: username: omarchy hostname: omarchy steps: - wait_for: Press Return to Start Install action: key key: Return - wait_for: "Username>" action: type text: "{{username}}" # ...wait_forMUST be screen-UNIQUE. A string present on every screen (an OS logo, a window title) passes vacuously and desynchronises the whole drive — measured on Omarchy, whose logo OCRs on every wizard screen.action:iskey/type/key-combo; an action-less step is a pure wait.{{name}}placeholders intext:resolve fromanswers:(literal, single-pass — NOT charly${VAR}expansion, which does NOT reach plugin_input).answer_secrets:maps a placeholder name to a CREDENTIAL-STORE KEY, resolved at run time, so a password or LUKS passphrase never lands incharly.yml.timeout_sec:bounds each screen’s wait (default 120; the final reboot step wants a large value).
A step can also reference the entity by name (install: {device: omarchy-kvm}),
resolved out-of-process over the reverse channel; authored step steps:/answers:
win over the entity’s.
Authoring a kind: jetkvm entity needs a load-time anchor. A flat external
kind connects only when its plugin candy is in the LOAD-TIME candy scan closure
(a project candy’s candy:/require: ref), because kind decode runs at the loader
walk’s Boundary — BEFORE any deploy tree exists. A deploy-node add_candy: is too
late for a kind (it connects at the deploy/check walk, the VERB path), even though
the parse-time prescan recognizes the word. So a project authoring a kind: jetkvm
entity must also reference the plugin from a discovered candy (see distro-omarchy’s
candy/omarchy-kvm).
The driver is NOT a backup of the machine. A console installer wipes a disk.
The machine behind a KVM is never disposable, so an install drive is a DELIBERATE
OPERATOR ACTION — not a disposable: true CI bed. Author the read-only OCR
pre-flight as a check: and the destructive drive as a separate, operator-run
deploy.
The raw escape hatch — rpc
Section titled “The raw escape hatch — rpc”rpc invokes a device JSON-RPC method the plugin has not typed yet: author
rpc_method: and optionally rpc_params: (a JSON object). It is deliberately
MUTATING — it can invoke ANY device method, so an ungated escape hatch would
bypass the safety gate — and requires allow_control: true. Even then it refuses
the irreversible reimaging methods (factoryReset, tryUpdate,
tryUpdateComponents), matching the never-autonomous rule their typed
counterparts obey.
Credentials
Section titled “Credentials”Prefer password_secret: (a key in the verb:credential store, resolved
peer-to-peer over the reverse channel) or the JETKVM_AUTH_TOKEN /
JETKVM_PASSWORD environment variables. An auth_token: may also be authored
directly when a session already exists. Never commit a device password to a
charly.yml.
Connection details
Section titled “Connection details”host accepts a bare hostname, host:port, or an explicit http(s):// URL; a
bare value defaults to the device’s plaintext scheme. Set insecure: true to
accept the self-signed certificate a JetKVM ships by default (needed for an
https:// device; also required for the signaling websocket).
The device address is resolved in order: the authored host:, then the
JETKVM_HOST environment variable, then the deploy venue’s address. Author no
host: and set JETKVM_HOST to keep a device-specific hostname out of a
committed plan — the same shape JETKVM_AUTH_TOKEN / JETKVM_PASSWORD already
give credentials. No device hostname belongs in a repository.
Input methods: move/mouse are always a pure position move — an authored
button: is validated but never sent. click/drag press the button, which
defaults to left. x/y (and from_x/from_y) are absolute HID pointer
coordinates in [0,32767], not desktop pixels; map a desktop pixel (px,py)
on a W×H screen with px*32767/(W-1), py*32767/(H-1) (centre of 1920×1080
≈ 16384,16384). key and key-combo resolve over the common USB HID
Keyboard/Keypad usages — letters a–z, digits 0–9, F1–F12, the
navigation/editing keys (Enter, Escape, Tab, arrows, Home, End,
PageUp, PageDown, Insert, Delete, Backspace), punctuation, and
modifier chords like Control_L+Alt_L+Delete; names outside that set fail with
unknown key. Uppercase or shifted symbols (A, !) imply Shift.
NOTE on video: do not gate a screenshot on the device’s getVideoState.ready
field. Measured on firmware 0.5.9, ready reports whether the device’s native
capture PIPELINE is currently running — which is started per WebRTC session and
stopped when the last one disconnects — not whether an HDMI signal is present. It
reads false with error:no_signal even while screenshots succeed. The verb’s
screenshot method is the authority; it opens the session and waits for a frame.
Requirements
Section titled “Requirements”- A reachable JetKVM device on the network
ffmpegon the HOST running the check (it decodes the H.264 keyframe to PNG); provided by thelayer-ffmpegcandy
Running charly ON the device
Section titled “Running charly ON the device”A JetKVM runs an armv7l (32-bit ARM) uClibc/BusyBox userland with no package
manager, so the distro charly packages cannot be installed on it. To put
charly on the appliance, use the opencharly/charly-jetkvm tool, which delivers
the published release binary + welded plugin set over ssh:
charly-jetkvm install --host root@jk.example.ts.netcharly-jetkvm verify --host root@jk.example.ts.netcharly-jetkvm status --host root@jk.example.ts.netcharly-jetkvm uninstall --host root@jk.example.ts.netIt downloads the release assets on the HOST (gh, TLS-validated) and streams them
over ssh into /userdata/charly — the device’s own wget does not validate TLS,
so an on-device download would be MITM-exposed. verify asserts charly version
equals the requested CalVer and that a baked command word dispatches project-less.
The same tool derives the jetkvm: verb’s environment from the ONE ssh
connection — charly-jetkvm env --host root@<device> --export reads the device’s
local_auth_token and prints JETKVM_HOST + JETKVM_AUTH_TOKEN, so a device can
be driven with no committed hostname and no hand-copied token:
eval "$(charly-jetkvm env --host root@jk.example.ts.net --export)"charly check run jetkvm-device-readonly # status + screenshotcharly check run jetkvm-input-probe # move/click/drag/key/type + screenshotsRepair a device with no auth token. Upstream JetKVM mints
local_auth_token ONLY on a successful login/setup and never re-mints it at
startup, so a password-mode device whose token was cleared (logout, config
reset, a crash before save) answers HTTP 401 to every authenticated request.
charly-jetkvm auth heals exactly that: it writes a fresh uuid into
local_auth_token and restarts jetkvm_app — ONLY when the field is empty,
NEVER overwriting an existing token. env --heal repairs then prints:
charly-jetkvm auth --host root@jk.example.ts.net # print the tokeneval "$(charly-jetkvm env --host root@jk.example.ts.net --export --heal)"There is deliberately NO charly mcp serve on the device. The appliance has
~199 MB RAM and one core; running the MCP server forked a full CLI model and
exhausted memory, hanging the device. The appliance gets charly only for local
project-less verbs. Driving the KVM remains the host-side jetkvm: verb above,
which talks to the device’s control plane directly and needs no charly on it.
Console terminal session, flow, LUKS, and boot order — see the canonical skill
Section titled “Console terminal session, flow, LUKS, and boot order — see the canonical skill”Beyond the one-shot installer wizard, the jetkvm: verb exposes the
TRANSPORT-NEUTRAL console actions — open-terminal, run-command (with
sudo + expect + prompt_anchors), close-terminal, luks-unlock,
flow (continuous OCR-until-condition + if/then/else + case/switch +
bounded while, with reference-screenshot outcomes and auto-resume), and
boot-order (the OS-side efibootmgr). The SAME actions serve the
JetKVM (USB HID) and the VM (SPICE).
The mechanism, the authoring surface, the marker-line/echo-trap and
OCR-upscale RCAs, the flow vocabulary, and the measured hardware facts
are documented ONCE in the canonical /charly-check:console-automation
skill — read it before authoring any console session/flow. This skill
covers only the JetKVM-specific device surface (above).
Cross-References
Section titled “Cross-References”/charly-check:check— parent router; thejetkvm:verb dispatches out-of-process viacandy/plugin-jetkvm./charly-check:vnc— the sibling desktop-automation verb (RFB for a container/VM display rather than a physical KVM)./charly-check:adb— the sibling out-of-process device verb./charly-image:layer— layer authoring; the orderedplan:step list is part of everycharly.yml.
When to Use This Skill
Section titled “When to Use This Skill”MUST be invoked when the task involves a JetKVM device, the jetkvm: check
verb, IP-KVM remote control (keyboard/video/mouse or power over the network), or
capturing a screenshot from a machine via a JetKVM. Invoke this skill BEFORE
reading source code or launching Explore agents.