From 5d7633c051280baed243c1dd967bc583397fe1f2 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 25 Sep 2026 23:42:34 -0500 Subject: [PATCH] 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 |