45 Commits

Author SHA1 Message Date
ad7defd471 docs(installer): document aarch64 and the upgrade backup in INSTALL.md
PLAN.md §5.4. The operator guide is the specification of the run, so
these are part of building the phase rather than a write-up after it.

- The download list gains runicgateway-installer-linux-aarch64, with
  `uname -m` as the way to tell, and says plainly that there is no macOS
  and no Windows-on-arm build: the shard dials the sidecar out on
  loopback, so the two share a host, and no ServUO host is either.
- Appendix A3 names the arm64 sidecar asset for the by-hand path, and
  points at the bundle from A1 for the version rather than the one
  written in the example.
- §7's `update` says what a backup is, when one is taken and when one is
  not, and that restoring is the operator's to do -- the guide already
  promised their Bridge.cfg edits survive, and this is the same promise
  for the .cs file they edited that gets overwritten by design.
- --no-backup joins the flags table; --purge's row and the uninstall
  table now name backups alongside the config, the database and the
  cached patch set.
- The paths tables and doctor's sample output gain the backups
  directory and its row.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 05:53:11 -05:00
e18eec8957 docs(installer): correct §5.3's trigger and record what building §5.2 found
Two things this section got wrong, both found by implementing it.

§5.3 said a backup is taken for an update and for an install over an
existing record, because "a first install overwrites nothing". That is
not true of a tree deployed by hand per INSTALL.md Appendix A2 -- the
path this project recommends while the binary is unreleased. There the
first install finds .cs files that differ, plans them as Change, and
overwrites them with no prior record anywhere to notice. The rule is
now the direct one: back up whenever the run is about to overwrite
something. A genuine first install onto a clean tree still writes
nothing, because there is nothing to copy.

Also records that sidecar.toml joins a backup rather than causing one
(nothing rewrites it, so triggering on it would leave a dated directory
after every no-op update), and that the directory is created lazily
with the manifest written last, so an interrupted run can neither be
mistaken for a backup nor evict a good one.

§5.2 gains what its cross-builds turned up: neither crate builds with
the arm64 compiler alone. gcc-aarch64-linux-gnu only recommends
libc6-dev-arm64-cross while both release workflows install with
--no-install-recommends, so the C in each crate -- bundled SQLite under
sqlx, ring under ureq's rustls -- fails on a missing libc header while
every Rust dependency compiles fine.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 05:49:09 -05:00
38f83ad2e0 docs(installer): settle Phase 5's scope
Phase 5 was sketched as four items; two of them are dropped rather than
deferred, because what stops them is an ownership conflict that does not
improve with time (org lead, 2026-08-05).

- No .deb and no MSI (5.1). A .deb under link's release would own the
  binary, the systemd unit and the service user -- the three things
  service.rs writes, hardens and removes and install.json records, so
  uninstall would leave a dpkg-installed-but-broken package and an
  apt upgrade would make doctor report drift nobody caused. The
  binary-only variant buys apt-managed upgrades of one file, which
  update already does from a protocol-checked bundle. An MSI
  contradicts "the installer does not install itself" and adds a second
  uninstall path beside the verb that owns install.json, the cached
  patch set and the ServUO-tree report.
- Linux aarch64 for both components (5.2), in the order the bundle CI
  forces: it hard-fails on an unrecognized link asset name and asserts
  the platform keys present, so the name is taught on main first, link
  publishes, the key becomes required, and only then does the crate on
  edge learn it. bundle.yml is never edited on edge, so the cutover
  merge has nothing to conflict over.
- Backup before overwrite (5.3), scoped by what cannot be fetched
  again: not the binary or the overlay files, and not the database
  (store.rs is CREATE TABLE IF NOT EXISTS over shard state the sweeps
  repopulate -- a cache with a schema), but an operator's edits to a
  deployed .cs file, which Phase 1 overwrites by design, and
  sidecar.toml, whose token the website already holds.
- The docs a first release invalidates (5.4), including the repo README
  still announcing Phase 1 four phases later.

Also records that the Windows SCM smoke was attempted on 2026-08-05 and
stopped at its first check on an unelevated shell, so that half remains
entirely unexecuted.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 8818c06f1d)
2026-08-05 05:20:26 -05:00
3eb9fa8653 Merge pull request 'docs(installer): put Phase 5 before the cutover, and flag Windows SCM as untested' (#95) from docs/installer-polish-before-cutover into main
Reviewed-on: #95
2026-08-05 10:12:17 +00:00
3a6b9196fa docs(installer): put Phase 5 before the cutover, and flag Windows SCM as untested
Two decisions from the org lead, recorded in the design of record.

Phase 5 (packaging polish) now runs BEFORE the edge -> main cutover
rather than after it. The original order assumed the cutover would cut a
v1 and packaging would follow as a v1.x, but this phase changes the
release layout itself: shipping first would mean a first release that is
immediately superseded, and operators who downloaded a bare binary being
told to re-download a package. Deferring costs nothing — nothing is
published from `edge`, and INSTALL.md's Appendix A is the supported path
meanwhile.

The cutover therefore has two entry criteria, stated in the status
header and at Phase 5: packaging polish, and the Windows SCM half being
verified on a real host. The second is called out explicitly because
`sc create`, the virtual service account, the failure actions and the
token-file ACL have still never been executed anywhere — and running the
systemd half for real is precisely what turned up a bug no unit test
had. Nothing should be released while the only untested code is the half
that registers a service.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 05:00:45 -05:00
c47e0fb303 Merge pull request 'docs(installer): record the first real systemd verification' (#94) from docs/installer-systemd-verified into main
Reviewed-on: #94
2026-08-05 09:30:29 +00:00
33fadbd254 docs(installer): record the first real systemd verification
Phase 2 shipped service registration that had never been executed: a
relocated test run deliberately skips it, `sc create` needs elevation,
and systemd needs a Linux host. It has now been run for real on a
privileged Debian 12 container with systemd as PID 1 — unit written and
enabled, service up as the unprivileged runicgateway user, sidecar.toml
600 and owned by it, database under /var/lib (so the UOLINK_DB_PATH pin
works), /health answering protocol 3, and uninstall taking the service,
unit, binary and account away while leaving the config, the database and
the whole ServUO tree alone.

That surfaced one bug only a real service host could show — user_created
was recorded per-run rather than as state, so an identical re-run
rewrote install.json and uninstall silently left behind the account the
installer had created (installer#8). Recorded here with the reason it is
invisible on Windows.

Also notes what is still unverified: the Windows SCM half, which needs an
elevated shell this machine's automation does not have.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 04:02:16 -05:00
1dc6084bf7 Merge pull request 'docs(installer): record Phase 4 — doctor, update and uninstall as built' (#93) from docs/installer-phase4 into main
Reviewed-on: #93
2026-08-05 08:49:12 +00:00
ecef87f120 docs(installer): record Phase 4 — doctor, update and uninstall as built
The installer crate now implements the whole command surface INSTALL.md
published before the binary existed, so this records what Phase 4 turned
out to be and corrects two places where the plan and the guide had drifted
apart.

PLAN.md
  - Status header: Phases 1–4 are on `edge`; the edge → main cutover now
    cuts a binary that does everything INSTALL.md describes, with Phase 5
    being packaging polish rather than capability.
  - A Phase 4 "as built" section: why `update` is the install pipeline in
    a different mode rather than a second implementation, why it neither
    reprints the token nor stays quiet about a protocol change, the tier's
    scope under `update` (re-resolve what was applied, without re-asking;
    name what is new), how `doctor` asks the binary the way the service
    does, the exit-code rule and why a stopped shard is a ⚠ while a
    running one that has not dialed in is a ✗.
  - §5's uninstall table: the cached patch set and patches/originals/ move
    from "removed" to "kept". The report that command prints tells the
    operator to diff against those originals — deleting them made the
    advice impossible to follow within one command's output. `--purge`
    removes them.

INSTALL.md
  - §2: exit codes stated (`doctor` and `uninstall` use 1 for a completed
    run that found something wrong), `--patches` now applies to `update`,
    `--yes` means yes on `uninstall`, `--purge` covers the patch cache.
  - §7 doctor: the real row set, what ✓/⚠/✗ mean, that it writes nothing
    and is safe to run with the shard up.
  - §7 update: it updates the tree install.json names, needs the shard
    stopped, does not reprint the token, calls out a protocol change, and
    what it does and does not do with the patch tier.
  - §7 uninstall: what survives, that edited files are flagged in the
    listing, the confirmation's default, and where the report file lands.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:00:45 -05:00
706b450828 Merge pull request 'docs(installer): record Phase 3 — the patch tier as built' (#92) from docs/installer-phase3 into main
Reviewed-on: #92
2026-08-05 01:07:46 +00:00
42e6f3a0cb docs(installer): record Phase 3 — the patch tier as built
PLAN.md gains a "Phase 3 as built" section covering the decisions the plan had
left open, and §2.2, §2.2.1 and §7.0 are brought in line with what shipped:

* The engine is fully native. §2.2.1 wrote rung 1 as "apply verbatim with
  git apply", but §1 chose the release tarball so there would be no git on the
  shard host, and rung 2 needs a native applier anyway. Rung 1 keeps its
  stronger verdict and shares rung 2's write path. On the real files this is
  not academic — the shipped patches are CRLF and two of their three targets
  are LF, so git apply refuses patches the installer places correctly.

* §7.0 documents `patch_tier` in the overlay manifest. Which patches form one
  unit, which companion follows which, whether a core rebuild is needed and
  what declining costs are not derivable from a diff, so the release declares
  them and adding a patch regenerates metadata rather than an installer.

* §2.2 gains the pre-image cache and the widened patch cache, and §2.2.1 gains
  the second, per-feature level of the all-or-nothing rule.

INSTALL.md's illustrated tier output is replaced with the real thing, the
status banner now says `install` is complete, §3's path tables list
patches/originals/, and Appendix A2 names the line-ending trap that makes
git apply refuse a patch whose region is visibly untouched.

Refs: RunicGateway/installer#6, RunicGateway/servuo-plugins#10

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 19:57:42 -05:00
5ae53d287f Merge pull request 'docs(installer): record Phase 2 as built — sidecar install and service' (#91) from docs/installer-phase2 into main
Reviewed-on: #91
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 20:44:59 +00:00
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
ce02abbc11 Merge pull request 'docs(installer): record Phase 1 as built' (#90) from feat/installer-phase-1 into main
Reviewed-on: #90
2026-08-04 20:00:56 +00:00
4bdc764742 docs(installer): record Phase 1 as built
PLAN.md moves from "Phase 0 complete, no code exists" to "Phase 1 built, on
edge", and the Phase 1 section gains an "As built" block in the same shape as
the Phase 0 entries — covering the decisions that were not already settled by
the design: why the crate lands on `edge` instead of `main`, why the library
target is not named after the binary (Windows UAC installer detection makes
`cargo test` unrunnable under that name), the dependency choices that follow
from the MinGW cross-build, path-based rather than name-based shard-running
detection, reading ServUO's version from Server/AssemblyInfo.cs, and the two
rules the smoke test corrected — install.json recording a state rather than the
run's verb, and the Bridge.cfg keep comparing against the last hash deployed
rather than the last hash seen.

INSTALL.md gains the same status note and one troubleshooting row: Windows
elevates the binary on launch because its file name contains "install", which
is expected and needs no action beyond running from an elevated shell.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 14:58:46 -05:00
a98fceb4bb Merge pull request 'docs(installer): review the patched region, not the whole-file hash' (#89) from docs/installer-patch-region-review into main
Reviewed-on: #89
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 19:19:14 +00:00
5a6cb58a33 docs(installer): review the patched region, not the whole-file hash
The patch tier refused on a whole-file hash mismatch, which is the wrong
question: the three patches touch three small regions of three large files, so
an operator who edited Logging.cs somewhere else entirely was handed a manual
patch job they did not need. Hand-modified shards are the norm, so that refusal
covered most of the audience.

Replace the single hash test with a four-rung ladder (PLAN §2.2.1), cheapest and
safest first:

  0  post-patch text already present  -> no-op, keeps re-runs idempotent
  1  whole file matches the pre-image -> apply verbatim
  2  file differs, patched region is still byte-identical -> apply at the
     matched offset
  3  anything else -> do not touch the file; print the hunk to apply by hand

Rung 2 needs no new metadata: a unified diff already carries the stock text of
the region it edits (context lines plus the '-' lines). Guardrails keep it from
becoming a fuzzy apply -- exact match with only CRLF/trailing-whitespace
normalisation, exactly one occurrence or it fails, line numbers advisory only,
and all-or-nothing per patch file so a half-patched EventSink.cs cannot happen.
install.json records which rung applied each patch, and doctor and uninstall
report it.

This retires the blanket 57.4-only version gate, so PLAN gains §2.2.2 to draw
the line the ladder does not: content matching is a mechanical guarantee about
where text lands, not a support commitment. 57.4 stays the only supported
version. A non-57.4 tree may attempt the tier, but unsupported, untested and not
guaranteed -- behind a loud banner, a prompt defaulted to no, and its own
--patches-unsupported-servuo flag, because a bare --patches can be hit by
accident in a copied script. The unsupported marker persists into install.json,
every later doctor run, and the uninstall report.

INSTALL.md gets the operator-facing half: a block-quoted warning naming the
silent-script-build failure mode, the updated prerequisite row, prompts and flag
table, and a sample run showing all three outcomes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 14:17:33 -05:00
d5838f7d4c Merge pull request 'docs(installer): mark Phase 0.4 merged in the status table' (#88) from docs/installer-0.4-status into main
Reviewed-on: #88
2026-08-04 19:03:55 +00:00
90459f2c49 docs(installer): mark Phase 0.4 merged in the status table
docs#87 landed; the row it added still said 'in review'. The status table is
the first thing anyone reads in this file, so a stale row there is worse than
no row.
2026-08-04 14:02:32 -05:00
a2e859bf9a Merge pull request 'docs(tree): sync installer/PROJECT_TREE.md' (#86) from chore/sync-installer-tree into main
Reviewed-on: #86
2026-08-04 19:01:27 +00:00
2dbe4d8388 Merge branch 'main' into chore/sync-installer-tree 2026-08-04 17:15:57 +00:00
e85ca632ce Merge pull request 'docs(installer): add the operator install guide (Phase 0.4)' (#87) from docs/installer-phase-0.4 into main
Reviewed-on: #87
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 17:10:33 +00:00
83bd4ec2d4 docs(installer): link the Phase 0.4 PR from the status table 2026-08-04 11:40:06 -05:00
5c8585fe75 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 <noreply@anthropic.com>
2026-08-04 11:39:02 -05:00
fc79bb6ed0 Merge pull request 'docs(installer): record Phase 0.3 — the bundle CI and where bundles live' (#85) from docs/installer-phase-0.3 into main
Reviewed-on: #85
2026-08-04 16:20:05 +00:00
runic-docs-bot
a6b20cb273 docs(tree): sync installer/PROJECT_TREE.md from RunicGateway/installer@0e7d5f3 [skip ci] 2026-08-04 16:19:51 +00:00
af6036b9c7 Merge pull request 'docs(tree): sync link/PROJECT_TREE.md' (#83) from chore/sync-link-tree into main
Reviewed-on: #83
2026-08-04 16:17:52 +00:00
e0245a209c Merge branch 'main' into chore/sync-link-tree 2026-08-04 16:17:39 +00:00
10ebce706f docs(installer): record Phase 0.3 — the bundle CI and where bundles live
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 <tag> 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 <noreply@anthropic.com>
2026-08-04 11:17:28 -05:00
d1cb3e9511 Merge pull request 'docs(installer): record Phase 0.2 — the sidecar's CLI and settled data paths' (#84) from docs/installer-phase-0.2 into main
Reviewed-on: #84
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 15:55:52 +00:00
c1957f0bfb docs(installer): record Phase 0.2 — the sidecar's CLI and settled data paths
Phase 0.2 landed in link#24: the sidecar gained a four-flag CLI, --print-config,
and config-anchored data paths. Three sections of the installer plan asserted
facts that change as a result, so they are corrected rather than appended to.

- Status table: 0.1 merged (servuo-plugins#7/#8, overlay v0.1.1 released), 0.2 in
  review, repo bootstrap merged. 0.3 (bundle CI) is next and now unblocked — both
  components it composes exist.
- Phase 0 item 2 gains an "As built" subsection matching item 1's: why four
  hand-rolled flags rather than a parsing crate, why --print-config provisions
  instead of only reporting, why config_created/token_generated exist, and why no
  platform data directories are compiled into the binary.
- §2.3 (working-directory trap): half-closed in the sidecar — a relative
  [store].path now anchors to the config file's directory — while the service
  definitions still pin both env vars, and why that is not redundant.
- §2.4 (token handoff): the installer reads the handoff block out of one
  --print-config call and never parses the log, which is not a contract.
- §5 Phase 2 / Phase 4, §6: the ordering that follows (print-config before service
  registration), which doctor rows the CLI answers, that only the host is
  substituted into the printed URLs because web.bind is often 0.0.0.0, and that the
  printed token must not reach a log or support bundle.
- link/INTEGRATION.md §1: how to read the token back, replacing "the sidecar logs
  it" with the supported command and its output.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 10:48:44 -05:00
runic-docs-bot
b6343a0937 docs(tree): sync link/PROJECT_TREE.md from RunicGateway/link@654a08a [skip ci] 2026-08-04 15:48:24 +00:00
e1608bb280 Merge pull request 'docs(installer): record Phase 0 progress and the overlay manifest' (#82) from docs/installer-phase0 into main
Reviewed-on: #82
2026-08-04 15:29:17 +00:00
f0975df598 docs(installer): record Phase 0 progress and the overlay manifest
Tracks what actually landed while starting the installer plan, and corrects
the parts of the plan that the work proved wrong or stale.

Progress:

  A Phase 0 status table at the top, so the plan says where it is rather
  than needing a reader to reconstruct it from PR links.

  Phase 0 item 1 (§5) now records the release workflow as built, including
  its three deviations from link's copy — structural gates instead of build
  gates, no bump commit and therefore no push to main, and overlay.toml as
  the home for the declared protocol version. Plus the fixed tarball prefix
  and why: the installer would otherwise have to parse the version it is
  trying to read.

New §7.0 documents the overlay manifest as generated, and states plainly the
two things about it that carry weight: `protocol` is hand-maintained and has
to be (nothing in CI can derive it, which is exactly why §7.1's gate 1 has
something to compare), and `files` is what lets `doctor` distinguish
"operator edited a deployed file" from "the overlay moved on".

Corrections:

  §2.6 the plugin's protocol version now has a home (overlay.toml), and
        servuo-plugins now has a release workflow.
  §7.2  the dispatch step is deliberately deferred to Phase 0 item 3.
  §7.4  no longer "open risk" — the v3 cutover merged. The rule it motivated
        (never hardcode a protocol version) is restated as permanent rather
        than as a workaround for a mid-flight cutover.
  §8    open question 4 (branch targeting) resolved: servuo-plugins#6 merged,
        main == edge, everything targets main.

Version examples in §3, §5 and §7.1 said uo-link v3.x.y / 3.0.1, conflating
the release version with the protocol version. link is actually at v0.3.0 —
the two are independent, and the bundle names release versions, so an example
implying they track each other is actively misleading. Now uses the real
values (link 0.3.0, overlay 0.1.0, 30 overlay files).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 09:28:23 -05:00
8322e8318c Merge pull request 'docs(installer): add release orchestration and the bundle manifest' (#81) from docs/installer-release-orchestration into main
Reviewed-on: #81
2026-08-01 21:42:33 +00:00
25a5734107 docs(installer): add release orchestration and the bundle manifest
The installer needs CI that reacts when a component publishes a release. Adds
that as section 7, folded into version tracking because the bundle IS the compat
matrix -- which closes the "where does the compat matrix live" gap section 7
previously left open.

- 7.1 Bundle manifest: CI publishes an exact, protocol-checked combination of
  component versions; the installer resolves against it at run time and
  --bundle <tag> pins one. A link release regenerates JSON and leaves the
  installer binary untouched, so operators don't re-download the installer for a
  sidecar patch and the repo doesn't accumulate releases with identical code.
  Two compose-time gates: sidecar PROTOCOL_VERSION must equal the overlay
  manifest's declared version, and every asset's SHA256 must match.
- 7.2 Triggers: each component's release job POSTs to the installer's
  workflow-dispatch endpoint (link's release.yml already declares
  workflow_dispatch and already holds a write:repository token), plus a nightly
  cron so a missed dispatch self-heals. repository_dispatch avoided -- support
  is uncertain on this Gitea version.
- 7.3 Stale overlay: dispatch, don't wait. Components self-release on merge to
  their own main, so the release normally already exists. If main is ahead with
  *releasable* commits (docs:/chore: correctly cut nothing), fire that repo's
  workflow, compose from what exists now, warn loudly, and let the nightly fold
  in the result. Dispatching another repo's workflow is fine -- it still runs
  its own gates -- but polling it is not, since Gitea's dispatch endpoint
  returns no run handle.

Bundle CI becomes a Phase 0 deliverable, since Phase 1 resolves what to install
from the bundle. `update` now moves between checked combinations rather than two
independently-latest artifacts.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 06:11:08 -05:00
c583ddb77f Merge pull request 'docs(installer): plan the Runic Gateway installer' (#80) from docs/installer-plan into main
Reviewed-on: #80
2026-08-01 10:46:13 +00:00
29056ba996 docs(installer): plan the Runic Gateway installer
Design of record for a deployment tool that takes a stock ServUO install and
configures it for Runic Gateway. Supersedes the informal overview it grew from,
which described a ServUO integration that does not match how servuo-plugins
actually ships.

Corrections that change the design:

- There is no RunicGateway.dll and no Plugins/ dir. The plugin ships as C#
  source compiled by ServUO at boot, so the step is a hash-compare sync of
  overlay/ -- but a successful copy does not mean a working bridge, because
  ScriptCompiler.Compile() ignores dotnet build's exit code and reloads the
  stale Scripts.dll.
- Stock ServUO files ARE modified, by three diffs in patches/. Made an opt-in,
  skippable tier: git apply against a hand-modified shard will fail, and the
  EventSink.cs patch needs a full core solution rebuild.
- Config paths collided with what the sidecar actually reads. Split ownership:
  sidecar.toml stays the sidecar's schema, install.json is the installer's.
  Service definitions pin UOLINK_CONFIG and UOLINK_DB_PATH, since the sidecar
  writes relative to CWD and would land in VirtualStore under Program Files.
- The token handoff was missing entirely -- the largest "installed it and
  nothing happened" failure mode.
- deploy.ps1 cannot be the cross-platform deployer; it stays the developer tool.

Decisions: public audience, unsigned binaries anchored on SHA256SUMS, Rust,
release-tarball plugin distribution, printed token handoff, new installer repo,
warn-and-skip on non-57.4 ServUO, and an uninstall that never edits the shard
tree -- it prints the files to delete and the hunks to revert.

Phases 0-5, with Phase 0 (a release workflow for servuo-plugins, which has
none today) gating everything else. Two open questions remain in section 8.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 05:44:44 -05:00
186f057bc0 Merge pull request 'docs(tree): sync android/PROJECT_TREE.md' (#79) from chore/sync-android-tree into main
Reviewed-on: #79
2026-08-01 07:50:46 +00:00
runic-docs-bot
33a013ca98 docs(tree): sync android/PROJECT_TREE.md from RunicGateway/Android-app@5eaf5d2 [skip ci] 2026-08-01 07:22:23 +00:00
5e2bc22a94 Merge pull request 'docs(tree): sync link/PROJECT_TREE.md' (#77) from chore/sync-link-tree into main
Reviewed-on: #77
2026-08-01 07:21:29 +00:00
04cd64b838 Merge branch 'main' into chore/sync-link-tree 2026-08-01 07:21:09 +00:00
916c11ee92 Merge pull request 'docs(tree): sync website/PROJECT_TREE.md' (#78) from chore/sync-website-tree into main
Reviewed-on: #78
2026-08-01 07:20:49 +00:00
runic-docs-bot
ea3755760d docs(tree): sync website/PROJECT_TREE.md from RunicGateway/website@5103b74 [skip ci] 2026-08-01 07:19:52 +00:00
runic-docs-bot
54b3002701 docs(tree): sync link/PROJECT_TREE.md from RunicGateway/link@295defb [skip ci] 2026-08-01 06:50:30 +00:00
8 changed files with 2360 additions and 7 deletions

View File

@@ -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`.

View File

@@ -85,6 +85,7 @@ android-app/
│ │ │ │ │ │ │ ├── PlayerShardDto.kt
│ │ │ │ │ │ │ ├── PostDto.kt
│ │ │ │ │ │ │ ├── PublicDto.kt
│ │ │ │ │ │ │ ├── ShardContentDto.kt
│ │ │ │ │ │ │ ├── ShardDto.kt
│ │ │ │ │ │ │ ├── SsoDto.kt
│ │ │ │ │ │ │ └── WikiDto.kt
@@ -106,6 +107,7 @@ android-app/
│ │ │ │ │ ├── NotificationsRepository.kt
│ │ │ │ │ ├── PlayerShardRepository.kt
│ │ │ │ │ ├── SettingsRepository.kt
│ │ │ │ │ ├── ShardFeaturesRepository.kt
│ │ │ │ │ ├── ShardRepository.kt
│ │ │ │ │ └── WikiRepository.kt
│ │ │ │ ├── di/
@@ -171,6 +173,8 @@ android-app/
│ │ │ │ │ ├── session/
│ │ │ │ │ │ └── SessionViewModel.kt
│ │ │ │ │ ├── shard/
│ │ │ │ │ │ ├── AtlasScreen.kt
│ │ │ │ │ │ ├── AtlasViewModel.kt
│ │ │ │ │ │ ├── ChampsScreen.kt
│ │ │ │ │ │ ├── ChampsViewModel.kt
│ │ │ │ │ │ ├── FrameFields.kt
@@ -180,7 +184,13 @@ android-app/
│ │ │ │ │ │ ├── GuildsViewModel.kt
│ │ │ │ │ │ ├── HousesScreen.kt
│ │ │ │ │ │ ├── HousesViewModel.kt
│ │ │ │ │ │ ├── LeaderboardsScreen.kt
│ │ │ │ │ │ ├── LeaderboardsViewModel.kt
│ │ │ │ │ │ ├── LiveBoard.kt
│ │ │ │ │ │ ├── MarketScreen.kt
│ │ │ │ │ │ ├── MarketViewModel.kt
│ │ │ │ │ │ ├── RulesScreen.kt
│ │ │ │ │ │ ├── RulesViewModel.kt
│ │ │ │ │ │ ├── ShardComponents.kt
│ │ │ │ │ │ ├── ShardEventText.kt
│ │ │ │ │ │ ├── ShardScreen.kt
@@ -284,6 +294,7 @@ android-app/
│ │ │ │ │ ├── PlayerShardDtoTest.kt
│ │ │ │ │ ├── PublicDtoTest.kt
│ │ │ │ │ ├── ShardBoardDtoTest.kt
│ │ │ │ │ ├── ShardContentDtoTest.kt
│ │ │ │ │ ├── ShardDtoTest.kt
│ │ │ │ │ ├── SsoDtoTest.kt
│ │ │ │ │ └── WikiDtoTest.kt
@@ -294,7 +305,8 @@ android-app/
│ │ │ │ └── FakeShardStream.kt
│ │ │ └── repository/
│ │ │ ├── AccountTrustedDevicesTest.kt
│ │ │ ── ConnectionVersionGuardTest.kt
│ │ │ ── ConnectionVersionGuardTest.kt
│ │ │ └── ShardFeaturesRepositoryTest.kt
│ │ ├── ui/
│ │ │ ├── admin/
│ │ │ │ ├── AdminContentViewModelTest.kt
@@ -304,7 +316,8 @@ android-app/
│ │ │ ├── contact/
│ │ │ │ └── ContactViewModelTest.kt
│ │ │ ├── navigation/
│ │ │ │ ── MenuAccessTest.kt
│ │ │ │ ── MenuAccessTest.kt
│ │ │ │ └── MenuFeatureGatingTest.kt
│ │ │ ├── notifications/
│ │ │ │ └── NotificationRoutingTest.kt
│ │ │ ├── player/
@@ -315,6 +328,8 @@ android-app/
│ │ │ │ ├── FrameFieldsTest.kt
│ │ │ │ ├── LiveBoardTest.kt
│ │ │ │ ├── ShardBoardViewModelTest.kt
│ │ │ │ ├── ShardContentHelpersTest.kt
│ │ │ │ ├── ShardContentViewModelTest.kt
│ │ │ │ └── ShardEventTextTest.kt
│ │ │ ├── theme/
│ │ │ │ └── BrandColorTest.kt

899
installer/INSTALL.md Normal file
View File

@@ -0,0 +1,899 @@
# 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 to 3 are built and live on the installer repo's `edge` branch: the installer core
> (bundle resolution, ServUO detection, the overlay sync, `install.json`), the sidecar half (the
> binary, its config, its service, and the token handoff), and the
> [patch tier](#4-the-patch-tier-optional). `install` is therefore complete; what is still missing
> is [`doctor`, `update` and `uninstall`](#7-day-two), each of which reports which phase it arrives
> in rather than failing as though you had mistyped it. The first release is being cut from that
> branch now.
>
> 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 cached copies of the patches and of every file the patch tier edited | 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)* | **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](#4-the-patch-tier-optional). |
| 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-linux-aarch64
runicgateway-installer-windows-x86_64.exe
SHA256SUMS
```
`linux-aarch64` is for arm64 hosts — Ampere/Graviton instances, Pi-class boxes. `uname -m` says
`aarch64` on those and `x86_64` otherwise. There is no macOS build and no Windows-on-arm build: the
shard dials the sidecar out on loopback, so the two have to share a host, and no ServUO host is
either of those.
**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 <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
```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. 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](#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 kept=0
Patch tier 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](#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 <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`, `update` | 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. On `update` it is what takes up a feature the shard does not already have. |
| `--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](#4-the-patch-tier-optional). 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. **On `uninstall` it means yes** — that prompt defaults to no, and typing `uninstall --yes` is not an accident. |
| `--no-backup` | `install`, `update` | Do not copy the files this run is about to overwrite. They are otherwise saved under the state directory — see [§7](#7-day-two). |
| `--purge` | `uninstall` | Also delete `sidecar.toml`, `uo-link.db`, the cached patch set and every backup, all of which are otherwise kept. |
Exit codes are `0` success, `1` the run failed, `2` the arguments were unusable. Two commands also
use `1` for a run that *completed* and found something wrong, so they can be read from a script:
`doctor` when any check failed, and `uninstall` when a step could not be carried out (everything
else still was).
---
## 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 the patches the tier evaluated, so `uninstall` can print the exact hunks long after the release tarball is gone, and a refused one is still on hand to apply yourself |
| `/etc/runicgateway/patches/originals/` | Each file the patch tier edited, exactly as it was beforehand — a revert you can verify rather than reconstruct |
| `/etc/runicgateway/backups/<timestamp>/` | Copies of the files a run replaced, with a `manifest.json` naming each. Newest three kept; skip with `--no-backup` |
| `/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\patches\originals\` | As above |
| `%ProgramData%\RunicGatewayackups\<timestamp>\` | 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](#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 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. A patch that
could have been placed but was held back by its sibling says exactly that; it is never reported as
applied.
- **It does not need `git`, and does not use it.** The matching and the writing are the installer's
own, which is why it can place a patch on a shard where `git apply` refuses — the shipped patches
and their target files do not all use the same line endings, and that alone defeats `git apply`.
Nothing outside a patched region is touched, down to the byte, and inserted lines take your file's
own line ending.
- **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. Patches that were *not*
applied are cached too, because that is the copy the run tells you to apply by hand.
- **A copy of every file it edits is kept, exactly as it was beforehand**, under
`patches/originals/` in the installer's own directory — not in your ServUO tree. It is written
before the first edit and never overwritten, so however many times you re-run `install`, it stays
the version from before the tier ever touched the file. That is what lets you verify a revert
rather than reconstruct one.
- **Re-running is safe.** A patch already in place is recognised and left alone, and the record
keeps the way it originally landed rather than relabelling it.
### ⚠ 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 — applied at line 171, 1521, 1771, 2416
✓ playervendor-sale-gump Scripts/Gumps/PlayerVendorGumps.cs
file modified, patched region stock — applied at line 95
✗ commandlogging-event Scripts/Commands/Logging.cs
patched region has been modified (hunk 1) — not applied
apply this by hand, then re-run install to record it:
/etc/runicgateway/patches/commandlogging-event.patch
⚠ Server/EventSink.cs — a CORE ServUO file was patched. Rebuild the solution:
dotnet build ServUO.sln
A shard restart is not enough; ServUO's dynamic script build does not rebuild the core, and it
will not tell you so.
Not applied, so you do not get: no in-game moderation audit forwarding.
Everything else works. Apply the hunks by hand if you want them, then re-run install to record it.
```
The line numbers are where each hunk was actually found in *your* file, not where it sits in stock
ServUO — they differ as soon as anything above the region has been edited, and yours is the one to
go to.
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 <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:
```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.
```
✓ Install record /etc/runicgateway/install.json (bundle 2026.08.04, installer 1.0.0, …)
✓ ServUO found /opt/ServUO (57.4)
✓ Overlay in sync 24 files, all hashes match install.json
⚠ Patch tier 1 applied — moderation-audit (region-match)
✓ uo-link installed uo-link-sidecar 1.1.0 (protocol 3)
config /etc/runicgateway/sidecar.toml database /var/lib/runicgateway/uo-link.db
✓ Service runicgateway-link.service active, enabled as runicgateway
✓ Sidecar reachable 127.0.0.1:8080 /health ok, up 6h, database ok
✓ Protocol sidecar 3 = overlay manifest 3
✗ Shard connected no — the shard is running (pid 8123) but has not dialed in
✓ Bundle 2026.08.04 — up to date
✓ Backups 2026-08-04T09:12:44Z — 3 file(s) replaced by update to bundle 2026.08.04
3 kept in /etc/runicgateway/backups
```
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 `install.json`, which is how it tells "you edited a
deployed file" from "the file is gone"; the bundle row is what tells you the overlay upstream has
moved on. Each patched file is re-checked against the cached copy of its patch, so a core upgrade or
a restored backup that quietly removed the tier's edits is caught here — nothing else would notice.
It writes nothing at all, and it is safe to run while the shard is up; that is in fact the only
state in which the last row can be `✓`.
**Reading the marks:**
| | |
|---|---|
| `✓` | as it should be |
| `⚠` | worth knowing, not broken — a stopped shard, a service you never registered, an unpatched tier, or no route to Gitea to check for a newer bundle |
| `✗` | broken. `doctor` exits `1` if any row is `✗`, so it can be run from a monitoring script; a `⚠` never causes that |
The distinction on the last row is worth spelling out: **shard not running** is a `⚠` (start it),
while **shard running and not dialed in** is a `✗` — that is the silent failure this whole guide
warns about, where ServUO reports a clean boot over a script build that failed.
### `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.
**Anything it does overwrite is copied first.** Every `.cs` file the overlay owns is replaced
unconditionally — that is deliberate, they are code — so if you have edited one, the run saves your
copy under `backups/<timestamp>/` in the state directory before writing, alongside `sidecar.toml`
and any stock ServUO file the patch tier is about to touch. Each backup carries a `manifest.json`
saying where every file came from. The newest three are kept; `--no-backup` skips taking one.
Putting a file back is yours to do — the installer will not restore an old file over a newer
release, because it cannot know what has changed since. A run that overwrites nothing takes no
backup, so a no-op `update` leaves nothing behind.
It updates the ServUO tree `install.json` names — not a tree it detects — and it needs the shard
stopped, exactly as `install` does. There is nothing to update on a host that was never installed;
it says so rather than performing a first install under a verb that promises to preserve.
**Your auth token is not reprinted.** It has not changed and your website already has it. The one
thing an update can change that the site must be told about is the **protocol version**, and it says
so plainly when that happens — a stale number in Admin → Shard is answered with `409` and looks
exactly like your shard going offline.
**The patch tier under `update`:** features you already have are re-checked against the new release
(normally nothing to do), without asking you again — you consented when they were installed, and
that includes a shard where the tier ran unsupported. Features you never took are **named, not
applied**; run `update --patches` (or `install --patches`) to take one up. A shard that declined the
tier stays unpatched through every update.
### `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` |
| **Kept** | `sidecar.toml`, `uo-link.db`, the cached patch set with its pre-patch originals, and every backup an upgrade took (`--purge` drops all of them) |
| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete — with any file you have edited since deployment flagged, so you do not delete your own work by mistake |
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert — with how each landed, since one placed into a file you had already modified is worth a closer look. The pre-patch copy kept under `patches/originals/` is there to diff against. |
It lists all of that **before** asking, and the prompt defaults to **no**. `--yes` proceeds, which is
what an unattended uninstall needs; nothing else about the command is destructive to your shard,
which is neither stopped nor started.
The report is also written to a file — `runicgateway-uninstall-<timestamp>.txt` in the directory you
ran the command from — so it survives the scrollback. That is why the cached patches and the
originals stay behind by default: they are the only offline record of what the tier changed once the
release tarball is gone, and the report tells you to diff against them.
---
## 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](#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 <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](#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: `<servuo>` 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/. <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):
```bash
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 that some core files are CRLF while others are LF: use `patch --binary`.
**If `git apply` refuses a patch whose target region is visibly untouched, line endings are the
usual cause** — the `.patch` files and their targets do not all use the same ones, and `git apply`
compares them literally. The installer's own tier normalizes line endings and trailing whitespace
for the *comparison* while writing back your file's own endings, which is why it can place patches
`git apply` rejects. Running the installer is the easier route here.
### A3. Install the sidecar
On an arm64 host substitute `uo-link-sidecar-linux-aarch64` for the asset name below (`uname -m`
says `aarch64`); releases from v1.2.0 carry both. Take the version from the bundle you fetched in
A1 rather than the one written here.
```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"
# 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](#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 <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](#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 |

1319
installer/PLAN.md Normal file

File diff suppressed because it is too large Load Diff

37
installer/PROJECT_TREE.md Normal file
View File

@@ -0,0 +1,37 @@
# Runic Gateway installer — Project Tree
> **Auto-generated.** This file is maintained by the `sync-project-tree` CI workflow in
> the [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) repository, which
> opens a pull request here whenever the tracked file layout on `main` changes. Do not edit
> by hand — changes will be overwritten by the next sync.
A snapshot of the tracked files in the repository (build output, dependencies, and other
git-ignored paths are excluded).
```text
installer/
├── .gitea/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── config.yaml
│ │ └── feature_request.md
│ ├── scripts/
│ │ └── gen_tree.py
│ ├── workflows/
│ │ ├── bundle.yml
│ │ ├── pr-checks.yml
│ │ ├── release.yml
│ │ └── sync-project-tree.yml
│ └── PULL_REQUEST_TEMPLATE.md
├── bundles/
│ ├── bundle-2026.08.04.json
│ ├── current.json
│ └── README.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── CONTRIBUTORS.md
├── LICENSE.md
├── README.md
└── SECURITY.md
```

View File

@@ -23,7 +23,31 @@ Every route **except `GET /health`** requires the shared token from `sidecar.tom
| REST | `X-Api-Key: <token>` |
| WebSocket | `?token=<token>` in the connect URL (browsers can't set headers on a WS handshake) |
Missing or wrong token → **401** `{"error":"missing or invalid auth token"}`. The token is compared in constant time. It is generated automatically on first run (the sidecar logs it); rotate by editing `sidecar.toml` and restarting.
Missing or wrong token → **401** `{"error":"missing or invalid auth token"}`. The token is compared in constant time. It is generated automatically on first run; rotate by editing `sidecar.toml` and restarting.
To read it back afterwards, ask the sidecar rather than hunting through the startup log or the TOML:
```console
$ uo-link-sidecar --print-config --config /etc/runicgateway/sidecar.toml
{
"component": "uo-link-sidecar",
"config_created": false,
"config_path": "/etc/runicgateway/sidecar.toml",
"protocol": 3,
"shard": { "bind": "127.0.0.1:7788" },
"store": { "path": "/var/lib/runicgateway/uo-link.db" },
"token_generated": false,
"version": "0.1.0",
"web": {
"auth_required": true,
"auth_token": "c0f04ace…",
"bind": "127.0.0.1:8080",
"ws_path": "/ws"
}
}
```
That is the same set of values Admin → Shard asks for — base URL and WS URL are `web.bind` (substituting a reachable host if it is `0.0.0.0`) plus `web.ws_path`. The output **contains the token in clear text**, so treat it as a secret: it belongs in a terminal, not in a log or a CI artifact. `--print-config` also performs first-run setup, writing the config file and generating a token if there is none, and reports whether it did via `config_created` / `token_generated`.
---

View File

@@ -18,12 +18,14 @@ link/
│ ├── scripts/
│ │ └── gen_tree.py
│ ├── workflows/
│ │ ├── pr-checks.yml
│ │ ├── release.yml
│ │ ├── sonarqube.yml
│ │ └── sync-project-tree.yml
│ └── PULL_REQUEST_TEMPLATE.md
├── sidecar/
│ ├── src/
│ │ ├── cli.rs
│ │ ├── config.rs
│ │ ├── main.rs
│ │ ├── rpc.rs

View File

@@ -164,6 +164,7 @@ website/
│ │ │ ├── heroLayout.js
│ │ │ ├── shardEvents.js
│ │ │ ├── useAsync.js
│ │ │ ├── useShardFeatures.js
│ │ │ └── useShardFeed.js
│ │ ├── routes/
│ │ │ ├── admin/
@@ -190,6 +191,8 @@ website/
│ │ │ │ │ ├── SettingsAdmin.jsx
│ │ │ │ │ ├── ShardAdmin.jsx
│ │ │ │ │ ├── ShardOps.jsx
│ │ │ │ │ ├── ShardVisibility.jsx
│ │ │ │ │ ├── SpawnAtlas.jsx
│ │ │ │ │ ├── UserDetail.jsx
│ │ │ │ │ ├── UserEditor.jsx
│ │ │ │ │ ├── UsersAdmin.jsx
@@ -213,17 +216,23 @@ website/
│ │ │ │ └── ResetPassword.jsx
│ │ │ ├── public/
│ │ │ │ ├── About.jsx
│ │ │ │ ├── Atlas.jsx
│ │ │ │ ├── AtlasCreature.jsx
│ │ │ │ ├── ChampSpawns.jsx
│ │ │ │ ├── CmsPage.jsx
│ │ │ │ ├── FiveOnFriday.jsx
│ │ │ │ ├── Governors.jsx
│ │ │ │ ├── Guilds.jsx
│ │ │ │ ├── Houses.jsx
│ │ │ │ ├── Leaderboards.jsx
│ │ │ │ ├── Maintenance.jsx
│ │ │ │ ├── Market.jsx
│ │ │ │ ├── MarketVendor.jsx
│ │ │ │ ├── News.jsx
│ │ │ │ ├── Newsletter.jsx
│ │ │ │ ├── NewsletterIssue.jsx
│ │ │ │ ├── Portal.jsx
│ │ │ │ ├── Rules.jsx
│ │ │ │ ├── Screenshots.jsx
│ │ │ │ ├── Shard.jsx
│ │ │ │ ├── ShardActivity.jsx
@@ -257,9 +266,12 @@ website/
│ └── sonar-test-reporter.mjs
├── server/
│ ├── db/
│ │ ├── data/
│ │ │ └── spawnAtlas.art.example.json
│ │ ├── schema.sql
│ │ └── seed.js
│ ├── scripts/
│ │ ├── importSpawnAtlas.js
│ │ └── routeManifest.js
│ ├── src/
│ │ ├── auth/
@@ -365,15 +377,27 @@ website/
│ │ │ ├── settings/
│ │ │ │ ├── settings.db.js
│ │ │ │ └── settings.model.js
│ │ │ ├── shardAtlas/
│ │ │ │ ├── shardAtlas.db.js
│ │ │ │ └── shardAtlas.model.js
│ │ │ ├── shardClilocs/
│ │ │ │ ├── shardClilocs.db.js
│ │ │ │ └── shardClilocs.model.js
│ │ │ ├── shardEvents/
│ │ │ │ ├── shardEvents.db.js
│ │ │ │ └── shardEvents.model.js
│ │ │ ├── shardLinks/
│ │ │ │ ├── shardLinks.db.js
│ │ │ │ └── shardLinks.model.js
│ │ │ ├── shardMarket/
│ │ │ │ ├── shardMarket.db.js
│ │ │ │ └── shardMarket.model.js
│ │ │ ├── shardState/
│ │ │ │ ├── shardState.db.js
│ │ │ │ └── shardState.model.js
│ │ │ ├── shardVisibility/
│ │ │ │ ├── shardVisibility.db.js
│ │ │ │ └── shardVisibility.model.js
│ │ │ ├── trustedDevices/
│ │ │ │ ├── trustedDevices.db.js
│ │ │ │ └── trustedDevices.model.js
@@ -418,7 +442,10 @@ website/
│ │ │ │ │ ├── posts.router.js
│ │ │ │ │ ├── settings.router.js
│ │ │ │ │ ├── shard.router.js
│ │ │ │ │ ├── shardAtlas.controller.js
│ │ │ │ │ ├── shardClilocs.controller.js
│ │ │ │ │ ├── shardOps.controller.js
│ │ │ │ │ ├── shardVisibility.controller.js
│ │ │ │ │ ├── uoLink.controller.js
│ │ │ │ │ ├── uoLink.router.js
│ │ │ │ │ ├── uploads.router.js
@@ -457,6 +484,8 @@ website/
│ │ │ │ │ ├── shard.controller.js
│ │ │ │ │ └── shard.router.js
│ │ │ │ ├── public/
│ │ │ │ │ ├── atlas.controller.js
│ │ │ │ │ ├── atlas.router.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── pages.router.js
│ │ │ │ │ ├── posts.router.js
@@ -474,6 +503,8 @@ website/
│ │ │ ├── auth.js
│ │ │ ├── botInternalClient.js
│ │ │ ├── botInternalKey.js
│ │ │ ├── clilocParse.js
│ │ │ ├── clilocSource.js
│ │ │ ├── db.js
│ │ │ ├── logger.js
│ │ │ ├── mailer.js
@@ -484,6 +515,9 @@ website/
│ │ │ ├── shardBroadcast.js
│ │ │ ├── shardIngest.js
│ │ │ ├── shardSales.js
│ │ │ ├── shardVisibility.js
│ │ │ ├── spawnAtlasParse.js
│ │ │ ├── spawnAtlasSource.js
│ │ │ ├── totp.js
│ │ │ ├── trustProxy.js
│ │ │ ├── uoLinkClient.js
@@ -502,11 +536,14 @@ website/
│ │ ├── appeals.pure.test.js
│ │ ├── appeals.test.js
│ │ ├── appLinks.test.js
│ │ ├── atlasController.test.js
│ │ ├── authController.test.js
│ │ ├── authMe.test.js
│ │ ├── authTrustedDevice.test.js
│ │ ├── botInternalKey.test.js
│ │ ├── botScore.test.js
│ │ ├── clilocParse.test.js
│ │ ├── clilocSource.test.js
│ │ ├── csp.test.js
│ │ ├── emailConfig.model.test.js
│ │ ├── honeypot.test.js
@@ -540,11 +577,19 @@ website/
│ │ ├── secretBox.test.js
│ │ ├── selfTrustedDevices.test.js
│ │ ├── session.test.js
│ │ ├── shardBroadcast.visibility.test.js
│ │ ├── shardControllerPublic.test.js
│ │ ├── shardIngest.champsPages.test.js
│ │ ├── shardIngest.market.test.js
│ │ ├── shardIngest.points.test.js
│ │ ├── shardIngest.protocol2.test.js
│ │ ├── shardIngest.ruleset.test.js
│ │ ├── shardMarket.model.test.js
│ │ ├── shardState.governorTerms.test.js
│ │ ├── shardState.model.test.js
│ │ ├── shardVisibility.test.js
│ │ ├── spawnAtlas.parse.test.js
│ │ ├── spawnAtlas.source.test.js
│ │ ├── ssoCallback.test.js
│ │ ├── ssoState.test.js
│ │ ├── ssoTrustedDevice.test.js
@@ -553,6 +598,11 @@ website/
│ │ ├── trustProxy.test.js
│ │ ├── uoLinkClient.test.js
│ │ └── usernamePolicy.test.js
│ ├── tools/
│ │ └── cliloc-export/
│ │ ├── clilocexport.csproj
│ │ ├── Program.cs
│ │ └── README.md
│ ├── .env.example
│ ├── package-lock.json
│ ├── package.json