feat(demo): the public demo's module-side locks and seed hook #62

Merged
whitlocktech merged 1 commits from feat/demo-mode into edge 2026-10-26 05:52:27 -05:00
Member

Step 2 of docs/website/DEMO_MODE.md §10 (RunicGateway/website#222). Core half: RunicGateway/website#224 (MODULE_API 1.19.0). This PR needs it, so ci/core-ref.json pins #224's head for now and moves to the sha #224 merges as.

Off the demo nothing changes. Every rule below sits behind ctx.demoMode (new core.demoMode getter, read in one place: server/demo.js).

The locks

  • demoLocks in module.json: PUT/DELETE /admin/rust/servers/:id (§3.3). Core refuses them before the handler.
  • The console-command deny list (§3.4, DM11): quit, restart, oxide.unload, oxide.reload, rcon.*, fps.limit, server.tickrate.
    • Matching is by first word, lower-cased, with global. stripped and o. read as oxide.. So Global.O.Unload RunicGateway is caught.
    • It covers both actions (rust.command.run and rust.command.reward), teardown commands included.
    • At save: each action declares demoRefuses (DM18), so core answers 403 DEMO_LOCKED: "quit" is turned off on the demo site.
    • At run: the same sentence with retry: false, before the rig is asked.
  • The bridge's config (§3.5, DM14):
    • left out of GET …/files;
    • GET/POST …/file refused 403;
    • its rows left out of GET …/writes, and GET …/writes/:id refused.
    • The path is recognised by the plugin name RunicGateway as a floor (so a catalogue that cannot be read never unlocks it), and also by the catalogue's self.
    • The path is canonicalised first, so ./RunicGateway.json is caught. Without that, it would have passed the existing path-shape check and reached the host.
  • Other plugins' secrets (§3.5):
    • fields flagged by the editor's own SECRET_WORDS reading are redacted in the text and in the form fields, and marked locked, so the form shows them read-only;
    • a form edit to one is refused;
    • raw-JSON saves are refused (the text holds placeholders);
    • a file that does not parse is withheld whole (fails closed);
    • the 409 conflict's current file is redacted too.
  • Servers view: sidecarBaseUrl is left out of the admin list (hasToken stays). The form now tolerates its absence, so a save attempt reaches the server lock instead of crashing on undefined.trim().

The seed hook (§7.3, DM15)

onDemoSeed writes from fixture.modules.rust:

  • servers: upserted, with the token encrypted.
  • links: demo-player's stand-in Steam id. A link is a plain insert by design (D23), so the upsert is spelled out: a link the right owner already holds is left alone, and one held by someone else is moved.

The whole section is checked before anything is written.

The release (§7.5, DM12)

release.yml adds protocol (read from PROTOCOL_VERSION in server/sidecarClient.js, and failing the release if it is missing) to module-rust-<v>.json. The demo's wipe picks the newest release whose protocol equals the bundle's. Core's installer ignores the extra field.

Verified

  • npm test (server): 728 pass, 0 fail. New test/demo.test.js has 19 tests; each lock is checked on the demo and off it.
  • check:imports, check:bundle (the new demo.js/demoSeed.js are in ci/bundle.json), check:swagger and check:engagement all pass. engagement-triggers.json was regenerated (it embeds coreApi).
  • Client: build OK, 101 pass, check:externals OK.
  • The protocol-extraction snippet from release.yml, run locally: 14.
  • Live smoke: core at website#224 with DEMO_MODE=true, this branch installed, scratch DB, 13/13 probes passed:
    • seed:demo with a Rust section ran twice (the second run kept the link);
    • the server list has no URL;
    • PUT/DELETE of the link are locked;
    • quit and global.restart are refused at create, say hi saves, and an edit to a teardown of o.unload RunicGateway is refused;
    • the bridge config is refused (also as ./RunicGateway.json), and so are a raw save and a credential edit;
    • an ordinary form save goes through to the host;
    • demo-player sees the seeded link.
  • Not run against a real rig. The editor against live configs, and a denied command at run on the rig, are in step 5's walk (§10).

Docs: RunicGateway/docs PR on docs/demo-mode-module.

  • AI-assisted: written with Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Step 2 of `docs/website/DEMO_MODE.md` §10 (RunicGateway/website#222). Core half: **RunicGateway/website#224** (MODULE_API 1.19.0). This PR needs it, so `ci/core-ref.json` pins #224's head for now and moves to the sha #224 merges as. **Off the demo nothing changes.** Every rule below sits behind `ctx.demoMode` (new `core.demoMode` getter, read in one place: `server/demo.js`). ## The locks - **`demoLocks`** in `module.json`: `PUT`/`DELETE /admin/rust/servers/:id` (§3.3). Core refuses them before the handler. - **The console-command deny list** (§3.4, DM11): `quit`, `restart`, `oxide.unload`, `oxide.reload`, `rcon.*`, `fps.limit`, `server.tickrate`. - Matching is by **first word**, lower-cased, with `global.` stripped and `o.` read as `oxide.`. So `Global.O.Unload RunicGateway` is caught. - It covers both actions (`rust.command.run` and `rust.command.reward`), teardown commands included. - **At save:** each action declares `demoRefuses` (DM18), so core answers `403 DEMO_LOCKED: "quit" is turned off on the demo site.` - **At run:** the same sentence with `retry: false`, before the rig is asked. - **The bridge's config** (§3.5, DM14): - left out of `GET …/files`; - `GET`/`POST …/file` refused 403; - its rows left out of `GET …/writes`, and `GET …/writes/:id` refused. - The path is recognised by the plugin name `RunicGateway` as a floor (so a catalogue that cannot be read never unlocks it), and also by the catalogue's `self`. - The path is canonicalised first, so `./RunicGateway.json` is caught. Without that, it would have passed the existing path-shape check and reached the host. - **Other plugins' secrets** (§3.5): - fields flagged by the editor's own `SECRET_WORDS` reading are redacted in the `text` and in the form `fields`, and marked `locked`, so the form shows them read-only; - a form edit to one is refused; - raw-JSON saves are refused (the text holds placeholders); - a file that does not parse is withheld whole (fails closed); - the 409 conflict's `current` file is redacted too. - **Servers view:** `sidecarBaseUrl` is left out of the admin list (`hasToken` stays). The form now tolerates its absence, so a save attempt reaches the server lock instead of crashing on `undefined.trim()`. ## The seed hook (§7.3, DM15) `onDemoSeed` writes from `fixture.modules.rust`: - `servers`: upserted, with the token encrypted. - `links`: demo-player's stand-in Steam id. A link is a plain insert by design (D23), so the upsert is spelled out: a link the right owner already holds is left alone, and one held by someone else is moved. The whole section is checked before anything is written. ## The release (§7.5, DM12) `release.yml` adds `protocol` (read from `PROTOCOL_VERSION` in `server/sidecarClient.js`, and failing the release if it is missing) to `module-rust-<v>.json`. The demo's wipe picks the newest release whose protocol **equals** the bundle's. Core's installer ignores the extra field. ## Verified - `npm test` (server): **728 pass, 0 fail**. New `test/demo.test.js` has 19 tests; each lock is checked on the demo and off it. - `check:imports`, `check:bundle` (the new `demo.js`/`demoSeed.js` are in `ci/bundle.json`), `check:swagger` and `check:engagement` all pass. `engagement-triggers.json` was regenerated (it embeds `coreApi`). - Client: build OK, **101 pass**, `check:externals` OK. - The protocol-extraction snippet from `release.yml`, run locally: `14`. - **Live smoke**: core at website#224 with `DEMO_MODE=true`, this branch installed, scratch DB, **13/13 probes passed**: - `seed:demo` with a Rust section ran twice (the second run kept the link); - the server list has no URL; - PUT/DELETE of the link are locked; - `quit` and `global.restart` are refused at create, `say hi` saves, and an edit to a teardown of `o.unload RunicGateway` is refused; - the bridge config is refused (also as `./RunicGateway.json`), and so are a raw save and a credential edit; - an ordinary form save goes through to the host; - demo-player sees the seeded link. - **Not run against a real rig.** The editor against live configs, and a denied command at run on the rig, are in step 5's walk (§10). Docs: RunicGateway/docs PR on `docs/demo-mode-module`. - [x] AI-assisted: written with Claude Code (Claude Opus 5.5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-10-26 03:40:57 -05:00
feat(demo): the public demo's module-side locks and seed hook
All checks were successful
PR Checks / client-build (pull_request) Successful in 54s
PR Checks / server-tests (pull_request) Successful in 1m0s
PR Checks / frozen-manifest (pull_request) Successful in 1m19s
7a0d0842ba
website/DEMO_MODE.md §3.2, §3.4, §3.5, §7.3 and §7.5 (step 2 of §10).
Nothing changes off the demo; everything below sits behind ctx.demoMode.

- demoLocks in module.json: PUT and DELETE /admin/rust/servers/:id.
- The console-command deny list (DM11): quit, restart, oxide.unload,
  oxide.reload, rcon.*, fps.limit and server.tickrate, by first word,
  with global. stripped and o. read as oxide. Refused at save through
  core's demoRefuses (MODULE_API 1.19.0, DM18) and again at run, with
  retry: false, before the rig is asked. Teardown commands too.
- The config editor (DM14): the bridge's files are left out of the
  list and refused for reading, writing and in the write history, by
  name (canonicalised) and by the catalogue's self. Other plugins'
  credential fields are redacted in the text and the form and made
  read-only; raw-JSON saves are refused; a file that does not parse is
  withheld. The admin server list leaves out the sidecar URL.
- onDemoSeed (DM15): upserts the rig's server row with an encrypted
  token and demo-player's stand-in Steam link.
- The release's install manifest carries protocol, read from
  PROTOCOL_VERSION, for the wipe's bundle match (DM12).

coreApi is ^1.19.0; ci/core-ref.json pins website#224's head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit c61f9f29ec into edge 2026-10-26 05:52:27 -05:00
whitlocktech deleted branch feat/demo-mode 2026-10-26 05:52:29 -05:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-Rust#62
No description provided.