feat: declare rust as the module's identity capability
All checks were successful
PR Checks / server-tests (pull_request) Successful in 14s
PR Checks / client-build (pull_request) Successful in 14s
PR Checks / frozen-manifest (pull_request) Successful in -1m4s

Phase 5 is the Android app's leg of this module's read path (R10), and it
gates its Rust navigation on one capability string the way `module-uo`'s five
shard rows gate on `shard`. There was no such string here: the five this module
declared all name a SURFACE, and core flattens every started module's
capabilities into one list, so `servers` is a word another module could declare
tomorrow and silently reveal these screens on a site that does not run Rust.

`rust` is the string only this module can mean. It is asserted against
`module.json`'s own `id` rather than a literal, so the two cannot drift.

The README says why it is not redundant with `id`: `id` is a mount prefix, and
MODULE_API.md §2.9 forbids a client inferring a route from a capability. Gating
on `id` would quietly make those the same thing.

Decided by the org lead as D16, 2026-09-16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-16 21:55:17 -05:00
parent 7e1f037aad
commit 8df850f73e
3 changed files with 40 additions and 1 deletions

View File

@@ -59,6 +59,28 @@ events, the live map, Discord commands — arrives phase by phase. **Nothing is
has something behind it:** a declared trigger nothing emits and a declared slot nothing fills are has something behind it:** a declared trigger nothing emits and a declared slot nothing fills are
both surfaces an operator can configure and then wait on, which is worse than an absent one. both surfaces an operator can configure and then wait on, which is worse than an absent one.
### What a client feature-detects on
`module.json` declares six capability strings, and `GET /api/v1/public/modules` hands them to any
client that asks — the website's own nav, and the Android app (`docs/modules/rust/PLAN.md` R10).
Five of them name a surface: `servers`, `killfeed`, `leaderboard`, `presence`, `wipes`.
The sixth is `rust`, and it names **the module itself**. It looks redundant beside `id`, and it is
not, for two reasons worth writing down before somebody tidies it away:
- **A client that asks "is this module installed" has nowhere else to ask.** Core flattens every
started module's capabilities into one list, so `servers` alone is a word another module could
declare tomorrow and silently reveal this one's screens. `rust` is the string that can only mean
this module, and it is the single gate a whole navigation group hangs on — exactly the job `shard`
does for `module-uo`.
- **`id` answers a different question.** It is a *mount prefix* (§2.1 requires it to equal the
directory core loads the module from), and `MODULE_API.md` §2.9 is explicit that a client must
never infer a route from a capability. Gating on `id` would quietly make the two the same thing,
and the day a client builds `/<id>/servers` from it, the contract that lets this module move its
own pages is gone.
An unknown capability is absent, and no route is ever derived from one.
## Build and check ## Build and check
```bash ```bash

View File

@@ -12,5 +12,5 @@
"admin": ["/rust"], "admin": ["/rust"],
"player": ["/rust"] "player": ["/rust"]
}, },
"capabilities": ["servers", "killfeed", "leaderboard", "presence", "wipes"] "capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes"]
} }

View File

@@ -148,3 +148,20 @@ test('the modules protocol version agrees with the manifest it ships beside',
assert.strictEqual(typeof sidecar.PROTOCOL_VERSION, 'number') assert.strictEqual(typeof sidecar.PROTOCOL_VERSION, 'number')
assert.ok(sidecar.PROTOCOL_VERSION >= 1) assert.ok(sidecar.PROTOCOL_VERSION >= 1)
}) })
test('an identity capability is declared, and it is the module id (phase 5, D16)', () => {
// Core flattens every started module's capabilities into ONE list, so a client
// asking "is this module installed" needs a string only this module can
// declare. `servers` is not that string — it names a surface, and another
// module could name it too — which is the whole reason this one exists beside
// the five surface words.
//
// It is asserted against `manifest.id` rather than against the literal "rust"
// so that the two cannot drift: the day the id changes, the capability a
// client gates a whole navigation group on has to change with it.
assert.ok(
manifest.capabilities.includes(manifest.id),
`module.json must declare "${manifest.id}" as a capability — it is the only string a client can` +
' use to tell this module apart from any other, and the Android app gates its Rust rows on it',
)
})