docs: add initial RVBox design and protocol contracts
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
version: v2
|
||||||
|
modules:
|
||||||
|
- path: protos
|
||||||
|
lint:
|
||||||
|
use:
|
||||||
|
- STANDARD
|
||||||
|
breaking:
|
||||||
|
use:
|
||||||
|
- FILE
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# RVBox v1 design set
|
||||||
|
|
||||||
|
This design set turns the initial proposal into an implementation contract.
|
||||||
|
Read the documents in this order:
|
||||||
|
|
||||||
|
1. [Architecture](architecture.md) — scope, trust model, state machine,
|
||||||
|
execution, storage, and retention decisions.
|
||||||
|
2. [Agent protocol](protocol.md) — WSS framing, sessions, replay, ordering,
|
||||||
|
script transfer, heartbeat, and failure containment.
|
||||||
|
3. [Control plane](control-plane.md) — `rvc`, Unix-socket gRPC, JSON-RPC, and
|
||||||
|
query/foreground semantics.
|
||||||
|
4. [Platform and operations](platform-and-operations.md) — Unix/Windows
|
||||||
|
contracts, recovery, storage safety, telemetry, and defaults.
|
||||||
|
|
||||||
|
The wire authority is in [`../protos/rvbox/v1`](../protos/rvbox/v1):
|
||||||
|
`common.proto` contains shared data types, `agent.proto` contains the
|
||||||
|
client/server WebSocket envelopes, and `control.proto` defines the local gRPC
|
||||||
|
service that the HTTP JSON-RPC adapter mirrors.
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# RVBox v1 architecture
|
||||||
|
|
||||||
|
## Purpose and scope
|
||||||
|
|
||||||
|
RVBox is a reverse-connection remote command system. A client daemon (`rvbox`)
|
||||||
|
maintains a WebSocket connection to `rvbox-server`; the server persists command
|
||||||
|
state and exposes a local control plane to `rvc` and, optionally, JSON-RPC
|
||||||
|
callers. This design is the v1 contract for Go implementations and a later Rust
|
||||||
|
client implementation.
|
||||||
|
|
||||||
|
RVBox deliberately executes arbitrary commands with the identity, permissions,
|
||||||
|
and base environment of the client daemon. It is therefore an administrative
|
||||||
|
tool, not a multi-tenant remote-execution service.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Mutual TLS, client certificates, enrollment tokens, and a client-ID allowlist
|
||||||
|
are not part of v1. TLS is terminated by the deployment's nginx instance.
|
||||||
|
- RVBox does not promise a definitive cross-platform “hung” determination.
|
||||||
|
- V1 does not provide arbitrary file transfer; `--script` transfers only the
|
||||||
|
temporary script required to execute that request.
|
||||||
|
- Resource limits are supported only when explicitly requested by an execution
|
||||||
|
profile; they are not imposed by default.
|
||||||
|
|
||||||
|
## Trust boundary
|
||||||
|
|
||||||
|
The server accepts a self-reported hostname as `client_id`; it is an opaque
|
||||||
|
1–128 ASCII-character routing/display key. Unknown IDs are accepted. A newer
|
||||||
|
registration for an ID replaces its prior live session. Consequently, a peer
|
||||||
|
able to reach nginx can impersonate or take over a client ID. This is an
|
||||||
|
accepted v1 limitation and deployments must restrict the endpoint to a trusted
|
||||||
|
network.
|
||||||
|
|
||||||
|
The Unix control socket is local-only and mode `0600`, owned by the server
|
||||||
|
account. The optional HTTP JSON-RPC endpoint is intentionally unauthenticated;
|
||||||
|
it defaults to loopback but can be bound elsewhere by configuration. Exposing
|
||||||
|
it to a network exposes full remote-command authority and is unsafe without an
|
||||||
|
external access-control layer.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
```text
|
||||||
|
rvc -- gRPC/Unix socket -- rvbox-server -- WSS/nginx -- rvbox client -- shell
|
||||||
|
| |
|
||||||
|
SQLite/WAL +-- optional HTTP JSON-RPC (debug/batch)
|
||||||
|
|
|
||||||
|
compressed output segment files + audit log
|
||||||
|
```
|
||||||
|
|
||||||
|
`rvc` is a thin control-plane client. The server owns durable command history,
|
||||||
|
dispatch, queueing, pagination, audit records, and session fencing. A client
|
||||||
|
owns active process supervision, unacknowledged output spooling, and safe
|
||||||
|
reconnection. Neither daemon lets a slow peer, command, or output stream block
|
||||||
|
its dispatch loops.
|
||||||
|
|
||||||
|
## Identity, sessions, and lifecycle
|
||||||
|
|
||||||
|
1. The client connects over WSS and sends `ClientHello` with its client ID,
|
||||||
|
protocol capability, OS/architecture, daemon version, current daemon CWD,
|
||||||
|
supported shells, and a fresh reconnect UUID.
|
||||||
|
2. The server accepts the current compatible protocol version, fences the
|
||||||
|
previous connection for that ID, and returns a fresh server-issued
|
||||||
|
`session_id` and monotonic `session_generation`.
|
||||||
|
3. Every client-to-server envelope and server dispatch is bound to that token.
|
||||||
|
The server discards traffic from superseded sessions, including late output.
|
||||||
|
4. The server queues work while a client is offline and dispatches it only when
|
||||||
|
the active session advertises capacity. A replacement session immediately
|
||||||
|
resumes non-terminal reconciliation.
|
||||||
|
|
||||||
|
## Command model
|
||||||
|
|
||||||
|
Each user request has a server-generated UUID (`issue_uuid`) and a durable
|
||||||
|
request record: target client, request/issue timestamps, shell type, command
|
||||||
|
text or script descriptor, CWD, environment overrides, resource-profile flags,
|
||||||
|
and lifecycle state. The UUID is the end-to-end idempotency key. Transport is
|
||||||
|
at-least-once, but the client durably remembers accepted UUIDs and never starts
|
||||||
|
the same request twice.
|
||||||
|
|
||||||
|
The server states are:
|
||||||
|
|
||||||
|
```text
|
||||||
|
queued -> dispatched -> accepted -> running -> succeeded | failed | terminated
|
||||||
|
\-------------------------------> cancelled
|
||||||
|
```
|
||||||
|
|
||||||
|
`accepted` means the client has durably accepted the request; `running` means
|
||||||
|
the process has been launched. `cancelled` is used when it is stopped before
|
||||||
|
launch. A kill racing launch is resolved by command revision: the client either
|
||||||
|
acknowledges cancellation before launch or launches then immediately applies
|
||||||
|
the requested signal, recording the race.
|
||||||
|
|
||||||
|
The client permits 16 concurrent processes and 100 pending commands by default.
|
||||||
|
Those values are configurable and advertised to the server. A full queue causes
|
||||||
|
a structured capacity error rather than creating unbounded work.
|
||||||
|
|
||||||
|
### Execution contract
|
||||||
|
|
||||||
|
- Shell selection is explicit: Unix-like clients support `sh` and `bash`;
|
||||||
|
Windows supports `cmd` and `powershell`. Defaults are `sh` and `powershell`.
|
||||||
|
Unsupported shells are rejected; no fallback occurs.
|
||||||
|
- A command receives the daemon account's permissions and startup environment,
|
||||||
|
overlaid with the persisted `env_overrides` map. The effective CWD is the
|
||||||
|
requested existing directory or the registered daemon CWD when omitted.
|
||||||
|
- Each command is isolated into a process tree: a Unix session/process group or
|
||||||
|
a Windows Job Object. A daemon that cannot supervise its children terminates
|
||||||
|
them and reports interruption rather than claiming recovery it cannot make.
|
||||||
|
- Resource profiles are composable flags (`LIGHT`, `CPU_MEDIUM`, `CPU_HEAVY`,
|
||||||
|
`MEM_MEDIUM`, `MEM_HEAVY`, `DISK_MEDIUM`, `DISK_HEAVY`). Profiles are opt-in;
|
||||||
|
the concrete administrator-configured limits are applied with cgroup v2 when
|
||||||
|
available on Linux and Job Object limits on Windows. Unsupported requested
|
||||||
|
controls are reported, never silently ignored.
|
||||||
|
|
||||||
|
### Script execution
|
||||||
|
|
||||||
|
Scripts are content-addressed uploads, not shell-escaped command strings. The
|
||||||
|
server sends a descriptor containing SHA-256 and then ordered chunks (default
|
||||||
|
maximum: 10 MiB). The client verifies the digest, writes an owner-only temporary
|
||||||
|
file beneath the effective CWD, executes it with the selected shell, and removes
|
||||||
|
it after the command reaches a terminal state. Script content is not copied into
|
||||||
|
the audit log; its digest and metadata are.
|
||||||
|
|
||||||
|
## Process control and diagnostics
|
||||||
|
|
||||||
|
On Unix, `kill` addresses the command's process group and accepts normal signal
|
||||||
|
names/numbers supported by that client. On Windows, only `SIGTERM` and `SIGKILL`
|
||||||
|
are valid. `SIGTERM` makes a best-effort `CTRL_BREAK_EVENT` delivery to the
|
||||||
|
dedicated console group, waits 10 seconds, then terminates the Job Object if
|
||||||
|
needed. `SIGKILL` immediately terminates the Job Object. The response reports
|
||||||
|
the actual escalation outcome.
|
||||||
|
|
||||||
|
Lifecycle state never asserts `hung`. A separate `suspected_hung` diagnostic is
|
||||||
|
emitted after the configurable default of 10 minutes without observable
|
||||||
|
progress. Linux enriches this with `/proc` state, CPU, memory, and I/O data;
|
||||||
|
Windows uses process and Job Object APIs where available. It is explicitly a
|
||||||
|
heuristic, not proof of an I/O stall.
|
||||||
|
|
||||||
|
## Durability, ordering, and retention
|
||||||
|
|
||||||
|
The server uses SQLite in WAL mode for metadata/indexes and append-only Zstandard
|
||||||
|
compressed segment files for output. It recovers non-terminal commands after a
|
||||||
|
restart and reconciles only these; server-confirmed historical terminal commands
|
||||||
|
are not re-reconciled. The client stores only active command state and output
|
||||||
|
not acknowledged by the server.
|
||||||
|
|
||||||
|
Every execution-originated event has a strictly increasing `event_seq` scoped to
|
||||||
|
one command. This includes lifecycle transitions, stdout/stderr chunks, stdin
|
||||||
|
acknowledgments, resource snapshots, signals, and terminal events. The server
|
||||||
|
preserves the sequence and additionally records receipt time. This is the
|
||||||
|
canonical reconstruction order across interleaved streams and retries.
|
||||||
|
Truncation is separate range metadata so it can truthfully describe missing
|
||||||
|
event sequences without consuming one itself.
|
||||||
|
|
||||||
|
Output chunks are Zstandard-compressed before persistent quota accounting.
|
||||||
|
Per-command history is a rolling compressed window (10 MiB default), so the
|
||||||
|
oldest output segments for that command are removed first and a sequence-range
|
||||||
|
truncation marker remains. This applies to active and terminal commands. When
|
||||||
|
connected, the client first removes server-acknowledged segments. While offline,
|
||||||
|
it must still honor both hard caps: it retains the newest tail, removes oldest
|
||||||
|
unacknowledged compressed chunks when necessary, and records their exact missing
|
||||||
|
ranges for durable reporting on reconnect.
|
||||||
|
|
||||||
|
Each client also has a 50 MiB aggregate compressed spool cap for active,
|
||||||
|
unacknowledged work. The server's matching per-client compressed-history cap is
|
||||||
|
50 MiB; it evicts that client's oldest terminal command records as needed. The
|
||||||
|
server-wide cap is 1 GiB; it evicts whole oldest terminal command records
|
||||||
|
(metadata and output), never arbitrary stdout/stderr rows. Active commands are
|
||||||
|
protected. If active commands alone consume a per-client budget, their oldest
|
||||||
|
acknowledged output rotates by the per-command rule; pipes continue draining so
|
||||||
|
a child cannot deadlock on output.
|
||||||
|
|
||||||
|
## Audit and timestamps
|
||||||
|
|
||||||
|
The server writes a separate durable audit trail for control actions and session
|
||||||
|
events: source/transport identity where available, target client, UUID, action,
|
||||||
|
request time, result, and error. It records command text, environment-override
|
||||||
|
names (not values), and script metadata/digest, but not duplicated
|
||||||
|
stdin/stdout/stderr payloads. The persisted execution request necessarily keeps
|
||||||
|
override values for dispatch/retry and must be access-controlled as sensitive
|
||||||
|
data. Audit retention is configured independently of output retention.
|
||||||
|
|
||||||
|
All protocol timestamps are UTC `google.protobuf.Timestamp` values. Client
|
||||||
|
observed timestamps and server receipt timestamps are distinct; the latter is
|
||||||
|
authoritative for server records, while `event_seq` is authoritative for order.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# RVBox v1 control plane
|
||||||
|
|
||||||
|
[`../protos/rvbox/v1/control.proto`](../protos/rvbox/v1/control.proto) defines
|
||||||
|
the canonical control API. `rvc` uses the `Control` gRPC service over the local
|
||||||
|
Unix-domain socket. The server also exposes the same unary operations through
|
||||||
|
an optional JSON-RPC 2.0 HTTP adapter for local debugging and batch automation.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- Unix socket: enabled by default, mode `0600`, owned by the server account.
|
||||||
|
- HTTP JSON-RPC: disabled unless enabled; default bind `127.0.0.1:6900`.
|
||||||
|
It has no authentication by design. Binding it beyond loopback is an explicit
|
||||||
|
deployment choice and requires external protection.
|
||||||
|
|
||||||
|
gRPC can stream `RunCommandAndFollow` and `FollowCommand`. JSON-RPC remains
|
||||||
|
simple: callers issue work, query command state, poll event/output pages after
|
||||||
|
an event sequence, append stdin, close stdin, or signal a command. It does not
|
||||||
|
invent a separate event-stream protocol.
|
||||||
|
|
||||||
|
The JSON-RPC method names are the lower-camel protobuf operation names:
|
||||||
|
`listClients`, `getClient`, `listCommands`, `getCommand`, `runCommand`,
|
||||||
|
`appendStdin`, `closeStdin`, `signalCommand`, and `getOutput`. Parameters and
|
||||||
|
results use protobuf JSON mapping (including base64 strings for `bytes` and UTC
|
||||||
|
RFC 3339 strings for timestamps); JSON-RPC errors carry the corresponding
|
||||||
|
`ControlError` code/data. `getOutput` and `getCommand` are the polling path for
|
||||||
|
what gRPC exposes as follow streams.
|
||||||
|
|
||||||
|
## CLI semantics
|
||||||
|
|
||||||
|
`rvc stat` maps to `ListClients`, `GetClient`, `ListCommands`, and `GetCommand`.
|
||||||
|
History pages default to 20 commands and may request at most 100. Output pages
|
||||||
|
default to 100 lines; a line is a display operation over ordered chunks, not a
|
||||||
|
protocol boundary. Output can be filtered by stream and timestamped with the
|
||||||
|
server's recorded client-observed timestamp plus stream name.
|
||||||
|
|
||||||
|
`rvc run` creates a command. Foreground mode runs `RunCommandAndFollow`, which
|
||||||
|
streams output and stops on a terminal event. `--background` uses `RunCommand`
|
||||||
|
and returns the UUID immediately. Interrupting the CLI, timing out its local
|
||||||
|
wait, or losing the local control connection never cancels remote work. The
|
||||||
|
explicit `rvc kill` operation is the only termination path.
|
||||||
|
|
||||||
|
`rvc append` turns a string into `StdinWrite` with `append_newline=true` unless
|
||||||
|
the caller selects raw mode; `--file` supplies raw bytes; `--attach` streams
|
||||||
|
local standard input. `CloseStdin` is available separately. All stdin actions
|
||||||
|
are acknowledged and idempotent.
|
||||||
|
|
||||||
|
## Query behavior
|
||||||
|
|
||||||
|
Command details expose persisted request fields, effective execution metadata,
|
||||||
|
lifecycle, terminal result, output availability/truncation, current resource
|
||||||
|
snapshot, and `suspected_hung` diagnostics. The latter is informational and
|
||||||
|
must not be shown as a terminal state. Pagination response cursors are stable
|
||||||
|
within their declared ordering (newest issue time for command lists; increasing
|
||||||
|
`event_seq` for events/output).
|
||||||
|
|
||||||
|
The service returns `ControlError` codes for not found, offline, capacity,
|
||||||
|
invalid request, unsupported platform feature, conflict, truncation, and
|
||||||
|
internal/transient errors. It never encodes errors only as CLI text.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# RVBox v1 platform and operations contract
|
||||||
|
|
||||||
|
## Unix-like clients
|
||||||
|
|
||||||
|
The client starts `sh` or `bash` in a new session/process group. Unix signals
|
||||||
|
address that group, so normally created descendants receive the signal too. On
|
||||||
|
orderly shutdown, or recovery after an unclean daemon failure, managed command
|
||||||
|
groups are terminated and marked interrupted because pipe capture cannot be
|
||||||
|
safely resumed.
|
||||||
|
|
||||||
|
Linux diagnostics sample `/proc/<pid>` and relevant children for state, CPU,
|
||||||
|
resident memory, I/O counters, CWD, and wait-channel information when readable.
|
||||||
|
These values may be unavailable due to permissions, kernel configuration, or a
|
||||||
|
short-lived process; absence is represented explicitly rather than fabricated.
|
||||||
|
Cgroup v2 is used for requested resource profiles only when available.
|
||||||
|
|
||||||
|
## Windows clients
|
||||||
|
|
||||||
|
The client launches `cmd` or `powershell` in an appropriate dedicated console
|
||||||
|
process group and assigns the root process to a per-command Job Object. Child
|
||||||
|
processes normally join the Job Object. Job Object limits enforce requested
|
||||||
|
profiles and `KILL_ON_JOB_CLOSE` protects against lost supervision.
|
||||||
|
|
||||||
|
Only `SIGTERM` and `SIGKILL` are accepted. `SIGTERM` attempts `CTRL_BREAK_EVENT`
|
||||||
|
and waits 10 seconds, then calls Job Object termination if the job persists;
|
||||||
|
`SIGKILL` calls Job Object termination immediately. A console signal is
|
||||||
|
best-effort, so callers receive an explicit escalation result. Windows status
|
||||||
|
uses process and Job Object accounting APIs; it does not claim Linux-only
|
||||||
|
diagnostics such as an I/O wait channel.
|
||||||
|
|
||||||
|
## Storage and recovery
|
||||||
|
|
||||||
|
SQLite runs in WAL mode with integrity checking on startup. Output segments are
|
||||||
|
written atomically, fsynced according to the configured durability interval, and
|
||||||
|
indexed only after successful durable append. Startup scans/repairs incomplete
|
||||||
|
tail records before accepting control requests. Segment compression is Zstandard;
|
||||||
|
limits always measure stored compressed bytes, while clients expose raw byte
|
||||||
|
counts separately.
|
||||||
|
|
||||||
|
The system must reserve headroom before writes and use transactional metadata
|
||||||
|
updates. Storage-full, permission, and corruption failures are surfaced as
|
||||||
|
structured server/client health states and audit events. They must isolate the
|
||||||
|
affected command/session, reject work when needed, and keep the daemon's
|
||||||
|
heartbeat/control loops alive.
|
||||||
|
|
||||||
|
## Metrics, logging, and safe defaults
|
||||||
|
|
||||||
|
Both daemons should emit structured logs and metrics for session transitions,
|
||||||
|
heartbeat timeout, reconnect backoff, command state transitions, queue depth,
|
||||||
|
spool bytes, segment rotation/eviction, output loss markers, storage errors,
|
||||||
|
and protocol violations. Never emit stdin or raw output in normal daemon logs.
|
||||||
|
|
||||||
|
Recommended configuration defaults are: 10-second heartbeat idle period,
|
||||||
|
30-second liveness timeout, 1–60-second full-jitter reconnect backoff,
|
||||||
|
60-second stable-session reset, 16 running/100 queued commands per client,
|
||||||
|
10 MiB per-command compressed window, 50 MiB per-client active spool and server
|
||||||
|
history, 1 GiB server history, 64 KiB uncompressed stream chunk, 1 MiB decoded
|
||||||
|
envelope, and 10 MiB script maximum.
|
||||||
|
|
||||||
|
These bounds protect RVBox's own loops; they cannot make arbitrary child
|
||||||
|
commands harmless when no resource profile is requested. Operators should
|
||||||
|
enable resource profiles for untrusted or expensive workloads and keep nginx,
|
||||||
|
Unix-socket permissions, filesystem capacity, and service supervision correctly
|
||||||
|
configured.
|
||||||
@@ -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.
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
# RVBox - A Reverse Shell Solution for LLM Agent
|
||||||
|
|
||||||
|
* It's a C/S architecture, the clients are the one connects to the server, and are controlled by it.
|
||||||
|
* The clients runs the `rvbox` daemon, actively and persistently connects to a configured server. It registers and identifies itself to the server first, if accepted, then keeps a long connection and accepting commands from the server.
|
||||||
|
* The server runs the `rvbox-server` daemon, listening on a `wss://` endpoint, waiting for incoming `rvbox` clients. An LLM agent on the server machine then can use `rvc` command to communicate with the server daemon, issuing commands to do all sorts of things:
|
||||||
|
- listing clients,
|
||||||
|
- issuing/terminating commands on specific client,
|
||||||
|
- retrieving running/stopping status of a command,
|
||||||
|
- getting stdout/stderr, or return code from a command,
|
||||||
|
- inputing into stdin of a command,
|
||||||
|
|
||||||
|
* commands can be issued in two modes: foreground or background, for foreground ones, `rvc` command wait for the return, otherwise, `rvc` returns immediately with metadata of the issued command process on the client.
|
||||||
|
* for each issued command from `rvc`, the server daemon attaches an `issue_uuid` to it, and sends it to the related client, along with these fields: `issue_timestamp`, `shell_type`, `shell_command_text`. The server also records all these fields into its data store, with another `target_client_id`, to track the full states of every command.
|
||||||
|
* The client's daemon runs the issued commands, and reports stdout/stderr of all issued commands to the server in real-time, meaning no waiting for new-line flush. Also, multiple commands can be issued to a client daemon, and the client daemon can execute them concurrently. For each running command, the daemon appends new stdout and stderr updates to the server in every short 3-5s window, or only reports the command's status in a longer 10-15s window if no new updates. The server keeps on track of everything, so it can reconstruct the full context/history correctly for each command, when demanded so by `rvc` .
|
||||||
|
* The connection should be guarded by a properly designed heartbeat mechanism on both C/S sides.
|
||||||
|
* `rvc` is just a cli command that communicates with the server daemon through local unixsock. Also, the same server daemon control interface can be accessed through an optional simple HTTP json rpc endpoint, e.g. `--json-rpc http://127.0.0.1:6900` by default, so that we can interact with the server in a more structured way when complex or large batch operations are needed.
|
||||||
|
|
||||||
|
### Example usages of `rvc` on server side
|
||||||
|
|
||||||
|
1. Listing clients
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat
|
||||||
|
CLIENT Commands Running Connected Time
|
||||||
|
client_a 5 10 days
|
||||||
|
client_b 0 10 min
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stating a client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a
|
||||||
|
Command ID Command Snippet Status Issue Time
|
||||||
|
<uuid_1> "cp /path1/file /path2/" Running 10s ago
|
||||||
|
<uuid_2> "python very_long_computing.py" Running 2 days ago
|
||||||
|
<uuid_3> "sha256sum /path/very_large_file" Hung 10min ago
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stating all commands of a client. Note this `--all` can return very long history, so it has a optional pager `--page <NO>` arg. By default the optional `--per-page` is at 20, and accepts 100 at max.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a --all
|
||||||
|
Page 1/34
|
||||||
|
Command ID Command Snippet Status Issue Time
|
||||||
|
<uuid_1> "cp /path1/file /path2/" Running 10s ago
|
||||||
|
<uuid_2> "python very_long_computing.py" Running 2 days ago
|
||||||
|
<uuid_3> "sha256sum /path/very_large_file" Hung(I/O) 10min ago
|
||||||
|
<uuid_4> "curl -fsLO https://..." Done 15min ago
|
||||||
|
<uuid_5> "culr -fsLO https://..." Failed(127) 15min ago
|
||||||
|
<uuid_6> "curl -fsLO https://..." Terminated(130) 15min ago
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Stating some commands of client_a
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a <uuid_1>
|
||||||
|
Command Text: "cp /path1/file /path2/"
|
||||||
|
Status: Running
|
||||||
|
Issue Time: 20xx-01-01T10:01:01Z
|
||||||
|
CWD: "/..."
|
||||||
|
CPU TIME: 0:00.23
|
||||||
|
RES RAM: 12K
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a <uuid_2>
|
||||||
|
Command Text: "python very_long_computing.py"
|
||||||
|
Status: Running
|
||||||
|
Issue Time: 20xx-12-30T12:01:01Z
|
||||||
|
CWD: "/..."
|
||||||
|
CPU TIME: 12h34:45
|
||||||
|
RES RAM: 892M
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a <uuid_3>
|
||||||
|
Command Text: "sha256sum /path/very_large_file"
|
||||||
|
Status: Hung
|
||||||
|
Reason: I/O
|
||||||
|
Issue Time: 20xx-01-01T10:01:01Z
|
||||||
|
CWD: "/..."
|
||||||
|
CPU TIME: 0:10.24
|
||||||
|
RES RAM: 125K
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a <uuid_4>
|
||||||
|
Command Text: "curl -fsLO https://<download_url>"
|
||||||
|
Status: Success
|
||||||
|
Issue Time: 20xx-01-01T10:01:01Z
|
||||||
|
CWD: "/..."
|
||||||
|
CPU TIME: 0:10.24
|
||||||
|
RES RAM: 125K
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a <uuid_5>
|
||||||
|
Command Text: "culr -fsLO https://<download_url>"
|
||||||
|
Status: Failure
|
||||||
|
Exit Code: 127
|
||||||
|
Issue Time: 20xx-01-01T10:01:01Z
|
||||||
|
Return Time: 20xx-01-01T10:01:01Z
|
||||||
|
CWD: "/..."
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a <uuid_6>
|
||||||
|
Command Text: "curl -fsLO https://<download url>"
|
||||||
|
Status: Terminated
|
||||||
|
Exit Code: 130
|
||||||
|
Issue Time: 20xx-01-01T10:01:01Z
|
||||||
|
Return Time: 20xx-01-01T10:02:01Z
|
||||||
|
CWD: "/..."
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Issue a new command to a client.
|
||||||
|
|
||||||
|
- By design, the commands triggered/executed by the client daemon should inherit its user/permission settings.
|
||||||
|
|
||||||
|
- By default, the optional `--current-work-dir/--cwd` of the issued command is inherited from the client daemon.
|
||||||
|
- If `--script` is used to issue a whole script, the script will first be put into the `--cwd` dir, then executed by the client daemon. As soon as it's exited, the script will be reclaimed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# wait until return by default
|
||||||
|
$ rvc run client_a --cwd '/home/user' 'ls'
|
||||||
|
Desktop Documents Downloads Pictures...
|
||||||
|
# run in background, return the command id immediately
|
||||||
|
$ rvc run --background client_a 'python -c "for i in range(15): print(\'hi\'); import time; time.sleep(1)"'
|
||||||
|
Command ID: <uuid_x>
|
||||||
|
# if the command is too long or the symbol escapings are too complicated, write a whole script then issue.
|
||||||
|
$ cat << 'EOF' > ./py_script
|
||||||
|
heredoc> #!/usr/bin/env python
|
||||||
|
heredoc> print("what's your name?")
|
||||||
|
heredoc> name = input("type your name here: ")
|
||||||
|
heredoc> print(f"Hello, {name}!")
|
||||||
|
heredoc> EOF
|
||||||
|
$ rvc run client_a --script ./py_script --cwd /tmp --background
|
||||||
|
Command ID: <uuid_y>
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Get stdout and/or stderr of a command,
|
||||||
|
|
||||||
|
- optionally with `--timestamped` to get a leading timestamp on each line,
|
||||||
|
- and optionally follow new updates with `--follow/-f`, just like the `tail -f` command
|
||||||
|
- the output of `--stdout` and `--stderr` are paged, with `--lines-per-page` defaults to 100 and `--page` defaults to 1.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc stat client_a <uuid_x> --stdout --stderr --timestamped --page 2 --lines-per-page 3
|
||||||
|
Page 2/4
|
||||||
|
[20xx-01-01T10:21:01Z stdout] hi
|
||||||
|
[20xx-01-01T10:21:02Z stdout] hi
|
||||||
|
[20xx-01-01T10:21:03Z stdout] hi
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Write new content into stdin of a command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# write the string into its stdin, and a \n will be appended to the end automatically
|
||||||
|
$ rvc append client_a <uuid_y> 'Alice'
|
||||||
|
$ rvc stat client_a <uuid_y> --stdout --stderr --timestamped
|
||||||
|
[20xx-01-01T10:23:11Z stdout] type your name here:
|
||||||
|
[20xx-01-01T10:24:09Z stdout] Hello, Alice!
|
||||||
|
# or write a file content into its stdin
|
||||||
|
$ rvc append client_a <uuid_y> --file ./name_file
|
||||||
|
# or patch the current stdin into it
|
||||||
|
$ rvc append client_a <uuid_y> --attach
|
||||||
|
Bob
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Send a signal to a command.
|
||||||
|
|
||||||
|
- For unix/linux client daemons, this `kill` subcommand behaves just like the normal `kill` command. It sends SIGTERM by default, and can be used to send other signals with args like `-1/-HUP`, `-2/-INT`, etc.
|
||||||
|
- On windows client daemons, it should only accept SIGTERM and SIGKILL, equivalent to `taskkill /IM` and `taskkill /F /IM`, respectively.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rvc kill client_a <uuid_3>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Implementation requirements
|
||||||
|
|
||||||
|
1. We should first do proper design on the protocol - both the C/S interface and the server daemon unixsock/jsonrpc control interface. protobuf should be a solid choice to do the protocol design.
|
||||||
|
2. Currently we'll implement both C/S daemon in Golang. We'll later implement a Rust version client to adapt on low power devices.
|
||||||
|
|
||||||
|
|
||||||
|
### Technical nuances
|
||||||
|
|
||||||
|
1. The `rvbox-server` daemon and the `rvbox` client daemon should always be responsive. The server daemon should always be dispatching clients, recording the status, and listening for requests from `rvc`, etc. And the client daemons should keep on receiving/dispatching incoming commands/requests, monitoring ongoing commands, reporting new updates/status to the server. In ABSOLUTELY NO circumstances should the daemons themselves be crashed, blocked, hung, flooded, unresponsive, etc., due to any possible reason.
|
||||||
|
|
||||||
|
2. The heartbeat mechanism that keeps and guards the websocket connection should be robust, clean and correct. When connection exceptions/interruptions/hangs happen, the heartbeat mechanism should detect them in time, and trigger the reconnect, re-register route correctly. It should guard on both the inbound and the outbound traffic. It should not disrupt, hog, block, interrupt, mutate the normal traffic in any way.
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package rvbox.v1;
|
||||||
|
|
||||||
|
option go_package = "github.com/rvbox/rvbox/gen/go/rvbox/v1;rvboxv1";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "rvbox/v1/common.proto";
|
||||||
|
|
||||||
|
// One AgentEnvelope is carried in one binary WebSocket message.
|
||||||
|
message AgentEnvelope {
|
||||||
|
// Empty only for ClientHello. All other envelopes are fenced to a session.
|
||||||
|
string session_id = 1;
|
||||||
|
uint64 session_generation = 2;
|
||||||
|
string message_id = 3;
|
||||||
|
|
||||||
|
oneof payload {
|
||||||
|
ClientHello client_hello = 10;
|
||||||
|
ServerWelcome server_welcome = 11;
|
||||||
|
CommandDispatch command_dispatch = 12;
|
||||||
|
CommandAccepted command_accepted = 13;
|
||||||
|
CommandEvent command_event = 14;
|
||||||
|
EventAck event_ack = 15;
|
||||||
|
StdinWrite stdin_write = 16;
|
||||||
|
CloseStdin close_stdin = 17;
|
||||||
|
SignalCommand signal_command = 18;
|
||||||
|
ScriptChunk script_chunk = 19;
|
||||||
|
ScriptCommit script_commit = 20;
|
||||||
|
ClientCapacity client_capacity = 21;
|
||||||
|
ReconcileRequest reconcile_request = 22;
|
||||||
|
ReconcileSnapshot reconcile_snapshot = 23;
|
||||||
|
AgentError error = 24;
|
||||||
|
ClientOutputTruncated client_output_truncated = 25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientHello {
|
||||||
|
// Configured hostname; opaque 1-128 ASCII characters.
|
||||||
|
string client_id = 1;
|
||||||
|
ProtocolRange supported_protocol = 2;
|
||||||
|
string daemon_version = 3;
|
||||||
|
Platform platform = 4;
|
||||||
|
string architecture = 5;
|
||||||
|
string daemon_cwd = 6;
|
||||||
|
repeated ShellType supported_shells = 7;
|
||||||
|
string reconnect_uuid = 8;
|
||||||
|
uint32 max_running_commands = 9;
|
||||||
|
uint32 max_queued_commands = 10;
|
||||||
|
google.protobuf.Timestamp sent_at = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ServerWelcome {
|
||||||
|
ProtocolVersion selected_protocol = 1;
|
||||||
|
// AgentEnvelope.session_id and .session_generation are canonical.
|
||||||
|
google.protobuf.Timestamp server_time = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CommandDispatch {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 command_revision = 2;
|
||||||
|
uint64 target_session_generation = 3;
|
||||||
|
google.protobuf.Timestamp issue_time = 4;
|
||||||
|
ExecutionSpec spec = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CommandAccepted {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 command_revision = 2;
|
||||||
|
bool accepted = 3;
|
||||||
|
ControlError rejection = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cumulative durable acknowledgement of client CommandEvent values.
|
||||||
|
message EventAck {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 through_event_seq = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StdinWrite {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 write_seq = 2;
|
||||||
|
bytes data = 3;
|
||||||
|
bool append_newline = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CloseStdin {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 write_seq = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignalCommand {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 command_revision = 2;
|
||||||
|
SignalKind signal = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ScriptChunk {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 offset = 2;
|
||||||
|
bytes data = 3;
|
||||||
|
bytes sha256 = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ScriptCommit {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 size_bytes = 2;
|
||||||
|
bytes sha256 = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientCapacity {
|
||||||
|
uint32 running_commands = 1;
|
||||||
|
uint32 queued_commands = 2;
|
||||||
|
uint32 max_running_commands = 3;
|
||||||
|
uint32 max_queued_commands = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReconcileRequest {
|
||||||
|
repeated ReconcileTarget targets = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReconcileTarget {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 last_server_event_seq = 2;
|
||||||
|
uint64 command_revision = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent only for requests in ReconcileRequest; server-confirmed terminal history
|
||||||
|
// is intentionally not requested.
|
||||||
|
message ReconcileSnapshot {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
bool known_to_client = 2;
|
||||||
|
CommandLifecycle lifecycle = 3;
|
||||||
|
uint64 last_client_event_seq = 4;
|
||||||
|
uint64 command_revision = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent before retained replay data when an offline client had to rotate
|
||||||
|
// unacknowledged output to remain within its hard spool limits.
|
||||||
|
message ClientOutputTruncated {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
OutputTruncation truncation = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AgentError {
|
||||||
|
ControlError error = 1;
|
||||||
|
bool close_session = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package rvbox.v1;
|
||||||
|
|
||||||
|
option go_package = "github.com/rvbox/rvbox/gen/go/rvbox/v1;rvboxv1";
|
||||||
|
|
||||||
|
import "google/protobuf/duration.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// An inclusive protocol-version range advertised during registration.
|
||||||
|
message ProtocolRange {
|
||||||
|
uint32 major = 1;
|
||||||
|
uint32 min_minor = 2;
|
||||||
|
uint32 max_minor = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProtocolVersion {
|
||||||
|
uint32 major = 1;
|
||||||
|
uint32 minor = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Platform {
|
||||||
|
PLATFORM_UNSPECIFIED = 0;
|
||||||
|
PLATFORM_LINUX = 1;
|
||||||
|
PLATFORM_DARWIN = 2;
|
||||||
|
PLATFORM_WINDOWS = 3;
|
||||||
|
PLATFORM_OTHER_UNIX = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ShellType {
|
||||||
|
SHELL_TYPE_UNSPECIFIED = 0;
|
||||||
|
SHELL_SH = 1;
|
||||||
|
SHELL_BASH = 2;
|
||||||
|
SHELL_CMD = 3;
|
||||||
|
SHELL_POWERSHELL = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Compression {
|
||||||
|
COMPRESSION_UNSPECIFIED = 0;
|
||||||
|
COMPRESSION_NONE = 1;
|
||||||
|
COMPRESSION_ZSTD = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CommandLifecycle {
|
||||||
|
COMMAND_LIFECYCLE_UNSPECIFIED = 0;
|
||||||
|
COMMAND_QUEUED = 1;
|
||||||
|
COMMAND_DISPATCHED = 2;
|
||||||
|
COMMAND_ACCEPTED = 3;
|
||||||
|
COMMAND_RUNNING = 4;
|
||||||
|
COMMAND_SUCCEEDED = 5;
|
||||||
|
COMMAND_FAILED = 6;
|
||||||
|
COMMAND_TERMINATED = 7;
|
||||||
|
COMMAND_CANCELLED = 8;
|
||||||
|
COMMAND_INTERRUPTED = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StreamKind {
|
||||||
|
STREAM_KIND_UNSPECIFIED = 0;
|
||||||
|
STREAM_STDOUT = 1;
|
||||||
|
STREAM_STDERR = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SignalKind {
|
||||||
|
SIGNAL_KIND_UNSPECIFIED = 0;
|
||||||
|
SIGNAL_HUP = 1;
|
||||||
|
SIGNAL_INT = 2;
|
||||||
|
SIGNAL_TERM = 3;
|
||||||
|
SIGNAL_KILL = 4;
|
||||||
|
SIGNAL_USR1 = 5;
|
||||||
|
SIGNAL_USR2 = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These flags are composable. Their numeric limits are local administrator
|
||||||
|
// policy rather than part of the interoperable protocol.
|
||||||
|
enum ExecutionProfile {
|
||||||
|
EXECUTION_PROFILE_UNSPECIFIED = 0;
|
||||||
|
EXECUTION_PROFILE_LIGHT = 1;
|
||||||
|
EXECUTION_PROFILE_CPU_MEDIUM = 2;
|
||||||
|
EXECUTION_PROFILE_CPU_HEAVY = 3;
|
||||||
|
EXECUTION_PROFILE_MEM_MEDIUM = 4;
|
||||||
|
EXECUTION_PROFILE_MEM_HEAVY = 5;
|
||||||
|
EXECUTION_PROFILE_DISK_MEDIUM = 6;
|
||||||
|
EXECUTION_PROFILE_DISK_HEAVY = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ScriptDescriptor {
|
||||||
|
string filename = 1;
|
||||||
|
uint64 size_bytes = 2;
|
||||||
|
bytes sha256 = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execution settings sent to the client. A script's bytes travel separately.
|
||||||
|
message ExecutionSpec {
|
||||||
|
ShellType shell_type = 1;
|
||||||
|
string cwd = 2;
|
||||||
|
map<string, string> env_overrides = 3;
|
||||||
|
repeated ExecutionProfile execution_profiles = 4;
|
||||||
|
oneof source {
|
||||||
|
string command_text = 5;
|
||||||
|
ScriptDescriptor script = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message CommandRecord {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
string target_client_id = 2;
|
||||||
|
google.protobuf.Timestamp issue_time = 3;
|
||||||
|
google.protobuf.Timestamp server_receipt_time = 4;
|
||||||
|
ExecutionSpec spec = 5;
|
||||||
|
CommandLifecycle lifecycle = 6;
|
||||||
|
uint64 last_event_seq = 7;
|
||||||
|
int32 exit_code = 8;
|
||||||
|
google.protobuf.Timestamp terminal_time = 9;
|
||||||
|
bool output_truncated = 10;
|
||||||
|
uint64 retained_compressed_bytes = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LifecycleChange {
|
||||||
|
CommandLifecycle lifecycle = 1;
|
||||||
|
int32 exit_code = 2;
|
||||||
|
string detail = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// data is compressed according to compression. uncompressed_size is mandatory
|
||||||
|
// when compression is ZSTD and must be validated before decompression.
|
||||||
|
message OutputChunk {
|
||||||
|
StreamKind stream = 1;
|
||||||
|
Compression compression = 2;
|
||||||
|
bytes data = 3;
|
||||||
|
uint64 uncompressed_size = 4;
|
||||||
|
uint64 compressed_size = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResourceSnapshot {
|
||||||
|
uint64 resident_memory_bytes = 1;
|
||||||
|
uint64 virtual_memory_bytes = 2;
|
||||||
|
google.protobuf.Duration cpu_time = 3;
|
||||||
|
uint64 read_bytes = 4;
|
||||||
|
uint64 write_bytes = 5;
|
||||||
|
string process_state = 6;
|
||||||
|
string wait_reason = 7;
|
||||||
|
bool suspected_hung = 8;
|
||||||
|
string diagnostic_detail = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OutputTruncationSource {
|
||||||
|
OUTPUT_TRUNCATION_SOURCE_UNSPECIFIED = 0;
|
||||||
|
OUTPUT_TRUNCATION_SOURCE_CLIENT_SPOOL = 1;
|
||||||
|
OUTPUT_TRUNCATION_SOURCE_SERVER_COMMAND_WINDOW = 2;
|
||||||
|
OUTPUT_TRUNCATION_SOURCE_SERVER_CLIENT_CAP = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistent query metadata for a missing contiguous event range. It is not a
|
||||||
|
// CommandEvent and therefore does not consume an event_seq.
|
||||||
|
message OutputTruncation {
|
||||||
|
uint64 first_removed_event_seq = 1;
|
||||||
|
uint64 last_removed_event_seq = 2;
|
||||||
|
uint64 removed_compressed_bytes = 3;
|
||||||
|
uint64 removed_uncompressed_bytes = 4;
|
||||||
|
string reason = 5;
|
||||||
|
OutputTruncationSource source = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StdinAcknowledgement {
|
||||||
|
uint64 write_seq = 1;
|
||||||
|
bool stdin_closed = 2;
|
||||||
|
string detail = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignalResult {
|
||||||
|
SignalKind signal = 1;
|
||||||
|
bool accepted = 2;
|
||||||
|
bool graceful_delivery_attempted = 3;
|
||||||
|
bool forced_termination_used = 4;
|
||||||
|
string detail = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ScriptUploadStatus {
|
||||||
|
uint64 received_bytes = 1;
|
||||||
|
bool complete = 2;
|
||||||
|
string detail = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-originated ordered history. event_seq is strictly increasing per
|
||||||
|
// issue_uuid and is reused exactly on retransmission.
|
||||||
|
message CommandEvent {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
uint64 event_seq = 2;
|
||||||
|
google.protobuf.Timestamp observed_at = 3;
|
||||||
|
oneof payload {
|
||||||
|
LifecycleChange lifecycle = 10;
|
||||||
|
OutputChunk output = 11;
|
||||||
|
ResourceSnapshot resource = 12;
|
||||||
|
StdinAcknowledgement stdin_ack = 13;
|
||||||
|
SignalResult signal_result = 14;
|
||||||
|
ScriptUploadStatus script_status = 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ControlError {
|
||||||
|
enum Code {
|
||||||
|
CODE_UNSPECIFIED = 0;
|
||||||
|
INVALID_ARGUMENT = 1;
|
||||||
|
NOT_FOUND = 2;
|
||||||
|
OFFLINE = 3;
|
||||||
|
CAPACITY_EXHAUSTED = 4;
|
||||||
|
CONFLICT = 5;
|
||||||
|
UNSUPPORTED = 6;
|
||||||
|
PROTOCOL_ERROR = 7;
|
||||||
|
TRANSIENT = 8;
|
||||||
|
INTERNAL = 9;
|
||||||
|
}
|
||||||
|
Code code = 1;
|
||||||
|
string message = 2;
|
||||||
|
bool retryable = 3;
|
||||||
|
string issue_uuid = 4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package rvbox.v1;
|
||||||
|
|
||||||
|
option go_package = "github.com/rvbox/rvbox/gen/go/rvbox/v1;rvboxv1";
|
||||||
|
|
||||||
|
import "rvbox/v1/common.proto";
|
||||||
|
|
||||||
|
service Control {
|
||||||
|
rpc ListClients(ListClientsRequest) returns (ListClientsResponse);
|
||||||
|
rpc GetClient(GetClientRequest) returns (GetClientResponse);
|
||||||
|
rpc ListCommands(ListCommandsRequest) returns (ListCommandsResponse);
|
||||||
|
rpc GetCommand(GetCommandRequest) returns (GetCommandResponse);
|
||||||
|
rpc RunCommand(RunCommandRequest) returns (RunCommandResponse);
|
||||||
|
rpc RunCommandAndFollow(RunCommandRequest) returns (stream CommandEvent);
|
||||||
|
rpc FollowCommand(FollowCommandRequest) returns (stream CommandEvent);
|
||||||
|
rpc AppendStdin(AppendStdinRequest) returns (AppendStdinResponse);
|
||||||
|
rpc CloseStdin(CloseStdinRequest) returns (CloseStdinResponse);
|
||||||
|
rpc SignalCommand(ControlSignalCommandRequest) returns (ControlSignalCommandResponse);
|
||||||
|
rpc GetOutput(GetOutputRequest) returns (GetOutputResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientSummary {
|
||||||
|
string client_id = 1;
|
||||||
|
bool connected = 2;
|
||||||
|
google.protobuf.Timestamp connected_at = 3;
|
||||||
|
google.protobuf.Timestamp last_seen_at = 4;
|
||||||
|
uint32 running_commands = 5;
|
||||||
|
uint32 queued_commands = 6;
|
||||||
|
Platform platform = 7;
|
||||||
|
string architecture = 8;
|
||||||
|
string daemon_version = 9;
|
||||||
|
string daemon_cwd = 10;
|
||||||
|
repeated ShellType supported_shells = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListClientsRequest {
|
||||||
|
uint32 page_size = 1;
|
||||||
|
string page_token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListClientsResponse {
|
||||||
|
repeated ClientSummary clients = 1;
|
||||||
|
string next_page_token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetClientRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetClientResponse {
|
||||||
|
ClientSummary client = 1;
|
||||||
|
ControlError error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListCommandsRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
bool include_terminal = 2;
|
||||||
|
uint32 page_size = 3;
|
||||||
|
string page_token = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListCommandsResponse {
|
||||||
|
repeated CommandRecord commands = 1;
|
||||||
|
string next_page_token = 2;
|
||||||
|
ControlError error = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCommandRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
string issue_uuid = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCommandResponse {
|
||||||
|
CommandRecord command = 1;
|
||||||
|
ResourceSnapshot latest_resource = 2;
|
||||||
|
ControlError error = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For script execution, script_content contains the bytes whose descriptor is
|
||||||
|
// placed in spec.script. The server chunks it for the Agent protocol.
|
||||||
|
message RunCommandRequest {
|
||||||
|
string target_client_id = 1;
|
||||||
|
ExecutionSpec spec = 2;
|
||||||
|
bytes script_content = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RunCommandResponse {
|
||||||
|
string issue_uuid = 1;
|
||||||
|
CommandLifecycle lifecycle = 2;
|
||||||
|
ControlError error = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FollowCommandRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
string issue_uuid = 2;
|
||||||
|
uint64 after_event_seq = 3;
|
||||||
|
bool include_existing = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AppendStdinRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
string issue_uuid = 2;
|
||||||
|
bytes data = 3;
|
||||||
|
bool append_newline = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AppendStdinResponse {
|
||||||
|
uint64 write_seq = 1;
|
||||||
|
ControlError error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CloseStdinRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
string issue_uuid = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CloseStdinResponse {
|
||||||
|
uint64 write_seq = 1;
|
||||||
|
ControlError error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ControlSignalCommandRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
string issue_uuid = 2;
|
||||||
|
SignalKind signal = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ControlSignalCommandResponse {
|
||||||
|
uint64 command_revision = 1;
|
||||||
|
ControlError error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetOutputRequest {
|
||||||
|
string client_id = 1;
|
||||||
|
string issue_uuid = 2;
|
||||||
|
repeated StreamKind streams = 3;
|
||||||
|
uint64 after_event_seq = 4;
|
||||||
|
uint32 page_size = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetOutputResponse {
|
||||||
|
repeated CommandEvent events = 1;
|
||||||
|
string next_page_token = 2;
|
||||||
|
bool output_truncated = 3;
|
||||||
|
ControlError error = 4;
|
||||||
|
repeated OutputTruncation truncations = 5;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user