Dicey Table

ModCapability

The twelve slugs manifest.capabilities.allowed can contain. Every one of the 25 api methods is gated on exactly one of them, and calling a method whose slug is absent throws Missing mod capability: <capability> synchronously — including for the methods that return a promise.

12 values, declared as modCapabilitySchema in packages/shared/src/modManifest.ts.

Omitting the capabilities block entirely defaults it to { version: "1", allowed: ["log"] }, so a mod that declares nothing can call api.log() and nothing else.

This page is the value table. Mod capabilities owns the full capability-to-method matrix, the thirteen detector regexes, the three checkpoints a declaration is validated at, and the exact rejection text — read that page when you are debugging a declaration.

The values#

Value Unlocks Contexts that accept it Surfaces it applies to
log log manifest.capabilities.allowed; the default when the block is omitted. Mod scripting only
spawn-object createObject manifest.capabilities.allowed Mod scripting only
register-action registerAction manifest.capabilities.allowed Mod scripting only
read-context getMySeat, getMyTeam, getTurn manifest.capabilities.allowed Mod scripting only
read-world getSnapshot, getObject, listObjects, getContainerContents, getHandObjects, getZoneObjects — all six least-privileged on every peer manifest.capabilities.allowed Mod scripting only
read-hidden-information getUnredactedSnapshot — the elevated read: real card faces, pile order and secretMetadata, from the host. Never implied by read-world, never granted by default. manifest.capabilities.allowed Mod scripting only
object-action objectAction manifest.capabilities.allowed Mod scripting only
saved-data getSavedData, setSavedData manifest.capabilities.allowed Mod scripting only
subscribe-events on — and therefore every hook manifest.capabilities.allowed Mod scripting only
ui getUiState, listUiElements, setUiElement, deleteUiElement manifest.capabilities.allowed Mod scripting only
play-sound playSound, setObjectSound manifest.capabilities.allowed Mod scripting only
plugin-call listPlugins, callPlugin — naming a declared function on an installed plugin. Also requires a matching manifest.plugins entry, and is not network access. manifest.capabilities.allowed Mod scripting only

Table scripting has no capability model at all. It is restricted instead by a three-type intent allowlist — spawn, object-action, transform — enforced on the host, so every table-scripting reference entry carries capability: none and there are no exceptions.

Two things a capability list does not do#

It does not stop you declaring more than you use. The validator walks the capabilities the script uses and checks each one is declared; it never walks the declared list looking for entries nothing calls. A manifest can declare all twelve against a script that only logs, and nothing warns. Least privilege here is a discipline you keep yourself. The one exception runs the other way: declaring plugins without plugin-call is rejected, because that manifest contradicts itself.

It does not tell you what a mod intends. It tells you what its script was scanned as calling.

read-hidden-information is the exception to everything below. It is the one grant whose absence genuinely withholds data rather than declaring an intent. Every read-world read is redacted to the least-privileged view on every peer, the host included, so a mod cannot reach a hidden card face by arranging to run on the host; getUnredactedSnapshot is the only door, its host-side check runs before the state is served, and detectScriptCapabilities matches the call by name so an undeclared use is a publish-time rejection. Treat its presence in a listing as "this mod can see hidden cards".

By design. Ten of the twelve grants are enforced in two places. requireCapability throws from inside the same untrusted JavaScript realm as the mod script (apps/web/src/mods/sandbox/modSandbox.html) — the error you meet while developing — and the host then re-validates the gated message against the mod's grants before acting on it, using the shared MOD_API_METHOD_CAPABILITIES map (apps/web/src/mods/SandboxedModRunner.ts). The host check is the authoritative one, because a script that posts to the host directly never runs the in-frame one. That covers log, spawn-object, register-action, read-world, read-hidden-information, object-action, play-sound, saved-data, ui and plugin-call.

read-context and subscribe-events are frame-local only. Neither sends the host a message to check: getMySeat, getMyTeam, getTurn and on are answered inside the frame, and the host pushes contextUpdate and hookEvent to every running frame unconditionally (apps/web/src/mods/SandboxedModRunner.ts, updateContext and dispatchEvent). A frame granted neither still holds seat, team and turn state and receives every hook payload in its own realm. For those two the declaration is disclosure, not a wall. Read a capability list as least-privilege disclosure all the same: what the platform scanned for, and what a reviewer or player can know about a mod before opening its code. The boundaries that hold regardless of what a manifest declares are separate from all of it — the opaque-origin iframe and its CSP, the neutered DOM, network and storage globals, the five scanner patterns, the host-side ten-action allowlist, the per-mod namespacing of saved data, and the ownership check on sound references. See Known limitations.

See also#