docs(link,installer): protocol 4 is live — correct every stale 3 #172

Merged
whitlocktech merged 1 commits from docs/protocol-4-accuracy into main 2026-08-19 23:25:51 +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: Consequences worth knowing:
- A sidecar patch release does **not** mean re-downloading the installer. The bundle is data. - 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. 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 ### 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) ServUO /opt/ServUO (57.4)
Shard process not running Shard process not running
Overlay servuo-plugins v0.1.1 protocol 3 Overlay servuo-plugins v1.0.0 protocol 4
Sidecar uo-link v1.1.0 protocol 3 Sidecar uo-link v2.0.0 protocol 4
✓ overlay tarball verified sha256 75dc6d6c… ✓ overlay tarball verified sha256 75dc6d6c…
✓ sidecar binary verified sha256 27d491ef… ✓ 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 Base URL http://shard.example.com:8080
WebSocket URL ws://shard.example.com:8080/ws WebSocket URL ws://shard.example.com:8080/ws
Protocol version 3 Protocol version 4
Auth token 4f9c… (also in /etc/runicgateway/sidecar.toml) Auth token 4f9c… (also in /etc/runicgateway/sidecar.toml)
Paste these into Admin → Shard on your Runic Gateway site: 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 | | Base URL (REST) | the **Base URL** line |
| WebSocket URL (feed) | the **WebSocket URL** line | | WebSocket URL (feed) | the **WebSocket URL** line |
| Auth token | the **Auth token** 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 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 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 ```bash
curl -s http://127.0.0.1:8080/health 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 `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. 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) ✓ ServUO found /opt/ServUO (57.4)
✓ Overlay in sync 24 files, all hashes match install.json ✓ Overlay in sync 24 files, all hashes match install.json
⚠ Patch tier 1 applied — moderation-audit (region-match) ⚠ 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 config /etc/runicgateway/sidecar.toml database /var/lib/runicgateway/uo-link.db
✓ Service runicgateway-link.service active, enabled as runicgateway ✓ Service runicgateway-link.service active, enabled as runicgateway
✓ Sidecar reachable 127.0.0.1:8080 /health ok, up 6h, database ok ✓ 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 ✗ Shard connected no — the shard is running (pid 8123) but has not dialed in
✓ Bundle 2026.08.04 — up to date ✓ Bundle 2026.08.19 — up to date
✓ Backups 2026-08-04T09:12:44Z — 3 file(s) replaced by update to bundle 2026.08.04 ✓ Backups 2026-08-19T09:12:44Z — 3 file(s) replaced by update to bundle 2026.08.19
3 kept in /etc/runicgateway/backups 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 ### A2. Deploy the plugin overlay
```bash ```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/v1.0.0/runicgateway-overlay-1.0.0.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/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing # must say: OK 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 cd runicgateway-overlay
cat manifest.json # version, commit, protocol, per-file hashes 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. A1 rather than the one written here.
```bash ```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/v2.0.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/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing sha256sum -c SHA256SUMS --ignore-missing
sudo install -m 0755 uo-link-sidecar-linux-x86_64 /usr/bin/runicgateway-link 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 ```json
{ {
"component": "uo-link-sidecar", "component": "uo-link-sidecar",
"version": "1.1.0", "version": "2.0.0",
"protocol": 3, "protocol": 4,
"config_path": "/etc/runicgateway/sidecar.toml", "config_path": "/etc/runicgateway/sidecar.toml",
"config_created": true, "config_created": true,
"token_generated": true, "token_generated": true,
@@ -859,7 +859,7 @@ path = "uo-link.db" # relative paths resolve against this file's directo
| Sidecar command | Output | | 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 --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. | | `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", "component": "uo-link-sidecar",
"config_created": false, "config_created": false,
"config_path": "/etc/runicgateway/sidecar.toml", "config_path": "/etc/runicgateway/sidecar.toml",
"protocol": 3, "protocol": 4,
"shard": { "bind": "127.0.0.1:7788" }, "shard": { "bind": "127.0.0.1:7788" },
"store": { "path": "/var/lib/runicgateway/uo-link.db" }, "store": { "path": "/var/lib/runicgateway/uo-link.db" },
"token_generated": false, "token_generated": false,
"version": "0.1.0", "version": "2.0.0",
"web": { "web": {
"auth_required": true, "auth_required": true,
"auth_token": "c0f04ace…", "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. The wire protocol is versioned so a mismatch is caught immediately instead of failing weirdly.
- Every response carries an **`X-UOLink-Version: 3`** header. The current version is **4**, shipped in sidecar **v2.0.0** and overlay **v1.0.0**.
- `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**: - 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 ```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. 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 sidecar. There is deliberately **no feature-negotiation array** — v3 implies all three kinds, so the
version number alone tells you what is available. 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 **v4 (Protocol 4.0)** adds `guild.roster` and `guild.leave`, and grows `GET /guilds` a `roster` key
client still declaring `2` gets a 409 on every protected route and, on the WebSocket, a closed ([`v4.md`](v4.md)). Before it, a guild's membership was a *count*; now the members themselves are on
connection on the `ws.hello` mismatch. So update the pinned version at the same time you deploy the the wire. Additive in the same shape as the previous two bumps — nothing that existed in v3 changed,
v3 sidecar. Nothing that existed in v2 changed shape, so that is the whole migration — the website so a v3 consumer that ignores the new kinds and the new key keeps working against a v4 sidecar, once
does it with a one-shot boot migration of its `uo_link_config.protocol` row ([`v3.md`](v3.md) §4.1); it declares `4`.
a third-party client changes the constant it sends.
**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 ```json
{ {
"status": "ok", // "ok" when plugin connected AND db reachable, else "degraded" "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? "plugin_connected": true, // is the shard link up right now?
"database": "ok", // "ok" | "error" "database": "ok", // "ok" | "error"
"uptime": "3d 12h", "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: **On connect**, the first frame is:
```json ```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`. **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) => { ws.onmessage = (m) => {
const ev = JSON.parse(m.data); const ev = JSON.parse(m.data);
switch (ev.kind) { 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 "mob.login": onLogin(ev); break;
case "vendor.sale": onSale(ev); break; case "vendor.sale": onSale(ev); break;
case "house.decay": onIdoc(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: A typical character page:
```js ```js
const H = { "Authorization": `Bearer ${TOKEN}`, "X-UOLink-Version": "3" }; const H = { "Authorization": `Bearer ${TOKEN}`, "X-UOLink-Version": "4" };
// 1. render the roster // 1. render the roster
const roster = await fetch(`${BASE}/roster/${account}`, { headers: H }).then(r => r.json()); 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 # 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. **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 **Date:** 2026-08-17; cutover 2026-08-19
**Codebase:** ServUO 57.4, `<servuo>`, net48 / x64, Expansion **EJ**. **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). **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 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. 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 **Still outstanding, and the cutover shipped without it:** the five-rung shard visibility walk against
`acct`/`webId` never reach a caller below their rung. 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).