feat(sidecar): carry the event plane (Phase 11b) #37

Merged
whitlocktech merged 1 commits from feature/events-p11b-leases-participation into edge 2026-09-05 04:11:13 +00:00
Member

EVENTS_PLAN.md Phase 11b, the sidecar half. Protocol 6 amended in place, so PROTOCOL_VERSION is unchanged — v6.md §7 says why that is safe on edge and would not be on main.

Six routes, one new responder, no store migration and no new machinery. One file.

event_call is admin_call without the required actor

Deliberately. Every verb behind the admin plane is a staff member pressing a button, and the shard's audit trail has to name them. An event verb's author is a run, which the body already carries as runId — demanding a human name for something no human is doing would have the runner inventing one.

Everything else is the same, including the idempotencyKey passthrough, and for the same reason 11a wrote down: the key is one of the body's remaining fields, and a refactor narrowing this to a known field list would silently turn every retried lease back into a possible duplicate with nothing here failing.

respond_event, the fourth responder

It exists for two mappings the generic one gets wrong.

lease.drifted is a 200. The shard was asked to compare and set, it compared, and it declined to overwrite somebody's deliberate change — that is the mechanism working, and cleanup.js on the website records drifted as a distinct successful outcome rather than an error. It is also why this is not a 409: 409 is the protocol-version gate's, and a version mismatch and a moved value want opposite dispositions from a caller. Exactly the argument protocol 6 already made for bridge.busy being a 425.

The event plane being switched off is a 403, not the 400 the generic responder's reason-sniffing would produce. Bridge.EventsEnabled is an operator's deliberate refusal to let the website change their world on a schedule; telling the website it sent a bad request would send an administrator hunting a bug in a step that is written correctly.

The routes

Route Command
GET /lease lease.list The whole catalog with current values. One read serves both the website's read() and its new inForce() — splitting them would be two round trips for one key
POST /lease lease.apply holdMs is authoritative, untilMs is display
POST /lease/release lease.release
POST /participation participation.open
POST /participation/:runId/snapshot participation.snapshot
POST /participation/:runId/close participation.close

snapshot is a POST for a read, and the reason is the phase's headline: it carries the caller's idempotencyKey, and on a well-attended run the shard walks its members across Core ticks rather than in one inbound call — so a repeat arriving mid-walk is answered bridge.busy. A read that can legitimately be refused as a repeat in flight is not a GET.

Verification

  • cargo test47 pass, 0 fail (4 new): bridge.busy → 425 on the new responder as well as the other three; a drifted lease is a 200 and explicitly not a 409; the event gate being off is a 403 on both lease.error and participation.error; an unknown lease key and an unknown run are 404s while other refusals are 400s.
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings clean.
  • The release binary against a real ServUO on the local rig: --version reports protocol 6, and every claim in the plugin PR's table was collected through these routes.

bridge.busy was reachable live for the first time, and it immediately failed. It came back 200, not 425 — because the shard's frame carried two kind fields and this side takes the last. That is a plugin defect (servuo-plugins#22 has it), and nothing here changed: the mapping was right, it was simply never being handed bridge.busy. Worth recording on this side too, since the sidecar is where the wrong answer surfaced.

  • AI-assisted: Claude Code (Opus 5).

Plugin: RunicGateway/servuo-plugins#22 · Core: RunicGateway/website#193 · Module: RunicGateway/Module-uo#30 · Docs: RunicGateway/docs#220

🤖 Generated with Claude Code

`EVENTS_PLAN.md` Phase 11b, the sidecar half. Protocol 6 **amended in place**, so `PROTOCOL_VERSION` is unchanged — `v6.md` §7 says why that is safe on `edge` and would not be on `main`. Six routes, one new responder, **no store migration** and no new machinery. One file. ## `event_call` is `admin_call` without the required `actor` Deliberately. Every verb behind the admin plane is a staff member pressing a button, and the shard's audit trail has to name them. An event verb's author is a **run**, which the body already carries as `runId` — demanding a human name for something no human is doing would have the runner inventing one. Everything else is the same, including the `idempotencyKey` passthrough, and for the same reason 11a wrote down: the key is one of the body's remaining fields, and a refactor narrowing this to a known field list would silently turn every retried lease back into a possible duplicate with nothing here failing. ## `respond_event`, the fourth responder It exists for two mappings the generic one gets wrong. **`lease.drifted` is a 200.** The shard was asked to compare and set, it compared, and it declined to overwrite somebody's deliberate change — that is the mechanism working, and `cleanup.js` on the website records `drifted` as a distinct *successful* outcome rather than an error. It is also why this is not a **409**: 409 is the protocol-version gate's, and a version mismatch and a moved value want opposite dispositions from a caller. Exactly the argument protocol 6 already made for `bridge.busy` being a 425. **The event plane being switched off is a 403**, not the 400 the generic responder's reason-sniffing would produce. `Bridge.EventsEnabled` is an operator's deliberate refusal to let the website change their world on a schedule; telling the website it sent a bad request would send an administrator hunting a bug in a step that is written correctly. ## The routes | Route | Command | | |---|---|---| | `GET /lease` | `lease.list` | The whole catalog with current values. **One read serves both** the website's `read()` and its new `inForce()` — splitting them would be two round trips for one key | | `POST /lease` | `lease.apply` | `holdMs` is authoritative, `untilMs` is display | | `POST /lease/release` | `lease.release` | | | `POST /participation` | `participation.open` | | | `POST /participation/:runId/snapshot` | `participation.snapshot` | | | `POST /participation/:runId/close` | `participation.close` | | **`snapshot` is a POST for a read**, and the reason is the phase's headline: it carries the caller's `idempotencyKey`, and on a well-attended run the shard walks its members across Core ticks rather than in one inbound call — so a repeat arriving mid-walk is answered `bridge.busy`. A read that can legitimately be refused as a repeat in flight is not a GET. ## Verification - `cargo test` — **47 pass, 0 fail** (4 new): `bridge.busy` → 425 on the new responder as well as the other three; a drifted lease is a 200 and explicitly *not* a 409; the event gate being off is a 403 on both `lease.error` and `participation.error`; an unknown lease key and an unknown run are 404s while other refusals are 400s. - `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` clean. - The **release** binary against a real ServUO on the local rig: `--version` reports `protocol 6`, and every claim in the plugin PR's table was collected through these routes. **`bridge.busy` was reachable live for the first time**, and it immediately failed. It came back **200**, not 425 — because the shard's frame carried two `kind` fields and this side takes the last. That is a plugin defect (`servuo-plugins#22` has it), and nothing here changed: the mapping was right, it was simply never being handed `bridge.busy`. Worth recording on this side too, since the sidecar is where the wrong answer surfaced. - [x] AI-assisted: Claude Code (Opus 5). Plugin: RunicGateway/servuo-plugins#22 · Core: RunicGateway/website#193 · Module: RunicGateway/Module-uo#30 · Docs: RunicGateway/docs#220 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-09-05 04:05:22 +00:00
feat(sidecar): carry the event plane (Phase 11b)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 3m6s
93411966d7
Protocol 6 amended in place, so PROTOCOL_VERSION is unchanged. Six routes and a
fourth responder; no store migration and no new machinery.

`event_call` is `admin_call` without the required `actor`: an event verb's author
is a RUN, which the body carries as `runId`, and demanding a human name for
something no human is doing would have the runner inventing one.

`respond_event` exists for two mappings the generic responder gets wrong. A
drifted lease is a 200 -- the shard was asked to compare and set, it compared,
and it declined to overwrite somebody's deliberate change, which is the mechanism
working -- and deliberately not the 409 the version gate owns, for the same
reason 425 is not. And the event plane being switched off is a 403 rather than a
reason-sniffed 400: it is an operator's deliberate refusal, and a 400 would send
an administrator hunting a bug in a step that is written correctly.

`participation.snapshot` is a POST for a read, because it carries the caller's
idempotency key and the shard may refuse it as a repeat in flight.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit d38a9e8a75 into edge 2026-09-05 04:11:13 +00:00
whitlocktech deleted branch feature/events-p11b-leases-participation 2026-09-05 04:11:14 +00:00
Sign in to join this conversation.
No description provided.