Files
docs/installer/INSTALL.md
wtclaude 3f12e5f49c docs(installer): record Phase 2 as built — sidecar install and service
Phases 1 and 2 now live on the installer repo's `edge` branch, so PLAN.md's
status, the config-path section, and the operator guide all move with them.

PLAN.md
- Status: phases 1 and 2 built. The `edge -> main` cutover now follows Phase 3
  rather than Phase 2, because INSTALL.md §4 describes the patch tier as part of
  the run and a release that answers "not implemented" to all of it is the same
  half-capable binary that kept Phase 1 off `main`.
- §2.3: the service definition always pins the config path, but only Linux pins
  the database. On Windows config and data share a directory, so the sidecar's
  own anchoring rule already lands it correctly — and `sc.exe` offers no
  per-service environment, only a machine-wide one that every process inherits
  and that outlives an uninstall.
- New "Phase 2 as built" section: the virtual service account, the config
  lockdown and why its two halves straddle registration, `--verify` running no
  part of the sidecar half, the protocol check against the installed binary,
  `RUNICGATEWAY_STATE_DIR` relocating the binary and suppressing service
  registration, degrading to a printed recipe with no root/LocalSystem fallback,
  and the token never entering install.json.
- §8 question 1 (Windows service mechanism) resolved: `sc create`, as
  recommended — plus the service identity the recommendation did not anticipate.

INSTALL.md
- Status banner: what is built, and that the patch tier is the remaining gap.
- §2: the illustrated run matches the sidecar block the binary actually prints.
- §3: a table of how each platform pins config and database, the dedicated
  service account on both, and the fact that sidecar.toml's permissions are
  restricted because it holds the auth token.
- Appendix A4: the Windows recipe now matches what the installer does —
  `--config` in binPath (single-quoted so PowerShell keeps the inner quotes),
  `obj=` for the virtual account, the icacls lockdown before and grants after,
  and no machine-wide environment variables.
- Troubleshooting: a row for a run that could not register a service, and one
  for a service that starts and immediately stops.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 15:40:26 -05:00

42 KiB
Raw Blame History

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.

Phases 1 and 2 are built and live on the installer repo's edge branch: the installer core (bundle resolution, ServUO detection, the overlay sync, install.json) and the sidecar half (the binary, its config, its service, and the token handoff). What is still missing is the patch tier (§4), which install reports as not applied rather than silently skipping. The first release follows Phase 3, so that a released binary answers every question this guide says it answers.

Everything it installs is released and published — the sidecar, the plugin overlay, and the bundle manifest 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 itAppendix A 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.


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 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 release binary
3 A record of what it didinstall.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, 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) 57.4 is the only supported version. The base install works on any reasonably current ServUO. The patch tier is written and tested against stock 57.4; on any other version it is unsupported and untested — you can still choose to run it, behind an explicit opt-in, and it applies only where the exact lines it patches are unchanged. See §4.
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:

runicgateway-installer-linux-x86_64
runicgateway-installer-windows-x86_64.exe
SHA256SUMS

Linux

sha256sum -c SHA256SUMS --ignore-missing
chmod +x runicgateway-installer-linux-x86_64

Windows (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 infoRun 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: 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 <tag> (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

sudo ./runicgateway-installer-linux-x86_64 install
# 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. On a ServUO that is not 57.4 the prompt defaults to no and carries an unsupported-version warning you have to answer past. See §4.
  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 sidecar
  binary           /usr/bin/runicgateway-link            install
✓ sidecar binary verified    sha256 27d491ef…
  config           /etc/runicgateway/sidecar.toml        created
  database         /var/lib/runicgateway/uo-link.db
  listening on     shard 127.0.0.1:7788   website 127.0.0.1:8080
  service          runicgateway-link.service             active, enabled
  running as       runicgateway

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.

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.
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 <path> install, doctor, update Name the ServUO root instead of detecting or prompting.
--bundle <tag> install, update Pin an exact published bundle instead of the current one.
--patches / --no-patches install Decide the patch tier non-interactively. --patches never loosens the region check: patches whose target lines are not stock are reported for you to apply by hand, not forced.
--patches-unsupported-servuo install Required in addition to --patches to run the patch tier on a ServUO that is not 57.4. Unsupported and untested — see §4. Ignored on 57.4.
--host <name> install The hostname to print in the website URLs.
--site-url <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, running as NT SERVICE\RunicGatewayLink

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 the config path, because the sidecar's own default is relative to its working directory — and a service manager's working directory is not somewhere you want a database or a config file. On Windows it can be %SystemRoot%\System32 or, under C:\Program Files\, a silently redirected VirtualStore copy.

How the database path is pinned differs by platform, and that is deliberate:

Config Database
Linux Environment=UOLINK_CONFIG= in the unit Environment=UOLINK_DB_PATH= in the unit — /etc and /var/lib are different directories, so both need naming
Windows --config inside the service's own binPath nothing to set: a relative [store] path resolves against the config's directory, which is %ProgramData%\RunicGateway

The Windows service would otherwise need a machine-wide environment variable — sc.exe has no per-service one — which every process on the host inherits and which outlives an uninstall.

Both run as a dedicated, unprivileged account. Linux gets a runicgateway system user; Windows gets a virtual service account, NT SERVICE\RunicGatewayLink, which Windows creates as part of registering the service and which has no password. Neither runs as root or LocalSystem.

sidecar.toml is locked down, because it holds your auth token. Neither default location protects it on its own — /etc is world-readable, and %ProgramData% grants Users read access by inheritance — so the installer sets the permissions itself: chmod 600 plus chown to the service user on Linux, and an explicit ACL of SYSTEM, Administrators and the service account on Windows.

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 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 before anything is applied, and reported per patch. Most real shards are hand-modified; a patch that does not apply is expected, not alarming.
  • A modified file is not automatically a refusal. These patches touch three small regions of three large files. If you have edited Logging.cs somewhere else entirely, the installer says so and still applies the patch — it checks whether the lines the patch edits are still stock, not whether the whole file is. It applies only where the surrounding lines match the patch exactly and appear exactly once; anything less and it stops and hands you the hunk to apply by hand. It never force-fits a patch by loosening the match.
  • All or nothing per feature. The two vendor-sale patches are one unit and are applied together or not at all — and within a patch, if one hunk cannot be placed safely, none are.
  • Your ServUO version is reported, not decisive — but see the warning below before running this on anything other than 57.4.
  • Recorded, and the .patch files cached, so re-runs stay idempotent and uninstall can print the exact hunks to revert — along with how each was applied, since a patch placed into a file you had already modified is one to look at more carefully when reverting.

⚠ On any ServUO that is not 57.4: unsupported, untested, no guarantees

Runic Gateway is designed, built and tested against stock ServUO 57.4. That is the only supported version.

On any other version — a newer release, an older one, or a fork — the patch tier is UNSUPPORTED, UNTESTED, and NOT GUARANTEED TO WORK. You may run it. If you do, you are on your own: it is not covered by support, and a bad outcome may not show up until your shard is live, because ServUO's script build reports success even when it failed and quietly keeps running the previous Scripts.dll.

The installer will still refuse to place a patch anywhere the exact lines it edits have changed — but matching text is not the same as matching behaviour. A hunk can land correctly and still be wrong for a tree that has diverged around it.

Back up your ServUO tree first, and verify your shard boots and compiles afterwards.

Because of that, on a non-57.4 tree the tier is off by default and takes a deliberate yes:

  • the interactive prompt defaults to no and prints the warning above;
  • --patches on its own is not enough — an unattended run must also pass --patches-unsupported-servuo;
  • the choice is recorded, and doctor keeps showing an unsupported-version row for the life of the install — so whoever looks after this shard next can see it without being told.

A run where the tier is selected on a shard that has been worked on looks like this:

Patch tier                    2 of 3 applied
  ✓ playervendor-sale-eventsink   Server/EventSink.cs            stock file
  ✓ playervendor-sale-gump        Scripts/Gumps/PlayerVendorGumps.cs
                                  file modified, patched region stock — applied at line 1180
  ✗ commandlogging-event          Scripts/Commands/Logging.cs
                                  patched region has been modified — not applied
                                  apply this hunk by hand, then re-run install:
                                    /etc/runicgateway/patches/commandlogging-event.patch

  ⚠ Server/EventSink.cs changed — rebuild the core: dotnet build ServUO.sln
  Without commandlogging-event: no in-game moderation audit forwarding.

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:

cd <servuo root>
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:

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 (region-match) — 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
Windows asks for Administrator as soon as you launch it Expected, and it needs Administrator anyway. Windows applies installer detection to unsigned executables whose file name contains install and elevates them before the program starts. Run it from an already-elevated PowerShell and you will not see the prompt.
"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. 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 <path>; do not retype it from a screenshot.
"service NOT REGISTERED" at the end of an otherwise successful run The host has no service manager the installer can drive — most often no systemd (a container, or a distro that never had it), or the runicgateway user could not be created. The binary and config are installed; the run prints the exact unit and commands to finish by hand. It never falls back to running the service as root or LocalSystem.
Service registered but stops immediately It cannot read its config. On Windows check that sc qc RunicGatewayLink shows --config in BINARY_PATH_NAME and that NT SERVICE\RunicGatewayLink has read access to sidecar.toml; on Linux check the runicgateway user can read /etc/runicgateway/sidecar.toml and write /var/lib/runicgateway/.
A patch will not apply Expected on a hand-modified shard. The base install is unaffected; you lose only the two features in §4. 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: <servuo> is your ServUO root, and the shard is stopped.

A1. Fetch the bundle (so you install a checked pair)

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

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/. <servuo>/                       # 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):

cd <servuo>
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

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:

sudo UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db \
     /usr/bin/runicgateway-link --print-config --config /etc/runicgateway/sidecar.toml
{
  "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:

[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
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%:

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"

# The config file now holds your auth token. Lock it down before anything else can read it:
icacls "$env:ProgramData\RunicGateway\sidecar.toml" /inheritance:r /grant:r '*S-1-5-18:(F)' /grant:r '*S-1-5-32-544:(F)'

# binPath carries the config path. The single quotes matter: the value itself contains the double
# quotes the service manager needs around a path with spaces in it.
sc.exe create RunicGatewayLink `
  binPath= '"C:\Program Files\RunicGateway\uo-link-sidecar.exe" --config "C:\ProgramData\RunicGateway\sidecar.toml"' `
  obj= 'NT SERVICE\RunicGatewayLink' start= auto
sc.exe failure RunicGatewayLink reset= 86400 actions= restart/5000

# The service account exists only once sc create has created it, so its grants come after:
icacls "$env:ProgramData\RunicGateway\sidecar.toml" /grant 'NT SERVICE\RunicGatewayLink:(R)'
icacls "$env:ProgramData\RunicGateway"              /grant 'NT SERVICE\RunicGatewayLink:(OI)(CI)M'

sc.exe start RunicGatewayLink

Three things there are easy to get wrong:

  • The config path goes in binPath, not in a machine environment variable. sc.exe has no per-service environment, and a machine-wide UOLINK_CONFIG would be inherited by every process on the host and survive an uninstall. Never leave the config path to the default — it is relative to the service's working directory, which for a service is %SystemRoot%\System32.
  • The database needs no pinning here. A relative [store] path resolves against the directory holding sidecar.toml, which is already %ProgramData%\RunicGateway.
  • obj= is what keeps this off LocalSystem. NT SERVICE\RunicGatewayLink is a virtual service account: Windows creates it with the service, it has no password, and it exists only for this service. Omit obj= and you get the most privileged local identity there is, for a process listening on two TCP ports.

A5. Connect the website, start the shard, verify

Exactly as in §5 and §6: 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.

[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 <PATH> 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. 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 The installer's design of record — phases, locked decisions, the bundle model
bundles/README.md The compat matrix: what a bundle is and how it is composed
link/INTEGRATION.md The sidecar's HTTP/WS API — for anyone integrating something other than the website
link/ADMIN_CONTROLS.md The staff write plane in detail, before you turn AdminWriteEnabled on
website/SHARD_VISIBILITY.md Which shard data each audience sees, configured on the website
link/SHARD_PREREQS.md A worked example of diagnosing a shard whose scripts silently stopped compiling