docs(link,installer): protocol 4 is live - correct every stale 3

Protocol 4 shipped on 2026-08-19 as sidecar v2.0.0 and overlay v1.0.0,
paired in bundle 2026.08.19. Three documents still said 3.

INTEGRATION.md contradicted itself: its event catalog fully documents the
protocol-4 kinds, including a subsection headed "On Protocol 4", while
its normative section 2 still declared X-UOLink-Version: 3, "protocol": 3
in /health and ws.hello, and a worked JS client sending "3". That is the
contract a third-party integrator implements against, so it mattered
most. Section 2 now states the current version and what shipped it, and
gains a v4 paragraph alongside v2 and v3. The "(Protocol 3.0)" markers on
individual features are left alone - those record which version
introduced a kind and are correct.

Two stale examples the audit had not spotted: the --print-config sample
reported version 0.1.0, and the minimal browser client told readers to
check ev.protocol === 1, a leftover from protocol 1.

INSTALL.md is the one that actively misconfigured a real install. Line
413 is the "Protocol version" value an operator pastes into Admin ->
Shard, and the document's own troubleshooting table says a stale number
comes back as a 409 that "looks exactly like your shard going offline".
Corrected along with the stale bundle, overlay and sidecar versions
throughout, including Appendix A's copy-pasteable curl commands, which
pointed at overlay v0.1.1 and sidecar v1.1.0.

v4.md still said the cutover had not happened. It has. Its outstanding-
work note said the five-rung shard visibility walk was "outstanding for
the cutover", which is now self-contradictory - so it says plainly that
the cutover shipped without it and no result is recorded anywhere.
guild.roster carries actor objects for every member of every guild, the
widest surface any protocol version has added to that check, so it is
worth walking against the released pair.

No code or contract changes. Every value verified against the Gitea API:
link main sidecar/src/main.rs PROTOCOL_VERSION, servuo-plugins
overlay.toml, and current.json on the installer bundles branch.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-19 17:58:26 -05:00
parent b6d059b1e5
commit 7579d6baa2
3 changed files with 51 additions and 40 deletions

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());