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>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
# Runic Gateway Installer — plan
|
||||
|
||||
Status: **Phase 1 built, on `edge`.** Phase 0's prerequisites all landed, the installer repo
|
||||
Status: **Phases 1 and 2 built, on `edge`.** Phase 0's prerequisites all landed, the installer repo
|
||||
publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) specified the operator-facing run
|
||||
before the binary existed. The crate now exists and implements the installer core — bundle
|
||||
resolution, ServUO detection and validation, the overlay sync and `install.json` — on the
|
||||
`edge` branch, not `main`, so no half-capable binary is released (see
|
||||
[Phase 1 as built](#phase-1--installer-core)). **Phase 2 (uo-link install + service) is next.**
|
||||
before the binary existed. The crate now implements the installer core (bundle resolution, ServUO
|
||||
detection and validation, the overlay sync, `install.json` — [Phase 1 as
|
||||
built](#phase-1--installer-core)) and the sidecar half (binary, config, service, token handoff —
|
||||
[Phase 2 as built](#phase-2--uo-link-install-and-service)). Both are on the `edge` branch, not
|
||||
`main`, so no half-capable binary is released. **Phase 3 (the patch tier) is next, and the
|
||||
`edge → main` cutover follows it** rather than Phase 2: `INSTALL.md` §4 describes the tier as part
|
||||
of the run, and a first release whose every patch-tier answer is "not implemented" is the same
|
||||
half-capable binary that kept Phase 1 off `main`.
|
||||
This document is the design of record; it supersedes the informal overview it grew out of, which
|
||||
described a ServUO integration that does not match how `servuo-plugins` actually ships (see
|
||||
[Corrections](#corrections-to-the-original-overview)).
|
||||
@@ -199,13 +203,21 @@ Under `C:\Program Files\` that fails or silently lands in VirtualStore. Phase 0.
|
||||
half in the sidecar — a relative `[store].path` now resolves against the directory holding
|
||||
`sidecar.toml`, so pinning the config alone is enough to put the database somewhere deterministic —
|
||||
but the config path itself is still CWD-relative by default, and "deterministic" is not the same as
|
||||
"where this install wants it". The service definitions therefore still pin `UOLINK_CONFIG` and
|
||||
`UOLINK_DB_PATH` explicitly:
|
||||
"where this install wants it". The service definition therefore always pins the **config** path:
|
||||
|
||||
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
|
||||
service user
|
||||
- Windows: binary under `%ProgramFiles%\RunicGateway\`, **data under `%ProgramData%\RunicGateway\`**
|
||||
|
||||
**How each is pinned differs by platform, and Phase 2 settled it that way deliberately.** Linux's
|
||||
unit carries `Environment=UOLINK_CONFIG=` *and* `Environment=UOLINK_DB_PATH=`, because `/etc` and
|
||||
`/var/lib` are different directories and both need naming. Windows passes the config as `--config`
|
||||
inside the service's own `binPath`, and pins nothing else: config and data are both
|
||||
`%ProgramData%\RunicGateway`, so the sidecar's own anchoring rule already puts the database exactly
|
||||
where the table above says. The alternative on Windows is a **machine-wide** environment variable —
|
||||
`sc.exe` offers no per-service one — which every process on the host would inherit and which would
|
||||
outlive an uninstall. See [Phase 2 as built](#phase-2--uo-link-install-and-service).
|
||||
|
||||
### 2.4 The token handoff was missing entirely
|
||||
|
||||
The whole point is the website reaching the sidecar, and today that is manual and undocumented in
|
||||
@@ -545,6 +557,65 @@ process running out of the tree.
|
||||
That both writes the config the service will read and returns the token to print, so the service
|
||||
never starts against a config that does not exist yet.
|
||||
|
||||
**As built** ([installer#5](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/5)) —
|
||||
`src/sidecar.rs` (binary, config, handoff) and `src/service.rs` (systemd, Windows SCM), wired into
|
||||
the same `install` run. The decisions that were not already settled above:
|
||||
|
||||
- **Both platforms run the sidecar as a dedicated unprivileged identity.** Linux gets the system
|
||||
user this section already specified; Windows gets a **virtual service account**
|
||||
(`sc create … obj= "NT SERVICE\RunicGatewayLink"`), which the SCM creates itself and which has no
|
||||
password. Plain `sc create` would have run it as `LocalSystem` — the most privileged local
|
||||
identity there is, for a process that listens on two TCP ports while its Linux twin deliberately
|
||||
does not run as root. The account only exists *after* `sc create`, which fixes the order of the
|
||||
file permissions below.
|
||||
- **`sidecar.toml` is locked down, because it holds the token.** Neither default location protects
|
||||
it: `/etc` is world-readable and `%ProgramData%` grants `Users` read by inheritance, so an
|
||||
unprivileged local account could read the shard's auth token out of a stock install. Linux gets
|
||||
`chmod 600` plus `chown` to the service user; Windows gets `icacls /inheritance:r` down to SYSTEM
|
||||
and Administrators **before** registration, then a read grant for the service account after it
|
||||
exists. The database directory gets a separate write grant, since SQLite writes journal and WAL
|
||||
files beside the database.
|
||||
- **`--verify` runs no part of the sidecar half.** `--print-config` provisions — it writes the
|
||||
config and mints a token — so a dry run that called it would create exactly the state it claims
|
||||
not to. A `--verify` run reports what would be installed, reads no token, and prints no handoff.
|
||||
It also **carries the existing `link` section of `install.json` through untouched**, so a dry run
|
||||
on an installed host cannot make its service disappear from the record.
|
||||
- **The installed binary's protocol version is checked against the bundle, and a mismatch stops the
|
||||
run before the service is registered.** Gate 1 (§7.1) read that number from source at the release
|
||||
tag; this is the same check applied to the binary that will actually answer the website. The
|
||||
binary is left on disk — harmless without a service — rather than the run pretending to succeed.
|
||||
- **`RUNICGATEWAY_STATE_DIR` now relocates the sidecar binary too, and suppresses service
|
||||
registration.** Phase 1 left the binary path alone because nothing wrote it. A relocated run that
|
||||
still dropped a binary into `/usr/bin` and registered a system service would be exactly the
|
||||
half-in-the-real-system accident the variable exists to avoid — and there is no such thing as a
|
||||
relocated systemd unit or Windows service. Such a run also leaves file permissions alone, because
|
||||
hardening a scratch config against the only account that will ever read it just breaks the next
|
||||
test run.
|
||||
- **A host the installer cannot drive gets the recipe, not a failure or a weaker service.** No
|
||||
systemd (`/run/systemd/system` absent — the correct test, since `systemctl` is present in plenty
|
||||
of containers where PID 1 is not systemd), or a service user that cannot be created: the binary
|
||||
and config are still installed, `install.json` records `service: null`, and the run prints the
|
||||
exact unit text and commands. There is **no fallback to `User=root` or `LocalSystem`** — a service
|
||||
quietly running with more privilege than its own documentation promises is worse than one that was
|
||||
not registered. The printed Windows recipe states plainly whether the run locked the config down
|
||||
or the operator still has to.
|
||||
- **`install.json` never records the token.** The `link` section holds versions, the binary's hash,
|
||||
the config and database paths, and the service's name, unit path and account. The token goes to
|
||||
the terminal and to `sidecar.toml`, and the record is a support artifact people paste into bug
|
||||
reports.
|
||||
- **The service is stopped before its binary is replaced, and restarted rather than started
|
||||
afterwards.** On Windows the file is locked while the service runs (and `sc stop` returns as soon
|
||||
as the stop is *pending*, so the stop is polled, not slept on); on Linux the replacement is
|
||||
permitted but leaves the old code serving until something restarts it. `systemctl start` on an
|
||||
active unit is a no-op, which is precisely the wrong outcome after a replacement.
|
||||
|
||||
Verified on this machine end to end against a relocated layout: the bundle's Windows sidecar
|
||||
downloaded and checksum-verified, `--print-config` provisioning a fresh config and returning a
|
||||
token, the §6 handoff printed with the URLs composed from the host rather than the bind address, a
|
||||
second run reporting `unchanged` / `already present` and leaving `install.json` byte-identical, a
|
||||
`--verify` run over an installed host writing nothing and preserving the `link` section, and a
|
||||
tampered binary detected by hash and replaced with no stray staging file left behind.
|
||||
|
||||
### Phase 3 — patch tier (opt-in)
|
||||
|
||||
Everything in §2.2. Detect applicability, dry-run, apply, record, warn about the core rebuild, and
|
||||
@@ -825,18 +896,24 @@ mismatched pair from being published as a bundle — which is the mechanism that
|
||||
|
||||
## 8. Open questions
|
||||
|
||||
1. **Windows service mechanism** — `sc create` against the plain console binary (simplest, works
|
||||
today), a bundled WinSW/NSSM shim, or a native `--service` mode in the sidecar using the
|
||||
`windows-service` crate (cleanest, but changes `link`). Recommendation: `sc create` for v1,
|
||||
revisit if restart semantics prove inadequate.
|
||||
2. **Does the installer manage ServUO stop/start?** Currently it refuses while ServUO runs and tells
|
||||
1. **Does the installer manage ServUO stop/start?** Currently it refuses while ServUO runs and tells
|
||||
the operator to restart afterward. Offering to stop/start would be friendlier but means owning
|
||||
another shard's process lifecycle, and the shard's own start scripts vary.
|
||||
3. **Co-location assumption** — the shard dials out to the sidecar on loopback `127.0.0.1:7788`, so
|
||||
2. **Co-location assumption** — the shard dials out to the sidecar on loopback `127.0.0.1:7788`, so
|
||||
sidecar and ServUO must share a host. Should the installer support installing only uo-link on a
|
||||
different host, or hard-assume co-location?
|
||||
Resolved and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
|
||||
|
||||
**Resolved — Windows service mechanism** (was question 1). `sc create` against the plain console
|
||||
binary, as recommended: it works on a stock host, ships nothing extra, and needs no change to
|
||||
`link`. A WinSW/NSSM shim would be a third binary to keep current, and a native `--service` mode
|
||||
using the `windows-service` crate would put Windows service plumbing inside a component whose whole
|
||||
job is being platform-agnostic. Restart semantics turned out to be adequate —
|
||||
`sc failure … actions= restart/5000` is the direct counterpart of systemd's `Restart=on-failure` /
|
||||
`RestartSec=5`. What the recommendation did *not* anticipate is the service identity: plain
|
||||
`sc create` runs as `LocalSystem`, so Phase 2 registers with `obj= "NT SERVICE\RunicGatewayLink"`
|
||||
instead (see [Phase 2 as built](#phase-2--uo-link-install-and-service)).
|
||||
|
||||
**Resolved — branch targeting for the new repo** (was question 4). The v3 cutover landed:
|
||||
`servuo-plugins#6` merged, so that repo's `main` and `edge` agree at protocol 3. The release
|
||||
workflow targets `main`, and the installer repo starts clean on `main`. §7.4's caution still applies
|
||||
|
||||
Reference in New Issue
Block a user