docs: add initial RVBox design and protocol contracts

This commit is contained in:
2026-08-21 09:02:57 +00:00
commit 97c2d5cf45
10 changed files with 1138 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
# RVBox v1 agent protocol
The authoritative schemas are [`../protos/rvbox/v1/common.proto`](../protos/rvbox/v1/common.proto)
and [`../protos/rvbox/v1/agent.proto`](../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.