feat(sidecar): protocol 10 — five reward forwards (phase 13b)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m13s
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m13s
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
This commit is contained in:
@@ -145,10 +145,18 @@ use tracing_subscriber::EnvFilter;
|
|||||||
/// (the module's PLAN.md §28). Five more thin forwards. The allowlist, the bounds, the monument
|
/// (the module's PLAN.md §28). Five more thin forwards. The allowlist, the bounds, the monument
|
||||||
/// vocabulary and the registry of what each run owns all live in the plugin.
|
/// vocabulary and the registry of what each run owns all live in the plugin.
|
||||||
///
|
///
|
||||||
|
/// # Protocol 10 — the rewards
|
||||||
|
///
|
||||||
|
/// `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). Five more thin forwards. `perm.sync` also gains a `credits` field, which
|
||||||
|
/// passes through untouched like the rest of that body. The tally, the kit catalogue and the chat
|
||||||
|
/// memory all live in the plugin.
|
||||||
|
///
|
||||||
/// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the
|
/// `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;
|
/// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases, §15 the world verbs,
|
||||||
/// this constant is one of its four declaration sites.
|
/// §16 the rewards; this constant is one of its four declaration sites.
|
||||||
pub const PROTOCOL_VERSION: u32 = 9;
|
pub const PROTOCOL_VERSION: u32 = 10;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let args = match cli::parse(std::env::args().skip(1)) {
|
let args = match cli::parse(std::env::args().skip(1)) {
|
||||||
|
|||||||
@@ -120,6 +120,14 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
|||||||
.route("/world/zone", post(world_zone))
|
.route("/world/zone", post(world_zone))
|
||||||
.route("/world/place", post(world_place))
|
.route("/world/place", post(world_place))
|
||||||
.route("/world/revert", post(world_revert))
|
.route("/world/revert", post(world_revert))
|
||||||
|
// 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.
|
||||||
|
.route("/tally/open", post(tally_open))
|
||||||
|
.route("/tally/snapshot", get(tally_snapshot))
|
||||||
|
.route("/tally/close", post(tally_close))
|
||||||
|
.route("/kits", get(kits))
|
||||||
|
.route("/chat", post(chat))
|
||||||
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -508,6 +516,48 @@ async fn world_revert(State(st): State<AppState>, Json(body): Json<Value>) -> Re
|
|||||||
forward_object(&st, body, "world.revert", "a revert").await
|
forward_object(&st, body, "world.revert", "a revert").await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start counting who takes part in a run. `tally.ok` or `tally.error` with a reason.
|
||||||
|
async fn tally_open(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "tally.open", "a tally").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What `GET /tally/snapshot` reads: one run's tally.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TallySnapshotQuery {
|
||||||
|
#[serde(rename = "runId")]
|
||||||
|
run_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Who has taken part in a run so far, with the plugin's score for each. Live and never cached:
|
||||||
|
/// a reward is granted from this answer, and a stale one would leave out whoever joined last.
|
||||||
|
async fn tally_snapshot(
|
||||||
|
State(st): State<AppState>,
|
||||||
|
Query(q): Query<TallySnapshotQuery>,
|
||||||
|
) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({ "cmd": "tally.snapshot", "reqId": req_id, "runId": q.run_id });
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop counting and forget the tally. **A tally already gone is a `200`**, like a world revert.
|
||||||
|
async fn tally_close(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "tally.close", "a tally close").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kits this server's Kits plugin has, with what each is gated by. Live, for the authoring
|
||||||
|
/// form, and refused by the plugin with a reason when Kits is not loaded.
|
||||||
|
async fn kits(State(st): State<AppState>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({ "cmd": "kits.list", "reqId": req_id });
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).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 {
|
||||||
|
forward_object(&st, body, "chat.say", "a chat line").await
|
||||||
|
}
|
||||||
|
|
||||||
/// Put `cmd` and `reqId` on a caller's object, **after** it is taken, so they overwrite anything
|
/// Put `cmd` and `reqId` on a caller's object, **after** it is taken, so they overwrite anything
|
||||||
/// the caller put there. `None` when the body is not an object.
|
/// the caller put there. `None` when the body is not an object.
|
||||||
fn stamp(body: Value, cmd: &str, req_id: &str) -> Option<Value> {
|
fn stamp(body: Value, cmd: &str, req_id: &str) -> Option<Value> {
|
||||||
@@ -972,6 +1022,25 @@ mod tests {
|
|||||||
assert_eq!(stamped["prefab"], "crate.elite");
|
assert_eq!(stamped["prefab"], "crate.elite");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Protocol 10's forwards are stamped the same way: a chat line cannot become a permission
|
||||||
|
/// sync by naming one.
|
||||||
|
#[test]
|
||||||
|
fn a_protocol_10_command_cannot_choose_its_own_command() {
|
||||||
|
let body = json!({ "cmd": "perm.sync", "reqId": "theirs", "key": "k-1", "message": "hi" });
|
||||||
|
let stamped = stamp(body, "chat.say", "r-2").expect("an object is stamped");
|
||||||
|
|
||||||
|
assert_eq!(stamped["cmd"], "chat.say");
|
||||||
|
assert_eq!(stamped["reqId"], "r-2");
|
||||||
|
assert_eq!(stamped["message"], "hi");
|
||||||
|
|
||||||
|
let body = json!({ "cmd": "tally.close", "runId": "9", "score": "both" });
|
||||||
|
let stamped = stamp(body, "tally.open", "r-3").expect("an object is stamped");
|
||||||
|
|
||||||
|
assert_eq!(stamped["cmd"], "tally.open");
|
||||||
|
assert_eq!(stamped["score"], "both");
|
||||||
|
assert!(stamp(json!(["9"]), "tally.close", "r-4").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
/// The same envelope rule as `perm_sync`, on the route that writes to a filesystem.
|
/// The same envelope rule as `perm_sync`, on the route that writes to a filesystem.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_configuration_write_cannot_choose_its_own_command() {
|
fn a_configuration_write_cannot_choose_its_own_command() {
|
||||||
|
|||||||
Reference in New Issue
Block a user