docs(modules): the module-rust plan — 18 decisions of record and a 21-phase schedule #249
@@ -1,6 +1,6 @@
|
||||
# `module-rust` — the plan
|
||||
|
||||
**Status:** approved in outline 2026-09-15, not started. **Seventeen decisions of record, no open
|
||||
**Status:** approved in outline 2026-09-15, not started. **Eighteen decisions of record, no open
|
||||
questions.** Audited against the whole contract, not just the game-facing chapters (§7); the event and
|
||||
engagement catalogues are §9 and §10.
|
||||
|
||||
@@ -380,6 +380,88 @@ probe cannot see all of core's** — several core endpoints are mounted at the t
|
||||
under a prefix. A noun from our own domain that equals the module id cannot collide, where
|
||||
`/servers` or `/map` very well might.
|
||||
|
||||
### R18 — plugin configuration is editable from the site, with a generated form and an auto-reload
|
||||
|
||||
**Decided 2026-09-15 (org lead).** An admin edits any loaded plugin's configuration from the website
|
||||
and it reloads automatically. Two tiers:
|
||||
|
||||
- **Base:** the site reads `oxide/config/<Plugin>.json` and **generates a form from the values
|
||||
themselves** — a boolean becomes a toggle, a number a numeric field, a string a text box, an array
|
||||
a list, a nested object a group. It is derived at read time, so **it works for whatever plugins
|
||||
happen to be installed**, including ones added or removed after we shipped.
|
||||
- **Advanced:** raw JSON editing for anything the generated form cannot express.
|
||||
|
||||
This is the same posture as R2 — the site as the authority over the game host — but a different
|
||||
*shape*. R2 is continuously reconciled state pushed on every connect; this is **request/reply,
|
||||
on demand** ([kit][kit] ch. 3 §2a). Do not build it on the permission mirror.
|
||||
|
||||
**The mechanics Oxide gives us, all verified 2026-09-15:** configs live at
|
||||
`oxide/config/<PluginName>.json`; `oxide.reload <name>` rereads one; and **`OnPluginLoaded` /
|
||||
`OnPluginUnloaded` are real hooks** in the Server category, so *whether a reload actually succeeded
|
||||
is observable* rather than assumed.
|
||||
|
||||
#### The trap that would silently corrupt every float
|
||||
|
||||
**JavaScript cannot tell `1` from `1.0`, and Oxide configs deserialize into typed C# classes.**
|
||||
`JSON.parse('{"Rate":1.0}')` yields the number `1`, and `JSON.stringify` writes it back as `1` — so
|
||||
a naive read-modify-write of a config file **silently rewrites every whole-numbered float as an
|
||||
integer**, on fields nobody touched. Newtonsoft may coerce it, or may throw, and a throw at load
|
||||
means the plugin does not come back.
|
||||
|
||||
So: **never parse the whole document, mutate, and re-serialize.** Edit the file *textually* — a
|
||||
surgical replacement of the edited key's value — or use a parser that preserves number literals. The
|
||||
fields at risk are exactly the ones a Rust server tunes: gather rates, multipliers, scales.
|
||||
|
||||
#### Five more limits of inferring a schema from values
|
||||
|
||||
Worth stating because the feature's whole promise is that it works without knowing the plugin:
|
||||
|
||||
- **Empty arrays and `null` carry no type.** Nothing can be inferred; render them advanced-only.
|
||||
- **Enum-like strings are indistinguishable from free text.** There is no allowed-value set to read,
|
||||
so a string field is a text box and cannot be validated.
|
||||
- **There are no descriptions, no minimums and no maximums.** The key name is the entire label —
|
||||
which is survivable because Oxide convention favours readable keys (BetterChat really does ship
|
||||
`"Maximal Titles"` and `"Reverse Title Order"`).
|
||||
- **Nested objects and arrays of objects need recursion**, a depth limit, and a fall-back to raw JSON
|
||||
past it.
|
||||
- **The file after a reload may not be what we wrote.** Oxide merges missing defaults and saves, so
|
||||
re-read after reloading rather than assuming our write is the current state.
|
||||
|
||||
#### Safety, and why it needs more than the usual
|
||||
|
||||
**A bad config does not fail the write — it fails the next load, and the plugin stays down.** Since
|
||||
R6 and R17 make four plugins *required*, a broken ZoneManager config takes event participation with
|
||||
it. So the write path is:
|
||||
|
||||
1. Read with a **version** (hash or mtime) and require it back on write — an operator editing on
|
||||
disk at the same time gets a conflict rather than a silent overwrite.
|
||||
2. Validate the edited document parses.
|
||||
3. **Back up the current file**, write, reload.
|
||||
4. **Watch for `OnPluginLoaded` within a window.** If it does not arrive, **restore the backup and
|
||||
reload again, automatically**, and report the failure with whatever Oxide logged.
|
||||
|
||||
That rollback is the feature's real content. Without it this is a web form that can take the shard's
|
||||
plugins down one typo at a time.
|
||||
|
||||
**Two more obligations.**
|
||||
|
||||
**Redact secrets.** Plugin configs routinely hold API keys and Discord webhooks. A config reader
|
||||
hands those to anyone who can open the page. Mask values whose key matches the usual shapes — key,
|
||||
token, secret, password, webhook — and treat them **write-only**, exactly as the platform already
|
||||
treats the uo-link token. This is a real leak vector and the generated-form approach walks straight
|
||||
into it.
|
||||
|
||||
**Gate it on its own site permission and audit every write** — who changed which key, from what to
|
||||
what, and whether the reload succeeded. It is an admin writing to the game host's filesystem, which
|
||||
is the most powerful thing the site can do to a server.
|
||||
|
||||
#### One distinction to keep
|
||||
|
||||
**Editing a plugin's config file is not a lease.** A lease (§9) borrows a *convar* for a while and
|
||||
the game restores it on a deadline. This writes a *file* and is permanent until someone changes it
|
||||
back. They look similar from a web form and they are not the same mechanism — an event should never
|
||||
reach for this one.
|
||||
|
||||
### R17 — ZoneManager joins the required base set, because events need to know where people are
|
||||
|
||||
**Decided 2026-09-15 (org lead).** **[ZoneManager](https://umod.org/plugins/zone-manager)** (k1lly0u,
|
||||
@@ -544,8 +626,8 @@ re-checks it.
|
||||
honest reading is that the first list was a game-bridge plan with a website module bolted on, where
|
||||
the kit treats the module as the bulk of the work.
|
||||
|
||||
0–4 produce a working read-only multi-server Rust site that an operator can actually install. 6–7 are
|
||||
the permissions product. 9 is Teams. 10 is the notifications set, whose catalogue is **§10**. 12–13
|
||||
0–4 produce a working read-only multi-server Rust site that an operator can actually install. 6–7b are
|
||||
the permissions and remote-administration product. 9 is Teams. 10 is the notifications set, whose catalogue is **§10**. 12–13
|
||||
are events, whose catalogue is **§9**. 14 is the map. 18 is how any of it reaches somebody who is
|
||||
not us. The Android legs (5, 8, 11, 15) each trail the
|
||||
website surface they consume by one phase, per R10.
|
||||
@@ -562,6 +644,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
| 5 | **Android leg A** (R10). Capability-driven shell from `GET /api/v1/public/modules`, plus the phase-4 screens | Android-app | The app renders a Rust site it has never seen, and a UO site unchanged |
|
||||
| 6 | **Identity** (R1), and the `admin.users.detail` slot (R13) | 3 + docs | A player links an account in-game; an operator sees the Steam id inside core's own user page |
|
||||
| 7 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
|
||||
| 7b | **Mod configuration from the site** (R18). Generated form from the live config values, raw-JSON advanced tier, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** on a failed load, secret redaction, its own permission and an audit trail | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why |
|
||||
| 8 | **Android leg B** (R10). Identity and permission surfaces | Android-app | A player links from the app |
|
||||
| 9 | **Teams from first-party clans** (R5). 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). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook — **the catalogue is §10**, including the in-game-popup question | Module-Rust + docs | The offline raid alert reaches the player whose base it was, and nobody else |
|
||||
|
||||
Reference in New Issue
Block a user