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