POST /dt/start, stamped dt.start whatever its body names, forwarded to the
plugin and answered with its dt.opened or dt.error. The plugin remembers the
key; DT despawns its own chest; nothing is kept here. dt.started, dt.won and
dt.ended ride the feed like any other event (docs rust-link/PROTOCOL.md §20.8).
72 tests (one new), fmt and clippy clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
POST /world/call, a thin forward like /world/place: the body is stamped
`world.call` whatever it names, so the route cannot place, revert or zone
anything. What a call starts is kept, so nothing is remembered here; the
plugin keeps the call's key. The `world.event` frames (24 types, told apart by
`event`) ride the feed like any other event, and hello's `console` like any
other field. Still protocol 14 (D332); one new test, 71 pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
POST /backpacks/read, a thin forward like /kits/uses: the body is stamped
`backpacks.read` whatever it names, so the route can only ever read. Nothing is
kept here. The bigger backpack an event lends is a permission in an ordinary
perm.sync (D337) and needs nothing new. Still protocol 14 (D332); one test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
POST /economics/pay, /economics/balances and /economics/adjust, thin
forwards like /kits/uses: each body is stamped with its own command and
passed to the plugin, which remembers a payment's key. Nothing is kept here.
The tally's `balance` and the `economics.wiped` event ride the feed like any
frame. Still protocol 14 (D332); one forward test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Opens protocol 14 (D332) for the step editor: one thin forward, POST /kits/uses
-> kits.uses ({kit, steamIds} in, counts out), for the kit weekend's warning.
perm.sync's new notices (D333) pass through untouched like the rest of the
body. PROTOCOL_VERSION 13 -> 14; one forward test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The install writes PopupNotifications.json into oxide/config or
carbon/configs as a banner across the top of the screen, by the installer's
rule: only where the file is missing or every setting is still the plugin's
own default (the Version aside). Anything else is kept whole. The plugin's
Version is written or kept, because PopupNotifications resets a config
without one. A failed write is said in the console and does not fail the
install: it is a look, not the bridge.
Tested in ghcr.io/ptero-eggs/installers:debian: missing, the rig's defaults,
defaults from a later release (version kept), four kinds of change, not
JSON, an existing banner, and an unmakeable directory. egg/build.sh builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The install script fetches RunicNPC's tarball with the rest, checks it against
the bundle's sha256, its API against the bundle and its file against its own
manifest, and places RunicNPC.cs before the bridge. Its data directory is left
for RunicNPC to make (runicnpc PLAN.md §1.5). rust-link/bundle.json names it.
Walked against a mock Gitea with and without RunicNPC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Rust-Plugins now releases a ZoneManager helper, RunicGatewayZones.cs, beside
the bridge (docs/modules/rust/PLAN_FIXES.md D181, D182), installed by default.
The egg copied only RunicGateway.cs out of the tarball.
It now takes every .cs the plugin manifest lists in `files`, checks each
against its own sha256 before anything is placed (the bridge's own checksum
was never checked by the egg before), and refuses a name that is not a plain
<Name>.cs. A release older than helpers lists only the bridge and installs
exactly what it did before. Exercised in an Alpine shell against both, a
tampered helper, a missing one, `../evil.cs` and `evil.dll`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The plugin now answers config.write once the files are on disk and reports
the reload as a config.outcome event, which this process files and feeds by
its type like any other event. The reload window no longer has to fit
inside REPLY_TIMEOUT, so CONFIG_RELOAD_WINDOW and the test that asserted
the pairing are gone, and the 504 body stops pointing at a rollback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Carbon's entrypoint puts LD_PRELOAD=libdoorstop.so in front of the whole
startup string, and with Carbon's DOORSTOP_* environment that preloader
breaks the launcher shell's own command substitution: the sidecar's
--print-config output went straight to the console and the capture came
back empty. The first Carbon server built from the egg printed its config
JSON, token included, unframed, then "server id 'main', sidecar URL
http://192.168.0.12: (listening on )" and never the once-only banner.
`env -u LD_PRELOAD` on the sidecar alone was not enough: the shell doing
the capturing had the preload too.
The launcher now re-execs itself once without LD_PRELOAD, keeps it aside,
and gives it back to the game in run_game, the only process that needs it.
Reproduced and fixed under Carbon's real environment (environment.sh +
libdoorstop.so): the released launcher dumps the JSON; this one prints the
banner and "server id 'egg-carbon', sidecar URL http://192.168.0.12:21019",
and the game still gets LD_PRELOAD and DOORSTOP_ENABLED. The four
no-preload cases in the game image are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
`[game].server_id` was a cross-check that WARNED and kept the plugin's id.
The phase 18 walk showed what that costs: a second server's plugin,
parked in this listener's backlog by a plugin bug (Rust-Plugins, D154),
was accepted the moment the first server's plugin reloaded, and the
website showed server "alpha" with beta's hostname and wipe.
Now, with `server_id` set, the connection is closed on the first frame
that names another server, BEFORE that frame reaches the store or the
feed, and both ids are logged at ERROR. The command channel is installed
only once a frame has named this server, so no website command (a grant,
a world write) can reach a plugin about to be refused, and /health reports
the plugin connected only from then. Blank `server_id`: nothing checked,
as before.
The egg's launcher now hands the sidecar the plugin config's ServerId once
that file exists. The plugin reads RUSTLINK_SERVER_ID only at its first
config write (D150); without this, a variable edited after the first boot
would be refused instead of changing nothing, as INSTALL.md promises.
Walked: a plugin aimed at another server's sidecar is refused with an
ERROR naming both ids, and the site keeps the right server's identity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
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
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
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
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
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
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
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
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
`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
`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
`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