Skip to content

Wire ops ​

The ndjson JSON-RPC convention every pavo driver speaks on its serve verb — the same framing as tamias-serve, myco serve, cuora serve, sepia serve. One JSON object per line on stdin, one response per line on stdout.

Rust source of truth: crates/pavo-protocol/src/wire.rs; protocol/v1/wire.schema.json mirrors it.

Envelope ​

Requests are discriminated by op (internally tagged, lowercase):

json
{"op":"manifest"}
{"op":"views"}
{"op":"view","view":"board","params":{"status":"open"}}
{"op":"query","query":"items","params":{"limit":5}}
{"op":"render","kind":"graph","node":{…},"data":{…},
 "surface":"cli","format":"svg"}
{"op":"submit","view":"edit","form":"set-config",
 "values":{"key":"myco.theme","value":"dark"}}
{"op":"jobs"}
{"op":"notifications"}

Responses:

json
{"ok":true,"result":…}
{"ok":false,"error":"…"}

Fields for the non-matching arm are absent (not null) — consumers match on ok alone. A driver must never crash the wire: malformed input and serialization failures become ok:false responses.

Ops ​

manifest ​

{"op":"manifest"} → DriverManifest. Discovery verb — cheap, must not require product state. This is how pavo decides a tamias-driver-* is a view driver ("serve" in verbs or "pavo" in capabilities).

views ​

{"op":"views"} → Vec<ViewSummary> — {id, title, route, description?, queries[]} per view, without the root trees. Lightweight listing for pavo views and picker UIs; full trees come from view.

view ​

{"op":"view","view":"<id>","params":{…}} → ViewResult:

json
{"view": { …ViewSpec including root tree… },
 "data": { "<binding-name>": <payload>, … }}

The host resolves each QueryBinding on the spec (calling its op with static params merged under caller params) and puts results in data keyed by binding name — data["items"] feeds every node whose query == "items". Partial degradation: a binding that fails appears as {"error": "…"} in data rather than failing the whole view. A QueryBinding may name a foreign driver — the host then issues the binding's op against that driver's serve and stores the payload under the same data[name] key (cross-driver queries; failures degrade identically).

query ​

{"op":"query","query":"<name>","params":{…}} → {"query":"<name>","data":<payload>} — execute a single named query binding, or a raw product op for thin drivers. Powers pavo query and ad-hoc data probes; also the transport the config API rides (query = get/set/ resolve/sources/config).

render ​

{"op":"render","kind":"<kind>","node":{…ViewNode verbatim…}, "data":{…view data map…},"surface":"cli|tui|web","format":"…"?} → RenderModel — the renderer-plugin verb. Only sent to drivers whose manifest claims the node's kind on the asking surface (see Renderer plugins).

  • node — raw ViewNode JSON, passed untyped so unknown/future fields survive.
  • data — the same binding map view results carry.
  • surface — open string; hosts may add surfaces.
  • format — the surface's format hint when one applies.

submit ​

{"op":"submit","view":"<id>","form":"<id>"?,"values":{…}} → SubmitResult — the form-submission verb. A surface collects the editable fields of a form ViewNode and posts their key → value pairs here.

  • view — the view the form lives in (same namespace as view).
  • form — optional form-node id when the view carries several forms; absent = the driver's default/first form.
  • values — submitted field values keyed by FormField.key.

The driver resolves the form node, reads its submit_op (driver-defined: "set", "update", "add", …), and answers a SubmitResult. Drivers without form support answer ok:false. pavo submit <driver>:<view> --param k=v … is the CLI face; GET /api/submit?ref=…&values={…} the web-bridge face.

jobs ​

{"op":"jobs"} → Vec<JobItem> — the job/queue feed op. Drivers whose manifest declares contributes.jobs are providers: the host fans this op out to every provider and merges the results, tagging each item with its driver (Host::jobs / GET /api/jobs / pavo jobs). Drivers without a job feed answer ok:false or an empty list — hosts tolerate either and skip per-driver failures.

notifications ​

{"op":"notifications"} → Vec<NotificationItem> — the notification feed op. Same provider pattern as jobs (contributes.notifications marks the provider; the host aggregates via Host::notifications / GET /api/notifications / pavo notifications).

Types ​

ViewSummary ​

{id, title, route, description?, queries[]} — a ViewSpec minus root, for cheap listing.

ViewResult ​

{view: ViewSpec, data: {<binding>: payload}} — the resolved view. Failed bindings carry {"error": "…"} payloads.

QueryResult ​

{query: string, data: any} — one binding's data payload; shape is driver-defined, hosts pass it through verbatim.

SubmitResult ​

{message?, errors?, data?, view?} — every field optional; a bare {"message":"…"} is complete. errors is a per-field validation map keyed by FormField.key — non-empty means the driver rejected the submit (hosts re-render with the messages; pavo submit exits 1). view, when present, is a fresh ViewResult to swap in. Unknown fields survive typed round-trips.

RenderModel ​

The canonical intermediate output a renderer plugin emits — internally tagged on "type":

typeshapeCLITUIweb
rows{type, headers[], aligns?[], rows[][]}aligned tabletable rows<table>
svg{type, markup} — self-contained <svg>raw markup[svg] hintv-html island
text{type, lines[]}lineslines<pre>
placeholder{type, component, props?}[component]box + detailnamed component

Rules: no HTML in RenderModel (self-contained markup is svg only); unknown type degrades like placeholder; plugins needing richer output use placeholder or svg.

JobItem ​

One entry from a driver's jobs op — a unit of background or queued work:

json
{"id":"build-42","title":"Build pavo","status":"running",
 "detail":"step 3/7","route":"/myco/job/build-42",
 "updated_at":"2026-01-02T15:04:05Z"}

id/title/status are required (status may be empty); detail, route, updated_at optional. route resolves like a contribution route. Timestamps are RFC 3339 strings; status is a driver-owned vocabulary ("queued", "running", "failed", …) — hosts render verbatim or degrade to neutral. Unknown fields survive via a flatten catch-all.

NotificationItem ​

One entry from a driver's notifications op:

json
{"id":"n-7","title":"Build failed","severity":"error",
 "body":"cargo xtask check exited 1","route":"/myco/job/build-42",
 "created_at":"2026-01-02T15:04:05Z","read":false}

id/title required; body, severity, route, created_at optional; read defaults false (drives unread badges — display only in v1, there is no write path). severity is driver-owned ("info", "warning", "error", …); timestamps RFC 3339; route resolves like a contribution route; unknown fields preserved.

Neither item type carries driver attribution — the host knows which driver produced an item (aggregated feeds tag it as driver/driver_title).

Versioning ​

Additive-only within v1: new optional fields, new ops, new kind/type variants may appear at any time; existing fields and spellings never change meaning. Unknown ops are protocol errors — hosts surface the driver's raw error text. Breaking changes bump the version directory (protocol/v2/) with a migration note; v1 stays frozen for existing drivers.