Skip to content

plugin-record

Placement runtime (out-of-process over gRPC)
Source github.com/opencharly/plugin-record/candy/plugin-record
Version 2026.182.1805
Candy plugin-record

This plugin is not listed in charly/charly.yml’s compiled_plugins:. It is not part of the shipped binary: charly builds and loads it out-of-process over gRPC when a plan references one of its words (the coexist path).

The reserved words this plugin serves:

  • record — verb class

OUT-OF-TREE charly plugin serving the record live-container check verb — a standalone Go module (go.mod + main.go) that manages recording sessions inside a running deployment (list, start, stop, cmd, gif) over go-plugin gRPC via the charly plugin SDK (github.com/opencharly/sdk). charly’s loader fetches this candy’s repo, go-builds the provider binary on the HOST, and serves it OUT-OF-PROCESS via LocalTransport — so the asciinema/wf-recorder/pixelflux recording driver lives HERE, out of charly’s core check surface (the first live-verb dep-shed of the EXEC-based kind, after the PORT-based candy/plugin-mcp/spice/kube). Since the schema-compaction cutover an authored record: step desugars to the internal plugin/plugin_input envelope, and every record-exclusive modifier (method/record_name/record_mode/record_fps/record_audio/text/artifact/settle_ms — record: run sends a command into the recording and waits for its output to settle) lives in the plugin’s OWN #RecordInput (schema/record.cue); the verb dispatches through the provider registry exactly like a built-in (ResolveVerb → grpcProvider → invokeVerbProvider hands it the full #Op). FIRST consumer of the executor reverse channel: record is EXEC-based, so the host attaches its live DeployExecutor over the E3b reverse channel and this plugin dials back through the SDK (sdk.ExecutorFromInvoke) to RunCapture the asciinema/wf-recorder commands in-container via tmux and GetFile the produced .cast/.mp4 artifact back to the host — it owns no podman / SSH machinery. The R10 consumer is a desktop pod bed whose check composes this plugin (the sway-browser-vnc bed’s record: start). Desktop (wf-recorder) recording on a VM/desktop venue needs the compositor session env: author record_env: (a string map, e.g. record_env: {XDG_RUNTIME_DIR: /run/user/1000, WAYLAND_DISPLAY: wayland-1}) to override the container defaults (/tmp + wayland-0); record: start verifies the recorder process is alive after spawn (no false-positive starts), and record: stop pulls the artifact host-side before the provider’s artifact validators run. record: gif renders a STOPPED terminal recording (.cast) to an animated GIF with agg (asciinema/agg — installed by the asciinema candy, which also ships a monospace font) and pulls the .gif to the artifact path; the agg options (theme/font_size/speed/idle_time_limit/fps_cap/select/ cols/rows/no_loop/last_frame_duration/renderer) map 1:1 to agg’s CLI flags.

The CUE schema below is the authoritative grammar for this plugin’s input. It is the same single source that generates the plugin’s Go parameter types and answers the runtime Describe RPC, so this page cannot disagree with either.

// The `record` plugin's OWN CUE schema — the typed plugin_input for the
// `record` session-recording check verb. It is the SINGLE SOURCE for this
// plugin's params, used two ways (the same contract core `spec` and the http
// plugin use):
//
// 1. GENERATE the Go param struct — `cue exp gengotypes` (driven by the cue:gen
// pipeline, which wraps this with `package params` + `@go(params)`) emits
// ../params/cue_types_gen.go, so the provider decodes plugin_input into a
// TYPED struct, never a hand-parsed map.
// 2. VALIDATE authored input AT RUNTIME — the plugin serves this source over the
// Describe channel; the host splices it onto the base (base ++ plugin) and
// validates every authored `record:` step's plugin_input against
// #RecordInput.
//
// Since the schema-compaction cutover the per-verb fields LEFT core #Op: an
// authored `record: <method>` step (scalar sugar) or `record: {method: …,
// record_name: …}` (map form) desugars to the INTERNAL plugin/plugin_input
// envelope, and every record-exclusive modifier lives HERE — the former core
// #RecordMethod enum is this def's `method` field. The shared assertion
// matchers (exit_status/stdout/stderr) and the general `timeout` stay on core
// #Op, read off the step Op by the provider.
//
// SELF-CONTAINED: it references NO base def, so it compiles standalone
// (gengotypes + the load-gate compile) AND splices onto the base (base ++ plugin
// is a def-name collision check, not a base-reference resolver).
#RecordInput: {
// method — the record method to dispatch (the former core #RecordMethod
// enum; also the scalar-sugar primary: `record: <method>`).
method: "list" | "start" | "stop" | "cmd" | "run" | "gif"
// record_name — the recording session name (default "default").
record_name?: string @go(RecordName)
// record_mode — terminal (asciinema) / desktop (pixelflux-record or
// wf-recorder); empty means auto-detect from the venue's tools.
record_mode?: string @go(RecordMode)
// record_fps — the desktop-recorder frame rate (default 30).
record_fps?: int & >=0 @go(RecordFps,type=int)
// record_audio — capture audio with the desktop recording.
record_audio?: bool @go(RecordAudio)
// record_env — extra environment for the recorder process (desktop mode needs the
// compositor session on VM/desktop venues: record_env: {XDG_RUNTIME_DIR: /run/user/1000,
// WAYLAND_DISPLAY: wayland-1}; container defaults are /tmp + wayland-0). Every stated
// key overrides the default; extra keys pass through. Values are static strings.
record_env?: { [string]: string } @go(RecordEnv,type=map[string]string)
// text — the command line `cmd`/`run` sends into the recording's tmux session.
text?: string
// settle_ms — how long `run` waits after sending the text before returning
// (default 1500). The command's output becomes part of the recording.
settle_ms?: int & >=0 @go(SettleMs,type=int)
// artifact — the host path `stop` copies the recording to, and `gif`
// copies the rendered .gif to.
artifact?: string
// artifact_min_bytes / artifact_min_cast_events — the post-run
// artifact-reality assertions (sdk.RunArtifactValidators).
artifact_min_bytes?: int & >=0 @go(ArtifactMinBytes,type=int)
artifact_min_cast_events?: int & >=0 @go(ArtifactMinCastEvents,type=int)
// --- gif method (agg) — render a stopped terminal recording to an animated GIF ---
// theme — agg color theme (asciinema, dracula, monokai, github-dark,
// solarized-dark, ...); empty uses the recording's embedded theme when present.
theme?: string @go(Theme)
// font_size — agg font size in px (default 16).
font_size?: int & >=0 @go(FontSize,type=int)
// speed — agg playback speed multiplier (default 1; >1 speeds up, <1 slows down).
speed?: number & >0 @go(Speed,type=float64)
// idle_time_limit — agg cap on any single inactive period, in seconds
// (default 5), so long pauses don't bloat the GIF.
idle_time_limit?: int & >=0 @go(IdleTimeLimit,type=int)
// fps_cap — agg maximum GIF frame rate (default 30); lower values produce
// smaller files at the cost of motion smoothness.
fps_cap?: int & >=0 @go(FpsCap,type=int)
// select — agg frame selection (e.g. "5..30", "50%", "marker:build..marker:test",
// "12.5"); renders only part of the recording or discrete terminal states.
select?: string @go(Select)
// cols / rows — agg terminal size override (re-render at a different geometry).
cols?: int & >0 @go(Cols,type=int)
rows?: int & >0 @go(Rows,type=int)
// no_loop — agg plays the GIF once instead of looping forever.
no_loop?: bool @go(NoLoop)
// last_frame_duration — agg holds the final frame for this many seconds
// (default 3) before the GIF loops or ends.
last_frame_duration?: int & >=0 @go(LastFrameDuration,type=int)
// renderer — agg rendering backend: "swash" (default) or "resvg".
renderer?: string @go(Renderer)
}

See also the candy reference for this candy’s install surface.