From 22159ec78fdd3ae51319e22f1451faaf8e0b6001 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 15 Sep 2026 21:51:11 -0500 Subject: [PATCH] docs(modules): the Carbon rig, R19 proven, and one refuted claim rust-carbon (id 18, 87fb1f67) is built on the same egg with FRAMEWORK=carbon, a clean install rather than a converted one. Install 122s, boot 543s. R19 is proven. The byte-identical RunicGateway.cs that runs on the Oxide rig, with no conditional compilation in it at all, loaded on Carbon 2.0.259.0 and behaved identically - same startup line, same no-stall retry against an absent sidecar. That contract now holds on three platforms from one source file: Windows/Mono, Linux/Oxide, Linux/Carbon. R18's amendment is confirmed the best way it could have been. The plugin's own config, written by the same Oxide-compat API on both rigs, landed at oxide/config/RunicGateway.json on one and carbon/configs/RunicGateway.json on the other. Had the R18 editor used a literal path it would not have found its own plugin's config on half of all installs, and no test would have caught it. One claim is REFUTED, and it was wrong about Oxide as well as Carbon. CARBON.md said Oxide stores permissions as JSON and Carbon as Protobuf or SQLite. Both rigs say otherwise: both store Protobuf, under IDENTICAL filenames (oxide.users.data, oxide.groups.data), differing only in directory - Carbon writes its own data into files named after Oxide. This strengthens R2's API-only rule rather than weakening it: a file reader would have worked on both rigs today and broken silently for the one operator who ran c.migrate_perms_sql. Two things nobody had thought to claim, found by looking. Carbon auto-creates a third default group, moderator, which R2's full-set push must tolerate or report drift for ever. And c.plugins exposes per-plugin hook telemetry that is most of section 6's fired-hook mechanism, free, and only on Carbon. Also: a wrong console command is indistinguishable from success at the API. Pterodactyl's command endpoint returns 204 either way and Carbon prints nothing for an unknown command, so oxide.plugins on Carbon looks exactly like it worked. Anything driving the console must read a log to know. CARBON.md gains a scorecard (section 10) and drops its unproven banner. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 --- modules/rust/CARBON.md | 97 ++++++++++++++++++++++++++++++++++++------ modules/rust/PLAN.md | 71 ++++++++++++++++++++++++++++--- 2 files changed, 150 insertions(+), 18 deletions(-) diff --git a/modules/rust/CARBON.md b/modules/rust/CARBON.md index c863d35..61a20ab 100644 --- a/modules/rust/CARBON.md +++ b/modules/rust/CARBON.md @@ -16,10 +16,10 @@ code has to care. Everything not listed here is identical by construction. > and wins any disagreement, exactly as uMod does for [`OXIDE_API.md`](OXIDE_API.md). Nothing here > is a Runic Gateway contract. > -> **Not yet proven on a live Carbon server.** Every claim here is read off metadata or source. This -> project's own record on that is poor — phases 0 and 1 each found source-read claims a running -> server contradicted — so treat the whole document as *the hypothesis phase 3 tests*, not as -> established fact. +> **Verified on a live Carbon server on 2026-09-15** — Carbon **2.0.259.0** `[2026.09.03.0]` on +> Linux, the `rust-carbon` rig (PLAN.md §14.5). Three of the four load-bearing claims held. **One was +> wrong, and it was wrong about Oxide as well as Carbon** — see §4. Corrected in place; §10 is the +> scorecard. --- @@ -57,6 +57,8 @@ compiler simply evaluates the unknown symbol as false. | Symbol | Meaning | |---|---| | `CARBON` | The framework is Carbon | +| `RUST` | The game is Rust | +| `OXIDE_PUBLICIZED` | Compiled against publicised Oxide assemblies | | `WIN`, `UNIX` | Host operating system | | `STAGING`, `AUX01`, `AUX02` | Rust branch | | `RUST_ABV_`, `RUST_BLW_`, `RUST_IS_` | Rust protocol above / below / exactly `` | @@ -65,6 +67,11 @@ compiler simply evaluates the unknown symbol as false. This works because the bridge plugin ships as **source** and is compiled by whichever framework loaded it. It would not work for a precompiled DLL — a reason, among others, not to ship one. +**Confirmed on the live rig**: `carbon/config.json` reports +`"ConditionalCompilationSymbols": ["CARBON", "RUST", "OXIDE_PUBLICIZED"]`, and the list is an +operator-editable setting (`c.addconditional` adds to it), so treat the three above as the ones +present by default rather than the ones guaranteed. + ### At run time `#if` is decided when the file is compiled, which is what we want for API differences. Where a @@ -116,7 +123,7 @@ the two roots are obtained. --- -## 4. Permissions — same API, different persistence +## 4. Permissions — same API, same format, different directory Every member R2 depends on exists with the same name and the same argument shape (`Carbon.Common/src/Oxide/Libraries/Permissions.cs`): `RegisterPermission`, `PermissionExists`, @@ -127,6 +134,9 @@ Every member R2 depends on exists with the same name and the same argument shape Two differences, and they pull in opposite directions. +**All of the member names above were confirmed present on the live Carbon rig**, which loaded and ran +our plugin against them unchanged. + **The return type differs, and the portable answer is the one we already chose.** Carbon's `GrantUserPermission` returns `bool`; Oxide's returns `void` — which is [§12.2](PLAN.md#122-four-rules-the-r2-permission-push-must-obey)'s finding, that a grant naming an @@ -138,14 +148,43 @@ is the framework that *would* have told us, and we still cannot listen. Carbon's signature also takes `BaseHookable` where Oxide's takes `Plugin`. Passing `this` is correct on both; a variable typed `Plugin` is not. -**The store is not a file we can read.** Oxide persists to JSON — `oxide/data/oxide.users.data` and -`oxide.groups.data`. Carbon persists to **Protobuf or SQLite**, switchable at run time -(`c.migrate_perms_proto`, `c.migrate_perms_sql`, with the SQLite path itself relocatable via -`-carbon.sqlpermsdb`, default `server/identity/carbon.perms.db`); `Oxide Overrides/PermissionSql.cs` -and `PermissionStoreless.cs` are the pluggable backends. +### The store — **this section was wrong, and the truth is worse** -R2 never planned to read the store file, so this changes nothing — but it **closes the option -permanently**, which is worth stating once. Drift detection reads the API, or it does not work. +> **Corrected 2026-09-15 against both live rigs.** This document previously said *"Oxide persists to +> JSON; Carbon persists to Protobuf or SQLite"*, and offered that difference as the reason not to read +> the file. **Both halves were wrong.** The real shape is more dangerous than the one that was +> imagined, which is the only reason it is worth the space. + +Read off the two running servers, byte for byte: + +| | Oxide rig | Carbon rig | +|---|---|---| +| Path | `oxide/data/oxide.users.data`, `oxide.groups.data` | `carbon/data/oxide.users.data`, `oxide.groups.data` | +| First bytes | `0a 16 0a 07 64 65 66 61 75 6c 74 …` | `0a 17 0a 07 64 65 66 61 75 6c 74 …` | +| Format | **Protobuf** | **Protobuf** | +| Default groups | `default`, `admin` | `default`, `admin`, **`moderator`** | + +**Neither framework writes JSON, and Carbon writes Carbon's data into files named after Oxide.** So +the trap is not "two formats you must tell apart". It is: + +1. **The filename is identical and tells you nothing**, so a reader keyed on `oxide.users.data` + silently follows the wrong framework's file if it ever guesses the directory wrong. +2. **The format is an undocumented binary**, not the JSON the name and the `.data` extension suggest. +3. **Carbon can change it out from under you at run time** and Oxide cannot. `PermissionSerialization` + in `carbon/config.json` defaults to `0` (the Protobuf above); `c.migrate_perms_sql` moves the whole + store to SQLite at `server/identity/carbon.perms.db`, itself relocatable via `-carbon.sqlpermsdb`. + `Oxide Overrides/PermissionSql.cs` and `PermissionStoreless.cs` are those backends. + +**R2's conclusion is unchanged and the argument for it is now much stronger.** A file reader would +have *worked* on both rigs today — same format, same names — and would break for the one operator +who ran a migrate command, with no error and no version marker to notice. **Drift detection reads the +API, or it does not work.** + +**One more thing R2 has to accommodate: Carbon creates a third default group.** `carbon/config.json` +names `PlayerDefaultGroup`, `AdminDefaultGroup` and `ModeratorDefaultGroup`, all auto-granted by auth +level (`AutoGrantPlayerGroup` / `AutoGrantAdminGroup` / `AutoGrantModeratorGroup`, all `true`). A +site that pushes its *full* group set on connect must not treat `moderator` as drift to be reported, +nor delete it — the framework will simply recreate it, and the site will report drift for ever. **Carbon does give R2 something Oxide's docs do not advertise: fourteen permission hooks**, a `Permissions` category of its own — `OnUserPermissionGranted`, `OnUserPermissionRevoked`, @@ -171,7 +210,17 @@ Carbon's 129 published commands are `c.`-prefixed. The ones with Oxide counterpa | `oxide.plugins` | `c.plugins` | Carbon can be configured to alias the old prefix, so an operator's muscle memory survives — but an -alias is opt-in and **we must never depend on one**. +alias is opt-in and **we must never depend on one**. **Confirmed on the live rig:** `c.version`, +`c.plugins`, `c.grant` and `c.group` all answered; **`oxide.plugins` produced no output at all**. Note +the shape of that failure — Pterodactyl's `command` endpoint returns `204` either way, and Carbon +prints nothing for an unknown command, so *a wrong prefix looks exactly like a command that worked.* + +`c.plugins` is also worth knowing about for a reason unrelated to permissions: **it reports per-plugin +`hook fires`, `hook time`, `hook memory`, `hook lag` and `hook exceptions`**, which is most of the +"log which of its expected hooks have fired at least once" mechanism [`PLAN.md`](PLAN.md) §6 requires +— for free, and only on Carbon. Useful when debugging *on* Carbon; **not a substitute for the +plugin's own counter**, which has to work on both. Our plugin appears there as +`Runic Gateway RunicGateway v0.1.0 … 2367ms [1077ms]`, under `Scripts`, with `failed plugins (0)`. **Where this reaches us is narrow but real.** R2 and R18 both act through the plugin API, not the console, so neither cares. The two that do care are **documentation** — every operator-facing @@ -305,3 +354,25 @@ framework, and that is a deliberate decision rather than an oversight. | Everything else | Unchanged | The honest summary: **Carbon costs three amendments and one extra rig, not a second codebase.** + +--- + +## 10. Scorecard — what the live rig confirmed and what it corrected + +Run 2026-09-15 against `rust-carbon` (Carbon **2.0.259.0** `[2026.09.03.0]` `21063e8`, Linux, +`production_build`, Rust 103/2633.288.1), with the Oxide rig alongside for comparison. + +| Claim | Verdict | Evidence | +|---|---|---| +| An `Oxide.Plugins` / `RustPlugin` source file loads unchanged | **CONFIRMED** | The byte-identical `RunicGateway.cs` that runs on the Oxide rig loaded as `Runic Gateway v0.1.0` in `2367ms`, printed the same startup line, and retried the absent sidecar the same way | +| The framework root is `carbon/`, config dir is `configs` (plural) | **CONFIRMED** | `/carbon/{configs,data,lang,logs,plugins,extensions,modules,managed,native,modifiers,temp,tools}`; **no `/oxide` directory at all** | +| `Interface.Oxide.ConfigDirectory` resolves there | **CONFIRMED, indirectly and decisively** | The plugin's own config was written to **`/carbon/configs/RunicGateway.json`** by the same code that writes `/oxide/config/RunicGateway.json` on the Oxide rig. A literal path in R18 would not have found it | +| Console prefix is `c.`, `oxide.` is not aliased | **CONFIRMED** | `c.version` / `c.plugins` / `c.grant` / `c.group` answered; `oxide.plugins` produced nothing | +| `#if CARBON` is defined | **CONFIRMED** | `carbon/config.json` → `ConditionalCompilationSymbols: ["CARBON", "RUST", "OXIDE_PUBLICIZED"]` — and two symbols this document had not known about | +| Carbon self-updates | **CONFIRMED** | `SelfUpdating.Enabled: true`, plus the egg refetching `production_build` every boot | +| *"Oxide stores JSON, Carbon stores Protobuf or SQLite"* | **WRONG — see §4** | **Both** store Protobuf, under **identical filenames**, differing only in directory. The refutation strengthens R2 rather than weakening it | +| The 13 uMod hook names missing from Carbon's catalogue | **NOT YET TESTED** | None is in a phase; the plugin's own fired-hook log is the standing answer either way | + +**Two things this document did not know to claim**, both found by looking rather than reading: +Carbon ships a **third default group** (`moderator`) that R2's push must tolerate, and `c.plugins` +exposes per-plugin hook telemetry Oxide has no equivalent for. diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 5443f6f..f651d92 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -1763,15 +1763,15 @@ including one finding that changes R20's shape. | | | |---|---| | Panel | `http://192.168.0.12` (no TLS — `https://` fails outright), node 1 `Main` | -| Server | `rust-oxide`, id **17**, identifier **`e6758c06`** | -| Egg | 18 `Rust Autowipe`, `ghcr.io/pterodactyl/games:rust` | -| `FRAMEWORK` | `oxide` | +| Servers | `rust-oxide` id **17** / **`e6758c06`**, and `rust-carbon` id **18** / **`87fb1f67`** (§14.5) | +| Egg | 18 `Rust Autowipe`, `ghcr.io/pterodactyl/games:rust` — both rigs, one egg | +| `FRAMEWORK` | `oxide` / `carbon` | | Limits | 8192 MB memory, 25600 MB disk — deliberately under half the node, so the Carbon rig fits beside it | -| Allocations | 21000 game (default), 21001 query, 21002 RCON, 21003 Rust+, **21004 held for the sidecar's `[web].bind`** | +| Allocations | oxide 21000-21004, carbon 21005-21009 — game, query, RCON, Rust+, **and one held for the sidecar's `[web].bind`** | | World | procedural, size 3000, seed 1234 | | SFTP | `192.168.0.12:2022` | -The RCON password is a generated 24-byte token rather than the old rig's `letmein`, kept out of this +The RCON passwords are generated 24-byte tokens rather than the old rig's `letmein`, kept out of this document and out of the repo. §6 named that shape as the thing nothing published should copy; this is the first rig where it was not copied. @@ -1943,6 +1943,67 @@ string is composed so that whatever the entrypoint prepends still lands on `Rust is more work and survives upstream changing its entrypoint; the second is free and depends on a line in somebody else's repository. Raised rather than settled. + +### 14.5 The Carbon rig, and R19 proven + +`rust-carbon` — id **18**, identifier **`87fb1f67`**, same egg, same world (procedural, 3000, seed +1234), same limits, allocations **21005-21009 with 21009 held for the sidecar**, `FRAMEWORK=carbon`. +**A clean install, never a converted one**, per R21: install 122s, boot 543s, running. + +**R19 is proven.** The byte-identical `RunicGateway.cs` that runs on the Oxide rig — no `#if CARBON` +anywhere in it, nothing conditional at all — loaded on Carbon **2.0.259.0** and behaved the same: + +``` +[INFO] Carbon 2.0.259.0 [2026.09.03.0] 21063e8 on Linux +[INFO] [Runic Gateway] protocol 1, serverId 'main', sidecar 127.0.0.1:7799 +[INFO] Loaded plugin Runic Gateway v0.1.0 by RunicGateway [2367ms] +[INFO] [Runic Gateway] cannot reach the sidecar: Connection refused - retrying quietly [RunicGateway Link|26] +``` + +That last line is the no-stall contract holding on its **third** platform now — Windows/Mono, +Linux/Oxide, Linux/Carbon — from one source file. [`CARBON.md`](CARBON.md) §10 is the full scorecard; +the parts that change decisions are below. + +**R18's amendment is confirmed the best way it could have been.** The plugin's own config, written by +the same Oxide-compat API on both rigs, landed at `/oxide/config/RunicGateway.json` on one and +**`/carbon/configs/RunicGateway.json`** on the other. D3 put the plugin's config inside R18's editor; +had that editor used a literal `oxide/config/`, **it would not have found its own plugin's config on +half of all installs.** No test would have caught it; only two rigs would. + +**And one claim was refuted — the one with the sharpest consequence.** `CARBON.md` had said Oxide +stores permissions as JSON and Carbon as Protobuf or SQLite, offering the difference as the reason +not to read the file. Both rigs say otherwise: + +| | Oxide rig | Carbon rig | +|---|---|---| +| Path | `oxide/data/oxide.users.data` | `carbon/data/`**`oxide.users.data`** | +| First bytes | `0a 16 0a 07 64 65 66 61 75 6c 74` | `0a 17 0a 07 64 65 66 61 75 6c 74` | +| Format | Protobuf | Protobuf | +| Default groups | `default`, `admin` | `default`, `admin`, **`moderator`** | + +**Same binary format, same filenames, different directory** — and Carbon writes *its* data into files +named after Oxide. **This makes R2's API-only rule more important, not less.** A file reader would +have worked on both rigs today and broken silently for the one operator who ran `c.migrate_perms_sql` +— no error, no version marker, just a site reporting drift against a store nobody is writing any +more. The rule survives; the reasoning behind it was wrong and is now right. + +Two things nobody had thought to claim, found by looking: + +- **Carbon auto-creates a third default group, `moderator`**, auto-granted by auth level alongside + `default` and `admin`. R2 pushes its *full* set on connect, so it has to tolerate a group the + framework will recreate the moment it is deleted — otherwise the site reports drift for ever. +- **`c.plugins` reports per-plugin `hook fires`, `hook time`, `hook memory`, `hook lag` and + `hook exceptions`** — most of §6's "log which expected hooks have fired" mechanism, free, and only + on Carbon. Useful when debugging on Carbon; **not a substitute** for the plugin's own counter, which + must work on both. + +**A warning about how a wrong console command fails here.** Pterodactyl's `command` endpoint returns +`204` whether or not anything happened, and Carbon prints nothing for an unknown command. So +`oxide.plugins` on Carbon — which is simply not a command — is indistinguishable from success at the +API. Anything driving the console has to read a log to know, which is the same conclusion §14.2c +reached about `oxide.reload` and the same shape R18's rollback needs. + + --- [rl]: https://gitea.whitlocktech.com/RunicGateway/Rust-Link