guest-tools — Wire protocol summary
Wire protocol summary
Section titled “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.
Protocol version
Section titled “Protocol version”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.
Transport
Section titled “Transport”| 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) |
Envelope
Section titled “Envelope”{ "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).
Message flow
Section titled “Message flow”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.
Message types
Section titled “Message types”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.
heartbeat — guest → host (periodic)
Section titled “heartbeat — guest → host (periodic)”Empty payload ({}). Sent every 15s by default. Two missed heartbeats
mark the agent stale but do not terminate the session.
command — host → guest (request)
Section titled “command — host → guest (request)”{ "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 |
ack — guest → host (success response)
Section titled “ack — guest → host (success response)”{ "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 installingtools_uninstalled— guest-tools package finished uninstallingshutdown_initiated— guest OS accepted a shutdownreboot_initiated— guest OS accepted a rebootclipboard_changed— guest clipboard changed;details = { "text": string }. The host pushes the new value to any noVNC consumer subscribed to the VMdisplay_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.
Telemetry side-channel (vsock)
Section titled “Telemetry side-channel (vsock)”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.
Type re-exports
Section titled “Type re-exports”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.
Compatibility rules
Section titled “Compatibility rules”- A v1 host MUST drop a connection that advertises
v: 2+and reply witherror { code: "E_PROTO_VERSION_UNSUPPORTED" }. - A v1 host MUST NOT crash on unknown
report.eventvalues; treat them asAgentEvent::Unknown { event, data }. - A v1 guest MUST NOT crash on unknown
command.actionvalues; reply witherror { code: "E_AGENT_UNSUPPORTED_ACTION" }. - Adding a new top-level envelope field is a v2 break.
- Adding a new payload field inside
reportis allowed in v1.
Where to look in code
Section titled “Where to look in code”| 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 |