feat(rust): --game rust, named instances, and schema-2 bundles (phase 18) #28

Merged
whitlocktech merged 1 commits from feat/phase-18-rust into edge 2026-09-26 05:33:53 +00:00
Member

Step 5 of the phase-18 build order (docs/modules/rust/PLAN.md §34.2.7), into edge. It is walked (§34.3) against the real Rust bundle once Rust-Plugins#13 and Rust-Link#13 have cut over, and only then goes to main, which releases the installer.

Bundles (D146, D147)

  • ServUO is read at schema 2 from v2/servuo/ and lowered into the schema-1 model, so every ServUO path and install.json are unchanged.
    • Schema 1 at the root is the fallback, so a --bundle pin from before schema 2 still reproduces.
    • A test proves the published v2/servuo/bundle-2026.09.15.json lowers to exactly what schema 1 says for the same matrix.
  • Rust bundles are read from v2/rust/ and checked: the protocol agrees across bundle, sidecar and plugin, and the payload kind is plugin.
  • v2 reads go through the contents API. The web /raw/ route answers Cache-Control: public, max-age=21600.

--game rust (D148, D149, D153), in src/rustgame/:

  • The framework is detected from RustDedicated_Data/Managed/Oxide.Rust.dll or carbon/managed/Carbon.dll, both read off the rigs first. Both, or neither, is refused.
  • --server-id names an instance, which gets its own:
    • service: runicgateway-rust@<id> (a systemd template) or RunicGatewayRust-<id>;
    • config and database;
    • game port (from 7799) and web port (from 8090, or --web-port).
  • Plugin config: written once, holding only ServerId and Port. An existing one is never rewritten, and one naming another server refuses the run with both ids named.
  • Sidecar config: each instance's sidecar.toml is written once, with its ports and an absolute database path. The sidecar then generates the token into it.
  • Paths:
    • Linux: /etc/runicgateway/rust/<id>.toml.
    • Windows: %ProgramData%\RunicGateway\rust\<id>\sidecar.toml. It is a directory per instance because a Windows service logs beside its config.
  • Record: rust/install.json, separate from ServUO's.
  • update moves every instance, since they share one binary. A replaced binary restarts all of them. update --server-id is refused with the reason.
  • doctor checks, per instance:
    • the framework, and the plugin file against its recorded hash;
    • the plugin config's ServerId;
    • the required uMod plugins (a warning);
    • the service;
    • /health, the protocol, and plugin connected. A stopped server is a warning, and a silent plugin on a running server is a failure, with the floors printed.
  • uninstall removes the service and our plugin, and keeps the plugin config. --purge also removes the sidecar config and database. Removing the last instance also removes the binary, the template and the record. The shared runicgateway user is removed only if no ServUO record remains.
  • service.rs now takes the service name as a parameter internally. The ServUO public API is unchanged.

For your review

  1. Carbon's directories come from the plan's correction, not the plan. §34.2.3 says a directory moved in carbon/config.json is honoured. On 2.0.259, the file on the rig has no folder keys at all, so there is nothing there to honour. The installer uses carbon/plugins and carbon/configs, and the source says why.
  2. The installer writes each instance's sidecar.toml. It sets the ports and the database path and leaves the token blank, and the sidecar still generates the token. The plan had the template unit carry the database path. With several instances, one file per instance holding everything is the single source, and it works the same on Windows, where a per-service environment isn't available.
  3. The shared service user. Every instance runs as runicgateway, which ServUO's sidecar also uses, so no single instance's removal may delete it.

Tested

  • Checks: Windows native and Linux (Docker) both pass fmt, clippy -D warnings and 175 tests.

  • Against a mock Gitea serving a real Rust bundle (the real rust-link-sidecar.exe and musl binary, and the real plugin tarball), via an uncommitted, reverted edit to the base URL.

  • Windows, relocated:

    • --verify;
    • alpha on an Oxide root and beta on a Carbon root, getting 7799/8090 and 7800/8091;
    • a rerun, which recorded "unchanged";
    • both conflicts refused;
    • doctor with alpha's sidecar running: reachable, protocol 12, "server not running";
    • a hand-edited plugin failed doctor, and update restored it;
    • uninstall --server-id beta left alpha;
    • uninstall --purge removed everything except the plugin config.
  • Linux, relocated: alpha.toml at mode 600, and the template unit reads RUSTLINK_CONFIG=…/%i.toml.

  • Not yet tested: real systemd and SCM registration. That is walk steps 2 to 4, on the published bundle.

  • AI-assisted: Claude Code (Claude Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Step 5 of the phase-18 build order (docs/modules/rust/PLAN.md §34.2.7), into `edge`. It is walked (§34.3) against the real Rust bundle once Rust-Plugins#13 and Rust-Link#13 have cut over, and only then goes to `main`, which releases the installer. **Bundles (D146, D147)** - **ServUO** is read at **schema 2** from `v2/servuo/` and *lowered* into the schema-1 model, so every ServUO path and `install.json` are unchanged. - Schema 1 at the root is the fallback, so a `--bundle` pin from before schema 2 still reproduces. - A test proves the published `v2/servuo/bundle-2026.09.15.json` lowers to exactly what schema 1 says for the same matrix. - **Rust** bundles are read from `v2/rust/` and checked: the protocol agrees across bundle, sidecar and plugin, and the payload kind is `plugin`. - v2 reads go through the **contents API**. The web `/raw/` route answers `Cache-Control: public, max-age=21600`. **`--game rust` (D148, D149, D153)**, in `src/rustgame/`: - **The framework is detected** from `RustDedicated_Data/Managed/Oxide.Rust.dll` or `carbon/managed/Carbon.dll`, both read off the rigs first. Both, or neither, is refused. - **`--server-id` names an instance**, which gets its own: - service: `runicgateway-rust@<id>` (a systemd template) or `RunicGatewayRust-<id>`; - config and database; - game port (from 7799) and web port (from 8090, or `--web-port`). - **Plugin config:** written once, holding only `ServerId` and `Port`. An existing one is never rewritten, and one naming another server refuses the run with both ids named. - **Sidecar config:** each instance's `sidecar.toml` is written once, with its ports and an absolute database path. The sidecar then generates the token into it. - **Paths:** - Linux: `/etc/runicgateway/rust/<id>.toml`. - Windows: `%ProgramData%\RunicGateway\rust\<id>\sidecar.toml`. It is a directory per instance because a Windows service logs beside its config. - **Record:** `rust/install.json`, separate from ServUO's. - **`update`** moves every instance, since they share one binary. A replaced binary restarts all of them. `update --server-id` is refused with the reason. - **`doctor`** checks, per instance: - the framework, and the plugin file against its recorded hash; - the plugin config's `ServerId`; - the required uMod plugins (a warning); - the service; - `/health`, the protocol, and *plugin connected*. A stopped server is a warning, and a silent plugin on a running server is a failure, with the floors printed. - **`uninstall`** removes the service and our plugin, and keeps the plugin config. `--purge` also removes the sidecar config and database. Removing the last instance also removes the binary, the template and the record. The shared `runicgateway` user is removed only if no ServUO record remains. - `service.rs` now takes the service name as a parameter internally. The ServUO public API is unchanged. **For your review** 1. **Carbon's directories come from the plan's correction, not the plan.** §34.2.3 says a directory moved in `carbon/config.json` is honoured. On 2.0.259, the file on the rig has no folder keys at all, so there is nothing there to honour. The installer uses `carbon/plugins` and `carbon/configs`, and the source says why. 2. **The installer writes each instance's `sidecar.toml`.** It sets the ports and the database path and leaves the token blank, and the sidecar still generates the token. The plan had the template unit carry the database path. With several instances, one file per instance holding everything is the single source, and it works the same on Windows, where a per-service environment isn't available. 3. **The shared service user.** Every instance runs as `runicgateway`, which ServUO's sidecar also uses, so no single instance's removal may delete it. **Tested** - **Checks:** Windows native and Linux (Docker) both pass fmt, clippy `-D warnings` and 175 tests. - **Against a mock Gitea** serving a real Rust bundle (the real `rust-link-sidecar.exe` and musl binary, and the real plugin tarball), via an uncommitted, reverted edit to the base URL. - **Windows, relocated:** - `--verify`; - `alpha` on an Oxide root and `beta` on a Carbon root, getting 7799/8090 and 7800/8091; - a rerun, which recorded "unchanged"; - both conflicts refused; - `doctor` with alpha's sidecar running: reachable, protocol 12, "server not running"; - a hand-edited plugin failed `doctor`, and `update` restored it; - `uninstall --server-id beta` left alpha; - `uninstall --purge` removed everything except the plugin config. - **Linux, relocated:** `alpha.toml` at mode 600, and the template unit reads `RUSTLINK_CONFIG=…/%i.toml`. - **Not yet tested:** real systemd and SCM registration. That is walk steps 2 to 4, on the published bundle. - [x] AI-assisted: Claude Code (Claude Opus 5.5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-09-26 04:40:36 +00:00
feat(rust): --game rust, named instances, and schema-2 bundles (phase 18)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m7s
7027a78a23
Module-rust phase 18, step 5 of docs/modules/rust/PLAN.md §34.2.7 (D146,
D148, D149, D153).

Bundles: ServUO is read at schema 2 from v2/servuo/ and lowered into the
schema-1 model. Schema 1 at the root is the fallback, so a pin from before
schema 2 still reproduces. Rust bundles are read from v2/rust/. v2 reads use
the contents API, because /raw/ is CDN-cached for six hours.

--game rust runs install, update, doctor and uninstall for Rust servers
(src/rustgame/):
- the framework is detected from its marker files, which were read off both
  rigs; both or neither is refused;
- --server-id names an instance: its own service (runicgateway-rust@<id>, or
  RunicGatewayRust-<id>), config, database and ports;
- the plugin config is written once, with ServerId and Port only. An existing
  one is never rewritten, and one naming another server refuses the run;
- each instance's sidecar.toml is written once with its ports and an absolute
  database path, and the sidecar generates the token into it;
- one binary per host. update moves every instance, and a replaced binary
  restarts all of them;
- doctor checks the plugin file hash, the plugin config's ServerId, the
  required uMod plugins (a warning), the service and /health, and passes when
  the plugin is connected;
- uninstall removes our plugin and keeps its config. --purge also removes the
  sidecar config and database. The last instance takes the binary, the
  template and the record, and the shared user only when no ServUO record
  remains.

service.rs takes the service name as a parameter internally. The ServUO
public API is unchanged.

Finding: Carbon 2.0.259's config.json has no folder keys, so carbon/plugins
and carbon/configs are what the installer uses. The plan expected a moved
directory to be readable there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit 0fad3e3b2d into edge 2026-09-26 05:33:53 +00:00
whitlocktech deleted branch feat/phase-18-rust 2026-09-26 05:33:54 +00:00
Sign in to join this conversation.
No description provided.