feat(sidecar): protocol 14 — the Backpacks forward (PLAN_REDESIGNS §11.3)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m29s

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
This commit is contained in:
2026-10-12 14:35:54 -05:00
parent 4eac5297e8
commit a95ecb8e25
2 changed files with 26 additions and 0 deletions

View File

@@ -188,6 +188,10 @@ use tracing_subscriber::EnvFilter;
/// and `POST /economics/adjust` (staff). The plugin remembers a payment's key; this process keeps
/// nothing. The tally's `balance` and the `economics.wiped` event ride the feed like any frame.
///
/// Backpacks (§20.4, the module's PLAN_REDESIGNS.md §11.3) adds one: `POST /backpacks/read`,
/// one player's backpack, read live. The plugin never writes a backpack, and a bigger one an event
/// lends is a `backpacks.size.N` permission in an ordinary `perm.sync`, so it needs nothing here.
///
/// `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;

View File

@@ -139,6 +139,8 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
.route("/economics/pay", post(economics_pay))
.route("/economics/balances", post(economics_balances))
.route("/economics/adjust", post(economics_adjust))
// Protocol 14, Backpacks (§20.4): one player's backpack, read live and never kept here.
.route("/backpacks/read", post(backpacks_read))
.route("/chat", post(chat))
// Protocol 11 (§30): the map. The picture passes through in slices and is never kept
// here, and positions are asked for while somebody is looking and never filed (D111). The
@@ -613,6 +615,13 @@ async fn economics_adjust(State(st): State<AppState>, Json(body): Json<Value>) -
forward_object(&st, body, "economics.adjust", "a balance adjustment").await
}
/// One player's backpack (protocol 14). The body is `{steamId}`; the answer is
/// `backpacks.contents` (`exists`, and with a backpack its `capacity`, `pageSize` and `items`), or
/// `backpacks.error` with a reason. The plugin only reads, and nothing is kept here.
async fn backpacks_read(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "backpacks.read", "a backpack read").await
}
/// Say one line in the server's chat. A repeated key is answered `ok` by the plugin and not said
/// again, so a retried step never repeats itself.
async fn chat(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
@@ -1248,6 +1257,19 @@ mod tests {
assert!(stamp(json!([1]), "economics.pay", "r-34").is_none());
}
/// Protocol 14, Backpacks: a read is stamped `backpacks.read` whatever its body names, so the
/// route can only ever read.
#[test]
fn protocol_14_backpacks_read_is_stamped() {
let body = json!({ "cmd": "backpacks.write", "steamId": "76561198000000001" });
let stamped = stamp(body, "backpacks.read", "r-40").expect("an object is stamped");
assert_eq!(stamped["cmd"], "backpacks.read");
assert_eq!(stamped["reqId"], "r-40");
assert_eq!(stamped["steamId"], "76561198000000001");
assert!(stamp(json!("76561198000000001"), "backpacks.read", "r-41").is_none());
}
/// 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]