wtclaude 06fa5d7330
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 3m14s
feat(sidecar): protocol 2 — file by type, a cursor feed, and bounded history
The sidecar now files a frame by its `type` and never by its `kind`. That is the
dumb-forwarder property made structural: `event` is appended to history,
`snapshot` replaces the board of its kind, `reply` is routed by `reqId`,
`control` is broadcast and kept nowhere. Ten new event kinds are no change here
at all, which is the whole point when the thing that grows fastest is the
catalogue.

A frame whose `type` this build does not know is dropped and counted, never
guessed at. Defaulting an absent one to `event` would file a BOARD as history —
the presence board appended a few thousand times, which nothing reports. The
count is on `/health` as `untyped_frames`, because the failure it diagnoses (a
plugin and a sidecar on different protocol versions, which the game link has no
handshake to catch) otherwise presents as a website showing nothing while the
game is plainly up. It caught exactly that within three seconds of first running,
against a protocol 1 plugin still live on a retired rig.

`boards` generalises protocol 1's single `server_state` row, and a database made
by protocol 1 is migrated in place: the two indexed columns are added by a
guarded `ALTER`, and the old board is carried across. Without that carry-over an
upgraded sidecar answers `204` until the game next connects, and the website
reads that as "never heard from" — losing a server it has rendered for weeks at
the exact moment somebody upgraded the bridge.

`GET /feed` is the ingest cursor: oldest first, strictly after an id, with
`lastId` and `more`. It is a separate route rather than a flag on `/events`
because one route with two orderings serves the other one to every caller that
forgets the parameter — and for the ingesting caller that means advancing its
cursor past rows it never read. Omitting `since` asks where the END is; `since=0`
is the other question entirely, and the two must not be separated by whether
somebody typed a parameter.

`[store].retain_days` (default 14) prunes events hourly. Boards are never pruned:
history grows and the present does not, and a pruned board is a server that has
never connected.

The repository also had no CI. `pr-checks.yml` runs the fmt, clippy and test
gates phases 1 and 3 have both been running by hand — a guard nothing invokes is
a guard whose state nobody knows.

44 tests pass, clippy clean at `-D warnings`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-16 08:18:43 -05:00
2026-09-15 19:52:25 -05:00
2026-09-15 19:52:25 -05:00
2026-09-15 19:52:25 -05:00
2026-09-15 19:52:25 -05:00

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.

Description
No description provided
Readme 145 KiB
Languages
Rust 100%