5.0 KiB
RVBox v1 agent protocol
The authoritative schemas are ../protos/rvbox/v1/common.proto
and ../protos/rvbox/v1/agent.proto. This
document specifies their use over WSS.
Transport and compatibility
The nginx-terminated wss:// connection carries exactly one serialized
rvbox.v1.AgentEnvelope in each binary WebSocket message. There is no extra
length prefix. A decoded envelope may not exceed 1 MiB. A chunk's uncompressed
payload may not exceed 64 KiB. Both peers validate declared and actual expanded
sizes before allocation/decompression.
The protobuf package is rvbox.v1. Registration negotiates a major/minor
protocol range: incompatible majors are rejected; the highest shared minor is
chosen. New fields are append-only. A peer ignores unknown optional fields but
must respond with PROTOCOL_ERROR to an unknown required envelope feature.
Heartbeat and reconnect
Both endpoints use the same algorithm. Any received valid WebSocket frame is inbound activity. After 10 seconds without inbound activity, send a WebSocket Ping. After 30 seconds without inbound activity, close the session and treat it as dead. Pong processing is normal WebSocket behavior; it is not an application message and never queues behind command traffic.
The client reconnects with full-jitter exponential backoff (initial 1 second, cap 60 seconds). A session stable for 60 seconds resets the backoff. The server does not reconnect; it waits for clients. A reconnect always registers again, receives a new fencing generation, resends unacknowledged delivery/output, and reconciles only commands the server still considers non-terminal.
Session fencing
ClientHello starts registration. ServerWelcome gives the selected version,
random session_id, and session_generation. Except ClientHello, all
envelopes carry those values. A new accepted registration fences and disconnects
the prior one for the same client ID. The server accepts messages only from the
current generation; command dispatches also identify their intended generation.
Reliable work flows
Dispatch and reconciliation
The server persistently creates a command before dispatching CommandDispatch.
It redelivers until it receives CommandAccepted. Clients durably deduplicate
on issue_uuid. A client whose queue is full sends a capacity rejection.
Following registration the server sends ReconcileRequest only for its
non-terminal commands for that client. The client replies with a
ReconcileSnapshot per requested known command, or unknown_to_client. It
continues normal event retransmission from the server's last acknowledged event
sequence. Terminal history already confirmed by the server is deliberately
excluded.
Output and events
Client execution events use an increasing event_seq; retries reuse the same
sequence and content. The server durably writes an event before sending
EventAck. EventAck is cumulative through a sequence number. Output is
Zstandard compressed with an explicit original-size field. Server storage can
reuse the validated compressed bytes.
When rolling output removes old retained segments, the server writes
OutputTruncation metadata containing the removed event/byte ranges. Queries
must show that marker rather than silently presenting an apparently complete
stream. An offline client that reaches a cap sends ClientOutputTruncated
before replaying its retained tail on reconnect. The server records it, permits
the named event-sequence gap, and exposes it in output queries. Truncation
metadata is not an execution event and does not consume an event_seq.
Stdin and signals
StdinWrite is binary-safe and ordered by write_seq; the client durably
deduplicates it and returns StdinAck. append_newline is true by default in
the CLI but explicit on the wire. CloseStdin is a separate idempotent action.
Signal and cancellation requests carry a command revision to settle start/kill
races.
Scripts
For a script command, dispatch first contains a ScriptDescriptor; then the
server sends ScriptChunk messages and a commit. The client checks offset,
chunk order, full length, and SHA-256 before it reports upload complete or
launches the process. A retransmitted chunk is idempotent by offset/content.
Flow control and failure containment
No receive loop runs an executor, database write, decompressor, or slow socket operation inline. Each side has bounded staging queues. Durable spools are the source of truth and are charged to the 10 MiB/50 MiB/1 GiB compressed retention budgets described in the architecture document. A full staging queue pauses the related read/dispatch path and drains from disk; it never grows without bound.
Malformed protobuf, over-size payload, invalid compressed data, impossible sequence, bad session token, or protocol-version violation yields a structured error where safe and closes that WebSocket session. It does not crash either daemon. Network loss is normal and is handled through idempotent resend.