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

`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
This commit is contained in:
2026-09-22 08:54:08 -05:00
parent c005f5e12f
commit 9532b7b26c
2 changed files with 164 additions and 2 deletions

View File

@@ -90,9 +90,22 @@ use tracing_subscriber::EnvFilter;
/// 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.
///
/// # Protocol 5 — configuration from the site
///
/// `GET /config/files`, `GET /config/file` and `POST /config/write` (R18). An admin edits a
/// plugin's settings on the website; the plugin writes them, reloads whatever owns them, watches
/// for the reload to announce itself, and **puts the old files back automatically** if it does
/// not.
///
/// Two things about that reach this process. The write is the only route here that causes a write
/// on the game host, and it is the only one whose reply routinely spends seconds rather than
/// milliseconds — the plugin holds the correlation open across a reload and, at worst, across a
/// rollback as well. `web::CONFIG_RELOAD_WINDOW` is that budget, mirrored from the plugin, and a
/// test asserts the pairing rather than trusting it.
///
/// `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;
/// mirror, §11 configuration; this constant is one of its four declaration sites.
pub const PROTOCOL_VERSION: u32 = 5;
fn main() -> anyhow::Result<()> {
let args = match cli::parse(std::env::args().skip(1)) {