chore(release): cut edge over to main — protocols 3–12, the egg, and the first release (rust phase 18, D145) #14

Merged
whitlocktech merged 22 commits from edge into main 2026-09-26 05:45:09 +00:00
Member

The cutover D145 put inside phase 18: edge → main, which fires this repo's first release (release.yml, landed in #13). Merge after Rust-Plugins#14 (§34.2.7 order).

main is at protocol 2 (phase 3); edge is at protocol 12 (PROTOCOL_VERSION = 12). 22 commits, nothing on main that edge lacks. What reaches main:

  • protocols 3–12 — the forwards for link codes, permissions, config, clans, raid frame, leases, world verbs, rewards, the map and titles (phases 6–17)
  • phase 18 — src/windows.rs (a real SCM entry point, service RunicGatewayRust-<id>, so no 1053); empty env treated as unset (the D152 regenerate-every-boot bug); the Pterodactyl egg (egg/egg.json, install.sh, with-sidecar.sh); release.yml building linux-x86_64 (static musl) + windows-x86_64 (D149)

What happens on merge

release.yml runs on the push to main, seeds 0.1.0 (no prior tag), builds both targets, and publishes the binaries, the launcher and the egg JSON with a manifest.json carrying protocol: 12. The installer's bundle.yml then pairs it with the plugin release into bundles/v2/rust/current.json — refusing if the two protocols disagree.

⚠ Secrets — please check before merging

Same as RP#14: REGISTRY_USER and REGISTRY_TOKEN (write:repository). Unverifiable from wtclaude. If absent, the credential preflight fails before any tag is pushed; add them and re-dispatch release.yml.

Part of docs/modules/rust/PLAN.md §34.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

The cutover D145 put inside phase 18: `edge` → `main`, which fires this repo's **first release** (`release.yml`, landed in #13). **Merge after Rust-Plugins#14** (§34.2.7 order). `main` is at protocol 2 (phase 3); `edge` is at protocol **12** (`PROTOCOL_VERSION = 12`). 22 commits, nothing on `main` that `edge` lacks. What reaches `main`: - protocols 3–12 — the forwards for link codes, permissions, config, clans, raid frame, leases, world verbs, rewards, the map and titles (phases 6–17) - phase 18 — `src/windows.rs` (a real SCM entry point, service `RunicGatewayRust-<id>`, so no 1053); empty env treated as unset (the D152 regenerate-every-boot bug); the Pterodactyl egg (`egg/egg.json`, `install.sh`, `with-sidecar.sh`); `release.yml` building **linux-x86_64 (static musl)** + **windows-x86_64** (D149) ## What happens on merge `release.yml` runs on the push to `main`, seeds **0.1.0** (no prior tag), builds both targets, and publishes the binaries, the launcher and the egg JSON with a `manifest.json` carrying `protocol: 12`. The installer's `bundle.yml` then pairs it with the plugin release into `bundles/v2/rust/current.json` — refusing if the two protocols disagree. ## ⚠ Secrets — please check before merging Same as RP#14: **`REGISTRY_USER`** and **`REGISTRY_TOKEN`** (`write:repository`). Unverifiable from `wtclaude`. If absent, the **credential preflight fails before any tag is pushed**; add them and re-dispatch `release.yml`. Part of `docs/modules/rust/PLAN.md` §34. - [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 22 commits 2026-09-26 05:36:04 +00:00
feat(sidecar): protocol 3 — the first route on this bridge that is not a GET
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m30s
fd6efd9a2c
`POST /link/confirm` forwards a one-time link code to the plugin and hands back
what it says. Everything before it was the website reading what the game had
already told us; this is the website asking the game a question only the game can
answer.

**It is still a forwarder and holds no authority of its own.** It does not mint
codes, does not store them, does not know what a website user is, and cannot tell
a good code from a bad one. Putting the code table here would give the sidecar a
credential and an opinion, and D2 and the bridge principles say it has neither.

**A refused code is a 200.** `link.ok` and `link.error` are both answers, and the
website has to tell "that code is wrong" from "the game never replied" to say the
right thing to a player. The two transport failures keep the codes `respond`
already gives them: 503 when the game is down, 504 when it is up and silent.

`usable_code` is split out and tested because its two rejections are easy to get
subtly wrong. It trims BEFORE it measures: a player pasting a code out of game
chat brings whitespace with it, a field of nothing but spaces is empty rather
than four characters long, and the length bound belongs on the trimmed value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #3
feat(sidecar): protocol 4 — two routes, and no opinion about either
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m38s
8f5440089c
`GET /permissions/catalogue` and `POST /permissions/sync` (R2). The first pair
that exists so the website can WRITE to the game, and the smallest change in this
repository that a protocol bump has ever needed.

That is the dumb-forwarder property paying for itself a second time: protocol 4
adds the largest command on the bridge and touches neither the store nor the feed.
The sidecar does not know what a group is, which names are managed, or what the
plugin will do with any of it. It puts an envelope on an object and forwards it.

**The envelope is this side's.** `cmd` and `reqId` are inserted AFTER the caller's
object is taken, so they overwrite anything a caller put there — no request can
arrive claiming to be a different command, or aimed at a correlation id somebody
else is waiting on.

**A command larger than the game link's line cap is refused here**, with the
limit in the body. Forwarded, it would be discarded silently by both ends
(§3.1 — an over-long line is dropped, not buffered) and present to the caller as
a `504`, which sends an operator to look at a game server that is working
perfectly.

Two tests, and both assert a refusal rather than a happy path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
Reviewed-on: #4
feat(sidecar): protocol 5 — three routes, and the one timeout worth explaining
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m27s
9532b7b26c
`GET /config/files`, `GET /config/file` and `POST /config/write` (R18). The
sidecar keeps the property protocol 4 relied on: it defines no schema for any of
it, stores none of it, and judges no path — only the process holding the
configuration directory can decide whether a path resolves inside it, and a
guard here would be a weaker second opinion with no way to check itself.

Nothing reaches the store or the feed. A config this sidecar cached would be an
edit an operator made over SSH that the website then silently overwrote, so all
three routes fail when the game is down, like `/status`.

The one thing added beyond forwarding is a better 504 on the write: the plugin
writes a whole set or restores a whole set and never half of either, so the body
says to re-read rather than guess, and names the reload window that is probably
still running. `CONFIG_RELOAD_WINDOW` mirrors the plugin's, and a test asserts
that two of them plus slack fit inside `REPLY_TIMEOUT` — a rollback that reports
after its caller has gone is worse than no rollback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
Reviewed-on: #5
feat(sidecar): protocol 6 — first-party clans, and no new code
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m24s
4adc9bccf2
Protocol 6 adds one board (`clans`) and five clan events. The sidecar
files frames by `type`, so none of them needs an arm here. That is
protocol 2's promise kept. The bump exists for the other declaration
sites: a website that reads clans must not pair with a plugin that
never sends them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #6
feat(sidecar): protocol 7 — the raid frame, and again no new code
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m7s
cb0913388d
entity.destroyed gains the cupboard's authorised list and covers doors,
walls and the cupboard. The sidecar stores and serves events as they
arrive, so only the number moves. The bump is what stops a website that
alerts on `authorized` from pairing with a protocol-6 plugin that never
sends it, which would read every raid as a base with no cupboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #7
feat(sidecar): protocol 8 — three lease forwards
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m42s
d688dfca66
GET /lease, POST /lease, POST /lease/release, each a correlated round trip with cmd and reqId stamped over the caller's. lease.expired is an ordinary event. The sidecar learns nothing about keys, bounds or deadlines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #8
feat(sidecar): protocol 9 — five world forwards (phase 13a)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m58s
a1a6177921
GET /world/monuments, GET /world/owned, POST /world/zone, POST /world/place
and POST /world/revert, each a correlated round trip with cmd and reqId
stamped over the caller's. The allowlist, bounds, monument vocabulary and
the registry of what each run owns live in the plugin (the module's PLAN.md
§28); this process moves lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #9
feat(sidecar): protocol 10 — five reward forwards (phase 13b)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m13s
c80994ce87
POST /tally/open, GET /tally/snapshot, POST /tally/close, GET /kits and
POST /chat: who took part in a run, the kits a reward can name, and one
line in the server's chat (the module's PLAN.md §29). Thin forwards like
protocol 9's; the tally, the kit catalogue and the chat memory all live
in the plugin. perm.sync's new `credits` field passes through untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #10
feat(sidecar): protocol 11 — four map forwards (phase 14)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m17s
9e2a83d7c9
GET /map, GET /map/chunk, POST /map/render and GET /map/live, stamped and
forwarded like the reward routes (PLAN.md §30.2). Nothing is stored: the
picture passes through in 512 KiB slices and positions are never filed
(D111). A test asserts a base64 slice plus its envelope fits the game
link's 1 MiB line cap on the way back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #11
feat(sidecar): protocol 12 — POST /titles (phase 17)
Some checks failed
PR Checks / rust-gates (pull_request) Failing after -35s
46ba35d89a
One more thin forward: the chat titles each player has earned, as a whole
set the plugin swaps in and BetterChat reads on the chat path. perm.sync's
BetterChat styles and chat.say's delivery and format pass through untouched
like the rest of their bodies. PROTOCOL_VERSION moves to 12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #12
feat(sidecar): a Windows service, the egg and its launcher, and the first release workflow (phase 18)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 3m53s
b3b66b1cc2
Module-rust phase 18, step 4 of docs/modules/rust/PLAN.md §34.2.7.

The Windows service (D149, §34.2.5): src/windows.rs, ported from link's fix
for error 1053. The same exe tries the SCM handshake and falls through to a
console run on 1063; it reports Running only once the listener and store are
up, and logs to a daily file beside its config. One binary serves every
RunicGatewayRust-<id> instance, because the SCM ignores the dispatcher's name
for an own-process service.

An empty environment variable now counts as unset. A Pterodactyl egg exports
every variable it declares, so a blank RUSTLINK_WEB_TOKEN arrived as "" and
overrode the saved token, and a new one was generated and persisted on every
boot. That breaks D152, which this change makes true.

The egg (R20, R22, D151, D152, §34.2.6), in egg/:
- install.sh is egg 18's script with two changes. A wipe guard moves
  rust-link/ to /tmp around `rm -rf ${REMOVE_FILES}`. The bridge block then
  fetches a schema-2 Rust bundle (pinnable by RUNICGATEWAY_BUNDLE), checks
  every asset's sha256 and the plugin's protocol before placing anything, and
  places the plugin by FRAMEWORK. Vanilla installs nothing and does not fail.
- with-sidecar.sh is the launcher. It unsets blank variables, builds the web
  bind from RUSTLINK_WEB_PORT, and runs --print-config so that a newly
  generated token is printed once. It prints the URL and server id for the
  admin page, then execs the game. It no longer uses `set -e`: nothing the
  bridge gets wrong may keep the game from booting.
- The startup's launcher prefix is conditional, so a server with no bridge
  boots exactly as egg 18 does.
- build.sh assembles egg-rust-runicgateway.json. PR Checks runs it.

The release (D145, §34.2.1) reuses servuo-plugins' engine. It publishes the
static musl Linux binary, the Windows exe, the launcher, the egg and
SHA256SUMS, and dispatches the installer's bundle.yml. PR Checks gains a
clippy run for the Windows target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #13
whitlocktech merged commit 1f3dfb55b2 into main 2026-09-26 05:45:09 +00:00
whitlocktech deleted branch edge 2026-09-26 05:45:10 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Rust-Link#14
No description provided.