Merge pull request 'docs(link,installer): protocol 4 is live — correct every stale 3' (#172) from docs/protocol-4-accuracy into main

Reviewed-on: #172
This commit is contained in:
2026-08-19 23:25:49 +00:00
3 changed files with 51 additions and 40 deletions

View File

@@ -116,7 +116,7 @@ time rather than hardcoding versions or blindly taking each repo's newest releas
Consequences worth knowing:
- A sidecar patch release does **not** mean re-downloading the installer. The bundle is data.
- `--bundle <tag>` (e.g. `--bundle 2026.08.04`) pins an exact past combination, so a reinstall six
- `--bundle <tag>` (e.g. `--bundle 2026.08.19`) pins an exact past combination, so a reinstall six
months from now reproduces today's install rather than tomorrow's.
---
@@ -156,12 +156,12 @@ one file); `doctor`, `update` and `uninstall` are run from it later. Examples be
### An illustrative run
```
Runic Gateway installer — bundle 2026.08.04 (protocol 3)
Runic Gateway installer — bundle 2026.08.19 (protocol 4)
ServUO /opt/ServUO (57.4)
Shard process not running
Overlay servuo-plugins v0.1.1 protocol 3
Sidecar uo-link v1.1.0 protocol 3
Overlay servuo-plugins v1.0.0 protocol 4
Sidecar uo-link v2.0.0 protocol 4
✓ overlay tarball verified sha256 75dc6d6c…
✓ sidecar binary verified sha256 27d491ef…
@@ -410,7 +410,7 @@ One manual step remains — connect the website to this sidecar:
Base URL http://shard.example.com:8080
WebSocket URL ws://shard.example.com:8080/ws
Protocol version 3
Protocol version 4
Auth token 4f9c… (also in /etc/runicgateway/sidecar.toml)
Paste these into Admin → Shard on your Runic Gateway site:
@@ -430,7 +430,7 @@ On your site, sign in as an administrator and open **Admin → Shard (uo-link)**
| Base URL (REST) | the **Base URL** line |
| WebSocket URL (feed) | the **WebSocket URL** line |
| Auth token | the **Auth token** line |
| Protocol | the **Protocol version** line (`3`) |
| Protocol | the **Protocol version** line (`4`) |
Saving restarts the site's ingest client, so the change takes effect immediately. The token is
AES-GCM encrypted at rest and **never returned to any client** — losing it means reading it back
@@ -490,7 +490,7 @@ shedding events rather than stalling — which it is designed to do. `[bridge re
```bash
curl -s http://127.0.0.1:8080/health
{"status":"ok","protocol":3,"plugin_connected":true,"database":"ok","uptime":"2m","last_event":"2026-08-04T18:22:10.412Z"}
{"status":"ok","protocol":4,"plugin_connected":true,"database":"ok","uptime":"2m","last_event":"2026-08-19T18:22:10.412Z"}
```
`plugin_connected: true` is the one that matters — it is the only value in this whole guide that
@@ -509,18 +509,18 @@ The command that makes this supportable. Run it before asking anyone for help
first thing a maintainer will want.
```
✓ Install record /etc/runicgateway/install.json (bundle 2026.08.04, installer 1.0.0, …)
✓ Install record /etc/runicgateway/install.json (bundle 2026.08.19, installer 0.1.0, …)
✓ ServUO found /opt/ServUO (57.4)
✓ Overlay in sync 24 files, all hashes match install.json
⚠ Patch tier 1 applied — moderation-audit (region-match)
✓ uo-link installed uo-link-sidecar 1.1.0 (protocol 3)
✓ uo-link installed uo-link-sidecar 2.0.0 (protocol 4)
config /etc/runicgateway/sidecar.toml database /var/lib/runicgateway/uo-link.db
✓ Service runicgateway-link.service active, enabled as runicgateway
✓ Sidecar reachable 127.0.0.1:8080 /health ok, up 6h, database ok
✓ Protocol sidecar 3 = overlay manifest 3
✓ Protocol sidecar 4 = overlay manifest 4
✗ Shard connected no — the shard is running (pid 8123) but has not dialed in
✓ Bundle 2026.08.04 — up to date
✓ Backups 2026-08-04T09:12:44Z — 3 file(s) replaced by update to bundle 2026.08.04
✓ Bundle 2026.08.19 — up to date
✓ Backups 2026-08-19T09:12:44Z — 3 file(s) replaced by update to bundle 2026.08.19
3 kept in /etc/runicgateway/backups
```
@@ -654,11 +654,11 @@ Use those versions together; that pairing is the only thing CI has verified.
### A2. Deploy the plugin overlay
```bash
curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v0.1.1/runicgateway-overlay-0.1.1.tar.gz
curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v0.1.1/SHA256SUMS
curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v1.0.0/runicgateway-overlay-1.0.0.tar.gz
curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v1.0.0/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing # must say: OK
tar xzf runicgateway-overlay-0.1.1.tar.gz # → runicgateway-overlay/
tar xzf runicgateway-overlay-1.0.0.tar.gz # → runicgateway-overlay/
cd runicgateway-overlay
cat manifest.json # version, commit, protocol, per-file hashes
@@ -702,8 +702,8 @@ says `aarch64`); releases from v1.2.0 carry both. Take the version from the bund
A1 rather than the one written here.
```bash
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/uo-link-sidecar-linux-x86_64
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/SHA256SUMS
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v2.0.0/uo-link-sidecar-linux-x86_64
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v2.0.0/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing
sudo install -m 0755 uo-link-sidecar-linux-x86_64 /usr/bin/runicgateway-link
@@ -722,8 +722,8 @@ sudo UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db \
```json
{
"component": "uo-link-sidecar",
"version": "1.1.0",
"protocol": 3,
"version": "2.0.0",
"protocol": 4,
"config_path": "/etc/runicgateway/sidecar.toml",
"config_created": true,
"token_generated": true,
@@ -859,7 +859,7 @@ path = "uo-link.db" # relative paths resolve against this file's directo
| Sidecar command | Output |
|---|---|
| `uo-link-sidecar --version` | `uo-link-sidecar 1.1.0 (protocol 3)` |
| `uo-link-sidecar --version` | `uo-link-sidecar 2.0.0 (protocol 4)` |
| `uo-link-sidecar --print-config [--config PATH]` | The JSON in [A3](#a3-install-the-sidecar). Provisions on first run. **Contains the token.** |
| `uo-link-sidecar --help` | Usage. An unrecognized argument exits `2` rather than starting a sidecar you did not ask for. |

View File

@@ -33,11 +33,11 @@ $ uo-link-sidecar --print-config --config /etc/runicgateway/sidecar.toml
"component": "uo-link-sidecar",
"config_created": false,
"config_path": "/etc/runicgateway/sidecar.toml",
"protocol": 3,
"protocol": 4,
"shard": { "bind": "127.0.0.1:7788" },
"store": { "path": "/var/lib/runicgateway/uo-link.db" },
"token_generated": false,
"version": "0.1.0",
"version": "2.0.0",
"web": {
"auth_required": true,
"auth_token": "c0f04ace…",
@@ -55,12 +55,14 @@ That is the same set of values Admin → Shard asks for — base URL and WS URL
The wire protocol is versioned so a mismatch is caught immediately instead of failing weirdly.
- Every response carries an **`X-UOLink-Version: 3`** header.
- `GET /health` and the WebSocket `ws.hello` frame include `"protocol": 3`.
- **Optionally**, send `X-UOLink-Version: 3` on your requests. If it disagrees with the sidecar, the request is rejected **409 Conflict**:
The current version is **4**, shipped in sidecar **v2.0.0** and overlay **v1.0.0**.
- Every response carries an **`X-UOLink-Version: 4`** header.
- `GET /health` and the WebSocket `ws.hello` frame include `"protocol": 4`.
- **Optionally**, send `X-UOLink-Version: 4` on your requests. If it disagrees with the sidecar, the request is rejected **409 Conflict**:
```json
{ "error": "protocol version mismatch", "sidecar_protocol": 3, "client_protocol": "2" }
{ "error": "protocol version mismatch", "sidecar_protocol": 4, "client_protocol": "3" }
```
Pin the version you built against and compare it to the header (or `/health.protocol`) at startup.
@@ -74,12 +76,18 @@ ignores unknown kinds keeps working against the live feed, but the three new end
sidecar. There is deliberately **no feature-negotiation array** — v3 implies all three kinds, so the
version number alone tells you what is available.
**Upgrading a v2 integration.** The bump is an operator-visible hard break in one direction only: a
client still declaring `2` gets a 409 on every protected route and, on the WebSocket, a closed
connection on the `ws.hello` mismatch. So update the pinned version at the same time you deploy the
v3 sidecar. Nothing that existed in v2 changed shape, so that is the whole migration — the website
does it with a one-shot boot migration of its `uo_link_config.protocol` row ([`v3.md`](v3.md) §4.1);
a third-party client changes the constant it sends.
**v4 (Protocol 4.0)** adds `guild.roster` and `guild.leave`, and grows `GET /guilds` a `roster` key
([`v4.md`](v4.md)). Before it, a guild's membership was a *count*; now the members themselves are on
the wire. Additive in the same shape as the previous two bumps — nothing that existed in v3 changed,
so a v3 consumer that ignores the new kinds and the new key keeps working against a v4 sidecar, once
it declares `4`.
**Upgrading a pinned client.** Every bump is an operator-visible hard break in one direction only: a
client still declaring the old number gets a 409 on every protected route and, on the WebSocket, a
closed connection on the `ws.hello` mismatch. So update the pinned version at the same time you
deploy the new sidecar. Nothing existing has ever changed shape across a bump, so that is the whole
migration — the website does it with a one-shot boot migration of its `uo_link_config.protocol` row
([`v3.md`](v3.md) §4.1); a third-party client changes the constant it sends.
---
@@ -92,7 +100,7 @@ GET /health (no auth)
```json
{
"status": "ok", // "ok" when plugin connected AND db reachable, else "degraded"
"protocol": 3,
"protocol": 4,
"plugin_connected": true, // is the shard link up right now?
"database": "ok", // "ok" | "error"
"uptime": "3d 12h",
@@ -115,7 +123,7 @@ A push-only stream of game events as they happen. You do **not** send commands o
**On connect**, the first frame is:
```json
{ "kind": "ws.hello", "protocol": 3 }
{ "kind": "ws.hello", "protocol": 4 }
```
**Then** a continuous stream of event frames, each with at least `t` (epoch ms) and `kind`. Route on `kind`.
@@ -133,7 +141,7 @@ const ws = new WebSocket(`ws://127.0.0.1:8080/ws?token=${TOKEN}`);
ws.onmessage = (m) => {
const ev = JSON.parse(m.data);
switch (ev.kind) {
case "ws.hello": /* check ev.protocol === 1 */ break;
case "ws.hello": /* check ev.protocol === 4 */ break;
case "mob.login": onLogin(ev); break;
case "vendor.sale": onSale(ev); break;
case "house.decay": onIdoc(ev); break;
@@ -1012,7 +1020,7 @@ sidecar defines no audiences. Deciding who may see what is the consuming site's
A typical character page:
```js
const H = { "Authorization": `Bearer ${TOKEN}`, "X-UOLink-Version": "3" };
const H = { "Authorization": `Bearer ${TOKEN}`, "X-UOLink-Version": "4" };
// 1. render the roster
const roster = await fetch(`${BASE}/roster/${account}`, { headers: H }).then(r => r.json());

View File

@@ -1,7 +1,7 @@
# Protocol 4 — Guild membership on the wire
**Status:** Feature-complete on `edge`. All work lands on an `edge` branch in each repo; `edge``main` is the v4 cutover.
**Date:** 2026-08-17
**Status:** **Shipped.** The `edge``main` cutover is done and protocol 4 is live — released as uo-link sidecar **v2.0.0** and plugin overlay **v1.0.0**, paired in bundle **2026.08.19**.
**Date:** 2026-08-17; cutover 2026-08-19
**Codebase:** ServUO 57.4, `<servuo>`, net48 / x64, Expansion **EJ**.
**Companion to** [`PLAN.md`](PLAN.md) (1.0 read/event plane), [`PROTOCOL_2.md`](PROTOCOL_2.md) (2.0 provisioning + world-state streams), [`v3.md`](v3.md) (3.0 shard content + the visibility framework), [`INTEGRATION.md`](INTEGRATION.md) (website API).
@@ -265,5 +265,8 @@ evidence and neither is a clean boot. What was actually run:
Step 2 is what caught the reassembly bug in §3.1: every unit test passed through it, because they all
exercised a single-frame roster. The case does not arise until a guild exceeds the cap.
Still outstanding for the cutover: the five-rung shard visibility walk against a live shard, confirming
`acct`/`webId` never reach a caller below their rung.
**Still outstanding, and the cutover shipped without it:** the five-rung shard visibility walk against
a live shard, confirming `acct`/`webId` never reach a caller below their rung. `guild.roster` carries
actor objects for every member of every guild, so it is the widest surface any protocol version has
added to that check — the walk is worth doing against the released pair even though v4 is now live.
No result is recorded here or in [`../website/SHARD_VISIBILITY.md`](../website/SHARD_VISIBILITY.md).