Asset Bridge phase 1, sidecar half (docs/link/v8.md §3.3, §14). Shard half: RunicGateway/servuo-plugins#28. Docs half: RunicGateway/docs#236. Three things, one of which is not additive. ## The inbound line cap (§3.3) — the one that matters `read_line` had **no bound at all**. That was survivable only because the shard had never had a reason to send a large line. Protocol 8 gives it one deliberately, and an unbounded read facing a component that now sends megabytes is a memory-exhaustion shape we would be inventing ourselves. `MAX_INBOUND_LINE_BYTES` is **1 MiB** — symmetric with the cap `BridgeLink.cs` has always applied to its own inbound lines, so both directions of this link now read the same. The shard's batch budget is 512 KiB, and the factor of two is load-bearing: a page always admits its first item even when that item alone exceeds the budget (the alternative is an oversized item skipped for the budget on every page forever), so the wire needs room for one overshoot. An over-long line is **discarded and the connection kept** — `BridgeLink.cs`'s own disposition in the other direction. Tearing the link down would take the live event feed with it over one malformed frame, and the lost reply just times out and is re-requested; everything on this plane is idempotent. **`LineReader` holds its state in a struct rather than in locals, and that is the subtle part.** This is polled inside a `tokio::select!`, so the future is dropped whenever a command wins the race. A `discarding` flag in a local would be lost with it — and losing it turns the tail of an over-long line into a line of its own, silently. There is a test for exactly that, and another for an over-long line whose terminator lands in the very chunk that crosses the cap. ## `GET /assets/sources` Stage 1 of the import gate, forwarded verbatim like everything else. `respond_assets` maps `bridge.busy` → **425** and a disabled plane → **403**. 425 deserves a note: on this plane it is not an idempotency collision, it is flow control, and it is the **ordinary** answer mid-import rather than a rare one. The shard serves one asset request at a time because its outbound queue is bounded in lines, not bytes. A caller treating it as an error would abandon a healthy transfer. 403 for the same reason the event plane's gate is a 403: `Bridge.AssetsEnabled` off is an operator declining to let the website read their client files, not a malformed request, and 400 would send an administrator hunting a bug in a correct call. ## `PROTOCOL_VERSION` 7 → 8 Paired with `servuo-plugins/overlay.toml` in the linked PR — the installer refuses to compose a bundle whose halves disagree, so a split bump fails silently at the next release. ## Also `docs/link/INTEGRATION.md` still advertised `X-UOLink-Version: 6`; it was already two versions stale before this change. Fixed in the docs PR. 61 tests pass, `cargo fmt --check` and `cargo clippy -- -D warnings` clean. Verified against the real shard: `/health` reports protocol 8, `/assets/sources` returns 200 with `X-UOLink-Version: 8`, and live events kept flowing through the new reader with no warnings logged. - [x] AI-assisted — Claude Code (Opus 5) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
uo-link — Rust sidecar
The Rust sidecar half of the Runic Gateway bridge. The ServUO shard dials out to this sidecar over a loopback TCP socket (newline-delimited JSON); the sidecar owns the WebSocket + REST API the website consumes, along with auth, buffering, and fan-out.
ServUO plugin (C#, net48) ──loopback TCP, newline-JSON──► Rust sidecar ──WebSocket/JSON──► website
(RunicGateway/servuo-plugins) >>> THIS REPO <<<
The shard never speaks WebSocket and exposes no port of its own — the sidecar is the only network-facing component, which is what keeps the game unreachable from the internet.
Running a shard? Don't build this
The Runic Gateway installer installs this sidecar for you — the released binary, its config, a hardened service account and the service registration — alongside the shard plugin, in one run, on Linux or Windows:
sudo ./runicgateway-installer-linux-x86_64 install
It ends by printing the base URL, WebSocket URL, protocol version and auth token to paste into Admin → Shard on your site. Guide: installer/INSTALL.md.
Installing it yourself is supported too — the release binaries on this repo's releases page are the same ones the installer fetches, and INSTALL.md Appendix A3–A4 covers placing the binary and registering the service by hand.
Everything below this line is for developing on the sidecar.
Related repos
| Repo | What |
|---|---|
this — RunicGateway/link |
The Rust sidecar (sidecar/). |
| RunicGateway/installer | The installer — deploys this sidecar and the plugin onto a shard host. The supported way to set one up. |
| RunicGateway/servuo-plugins | The C# ServUO plugin — the shard side of the bridge (overlay/, patches/, deploy.ps1, test scaffolding). |
| RunicGateway/docs | All project documentation — design docs, protocol spec, integration guide, research. |
Layout
| Path | What |
|---|---|
sidecar/ |
The Rust sidecar crate — terminates the loopback link to the shard, exposes WS + REST to the website. See sidecar/README.md. |
.gitea/workflows/pr-checks.yml |
Gates every PR into main on cargo fmt --check, cargo clippy -D warnings, and cargo test. |
.gitea/workflows/release.yml |
Builds + releases the sidecar binary (Linux + Windows) on every merge to main. |
Build & run (development)
Building from source is for working on the sidecar; a deployment gets its binary from a release, via the installer or by hand. The sidecar is a standard cargo crate:
cd sidecar
cargo build --release # binary at target/release/uo-link-sidecar
cp sidecar.toml.example sidecar.toml # then edit
cargo run --release
Deploying it by hand rather than developing on it: --config <PATH> names the config file (as does
$UOLINK_CONFIG), and --print-config prints the resolved settings — including the auth token
the website needs — as JSON, provisioning the config file on first run. That is the supported way
to read the token back; it is not meant to be scraped from the log.
uo-link-sidecar --print-config --config /etc/runicgateway/sidecar.toml
Running as a service
The same binary runs in the foreground and as a system service — there is no --service flag to
remember, because the process can tell how it was started.
- Linux/systemd supervises any foreground process, so the unit just runs the binary.
SIGTERM(whatsystemctl stopsends) andSIGINTboth unwind it cleanly; logs go to the journal. - Windows cannot. The service control manager only supervises a process that connects back to
it within ~30 seconds via
StartServiceCtrlDispatcher; a plain console program registered withsc.exe createis killed with error 1053 despite running perfectly. So on Windows the sidecar speaks that handshake: started by the SCM it runs as a service, started from a shell the connect fails withERROR_FAILED_SERVICE_CONTROLLER_CONNECTand it falls through to an ordinary foreground run. It reportsRunningonly once the shard port is bound and the store is open, and — having no console — logs touo-link-sidecar.<date>.logbeside its config, rolled daily.
Only the starting and stopping is platform-specific: src/app.rs is the entire sidecar and is
shared, while src/windows.rs and src/unix.rs do nothing but start it and tell it when to stop.
The Windows crates are declared under [target.'cfg(windows)'.dependencies], so Cargo neither
resolves nor builds them for a Linux target.
Registering the service is the installer's job; to do it by hand see INSTALL.md Appendix A4.
.gitea/workflows/release.yml cross-compiles Linux + Windows binaries and cuts a Gitea release on
every merge to main (conventional-commit versioning). See sidecar/README.md
for configuration and the wire protocol.
Before that, .gitea/workflows/pr-checks.yml runs the same gates on every pull request into main —
cargo fmt --check, cargo clippy --all-targets -- -D warnings, then cargo test --locked. Run them
locally before pushing and the PR will be green:
cd sidecar
cargo fmt # or --check to just report
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
Deployment & compatibility
The plugin (RunicGateway/servuo-plugins) and this sidecar are deployed together but built independently:
- The plugin is deployed as source into the ServUO server root and compiled by ServUO at boot — no build artifact, no CI build.
- The sidecar is a standalone Rust binary released from this repo.
The only coupling is the loopback JSON protocol (the shard dials 127.0.0.1). Compatibility is a
protocol concern, not a build-order one — keep the event/command catalog in sync across the two
repos. Canonical spec:
PLAN.md §5/§7 and
INTEGRATION.md.
Because a wedged or absent sidecar cannot stall the shard, either side can be deployed or restarted
independently.
License
Runic Gateway is free software, licensed under the GNU General Public License v3.0 or later — see LICENSE.md.
Copyright (C) 2026 Runic Gateway
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version. It is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
Contributions are welcome — please read CONTRIBUTING.md (note the AI-usage disclosure requirement) and our Code of Conduct. Report vulnerabilities privately per SECURITY.md.