Configuration
Everything Operator.Start accepts. Every field is optional. Operator.Start({}) is valid and gives you a console for the place owner and nobody else.
local admin = script.Parent
Operator.Start({
Commands = admin.Commands,
ClientCommands = admin.ClientCommands,
Types = admin.Types,
Guards = admin.Guards,
DefaultCommands = { "debug", "moderation" },
Roles = {
owner = { UserIds = { 1234567 }, Inherits = { "moderator" } },
moderator = { GroupId = 7654321, MinRank = 200 },
},
Audit = { Enabled = true },
Logs = { Enabled = true },
Client = { UI = true, ActivationKey = Enum.KeyCode.F2, Theme = "signal" },
})| Field | Default | Meaning |
|---|---|---|
Commands | none | an Instance whose descendant ModuleScripts are your commands |
ClientCommands | none | an Instance holding your client commands |
Types | none | your argument types; also delivered to authorised clients |
Guards | none | modules returning a guard function, or { guard, path } |
DefaultCommands | none | which built-in packs to enable |
Roles | place owner only | role definitions |
Audit | enabled | see Audit |
Logs | enabled | see Logs |
Client | UI on, F2 | see Client |
Logger | warnings to output | see Logging |
Serializer | pass-through | see Serialization |
Audit
Controls the audit log.
| Field | Default | Meaning |
|---|---|---|
Enabled | true | set false and no records are kept |
BufferSize | 200 | how many records the in-memory ring buffer holds |
DiscordWebhook | none | a webhook URL to attach the Discord sink to |
DiscordSend | "all" | which records reach Discord; see below |
DiscordFooterIcon | the Operator mark | the embed footer icon URL |
DiscordSend takes "all", "failures", or a (record) -> boolean of your own:
Audit = {
DiscordWebhook = webhookUrl,
DiscordSend = "failures",
}On a busy game one embed per command is a channel nobody reads. "failures" forwards only records where ok is false.
The filter applies only to the webhook. The ring buffer, the log panel and audit:query still see everything, so this changes what you are notified about, never what is recorded.
The webhook URL is configuration, not code
Pass it in from somewhere that is not a committed script: Roblox's secrets store, or a server-only module you do not commit.
Logs
Controls the log panel.
| Field | Default | Meaning |
|---|---|---|
Enabled | true | set false and the panel reports that logging is off |
BufferSize | 200 | how many moderation actions are kept |
Permission | none | a role required to read logs; by default, anyone who can run logs |
Client
Travels with the delivered bundle.
| Field | Default | Meaning |
|---|---|---|
UI | true | whether the built-in console is delivered |
Mount | none | a ModuleScript of yours to mount instead; see Custom Interfaces |
ActivationKey | Enum.KeyCode.F2 | the toggle key |
Theme | "signal" | signal, operator, iris, or a table of token overrides |
HistoryLimit | 500 | console lines kept; 0 means uncapped |
Icon | the >_ glyph | the topbar icon image, or false for none |
Setting both Mount and UI = true is an error rather than a silent choice between them.
What is bounded
Everything that accumulates is capped. Three of the caps are yours to set; the rest are fixed.
| What | Set with | Documented in |
|---|---|---|
| Console history, and command history | Client.HistoryLimit | Client |
| Audit records in memory | Audit.BufferSize | Audit |
| Moderation actions in memory | Logs.BufferSize | Logs |
| Dispatch, log query and report rate limits | fixed | Transport |
| Suggestion cache entries | fixed | SuggestionCache |
| Log lines per key, per window | fixed | Log |
| In-flight dispatches | fixed | cleared on result or timeout |
Validation
Config is checked before anything is built, and a mistake names the offending key:
Operator.Start: "Commmands" is not a config option; expected one of Audit, Client,
ClientCommands, Commands, DefaultCommands, Guards, Logger, Logs, Roles, Serializer, Types
Operator.Start: "nope" is not a default command pack; expected one of debug, fun, moderationA wrong-typed value, an unknown sub-key, or a bad Client.Theme all fail the same way. You never get a silent nil. A theme table is checked token by token:
Operator.Start: Client.Theme is invalid: "highlite" is not a theme token; expected one of
background, surface, text, subtext, highlight, success, error, severityBan, severityKick,
severityWipe, severityWarn, severityCustom
Operator.Start: Client.Theme is invalid: theme token "highlight" must be a Color3, got stringThe fix is always a typo in your own config table. The message names the key it did not recognise and lists the ones it accepts.
Start is idempotent. Calling it twice logs a warning and returns the same handle. It never double-registers or re-delivers a bundle.
The handle
Start returns a handle, so one-call setup does not lock functionality away:
local operator = Operator.Start({ DefaultCommands = { "debug" } })
operator.registry:registerCommand(myCommand)
operator.refreshAll()| Member | Use |
|---|---|
registry | register commands or types after startup |
dispatcher | run commands from your own code, add guards |
roles | query or invalidate a player's roles |
audit | query the log, add sinks |
logs | the log panel's store |
refresh(player) / refreshAll() | re-evaluate access after a role change |
stop() | tear everything down |
Commands registered after Start work immediately; the next refresh puts them in open consoles.
Logging
Operator is silent unless you opt in, so it never prints into someone else's output. With no Logger configured, warnings and errors go to Studio output prefixed with [Operator] and info is dropped.
Operator.Start({
Logger = function(level, message)
MyLogger[level](message)
end,
})Logger accepts a function, or a table with info, warn and error. See api/log for the throttling behaviour, which matters if your sink forwards to a webhook.
Serialization
Remote payloads pass through a serializer. The default is an identity pass-through, since Roblox already serializes remote arguments.
Operator.Start({
Serializer = {
serialize = function(value) return MySera.encode(value) end,
deserialize = function(value) return MySera.decode(value) end,
},
})Supply one if you are slotting Operator into a framework that has its own wire format.