feat(sidecar): protocol 4 — two routes, and no opinion about either
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m38s

`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
This commit is contained in:
2026-09-21 18:27:54 -05:00
parent 47ffc9a78c
commit 8f5440089c
2 changed files with 130 additions and 3 deletions

View File

@@ -71,9 +71,28 @@ use tracing_subscriber::EnvFilter;
/// * **`GET /feed`** is the ingest cursor, oldest-first, separate from `/events` so that no
/// caller can get the other ordering by forgetting a parameter.
///
/// `docs/rust-link/PROTOCOL.md` §8 is the specification; this constant is one of its four
/// declaration sites.
pub const PROTOCOL_VERSION: u32 = 3;
/// # Protocol 3 — identity
///
/// `POST /link/confirm`, the first route here that is not a GET, and the first message on this
/// bridge the WEBSITE originates. It forwards a six-character code to the plugin and hands back
/// what the plugin said. The codes live in the game's memory and nowhere else: putting the table
/// here would give this process a credential and an opinion, and it is designed to have neither.
///
/// # Protocol 4 — the permission mirror
///
/// `GET /permissions/catalogue` and `POST /permissions/sync` (R2). The first command that WRITES
/// to the game: the website sends the whole permission set it authors for this server and the
/// plugin reconciles the store against it.
///
/// Nothing about that shape is visible in this process beyond two routes, and 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 one thing this side owns is the
/// envelope: `cmd` and `reqId` are written over whatever the caller sent, and a command that would
/// not fit on the game link is refused here rather than discarded silently at the other end.
///
/// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the
/// mirror; this constant is one of its four declaration sites.
pub const PROTOCOL_VERSION: u32 = 4;
fn main() -> anyhow::Result<()> {
let args = match cli::parse(std::env::args().skip(1)) {