Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Mod permissions, channels and blob storage

Three host services that Enki B specifies for every mod (Enki B (docs/superpowers/specs/2026-09-27-enki-b-mod-platform-contract-design.md) §4, §7, §8). Their native Rust APIs and host plumbing exist on both hosts; the guest-facing WIT interfaces (manifold:perms/permissions, manifold:net/mod-channel, manifold:storage/blob-store) are published after the contract foundation gate, and mods’ declarations reach the host as ModDeclarations fields from contract M1b. Until then a declaration is registered through the native APIs below.

Permission nodes

A player has a level (Player < Admin) and a set of permission nodes, dotted names <namespace>.<name> such as build.edit. A limit is a valued node, an i64 per player, named <namespace>.<thing>.max.

  • One catalogue. PermissionCatalogue (crates/manifold-server-state/src/permissions/catalogue.rs) holds every node and limit. The platform programs register theirs at boot (permissions::platform (crates/manifold-server-state/src/permissions/platform.rs)): Enki’s build.* and world.*, Pygmalion’s animation.* and Talos’s talos_observe::nodes::NODES (crates/manifold-server-state/src/talos_observe/nodes.rs), each with its own defaults. A mod’s declarations join through register_mod, which enforces the namespace rule: a mod declares only under its own id and references only platform names and its own.
  • One seam. Every check goes through anim_contract::player_has_node (crates/manifold-server-state/src/anim_contract.rs), a bit test in the player’s resolved set in ConnectionPermission (crates/manifold-server-state/src/connection_permission.rs). Admin and the local host hold every node; a node the catalogue does not know is Admin’s only; an unknown connection holds nothing.
  • Resolution. A connection’s set is resolved at accept and on change, never per check, from its level, local-host status and, for a verified platform username only, permissions.ron. A node is held when its default is player or the file grants it, unless the file denies it; deny never removes anything from Admin or the local host. Limits take the local host’s max, else the declared default for the level.
  • The client’s copy. Each connection receives its own level, nodes and limits in S2C::PermissionsUpdate (client state: OwnPermissions (crates/manifold-client-state/src/mod_channel.rs)), once its grants are installed and after every change. It is for display and UI gating only.
  • Mod UIs. The reserved manifold:ui-actor-permission bundle entry carries the actor’s level, then the nodes it holds and the limit values within the mod’s scope (UiActorPermissionWire (crates/manifold-mod-types/src/ui_actor.rs)). A guest that decodes only the level still reads it.

permissions.ron

An optional file beside ops.ron in the server data directory. ops.ron keeps meaning Admin.

// permissions.ron — platform usernames, matched only when verified
(
    players: {
        "Carol": (grant: ["enki.use"]),
        "Dave": (grant: ["enki.use"], deny: ["build.edit"]),
    },
)

A missing file grants nothing; a malformed one grants nothing and logs a warning. /perm show <user>, /perm grant <user> <node> and /perm revoke <user> <node> (Admin) read and rewrite it atomically and update connected players at once. Tiers, default overrides and per-player limit values are not applied yet (#249); a file that uses them loads its players section and warns.

Platform namespaces

The namespaces platform programs own are reserved mod ids (reserved.rs (crates/manifold-modloader/src/reserved.rs)), listed with their owners in the platform-namespaces table (docs/architecture/extension-contracts.md).

Mod message channel

A mod declares named channels between its halves (ChannelDecl (crates/manifold-mod-types/src/channel.rs)): a direction, a size cap, a token bucket and an optional requires node. Every message rides one reliable transport channel, CH_MOD (7), as C2S::Mod or S2C::Mod, so a large mod payload never delays engine events.

  • Handles. Both sides build the same ModChannelRegistry (crates/manifold-host-core/src/mod_channel.rs): mods sorted by id, then each mod’s channels in declaration order. A guest numbers its channels by declaration position.
  • Server delivery. The inbound phase stages each message (mod_channel (crates/manifold-server-state/src/mod_channel.rs) in server state); before the tick’s guest systems, deliver_server_inbound drops and counts messages on an unknown handle, against the channel’s direction, over its size cap, from a sender without its requires node, or over the sender’s token bucket. The rest reach the mod’s inbox in arrival order with the sender’s player, level, and nodes and limits within the mod’s scope, captured at delivery.
  • Limits. A declaration is clamped by the server’s ChannelLimits; an inline message never exceeds 1 MiB. Buckets refill per server tick in integer milli-tokens, identically on both hosts.
  • Replies (server_send_to, server_broadcast) queue S2C::Mod on the connection at once. The client checks its sends too (client_send), so a client learns it is rate limited instead of being dropped.

Only reliable inline messages are served. Transfers come later; an unreliable-latest channel is served reliably, capped at the datagram size.

Blob storage

ModBlobStore (crates/manifold-server-state/src/mod_blobs.rs) keeps per-mod blobs in the world save, in the world scope (per mod) and the world-player scope (per mod and durable player id). Both hosts store them in one redb table, mod_blobs_v1 (table helpers (crates/manifold-saves-types/src/mod_blobs.rs)), in the same database as the chunks: the native save store and the browser’s OPFS-backed RedbOpfsSaveStore.

  • Keyed by the host. Every operation names the mod id the host authenticated; a guest never states it.
  • Deferred results. Each call returns a request id; results drain through take_completions on both executors.
  • Saved with the world. A write is staged and reaches the table in the world’s next save transaction, so blobs never disagree on disk with the world they describe. Reads see staged changes first; a failed save stages its rows again.
  • Quotas. 1 GiB per mod for world and 256 MiB per mod per player for world-player (no world-player quota in browser singleplayer); a write over quota fails and changes nothing.
  • Paths are 1 to 8 /-separated segments, at most 256 bytes, with no empty, . or .. segment.

Regression coverage

  • Catalogue, resolution and namespace rule: unit tests in manifold-server-state (permissions, connection_permission); permissions.ron and /perm in manifold-server.
  • Channel registry and token buckets: manifold-host-core mod_channel tests; blob store: manifold-server-state mod_blobs; the redb table: manifold-saves-types mod_blobs (the helpers both hosts use).
  • On a real server over the local transport: mod_platform_e2e.rs (crates/manifold-server/tests/mod_platform_e2e.rs) (ordering, every drop reason, sender context, replies, permission updates, blobs across a restart).

The browser runs the same server-state code in its worker; no browser test exercises these services end to end yet.