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>
This commit is contained in:
2026-08-04 15:55:52 +00:00
2 changed files with 90 additions and 9 deletions

View File

@@ -7,11 +7,11 @@ not match how `servuo-plugins` actually ships (see
| Phase 0 item | State | | Phase 0 item | State |
|---|---| |---|---|
| 0.1 `servuo-plugins` release workflow | 🟨 In review — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) | | 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) |
| 0.2 `link` installable (data paths + `--print-config`) | ⬜ Not started | | 0.2 `link` installable (data paths + `--print-config`) | 🟨 In review — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) |
| 0.3 Bundle CI in the installer repo | ⬜ Not started | | 0.3 Bundle CI in the installer repo | ⬜ Not started **next**; both components it composes now exist |
| 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` waits on the shape settling | | 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` waits on the shape settling |
| — Repo bootstrap (governance + CI) | 🟨 [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows in [installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1) | | — Repo bootstrap (governance + CI) | [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) |
--- ---
@@ -114,9 +114,13 @@ Two files, two owners:
| `/etc/runicgateway/sidecar.toml` | uo-link | The sidecar's own schema, unchanged. Service sets `UOLINK_CONFIG` to this path | | `/etc/runicgateway/sidecar.toml` | uo-link | The sidecar's own schema, unchanged. Service sets `UOLINK_CONFIG` to this path |
| `/etc/runicgateway/install.json` | installer | Deployed versions, file hashes, applied patches, ServUO path, timestamps | | `/etc/runicgateway/install.json` | installer | Deployed versions, file hashes, applied patches, ServUO path, timestamps |
**Working-directory trap:** the sidecar writes both `sidecar.toml` and `uo-link.db` relative to CWD. **Working-directory trap:** the sidecar wrote both `sidecar.toml` and `uo-link.db` relative to CWD.
Under `C:\Program Files\` that fails or silently lands in VirtualStore. The service definitions must Under `C:\Program Files\` that fails or silently lands in VirtualStore. Phase 0.2 fixed the second
pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly: 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:
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated - Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
service user service user
@@ -131,6 +135,12 @@ becomes write-only. This is the largest "I installed it and nothing happened" fa
The installer closes it by printing a copy-paste block at the end of a successful run — see §6. The installer closes it by printing a copy-paste block at the end of a successful run — see §6.
Phase 0.2 supplied the missing half of that: `uo-link-sidecar --print-config` provisions the config
if absent and prints the resolved settings — token, both binds, `ws_path`, protocol version, db
path — as JSON. The installer reads the block it prints out of that one call. **It never parses the
log**, which was the alternative and would have made the handoff depend on a log format that is not
a contract.
### 2.5 `deploy.ps1` cannot be the cross-platform deployer ### 2.5 `deploy.ps1` cannot be the cross-platform deployer
It is PowerShell-only; a Linux ServUO host running .NET typically has no `pwsh`. It also hard-throws It is PowerShell-only; a Linux ServUO host running .NET typically has no `pwsh`. It also hard-throws
@@ -255,6 +265,35 @@ Repo work that must land before an installer can exist.
2. **`link`: make the sidecar installable.** Confirm/settle default data paths, and add a way to 2. **`link`: make the sidecar installable.** Confirm/settle default data paths, and add a way to
read back config non-interactively (e.g. `--print-config` emitting JSON: bind addresses, token, read back config non-interactively (e.g. `--print-config` emitting JSON: bind addresses, token,
protocol version, db path) so the installer does not have to scrape logs for the token. protocol version, db path) so the installer does not have to scrape logs for the token.
As built ([link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24)) — the sidecar
had **no CLI at all** before this, so the shape was chosen rather than inherited:
- **Four flags, hand-rolled:** `--print-config`, `--config <PATH>`, `--version`, `--help`. No
argument-parsing crate — it would be larger than the code it replaced — and deliberately no
flags that duplicate a config key, so `sidecar.toml` stays the single place settings live.
An unrecognized argument exits `2`; silently ignoring a typo'd flag would start a sidecar that
is not the one the installer asked for.
- **`--print-config` performs first-run setup rather than only reporting.** It runs the same
load path a normal start does, so a missing config file is written and a blank token is
generated and saved. That collapses "provision the sidecar" and "find out its token" into one
non-interactive call — which is exactly the sequence §6 needs. `config_created` and
`token_generated` say whether *this* run did either, because the values alone cannot
distinguish a fresh install from a re-read of an existing one, and a re-run must not report a
token as newly minted.
- **The document is the whole of stdout.** The log subscriber writes to stdout, so it is not
started in this mode. `ws_path` is emitted from the same constant the route is registered
with, so the installer's WebSocket URL cannot drift from the server's.
- **Relative `[store].path` now anchors to the config file's directory, not the CWD** — see
§2.3, which this half-closes on the sidecar side. Absolute paths are used as written; parent
directories are created; `:memory:` and `file:` URIs are left alone.
- **The db path is handed to sqlx as a path, not a `sqlite://` URL.** The URL spelling is
parsed as one: it percent-decodes the path and splits it on `?`, so an installed path
containing `%20` opened a different file than the operator named.
- **No platform data directories are compiled in.** That is the "settle" half of this item, and
the answer is that the *installer* owns layout (§2.3) and pins `UOLINK_CONFIG` /
`UOLINK_DB_PATH` in the service definition. Baking `/etc` and `%ProgramData%` defaults into
the binary would give the same paths two owners and break `cargo run` in a working tree.
3. **Bundle CI in the installer repo** (§7). Compose job (read both repos' latest releases → run the 3. **Bundle CI in the installer repo** (§7). Compose job (read both repos' latest releases → run the
two gates → publish `bundle.json`), the nightly cron, and the dispatch step appended to each two gates → publish `bundle.json`), the nightly cron, and the dispatch step appended to each
component's release workflow. This must exist before Phase 1 is useful, since the installer component's release workflow. This must exist before Phase 1 is useful, since the installer
@@ -279,7 +318,10 @@ Repo work that must land before an installer can exist.
- Windows: `%ProgramFiles%\RunicGateway\`, data in `%ProgramData%\RunicGateway\`, service - Windows: `%ProgramFiles%\RunicGateway\`, data in `%ProgramData%\RunicGateway\`, service
registration with automatic start and restart-on-failure. registration with automatic start and restart-on-failure.
- Both: `UOLINK_CONFIG` and `UOLINK_DB_PATH` pinned in the service definition (§2.3). - Both: `UOLINK_CONFIG` and `UOLINK_DB_PATH` pinned in the service definition (§2.3).
- Token surfacing (§6). - Token surfacing (§6): run the installed binary once as
`uo-link-sidecar --print-config --config <the pinned path>` **before** registering the service.
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.
### Phase 3 — patch tier (opt-in) ### Phase 3 — patch tier (opt-in)
@@ -304,6 +346,12 @@ degrade loudly rather than silently.
The last check matters most: it is the only thing that distinguishes "files copied" from "the bridge The last check matters most: it is the only thing that distinguishes "files copied" from "the bridge
actually works" (§2.1). actually works" (§2.1).
Three of those rows are answered by the sidecar's own CLI rather than by inspecting the filesystem:
`--version` prints `uo-link-sidecar <ver> (protocol <n>)`, and `--print-config` gives the config and
db paths the *installed service* resolves — so `doctor` reports what the binary would actually do,
not what `install.json` believes it was told to do. The protocol row compares that number against
the overlay manifest's declared one (§7.0).
`runicgateway update` — resolves the current bundle (§7.1), then acts asymmetrically by component, `runicgateway update` — resolves the current bundle (§7.1), then acts asymmetrically by component,
deliberately: deliberately:
@@ -355,11 +403,20 @@ Paste these into Admin → Shard on your Runic Gateway site:
The token is write-only once saved — the site will never show it back to you. The token is write-only once saved — the site will never show it back to you.
``` ```
Every value in that block except the host and the site URL comes from one
`uo-link-sidecar --print-config` call (§2.4): `web.auth_token`, `protocol`, and `web.bind` +
`web.ws_path` for the two URLs. Only the **host** is substituted — `web.bind` is frequently
`0.0.0.0`, which is not something to hand a website — so the installer composes the URLs from the
host it detects or prompts for, rather than echoing the bind address.
The installer prompts for the site URL only to build that link; it never contacts the website. A The installer prompts for the site URL only to build that link; it never contacts the website. A
future "installer registers itself with the website" flow (claim code + authenticated endpoint) is future "installer registers itself with the website" flow (claim code + authenticated endpoint) is
explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added
later without changing anything here. later without changing anything here.
The printed token is a secret in transit: `--print-config` output must go to the operator's
terminal and the config file, never into an installer log file or a support bundle.
--- ---
## 7. Version tracking, the bundle, and release orchestration ## 7. Version tracking, the bundle, and release orchestration

View File

@@ -23,7 +23,31 @@ Every route **except `GET /health`** requires the shared token from `sidecar.tom
| REST | `X-Api-Key: <token>` | | REST | `X-Api-Key: <token>` |
| WebSocket | `?token=<token>` in the connect URL (browsers can't set headers on a WS handshake) | | 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`.
--- ---