docs(rust-link): protocol 2 — the read path, and phase 3 as built #255

Merged
whitlocktech merged 2 commits from docs/rust-protocol-2 into main 2026-09-16 16:36:45 +00:00
Member

The specification the other three repositories are held against, plus the phase record.

PROTOCOL.md §8 — the read path

Its centre is one field. Every frame now carries typeevent, snapshot, reply, control — and the sidecar files on that and nothing else. That is the dumb-forwarder property made structural rather than intended: ten new event kinds are zero change in Rust-Link, and only a version adding an indexed column touches it at all.

Also in §8:

  • the fifteen-kind catalogue and what each frame carries;
  • wipeId derived by the plugin, which reverses §3.2's "deriving one is the website's job" — by protocol 2 three components store rows that need it and only one can read the value;
  • boards re-sent on connect and on a cadence, which is what makes "a restarted sidecar is fully populated within one connection" true by construction;
  • the aggregate rule: a hook that can fire more than once a second per player is a counter, not an event (OnDispenserGather fires on every swing at a tree);
  • the void rule, which stops a read-path hook vetoing a death, a gather or a login — enforced by the signature rather than by remembering to write return null;
  • GET /feed, a cursor route deliberately separate from /events, because one route with two orderings serves the wrong one to every caller that forgets the parameter — and for an ingesting caller that means advancing past rows it never read.

§8.5 is the part to read twice. Whether a kind is public is not on the wire. A boundary declared by the sender is one a compromised — or merely out-of-date — game host can widen, so the module holds a default-deny allowlist and this table is what its test holds it against. Protocol 2 carries IP addresses, player reports, and the grid reference of somebody's base.

§8.8 corrects a catalogue, not a defect. PLAN.md §10 sources rust.login.denied from CanUserLogin — but that hook fires on every attempt, and the only way to learn of a denial from it is to be the denier. A denial is the absence of an approval; protocol 2 emits both facts so phase 10 can pair them.

PLAYER_WALK.md — new

Half this catalogue cannot fire without somebody holding a mouse. Ten steps, what each one should produce, and what counts as a pass — written so it can be run without watching the output live, and so the result afterwards reads as a transcript.

PLAN.md §16 — phase 3 as built

The four org-lead decisions, what was proven and how, and the two defects only a server that BOOTED with the plugin could find:

  • the wipe id was null for every real sessionInit runs before the save is loaded, and every development iteration was a hot reload, which reads an already-loaded world and gets the right answer every time;
  • Unload blocked the game's main thread for 2002 ms — a blocking TcpClient.Connect against a host that drops rather than refuses. Every loopback test is the first kind.

It also states plainly what is not proven yet and who it is waiting on (§16.7).

Companion PRs: Rust-Plugins, Rust-Link, Module-Rust.


  • AI-assisted: written with Claude Code (Opus 5)

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

The specification the other three repositories are held against, plus the phase record. ## `PROTOCOL.md` §8 — the read path Its centre is one field. Every frame now carries **`type`** — `event`, `snapshot`, `reply`, `control` — and the sidecar files on that and nothing else. That is the dumb-forwarder property made structural rather than intended: ten new event kinds are zero change in Rust-Link, and only a version adding an *indexed column* touches it at all. Also in §8: - the **fifteen-kind catalogue** and what each frame carries; - **`wipeId` derived by the plugin**, which reverses §3.2's *"deriving one is the website's job"* — by protocol 2 three components store rows that need it and only one can read the value; - **boards** re-sent on connect and on a cadence, which is what makes *"a restarted sidecar is fully populated within one connection"* true by construction; - the **aggregate rule**: a hook that can fire more than once a second per player is a counter, not an event (`OnDispenserGather` fires on every swing at a tree); - the **void rule**, which stops a read-path hook vetoing a death, a gather or a login — enforced by the signature rather than by remembering to write `return null`; - **`GET /feed`**, a cursor route deliberately separate from `/events`, because one route with two orderings serves the wrong one to every caller that forgets the parameter — and for an ingesting caller that means advancing past rows it never read. **§8.5 is the part to read twice.** Whether a kind is public is *not* on the wire. A boundary declared by the sender is one a compromised — or merely out-of-date — game host can widen, so the module holds a default-deny allowlist and this table is what its test holds it against. Protocol 2 carries IP addresses, player reports, and the grid reference of somebody's base. **§8.8 corrects a catalogue, not a defect.** `PLAN.md` §10 sources `rust.login.denied` from `CanUserLogin` — but that hook fires on *every* attempt, and the only way to learn of a denial from it is to be the denier. A denial is the **absence of an approval**; protocol 2 emits both facts so phase 10 can pair them. ## `PLAYER_WALK.md` — new Half this catalogue cannot fire without somebody holding a mouse. Ten steps, what each one should produce, and what counts as a pass — written so it can be run without watching the output live, and so the result afterwards reads as a transcript. ## `PLAN.md` §16 — phase 3 as built The four org-lead decisions, what was proven and how, and the **two defects only a server that BOOTED with the plugin could find**: - **the wipe id was null for every real session** — `Init` runs before the save is loaded, and every development iteration was a hot reload, which reads an already-loaded world and gets the right answer every time; - **`Unload` blocked the game's main thread for 2002 ms** — a blocking `TcpClient.Connect` against a host that *drops* rather than refuses. Every loopback test is the first kind. It also states plainly what is **not** proven yet and who it is waiting on (§16.7). Companion PRs: `Rust-Plugins`, `Rust-Link`, `Module-Rust`. --- - [x] AI-assisted: written with Claude Code (Opus 5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-16 13:40:10 +00:00
The specification the other three repositories are held against, plus the phase
record.

`PROTOCOL.md` §8 is the new contract. Its centre is one field: every frame now
carries `type` — `event`, `snapshot`, `reply`, `control` — and the sidecar files
on that and nothing else. That is the dumb-forwarder property made structural
rather than intended: ten new event kinds are zero change in Rust-Link, and only
a version adding an indexed column touches it at all.

Also in §8: the fifteen-kind catalogue and what each frame carries; `wipeId`
derived by the plugin, which REVERSES §3.2's "deriving one is the website's job"
and says why; boards re-sent on connect and on a cadence; the aggregate rule (a
hook that can fire more than once a second per player is a counter, not an
event); the void rule that stops a read-path hook vetoing a death or a login; and
`GET /feed`, a cursor route separate from `/events` because one route with two
orderings serves the wrong one to every caller that forgets the parameter.

§8.5 is the part to read twice. The classification of a kind as public or staff
is NOT on the wire, deliberately: a boundary declared by the sender is one a
compromised or out-of-date game host can widen, so the module holds a
default-deny allowlist and this table is what its test holds it against.

§8.8 corrects a catalogue rather than a defect: PLAN.md §10 sources
`rust.login.denied` from `CanUserLogin`, and that hook fires on every attempt —
the only way to learn of a denial from it is to be the denier. A denial is the
absence of an approval, and protocol 2 emits both facts so phase 10 can pair them.

`PLAYER_WALK.md` is new, and it exists because half this catalogue cannot fire
without somebody holding a mouse. Ten steps, what each one should produce, and
what counts as a pass — written so the walk can be run without watching the
output live, and so the answer afterwards is readable as a transcript.

PLAN.md §16 is phase 3 as built: the four decisions, the two defects only a
server that BOOTED with the plugin could find (a wipe id that was null for every
real session, and a two-second main-thread stall on unload), what was proven and
how, and — stated plainly rather than implied — the three measurements still
queued on the org lead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-16 13:52:58 +00:00
The module half exercised against the live rig rather than only its own tests:
the working tree installed into a running core, pointed at the workstation
sidecar with a real Rust server behind it. The fresh-install path logged
`cursor started at the feed tail`, a console banid/unban arrived as two
rust_events rows carrying the wipe id, and the wipe row was created by being
mentioned rather than by any "a wipe started" call.

Then the boundary on real rows: with both ban events in the table, the public
events route answered {"events":[]} — and answered the same when asked for
player.banned BY NAME.

The trap is worth more than the proof. The state row read as online=1 with
hostname=NULL, a shape no code path writes. Two cores shared one database: a
second instance still held the phase 2 release in memory, spoke protocol 1, was
refused 409, and rewrote the row every thirty seconds. Nothing was wrong with
either. What made it look like a defect was two SELECTs in two round trips
assuming the state did not move between them — the same mistake the Pterodactyl
work recorded, and a single atomic read caught the coherent row alternating with
the other writer's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit a221d4409b into main 2026-09-16 16:36:45 +00:00
whitlocktech deleted branch docs/rust-protocol-2 2026-09-16 16:36:46 +00:00
Sign in to join this conversation.
No description provided.