# Bundles **These files are generated. Do not edit them by hand.** A *bundle* names one exact, protocol-checked combination of the two components the installer deploys for one game: a sidecar release and a game-side payload release. For ServUO that is `link` and a `servuo-plugins` overlay; for Rust it is `Rust-Link` and a `Rust-Plugins` plugin. The installer does not hardcode versions and does not resolve "latest" at run time; it fetches one of these documents and installs what it names. **The bundle is the compat matrix.** They are written by [`.gitea/workflows/bundle.yml`](../.gitea/workflows/bundle.yml) (the logic is in [`.gitea/scripts/compose-bundles.sh`](../.gitea/scripts/compose-bundles.sh)), which composes one whenever a component publishes a release (dispatched by that release's own workflow) and nightly, so a missed dispatch self-heals. A run that finds nothing changed writes nothing. See `docs/installer/PLAN.md` §7 for the design, and `docs/modules/rust/PLAN.md` §34.2.2 for schema 2. ## Where they live: the `bundles` branch **The JSON documents are not in this directory.** They are published to a branch of their own, [`bundles`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/bundles). There is one stream per game and schema, each a directory holding the same two kinds of file: | Directory | Stream | |---|---| | *(root)* | Schema 1, ServUO. What every installer up to v0.2.x reads. **Retires 2027-01-01** | | `v2/servuo/` | Schema 2, `"game": "servuo"` | | `v2/rust/` | Schema 2, `"game": "rust"` | | File | What it is | |---|---| | `current.json` | The bundle the installer uses by default. Always a copy of the newest `bundle-*.json`. | | `bundle-.json` | Every bundle ever published, kept forever so `--bundle ` stays reproducible. | **Schema 1 retires on 2027-01-01.** Until then it is composed exactly as before, beside schema 2, so every installer already in the field keeps updating. After that date it stops being composed and is left **frozen at its last bundle, never deleted**: an old installer still resolves something, and every `bundle-.json` stays pinnable. A ServUO matrix carries the **same tag** at both schemas. A schema-2 document names one game rather than both, because the games release on their own schedules: a document naming both would hand a ServUO host a new bundle every time a Rust plugin shipped. A game none of whose repos has released composes nothing, and that is not a failure. Tags are UTC dates — `2026.08.04`. A second bundle on the same day (a sidecar release in the morning, an overlay release in the afternoon) becomes `2026.08.04.2`, so one tag always names exactly one matrix. **Why a branch rather than `main`.** `main` is protected and this job is unattended: the pre-receive hook declines a push from CI, which is not something a nightly cron can resolve. A branch of its own keeps everything the original choice was for — a reviewable diff, a git history of the compat matrix, plain anonymous raw URLs, no credentials on the shard host — and needs no protection exception. Whitelisting a scheduled job for pushes to the default branch would buy nothing this does not. This directory keeps the documentation, because that is what belongs on `main`: the branch carries data, and only data. ## How the installer fetches these Plain anonymous `GET`s against a public repo. The shard host gets no git and no Gitea credentials (`PLAN.md` §1), so nothing here may require auth: ``` https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/current.json https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/bundle-2026.08.04.json https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/v2/rust/current.json ``` Bundles are committed rather than published as Gitea releases because this repo's *own* releases are the installer binaries, and `/releases/latest` returns whichever release is newest regardless of kind — interleaving the two would make "latest" intermittently resolve to a release containing no installer binary. ## Schema 1 (ServUO, retiring) `schema` is the version of *this document's* shape, and is unrelated to `protocol` (the uo-link wire protocol) or to either component's release version. All three move independently. ```jsonc { "schema": 1, "bundle": "2026.08.04", // this bundle's tag; what --bundle takes "generated": "2026-08-04T16:07:13Z", "protocol": 3, // the wire protocol both halves speak (gate 1 proved it) "link": { "repo": "RunicGateway/link", "tag": "v1.1.0", "version": "1.1.0", "protocol": 3, "assets": { // per-platform: the installer runs on each "linux-x86_64": { "name": "…", "url": "…", "sha256": "…" }, "linux-aarch64": { "name": "…", "url": "…", "sha256": "…" }, "windows-x86_64": { "name": "…", "url": "…", "sha256": "…" } } }, "overlay": { "repo": "RunicGateway/servuo-plugins", "tag": "v0.1.1", "version": "0.1.1", "commit": "3a52abb…", // recorded into install.json at deploy time "protocol": 3, "servuo": { "min_version": "57.4", // base overlay: only adds files "patches_verified_against": "57.4" // patch tier: skipped with a warning elsewhere }, "asset": { "name": "runicgateway-overlay-0.1.1.tar.gz", "url": "…", "sha256": "…" } } } ``` ### The two things worth knowing **`sha256` is load-bearing, not decorative.** Every artifact Runic Gateway publishes is deliberately **unsigned** (`PLAN.md` §3) — the checksum is the entire trust anchor. Each hash here was computed by CI from the asset it actually downloaded, *after* verifying it against the `SHA256SUMS` the publishing repo shipped beside it. The installer must verify every download against these values and refuse on a mismatch. A bundle whose hashes are trusted but never checked buys nothing. **`link.protocol` and `overlay.protocol` are always equal, and that is the point.** The sidecar rejects a protocol mismatch with `409` rather than mis-parsing, so a mismatched pair is a shard emitting into a void. CI refuses to publish one: it reads `PROTOCOL_VERSION` from the sidecar's source at its release tag and the declared `protocol` from the overlay tarball's `manifest.json`, and fails if they differ. The top-level `protocol` is that agreed value. ## Schema 2 The same document for either game: a `game` discriminant, a `sidecar`, and a `payload` whose `kind` says what it is. A reader switches on `game`, and refuses a `schema` it does not know. ```jsonc { "schema": 2, "game": "rust", // or "servuo" "bundle": "2026.09.27", "generated": "2026-09-27T…Z", "protocol": 12, "sidecar": { "repo": "RunicGateway/Rust-Link", "tag": "v…", "version": "…", "protocol": 12, "assets": { // Rust: no linux-aarch64 — RustDedicated has no arm64 build "linux-x86_64": { "name": "…", "url": "…", "sha256": "…" }, "windows-x86_64": { "name": "…", "url": "…", "sha256": "…" } }, "launcher": { "name": "with-sidecar.sh", "url": "…", "sha256": "…" } // Rust only: the egg's startup }, "payload": { "kind": "plugin", // "overlay" for ServUO "repo": "RunicGateway/Rust-Plugins", "tag": "v…", "version": "…", "commit": "…", "protocol": 12, "compat": { // per game; for ServUO it is schema 1's `servuo` block "frameworks": { "oxide": { "min_version": "2.0.7585" }, "carbon": { "min_version": "2.0.259" } }, "requires_plugins": ["Kits", "ZoneManager"] }, "asset": { "name": "runicgateway-rust-plugin-….tar.gz", "url": "…", "sha256": "…" } } } ``` A ServUO schema-2 document is schema 1's content re-shaped: `link` becomes `sidecar` (no `launcher`), `overlay` becomes a `payload` of `"kind": "overlay"`, and its `servuo` block becomes `compat` (`min_version`, `patches_verified_against`). The two gates hold for both games. For Rust, gate 1 reads `PROTOCOL_VERSION` from `Rust-Link`'s `sidecar/src/main.rs` at its release tag and `protocol` from the `manifest.json` inside the plugin tarball (`runicgateway-rust-plugin/manifest.json`). The stakes are higher there than for ServUO: the Rust game link has no `409`, so a mismatched plugin would mis-parse rather than be refused.