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:
@@ -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. |
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
11
link/v4.md
11
link/v4.md
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user