Skip to content

Expand the sandboxed ctx surface so plugins like levels/counting don't need system:raw-client #39

Description

@DeadIndian

Summary

The isolation architecture is great, but the worker ctx.discord shim exposes far less than the broker already implements, so plugins that only need one or two "action" calls (add a role, delete a message, react) are forced to declare system:raw-client and run fully un-sandboxed. That's a blunt trade: to react with ✅ we hand a plugin the entire bot process.

We just migrated the registry plugins and hit this wall. Only todo and reminders could stay sandboxed. levels, counting, moderation, giveaways, invite-tracker, confessions all went raw-client — several of them for a single missing shim method.

The ask: expand the sandboxed surface so most plugins never need raw-client. Keep raw-client as the rare escape hatch it was meant to be (voice, cross-plugin introspection), not the default for anything that touches a role or a message.


Tier 1 — wire the RPC methods the broker ALREADY implements (near-zero effort, biggest win)

core/rpc/broker.js implements 14 discord* handlers and core/rpc/methods.js declares all 14 with capabilities. But core/rpc/worker-bootstrap.js createShimContext().discord only wires 5 of them:

Broker handler (exists) Declared in methods.js Exposed in worker shim?
sendRichMessage / sendDM / getGuild / getMember / fetchChannel
addRole / removeRole
deleteMessage
addReaction
timeout / kick / ban
sendEmbed / sendMessage

The plumbing and capability gates are done — the methods are just not surfaced to plugins. Adding them to the discordProxy object is a handful of lines each and unblocks:

  • levels → needs only addRole (role rewards). Otherwise 100% sandbox-safe. This alone removes its raw-client.
  • counting → needs deleteMessage + addReaction. Removes its raw-client.
  • moderationban/kick/timeout/deleteMessage cover ban, kick, timeout, purge. Leaves only ticket-channel-create + lock/slowmode for Tier 3.

Proposed additions to the shim (mirror the existing 5):

addRole:      (guildId, userId, roleId, reason)   => rpc.call("discord.addRole",      { guildId, userId, roleId, reason }),
removeRole:   (guildId, userId, roleId, reason)   => rpc.call("discord.removeRole",   { guildId, userId, roleId, reason }),
deleteMessage:(channelId, messageId)              => rpc.call("discord.deleteMessage",{ channelId, messageId }),
addReaction:  (channelId, messageId, emoji)       => rpc.call("discord.addReaction",  { channelId, messageId, emoji }),
timeout:      (guildId, userId, ms, reason)       => rpc.call("discord.timeout",      { guildId, userId, ms, reason }),
kick:         (guildId, userId, reason)           => rpc.call("discord.kick",         { guildId, userId, reason }),
ban:          (guildId, userId, reason, days)     => rpc.call("discord.ban",          { guildId, userId, reason, deleteMessageDays: days }),

(Confirm each broker case's expected params shape and match it.)


Tier 2 — real interaction replies (so sandboxed commands stay ephemeral)

Right now the interaction proxy in worker-bootstrap.js routes interaction.reply/editReply/followUp through discord.sendRichMessage — i.e. a public channel message. Consequences for every isolated command:

  • ephemeral: true is silently dropped (todo/reminders leak "🗑️ Task removed" into the channel).
  • No deferReply for slow work, no real editReply, no modal/component responses.

Proposal: route interaction responses back through the gateway on the real interaction token (Core holds it) instead of faking them as channel sends. A minimal interaction.reply({ ephemeral }) + deferReply + editReply would make the sandbox viable for the majority of slash commands that expect private responses.


Tier 3 — new RPC methods for the remaining raw-client cases

Each of these is a specific capability a plugin needs that has no sandbox equivalent today. Adding scoped RPC methods (capability-gated like the rest) would retire the last raw-client holdouts:

Need Plugin blocked Proposed RPC
Create/edit/delete a channel; edit permission overwrites (lock); set slowmode moderation (tickets, lock, slowmode) discord.createChannel, discord.editChannel, discord.setSlowmode, discord.setPermissionOverwrite (gated by ManageChannels)
Create + post via channel webhook confessions discord.createWebhook / discord.sendViaWebhook (gated by ManageWebhooks)
List a guild's invites + who invited whom invite-tracker discord.fetchInvites (gated by ManageGuild), plus forward inviteCreate / inviteDelete events (currently not in EVENTS_TO_FORWARD)
Button/component interactions + edit the message components giveaways forward component interactions to workers; allow interaction.update() / message component edit over RPC; serialize a components payload on send
Edit an already-sent message (embeds/components) giveaways discord.editMessage (gated by SendMessages/ManageMessages)

Also: thicker event payloads (opt-in)

core/PluginManager.js _serializeDiscordEvent strips messages to { id, content, author, guildId, channelId } and members to ids. That's fine for most, but a couple of features die quietly:

  • message attachments / reactions (needed for media-aware plugins)
  • full role objects on members (levels/invite-tracker do a role lookup)

Suggestion: allow a plugin to opt into a richer serialization for specific events (bounded, still no live objects), or expose discord.getMessage(channelId, messageId) so a plugin can fetch the extra fields on demand instead of running raw-client for them.


Impact if Tier 1 + 2 land

Plugin Today After Tier 1 After Tier 1+2+3
levels raw-client isolated isolated
counting raw-client isolated isolated
moderation raw-client mostly isolated (tickets/lock remain) isolated
giveaways raw-client raw-client isolated
invite-tracker raw-client raw-client isolated
confessions raw-client raw-client isolated

Tier 1 is a few lines and immediately reclaims 2 plugins with a third mostly there. Worth doing on its own.


Filed after migrating the registry plugins to manifest v2. ctx.discord currently exposes: sendToChannel, sendDM, getGuild, getMember, fetchChannel. Everything above is additive and stays behind the existing capability gates.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions