Files
Rust-Link/README.md
wtclaude e2a58f3455 feat(sidecar): protocol 1 — the transport
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
2026-09-15 19:52:55 -05:00

3.2 KiB

rust-link

The sidecar half of the Runic Gateway bridge for Rust. It terminates the loopback link from a Rust server's Oxide bridge plugin and exposes the WebSocket + REST surface the website consumes.

It is the mirror of RunicGateway/link, which does the same job for Ultima Online, and it keeps that bridge's central invariant unchanged:

The game server is never reachable from the website. The plugin dials out to this process; this process owns the listener. Only the sidecar is exposed, and only the website's backend talks to it.

Rust server + Oxide  (RunicGateway/Rust-Plugins, C#)
   │  loopback TCP 127.0.0.1:7799, newline-delimited JSON, bidirectional
   │  the PLUGIN dials out (the game opens no listening port for us)
   ▼
rust-link sidecar  (this repo, Rust)   ← the only network-facing bridge component
   │  WebSocket (live feed) + REST (point-in-time reads), bearer-token auth
   ▼
website backend + module-rust  (RunicGateway/Module-Rust, Node)

One server, one sidecar

This binary serves exactly one Rust game server. A community running six servers runs six pairs, each with its own port, database and token; module-rust holds six clients and the website core never learns there is more than one. Nothing here is multiplexed, and nothing here should become multiplexed.

Build and run

cd sidecar
cargo build --release          # → target/release/rust-link-sidecar
cargo run                      # info logging; writes sidecar.toml (with a generated token) on first run
RUST_LOG=debug cargo run       # verbose, including heartbeats

Everything is configured from sidecar.toml — nothing is compiled into the binary. Auth is always on: a blank token is generated and written back on first start, so there is no state in which this process listens without one. --print-config resolves the configuration and prints it as JSON, which is how an installer reads the token back without scraping a log.

See sidecar/README.md for the configuration reference and the endpoint list.

The protocol is a contract

The loopback JSON protocol (plugin ↔ sidecar) and this sidecar's HTTP/WS API (sidecar ↔ website) are versioned compatibility contracts, not build dependencies. PROTOCOL_VERSION lives in sidecar/src/main.rs; every response carries X-RustLink-Version, and a client that declares a different one is refused 409 rather than served something it will mis-parse.

A version is declared in three places and they must agree:

Where Repo
PROTOCOL_VERSION this repo
overlay.toml RunicGateway/Rust-Plugins
module.json RunicGateway/Module-Rust

The canonical spec is docs/rust-link/. If you add or change an event or a command, update all three repos and the spec in the same change.

Licence

GPL-3.0-or-later. See LICENSE.md.