docs(modules): phase 0 fully closed — both zone transitions observed live

The org lead connected, so the occupancy criterion is met: a zone created
on the player's own position reported OnEnterZone, occupancy=1 by name,
and OnExitZone. R17's "presence transitions as events" is verified, which
is what the participation ledger and advance conditions rest on.

The exit was produced by relocating the zone rather than walking the
player out — CreateOrUpdateZone on an existing id moves the trigger
volume and fires OnExitZone. Useful for testing presence unattended.

Two ceilings recorded, both constraining later phases:

- No console session can observe a gate (the !player || idiom).
- An admin account cannot see a refusal from most plugins either, and the
  bypass is NOT uniform. PopupNotifications (player.IsAdmin) and
  ZoneManager (authLevel > 0) hard-bypass; Kits' RequiredPermission gate
  is config-gated with a shipped default of false, and Kits' own IsAdmin
  is a permission rather than auth level. So phase 13 is demonstrable on
  this rig as it stands and phase 7 is not — phase 7 needs a second,
  non-admin Steam account.

Adds §12.6: R18's two trees as they actually look now the configs exist.
Its data/ inventory was exactly right, but config/ is FLAT on a fresh
base set — it is data/ that nests, i.e. the nesting R18 cites to justify
the recursive walk is currently only visible in the tree it must never
walk. Also: oxide/data/ holds Oxide's own permission store, so straying
one directory over would edit R2's mirror underneath itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-15 16:40:08 -05:00
parent 98bed8ff3d
commit b12c6dfde4

View File

@@ -748,7 +748,7 @@ Each phase ends with its findings written down, as every workstream here does.
| # | Phase | Repos | Done when | | # | 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 | | 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` | | 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 | | 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 ## 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 The rig is current, the base set runs, and **both acceptance criteria are met**. Six things were
had never asked, and three of them change work in later phases. 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 ### 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 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. "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 > `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 **Both halves done**, the second with the org lead connected. The zone was created on the player's
directions, through the same `UserHasPermission` call every third-party plugin makes. 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: ```
[probe] ENTER zone=rgtest player=76561198038695917 (whitlocktech)
```csharp [probe] zone=rgtest occupancy=1 [76561198038695917:whitlocktech]
private bool HasPermission(ConsoleSystem.Arg arg, string perm) [probe] EXIT zone=rgtest player=76561198038695917 (whitlocktech)
{
BasePlayer player = arg.Player();
return !player || permission.UserHasPermission(player.UserIDString, perm);
}
``` ```
`!player ||` — a command from RCON has no `BasePlayer`, so **the server console is unconditionally The exit was produced by **moving the zone off the player** rather than walking them out —
allowed**. This is the standard Oxide idiom, not a ZoneManager quirk, and it means no console session `CreateOrUpdateZone` on an existing id relocates the trigger volume and fires `OnExitZone` as it
can ever observe a gate closing. Zone occupancy has the same shape from the other end: NPCs are leaves. Useful for testing presence without choreographing a person.
excluded by construction, so nothing but a real connected player puts a name in that list.
Everything up to the body was proven: a zone was created through the API, listed by both the probe So **R17's "presence transitions as events" is verified**, which is the claim the participation
and ZoneManager's own `zone_list`, read for occupancy (a real empty list, so the call resolved), and ledger and the advance conditions both rest on.
erased. What remains is one person, connected, standing inside it.
**This is a standing property of the rig, not a phase-0 leftover.** Any phase whose acceptance Two ceilings surfaced on the way, and both constrain later phases:
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. **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 [kit]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit