docs: add initial RVBox design and protocol contracts
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user