docs(link): protocol 6 part b -- leases, participation, and what the walk found
Six files. `v6.md` gains sections 7-10: one version with two halves, the lease plane, the participation ledger, and 11b's verification. Three corrections to what the plan and the design of record assumed. Phase 11b is FIVE repos, not four, and in the opposite direction from 11a's correction: a lease's ledger row had no reconcile path anywhere, because the step that made it names `core.lease` and that is core's own action. `website` joins. EVENTS.md's §D frames the 258 `Config.Get` call sites as splitting into two patterns. Measured on 57.4: of the 158 non-Bridge sites in `Scripts/`, roughly eight are read live. The allowlist is not a curated subset of a large pool, it is nearly the whole of what exists. And `Config.Set` has exactly one caller in the entire tree, so on a stock shard a GM cannot drift a configuration lease even deliberately -- which is why proving `drifted` needed a scaffolding verb. §G's "participation attribution is now the largest piece of new UO work" closes, and the live-config-lease row goes to built-with-one-key. §10.1 records the defect the phase's own deferral found in 11a's shipped code: `bridge.busy` answered 200 instead of 425 because the frame carried two `kind` fields and parsers take the last. Unreachable in 11a by construction; produced on the first collision here. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,12 @@ Pin the version you built against and compare it to the header (or `/health.prot
|
||||
|
||||
**v2 (Protocol 2.0)** added the account-provisioning surface (§6.x: `POST /accounts/create`, `DELETE /link/{account}`) and the `account.*` events. Outbound event kinds are **additive** — a v1 client that ignores unknown kinds keeps working against the live feed — but the new *endpoints* require a v2 sidecar. If you send `X-UOLink-Version: 1`, calls to the new endpoints are refused with the 409 above.
|
||||
|
||||
**v6 (Protocol 6)** is the first bump that adds a **promise** rather than data: a command carrying
|
||||
an `idempotencyKey` is executed at most once (see *Retrying a command safely* in §6). It also adds
|
||||
`champ.boss.killed`, and the **event plane** — leases and the run-scoped participation ledger, six
|
||||
endpoints, all of them gated on the shard by `Bridge.EventsEnabled` and answering **403** when an
|
||||
operator has not switched it on. See [`v6.md`](v6.md).
|
||||
|
||||
**v3 (Protocol 3.0)** adds `world.ruleset`, `points.board` and `vendor.listing` /
|
||||
`vendor.listing.remove`, with the `GET /ruleset`, `/points` and `/market` reads that serve them from
|
||||
the sidecar's store. Same shape as the v2 bump: the event kinds are additive, so a v2 client that
|
||||
@@ -970,6 +976,89 @@ human just pressed a button for and can see the result of.
|
||||
The DELETE forms (`/towncrier/{id}`, `/news/{id}`) take no key: their idempotency is inherent — the
|
||||
second removal of an entry is a no-op the shard is already happy to perform.
|
||||
|
||||
### The event plane — leases (Protocol 6)
|
||||
|
||||
**Off by default.** Every endpoint below answers **403** unless the operator has set
|
||||
`Bridge.EventsEnabled` on the shard. That is deliberately not the admin write plane's switch:
|
||||
enabling admin writes is consenting to staff moderation from a screen a human is looking at, and
|
||||
enabling this is consenting to your world being changed and watched on a schedule, unattended.
|
||||
|
||||
A **lease** is a live configuration value held at a new setting for a bounded time. The shard
|
||||
restores the baseline when the deadline passes **whether or not you are ever heard from again** —
|
||||
so the worst case is a world back at baseline early, never one stuck changed indefinitely.
|
||||
|
||||
```json
|
||||
GET /lease
|
||||
→ 200 { "kind":"lease.list.ok", "leases":[
|
||||
{ "key":"PlayerCaps.SkillCap", "label":"Starting skill cap", "type":"float",
|
||||
"min":1000, "max":1500, "default":"1000", "current":"1000", "held":false } ] }
|
||||
|
||||
POST /lease
|
||||
{ "key":"PlayerCaps.SkillCap", "value":"1200", "holdMs":600000,
|
||||
"untilMs":1788567000000, "runId":"77", "idempotencyKey":"…" }
|
||||
→ 200 { "kind":"lease.ok", "baseline":"1000", "applied":"1200", "untilMs":1788567000000 }
|
||||
|
||||
POST /lease/release
|
||||
{ "key":"PlayerCaps.SkillCap", "expected":"1200", "baseline":"1000" }
|
||||
→ 200 { "kind":"lease.ok", "released":true, "current":"1000" }
|
||||
```
|
||||
|
||||
Five things a caller needs:
|
||||
|
||||
- **The catalog is an allowlist and it is short.** A shard advertises only keys it has verified take
|
||||
effect. Most of ServUO's configuration is cached at type initialisation, where a lease would apply
|
||||
cleanly and do nothing — the worst failure this feature has — so `lease.list` is the authority and
|
||||
"any config key" is not offered.
|
||||
- **`holdMs` is what the shard honours; `untilMs` is for display.** Send both. An absolute deadline
|
||||
is measured against two clocks, and a shard whose clock runs fast would restore your lease the
|
||||
moment it took it.
|
||||
- **Values cross as text, whatever `type` says.** `"1200"`, not `1200`. Comparison is done on parsed
|
||||
values at the other end; the text is so a compare-and-set is comparing what you sent.
|
||||
- **`released` can answer `lease.drifted` at 200.** That means somebody moved the value while you
|
||||
held it, the shard **did not overwrite them**, and `current` is what is there now. It is not an
|
||||
error: the mechanism did its job, and only a human can decide what should happen next.
|
||||
- **`held` means the shard still has a record of the lease, not that the value is still overridden.**
|
||||
A lease whose deadline has fired stays listed with `expired: true` until you release it, so a
|
||||
reconcile in that window does not read a working backstop as a lost resource. A shard restart, by
|
||||
contrast, reverts every lease and clears the record — `held: false` is how you learn that.
|
||||
|
||||
### The event plane — participation (Protocol 6)
|
||||
|
||||
A run-scoped tally of who took part: presence in a declared area, plus kill credit inside it, keyed
|
||||
by **character serial**. The shard computes a score and you store it; the components ride along so
|
||||
you can explain it.
|
||||
|
||||
```json
|
||||
POST /participation
|
||||
{ "runId":"99", "map":"Felucca", "x":1496, "y":1628, "radius":40, "holdMs":3600000 }
|
||||
→ 200 { "kind":"participation.ok", "runId":"99", "members":0, "closed":false }
|
||||
|
||||
POST /participation/99/snapshot
|
||||
{ "idempotencyKey":"…" }
|
||||
→ 200 { "kind":"participation.snapshot.ok", "runId":"99", "members":2, "killWeight":5,
|
||||
"participants":[ { "serial":"0xCB20", "name":"Jarvis", "acct":"seed_001", "webId":"17",
|
||||
"seconds":3600, "minutes":"60.00", "kills":3, "score":"75.0000",
|
||||
"firstMs":1788550182074, "lastMs":1788553782074 } ] }
|
||||
|
||||
POST /participation/99/close
|
||||
→ 200 { "kind":"participation.ok", "closed":true, "known":true, "members":2 }
|
||||
```
|
||||
|
||||
- **The area is a point and a radius, not a region name.** The most specific region containing an
|
||||
event is routinely anonymous on a UO map — an active champion spawn registers a nameless region
|
||||
over its own area — so a region-named venue would be undeclarable for exactly the places events
|
||||
happen.
|
||||
- **`snapshot` is a POST for a read**, because it carries your `idempotencyKey`. On a well-attended
|
||||
run the shard walks its members across game ticks rather than in one call, so a repeat arriving
|
||||
mid-walk is answered **425**. Come back.
|
||||
- **The tally is persisted in the world save**, so it survives a restart mid-event. `close` on a run
|
||||
the shard has forgotten answers `known: false` at 200 rather than an error: nothing is being
|
||||
counted for it either way.
|
||||
- **`refused`** on a snapshot is the number of members the shard's cap turned away. A truncated tally
|
||||
says so rather than quietly being short.
|
||||
- **`webId`** is present only where the character's game account is linked to a website user. Most
|
||||
characters carry none; treat its absence as ordinary.
|
||||
|
||||
### Help-page (support) queue
|
||||
|
||||
Read the open queue, respond to a player, or close a page. Staff-facing — gate behind your own
|
||||
@@ -1165,7 +1254,8 @@ sidecar defines no audiences. Deciding who may see what is the consuming site's
|
||||
| 200 | OK |
|
||||
| 400 | Bad request (malformed body, invalid parameter, or a shard `*.error` that isn't a not-found) |
|
||||
| 401 | Missing or invalid auth token |
|
||||
| 404 | Not found (unknown account / character / id, or a not-linked account) |
|
||||
| 403 | Refused by the operator — the admin write plane, or the event plane (`Bridge.EventsEnabled`), is switched off on the shard |
|
||||
| 404 | Not found (unknown account / character / id, a not-linked account, an unoffered lease key, or a run the shard is not counting) |
|
||||
| 409 | Conflict — protocol version mismatch, or an account name already taken on `POST /accounts/create` |
|
||||
| 425 | Too Early — a command with this `idempotencyKey` is still in flight on the shard (Protocol 6). Nothing ran; retry |
|
||||
| 429 | Too many requests — the shard's per-IP account cap was hit on `POST /accounts/create` |
|
||||
|
||||
Reference in New Issue
Block a user