From 6bf8a3624e713ad6ac5ba50c5935d401aea68fcd Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 16 Oct 2026 02:32:02 -0500 Subject: [PATCH] =?UTF-8?q?feat(sidecar):=20protocol=2014=20=E2=80=94=20th?= =?UTF-8?q?e=20console=20command=20run=20forward=20(PLAN=5FREDESIGNS=20?= =?UTF-8?q?=C2=A716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `POST /commands/run`, stamped `command.run` the way `world.call`, `dt.start` and `gc.place` are (PROTOCOL.md §20.11). The plugin keeps the key, fills {steamid} and {name} and decides who is online; the sidecar moves the line and knows nothing of what a command does (D384, no allowlist). Joins protocol 14, still unreleased on edge. Stamp test: the body's key, commands and recipients pass through byte for byte, placeholders and quotes included. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- sidecar/src/main.rs | 5 +++++ sidecar/src/web.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/sidecar/src/main.rs b/sidecar/src/main.rs index 65efcf7..668ac3e 100644 --- a/sidecar/src/main.rs +++ b/sidecar/src/main.rs @@ -208,6 +208,11 @@ use tracing_subscriber::EnvFilter; /// retry never places a second; the crate is kept when the run ends (D373). `gc.started`, /// `gc.ended` and `gc.failed` ride the feed like any other event. /// +/// Console commands as event steps (§20.11, the module's PLAN_REDESIGNS.md §16) add one forward, +/// `POST /commands/run`: the commands a step's author typed, run once or once per online +/// recipient. The plugin remembers the key so nothing runs twice; this process keeps nothing and +/// knows nothing of what a command does. +/// /// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the /// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases, §15 the world verbs, /// §16 the rewards, §17 the map, §18 the optional mods, §19 the walk's fixes, §20 the step editor; diff --git a/sidecar/src/web.rs b/sidecar/src/web.rs index 7305883..0d95b06 100644 --- a/sidecar/src/web.rs +++ b/sidecar/src/web.rs @@ -131,6 +131,10 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> { // Protocol 14 (§20.9): place one guarded crate. The plugin remembers the key; the crate is // kept when the run ends (Guarded Crate cannot end one alone), so nothing is kept here. .route("/gc/place", post(gc_place)) + // Protocol 14 (§20.11): console commands a step's author typed. The plugin keeps the key + // so nothing runs twice, fills the placeholders and decides who is online; this process + // moves the line and knows nothing of what any command does (no allowlist, D384). + .route("/commands/run", post(commands_run)) // Protocol 10 (§29): who took part, what they may redeem, and a line in chat. The tally // is counted and held by the plugin, the kit catalogue is Kits' own, and whether a chat // line was already said is the plugin's memory. These move lines, like everything above. @@ -583,6 +587,16 @@ async fn gc_place(State(st): State, Json(body): Json) -> Respon forward_object(&st, body, "gc.place", "a guarded crate placement").await } +/// Run the console commands a step's author typed (protocol 14): once, or once per online +/// recipient. `command.ran` (how many ran, the console's replies, who was skipped as offline, and +/// `repeat` for a key already seen) or `command.error` with a reason, `unknown-command` among +/// them. A step's `teardown` rides its run and the plugin keeps it with the key; `teardownOf` +/// asks for the teardown kept under another key (D385). The plugin remembers the key, so nothing +/// is remembered here. +async fn commands_run(State(st): State, Json(body): Json) -> Response { + forward_object(&st, body, "command.run", "a console command run").await +} + /// Start counting who takes part in a run. `tally.ok` or `tally.error` with a reason. async fn tally_open(State(st): State, Json(body): Json) -> Response { forward_object(&st, body, "tally.open", "a tally").await @@ -1346,6 +1360,43 @@ mod tests { assert!(stamp(json!("Elite"), "gc.place", "r-63").is_none()); } + /// Protocol 14, console commands: a run is stamped `command.run` whatever its body names, so + /// the route runs commands and nothing else, and its key, commands and recipients pass through + /// byte for byte; the placeholders are the plugin's to fill. + #[test] + fn protocol_14_command_run_is_stamped() { + let body = json!({ "cmd": "gc.place", "reqId": "theirs", "key": "12:5", + "commands": ["sr add {steamid} 100", "say \"{name} won\""], + "recipients": [{ "steamId": "76561198000000001", "name": "Some \"One\"" }] }); + let stamped = stamp(body, "command.run", "r-64").expect("an object is stamped"); + + assert_eq!(stamped["cmd"], "command.run"); + assert_eq!(stamped["reqId"], "r-64"); + assert_eq!(stamped["key"], "12:5"); + assert_eq!(stamped["commands"][0], "sr add {steamid} 100"); + assert_eq!(stamped["commands"][1], "say \"{name} won\""); + assert_eq!(stamped["recipients"][0]["name"], "Some \"One\""); + assert!(stamp(json!(["rbe"]), "command.run", "r-65").is_none()); + + // D385: a step's teardown rides its run, and a teardown asked for by key alone names the + // run it is for; both are the plugin's to read. + let run = stamp( + json!({ "key": "12:6", "commands": ["rbe 1"], "teardown": ["rbe stop"] }), + "command.run", + "r-66", + ) + .expect("an object is stamped"); + assert_eq!(run["teardown"][0], "rbe stop"); + let teardown = stamp( + json!({ "key": "12:6:teardown", "teardownOf": "12:6" }), + "command.run", + "r-67", + ) + .expect("an object is stamped"); + assert_eq!(teardown["cmd"], "command.run"); + assert_eq!(teardown["teardownOf"], "12:6"); + } + /// Protocol 12: a title set is forwarded whole. The markup inside each `text` is the module's /// and the plugin's business, so it must arrive byte for byte as the website composed it. #[test]