From 5d7633c051280baed243c1dd967bc583397fe1f2 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 25 Sep 2026 23:42:34 -0500 Subject: [PATCH 1/5] docs(rust): the Rust operator guide and bundle schema 2 (phase 18, draft) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- installer/PLAN.md | 30 ++++++ rust-link/INSTALL.md | 234 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 rust-link/INSTALL.md diff --git a/installer/PLAN.md b/installer/PLAN.md index 236dce8..bbfb423 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -1309,6 +1309,36 @@ version anywhere.** It reads what the artifacts declare, and §7.1's gate 1 is w mismatched pair from being published as a bundle — which is the mechanism that will matter at the *next* protocol bump, not just this one. See `docs/link/v3.md`. +### 7.5 Schema 2: a second game (module-rust phase 18) + +Rust reached the installer in module-rust phase 18 (`docs/modules/rust/PLAN.md` §34, D146–D148), and +schema 1 could not carry it: it names ServUO in its *shape* (`link`, `overlay.servuo`), and every +shipped installer refuses any schema but 1. So the bundle gained a second schema, **for both games**, +rather than a separate format for Rust: + +- **Schema 2 names one game.** A `game` discriminant (`servuo` or `rust`), a `sidecar`, and a + `payload` whose `kind` is `overlay` or `plugin`. One document per game, because the games release + on their own schedules and a document naming both would hand a ServUO host a new bundle every time + a Rust plugin shipped. The shape is in `installer/bundles/README.md`. +- **Three streams on the `bundles` branch:** schema 1 at the root (ServUO), `v2/servuo/`, and + `v2/rust/`. `bundle.yml` composes all three in `.gitea/scripts/compose-bundles.sh`, each game + independently — a Rust failure still lets a ServUO bundle publish, and the run goes red after. A + game none of whose repos has released composes nothing, and that is not a failure. +- **One matrix, one tag.** A ServUO pair carries the same tag at both schemas, so the first + `v2/servuo` bundle is `2026.09.15` — the tag schema 1 had already given that pair. +- **Schema 1 is kept alive until 2027-01-01** (D147), composed exactly as before so every installer + in the field keeps updating. After that date it is **frozen, never deleted**: an old installer still + resolves its last bundle and every `bundle-.json` stays pinnable. +- **The installer from phase 18 reads both.** ServUO resolves `v2/servuo/` first and is *lowered* + into the schema-1 model, so `install.json` and every ServUO code path are unchanged; a pin that + exists only at schema 1 falls back to the root. Rust resolves `v2/rust/`. +- **v2 is read through Gitea's contents API**, not `/raw/`: the web raw route answers + `Cache-Control: public, max-age=21600`, so a bundle read through it can be hours behind the + branch. The contents API is `private, must-revalidate`. (Schema 1's fallback still uses `/raw/`, + as every shipped installer does.) + +What `--game rust` installs from a Rust bundle, and where, is `docs/rust-link/INSTALL.md`. + --- ## 8. Open questions diff --git a/rust-link/INSTALL.md b/rust-link/INSTALL.md new file mode 100644 index 0000000..2669dcf --- /dev/null +++ b/rust-link/INSTALL.md @@ -0,0 +1,234 @@ +# Installing the Rust bridge + +This connects **Rust servers** — running Oxide or Carbon — to a Runic Gateway website that has the +Rust module. It is the Rust counterpart of [`../installer/INSTALL.md`](../installer/INSTALL.md), and +the design of record is [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §34. + +## What this installs + +Two components per Rust server, released together as a **bundle** — an exact pair CI has checked +speaks one protocol, never "the latest of each": + +| Component | What it is | Released from | +|---|---|---| +| **The plugin** | `RunicGateway.cs`, one file that runs unchanged on Oxide and Carbon | [Rust-Plugins](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/releases) | +| **The sidecar** | `rust-link-sidecar`, which the plugin dials on loopback and the website reaches over HTTP | [Rust-Link](https://gitea.whitlocktech.com/RunicGateway/Rust-Link/releases) | + +The game server opens no port for the bridge: the plugin is the client and the sidecar the +listener, on `127.0.0.1`. **One sidecar serves one Rust server.** A community running six servers +runs six sidecars, each with its own port, database and token, and the website holds six entries. + +There are three ways to set a server up. They produce the same result: + +| | Use it when | +|---|---| +| [**A. The installer**](#a-the-installer) | The server runs on a Linux host with systemd, or on Windows | +| [**B. The Pterodactyl egg**](#b-the-pterodactyl-egg) | The server runs on a Pterodactyl panel | +| [**C. By hand**](#c-by-hand) | Neither fits, or you want to place every file yourself | + +## Before you begin + +- **Oxide or Carbon is installed, and the server has started once** with it, so its directories + exist. The bridge is a plugin; a vanilla server has nothing to load it. +- **Kits and ZoneManager** from uMod are what the reward and zone features use. The bridge works + without them and names what is missing; nothing here installs them. +- **The website has the Rust module**, and you are an administrator on it. Servers are added at + **Admin → Rust → Servers** (`/admin/rust/servers`). + +> **Until module-rust phase 19**, the released Rust module (Module-Rust v0.3.0) speaks protocol 2 +> while the released bridge speaks 12, and a site pairing them answers `409`. This is expected and +> closes with phase 19's release; it is not a fault in your install. + +--- + +## A. The installer + +The [Runic Gateway installer](https://gitea.whitlocktech.com/RunicGateway/installer/releases) — +the same binary that sets up ServUO shards — with `--game rust`. Run it as root, or from an +elevated PowerShell: + +```bash +sudo ./runicgateway-installer-linux-x86_64 install --game rust --rust /srv/rust --server-id main +``` + +```powershell +.\runicgateway-installer-windows-x86_64.exe install --game rust --rust D:\rust --server-id main +``` + +| Flag | | +|---|---| +| `--rust ` | The server root: the directory holding `RustDedicated` | +| `--server-id ` | **Required.** The id the website will know this server by: lowercase letters, digits and `-`, up to 64. It names this server's sidecar, service and files | +| `--web-port ` | The port the website reaches this sidecar on. Default: the first free from 8090 | +| `--bundle ` | Install an exact published bundle instead of the current one | +| `--verify` | Report everything it would do, and write nothing | +| `--host ` | The hostname to print in the sidecar URL | +| `--site-url ` | Your site, so the printed link opens the right page | + +What a run does: + +1. **Resolves the bundle** for Rust and checks the plugin's own manifest against it. +2. **Detects the framework** — Oxide or Carbon — from the server itself. A tree with both is + refused (they cannot run together); one with neither is refused with what to install. +3. **Writes the plugin's config** (`oxide/config/RunicGateway.json` or + `carbon/configs/RunicGateway.json`) holding just `ServerId` and `Port`, **only if there is none**. + The plugin fills in every other key on its first load. An existing config is never rewritten, + and one that names a different server stops the run and says so — the website locks a server's + id once it has seen it. +4. **Installs the sidecar**, writes its config with this server's ports, and registers its service. +5. **Places the plugin** in `oxide/plugins/` or `carbon/plugins/`. Both frameworks load a plugin + the moment it lands, so on a running server the bridge comes up at once; on a stopped one, at + the next boot. The installer never starts or stops your server. +6. **Prints what the website needs.** + +### More than one server on a host + +Run it once per server, each with its own `--rust` and `--server-id`. Each gets its own service, +config, database, game port (from 7799) and web port (from 8090). They share one sidecar binary, so +`update` moves them all together. + +### Where everything lands + +| | Linux | Windows | +|---|---|---| +| The sidecar binary (shared) | `/usr/bin/runicgateway-rust-link` | `%ProgramFiles%\RunicGateway\rust-link-sidecar.exe` | +| One server's sidecar config — **holds its token** | `/etc/runicgateway/rust/.toml` (mode 600) | `%ProgramData%\RunicGateway\rust\\sidecar.toml` | +| One server's database | `/var/lib/runicgateway/rust//rust-link.db` | beside its config | +| One server's service | `runicgateway-rust@.service` | `RunicGatewayRust-`, as `NT SERVICE\RunicGatewayRust-` | +| The record | `/etc/runicgateway/rust/install.json` | `%ProgramData%\RunicGateway\rust\install.json` | + +The Rust record is separate from ServUO's, so a host running both games keeps two records that +cannot interfere. The token is in the sidecar config and nowhere else — not in the record. + +--- + +## B. The Pterodactyl egg + +Each Rust-Link release carries **`egg-rust-runicgateway.json`**: egg 18 "Rust Autowipe" with the +bridge added. A panel administrator imports it (**Admin → Nests → Import Egg**); the panel has no +API for writing an egg. + +When you create a server from it: + +| Variable | | +|---|---| +| **Modding Framework** (`FRAMEWORK`) | `oxide` or `carbon`. `vanilla` installs no bridge and the server boots exactly as egg 18's | +| **Runic Gateway: server id** (`RUSTLINK_SERVER_ID`) | The id on the website. Read **once**, when the plugin writes its first config; after that the file holds it and the site locks it, so changing the variable later changes nothing | +| **Runic Gateway: sidecar port** (`RUSTLINK_WEB_PORT`) | **One of this server's allocations.** The panel does not tell a server which ports it holds, so a port that is not allocated binds but is never reachable | +| **Runic Gateway: sidecar token** (`RUSTLINK_WEB_TOKEN`) | Leave blank: the sidecar generates one. Anyone who can see this server's startup variables can read a token typed here | +| **Runic Gateway: bundle** (`RUNICGATEWAY_BUNDLE`) | Blank takes the current bundle. A tag pins one | +| **Runic Gateway: history days** (`RUSTLINK_RETAIN_DAYS`) | Blank keeps the default | + +**The install** downloads the sidecar, its launcher and the plugin from the bundle, checks each +against the bundle's checksum, and only then places them: the sidecar in `rust-link/`, the plugin in +`oxide/plugins/` or `carbon/plugins/`. Any mismatch fails the install with the reason, before +anything is placed. + +**The first boot** prints, in the console, the token the sidecar generated — **once** — and a line +with what the website needs: + +``` +[rust-link] A new sidecar token was generated. It is shown ONCE, here: +[rust-link] 4f9c… +[rust-link] It is kept in rust-link/sidecar.toml; read it there if you lose it. +[rust-link] Admin -> Rust -> Servers: server id 'main', sidecar URL http://:21009 (listening on 0.0.0.0:21009) +``` + +Every later boot prints only the second line. The sidecar runs inside the server's container +and stops when the server stops. + +**Updating is a reinstall.** The bridge is fetched at install and reinstall only, never on a +restart, so a restart can never change the protocol under a website that has not been updated. +Reinstall with **Regen Server** off to take the current bundle (or the pinned one) and keep the map. + +**A wipe keeps the bridge's history.** With Regen Server on, the install script deletes +`REMOVE_FILES` — and moves `rust-link/` out of the way while it does, so no list, wildcards +included, can reach the database or the token. After a wipe the website still has the server's +history, and the token is unchanged. + +--- + +## C. By hand + +1. **Pick the bundle** — the current one is + [`v2/rust/current.json`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/bundles/v2/rust/current.json) + on the installer's `bundles` branch. It names the sidecar binary for your platform and the + plugin tarball, each with a `sha256`. +2. **Download and verify** both against those checksums (`sha256sum -c`, or `Get-FileHash`). +3. **The plugin:** copy `runicgateway-rust-plugin/RunicGateway.cs` from the tarball into + `oxide/plugins/` or `carbon/plugins/`. To name the server, create its config first — + `oxide/config/RunicGateway.json` or `carbon/configs/RunicGateway.json` — holding + `{ "ServerId": "", "Port": 7799 }`. Without it, the plugin calls the server `main`. +4. **The sidecar:** put the binary somewhere stable and give it a config: + + ```toml + [game] + bind = '127.0.0.1:7799' # the plugin's Port + server_id = '' + + [web] + bind = '127.0.0.1:8090' + auth_token = "" # generated and saved on first start + + [store] + path = '/var/lib/rust-link//rust-link.db' + ``` + + `rust-link-sidecar --print-config --config ` generates the token, saves it, and prints it. +5. **Run it** under your supervisor of choice with `--config `, as an unprivileged user that + can write the database directory. On Windows the same `.exe` runs as a service + (`sc.exe create … binPath= "\"\" --config \"\""`); it speaks the service handshake + itself. + +Every environment variable the sidecar reads (`RUSTLINK_CONFIG`, `RUSTLINK_WEB_BIND`, +`RUSTLINK_WEB_TOKEN`, …) overrides the file; an empty one is ignored. + +--- + +## Connect the website + +On your site, open **Admin → Rust → Servers** and add the server: + +| Field | Value | +|---|---| +| Id | the server id — the installer's `--server-id`, or the egg's variable | +| Name | anything; players see it | +| Sidecar address | the sidecar URL the installer or the console printed | +| Sidecar token | the token | + +Test it from the same page: it should answer *plugin connected* with the protocol. + +### If the website is on another machine + +The installer's sidecar listens on `127.0.0.1`, reachable only from the game host. Put a TLS +reverse proxy (or a VPN link) in front of it and give the website the proxy's URL — the token is a +plain bearer token, and the sidecar speaks HTTP. The egg's sidecar listens on its allocation, +which is reachable from the network; firewall it to the website's address. **Leave the game bind +on loopback either way**: that socket takes commands for the game server and has no token, and +being loopback-only is what makes that safe. + +--- + +## Day two + +With the installer: + +| | | +|---|---| +| `doctor --game rust [--server-id ]` | Per server: the framework; whether the plugin file is still the one deployed; that the plugin's config names this server; the required uMod plugins; the service; and `/health` through to **plugin connected**. A stopped server is a warning; a running one whose plugin never connected is a failure, printed with the framework versions the plugin is known good on | +| `update --game rust` | Moves the sidecar and every server's plugin to the current bundle, and restarts the sidecars. Always all servers together — they share one binary | +| `uninstall --game rust [--server-id ] [--purge]` | Removes the service and the plugin file. **Keeps the plugin's config** — it is the website's, and it names the server. `--purge` also removes the sidecar config (the token) and the database. Removing the last server removes the shared binary too | + +With the egg: reinstall to update (above); the console is the diagnosis. + +## Troubleshooting + +| Symptom | Cause | +|---|---| +| The install stops: *already names this server "x"* | The plugin's config names another server. Use that id, or — if the server really is new to the site — remove the config file | +| The install stops: *has both Oxide and Carbon* | A tree mid-migration. Remove the framework you are leaving | +| The site says the sidecar is unreachable | The website cannot reach the URL: a loopback bind with the site elsewhere (see above), a firewall, or — on the egg — a sidecar port that is not one of the server's allocations | +| The site answers `409` | The site's Rust module speaks another protocol than the bridge. Update whichever is behind (and see the note in *Before you begin*) | +| `doctor`: *plugin connected — no, and the server is running* | The plugin did not compile or cannot reach its sidecar. The server console names a compile error; the plugin's `Port` must equal the sidecar's game port | +| `doctor`: *not the file that was deployed* | The plugin was edited by hand. `update --game rust` puts the released one back | +| The egg's console prints a new token after every boot | Only a sidecar older than the first Rust-Link release does this; reinstall the server | -- 2.49.1 From 7bbe9fa5eff95eb7887ef42bc2a665df351ffb1a Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 01:22:53 -0500 Subject: [PATCH 2/5] docs(rust-link): a sidecar refuses a plugin that names another server (D155) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- rust-link/INSTALL.md | 3 ++- rust-link/INTEGRATION.md | 9 +++++---- rust-link/PROTOCOL.md | 10 +++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/rust-link/INSTALL.md b/rust-link/INSTALL.md index 2669dcf..dbb6ce5 100644 --- a/rust-link/INSTALL.md +++ b/rust-link/INSTALL.md @@ -113,7 +113,7 @@ When you create a server from it: | Variable | | |---|---| | **Modding Framework** (`FRAMEWORK`) | `oxide` or `carbon`. `vanilla` installs no bridge and the server boots exactly as egg 18's | -| **Runic Gateway: server id** (`RUSTLINK_SERVER_ID`) | The id on the website. Read **once**, when the plugin writes its first config; after that the file holds it and the site locks it, so changing the variable later changes nothing | +| **Runic Gateway: server id** (`RUSTLINK_SERVER_ID`) | The id on the website. Read **once**, when the plugin writes its first config; after that the file holds it and the site locks it, so changing the variable later changes nothing — the launcher hands the sidecar the id from the plugin's config and says so in the console | | **Runic Gateway: sidecar port** (`RUSTLINK_WEB_PORT`) | **One of this server's allocations.** The panel does not tell a server which ports it holds, so a port that is not allocated binds but is never reachable | | **Runic Gateway: sidecar token** (`RUSTLINK_WEB_TOKEN`) | Leave blank: the sidecar generates one. Anyone who can see this server's startup variables can read a token typed here | | **Runic Gateway: bundle** (`RUNICGATEWAY_BUNDLE`) | Blank takes the current bundle. A tag pins one | @@ -230,5 +230,6 @@ With the egg: reinstall to update (above); the console is the diagnosis. | The site says the sidecar is unreachable | The website cannot reach the URL: a loopback bind with the site elsewhere (see above), a firewall, or — on the egg — a sidecar port that is not one of the server's allocations | | The site answers `409` | The site's Rust module speaks another protocol than the bridge. Update whichever is behind (and see the note in *Before you begin*) | | `doctor`: *plugin connected — no, and the server is running* | The plugin did not compile or cannot reach its sidecar. The server console names a compile error; the plugin's `Port` must equal the sidecar's game port | +| The sidecar logs *refusing a plugin that names another server* | Two servers dial one sidecar: a plugin's `Port` is another server's game port. Correct that plugin's `Port` (in its config) and reload it. Nothing it sent was stored | | `doctor`: *not the file that was deployed* | The plugin was edited by hand. `update --game rust` puts the released one back | | The egg's console prints a new token after every boot | Only a sidecar older than the first Rust-Link release does this; reinstall the server | diff --git a/rust-link/INTEGRATION.md b/rust-link/INTEGRATION.md index 82c84fb..9511c6f 100644 --- a/rust-link/INTEGRATION.md +++ b/rust-link/INTEGRATION.md @@ -197,10 +197,11 @@ Each pair is fully independent: its own ports, its own `sidecar.toml`, its own d own token, its own row on the website. Set `[game].server_id` in each `sidecar.toml` to match that server's plugin config. It is a -**cross-check**, not a second source of truth — the plugin's announcement wins — and it exists to -catch exactly one mistake: two game servers pointed at one sidecar by a copied config, which is -silent in every other design and produces one server's history under another's name. When it fires -you get a warning naming both ids. +**cross-check**, not a second source of truth, and it exists to catch exactly one mistake: two game +servers dialling one sidecar, which is silent in every other design and produces one server's +history under another's name. When it fires, the sidecar refuses the plugin that names the other +server, before anything it sent is stored, and logs an `ERROR` naming both ids; that plugin retries +every few seconds until its `Port` is corrected. The installer and the egg set it for you. --- diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 0c552c2..0c4a8ff 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -324,9 +324,13 @@ Two things about those are load-bearing: working directory. A service manager's working directory must not decide where the database lands — on Windows that can be `%SystemRoot%\System32`, or a silently redirected VirtualStore copy. - **`[game].server_id` is a cross-check, not a second source of truth.** The plugin announces its own - `serverId` and that is the authority; when both are set and they disagree, the sidecar logs the - disagreement loudly and keeps the plugin's. Two game servers pointed at one sidecar by a copied - config is the mistake this catches, and it is silent in every other design. + `serverId` and that is the authority for what a server is called. When `server_id` is set, the + sidecar **refuses a plugin that names another server**: it closes the connection on the first + frame that disagrees, before that frame is filed, and logs both ids at `ERROR` (D155). Until a + frame has named this server, no command is sent to the peer and `/health` does not report a + plugin connected. Blank, nothing is checked. Two game servers dialling one sidecar is the mistake + this catches. It was a warning that kept the plugin's id until phase 18, when a walk showed the + cost: one server's history filed under another's name, on the website (PLAN.md §34.5). `rust-link-sidecar --print-config` resolves the configuration exactly as a normal start would — writing the file and generating the token if they are missing — and prints it as JSON on stdout, -- 2.49.1 From 42419c1bfa0e9c5623749bb56365318d36c8879e Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 04:14:13 -0500 Subject: [PATCH 3/5] =?UTF-8?q?docs(rust):=20PLAN=20=C2=A734.5=20=E2=80=94?= =?UTF-8?q?=20as=20built,=20D154-D157,=20and=20what=20the=20walk=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN.md | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 6f9d7a2..f9a4998 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -6876,6 +6876,80 @@ The two existing rigs keep their hand-set startup until the egg is proven, and t - **The launcher ships in Rust-Link's release**, not inside the egg. - **The first release of each bridge repository takes whatever version the release engine derives from no previous tag.** The version is not the protocol. + +### 34.5 As built, and what the walk found (2026-09-26) + +**What shipped.** Rust-Plugins and Rust-Link cut over to `main` and released for the first time +(both v0.1.0, protocol 12). Their `edge` branches were deleted at the cutover, so both now take +work straight into `main`, the way `link` and `servuo-plugins` do. The fixes below took them to +Rust-Plugins **v0.1.1** and Rust-Link **v0.1.2**, and the compose job published bundles +`2026.09.26` through `2026.09.26.4` on its own after each release. The installer went out through +its own cutover (installer#32). The egg is named **`runicgateway-rust-autowipe`**, at the org +lead's request, in `egg.json` itself, so it imports under that name every time. + +**The walk rig.** Steps 2, 3 and 5 ran in a privileged systemd container (`jrei/systemd-debian:12`) +holding three server roots from one steamcmd download: `alpha` and `beta` on Oxide, `gamma` on a +clean Carbon. A second core on `:3270` stood in for the site. Step 4 ran on the development machine +under the SCM, from an elevated shell the org lead approved. + +**Four decisions the walk needed (continuing from D153):** + +- **D154: fold the plugin's port bug into phase 18**, rather than ship with it and fix it later. +- **D155: a sidecar refuses a plugin that names another server.** It closes the connection on + the first frame that disagrees with `[game].server_id`, before that frame is filed, and logs both + ids at `ERROR`. It sends no command to a peer until a frame has named this server. This replaces + the old warning, which kept the plugin's id (PROTOCOL.md §`server_id`). +- **D156: the installer's handoff says what the run did**, never that a link exists. +- **D157: fix the Windows start order for both games**, because the code is shared and the + released ServUO installer has the same bug. + +**What the walk found.** Six defects; none was visible to a test. + +1. **Every plugin dialled 7799, whatever its config said** (Rust-Plugins#15, D154). `Init` started + the link thread before `Loaded` read the config, so the first dial used the fallback port, on + both frameworks. With one server per host, 7799 was its own sidecar by coincidence, which is how + every rig since phase 1 passed. With two, beta's plugin sat in alpha's accept backlog. When + alpha's plugin reloaded, alpha's sidecar took beta's connection, and **the site showed server + `alpha` with beta's hostname and wipe**. The only trace was nine `WARN` lines. +2. **The sidecar let it happen** (Rust-Link#15, D155). The cross-check existed to catch exactly + this and only warned. The egg's launcher now hands the sidecar the id from the plugin's config + once that file exists. Otherwise a panel variable edited after the first boot would get the + plugin refused, where §34.2.6 promises the edit changes nothing. +3. **The plugin's reconnect backoff never held** (Rust-Plugins#15). It waited on the handle `Emit` + signals for every frame, so any hook ended the wait. That was invisible while a failed connect + was silent. Against a refusing sidecar it measured about 250 redials a second. It now waits on + `_stopping`, and resets only after a link has held for ten seconds. A refused plugin now retries + every 5 s, and a stable link whose sidecar restarts is back in half a second, as before. +4. **The handoff said "is connected to its sidecar"** (installer#29, D156) on a stopped server, + before anything had dialled. It said the same beside "no service was registered". +5. **`update` on a running server unloaded the bridge and never loaded the new one** + (installer#30). The plugin was written with the installer's atomic write, which removes the old + file and renames a temporary one over it. Oxide and Carbon treat the removal as a deletion and + ignore the rename. §34.2.3's "refusing a running server is not needed" holds only for a write + in place, which is what the installer now does, as an operator's `cp` does. The move to v0.1.1 + had silently left all three walk servers without a bridge until they were restarted. +6. **On Windows, `install` left the service stopped** (installer#31, D157). `sc start` ran before + the grant that lets the service's virtual account read `sidecar.toml`, which is locked to SYSTEM + and Administrators because it holds the token. The first start died on `Access is denied`, and + the SCM's restart policy does not fire for a clean exit with an error code. The released ServUO + installer (v0.2.0) has the same order, so a first-time ServUO install on Windows does the same. + +Minor findings: a hand-dispatched release run racing the push-triggered one fails with 409 after +the release exists, harmless but red (the workflows have no concurrency group). `uninstall` said +the service account was left alone because "this installer did not create it" when it had; it now +gives the true reason. + +**The walk (§34.3):** + +| Step | Result | +|---|---| +| 1. The pipeline | ✅ Both cutovers released; compose wrote `v2/rust/current.json`; schema-1 `current.json` untouched at `2026.09.15` | +| 2. Linux, Oxide, two instances | ✅ after D154/D155, from a cold boot on v0.1.1: distinct ports (7799/8090, 7800/8091), `doctor` clean, both on the site under their own names | +| 3. Carbon | ✅ after D154: plugin in `carbon/plugins/`, port 7801, connected | +| 4. Windows | ✅ after D157: running on install, `/health` at once, stop in 22 ms, restart, uninstall clean | +| 5. Day two | ✅ after installer#30: `update` no-op; `doctor` ✗ on a hand-edited plugin and ⚠ on a missing `ZoneManager.cs`; `update` puts the plugin back and it reloads; `uninstall --server-id beta` leaves alpha and gamma running | +| 6. The egg | *pending* | +| 7. The released installer | *pending* | --- [uo46]: https://gitea.whitlocktech.com/RunicGateway/Module-uo/issues/46 -- 2.49.1 From 85d52c69dc95528bfc53a976c71d35a9e6944a7c Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 04:47:49 -0500 Subject: [PATCH 4/5] =?UTF-8?q?docs(rust):=20=C2=A734.5=20=E2=80=94=20step?= =?UTF-8?q?s=206-7,=20the=20Carbon=20launcher,=20and=20the=20glibc=20floor?= =?UTF-8?q?=20(D158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index f9a4998..66ff02b 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -6892,7 +6892,7 @@ holding three server roots from one steamcmd download: `alpha` and `beta` on Oxi clean Carbon. A second core on `:3270` stood in for the site. Step 4 ran on the development machine under the SCM, from an elevated shell the org lead approved. -**Four decisions the walk needed (continuing from D153):** +**Five decisions the walk needed (continuing from D153):** - **D154: fold the plugin's port bug into phase 18**, rather than ship with it and fix it later. - **D155: a sidecar refuses a plugin that names another server.** It closes the connection on @@ -6902,8 +6902,9 @@ under the SCM, from an elevated shell the org lead approved. - **D156: the installer's handoff says what the run did**, never that a link exists. - **D157: fix the Windows start order for both games**, because the code is shared and the released ServUO installer has the same bug. +- **D158: the installer's Linux builds are static musl**, so it runs on the hosts operators have. -**What the walk found.** Six defects; none was visible to a test. +**What the walk found.** Eight defects; none was visible to a test. 1. **Every plugin dialled 7799, whatever its config said** (Rust-Plugins#15, D154). `Init` started the link thread before `Loaded` read the config, so the first dial used the fallback port, on @@ -6934,6 +6935,20 @@ under the SCM, from an elevated shell the org lead approved. the SCM's restart policy does not fire for a clean exit with an error code. The released ServUO installer (v0.2.0) has the same order, so a first-time ServUO install on Windows does the same. +7. **On Carbon, the egg's launcher lost its own output** (Rust-Link#17). Carbon's entrypoint puts + `LD_PRELOAD=libdoorstop.so` in front of the whole startup, and with Carbon's `DOORSTOP_*` + environment that preloader breaks the launcher shell's `$(…)`. The sidecar's config JSON, token + included, went to the console unframed, and the Admin line read `server id 'main', sidecar URL + http://192.168.0.12:`. Dropping the preload for the sidecar alone was not enough, because the + capturing shell had it too. The launcher now re-executes itself once without it and hands it back + to the game only. The Oxide server from the same egg was correct throughout. + +8. **The released Linux installer needed glibc 2.39** (installer#33, D158), so it did not start + on Debian 12 (2.36) or Ubuntu 22.04 (2.35). v0.2.0 was the same, so this predates the phase. + Both Linux builds are now static musl, as the sidecar has been since D149, built with + `cargo zigbuild` because `ring` needs a musl C compiler for each target. The release refuses a + Linux binary that is not statically linked. + Minor findings: a hand-dispatched release run racing the push-triggered one fails with 409 after the release exists, harmless but red (the workflows have no concurrency group). `uninstall` said the service account was left alone because "this installer did not create it" when it had; it now @@ -6948,8 +6963,8 @@ gives the true reason. | 3. Carbon | ✅ after D154: plugin in `carbon/plugins/`, port 7801, connected | | 4. Windows | ✅ after D157: running on install, `/health` at once, stop in 22 ms, restart, uninstall clean | | 5. Day two | ✅ after installer#30: `update` no-op; `doctor` ✗ on a hand-edited plugin and ⚠ on a missing `ZoneManager.cs`; `update` puts the plugin back and it reloads; `uninstall --server-id beta` leaves alpha and gamma running | -| 6. The egg | *pending* | -| 7. The released installer | *pending* | +| 6. The egg | ✅ on Oxide; Carbon after Rust-Link#17 (below). Imported as egg 25; `egg-oxide` printed its token once, framed, and its URL; both servers on the site with the plugin connected; a `REGEN_SERVER=1` reinstall kept `rust-link/` whole (same token), made a new wipe, and the site lists both wipes | +| 7. The released installer | ✅ on Debian 13: installer v0.3.0 downloaded, checksum-verified, installed `alpha`, which connected with `doctor` clean. It does not start on Debian 12 (below) | --- [uo46]: https://gitea.whitlocktech.com/RunicGateway/Module-uo/issues/46 -- 2.49.1 From 7505ee9ba7396c6181a15f836c22da665e8d0851 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 10:10:35 -0500 Subject: [PATCH 5/5] =?UTF-8?q?docs(rust):=20=C2=A734.5=20=E2=80=94=20step?= =?UTF-8?q?=206=20on=20Carbon=20and=20step=207=20on=20Debian=2012,=20both?= =?UTF-8?q?=20on=20releases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust-Link v0.1.3 (bundle 2026.09.26.5) walked on the egg-carbon server by a REGEN reinstall; installer v0.3.1 walked on Debian 12. Records installer#34. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 66ff02b..1a89c18 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -6882,9 +6882,9 @@ The two existing rigs keep their hand-set startup until the egg is proven, and t **What shipped.** Rust-Plugins and Rust-Link cut over to `main` and released for the first time (both v0.1.0, protocol 12). Their `edge` branches were deleted at the cutover, so both now take work straight into `main`, the way `link` and `servuo-plugins` do. The fixes below took them to -Rust-Plugins **v0.1.1** and Rust-Link **v0.1.2**, and the compose job published bundles -`2026.09.26` through `2026.09.26.4` on its own after each release. The installer went out through -its own cutover (installer#32). The egg is named **`runicgateway-rust-autowipe`**, at the org +Rust-Plugins **v0.1.1** and Rust-Link **v0.1.3**, and the compose job published bundles +`2026.09.26` through `2026.09.26.5` on its own after each release. The installer went out through +its own cutover (installer#32) as v0.3.0, and then as **v0.3.1** with the static build (D158). The egg is named **`runicgateway-rust-autowipe`**, at the org lead's request, in `egg.json` itself, so it imports under that name every time. **The walk rig.** Steps 2, 3 and 5 ran in a privileged systemd container (`jrei/systemd-debian:12`) @@ -6952,7 +6952,8 @@ under the SCM, from an elevated shell the org lead approved. Minor findings: a hand-dispatched release run racing the push-triggered one fails with 409 after the release exists, harmless but red (the workflows have no concurrency group). `uninstall` said the service account was left alone because "this installer did not create it" when it had; it now -gives the true reason. +gives the true reason. The running-server handoff line carried a run of spaces from a wrapped source +line, `confirms it connected.` (installer#34). **The walk (§34.3):** @@ -6963,8 +6964,9 @@ gives the true reason. | 3. Carbon | ✅ after D154: plugin in `carbon/plugins/`, port 7801, connected | | 4. Windows | ✅ after D157: running on install, `/health` at once, stop in 22 ms, restart, uninstall clean | | 5. Day two | ✅ after installer#30: `update` no-op; `doctor` ✗ on a hand-edited plugin and ⚠ on a missing `ZoneManager.cs`; `update` puts the plugin back and it reloads; `uninstall --server-id beta` leaves alpha and gamma running | -| 6. The egg | ✅ on Oxide; Carbon after Rust-Link#17 (below). Imported as egg 25; `egg-oxide` printed its token once, framed, and its URL; both servers on the site with the plugin connected; a `REGEN_SERVER=1` reinstall kept `rust-link/` whole (same token), made a new wipe, and the site lists both wipes | -| 7. The released installer | ✅ on Debian 13: installer v0.3.0 downloaded, checksum-verified, installed `alpha`, which connected with `doctor` clean. It does not start on Debian 12 (below) | +| 6. The egg | ✅ both frameworks. Imported as egg 25. `egg-oxide` printed its token once, framed, and its URL; both servers on the site with the plugin connected. A `REGEN_SERVER=1` reinstall kept `rust-link/` whole (same token), made a new wipe, and the site lists both wipes. On Carbon that held only after Rust-Link#17: a reinstall fetched v0.1.3 from bundle `2026.09.26.5`, the console printed `server id 'egg-carbon', sidecar URL http://192.168.0.12:21019` with no config dump, the plugin connected, the regen made wipe `w-20260926T150815Z`, and the site reached the sidecar with the token it already held | +| 7. The released installer | ✅ on Debian 12 (glibc 2.36) after D158. v0.3.1 was downloaded and checksum-verified, then installed `beta` into the same container `alpha` had left. `doctor` is clean for `beta` and `gamma`, and the site shows `beta` online under its own name. Installing at a newer bundle replaced the shared sidecar binary and restarted `gamma` on it, which is what §34.4 says `update` does. v0.3.0 had passed on Debian 13 and does not start on 12 | + --- [uo46]: https://gitea.whitlocktech.com/RunicGateway/Module-uo/issues/46 -- 2.49.1