Skip to content

guest-tools — Wire protocol summary

The full, authoritative spec lives at docs/GUEST_AGENT_PROTOCOL.md. When the spec and this summary disagree, the spec wins. This file is a quick reference for engineers who already read the spec at least once.

v: 1. The version is part of every envelope so a v2 receiver can keep responding E_PROTO_VERSION_UNSUPPORTED to a v1 peer instead of silently misparsing.

Layer Value
Bus libvirt virtio-serial <channel>
Channel name com.synnet.synvirt.0
Host endpoint unix socket /var/run/synvirt/agent/{uuid}.sock (QEMU bind, daemon connect)
Guest endpoint /dev/virtio-ports/com.synnet.synvirt.0 (Linux/BSD), \\.\Global\com.synnet.synvirt.0 (Windows)
Direction bi-directional, full-duplex
Framing JSON Lines — one JSON object per \n-terminated line, UTF-8, no BOM
Max line 64 KiB soft cap (either side MAY drop the connection with E_PROTO_MALFORMED)
{
"v": 1,
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "hello",
"ts": "2026-04-30T17:05:00Z",
"payload": { ... }
}
Field Type Notes
v int always 1 for this protocol
id string (uuid v4) new uuid per message; for ack/error the original id is in the payload
type string one of hello, heartbeat, report, command, ack, error
ts string RFC-3339 UTC, Z suffix
payload object type-specific body, never null

Unknown top-level fields → E_PROTO_MALFORMED. Unknown payload fields are tolerated only inside report (forward compatibility for new agent events).

guest host
│ │
│ hello (agent_version, os_family, …) │
│ ───────────────────────────────────────►│
│ │
│ heartbeat (every 15s) │
│ ───────────────────────────────────────►│
│ │
│ command (action) │
│ ◄───────────────────────────────────────│
│ │
│ ack { original_id, payload } │
│ | error { original_id, code, msg } │
│ ───────────────────────────────────────►│
│ │
│ report (boot_complete, os_update, …) │
│ ───────────────────────────────────────►│
│ │

The host MUST NOT process anything before a well-formed hello. The guest MUST send hello first, before any heartbeat / report.

hello — guest → host (mandatory first message)

Section titled “hello — guest → host (mandatory first message)”
{
"type": "hello",
"payload": {
"agent_version": "0.13.0",
"os_family": "windows",
"os_version": "10.0.22631",
"hostname": "WIN-EXAMPLE",
"vm_uuid": "550e8400-…",
"capabilities": ["get_info", "shutdown", "reboot", "set_hostname"]
}
}
Field Type Required Notes
agent_version string yes semver of the guest agent
os_family enum string yes windows / linux / bsd
os_version string yes kernel version on Linux/BSD, NT version on Windows
hostname string yes best-effort; “” is acceptable
vm_uuid uuid yes the guest’s libvirt UUID — sanity check against the socket name
capabilities array of CommandAction yes which actions the agent can service

Mismatched vm_uuid (guest claims a different uuid than the socket file) → E_GT_HANDSHAKE_REJECTED and the session is dropped.

Empty payload ({}). Sent every 15s by default. Two missed heartbeats mark the agent stale but do not terminate the session.

{
"type": "command",
"payload": {
"action": "get_info",
"args": {}
}
}

action is a string variant of CommandAction:

Action Args Effect
get_info {} Returns full os_info snapshot in the ack
shutdown { "delay_seconds": u32 } Schedules graceful OS shutdown
reboot { "delay_seconds": u32 } Schedules graceful reboot
set_hostname { "hostname": string } Sets persistent OS hostname
freeze_fs {} Calls fsfreeze --freeze / (Linux) or VSS quiesce (Win)
thaw_fs {} Pair with freeze_fs
clipboard_get { "format": "text"|"html" } Reads guest clipboard. Linux/BSD via wl-paste/xclip; Windows via OpenClipboard+GetClipboardData
clipboard_set { "format": "text"|"html", "data": string } Writes guest clipboard. Linux/BSD via wl-copy/xclip; Windows via OpenClipboard+SetClipboardData
resize_display { "width": u32, "height": u32 } Resizes guest’s primary display. Linux/BSD via xrandr; Windows via ChangeDisplaySettingsExW
{
"type": "ack",
"payload": {
"original_id": "550e8400-…",
"info": { ... } // present iff command was get_info
}
}

The host correlates by original_id. Late acks are dropped as orphans.

error — guest → host (failure response) or either direction

Section titled “error — guest → host (failure response) or either direction”
{
"type": "error",
"payload": {
"original_id": "550e8400-…",
"code": "E_AGENT_PERMISSION_DENIED",
"message": "set_hostname requires admin"
}
}

Error codes are stable strings. Spec lists the canonical set; new codes go through a protocol-revision review.

report — guest → host (asynchronous events)

Section titled “report — guest → host (asynchronous events)”
{
"type": "report",
"payload": {
"event": "boot_complete",
"data": { ... }
}
}

Reports do not require an ack. The host pushes them onto the daemon’s AgentEvent channel for observers (the activity log subscribes; the SSE stream forwards to the dashboard).

Defined event values today:

  • tools_installed — guest-tools package finished installing
  • tools_uninstalled — guest-tools package finished uninstalling
  • shutdown_initiated — guest OS accepted a shutdown
  • reboot_initiated — guest OS accepted a reboot
  • clipboard_changed — guest clipboard changed; details = { "text": string }. The host pushes the new value to any noVNC consumer subscribed to the VM
  • display_resized — guest desktop resolution changed (e.g. user pressed Win+P, hot-plugged a virtual monitor); details = { "width": u32, "height": u32 }

Unknown events are tolerated and forwarded as AgentEvent::Unknown so newer agents can talk to slightly older daemons without breaking.

The control plane above (virtio-serial JSON-Lines) carries acks-required commands and small reports. Bulk telemetry rides a separate virtio-vsock stream so a chatty sample loop can never queue behind a clipboard or shutdown command.

Item Value
Bus virtio-vsock
Host CID VMADDR_CID_HOST (kernel-defined 2)
Host port VSOCK_TELEMETRY_PORT = 7777
Guest CID kernel-allocated; daemon emits <vsock model='virtio'><cid auto='yes'/></vsock> and reads back the resolved address from virsh dumpxml
Cadence TELEMETRY_INTERVAL_SECS = 5 (one sample per 5 s)
Framing JSON-Lines, one [TelemetrySample] per \n-terminated line, no envelope

Wire format:

{
"timestamp_unix_ms": 1714693512345,
"cpu_percent": 12.5,
"mem_used_bytes": 268435456,
"mem_total_bytes": 1073741824,
"disk_io": { "read_bytes_per_sec": 1234, "write_bytes_per_sec": 5678 },
"net_io": { "rx_bytes_per_sec": 90, "tx_bytes_per_sec": 80 }
}

disk_io and net_io are Option. Older agents that don’t measure IO omit those keys entirely; the schema still validates and the host renders an em-dash on the dashboard rather than a misleading zero.

The host code touches the protocol exclusively through synvirt-guest-tools re-exports:

pub use synvirt_guest_proto::{
AckPayload, CommandAction, ErrorCode, OsFamily, ReportEvent,
TelemetrySample,
};

The daemon never depends on synvirt-guest-proto directly — that lets us bump the protocol crate without rebuilding handlers that only need the higher-level supervisor surface.

  • A v1 host MUST drop a connection that advertises v: 2+ and reply with error { code: "E_PROTO_VERSION_UNSUPPORTED" }.
  • A v1 host MUST NOT crash on unknown report.event values; treat them as AgentEvent::Unknown { event, data }.
  • A v1 guest MUST NOT crash on unknown command.action values; reply with error { code: "E_AGENT_UNSUPPORTED_ACTION" }.
  • Adding a new top-level envelope field is a v2 break.
  • Adding a new payload field inside report is allowed in v1.
Concern File
Envelope + payload types crates/synvirt-guest-proto/src/types.rs
Error codes crates/synvirt-guest-proto/src/error.rs
Codec on the host crates/synvirt-guest-tools/src/session.rs
Codec on the guest (Rust) crates/synvirt-guest-agent/src/transport/{unix,windows}.rs
Codec on the guest (C) crates/synvirt-guest-agent-legacy/c/transport.c + json_mini.c
End-to-end test crates/synvirt-guest-tools/tests/listener_roundtrip.rs