diff --git a/link/ADMIN_CONTROLS.md b/link/ADMIN_CONTROLS.md index 247d120..18f6fe5 100644 --- a/link/ADMIN_CONTROLS.md +++ b/link/ADMIN_CONTROLS.md @@ -21,6 +21,12 @@ None of these are *moderation*. A staff member who wants to kick a cheater, ban **The thesis up front:** a small, well-guarded set of account/session-moderation verbs plus the help-page queue covers the overwhelming majority of "why do I have to log in to the game for this" moments. World-building and object manipulation (`[add`, `[set`, `[dupe`, decorate, spawners) should stay in the game client — they are target-driven, high-blast-radius, and gain nothing from a web form. +> ⚠ **Amended 2026-09-01 — see [§8a](#8a-amendment--2026-09-01).** That thesis holds for *ad-hoc staff +> action*, and the Event System does not contest it: a GM still does the target-driven parts in-client. +> What was reopened is narrower — declared, cap-bounded, ledgered, idempotent creation and **leased** +> mutation, both owned by an event run and both undone at teardown. `[set` / `[get` / `[add` and +> loot-table changes stay excluded exactly as this paragraph argues. + --- ## 2. How ServUO admin controls actually work @@ -284,6 +290,11 @@ All resolved by source inspection (ServUO checkout at ``). No live-shard ## 8. Decisions — locked 2026-07-12 +> ⚠ **Partly amended on 2026-09-01. Read [§8a](#8a-amendment--2026-09-01) with this section.** Two of +> the exclusions in the first bullet below — world creation, and item/gold grants — were reopened for +> the Event System under conditions that did not exist when this was locked. The bullet is left +> standing verbatim because it is the decision that was actually taken; §8a is what changed and why. + - **Scope:** Phase 1 (kick / ban / unban / broadcast) + Phase 2 (help-page queue) + Phase 3 second-wave. **The former Tier-H verbs (firewall, kill/res, jail, item/gold grants, set-access-level) are cut entirely** — not now, not later. - **Authorization:** enforced on the **website** (admin-only + moderator roles). Inbound sidecar commands are applied on the shard as **CoOwner-level** authority, with a hard floor that refuses any target at `AccessLevel >= CoOwner` (Owner-only shield). Write plane defaults **off** in `Bridge.cfg`. - **Attribution:** `web:` in `CommandLogging` and the `BanDealer` tag; no core edits. @@ -292,6 +303,70 @@ All resolved by source inspection (ServUO checkout at ``). No live-shard --- +## 8a. Amendment — 2026-09-01 + +**What changed:** §8's blanket exclusion of world writes is narrowed, in **two** places, and only for +the Event System. Everything §8 decided about the moderation write plane — its verbs, its +CoOwner-level authority, its Owner-only shield, its `Bridge.cfg` default-off switch, its attribution +and its logging — stands unchanged. + +**Decided by:** the org lead, 2026-09-01, answering +[`../website/EVENTS.md`](../website/EVENTS.md) §N1. The design of record is `EVENTS.md`; this section +records the reversal against the decision it reverses, so §8 is never read alone. + +### What is now permitted + +| | Permitted | Under what conditions | +|---|---|---| +| **1** | **Event-owned creation** — creatures, "simple" boss variants, oracle NPCs, temporary gates, temporary decoration | Created by an event run, counted against a module-declared cap dimension, recorded in the resource ledger with the run, step and actor that produced it, and **deleted by serial at teardown** on every terminal path — completion, cancellation and abort alike | +| **2** | **Leased mutation** of a plugin-declared allowlist of config values and object properties | Baseline recorded before the write, drift-checked on read-back, and **deadline-enforced by the game itself** — the shard restores baseline when the lease expires whether or not the website ever comes back | +| **3** | **Item and gold grants** | As (1): cap-bounded, ledgered and attributable — but `reversible: 'none'`. A granted item is not clawed back | + +### What stays excluded, permanently + +Unchanged by this amendment and not reopened: **loot-table changes**, and **arbitrary `[set` / `[get` +/ `[add`**. Both are the "change something with no baseline recorded" case, which is the thing §8 was +actually objecting to; no cap makes them safe. `firewall`, `kill`/`res`, `jail` and `set-access-level` +also stay cut — this amendment is about what an *event* may create and borrow, not about restoring the +Tier-H moderation verbs. + +### Why the reversal, and why item grants are called out separately + +**§8's objection was to an unrecorded change with a large blast radius, made from a web form.** It was +locked when the bridge was a read plane with three inbound verbs, none of which were moderation, and +when the only way to express "change the world" would have been a passthrough command string. In that +setting the objection was correct and it would still be correct today for anything shaped that way — +which is exactly why `[set` / `[get` / `[add` remain excluded. + +**The Event System proposes something with four properties that shape did not have**, all of which are +required before P12 ships and none of which are optional: + +1. **Declared, not typed.** A module declares a verb with its params and their types; the browser posts + an action id and a params object. There is no passthrough field and no place a request body can name + a sidecar route. +2. **Cap-bounded.** Every dimension is enforced in a conditional `UPDATE` with the cap in the `WHERE` + clause — a control a stolen admin session cannot talk its way past, because a role check is exactly + what a stolen session has already passed. +3. **Ledgered.** Every created object and every borrowed value is recorded in `event_run_resources` + before it is confirmed, with its run, step and actor, and with a declared revert. +4. **Idempotent on the wire.** A key minted once per step rides core → module → sidecar → plugin, and + the plugin answers a repeat with the original result rather than acting twice — so a lost + acknowledgement cannot double a spawn or a grant. + +**Item grants are named as their own row because they are a second reversal, not a consequence of the +first.** §8's cut list names "item/gold grants" explicitly and separately from world building; reading +the creation reopening as carrying grants along with it would be the silent reversal this amendment +exists to avoid. They are permitted on the same four conditions as creation — with the difference that +they are **not reversible**, which is why the cap and the ledger carry the entire weight for this row +and why the action ships declared as `reversible: 'none'` rather than pretending at a revert. + +**What has not changed:** the module boundary is still not a security boundary, and none of the above +defends against a hostile module. It defends against a compromised session and an operator mistake — +both of which *scheduling* makes larger, because a scheduled change happens while nobody is watching. +That is the reason the caps and the leases matter more here than the role check does. + +--- + ## 9. Where the code goes | File | Responsibility | diff --git a/website/ENGAGEMENT.md b/website/ENGAGEMENT.md index 76b524b..6a38c96 100644 --- a/website/ENGAGEMENT.md +++ b/website/ENGAGEMENT.md @@ -1676,7 +1676,7 @@ deferred to this phase on purpose: | | Question | Decision | |---|---|---| | §7.2 | one namespace, or two? | **ONE.** A trigger is a payload contract attached to an id that may also carry a subscription toggle | -| §7.1 Q6 | time-based triggers | **declare now, build after Phase 9** — `kind: 'scheduled'` is in the contract, the manifest and every stored declaration from today; no evaluator yet | +| §7.1 Q6 | time-based triggers | **declare now, build after Phase 9** — `kind: 'scheduled'` is in the contract, the manifest and every stored declaration from today. **The evaluator was assigned on 2026-09-01** ([EVENTS.md §N3](EVENTS.md#n--decisions)): the Event System runner becomes it, built in that workstream P2 | | — | what "narrower" means for a ceiling | an explicit **subset lattice**; two incomparable ceilings have no bound and the save is REFUSED, never guessed | **One namespace was the more invasive of the two options and it is worth stating what it bought and @@ -4078,6 +4078,14 @@ day it ships. caller's — so `kind` means something from the moment it is declarable rather than from the moment it is honoured. The lifecycle uses in §8.5 are the highest-value non-game triggers on the list and the first thing anyone will ask for after the IDOC mail works. + + > **The evaluator now has an owner, decided 2026-09-01** ([`EVENTS.md`](EVENTS.md#n--decisions) + > §N3). The **Event System runner** becomes the `kind: 'scheduled'` evaluator rather than a ninth + > poller being written for it — the runner already materialises due occurrences on a tick, and a + > second timer doing the same shape of work is how a codebase acquires two schedulers that + > disagree. It is built in that workstream's P2. Nothing in this phase's contract changes: a + > `scheduled` declaration means today exactly what it meant on 2026-08-29, and `ctx.events.emit` + > still refuses to fire one. 7. ✅ **ANSWERED — manual/operator-authored sends.** *"There is no campaign in the normal sense of email marketing. But admins can create all sorts of trigger conditions"*, and separately: *"lists can be built if they are powered by game data — say team X members or governors or whatever — thru the uo diff --git a/website/EVENTS.md b/website/EVENTS.md index 735916e..1e8c0b8 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -475,6 +475,15 @@ the fishing contest on Drachenfels is exactly that shape. | An orphaned claim | Reclaim on lease expiry, **without resetting `attempts`** | Engagement Phase 14's exact defect: a reclaim that reset state made `MAX_ATTEMPTS` unreachable and the row cycled forever, never terminal and therefore never retention-eligible. | | Two events overlapping | `concurrency_key` as a **template rendered from the run's params** — e.g. `invasion:{region}` | a flat definition-id key would wrongly stop the same definition running on two Rust servers, or in two regions, at once. | +> **This deployment runs one app instance, and every protection above is built anyway** +> ([§N4](#n--decisions)). The "two instances" column names the *hardest* contender for each row, not +> the only one: the unique index and the CAS equally protect a tick that runs long while the next one +> fires, and the lease and its reclaim are what recover a step whose process died mid-dispatch. Both +> happen with one container. What the single-instance decision changes is the **test**, not the +> design — P2 does not build the `--scale app=2` rig — so the multi-instance property is true by +> construction and unproven by experiment. Scaling this deployment, or adopting a rolling deploy that +> briefly runs two containers, is the trigger to build that rig before anything else. + ### Idempotency ```js @@ -825,15 +834,23 @@ live game world on a schedule. ### Which role, per surface Following the split module-uo already uses — `requireRole('admin','moderator')` for live operations, -`requireRole('admin')` for configuration that can break things: +`requireRole('admin')` for configuration that can break things — **with one deliberate departure, +decided in [§N2](#n--decisions): starting a run is `admin` only, while stopping one is not.** | Surface | Role | | --- | --- | | The calendar, run history, the run console, the diagnostic log | `staff` — the tier gate, nothing added | | Authoring and editing a draft | `admin`, `editor` | -| Publishing a version, starting a run, live controls | `admin`, `moderator` | +| Publishing a version, **starting a run** | `admin` only | +| **Live control of a run in flight** — cancel, abort | `admin`, `moderator` | | Any step whose action is above `notify`, and the action switchboard | `admin` only | +> **Why start and stop are gated differently.** Starting commits the deployment to everything the +> definition contains, unattended, up to every cap it declares — it wants the narrowest gate there is. +> Cancelling is incident response, and the incident is "the event is doing something wrong at 2am" — +> it wants the widest. A split that read consistent, with one role owning both buttons, would behave +> badly in exactly the case the moderator role exists for. + > **Keep the check in one function.** The whole authorisation decision — role, enablement, cap, shard > switch — should live behind a single `mayInvoke(user, action, run)` rather than being spread across > route middleware. Not for tidiness: it is what makes an EM-style delegation model a *later* option @@ -925,15 +942,15 @@ no URL moved. | `GET /admin/events` | staff | definitions, state, next occurrence, health | | `POST /admin/events` | admin, editor | create a draft | | `PUT /admin/events/:id` | admin, editor | edit the draft spec | -| `POST /admin/events/:id/publish` | admin, moderator | snapshot a version and go `ready` | +| `POST /admin/events/:id/publish` | admin | snapshot a version and go `ready` | | `DELETE /admin/events/:id` | admin | archive — never a hard delete while runs reference it | | `GET /admin/events/:id/versions` | staff | version history | -| `POST /admin/events/:id/verify` | admin, moderator | **dry run** — dispatch with `verify: true`, report cost against the caps | -| `POST /admin/events/:id/runs` | admin, moderator | start now; optional `rehearsal: true`, optional `scope` | +| `POST /admin/events/:id/verify` | admin, editor | **dry run** — dispatch with `verify: true`, report cost against the caps | +| `POST /admin/events/:id/runs` | admin | start now; optional `rehearsal: true`, optional `scope` | | `GET /admin/events/runs` | staff | run history across definitions | | `GET /admin/events/runs/:runId` | staff | status, phase, steps, caps, resources, cleanup | | `GET /admin/events/runs/:runId/log` | staff | the diagnostic log | -| `POST /admin/events/runs/:runId/pause\|resume\|advance\|cancel` | admin, moderator | `cancel` takes `{ cleanup, reason }` | +| `POST /admin/events/runs/:runId/pause\|resume\|advance\|cancel` | admin, moderator | live control of a run in flight; `cancel` takes `{ cleanup, reason }` | | `POST /admin/events/runs/:runId/steps/:stepId/skip\|retry\|confirm` | admin, moderator | `confirm` resolves a GM cue step | | `POST /admin/events/runs/:runId/cleanup` | admin | re-run cleanup over unreverted resources | | `GET /admin/events/catalog` | staff | registered actions, param schemas, risk classes, budget dimensions | @@ -944,6 +961,11 @@ no URL moved. | `GET /public/events/series/:slug` | — | the arc | | `GET /player/events/history` | auth | this account's participation | +> **Publish and start are `admin`; cancel is not** — that asymmetry is deliberate and decided in +> [§N2](#n--decisions). `verify` sits with `admin, editor` rather than with `start`: a dry run +> dispatches nothing, and the author who wrote the definition is exactly who should be able to price +> it against the caps before asking an admin to publish it. + A module registers actions server-side and adds **no routes** for them beyond its option endpoints, which is what keeps the browser from being able to name a transport. @@ -991,11 +1013,12 @@ that only *terminal* rows are eligible. Seventeen PR-sized slices (P0–P16) on an `edge` branch with one cutover, matching the discipline every previous workstream used. Phases 1–10 and 13–16 ship a complete, useful system that schedules, announces, orchestrates and records — **without creating or changing anything in the game world** — -and need no answer to [N1](#n--open-questions). Only P11 and P12 are gated. +and needed no answer to [N1](#n--decisions). P11 and P12 were the gated pair; **§N1 was answered on +2026-09-01 and the gate is lifted.** | Phase | | Repos | | --- | --- | --- | -| **P0** | Design of record; answer §N; amend `ADMIN_CONTROLS.md` §8 | `docs` | +| **P0** ✓ | Design of record; §N answered 2026-09-01; `ADMIN_CONTROLS.md` §8 amended | `docs` | | **P1** | Schema, CRUD, and the action registry with core as its first registrant | `website` `docs` | | **P2** | The runner — materialise, claim, advance, drain; leases, `missed`, concurrency | `website` | | **P3** | The minimal admin surface — **first demo** | `website` | @@ -1006,8 +1029,8 @@ and need no answer to [N1](#n--open-questions). Only P11 and P12 are gated. | **P8** | The resource ledger, leases and generated cleanup | `website` `docs` | | **P9** | UO wave 1 — the actions that need no protocol change | `module-uo` `docs` | | **P10** | Integrations — the `event.` triggers, participants, results, announce legs | `website` `docs` | -| **P11** ⚠ | *Needs N1.* Protocol: idempotency key, lease deadline, participation ledger | `servuo-plugins` `link` `module-uo` `installer` `docs` | -| **P12** ⚠ | *Needs N1.* UO wave 2 — the world verbs, owned or borrowed | `servuo-plugins` `link` `module-uo` `docs` | +| **P11** | *N1 answered.* Protocol: idempotency key, lease deadline, participation ledger | `servuo-plugins` `link` `module-uo` `installer` `docs` | +| **P12** | *N1 answered.* UO wave 2 — the world verbs, owned or borrowed | `servuo-plugins` `link` `module-uo` `docs` | | **P13** | The authoring UI proper — timeline, schema-driven steps, cap meter | `website` | | **P14** | Public calendar, player history, mobile — and the Android module-path fix | `website` `android-app` `docs` | | **P15** | Integration Kit chapter 5 — cannot merge before the cutover exists | `integration-kit` | @@ -1022,10 +1045,36 @@ P15 and P16 are not optional if anything ships to `main`; they are the cost of t --- -## N — Open questions +## N — Decisions -Each needs an architectural decision before the phase that depends on it. A recommendation is given -for every one. +**All eleven were settled by the org lead on 2026-09-01**, before any code, which closes the second +half of [`EVENTS_PLAN.md`](EVENTS_PLAN.md) Phase 0 and lifts the ⚠ gate from P11 and P12. Each entry +below keeps the recommendation it was decided against, so the reasoning survives alongside the answer. +**Nine went the way the recommendation argued; two did not** — N2, where starting a run became +`admin`-only, and N4, where the deployment is declared single-instance for good. + +Two answers have consequences outside this document, and both are written where they will be found +rather than only here: + +- **N1** is an amendment to [`../link/ADMIN_CONTROLS.md`](../link/ADMIN_CONTROLS.md) §8, recorded there + as a dated amendment with its reasoning. §8's locked scope is reopened in two places, not one — see + the decision below. +- **N3** closes [`ENGAGEMENT.md`](ENGAGEMENT.md) §7.1 Q6, whose "no evaluator yet" row is updated in + the same pull request. + +| | Question | Decision | +|---|---|---| +| N1 | Scope of the §8 reopening | **As recommended, including item grants** — §8 is amended in two places | +| N2 | Role split across the four surfaces | **Starting and publishing are `admin` only**; a moderator gets live control of a run already in flight and nothing more | +| N3 | Runner as the `kind: 'scheduled'` evaluator | **Yes** — one scheduler, not two | +| N4 | More than one app instance? | **No, and not later** — single-instance is a recorded assumption; the claim paths stay, the two-instance test is not built | +| N5 | Live console: poll, or core SSE? | **Poll** in v1 | +| N6 | Module-declared risk classes? | **No** — four closed values, core-owned | +| N7 | Plugin-side runtime and phase persistence | **The shard stays stateless about events**; reconciliation-on-reconnect reunites objects with phases | +| N8 | One event invoking another? | **Not as a feature** — composition covers it | +| N9 | Does core know a reward is a reward? | **No** — a reward is an ordinary action with its own cap dimension | +| N10 | Who verifies the lease allowlist? | **The plugin, at boot** — set, read back, restore; a key that does not take drops itself from the catalog | +| N11 | Caps per run, or also per period? | **Per run only** | **N1 · Confirm the scope of the §8 reopening.** The direction is settled; what needs writing down is the boundary. Recommendation: permit two things @@ -1036,6 +1085,27 @@ loot-table changes, and arbitrary `[set` / `[get` / `[add` — which is precisel no baseline recorded". This should be an amendment recorded in `ADMIN_CONTROLS.md`, not a silent reversal. +> **Decided 2026-09-01 — as recommended, item grants included.** +> +> **`ADMIN_CONTROLS.md` §8 is reopened in two places, and the amendment says both out loud.** §8's +> locked scope cut the Tier-H verbs "entirely — not now, not later", and that list names **item/gold +> grants** as well as world creation. Permitting an event to grant an item is therefore not a widening +> of §8's creation cut; it is a second, independent reversal of a decision that was taken deliberately, +> and an amendment that mentioned only the first would be the silent reversal §8 deserves not to get. +> +> **What makes the grant a different proposition from the one §8 refused** is the four properties it +> did not have then, all of which this document requires before P12 ships: it is *declared* by a module +> rather than typed as a command, *cap-bounded* by `uo.rewards` in a conditional `UPDATE` a stolen +> session cannot talk its way past, *ledgered* in `event_run_resources` with the run, step and actor +> that produced it, and *idempotent* on the wire so a lost acknowledgement cannot double it. §8's +> objection was to an unrecorded change with no baseline; a recorded, bounded, attributable one is a +> different argument. It remains `reversible: 'none'` — a granted item is not clawed back — which is +> exactly why the cap and the ledger carry the whole weight here and are non-negotiable. +> +> **Still permanently excluded, and this decision does not touch them:** loot-table changes, and +> arbitrary `[set` / `[get` / `[add`. Those are the "change something with no baseline recorded" case +> and no cap makes them safe. + **N2 · Confirm the role split across the four event surfaces.** Settled in principle — gate on the existing roles, add no permission system — so what is left is the mapping, and it is a judgement call rather than a design one. Recommendation in [§K](#k--security-model): @@ -1043,53 +1113,109 @@ viewing is the staff tier gate; authoring is `admin` + `editor`; publishing, sta are `admin` + `moderator`; anything above `notify` and the action switchboard are `admin` only. The one worth a second look is whether a moderator should be able to start a run at all. +> **Decided 2026-09-01 — no, and the recommendation was changed because of it.** Publishing a version +> and starting a run are **`admin` only**. A moderator keeps **live control of a run already in +> flight** — cancel and abort — and nothing more. +> +> **The asymmetry is the point.** Starting is the act that commits the deployment to everything the +> definition contains, unattended, up to every cap it declares; cancelling is the act that stops it. +> One of those wants the narrowest possible gate and the other wants the widest, because the moderator +> role exists for incident response and the incident here is "the event is doing something wrong at +> 2am". Gating the stop button on the same role as the start button would be the version of this that +> reads consistent and behaves badly. +> +> This is the one place the split departs from the `requireRole('admin','moderator')` / +> `requireRole('admin')` shape module-uo uses, and [§K](#k--security-model)'s table is the normative +> statement of it. + **N3 · Does the event runner also become the `kind: 'scheduled'` trigger evaluator?** Recommendation: yes. It closes ENGAGEMENT §7.1 Q6, and a second timer doing the same shape of work is how a codebase acquires two schedulers that disagree. +> **Decided 2026-09-01 — yes, as recommended.** [`ENGAGEMENT.md`](ENGAGEMENT.md) §7.1 Q6's row is +> updated from "no evaluator yet" in the same pull request as this decision. The evaluator itself is +> built by P2, which is the phase that acquires the tick. + **N4 · Is this deployment ever more than one app instance?** Affects nothing in the design — every claim is already multi-instance safe — but it decides how hard P2's tests must work, and whether the answer is "safe" or merely "documented". `docker compose up --scale app=2` is how the engagement workstream's Phase 13 acceptance walk found the seed race. +> **Decided 2026-09-01 — one instance, and not planned to change.** The `app` service runs as a single +> container; there is no load balancer, no rolling deploy with two containers briefly alive together, +> and no intent to add either. **P2 does not build the two-instance test.** +> +> **Every claim in [§E](#concurrency) stays exactly as specified, and this decision is not a licence to +> simplify them.** They are not only there for a second container: the unique index and the CAS are +> equally what protect a tick that runs long while the next one fires, and the lease and its reclaim are +> what recover a step whose process died mid-dispatch — both of which happen in a single-instance +> deployment. They also cost nothing to keep, being one index and one `WHERE` clause. +> +> **What this actually buys is a smaller P2 and one honest sentence:** the multi-instance property is +> *by construction*, not *proved*. If this deployment is ever scaled, or acquires a rolling deploy, the +> event runner is the component most likely to break and it breaks by duplicating world writes rather +> than by erroring — so scaling is the trigger to build the test that was skipped here, and this +> paragraph is where whoever does that will find out why it does not already exist. + **N5 · Does the live run console poll, or does core gain SSE?** Recommendation: *poll* in v1. Core has no SSE at all and adding one is a larger core change than this needs. If a live channel is ever built it should be core infrastructure with module-uo's two streams migrating onto it, not an event-system side effect. +> **Decided 2026-09-01 — poll, as recommended.** The run console polls; core gains no SSE in this +> workstream. + **N6 · May a module declare its own risk classes?** Recommendation: no. Four closed values, core-owned. A module-defined class is a module choosing which gate it sits behind. Budget *dimensions* are the opposite case and are module-declared, because core cannot name what a game counts. +> **Decided 2026-09-01 — no, as recommended.** Four closed, core-owned risk classes. Budget +> dimensions stay module-declared. + **N7 · Where does the plugin-side runtime live, and does a phase persist across a shard restart?** An asymmetry needing a decision: a spawned creature is an object and persists; a *phase* does not exist on the shard at all. Recommendation: keep the shard **stateless about events** — it owns objects and leases, core owns phases — and make reconciliation-on-reconnect the mechanism that reunites them. A plugin that remembered a phase would be a second event engine. +> **Decided 2026-09-01 — as recommended.** The shard is stateless about events: it owns objects and +> leases, core owns phases, and reconciliation-on-reconnect is what reunites them. + **N8 · Can one event invoke another?** Recommendation: not as a feature. It already works by composition — a second event's trigger condition can be `event.run.completed` — and a direct edge would need cycle detection, a depth bound and a story about cancelling a parent. Series and arcs cover the storyline case without invocation. +> **Decided 2026-09-01 — not as a feature, as recommended.** Composition covers it; there is no +> direct edge, therefore no cycle detection and no depth bound to design. + **N9 · Does core need to know a reward is a reward?** Recommendation: **no**. A reward is an ordinary action, enabled or not on the switchboard like any other, with its own cap dimension — a shard that wants none simply never enables it. An action returning a resource that carries a `memberKey` is enough to answer who received what, and inventing a category would put core back in the business of knowing what a game can give. +> **Decided 2026-09-01 — no, as recommended.** A reward is an ordinary action with its own cap +> dimension. Core learns no reward category. + **N10 · Who verifies the lease allowlist, and how often?** A key that is live-read today can become `static readonly` in a later ServUO release, and the failure is silent — the lease applies and nothing changes. Recommendation: the allowlist ships with the plugin and each key carries a boot-time self-check (set, read back, restore) that drops a key from the advertised catalog if it does not take. Better a capability that disappears loudly than one that lies. +> **Decided 2026-09-01 — as recommended.** The allowlist ships with the plugin and each key +> self-checks at boot, dropping itself from the advertised catalog if the write does not take. + **N11 · Are caps per run only, or also per period?** Recommendation: **per run only**. A cumulative cap that silently exhausts mid-event is a worse failure than one that refuses at authoring time, and with no delegation there is nobody whose longer-term allowance needs tracking. Per-period is a later addition if it is ever wanted. +> **Decided 2026-09-01 — per run only, as recommended.** Per-period caps are a later addition if +> they are ever wanted; nothing in the schema forecloses one. + --- ## O — Recommendations beyond the brief diff --git a/website/EVENTS_PLAN.md b/website/EVENTS_PLAN.md index 0c56a79..2915575 100644 --- a/website/EVENTS_PLAN.md +++ b/website/EVENTS_PLAN.md @@ -6,11 +6,13 @@ is proved. It re-specifies nothing — where this and `EVENTS.md` disagree, `EVE file is the one with the bug. **Seventeen phases, P0–P16.** Every phase is independently shippable and leaves the site working. -**Two are gated** on the open question `EVENTS.md` §N1 (the `ADMIN_CONTROLS.md` §8 amendment) and are -deliberately late: P11 and P12. Everything else — the engine, the scheduler, the calendar, the -conditions, the caps, the module contract, the ledger, the first wave of UO actions, the integrations, -the authoring UI, the public surface — needs no decision beyond P0. Those fourteen phases reach the -game only to *announce*, over verbs the write plane already carries; nothing in them creates or +**No phase is gated any more.** P11 and P12 hung on `EVENTS.md` §N1 (the `ADMIN_CONTROLS.md` §8 +amendment); §N1 and the other ten **were answered by the org lead on 2026-09-01**, which closes P0 +and lifts the gate. They stay deliberately late all the same — they are the phases that reach into +the world, and they belong after the ledger that makes them safe. Everything else — the engine, the +scheduler, the calendar, the conditions, the caps, the module contract, the ledger, the first wave of +UO actions, the integrations, the authoring UI, the public surface — needed no decision beyond P0. +Those fourteen phases reach the game only to *announce*, over verbs the write plane already carries; nothing in them creates or changes a thing in the world. --- @@ -96,9 +98,9 @@ early on purpose. ### Phase 0 — Design of record (`docs`) -> **Half-done.** The pull request that introduced this file and `EVENTS.md` discharges the first -> half — the design of record exists. What remains is the decisions, and they are the part that -> gates P11 and P12. +> **Complete, in two pull requests.** The first introduced this file and `EVENTS.md`; the second +> recorded the eleven decisions and amended the two documents they reach into. **All of §N1–N11 +> were answered by the org lead on 2026-09-01**, before any code, so nothing below is gated. Land `docs/website/EVENTS.md`, and answer §N1–N11. Two of those answers have consequences outside this document and should be written where they will be found: @@ -113,6 +115,24 @@ document and should be written where they will be found: **Ships:** the design of record, and two documents that stop contradicting each other. **Verify:** `docs` CI link check; every relative link resolves. +> **As answered, 2026-09-01.** Nine of the eleven went the way `EVENTS.md` §N recommended. **Two did +> not, and each changes a phase below.** +> +> - **N2 — starting a run is `admin` only.** A moderator gets live control of a run already in +> flight — cancel and abort — and nothing more. Starting commits the deployment to everything a +> definition contains, unattended; cancelling is incident response, and gating the stop button on +> the same role as the start button would behave badly in exactly the case moderators exist for. +> **P3 and P6 carry this**, and `EVENTS.md` §K's table is the normative statement of it. +> - **N4 — this deployment is single-instance, and not planned to change.** **P2 does not build the +> `--scale app=2` test.** Every claim path is built exactly as specified regardless: the unique +> index and the CAS equally protect a tick that overruns into the next one, and the lease and its +> reclaim recover a step whose process died mid-dispatch. The multi-instance property becomes +> true-by-construction rather than proved; `EVENTS.md` §E records what to build first if this +> deployment is ever scaled or acquires a rolling deploy. +> +> **N1 was taken in full, item grants included** — a reversal of *two* separate lines in +> `ADMIN_CONTROLS.md` §8 rather than one, and the amendment at §8a says both out loud. + --- ### Phase 1 — Schema, CRUD and the core action registry (`website` + `docs`) @@ -155,8 +175,11 @@ Also here: `missed` and the grace window, the lease and its reclaim, `concurrenc run params, and `health` as a column separate from `status`. **Ships:** a manually started event that broadcasts, waits, and completes. Demoable over curl. -**Verify:** `npm test`, including a **deliberate two-instance test** — the claim paths are the whole -point of this phase and a single-process test proves nothing about them. +**Verify:** `npm test`. **No two-instance test** — §N4 settled this deployment as single-instance, so +the `--scale app=2` rig the engagement workstream used is not built here. The claim paths are still +built exactly as `EVENTS.md` §E specifies, and they are still the point of the phase: they are what +protects a tick that overruns into the next one, and what recovers a step whose process died +mid-dispatch. Test both in-process. The decision is not licence to drop a CAS. **Two traps, both already paid for once in this codebase.** - **A reclaim must not reset `attempts`.** Engagement Phase 14's defect: a sweep that returned every @@ -173,6 +196,11 @@ point of this phase and a single-process test proves nothing about them. A list with state and next occurrence; a create/edit form; publish; start now; cancel; and a run console showing the phase, the step list with status and attempts, and the log. +**Publish and start now are `admin` only from this phase, not from P6** (§N2). Cancel is `admin` + +`moderator`. The routes get their real gates here even though the switchboard they will eventually +consult does not exist yet — a button that is admin-only later and open now is a gate nobody +notices was missing. + The spec is edited as **structured fields for the parts that exist** (name, description, schedule, phases with their steps) and the step's params as a raw JSON field — a deliberate placeholder that P13 replaces with the schema-driven editor. Say so in the UI, so it does not read as the finished thing. @@ -247,7 +275,10 @@ error, not an outage. enablement, cap, and the shard's own switch. Not for tidiness — it is what keeps an EM-style delegation model a later option rather than a redesign. -The role split from `EVENTS.md` §K is applied to the routes here. +The role split from `EVENTS.md` §K is applied to the routes here — completing what P3 started, and +including §N2's departure from the module-uo shape: **publishing and starting are `admin` only, +while cancelling and aborting a run in flight are `admin` + `moderator`**. Start and stop are +deliberately not the same gate. **Ships:** an admin switchboard, and a system that cannot be made to do an unbounded amount of anything. @@ -344,9 +375,9 @@ push; the ceiling on `run.failed` proved to exclude a moderator. --- -### Phase 11 ⚠ — Protocol: idempotency, lease deadlines, participation (`servuo-plugins` + `link` + `module-uo` + `installer` + `docs`) +### Phase 11 — Protocol: idempotency, lease deadlines, participation (`servuo-plugins` + `link` + `module-uo` + `installer` + `docs`) -**Gated on §N1.** A five-repo protocol bump, and the shape TEAMS Phase 1 already walked. +**§N1 answered 2026-09-01 — no longer gated.** A five-repo protocol bump, and the shape TEAMS Phase 1 already walked. - **Plugin:** an idempotency key on every inbound command with a bounded recent-key set that answers a repeat with the original result; a lease deadline timer that restores baseline **without being @@ -366,9 +397,10 @@ proving baseline returns anyway. --- -### Phase 12 ⚠ — UO wave 2: the world verbs (`servuo-plugins` + `link` + `module-uo` + `docs`) +### Phase 12 — UO wave 2: the world verbs (`servuo-plugins` + `link` + `module-uo` + `docs`) -**Gated on §N1.** The capability set the two UO programs demonstrate, each cap-bounded, ledgered, and +**§N1 answered 2026-09-01 — no longer gated**, and taken in full, so the item grant row below +stands. The capability set the two UO programs demonstrate, each cap-bounded, ledgered, and either owned or borrowed: | Verb | Owned or borrowed | Cap dimension | @@ -493,9 +525,9 @@ flowchart TD P8 --> P9["P9 · UO wave 1
no protocol change"] P5 --> P10["P10 · integrations"] P9 --> P10 - P0 -.->|"§N1"| P11["P11 ⚠ · protocol bump"] + P0 -.->|"§N1 answered"| P11["P11 · protocol bump"] P8 --> P11 - P11 --> P12["P12 ⚠ · UO world verbs"] + P11 --> P12["P12 · UO world verbs"] P7 --> P13["P13 · authoring UI"] P4 --> P13 P10 --> P14["P14 · public + mobile"] @@ -523,7 +555,8 @@ change. This is a real product and it is roughly a third of the work. **Stop after P10** — everything above plus the module seam, the ledger, the first UO actions and every notification channel. The website orchestrates and announces; a GM does the target-driven parts in-client, exactly as `ADMIN_CONTROLS.md` §8 argued they should. **This is the recommended minimum -viable scope**, and it requires no answer to §N1 at all. +viable scope**, and it was the scope that required no answer to §N1 at all. §N1 has since been +answered in full, so this is now a scope choice rather than a way around an open decision. **Stop after P14** — everything, including the world verbs and the public calendar. P15 and P16 are not optional if anything ships to `main`; they are the cost of the `edge` discipline.