docs(rust): phase 12 as built and walked — §27.5-27.7, PROTOCOL.md §14

Walked on both rigs through real core: 21 keys observed live, the game's own deadline with core stopped, drift, reload and restart mid-hold, and the switch. Records two defects fixed in the phase and four found outside it and raised: the phase-7 grant owner bug, core restoring orphaned lease rows over a later run, core.lease dropping retry:false, and the 10s budget squeeze.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-23 21:14:09 -05:00
parent 46b565ac9b
commit 5f18097223
2 changed files with 252 additions and 2 deletions

View File

@@ -998,7 +998,7 @@ Each phase ends with its findings written down, as every workstream here does.
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* ✅ **Built 2026-09-23 — plan, as built and findings in §24** (D47–D58). Walked on the Oxide rig end to end and on Carbon hook by hook; the game's 100-clan ceiling accepted (D55); **one module per site** became a core rule on the way (§24.5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
| 10 | **Notifications and engagement** (R7). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §25 (D59–D68).** The criterion walked on the Oxide rig (two authorised owners alerted once each; bystander, raider and an unlinked owner told nothing), and the same plugin loaded on Carbon; a live walk found that every generic notice never said which server (§25.6). Streams, triggers with `ceiling` and `subjectKey`, audiences and engagement seeds — **the catalogue is §10, as corrected by §25.2**. The announce leg and the post hook are **not** in it (D62 amends R7), and protocol 7 widens the raid frame, which brings the two bridge repositories in | all 3 + docs | The offline raid alert reaches the player whose base it was, and nobody else |
| 11 | **Android leg C** (R10). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §26 (D69–D72).** The link check behind personal streams asked `module-uo` and locked the raid alert on every Rust site; it now asks the site's own module and holds back only push-on. A tickle is titled from the inbox row it names, and two links stopped opening the browser. **The relay hop was walked on `ntfy.sh` for the first time on any site**: a rig raid reached the emulator in 19 seconds, and after push was switched off in the app the next raid enqueued in-app only | Android-app + docs | A Rust notification arrives on a phone and can be switched off there |
| 12 | **Events: option sources and the leases** (§9, **as corrected by §27**). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised. **Plan of record in §27 (D73–D79)**: the target names the server, game convars only (vanilla Rust has no gather/craft/smelt rate), the weekend-VIP lease is a group-wide permission, an `EventsEnabled` switch off by default, seven-day holds, and no budget dimension until phase 13; protocol 8 | Module-Rust + 2 + docs | A leased value is observed changing in the running game and restored, per key; `rust.group.permission` expires without core asking |
| 12 | **Events: option sources and the leases** (§9, **as corrected by §27**). ✅ **Built and walked 2026-09-24 on both rigs — as built and findings in §27.5–27.7.** 21 keys walked live, two `spawn.max_*` left out; two defects of its own fixed (the Oxide grant owner, `EventsEnabled` not applying); four found outside it and raised. [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised. **Plan of record in §27 (D73–D79)**: the target names the server, game convars only (vanilla Rust has no gather/craft/smelt rate), the weekend-VIP lease is a group-wide permission, an `EventsEnabled` switch off by default, seven-day holds, and no budget dimension until phase 13; protocol 8 | Module-Rust + 2 + docs | A leased value is observed changing in the running game and restored, per key; `rust.group.permission` expires without core asking |
| 13 | **Events: the actions** (§9, R3, R16). `rust.kit.entitle` first, then `rust.prefab.place` and `rust.announce`; `reversible: 'ledger'`; the kit option source flags kits with no permission gate, plus **`reconcile()` and the boot-id watch calling `ctx.events.reconcile()`** (§11.1) | all 3 + docs | A reward granted at 03:00 is waiting in the kit menu when the player next logs in, and a revert withdraws it; a wipe reconciles the ledger instead of stranding it |
| 14 | **The live map** (R9). The map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch built on **our own** visibility layer (§11.2 — `shardVisibility` is `module-uo`'s, not core's) | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it |
| 15 | **Android leg D** (R10). Map and events | Android-app | The map renders on a phone with the same layer gates |
@@ -4252,6 +4252,110 @@ on the Oxide rig against real core, with an event run authored in the admin UI:
6. **Carbon.** The same plugin gets a sidecar on the Carbon rig (`INSTALL_RIG.md`), and at least one
key from each family is walked there. If that cannot be done, this section says so.
### 27.5 As built, 2026-09-24
[Rust-Plugins][rp], [Rust-Link][rl] and [Module-Rust][mr] on `feat/phase-12-leases`, into `edge`.
**Core is unchanged and `MODULE_API` did not move**, which D73 bought. Walked against real core on
`main` (a fresh `rustp12` database) with this module installed, on **both** rigs.
**The catalogue as shipped** is narrower than §27.2 planned, and each cut was made by the walk:
| Lease | Keys | Walked |
|---|---|---|
| `rust.decay.scale` | `decay.scale` | a lone twig foundation with `decay.tick` lowered on the rig lost 0.081 health per 32 s at 1, **0.404 at a lease of 5**, and 0.081 again after release |
| `rust.population` | **18** animal and vehicle populations | every one: the game's own target in `spawn.report` moved and came back (bear 18 → 45 → 18, and so on) |
| `rust.spawn.scalar` | `spawn.min_rate`, `spawn.min_density` only | `spawn.scalars`: the population rate 0.5 → 2 → 0.5, the density 0.5 → 1.5 → 0.5 |
| `rust.group.permission` | any registered permission on any group | a synthetic player in `default` passed `UserHasPermission` while the lease was held and failed it after |
- **The two `spawn.max_*` scalars are not lent.** The game interpolates between min and max by how
full the server is, so on an empty rig the effective values are the minimums and the maximums do
nothing that can be seen. §9's rule is that a key which cannot be seen doing something is not
advertised. A walk with players online is what would add them.
- **Every population is per square kilometre, vehicles included.** The game's own help text says
vehicles are "active on the server", but on the 9 km² rig a value of 3 is a target of 27.
**What the walk proved, against §27.4:**
1. **Each key's effect, observed**, as in the table. The first run held all four families through
core and tore them down at the end: every ledger row `reverted`, every value back.
2. **The game's own deadline, with the website gone.** A run took a 3-minute hold on a group
permission and on `decay.scale`, then core was **stopped**. At the deadline the plugin restored both
and emitted two `lease.expired` frames, which the sidecar stored. When core came back, its boot
reconcile asked about both holds, was told `held: false`, and marked both rows `orphaned` with the
module's reason.
3. **Drift.** `decay.scale 3` typed at the console during a hold. At teardown the row read `drifted`,
with *"the value is now "3" rather than what this run applied, so it was left alone"*, and the
world was left at 3.
4. **Reload and restart.** A plugin reload in the middle of three holds kept all three, deadlines
intact (`leases: 3 held`). A **server restart** in the middle of a convar hold and a permission
hold logged `leases: 1 held, 1 ended by a restart`: `decay.scale` came back at 1 and the permission
stayed granted. At teardown the permission was given back and the convar release found it
`already` restored. Both rows `reverted`.
5. **The switch.** With `EventsEnabled` off, an apply was refused with the switch named. It was
turned on **from the site's own config editor**, which is how an operator would do it (below).
6. **Carbon.** The same plugin file, with a sidecar installed in the Carbon rig's container the way
`INSTALL_RIG.md` describes. All 21 keys resolved there and none was `Saved`. Through core: bear
target 54, `decay.scale` 5 (0.47 per 32 s), `spawn.min_rate` 2, and a group permission
(`adminmodule.greet`, which Carbon registers itself) granted and then given back. All rows
`reverted`.
The permission mirror's side of §27.3 was walked too. A `perm.sync` sent during a hold listed the
pair under `leased` and not under `foreign`, and the site's wish was stored on the hold. At teardown
the pair stayed granted (`already true`), because the site had asked for it.
### 27.6 What the walk found that the plan did not say
**Two defects in this phase's own code, both fixed before merge:**
- **Every group-permission grant was a silent no-op.** `GrantGroupPermission(group, perm, this)`:
given an owner, Oxide first asks `PermissionExists(perm, owner)`, meaning *did this plugin register
the name*, and returns without a word when it did not (confirmed in the decompiled
`Oxide.Core.Libraries.Permission`). A lease only ever borrows another plugin's permission. The
plugin's read-back caught it (*"the store did not grant …"*), and the owner is now `null`.
- **`EventsEnabled` did not take effect when an operator changed it.** The site's editor saved the
file and answered `changed: true`, and the plugin went on refusing. It never re-reads its own
config, because it never reloads itself (§21.3). It is the first bridge setting an operator is
*meant* to change from the site, so a write to the bridge's own file now re-reads that one key.
The report says so: *"EventsEnabled took effect at once; the bridge's other settings apply on its
next reload."*
**Four findings outside this phase, raised and NOT fixed**:
- **The phase-7 permission mirror has the same owner bug.** `EnsureGroupPermission` and
`EnsureGrant` both call Oxide with `this` and count the grant without reading it back. On Oxide,
then, every site-authored grant of another plugin's permission has probably never landed, while the
report counted it as applied. It stayed hidden because every earlier walk used a permission that was
`unresolved` (Kits was not installed) and so never reached the call. The fix is one token in two
places plus a read-back. It needs a decision because it changes what the site has been told since
phase 7.
- **Core's cleanup restores `orphaned` lease rows, and that can overwrite a later run's world.** Run
2's rows were orphaned at core's boot reconcile, and run 2 was cancelled. Cleanup then tried to
move them back to `reverting`. That collided with run 3's live row for the same target on
`uq_evres_target` and failed on every sweep (13 times). Once run 3 released, the stale `restore()`
went through and **revoked the permission run 3 had deliberately left granted**. A lease's
compare-and-set cannot catch this when the later holder applied the same value. For a spawn,
reverting something that is gone is harmless. For a value, "no longer in force" plus a later
restore is a write of an old baseline over the present.
- **`core.lease` drops the module's `retry: false`.** Both of its failure returns rebuild the envelope
as `{ ok: false, error }`, so an `events-disabled` or `unresolved` refusal is retried three times.
The Carbon walk's step 27 did exactly that. Harmless, but it is noise and it makes
`retry: false` unreachable in a lease callable.
- **`core.lease` runs under the default 10 s with two module calls inside it.** The module fits by
giving lease calls a 4.5 s timeout (asserted in a test), which is below the sidecar's 10 s reply
timeout. A `budgetMs` on `core.lease` would remove the squeeze. §27.1 asked what a late-landing
retry does; the module's release-after-timeout (§14.7) covers the world side, and the question
stands for core.
### 27.7 What is not proven here
- **`spawn.max_rate` / `spawn.max_density`**, for want of players. They are left out, not guessed.
- **A real player seeing a kit unlock.** Kits is not installed on either rig and there is no second
client. The permission was observed through `UserHasPermission`, which is what Kits calls.
- **A crash, as opposed to a restart**, for the holds file. The same load path runs either way.
- **Walk 3's permission outcome as first observed.** It came out `false`. Walk 4 then showed the
intended behaviour (`already true`), and the cause turned out to be the core cleanup finding above,
not the plugin.
---
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app