feat(sidecar): protocol 1 — the transport #1

Merged
whitlocktech merged 1 commits from feat/phase-1-transport into main 2026-09-16 01:31:30 +00:00
Member

First code in this repo. The rust-link sidecar: it owns the loopback listener the Oxide bridge plugin dials into, and serves the website a WebSocket feed plus store-backed reads.

Phase 1 of modules/rust/PLAN.md. Lands with the Rust-Plugins, Module-Rust and docs PRs; the four are one change across four repos.

What protocol 1 is

Three frames, deliberately: server.hello, ping/pong, and one correlated server.status. Phase 1's job is the kit's own argument — get every seam working at once with almost nothing in them, so that afterwards you break exactly one at a time.

What is load-bearing rather than incidental

  • The plugin is the TCP client and this process owns the listener, so a Rust server opens no extra port. Loopback is the trust boundary on that link and there is no token on it (org-lead decision D2). The website-facing surface is the opposite: auth always on, token generated and persisted on first start.
  • Inbound lines are capped at 1 MiB from the start, not after the first large frame arrives. An over-long line is discarded and the connection stays up — one malformed frame is not a reason to drop a link live events flow over.
  • Store-backed reads answer while the game is off, which is what lets a website render a server list during a wipe. /status is the one route that fails when the game is down, and /server answers 204 rather than a null when the game has never connected. Those are different answers, and a client that cannot tell them apart renders a server that does not exist.
  • The two RPC failures get distinct codes. 503 means the game is down; 504 means it is up and did not answer. Different fixes.
  • rpc::REPLY_TIMEOUT is a ceiling every later command budget sits under. Core classifies a budget overrun as retryable unconditionally, so an action whose budgetMs does not exceed it can never report retry: false.

One defect found while building

A four-connection SQLite pool over :memory: hands out four separate empty databases, because an in-memory database is per connection. It presents as no such table from a random subset of queries. The pool is now capped at one connection for an in-memory path, with a test asserting the pool size rather than just that a query works — asserting the query would pass again the moment someone tidied the sizing back.

How it was verified

  • cargo test — 36 tests, green. cargo clippy --all-targets -- -D warnings clean, cargo fmt --check clean.
  • Against a live Rust server: a server.hello travelled game → sidecar → module → the public website API. Killing this process left the game untouched; restarting it produced a second hello and the event history survived.
  • The 409 version gate, the 401 on an unauthenticated read and the 204 on an unpopulated board were each exercised by hand.

  • This contribution was AI-assisted (Claude Code).

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

First code in this repo. The rust-link sidecar: it owns the loopback listener the Oxide bridge plugin dials into, and serves the website a WebSocket feed plus store-backed reads. **Phase 1 of [`modules/rust/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust/PLAN.md).** Lands with the Rust-Plugins, Module-Rust and docs PRs; the four are one change across four repos. ## What protocol 1 is Three frames, deliberately: `server.hello`, `ping`/`pong`, and one correlated `server.status`. Phase 1's job is the kit's own argument — get every seam working at once with almost nothing in them, so that afterwards you break exactly one at a time. ## What is load-bearing rather than incidental - **The plugin is the TCP client and this process owns the listener**, so a Rust server opens no extra port. Loopback is the trust boundary on that link and there is no token on it (org-lead decision D2). The website-facing surface is the opposite: auth always on, token generated and persisted on first start. - **Inbound lines are capped at 1 MiB from the start**, not after the first large frame arrives. An over-long line is discarded and the connection stays up — one malformed frame is not a reason to drop a link live events flow over. - **Store-backed reads answer while the game is off**, which is what lets a website render a server list during a wipe. `/status` is the one route that fails when the game is down, and `/server` answers `204` rather than a null when the game has never connected. Those are different answers, and a client that cannot tell them apart renders a server that does not exist. - **The two RPC failures get distinct codes.** `503` means the game is down; `504` means it is up and did not answer. Different fixes. - **`rpc::REPLY_TIMEOUT` is a ceiling every later command budget sits under.** Core classifies a budget overrun as retryable unconditionally, so an action whose `budgetMs` does not exceed it can never report `retry: false`. ## One defect found while building A four-connection SQLite pool over `:memory:` hands out four separate empty databases, because an in-memory database is per *connection*. It presents as `no such table` from a random subset of queries. The pool is now capped at one connection for an in-memory path, with a test asserting the pool size rather than just that a query works — asserting the query would pass again the moment someone tidied the sizing back. ## How it was verified - `cargo test` — 36 tests, green. `cargo clippy --all-targets -- -D warnings` clean, `cargo fmt --check` clean. - **Against a live Rust server**: a `server.hello` travelled game → sidecar → module → the public website API. Killing this process left the game untouched; restarting it produced a second hello and the event history survived. - The `409` version gate, the `401` on an unauthenticated read and the `204` on an unpopulated board were each exercised by hand. --- - [x] This contribution was AI-assisted (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-16 00:57:23 +00:00
The rust-link sidecar: it owns the loopback listener the Oxide bridge plugin
dials into, and serves the website a WebSocket feed plus store-backed reads.

Protocol 1 is deliberately three frames — server.hello, ping/pong, and one
correlated server.status — because phase 1's job is to get every seam working at
once with almost nothing in them.

What is load-bearing rather than incidental:

* The plugin is the TCP client and this process owns the listener, so a Rust
  server opens no extra port. Loopback is the trust boundary on that link and
  there is no token on it; the website-facing surface is the opposite, with auth
  always on and a token generated and persisted on first start.
* Inbound lines are capped at 1 MiB from the start rather than after the first
  large frame arrives. An over-long line is discarded and the connection stays
  up: one malformed frame is not a reason to drop a link live events flow over.
* Store-backed reads answer while the game is off, which is what lets a website
  render a server list during a wipe. /status is the one route that fails when
  the game is down, and /server answers 204 rather than a null when the game has
  never connected -- those are different answers and a client that cannot tell
  them apart renders a server that does not exist.
* The two RPC failures get distinct codes. 503 means the game is down; 504 means
  it is up and did not answer. Different fixes.
* rpc::REPLY_TIMEOUT is a ceiling every later command budget sits under: core
  classifies a budget overrun as retryable unconditionally, so an action whose
  budgetMs does not exceed it can never report retry:false.

One defect found while building, which no unit test would have caught: a
four-connection SQLite pool over :memory: hands out four separate empty
databases, because an in-memory database is per connection. It presents as
'no such table' from a random subset of queries. The pool is now capped at one
connection for an in-memory path, which is the only coherent reading of
:memory: and is what makes it usable at all.

Exercised end to end against a live Rust server: a server.hello travelled game
-> sidecar -> module -> the public website API, and killing this process left
the game untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit c526c55c36 into main 2026-09-16 01:31:30 +00:00
whitlocktech deleted branch feat/phase-1-transport 2026-09-16 01:31:31 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Rust-Link#1
No description provided.