Files
rvbox/docs/architecture.md
T

20 KiB
Raw Blame History

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.

The first supported Go client is the Windows service client. The Linux client is implemented afterward against the already proven shared runtime/protocol and remains required for the complete v1 scope. It is explicitly deferred from the current implementation effort: implement the Linux server and native Windows client first, and do not begin Unix-like client implementation yet. The server remains Linux-first.

RVBox deliberately executes arbitrary commands under locally selected execution identities. It is therefore an administrative tool, not a multi- tenant remote-execution service. On Windows the main service runs as LocalSystem and maps the request's elevation intent plus current login state to an effective user/session context. A peer that can successfully impersonate the client's routing identity can request elevation, whose fallback may reach SYSTEM. This makes the accepted self-reported-identity/network trust boundary especially consequential; the explicit elevation bit and tray visibility are intent/audit signals, not authorization controls.

Both daemons use strict TOML 1.0 configuration. The normative schema and fully annotated examples are in the configuration contract.

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 from the same durable client instance replaces its prior live session. A different instance is accepted normally when no session for that client ID is live. While one is live, a different instance is rejected unless an operator grants a one-shot override for that exact pending instance. This prevents accidental hostname collisions but is not authentication: a peer able to reach nginx and copy or guess the identifiers can still impersonate a client. 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

rvc -- gRPC/Unix socket -- rvbox-server -- WSS/nginx -- rvbox service -- shell
                         |       |                         |
                    SQLite/WAL   |                  local named pipe
                         |       |                         |
               compressed data  +-- optional JSON-RPC    +-- per-session tray

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.

On Windows, SCM starts one machine-wide LocalSystem service before login. It alone owns networking, durable state, logs, command admission, process handles, and Job Objects. A separate unelevated tray may run in each logged-in session and communicates only through an ACL-protected local named pipe; it is an optional frontend and its exit or absence does not stop the service. Task Scheduler is not used.

Identity, sessions, and lifecycle

  1. The client connects over WSS and sends ClientHello with its client ID, protocol capability, OS/architecture, daemon version, configured daemon CWD/ Windows work root, supported shells, and a durable random client-instance UUID.
  2. The server accepts the current compatible protocol version, fences the previous connection for that ID and same client instance, and returns a fresh server-issued session_id and monotonic session_generation. A live claim from a different client instance is rejected while the current session is live unless an operator has explicitly authorized that pending instance. With no live session, the new instance is accepted normally.
  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 performs bidirectional reconciliation between the server's non-terminal set and the client's complete retained-command set. The server returns explicit local terminate/discard decisions before new dispatch begins.

Command model

Each user request has a UUID (issue_uuid) and a durable request record: target client, request/issue timestamps, shell type, command text or script descriptor, CWD, environment overrides, Windows elevation intent and effective execution identity, resource-profile flags, and lifecycle state. rvc normally supplies this UUID as its optional request_id; the server generates one when it is omitted. Command and mutation identifiers are UUIDv7 values. Transport is at-least-once, but the client durably remembers accepted UUIDs and provides an at-most-once execution guarantee: it never authorizes the same request to execute twice. A crash in the launch window may interrupt a command before its requested code runs, but must never cause an automatic retry with an uncertain prior outcome.

After full terminal history is removed, each client retains a compact FIFO of the most recent 1,000,000 command tombstones containing binary UUIDv7, immutable request hash, and completion/acknowledgement time. The server retains the most recent 1,000,000 command tombstones globally as a first-line duplicate check. A matching UUID/hash returns structured ALREADY_EXECUTED; a matching UUID with different immutable content is a conflict. These ledgers have separate count-based budgets and do not retain command payload or output. Replay protection older than the retained client tombstone horizon is best-effort.

The server states are:

queued      -> dispatched | cancelled | expired
dispatched  -> queued | accepted | rejected | cancelled
accepted    -> running | rejected | cancelled
running     -> succeeded | failed | terminated | interrupted

Recovery/corruption handling may also move an affected dispatched or accepted command to interrupted; those are exceptional reconciliation transitions, not normal execution outcomes.

accepted means the client has durably admitted the request; running means the requested process has been launched. cancelled is used when it is stopped before launch. A permanent pre-launch validation, script-transfer, or process- preparation failure is terminal rejected; failed is reserved for code that actually launched. A transient capacity rejection returns to queued with backoff and remains subject to its queue TTL. The server may cancel work that has never been dispatched immediately. For dispatched or accepted work, it persists a higher command revision and sends the revisioned signal without prematurely declaring a terminal state. The client either returns a revisioned cancelled lifecycle before launch authorization or applies the signal after authorization and returns a revisioned signal result. Cancellation intent remains internal rather than adding a public lifecycle state.

Queued work has a configurable acceptance deadline, default 15 minutes; zero explicitly means no expiry. A command that was never dispatched becomes terminal expired at its deadline. A dispatched command whose acceptance is uncertain remains non-terminal and is displayed as expired pending reconciliation. Later client evidence updates the actual lifecycle. Acceptance or execution observed after the deadline creates an incident and is displayed as late-after-expiry; the server requests termination but continues recording the actual client-reported outcome.

Process launch has internal durable phases launch_prepared and launch_authorized between public accepted and running. The client creates the process behind an OS-specific execution barrier, durably records its process identity, durably authorizes launch, and only then releases requested command code. Once authorization is durable, an uncertain outcome is reconciled as interrupted, never by redispatching that UUID.

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. The server separately defaults to at most 1,000 queued commands for one target client and 10,000 queued commands globally, subject to the stricter byte quotas.

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.
  • The client materializes command_text as a private generated wrapper and executes that file with exactly the selected shell: sh/bash, cmd, or PowerShell. This avoids transport quoting and Windows command-line limits; it never performs shell detection or fallback. Wrapper cleanup follows the same terminal rule as uploaded scripts.
  • Unix commands receive the daemon account's permissions and startup environment. Windows interprets the request's elevated boolean through a login-aware hierarchy. With a usable active session, normal work uses a deliberately non-elevated active_user token; elevated work tries active_user_elevated, then active_system, then local_system. With no usable active session, normal work uses local_service and elevated work uses local_system. Fallback is allowed only during token selection before launch_prepared, never after a process may have started. Requested elevation, attempted contexts, effective token SID, session ID/session-owner SID, and selection detail are persisted in command history.
  • A command receives the base environment for its effective identity, overlaid with the persisted env_overrides map. The effective CWD is the requested existing accessible directory when supplied. When omitted it is the registered daemon CWD on Unix or an ACL-isolated identity child beneath that registered root on Windows.
  • 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.
  • Terminal lifecycle means the supervised tree is empty, not only that its root shell exited. After root exit, RVBox allows a configurable 5-second descendant and output-drain grace period, then terminates residual descendants. It drains capture pipes and durably sequences all retained output (or an explicit OutputIncomplete marker) before emitting the terminal lifecycle event.
  • 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 exactly the selected shell, and removes it after the command reaches a terminal state. The descriptor filename is display metadata only and is never used as a path component. 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 the portable v1 set HUP, INT, TERM, KILL, USR1, and USR2 (including their SIG-prefixed CLI spellings). Arbitrary native signal numbers are not part of v1. On Windows, only TERM and KILL are valid. TERM makes a best-effort CTRL_BREAK_EVENT delivery to the dedicated console group, waits 10 seconds, then terminates the Job Object if needed. KILL 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 transmitted execution event has a strictly increasing event_seq scoped to one command. The client first stores lifecycle transitions, stdout/stderr, stdin acknowledgements, resource snapshots, signals, and terminal state in a durable local order. It durably assigns wire sequences only as entries enter the bounded send window, after any unsent-output compaction. Once assigned, an event is pinned until acknowledged and retry content is immutable. A client-side truncation marker therefore consumes a normal sequence without creating a wire gap. Server-side retention exposes removed event ranges as query metadata without allocating client sequences. The server also records receipt time; event_seq remains the canonical transmitted order across streams and retries.

The terminal lifecycle event is always the final client event for a command. The control-plane follow wrapper emits any server-created retention metadata before returning that terminal event. Followers may therefore stop on terminal without missing subsequently sequenced stdout/stderr or known truncation data.

All command-owned stored data is quota-accounted: execution metadata, script body, pending stdin, events, and output. Stored message/blob payloads are Zstandard-compressed; necessary SQLite index/state columns are charged by their encoded lengths plus a conservative versioned per-row/index overhead rather than pretending they are free. This logical accounting is deterministic across SQLite compaction. A separate filesystem free-space floor protects WAL, temporary files, tombstones, and accounting variance. The default total is 32 MiB per command, 256 MiB per client on both client and server, and 4 GiB server-wide. A separate 10 MiB rolling output window remains per command, and raw script input remains limited to 10 MiB. Client accounting also charges the raw generated execution wrapper/script file while it exists, even though the compressed durable source was already charged.

Each accepted active command reserves 64 KiB of its quota for bounded closeout metadata. Essential state for an accepted stdin/signal mutation is additionally reserved before that mutation succeeds. Essential active state is never silently rolled. Server output is evictable: the oldest retained chunks are removed first and sequence-range query metadata remains. The client removes acknowledged data, pins its bounded assigned send window, and may replace older unsequenced output with normally sequenced byte-loss markers. While offline or under sustained overload, it retains the newest tail and records exact known lost bytes. At client/server aggregate limits, terminal commands are evicted as whole UUID records in oldest server issue-time/UUIDv7 order first. If active data alone reaches a limit, output rotates or enters loss mode and new essential allocations are rejected with CAPACITY_EXHAUSTED; pipes continue draining.

Transient raw output also has bounded high/low watermarks before compression: 1 MiB/256 KiB per command, 8 MiB/4 MiB per client daemon, and 64 MiB/32 MiB server-wide by default. Crossing a client high watermark enters loss mode; still-unsequenced output bytes may be discarded before compression and replaced in durable local order by a later OutputTruncation. Loss mode ends only below the corresponding low watermark. The server never drops an already sequenced client event: at its ingress high watermark it withholds acknowledgement and closes an overproducing session if bounded admission cannot continue, letting the durable client retry after the server backlog falls below its low watermark. Lifecycle, stdin acknowledgements, signal results, and truncation/incomplete markers use reserved capacity and are never treated as droppable output.

Independently of byte pressure, the server reclaims each whole terminal command and all command-owned data 30 days after its terminal time by default. Zero explicitly disables age rotation. Compact replay tombstones, audit records, and storage incidents remain under their separate retention policies.

Storage health is derived from durable incident records. Safe repairs resolve an incident automatically; known data loss remains dirty until an operator explicitly acknowledges it. Resolution clears the dirty health flag but does not erase records as part of that action. Unresolved compact records are non-evictable; resolved summaries and audit entries follow the separate 100 MiB audit/incident history rotation. Recovery and incident management are described in the platform contract and exposed through the control plane.

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. In v1, command text, scripts, environment values, stdin, output, and other command-owned payloads are stored in plaintext; application-level encryption and key management are deferred to a future version. Audit storage uses compressed segments under a separate 100 MiB default quota and rotates complete oldest segments. Audit retention is configured independently of command retention; its default zero age limit means quota-only rotation.

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.