docs(modules): module-rust phase 0 as built — the rig is current, and four plan claims were wrong #251
@@ -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/<Mod>/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
|
||||
|
||||
Reference in New Issue
Block a user