diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 8948535..ad91073 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -748,7 +748,7 @@ Each phase ends with its findings written down, as every workstream here does. | # | Phase | Repos | Done when | |---|---|---|---| -| 0 | **The rig.** ✅ **Done 2026-09-15 — as built and findings in §12.** Updated to the current wipe (the script was fixed *again*, properly), Oxide re-laid, base set installed, the grant path proven end to end. One acceptance criterion is **open**: zone occupancy needs a connected player and cannot be closed headlessly (§12.5) | docs | A current server boots with all four loaded, `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it | +| 0 | **The rig.** ✅ **Done 2026-09-15 — as built and findings in §12.** Updated to the current wipe (the script was fixed *again*, properly), Oxide re-laid, base set installed, the grant path proven end to end and both zone transitions observed live with a player connected. **Both criteria met** | docs | A current server boots with all four loaded, `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it | | 1 | **Protocol 1, three skeletons, and every bundle seam at once.** Plugin: bounded drop-oldest queue, one writer thread, tagged reconnect epoch, dial-out. Sidecar: listener, SQLite, always-on token auth, version header, rpc correlation. Module: `id: rust`, `/rust` on all three tiers (R14), `schema.sql` **and `purge.sql`**, the **`extensions`** declaration (§11.3), per-server sidecar tokens through **`ctx.secretBox`** (§11.4), the vite aliases and shims, `checkExternals`, `checkImports`, the swagger fragment and its staleness check, explicit `onBoot`/`onShutdown`, `capabilities` | all 3 + docs | One hello line travels game -> sidecar -> module; killing the sidecar does not stall the game; all five guards green on an untouched skeleton | | 2 | **Packaging and release.** `release.yml`, the install manifest, the `sha256`, the host allowlist — and a real install into a running core from a manifest URL | Module-Rust + docs | An operator installs the empty module from Admin -> Modules and it reaches `started` | | 3 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, and a wipe does not erase a player's history | @@ -1173,8 +1173,9 @@ The app needs no failure handling for this case because core does not expose the ## 12. Phase 0 as built — the rig, 2026-09-15 -The rig is current and the base set runs. Five things were learned that the plan had either wrong or -had never asked, and three of them change work in later phases. +The rig is current, the base set runs, and **both acceptance criteria are met**. Six things were +learned that the plan had either wrong or had never asked, and four of them change work in later +phases. ### 12.0 What the rig is now @@ -1284,34 +1285,67 @@ Three more things the source and the live rig agreed on: them to the zone's *entity* list instead. Useful to know before designing a condition that counts "players at the monument" on a server with scientists. -### 12.5 The one criterion phase 0 could not close, and why +### 12.5 The criterion is closed, and it revealed two ceilings on the rig > `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it -The first half is **done** — proven for both an online-known and a never-seen player, in both -directions, through the same `UserHasPermission` call every third-party plugin makes. +**Both halves done**, the second with the org lead connected. The zone was created on the player's +own position, and ZoneManager reported both transitions live: -The second half is **open, and not by choice of method.** A plugin's own permission check reads: - -```csharp -private bool HasPermission(ConsoleSystem.Arg arg, string perm) -{ - BasePlayer player = arg.Player(); - return !player || permission.UserHasPermission(player.UserIDString, perm); -} +``` +[probe] ENTER zone=rgtest player=76561198038695917 (whitlocktech) +[probe] zone=rgtest occupancy=1 [76561198038695917:whitlocktech] +[probe] EXIT zone=rgtest player=76561198038695917 (whitlocktech) ``` -`!player ||` — a command from RCON has no `BasePlayer`, so **the server console is unconditionally -allowed**. This is the standard Oxide idiom, not a ZoneManager quirk, and it means no console session -can ever observe a gate closing. Zone occupancy has the same shape from the other end: NPCs are -excluded by construction, so nothing but a real connected player puts a name in that list. +The exit was produced by **moving the zone off the player** rather than walking them out — +`CreateOrUpdateZone` on an existing id relocates the trigger volume and fires `OnExitZone` as it +leaves. Useful for testing presence without choreographing a person. -Everything up to the body was proven: a zone was created through the API, listed by both the probe -and ZoneManager's own `zone_list`, read for occupancy (a real empty list, so the call resolved), and -erased. What remains is one person, connected, standing inside it. +So **R17's "presence transitions as events" is verified**, which is the claim the participation +ledger and the advance conditions both rest on. -**This is a standing property of the rig, not a phase-0 leftover.** Any phase whose acceptance -depends on a permission actually *refusing* somebody, or on presence, needs a client attached. Worth -knowing at phase 7 and phase 12 rather than discovering there. +Two ceilings surfaced on the way, and both constrain later phases: + +**1. No console session can observe a gate.** The standard idiom is +`return !player || permission.UserHasPermission(...)` — a command from RCON has no `BasePlayer`, so +the console is unconditionally allowed. Anything whose acceptance needs a permission to actually +*refuse* somebody needs a client attached. + +**2. An admin account cannot see a refusal either — from most plugins.** The bypass is not uniform, +and the difference decides which phases can be demonstrated on the org lead's own account: + +| Plugin | Admin bypass | Demonstrable as owner? | +|---|---|---| +| Popup Notifications | `player.IsAdmin \|\|` — hard | **No** | +| Zone Manager | `authLevel > 0 \|\|` — hard | **No** | +| Kits (`RequiredPermission`) | `Configuration.AdminIgnoreRestrictions && IsAdmin(player)`, and Kits' own `IsAdmin` is the `kits.admin` **permission**, not auth level. The shipped default is **`false`** | **Yes** | + +So **phase 13 is demonstrable on this rig as it stands** — R16's entitlement gate applies to a server +owner like anyone else. **Phase 7 is not**, if its acceptance is "a grant made on the website gates a +third-party plugin in-game" against Popup Notifications or Zone Manager: that needs a **second, +non-admin Steam account**. Worth arranging before phase 7 rather than discovering there. + +### 12.6 R18's trees, as they actually look + +The four base plugins wrote their configs on first boot, so R18's two trees can be compared against +something real rather than predicted: + +``` +oxide/config/ Clans.json Kits.json PopupNotifications.json ZoneManager.json +oxide/data/ clan_data.json Kits/kits_data.json Kits/player_data.json + ZoneManager/zone_data.json + oxide.users.data oxide.groups.data oxide.covalence.data oxide.lang.data +``` + +R18's inventory of `data/` was exactly right. One nuance worth correcting, though: R18 motivates the +**recursive** walk with *"plugins nest (`config//x.json` and deeper)"*, and on a fresh install of +the base set **`config/` is flat — it is `data/` that nests.** The recursive walk is still correct +(other plugins do nest configs), but the nesting the plan cites as its reason is currently visible +only in the tree it must never walk. + +And a reason to hold that boundary harder than R18 states: **`oxide/data/` is where Oxide keeps its +own permission store** (`oxide.users.data`, `oxide.groups.data`). A config editor that strayed one +directory over would be editing R2's mirror underneath itself. [kit]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit