feat(rust): permissions as event rewards — lend or give any registered permission (PLAN_REDESIGNS §12)
All checks were successful
PR Checks / client-build (pull_request) Successful in 20s
PR Checks / server-tests (pull_request) Successful in 27s
PR Checks / frozen-manifest (pull_request) Successful in -1m32s

D342-D346 (docs#342). Two steps over the permission mirror's own tables, so
no plugin, sidecar or protocol change:

- rust.perm.lend, "Permissions until the event ends": a kit reward's lease,
  one rust_perm_run_grants row per recipient per permission, the message on
  one row each; change/ledger with rust.kit.entitle's revert and reconcile;
  budget rust.permissions.
- rust.perm.give, "Permissions for good": a standing rust_perm_grants row per
  website user per permission, scoped to the step's server, source `event`,
  note "event run <run>, step <step>"; a grant already there is left alone,
  and a retry's own grants are counted as given; the message rides a
  notice-only run-grant row; irreversible/none; budget rust.permissions.kept.
- Both: several permissions through core's `multiple` param (MODULE_API
  1.14.0, at most 10); a required "I am sure" toggle refused unticked in the
  dry run and at run time, naming the permissions (D343); a permission the
  server's catalogue lacks refused by name; a blank message tells nobody.
- rust.options.permissions: the enabled servers' catalogues, grouped by the
  registering plugin (by prefix when none), sorted plugin then permission
  without case, a row noted "Only on X" when not on every server.
- Permission manager: an event-given grant shows "given by event run N,
  step M"; staff turn it off like any grant.
- coreApi ^1.14.0; ci/core-ref.json pinned to website#213's head until it
  merges; engagement-triggers.json regenerated; routes.manifest.json
  unchanged (no new routes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-12 15:49:44 -05:00
parent 89557ddf66
commit 63c82d5a2f
12 changed files with 942 additions and 18 deletions

View File

@@ -191,12 +191,15 @@ function playerState(facts, view, player, permission) {
if (direct) {
const wider = direct.sources.some((s) => (s.type === 'userGrant' || s.type === 'steamGrant') && s.scope !== view.server.id)
const fromEvent = direct.sources.every((s) => s.type === 'runGrant')
// Given for good by an event (PLAN_REDESIGNS.md §12.5): a standing grant like
// a staff member's, toggled off like one, and marked with its note.
const given = direct.sources.find((s) => s.type === 'userGrant' && s.origin === 'event')
let chip
if (!facts.registered.has(permission)) chip = <Chip tone="bad" title="No loaded plugin registers it here">not registered here</Chip>
else if (facts.notLanded.has(`${player.steamId}:${permission}`)) chip = <Chip tone="bad">did not land</Chip>
else if (facts.landed.has(`grant ${player.steamId} ${permission}`)) chip = <Chip tone="good">granted</Chip>
else chip = <Chip tone="warn">waiting for a sync</Chip>
return { on: true, chip, wider, fromEvent, excepted: null, viaGroups }
return { on: true, chip, wider, fromEvent, given: given ? given.note || 'an event' : null, excepted: null, viaGroups }
}
if (viaGroups.length) {
@@ -294,6 +297,7 @@ function PlayerPanel({ view, player, act, busy }) {
<Row key={perm}>
<code style={{ flex: 1, fontSize: '0.8rem' }}>{perm}</code>
{s.fromEvent && <Chip tone="info" title="An event gave it; the event’s revert takes it back">from an event</Chip>}
{s.given && <Chip tone="info" title={`Given for good by ${s.given}; turn it off to take it back`}>given by {s.given}</Chip>}
{s.chip}
<Toggle on={s.on} disabled={busy || s.readOnly || s.fromEvent} onChange={(on) => toggle(perm, on)} />
</Row>