From c1957f0bfbc2229a09fab11e5e8ef07c24272cef Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 4 Aug 2026 10:48:44 -0500 Subject: [PATCH] =?UTF-8?q?docs(installer):=20record=20Phase=200.2=20?= =?UTF-8?q?=E2=80=94=20the=20sidecar's=20CLI=20and=20settled=20data=20path?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- installer/PLAN.md | 73 ++++++++++++++++++++++++++++++++++++++++----- link/INTEGRATION.md | 26 +++++++++++++++- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/installer/PLAN.md b/installer/PLAN.md index a0f9ec0..fd10c38 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -7,11 +7,11 @@ not match how `servuo-plugins` actually ships (see | 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.2 `link` installable (data paths + `--print-config`) | ⬜ Not started | -| 0.3 Bundle CI in the installer repo | ⬜ Not started | +| 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) | +| 0.2 `link` installable (data paths + `--print-config`) | 🟨 In review — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) | +| 0.3 Bundle CI in the installer repo | ⬜ Not started — **next**; both components it composes now exist | | 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` waits on the shape settling | -| — 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/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. -Under `C:\Program Files\` that fails or silently lands in VirtualStore. The service definitions must -pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly: +**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. Phase 0.2 fixed the second +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 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. +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 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 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. + + 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 `, `--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 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 @@ -279,7 +318,10 @@ Repo work that must land before an installer can exist. - Windows: `%ProgramFiles%\RunicGateway\`, data in `%ProgramData%\RunicGateway\`, service registration with automatic start and restart-on-failure. - 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 ` **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) @@ -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 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 (protocol )`, 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, 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. ``` +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 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 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 diff --git a/link/INTEGRATION.md b/link/INTEGRATION.md index a9fa80d..147f2f1 100644 --- a/link/INTEGRATION.md +++ b/link/INTEGRATION.md @@ -23,7 +23,31 @@ Every route **except `GET /health`** requires the shared token from `sidecar.tom | REST | `X-Api-Key: ` | | WebSocket | `?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`. ---