From 10ebce706f01472aeaa26b051b06c114e7cb1a4e Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 4 Aug 2026 11:17:10 -0500 Subject: [PATCH 1/3] =?UTF-8?q?docs(installer):=20record=20Phase=200.3=20?= =?UTF-8?q?=E2=80=94=20the=20bundle=20CI=20and=20where=20bundles=20live?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation half of installer Phase 0.3. Code PRs: RunicGateway/installer#3, RunicGateway/link#25, RunicGateway/servuo-plugins#9. Phase 0 item 3 asked for a compose job, a nightly cron, and a dispatch step in each component. All three landed, and building them settled questions §7 had left open — so those sections are corrected rather than appended to. Status: Phase 0 is now all but complete. 0.2 is merged and released as link v1.1.0; 0.3 is in review; 0.4 (INSTALL.md) is the remaining item, and the shape it was waiting on has settled. Phase 0 item 3 gains an "As built" subsection matching items 1 and 2, recording what was chosen rather than inherited: why gate 1 reads the sidecar's protocol from source at the release tag instead of asking the binary via --print-config (it only answers for v1.1.0+, and --bundle must be able to recompose an older pair); why gate 2 records the hash CI computed itself and separately checks for assets absent from SHA256SUMS; why release metadata is read anonymously; why an unrecognized asset name is a hard failure; and why a run that changes nothing writes nothing. §7.1 is corrected in two places. The manifest shape now shows link.assets as a map keyed by platform — the single sha256 this section sketched could only ever have described one of the two binaries link publishes — plus schema/generated and the servuo block. And it gains a subsection naming where bundles are published: committed under bundles/ in the installer repo, NOT one Gitea release per bundle, because that repo's own releases are the installer binaries and /releases/latest returns whichever release is newest regardless of kind. §7.2 records that the dispatch steps now exist, and that a failed dispatch is a warning rather than a failed release — which is what keeps the installer repo's token out of the components' hard requirements. §7.3 records that the releasable-commit rule must also exclude merge commits, whose subject quotes the real one: without that, merging a docs: branch whose title mentions a fix: would re-dispatch a declining release workflow nightly. Co-Authored-By: Claude --- installer/PLAN.md | 114 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 10 deletions(-) diff --git a/installer/PLAN.md b/installer/PLAN.md index fd10c38..76f005f 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -1,6 +1,9 @@ # Runic Gateway Installer — plan -Status: **Phase 0 in progress.** No installer code exists yet. This document is the design of record; +Status: **Phase 0 all but complete** — every prerequisite in another repo has landed, and the +installer repo now publishes the bundle manifest, so *what* the installer will install is already +released and composed ahead of the binary that installs it. No installer code exists yet; `0.4` +(`INSTALL.md`) is the remaining item, then Phase 1. This document is the design of record; it supersedes the informal overview it grew out of, which described a ServUO integration that does not match how `servuo-plugins` actually ships (see [Corrections](#corrections-to-the-original-overview)). @@ -8,9 +11,9 @@ not match how `servuo-plugins` actually ships (see | Phase 0 item | State | |---|---| | 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) | -| 0.2 `link` installable (data paths + `--print-config`) | 🟨 In review — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) | -| 0.3 Bundle CI in the installer repo | ⬜ Not started — **next**; both components it composes now exist | -| 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` waits on the shape settling | +| 0.2 `link` installable (data paths + `--print-config`) | ✅ Merged — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) (docs half [docs#84](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/84)); released as [`v1.1.0`](https://gitea.whitlocktech.com/RunicGateway/link/releases/tag/v1.1.0) | +| 0.3 Bundle CI in the installer repo | 🟨 In review — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: `2026.08.04` | +| 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` is **next** — the shape has now settled | | — Repo bootstrap (governance + CI) | ✅ [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) | --- @@ -298,6 +301,39 @@ Repo work that must land before an installer can exist. two gates → publish `bundle.json`), the nightly cron, and the dispatch step appended to each component's release workflow. This must exist before Phase 1 is useful, since the installer resolves what to install *from* the bundle. + + As built ([installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), + [link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), + [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)) — + `installer/.gitea/workflows/bundle.yml`, with the decisions §7 had left open: + + - **Bundles are committed to the installer repo, not published as releases** — see §7.1 for + where and why. That was the one genuinely open question here, and the deciding factor is that + this repo's *own* releases are the installer binaries. + - **Gate 1 reads the sidecar's protocol from source at the release tag**, not from the binary. + `--print-config` (Phase 0.2) would answer authoritatively, but only for releases from `v1.1.0` + onward, and `--bundle ` has to be able to recompose a bundle from an older pair. Reading + `sidecar/src/main.rs` at the tag the release was built from works uniformly, needs no execution + of a downloaded artifact, and does not provision a throwaway config whose auth token would then + be sitting in a CI log. A constant that has moved or been renamed is a hard failure — treating + "could not read" as "matches" is exactly how a mismatched pair would ship. + - **Gate 2 records the hash CI computed itself**, after verifying the download against the + publishing repo's `SHA256SUMS`. It also asserts the reverse direction — an asset with *no* + `SHA256SUMS` entry — because `sha256sum -c` silently passes over a file the sums file does not + mention, which would put an unverified artifact in the bundle. + - **Release metadata is read anonymously**, on purpose: those are exactly the requests the + shipped installer makes on a host with no Gitea credentials, so a repo flipped to private + fails CI here instead of on an operator's machine. + - **An unrecognized asset name is a hard failure.** link's binaries are mapped onto platform keys + by suffix; adding a target (aarch64, macOS) to its release workflow therefore reddens this job + rather than silently omitting the new binary from every bundle. + - **A run that changes nothing writes nothing** — the comparison excludes `bundle` and + `generated`, which are metadata about the run. Without that the nightly cron would commit a + dated duplicate of the same matrix every morning. + + The workflow's compose steps were run against the live releases before merge, producing the + first bundle (`2026.08.04`: link `v1.1.0` + overlay `v0.1.1`, protocol 3), which is committed so + the manifest exists ahead of the binary that reads it. 4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the shape is settled. @@ -471,15 +507,33 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi ```json { - "bundle": "2026.08.01", + "schema": 1, + "bundle": "2026.08.04", + "generated": "2026-08-04T16:07:13Z", "protocol": 3, - "link": { "version": "0.3.0", "sha256": "a91f..." }, - "overlay": { "version": "0.1.0", "commit": "968b526", "sha256": "7c3e..." } + "link": { + "repo": "RunicGateway/link", "tag": "v1.1.0", "version": "1.1.0", "protocol": 3, + "assets": { + "linux-x86_64": { "name": "uo-link-sidecar-linux-x86_64", "url": "…", "sha256": "27d491ef…" }, + "windows-x86_64": { "name": "uo-link-sidecar-windows-x86_64.exe", "url": "…", "sha256": "fbefd886…" } + } + }, + "overlay": { + "repo": "RunicGateway/servuo-plugins", "tag": "v0.1.1", "version": "0.1.1", + "commit": "3a52abb…", "protocol": 3, + "servuo": { "min_version": "57.4", "patches_verified_against": "57.4" }, + "asset": { "name": "runicgateway-overlay-0.1.1.tar.gz", "url": "…", "sha256": "75dc6d6c…" } + } } ``` +Note `link.assets` is a **map keyed by platform**, not the single `sha256` this section originally +sketched: link publishes a Linux binary and a Windows `.exe`, and the installer runs on both, so one +hash could only ever have described one of them. `schema` versions this document's shape and is +independent of `protocol` and of either component's release version — all three move separately. + The installer fetches the current bundle at run time; `--bundle ` pins an older one for a -reproducible install. Because the bundle is data, **a new `link` release regenerates ~20 lines of +reproducible install. Because the bundle is data, **a new `link` release regenerates ~30 lines of JSON and leaves the installer binary untouched** — operators do not re-download the installer to pick up a sidecar patch, and the installer does not accumulate releases whose code is byte-identical. @@ -487,19 +541,53 @@ Two gates run at compose time, both cheap and both worth it: 1. The sidecar's `PROTOCOL_VERSION` must equal the overlay manifest's declared protocol version. This is the check that catches an `edge`/`main` protocol mismatch before it reaches an operator. + The two halves are read from different places because they *are* different: the overlay's from + `manifest.json` inside the tarball (the only statement of it that exists — §7.0), the sidecar's + from `sidecar/src/main.rs` at the release tag (see Phase 0 item 3 for why not from the binary). 2. Every referenced asset must exist and its SHA256 must match the publishing repo's `SHA256SUMS`. + The hash recorded in the bundle is the one CI computed from the asset it downloaded, *after* that + check — and the installer verifies every download against it. These artifacts are deliberately + unsigned (§3), so the checksum is the whole trust anchor; a hash copied from a file nobody + verified would make the chain decorative. + +#### Where bundles are published + +Committed to the installer repo under `bundles/`, so the installer's fetch is a plain anonymous +`GET` against a public repo — the shard host has no Gitea credentials (§1): + +``` +bundles/current.json → …/RunicGateway/installer/raw/branch/main/bundles/current.json +bundles/bundle-.json → …/raw/branch/main/bundles/bundle-2026.08.04.json (--bundle) +``` + +Every bundle is kept forever, so `--bundle` stays reproducible. Tags are UTC dates; a second bundle +on the same day — a sidecar release in the morning and an overlay release in the afternoon is the +normal way that happens — becomes `2026.08.04.2`, so one tag always names exactly one matrix. + +**Not one Gitea release per bundle**, which was the obvious alternative. This repo's own releases +are the installer *binaries*, and `/releases/latest` returns whichever release is newest regardless +of kind — interleaving bundle releases would make "latest" intermittently resolve to a release +carrying no installer binary. Committing also yields a reviewable diff and a git history of the +compat matrix, and needs no new branch-protection exception: `release.yml`'s version-bump commit +already requires the CI user to be able to push to `main`. ### 7.2 What triggers a bundle | Trigger | Why | |---|---| -| `link` publishes a release | Its release job `POST`s to the installer repo's workflow-dispatch endpoint as its final step. `link/.gitea/workflows/release.yml` already declares `workflow_dispatch: {}` and already holds a `write:repository` token | -| `servuo-plugins` publishes a release | Same. Phase 0 item 1 gave it the release workflow; the dispatch step is marked as a TODO in that workflow's header and lands with the bundle CI it would call (item 3) — a step that `404`s on every release is worse than no step | +| `link` publishes a release | Its release job `POST`s to the installer repo's workflow-dispatch endpoint as its final step | +| `servuo-plugins` publishes a release | Same. Phase 0 item 1 gave it the release workflow; the dispatch step was left as a marked TODO until there was something to dispatch, and landed with the bundle CI it calls (item 3) — a step that `404`s on every release is worse than no step | | Nightly cron on the installer repo | Recomputes from whatever the latest releases actually are, so a missed or failed dispatch self-heals instead of silently pinning operators to a stale sidecar | `repository_dispatch` is deliberately avoided — support for it is uncertain on this Gitea version, whereas dispatching an existing `workflow_dispatch` workflow via the API works today. +**A failed dispatch is a warning, never a failed release.** By the time that step runs the component +release is published and correct; failing the job would misreport it. This also keeps the dispatch +from becoming a new hard credential requirement — `REGISTRY_TOKEN` having write on the installer +repo is a nicety, and without it the nightly cron picks the release up anyway. A dropped dispatch +costs latency, not correctness, which is the whole reason the cron exists. + ### 7.3 Stale-overlay handling: dispatch, don't wait Each component **self-releases on merge to its own `main`**, using the same conventional-commit @@ -507,6 +595,12 @@ engine. Note that "updated since the last release" must mean *releasable* commit `RELEASE=false` when nothing but `docs:`/`chore:` has landed, so a docs typo correctly does **not** cut an overlay release, and the bundle keeps using the existing one. +The compose job's copy of that rule additionally **excludes merge commits**, whose subject is +`Merge pull request ''`. Without that, every squash-free merge of a `feat:` branch +would be counted twice, and worse, a merge of a `docs:` branch whose *title* happens to quote a +`fix:` would be read as releasable — re-dispatching, every night, a release workflow that correctly +declines to run. + So by the time the installer's CI looks, the release normally already exists. If it finds `servuo-plugins` main ahead of its latest release *with* releasable commits, it: From 5c8585fe75986ae4e7cfdee75e6b12e4b8832a88 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 4 Aug 2026 11:39:02 -0500 Subject: [PATCH 2/3] docs(installer): add the operator install guide (Phase 0.4) Closes the last Phase 0 item. INSTALL.md is written before the installer binary on purpose: everything it installs is already released (0.1-0.3), so the guide is not speculation about a tool that might exist - it is the specification of what the run asks, where it writes, what it prints, and what the operator does next. It is useful today. Appendix A is the same deployment done by hand - bundle fetch, tarball verify and overlay copy, the optional patch tier, --print-config provisioning, systemd unit / sc create - composed from the released artifacts' actual contents and the sidecar's config and CLI source. That appendix doubles as Phase 1's acceptance test. Writing it settled four things the plan had left implicit, now recorded in PLAN.md: - The installer does not install itself; day-two commands run from the downloaded binary. - The flag surface: --servuo, --patches/--no-patches, --host, --site-url and --yes join the --verify/--bundle/--purge the plan already named, so every prompt has a non-interactive equivalent. - A modified Config/Bridge.cfg is reported, not overwritten - one deliberate deviation from deploy.ps1, whose overwrite-on-hash-differs rule is right for a developer and would silently revert an operator's whole shard config on update. install.json's recorded hashes are what make the distinction possible. - Remote-website deployments: widen [web] bind, firewall it to the site's address, front it with TLS or a VPN off a trusted network. [shard] bind stays on loopback because that socket carries commands into the game. Also adds the missing installer/ section to the docs index, and refreshes two stale examples in PLAN.md (overlay file count, sidecar version). Co-Authored-By: Claude --- README.md | 15 +- installer/INSTALL.md | 692 +++++++++++++++++++++++++++++++++++++++++++ installer/PLAN.md | 63 +++- 3 files changed, 753 insertions(+), 17 deletions(-) create mode 100644 installer/INSTALL.md diff --git a/README.md b/README.md index 679624b..6df81ff 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ so they live in one place, independent of either codebase. ## Layout ``` -website/ docs from the shard website (Node/Express + MariaDB + React/Vite) -link/ docs from the ServUO bridge (C# plugin + Rust sidecar + Node WS) -android/ docs from the native Android client (Kotlin + Jetpack Compose) -ci/ cross-cutting CI/quality notes +website/ docs from the shard website (Node/Express + MariaDB + React/Vite) +link/ docs from the ServUO bridge (C# plugin + Rust sidecar + Node WS) +android/ docs from the native Android client (Kotlin + Jetpack Compose) +installer/ docs for the installer that deploys a shard's bridge components +ci/ cross-cutting CI/quality notes ``` ### `website/` @@ -50,6 +51,12 @@ ci/ cross-cutting CI/quality notes | [TRUSTED_DEVICES_APP_HANDOFF.md](android/TRUSTED_DEVICES_APP_HANDOFF.md) | Trusted-devices app handoff notes | | [PROJECT_TREE.md](android/PROJECT_TREE.md) | Auto-generated snapshot of the repo's tracked file layout | +### `installer/` +| Doc | What it covers | +|---|---| +| [INSTALL.md](installer/INSTALL.md) | **Operator guide** — installing Runic Gateway on a ServUO shard, connecting it to the website, and diagnosing it. Includes the by-hand path, which works today | +| [PLAN.md](installer/PLAN.md) | Installer design of record — phases, locked decisions, the bundle/compat-matrix model | + ## Provenance - `website/*` was extracted from `RunicGateway/website` via `git filter-repo`. diff --git a/installer/INSTALL.md b/installer/INSTALL.md new file mode 100644 index 0000000..ef4bca6 --- /dev/null +++ b/installer/INSTALL.md @@ -0,0 +1,692 @@ +# Installing Runic Gateway on your shard + +Operator guide for the **Runic Gateway installer** — the tool that takes a working ServUO +installation and connects it to a Runic Gateway website. + +> **Status: the installer binary is not released yet.** +> +> Everything it installs *is* released and published — the sidecar, the plugin overlay, and the +> [bundle manifest](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) +> that names the checked combination of the two. This guide is the operator-facing contract those +> phases build to, and it is written first on purpose: it is the specification of what the run +> looks like, what it asks, where it writes, and what it prints. +> +> **You can install today without it** — [Appendix A](#appendix-a--installing-by-hand) is the same +> deployment done by hand, with the commands verified against the current releases. When the binary +> ships, Appendix A stays as the reference for what it does under the hood. +> +> Design of record: [PLAN.md](PLAN.md). + +--- + +## What this installs + +Three things, on the machine that runs your shard: + +| # | Component | Where it comes from | +|---|---|---| +| 1 | **The plugin overlay** — C# source that ServUO compiles at boot, copied into your server tree | [`RunicGateway/servuo-plugins`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) release tarball | +| 2 | **The uo-link sidecar** — a small Rust service that the shard dials out to, and that your website reads from | [`RunicGateway/link`](https://gitea.whitlocktech.com/RunicGateway/link) release binary | +| 3 | **A record of what it did** — `install.json`, plus a cached copy of any patches it applied | Written by the installer | + +``` + ServUO shard ──loopback TCP 127.0.0.1:7788──► uo-link sidecar ──HTTP + WebSocket──► website + (1) overlay (2) binary + service (yours) +``` + +The shard **dials out**; it never listens for the website and is never reachable from the internet. +Only the sidecar is exposed, and only to your website. + +### What it deliberately does not do + +- **It never restarts or manages ServUO.** Your shard keeps starting the way it always has. The + installer refuses to run while ServUO is up, and tells you when a restart is required. +- **It never deletes anything from your server tree.** The overlay sync only adds and overwrites. +- **It never contacts your website.** It prints four values for you to paste into Admin → Shard. +- **It never edits stock ServUO files without asking.** That is the opt-in + [patch tier](#4-the-patch-tier-optional), and skipping it still leaves you with a working bridge. + +--- + +## Before you begin + +| Requirement | Detail | +|---|---| +| A working ServUO install | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. | +| ServUO **57.4** *(patch tier only)* | The base install works on any reasonably current ServUO. The patch tier is verified against stock 57.4 only, and is skipped with a warning on anything else. | +| ServUO **stopped** | `ServUO.exe` holds a lock on `Scripts.dll` and writes `Saves/` on exit. The installer refuses to deploy under a running shard. | +| Administrator / root | It writes into system directories and registers a service. | +| Outbound HTTPS | To `gitea.whitlocktech.com`, to fetch the bundle and the two artifacts. Nothing inbound is needed, and no Gitea account or git client is required. | +| The sidecar on the **same host** as the shard | The shard connects to `127.0.0.1:7788`. Splitting them is not supported — the loopback socket *is* the trust boundary for inbound commands. | +| Admin access to your Runic Gateway site | The last step is pasting four values into Admin → Shard. | + +**Back up first.** The overlay overwrites `Scripts/Scripts.csproj` (a stock file), and the patch +tier edits stock sources. A copy of `Scripts/` and `Config/` before you start costs nothing. + +--- + +## 1. Download and verify + +Releases are **unsigned**. There is no code-signing certificate and no notarization, so the +`SHA256SUMS` file published beside every artifact is the whole trust anchor — check it. + +Download the installer for your OS, plus `SHA256SUMS`, from the +[installer releases page](https://gitea.whitlocktech.com/RunicGateway/installer/releases): + +``` +runicgateway-installer-linux-x86_64 +runicgateway-installer-windows-x86_64.exe +SHA256SUMS +``` + +**Linux** + +```bash +sha256sum -c SHA256SUMS --ignore-missing +chmod +x runicgateway-installer-linux-x86_64 +``` + +**Windows** (PowerShell) + +```powershell +(Get-FileHash .\runicgateway-installer-windows-x86_64.exe -Algorithm SHA256).Hash +Get-Content .\SHA256SUMS # compare the line for this file, case-insensitively +``` + +Windows will show a **SmartScreen "Windows protected your PC"** prompt on first run, because the +binary is unsigned and unknown. Once you have verified the checksum above: *More info* → +*Run anyway*. If you would rather not, Appendix A's manual path uses no unsigned binary except the +sidecar itself, which you verify the same way. + +The installer applies the same standard to everything **it** downloads: each artifact's SHA256 is +checked against the value recorded in the bundle manifest — which CI computed after verifying it +against the publishing repo's own `SHA256SUMS` — and a mismatch aborts the run. + +### What it installs is a bundle, not "latest" + +The three components version independently but must agree on one wire protocol, so CI publishes a +[**bundle**](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/README.md): +one exact, protocol-checked pair of sidecar + overlay versions. The installer resolves that at run +time rather than hardcoding versions or blindly taking each repo's newest release. + +Consequences worth knowing: + +- A sidecar patch release does **not** mean re-downloading the installer. The bundle is data. +- `--bundle ` (e.g. `--bundle 2026.08.04`) pins an exact past combination, so a reinstall six + months from now reproduces today's install rather than tomorrow's. + +--- + +## 2. Run it + +```bash +sudo ./runicgateway-installer-linux-x86_64 install +``` + +```powershell +# Windows: from an elevated PowerShell +.\runicgateway-installer-windows-x86_64.exe install +``` + +Run `install --verify` first if you want to see exactly what would change and write nothing — the +same idea as `deploy.ps1 -Verify`, which developers of the plugin use. + +The installer **does not install itself.** Keep the binary somewhere sensible on the host (it is +one file); `doctor`, `update` and `uninstall` are run from it later. Examples below shorten it to +`runicgateway`. + +### What it asks + +1. **Your ServUO root** — detected if the installer is run from inside it or from an obvious + sibling, otherwise prompted. A directory qualifies only if it contains `ServUO.exe`, `Scripts/` + and `Config/`. +2. **Whether to apply the patch tier** — off unless you say yes, and not offered at all if your + ServUO is not 57.4. See [§4](#4-the-patch-tier-optional). +3. **The hostname your website should use to reach this machine** — used only to compose the two + URLs it prints at the end. The sidecar's bind address is frequently `127.0.0.1` or `0.0.0.0`, + neither of which is something to hand to a website. +4. **Your site's URL** — used only to print a clickable link to its Admin → Shard page. The + installer never contacts your website. + +### An illustrative run + +``` +Runic Gateway installer — bundle 2026.08.04 (protocol 3) + + ServUO /opt/ServUO (57.4) + Shard process not running + Overlay servuo-plugins v0.1.1 protocol 3 + Sidecar uo-link v1.1.0 protocol 3 + +✓ overlay tarball verified sha256 75dc6d6c… +✓ sidecar binary verified sha256 27d491ef… + +Overlay sync + ADD Config/Bridge.cfg + ADD Scripts/Custom/Bridge/*.cs (22 files) + CHANGE Scripts/Scripts.csproj + deployed. add=23 change=1 unchanged=0 + +Patch tier skipped (not selected) + Without it: no vendor.sale events, no in-game moderation audit forwarding. + +uo-link + binary /usr/bin/runicgateway-link + config /etc/runicgateway/sidecar.toml (created) + database /var/lib/runicgateway/uo-link.db + service runicgateway-link.service enabled, running + +Recorded /etc/runicgateway/install.json + +Scripts.csproj changed — ServUO rebuilds Scripts.dll on next boot. +Start your shard when ready; the installer does not start it for you. +``` + +Then the [token handoff](#5-connect-the-website). + +### Commands and flags + +The surface this guide specifies. Each command is idempotent: a second run with nothing new to do +reports "unchanged" and writes nothing. + +| Command | What it does | +|---|---| +| `install` | The full run above. | +| `doctor` | Diagnoses an existing deployment end to end — see [§7](#7-day-two). | +| `update` | Re-resolves the bundle; updates the sidecar (replace + restart) and the overlay (re-sync + tell you to restart ServUO). | +| `uninstall` | Removes only what the installer exclusively owns; prints — never performs — anything inside your ServUO tree. | + +| Flag | Applies to | Meaning | +|---|---|---| +| `--verify` | `install`, `update` | Dry run. Report every change that would be made; write nothing. | +| `--servuo ` | `install`, `doctor`, `update` | Name the ServUO root instead of detecting or prompting. | +| `--bundle ` | `install`, `update` | Pin an exact published bundle instead of the current one. | +| `--patches` / `--no-patches` | `install` | Decide the patch tier non-interactively. `--patches` still refuses on a non-57.4 tree. | +| `--host ` | `install` | The hostname to print in the website URLs. | +| `--site-url ` | `install` | Your site's base URL, for the Admin → Shard link. | +| `--yes` | all | Assume the default answer to every prompt. Combine with the flags above for an unattended run. | +| `--purge` | `uninstall` | Also delete `sidecar.toml` and `uo-link.db`, which are otherwise kept. | + +--- + +## 3. Where everything lands + +**Linux** + +| Path | What | +|---|---| +| `/usr/bin/runicgateway-link` | The sidecar binary | +| `/etc/runicgateway/sidecar.toml` | Sidecar config, including the auth token | +| `/etc/runicgateway/install.json` | What the installer deployed: versions, commit, per-file hashes, applied patches, timestamps | +| `/etc/runicgateway/patches/` | Copies of any patches applied, so `uninstall` can print the exact hunks long after the release tarball is gone | +| `/var/lib/runicgateway/uo-link.db` | The sidecar's SQLite store (event history, cached profiles, link map) | +| `/etc/systemd/system/runicgateway-link.service` | The service unit, running as a dedicated user | + +**Windows** + +| Path | What | +|---|---| +| `%ProgramFiles%\RunicGateway\uo-link-sidecar.exe` | The sidecar binary | +| `%ProgramData%\RunicGateway\sidecar.toml` | Sidecar config, including the auth token | +| `%ProgramData%\RunicGateway\install.json` | As above | +| `%ProgramData%\RunicGateway\patches\` | As above | +| `%ProgramData%\RunicGateway\uo-link.db` | The sidecar's SQLite store | +| Service `RunicGatewayLink` | Automatic start, restart on failure | + +**Inside your ServUO tree** (added by the overlay sync — 24 files): + +``` +Config/Bridge.cfg every bridge setting, heavily commented +Scripts/Custom/Bridge/*.cs 22 files: the plugin itself +Scripts/Scripts.csproj OVERWRITES a stock file (see below) +``` + +Both service definitions pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly. The sidecar's own +defaults are relative to its working directory, and a service manager's working directory is not +somewhere you want a database — on Windows it can be `%SystemRoot%\System32` or, under +`C:\Program Files\`, a silently redirected VirtualStore copy. + +> **`Scripts.csproj` is overwritten deliberately.** The stock file omits `Scripts/Custom/`, so the +> plugin would sit in the tree and never compile — and ServUO would not tell you, because it +> ignores the script build's exit code and silently reloads the previous `Scripts.dll`. That +> failure mode is the reason [§6](#6-start-servuo-and-verify) exists. + +--- + +## 4. The patch tier (optional) + +Most of the plugin ships as **added** files, which is why the base install is a safe file copy. Two +features cannot: they need edits to stock ServUO sources, because the events they depend on do not +exist. + +| Patch | Edits | Gives you | Rebuild needed | +|---|---|---|---| +| `playervendor-sale-eventsink.patch` + `playervendor-sale-gump.patch` | `Server/EventSink.cs`, `Scripts/Gumps/PlayerVendorGumps.cs` | `vendor.sale` events — player-vendor purchases with buyer, owner, price and commission, which is what cheat detection needs | **Core solution rebuild** (`dotnet build ServUO.sln`) — the dynamic script build is not enough | +| `commandlogging-event.patch` | `Scripts/Commands/Logging.cs` | In-game moderation actions (`[ban`, `[kick`, `[bcast`) forwarded to the website's moderation log as `admin.audit` | Script build only — a shard restart is enough | + +Each patch has a companion `.cs` file that is copied **only after** its patch applies, because it +references symbols the patch introduces. That is why they are not in the base overlay: shipping them +unconditionally would break the build on every unpatched install. + +How the installer handles it: + +- **Opt-in.** The base install completes without it, and declining is a supported outcome, not a + degraded one. +- **Dry-run first, always.** Every patch is checked (`git apply --check`) before anything is + applied, and reported per patch. Most real shards are hand-modified; a patch that does not apply + is expected, not alarming. +- **All or nothing per feature.** The two vendor-sale patches are one unit and are applied together + or not at all. +- **Skipped entirely on a ServUO that is not 57.4**, with a warning. Unverified diffs are never + applied to an unknown tree. +- **Recorded, and the `.patch` files cached**, so re-runs stay idempotent and `uninstall` can print + the exact hunks to revert. + +If it is skipped or fails, you lose exactly two things — **`vendor.sale` events** and **in-game +moderation audit forwarding**. Everything else works. You can apply the patches later by hand (see +`patches/README.md` in the tarball) and re-run `install` to record it. + +--- + +## 5. Connect the website + +The installer ends a successful run by printing the one manual step it cannot do for you: + +``` +Runic Gateway is installed. + +One manual step remains — connect the website to this sidecar: + + Base URL http://shard.example.com:8080 + WebSocket URL ws://shard.example.com:8080/ws + Protocol version 3 + Auth token 4f9c… (also in /etc/runicgateway/sidecar.toml) + +Paste these into Admin → Shard on your Runic Gateway site: + https://your-site.example/admin/shard + +The token is write-only once saved — the site will never show it back to you. +``` + +Every value there comes from asking the installed sidecar itself (`--print-config`), not from a +log file or a guess, so it cannot drift from what the service actually runs. + +On your site, sign in as an administrator and open **Admin → Shard (uo-link)**: + +| Field on the page | Paste | +|---|---| +| Enable the shard integration | ✔ on | +| Base URL (REST) | the **Base URL** line | +| WebSocket URL (feed) | the **WebSocket URL** line | +| Auth token | the **Auth token** line | +| Protocol | the **Protocol version** line (`3`) | + +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 +from `sidecar.toml` on the shard host, not from the website. + +### If your website is on a different machine + +The sidecar binds `127.0.0.1:8080` by default, which is reachable only from the shard host. If your +website runs elsewhere, you must widen the bind — and then narrow the access: + +1. Set `[web] bind` in `sidecar.toml` to `0.0.0.0:8080` (or a specific LAN address) and restart the + service. +2. **Firewall port 8080 to your website's address only.** The auth token is always on, but it + travels as a plain bearer token — the sidecar speaks HTTP, not HTTPS. +3. If the two hosts are not on a trusted network, put the sidecar behind a TLS reverse proxy or a + VPN/WireGuard link, and give the website the proxied `https://` / `wss://` URLs. + +The `[shard] bind` line is a different matter: leave it on `127.0.0.1:7788`. That socket accepts +*inbound commands* to the game, and being loopback-only is what makes that safe. + +--- + +## 6. Start ServUO and verify + +Start your shard the way you always do. Then confirm the bridge is actually live — not merely +installed. **A successful file copy is not a working bridge**: ServUO shells out to `dotnet build`, +prints the output, ignores the exit code, and reloads the existing `Scripts.dll`, so a broken script +build looks exactly like a clean boot. + +**a. Watch the boot output.** You want to see the build succeed *and* the bridge announce itself: + +``` +Core: Compiling scripts... +Build succeeded. +[Bridge] enabled=True endpoint=127.0.0.1:7788 queueCap=10000 sweeps(stat=30s decay=60s … +``` + +If you scrolled past it, force the question: + +```bash +cd +dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64 # must be 0 errors +``` + +**b. Ask the shard, in game.** As an Administrator: + +``` +[bridge status +``` + +It reports the config plus `connected=True depth=0 sent=… dropped=0 …`. `connected=False` means the +shard cannot reach the sidecar; `dropped` climbing means the sidecar is wedged and the shard is +shedding events rather than stalling — which it is designed to do. `[bridge reload` re-reads +`Bridge.cfg` without a restart; `[bridge sweepnow` forces one pass of every stream. + +**c. Ask the sidecar.** `/health` needs no auth, so it is safe to curl from a terminal: + +```bash +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"} +``` + +`plugin_connected: true` is the one that matters — it is the only value in this whole guide that +distinguishes "files copied" from "the bridge works". + +**d. Ask the website.** The public site should stop showing the shard as offline, and live events +should appear on the admin dashboard within seconds. + +--- + +## 7. Day two + +### `runicgateway doctor` + +The command that makes this supportable. Run it before asking anyone for help — its output is the +first thing a maintainer will want. + +``` +✓ ServUO found /opt/ServUO (57.4) +✓ Overlay in sync 24 files, all hashes match install.json +⚠ Patch tier 1 of 3 applied — vendor.sale unavailable +✓ uo-link installed 1.1.0 +✓ Service running, enabled +✓ Sidecar reachable 127.0.0.1:8080 /health ok +✓ Protocol sidecar 3 = overlay manifest 3 +✗ Shard connected no shard has dialed in since boot +``` + +Three of those rows come from asking the installed sidecar (`--version`, `--print-config`) rather +than from reading `install.json`, so `doctor` reports what the binary would actually do — including +which config and database file the *service* resolves — rather than what the installer believes it +was told. The overlay row compares live file hashes against both `install.json` and the release +manifest, which is how it tells "you edited a deployed file" from "the overlay moved on". + +### `runicgateway update` + +Re-resolves the bundle and moves both halves to a combination whose protocol versions were checked +together — never to two independently-latest artifacts that may disagree. + +- **Sidecar**: download → verify → replace binary → restart service. No shard downtime. +- **Overlay**: download → verify → re-sync → record the new commit → **tell you to restart ServUO.** + It does not restart your shard. + +Your `sidecar.toml`, your `Bridge.cfg` edits and your database are not touched. `Bridge.cfg` is +overwritten only if you have not changed it; a modified copy is reported, not clobbered. + +### `runicgateway uninstall` + +Removes what it exclusively owns, and **prints** everything else. The installer cannot know what you +have changed in your own server tree since deployment, so an automatic revert risks silently eating +your work. + +| | | +|---|---| +| **Removed** | The sidecar binary, its service entry, `install.json`, the cached patch set | +| **Kept** | `sidecar.toml` and `uo-link.db` — config and history survive (`--purge` drops them) | +| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete | +| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert | + +The report is also written to a file, so it survives the scrollback. + +--- + +## Troubleshooting + +| Symptom | Cause and fix | +|---|---| +| **"ServUO is running — stop it before installing"** | Correct, and not overridable. `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit; deploying underneath it corrupts one or both. Stop the shard, install, start it again. | +| Shard boots clean but nothing reaches the site | The classic silent failure: ServUO ignores the script build's exit code and reloaded a **stale `Scripts.dll`**. Run `dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64` and read the errors it prints. | +| `[bridge status` says `connected=False` | The sidecar is not listening on `127.0.0.1:7788`. Check the service is running, and that `[shard] bind` in `sidecar.toml` matches `Host`/`Port` in `Bridge.cfg`. | +| `[bridge` is not a command | The plugin did not compile, or `Bridge.cfg` has the bridge disabled. See the row above. | +| Website says the shard is offline; `/health` is fine locally | The website cannot reach port 8080 — bind address or firewall. See [§5](#if-your-website-is-on-a-different-machine). Note that the site is *designed* to render normally with the shard offline, so this fails quietly by design. | +| Website logs `409` from the sidecar | Protocol mismatch: the number in Admin → Shard does not match the sidecar's. The sidecar rejects rather than mis-parsing. Set the field to what `/health` reports (`protocol`). If the *sidecar* and *overlay* disagree, you have a hand-assembled pair — reinstall from a bundle. | +| `401` from the sidecar | Wrong or missing auth token. Read the live one back with `uo-link-sidecar --print-config --config `; do not retype it from a screenshot. | +| A patch will not apply | Expected on a hand-modified shard. The base install is unaffected; you lose only the two features in [§4](#4-the-patch-tier-optional). Apply the hunks by hand if you want them. | +| `vendor.sale` events never arrive despite patching | The `EventSink.cs` patch is a **core** change. A shard restart is not enough — rebuild the solution (`dotnet build ServUO.sln`). | +| Sidecar writes its database somewhere unexpected | A relative `[store] path` resolves against the directory holding `sidecar.toml` — not the working directory. Run `--print-config` to see the absolute path it will actually use. | +| Token leaked into a log or a screenshot | Clear `[web] auth_token` in `sidecar.toml`, restart the service (a new token is generated and saved), read it back with `--print-config`, and re-save it in Admin → Shard. | + +--- + +## Appendix A — installing by hand + +This is what the installer automates. It works today, on the current releases, and is the fallback +whenever you would rather not run an unsigned binary. + +Throughout: `` is your ServUO root, and **the shard is stopped**. + +### A1. Fetch the bundle (so you install a checked pair) + +```bash +curl -s https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/current.json +``` + +It names the sidecar tag, the overlay tag, their agreed `protocol`, and the SHA256 of every asset. +Use those versions together; that pairing is the only thing CI has verified. + +### A2. Deploy the plugin overlay + +```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/v0.1.1/SHA256SUMS +sha256sum -c SHA256SUMS --ignore-missing # must say: OK + +tar xzf runicgateway-overlay-0.1.1.tar.gz # → runicgateway-overlay/ +cd runicgateway-overlay +cat manifest.json # version, commit, protocol, per-file hashes + +cp -r overlay/. / # adds files; overwrites Scripts/Scripts.csproj +``` + +On Windows, `Expand-Archive` does not read `.tar.gz`; use `tar.exe` (shipped with Windows 10+) and +`Copy-Item -Recurse -Force`. Plugin developers have `deploy.ps1` in the source repo, which does the +same copy with a hash diff and a `-Verify` dry run — it is not shipped in the tarball. + +The overlay only ever **adds or overwrites**. Nothing in your tree is deleted. + +*Optional — the patch tier* (stock ServUO 57.4 only; see `patches/README.md` in the tarball for the +full explanation): + +```bash +cd +git apply --check patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch +git apply patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch +cp patches/BridgeVendorSale.cs Scripts/Custom/Bridge/ +dotnet build ServUO.sln # REQUIRED — EventSink.cs is a core file + +git apply --check patches/commandlogging-event.patch +git apply patches/commandlogging-event.patch +cp patches/BridgeModerationAudit.cs Scripts/Custom/Bridge/ +``` + +`git apply` works in a plain directory — the shard does not need to be a git repo. If you use +`patch` instead, note the core files are CRLF: use `patch --binary`. + +### A3. Install the sidecar + +```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/v1.1.0/SHA256SUMS +sha256sum -c SHA256SUMS --ignore-missing + +sudo install -m 0755 uo-link-sidecar-linux-x86_64 /usr/bin/runicgateway-link +sudo mkdir -p /etc/runicgateway /var/lib/runicgateway +``` + +Provision the config and read back the token in one step. `--print-config` writes the file if it is +missing, generates the auth token if there is none, and prints the resolved settings as JSON — it is +the supported alternative to scraping the startup log: + +```bash +sudo UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db \ + /usr/bin/runicgateway-link --print-config --config /etc/runicgateway/sidecar.toml +``` + +```json +{ + "component": "uo-link-sidecar", + "version": "1.1.0", + "protocol": 3, + "config_path": "/etc/runicgateway/sidecar.toml", + "config_created": true, + "token_generated": true, + "shard": { "bind": "127.0.0.1:7788" }, + "web": { + "bind": "127.0.0.1:8080", + "ws_path": "/ws", + "auth_required": true, + "auth_token": "4f9c…" + }, + "store": { "path": "/var/lib/runicgateway/uo-link.db" } +} +``` + +`config_created` and `token_generated` tell you whether *this* run provisioned anything — the values +alone cannot distinguish a fresh install from a re-read. **The output contains the auth token in +clear text**: keep it out of shell transcripts, logs and support bundles. + +### A4. Register the service + +**Linux** — `/etc/systemd/system/runicgateway-link.service`: + +```ini +[Unit] +Description=Runic Gateway uo-link sidecar +After=network.target + +[Service] +Type=simple +User=runicgateway +Environment=UOLINK_CONFIG=/etc/runicgateway/sidecar.toml +Environment=UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db +ExecStart=/usr/bin/runicgateway-link +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo useradd --system --no-create-home runicgateway +sudo chown -R runicgateway /var/lib/runicgateway /etc/runicgateway +sudo systemctl daemon-reload +sudo systemctl enable --now runicgateway-link +systemctl status runicgateway-link +``` + +**Windows** (elevated PowerShell) — binary under `%ProgramFiles%`, data under `%ProgramData%`: + +```powershell +New-Item -ItemType Directory -Force "$env:ProgramFiles\RunicGateway", "$env:ProgramData\RunicGateway" | Out-Null +Copy-Item .\uo-link-sidecar-windows-x86_64.exe "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe" + +& "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe" --print-config --config "$env:ProgramData\RunicGateway\sidecar.toml" + +sc.exe create RunicGatewayLink binPath= "\"$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe\"" start= auto +sc.exe failure RunicGatewayLink reset= 86400 actions= restart/5000 +[Environment]::SetEnvironmentVariable('UOLINK_CONFIG', "$env:ProgramData\RunicGateway\sidecar.toml", 'Machine') +[Environment]::SetEnvironmentVariable('UOLINK_DB_PATH', "$env:ProgramData\RunicGateway\uo-link.db", 'Machine') +sc.exe start RunicGatewayLink +``` + +Machine environment variables are read at service start, so set them before starting — and never +leave the config path to the default, which is relative to the service's working directory. + +### A5. Connect the website, start the shard, verify + +Exactly as in [§5](#5-connect-the-website) and [§6](#6-start-servuo-and-verify): paste the four +values into Admin → Shard, start ServUO, then check `[bridge status` in game and `/health` on the +sidecar. + +### A6. Updating by hand + +Re-read `current.json`, and if either version moved: replace the sidecar binary and restart its +service; re-extract the overlay tarball over your tree and restart ServUO. Keep the two in step — +`current.json` is the only statement that a given pair speaks the same protocol. + +--- + +## Appendix B — `sidecar.toml` reference + +Written on first run with a generated token. Environment variables override the file; the file +overrides these defaults. + +```toml +[shard] +bind = "127.0.0.1:7788" # where the SHARD dials in. Keep this on loopback. + +[web] +bind = "127.0.0.1:8080" # where the WEBSITE connects. Widen only with a firewall in front. +auth_token = "…" # generated if blank; the website's Admin → Shard "Auth token" + +[store] +path = "uo-link.db" # relative paths resolve against this file's directory, not the CWD +``` + +| Environment variable | Overrides | +|---|---| +| `UOLINK_CONFIG` | Which config file to read (`--config ` outranks it) | +| `UOLINK_SHARD_BIND` | `[shard] bind` | +| `UOLINK_WEB_BIND` | `[web] bind` | +| `UOLINK_WEB_TOKEN` | `[web] auth_token` | +| `UOLINK_DB_PATH` | `[store] path` | + +| Sidecar command | Output | +|---|---| +| `uo-link-sidecar --version` | `uo-link-sidecar 1.1.0 (protocol 3)` | +| `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. | + +Authentication is **always on**: a blank token is generated and written back, so the web surface is +never unauthenticated. `/health` is the one unauthenticated route, so monitoring can reach it. + +--- + +## Appendix C — `Config/Bridge.cfg` settings worth reviewing + +The file is deployed heavily commented and every setting has a working default — you can leave it +entirely alone. These are the ones most shards want to look at once. Run `[bridge reload` after +editing; endpoint changes take effect on the next reconnect. + +| Setting | Default | Why you might change it | +|---|---|---| +| `LinkUrl` | `https://yoursite/link` | Shown in game when a player runs `[link` to connect their account. **Set this to your site.** | +| `PublicConnectAddress` | *(blank)* | The one connection detail the bridge will publish, e.g. `play.myshard.com,2593`. Blank omits it; `Server.cfg`'s address is **never** published automatically. | +| `AdminWriteEnabled` | `false` | Opt-in staff write plane: kick/ban/broadcast from the website. Authorization is enforced on the website; `AdminAccessFloor` is the shard-side floor that even a compromised sidecar cannot cross. | +| `MarketEnabled`, `MarketSweepSeconds`, `MarketSweepBatch` | `true`, `60`, `25` | The player-vendor index. Coverage takes `ceil(vendors / batch) × seconds` — 500 vendors is one full pass every 20 minutes at the defaults. | +| `PointsLeaderboardEnabled`, `PointsTopN`, `PointsSystems` | `true`, `10`, *(all shown on the loyalty gump)* | Standings boards. One frame **per system**, and ServUO carries ~25 of them, so a large `TopN` multiplies. | +| `RulesetEnabled`, `RulesetIncludeSchedule` | `true`, `true` | Publishes your ruleset (expansion, caps, systems on/off) to the site's rules page. Turn the schedule off if you would rather not advertise a predictable restart window. | +| `SignupMode` | `hybrid` | Which side may mint accounts — `website`, `game`, or `hybrid`. Pair `website` with `Accounts.AutoCreateAccounts=false`, or an in-game login still creates accounts. | +| `QueueCap` | `10000` | Outbound queue cap. On overflow the plugin **drops oldest** and counts drops, because a stalled sidecar must never take the shard down with it. | + +Sweep intervals (`StatSweepSeconds`, `DecaySweepSeconds`, `EconomySweepSeconds`, and the rest) trade +freshness against Core-thread time. The measured cost is small — a vitals sweep is 0.0015 ms per +character, so 1000 online players is ~1.5 ms per pass — but there is rarely anything to gain by +hurrying them. + +--- + +## Where to go next + +| Doc | What | +|---|---| +| [PLAN.md](PLAN.md) | The installer's design of record — phases, locked decisions, the bundle model | +| [`bundles/README.md`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/README.md) | The compat matrix: what a bundle is and how it is composed | +| [link/INTEGRATION.md](../link/INTEGRATION.md) | The sidecar's HTTP/WS API — for anyone integrating something other than the website | +| [link/ADMIN_CONTROLS.md](../link/ADMIN_CONTROLS.md) | The staff write plane in detail, before you turn `AdminWriteEnabled` on | +| [website/SHARD_VISIBILITY.md](../website/SHARD_VISIBILITY.md) | Which shard data each audience sees, configured on the website | +| [link/SHARD_PREREQS.md](../link/SHARD_PREREQS.md) | A worked example of diagnosing a shard whose scripts silently stopped compiling | diff --git a/installer/PLAN.md b/installer/PLAN.md index 76f005f..16ca17c 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -1,19 +1,19 @@ # Runic Gateway Installer — plan -Status: **Phase 0 all but complete** — every prerequisite in another repo has landed, and the -installer repo now publishes the bundle manifest, so *what* the installer will install is already -released and composed ahead of the binary that installs it. No installer code exists yet; `0.4` -(`INSTALL.md`) is the remaining item, then Phase 1. This document is the design of record; -it supersedes the informal overview it grew out of, which described a ServUO integration that does -not match how `servuo-plugins` actually ships (see +Status: **Phase 0 complete.** Every prerequisite in another repo has landed, the installer repo +publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) now specifies the operator-facing run +— so *what* the installer installs and *what using it looks like* both exist ahead of the binary. +No installer code exists yet; **Phase 1 is next.** This document is the design of record; it +supersedes the informal overview it grew out of, which described a ServUO integration that does not +match how `servuo-plugins` actually ships (see [Corrections](#corrections-to-the-original-overview)). | Phase 0 item | State | |---|---| | 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) | | 0.2 `link` installable (data paths + `--print-config`) | ✅ Merged — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) (docs half [docs#84](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/84)); released as [`v1.1.0`](https://gitea.whitlocktech.com/RunicGateway/link/releases/tag/v1.1.0) | -| 0.3 Bundle CI in the installer repo | 🟨 In review — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: `2026.08.04` | -| 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` is **next** — the shape has now settled | +| 0.3 Bundle CI in the installer repo | ✅ Merged — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: [`2026.08.04`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) | +| 0.4 This file + `INSTALL.md` | ✅ [`INSTALL.md`](INSTALL.md) — the operator guide, written before the binary because it *is* the specification of the run | | — Repo bootstrap (governance + CI) | ✅ [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) | --- @@ -180,7 +180,7 @@ Runic Gateway Installer v1.0.0 ├── runicgateway-installer-linux-x86_64 └── SHA256SUMS -uo-link v0.x.y (existing release, extended) +uo-link v1.1.0 (existing release, extended) ├── uo-link-sidecar-windows-x86_64.exe ├── uo-link-sidecar-linux-x86_64 ├── runicgateway-link__amd64.deb (Phase 5) @@ -337,12 +337,48 @@ Repo work that must land before an installer can exist. 4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the shape is settled. + As built ([`INSTALL.md`](INSTALL.md)) — written *before* the binary on purpose. Everything it + installs is already released (items 1–3), so the guide is not speculation about a tool that + might exist; it is the specification of what the run asks, where it writes, what it prints, and + what the operator does next. Phase 1–4 implement it. + + - **It is useful before the installer exists.** Appendix A is the same deployment done by hand — + bundle fetch, tarball verify + overlay copy, the optional patch tier, `--print-config` + provisioning, and a systemd unit / `sc create` service — composed from the released artifacts' + actual contents and the sidecar's config and CLI source rather than from memory. That appendix + doubles as **Phase 1's acceptance test**: walking it end to end on a real shard is what proves + the automated path has nothing left to discover. + - **The installer does not install itself.** §5's `runicgateway doctor` sketch implied a name on + `PATH`; nothing places one there, and adding self-installation would give the tool a second + lifecycle to manage. The guide names the downloaded artifact, says to keep it, and shortens it + in later examples. + - **The flag surface got fixed here**, because a guide cannot describe a run in the abstract: + `--verify`, `--bundle`, `--purge` were already named by §5/§7; `--servuo`, `--patches` / + `--no-patches`, `--host`, `--site-url` and `--yes` are the remainder, chosen so every prompt + in §6's handoff has a non-interactive equivalent and an unattended install is expressible. + - **A modified `Bridge.cfg` must survive an update** — see Phase 1, where this changes the sync + rule inherited from `deploy.ps1`. + - **Remote-website deployments needed an answer.** `[web] bind` defaults to `127.0.0.1`, which + only works when the site runs on the shard host. The guide says to widen it, firewall it to + the website's address, and front it with TLS or a VPN off a trusted network — because the + token is always required but travels as a plain bearer token over HTTP. `[shard] bind` stays + on loopback, since that socket carries inbound commands *into* the game. + ### Phase 1 — installer core - ServUO root detection and validation (`ServUO.exe`, `Scripts/`, `Config/`), with version detection and an explicit refusal when the ServUO process is running. - Overlay sync: fetch tarball → verify SHA256 → hash-compare against the server tree → add/change, **never delete**. Port of `deploy.ps1` semantics including its `-Verify` dry run (`--verify`). +- **One deviation from `deploy.ps1`: an operator-modified `Config/Bridge.cfg` is reported, not + overwritten.** `deploy.ps1` overwrites every file whose hash differs, which is right for a + developer redeploying their own tree and wrong for an operator who has set `LinkUrl`, + `PublicConnectAddress` and sweep intervals — an `update` would silently revert the shard's entire + configuration. `install.json` records the hash deployed, so the installer can distinguish "the + operator edited this" from "the overlay moved on" (§7.0) and act only on the second. The rule is + specific to `Bridge.cfg`: it is the only file in the overlay that is *meant* to be edited in + place, and it carries no code, so a stale copy cannot break the build. Every `.cs` file and + `Scripts.csproj` still overwrite unconditionally. - Write `install.json`: component, version, source commit, per-file hashes, applied patches, timestamp. - Idempotent re-runs; a second run with no upstream change reports "unchanged" and writes nothing. @@ -370,9 +406,9 @@ degrade loudly rather than silently. ``` ✓ ServUO found /opt/ServUO (57.4) -✓ Overlay in sync 30 files, all hashes match install.json +✓ Overlay in sync 24 files, all hashes match install.json ⚠ Patch tier 1 of 3 applied — vendor.sale unavailable -✓ uo-link installed 0.3.0 +✓ uo-link installed 1.1.0 ✓ Service running, enabled ✓ Sidecar reachable 127.0.0.1:8080 /health ok ✓ Protocol sidecar 3 = overlay manifest 3 @@ -393,8 +429,9 @@ deliberately: - **uo-link**: compare the bundle's version against what is installed → download → verify checksum → replace binary → restart service. -- **plugin overlay**: download the bundle's overlay tarball → verify → re-sync → record commit → - tell the operator ServUO must restart (the installer does not restart the shard). +- **plugin overlay**: download the bundle's overlay tarball → verify → re-sync (leaving a modified + `Bridge.cfg` alone — Phase 1) → record commit → tell the operator ServUO must restart (the + installer does not restart the shard). Because both come from one bundle, an update always moves to a combination whose protocol versions were checked together, rather than to two independently-latest artifacts that may disagree. From 83bd4ec2d44683f087df4b4ed8ac12d1889cf7b3 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 4 Aug 2026 11:40:06 -0500 Subject: [PATCH 3/3] docs(installer): link the Phase 0.4 PR from the status table --- installer/PLAN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/PLAN.md b/installer/PLAN.md index 16ca17c..b0f4b91 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -13,7 +13,7 @@ match how `servuo-plugins` actually ships (see | 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) | | 0.2 `link` installable (data paths + `--print-config`) | ✅ Merged — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) (docs half [docs#84](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/84)); released as [`v1.1.0`](https://gitea.whitlocktech.com/RunicGateway/link/releases/tag/v1.1.0) | | 0.3 Bundle CI in the installer repo | ✅ Merged — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: [`2026.08.04`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) | -| 0.4 This file + `INSTALL.md` | ✅ [`INSTALL.md`](INSTALL.md) — the operator guide, written before the binary because it *is* the specification of the run | +| 0.4 This file + `INSTALL.md` | 🟨 In review — [docs#87](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/87). [`INSTALL.md`](INSTALL.md) is the operator guide, written before the binary because it *is* the specification of the run | | — Repo bootstrap (governance + CI) | ✅ [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) | ---