docs(modules): the module-rust plan — 18 decisions of record and a 21-phase schedule #249
@@ -385,7 +385,7 @@ under a prefix. A noun from our own domain that equals the module id cannot coll
|
|||||||
**Decided 2026-09-15 (org lead).** An admin edits any loaded plugin's configuration from the website
|
**Decided 2026-09-15 (org lead).** An admin edits any loaded plugin's configuration from the website
|
||||||
and it reloads automatically. Two tiers:
|
and it reloads automatically. Two tiers:
|
||||||
|
|
||||||
- **Base:** the site reads `oxide/config/<Plugin>.json` and **generates a form from the values
|
- **Base:** the site walks `oxide/config/` **recursively** 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
|
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
|
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.
|
happen to be installed**, including ones added or removed after we shipped.
|
||||||
@@ -400,6 +400,43 @@ on demand** ([kit][kit] ch. 3 §2a). Do not build it on the permission mirror.
|
|||||||
`OnPluginUnloaded` are real hooks** in the Server category, so *whether a reload actually succeeded
|
`OnPluginUnloaded` are real hooks** in the Server category, so *whether a reload actually succeeded
|
||||||
is observable* rather than assumed.
|
is observable* rather than assumed.
|
||||||
|
|
||||||
|
#### Discovery is a recursive walk, and it stops at `oxide/config/`
|
||||||
|
|
||||||
|
**Amended 2026-09-15 (org lead).** Configuration is **not** one flat `oxide/config/<Plugin>.json` per
|
||||||
|
plugin. Plugins nest — `oxide/config/<Mod>/whatever.json`, and deeper — and one plugin may own
|
||||||
|
several files. So discovery is a **recursive walk** of the config tree, and the UI groups by plugin
|
||||||
|
rather than assuming one file each.
|
||||||
|
|
||||||
|
Four things follow, and the first is a boundary rather than a detail.
|
||||||
|
|
||||||
|
**`oxide/data/` is not the settings surface, and must not be walked into.** `DataFileSystem` writes
|
||||||
|
to `oxide/data/`, and that is **live state, not configuration**. The base set makes the point by
|
||||||
|
itself: Kits keeps `Kits/kits_data.json` and `Kits/player_data.json`, ZoneManager keeps
|
||||||
|
`ZoneManager/zone_data.json`, Clans keeps `clan_data.json` (and a legacy `clans_data.json` beside it,
|
||||||
|
which is also a reminder that these names are not stable). Editing those from a web form edits
|
||||||
|
**players' kit cooldowns and the live zone definitions**, a running plugin overwrites the change on
|
||||||
|
its next save, and `oxide.reload` does not make most plugins safely re-read them. It is a different
|
||||||
|
problem with a different answer and it is deliberately out of scope. If a plugin's *settings* genuinely
|
||||||
|
live under `data/`, that is a per-plugin exception someone opts into knowingly, never something the
|
||||||
|
walk discovers on its own.
|
||||||
|
|
||||||
|
**The reload target cannot be inferred from the path.** `oxide/config/Foo/bar.json` may belong to
|
||||||
|
plugin `Foo`, or to something else entirely — the folder name is convention, not contract. So the
|
||||||
|
reload target is an **explicit field with the folder name as its default guess**, confirmable by the
|
||||||
|
admin. Infer it silently and the failure is the nastiest kind available here: we reload the wrong
|
||||||
|
plugin, observe `OnPluginLoaded` for *it*, and report success while the plugin that was actually
|
||||||
|
edited never re-read anything.
|
||||||
|
|
||||||
|
**A relative path from a web form is now a path-traversal surface.** Canonicalise the resolved path,
|
||||||
|
assert it is genuinely under the config root, reject absolute paths, and reject symlinks that resolve
|
||||||
|
outside. Before this amendment the feature addressed files by plugin name; now it addresses them by
|
||||||
|
path, and that is exactly the change that introduces the bug class.
|
||||||
|
|
||||||
|
**Bound the walk and the file.** A depth limit, a file-count limit and a per-file size cap — a
|
||||||
|
pathological tree must not be enumerated and a multi-megabyte JSON must not be loaded into a form.
|
||||||
|
And because one plugin can own several files, **the backup and rollback operate on the whole set a
|
||||||
|
save touches**, not one file at a time.
|
||||||
|
|
||||||
#### The trap that would silently corrupt every float
|
#### The trap that would silently corrupt every float
|
||||||
|
|
||||||
**JavaScript cannot tell `1` from `1.0`, and Oxide configs deserialize into typed C# classes.**
|
**JavaScript cannot tell `1` from `1.0`, and Oxide configs deserialize into typed C# classes.**
|
||||||
@@ -644,7 +681,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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 7b | **Mod configuration from the site** (R18). **Recursive** walk of `oxide/config/` (never `oxide/data/`), generated form from the live values, raw-JSON advanced tier, explicit reload target, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** over the whole file set, path-traversal guards, 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; a nested `<Mod>/x.json` is found and reloads the right plugin |
|
||||||
| 8 | **Android leg B** (R10). Identity and permission surfaces | Android-app | A player links from the app |
|
| 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 |
|
| 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 |
|
| 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