Log inGet started
What it does

dotbot_service

The DotBot bridge, minus its privileges.

src/lua/trusted/dotbot/ is the layer between the DotBot panel (user-VM Luau, untrusted) and the DotBot plugin (trusted scope, holds a person's ChatGPT / Claude sign-in). Everything in that layer that is a rule rather than a capability lives in this module: the job table, the consent choreography, the event ring, the mirror the read-only exports answer from, and the scrub every value passes through on its way out.

local Service = require("@builtin::modules.dotbot_service")
local service = Service.new({
    plugin = function() return rawget(_G, "dotbot") end,
    await = task.await,
    spawn = task.spawn,
    consentRequest = function(o) return consent.request(o) end,
    consentGranted = function(o) return consent.granted(o) end,
    openUrl = function(url) return system.openUrl(url) end,
    newJobId = function() return __crypto.uuid_v4() end,
    nowMs = function() return os.clock() * 1000 end,
})

Why the split

It is what makes the layer testable. The module is a factory: it holds no state of its own and no privileged global, so a test can construct an instance over fakes and drive every rule in it. Being a builtin module, a world script can construct one too — and wins nothing, because its instance talks to its own fakes. The one instance that talks to the real consent gate, the real plugin namespace and the real system.openUrl is built in the trusted VM, out of globals only the trusted VM holds, and is reachable only as trusted.call("dotbot.<method>", …).

The shape, and where it comes from

ConstraintConsequence
A trusted.call handler must not yield (trusted/exports.luau)Nothing that awaits is answered in the frame. Awaiting verbs are start/poll jobs; reads are answered from a mirror a spawned task refreshes.
There is no push from the trusted VM into a user VMThe panel drains a cursor (events(cursor)) rather than registering a callback — so no trusted code ever calls into a user VM, and a world script cannot install itself as the panel's event handler.

stop is the one verb answered in the frame: stopping is always allowed and always cheap, and a stop that needed a confirmation would be a stop that lands after the turn it was meant to end. Its outcome arrives as a turn event.

Signing in

A person's plan is reached by one of two flows, and the plugin says which. connectStart reads it from the plugin's own account state (M.signInFlow) and, when a logged-out account names none, asks the plugin — never the caller, whose choice of flow would be a choice of which endpoint the person is sent to.

Flowconnect.start answersThenFinished by
deviceCode (ChatGPT){ flow = "deviceCode", state = { userCode, verificationUrl, … } }the person types the code on the provider's pagecheckSignIn until the account connects
pasteCode (Claude){ flow = "pasteCode", state = { authorizeUrl } }the person signs in and the provider shows a codecompleteSignIn(provider, pasted)

cancelSignIn abandons either.

Only connect asks for a confirmation. The three that continue it do not, for stop's reason: they carry out a connect the person has already confirmed and is at that moment completing on the provider's own page, they spend nothing, and completeSignIn cannot connect anything on its own — the plugin holds a single-use verifier from the gated connect and answers "no pending login" to anything else. A second dialog inside one flow is how a person learns to stop reading the first.

Spending

sendStart and commandStart both ask for the spend grant, keyed by action, plugin and the agent's display name, remembered for the world session. A slash command is gated because a slash command can run a turn — /compact summarises a conversation with the model — so it spends the plan exactly as a message does.

What it will not do

  • Return a token, a credential id or a header. M.scrub drops every credential-named key at every depth, on every value leaving the layer — results and event payloads alike. It is the second line: DotBot's account and event types carry no token by construction. The names it drops are exact, not a substring match on token, so the usage counters a person is shown (tokens) and a turn's turnId survive.
  • Take a URL. openSignInStart(provider) reads the verification URL out of the plugin's own account state for the provider named, checks it is an http(s) page, shows a person its host in the dialog, and only then opens it. Account state naming none errors the job.
  • Answer, dismiss or count a consent dialog. There is nothing here that settles one; the gate is the engine's, and only a person at the keyboard answers it.
  • Put a caller's own words on a dialog. detail is resolved through the plugin's own agent and account lists and the flow errors when it cannot be named rather than falling back to the id it was handed. The two arguments a caller does supply beyond a message — a pasted sign-in code and a command line — are shape-checked (M.pastedCode, M.commandLine) and reach no dialog: the pasted code's flow draws none, and a command's dialog names the agent.
  • Read a plugin's refusal as an answer. The plugin host reports one by resolving the promise with __plugin_error rather than raising, so call checks for it; otherwise a job settles done on a sign-in that never happened.

Constants

NameMeaning
RING_CAPACITY512 — events held. A cursor older than the tail is answered dropped = true.
JOB_LIMIT8 — jobs live at once, across every caller. A ninth start is { ok = false, reason = "busy" }.
JOB_TTL_MS600000 — a job nobody polls is dropped after ten minutes.
STALE_MS2000 — how long a mirrored read stays current before the next read kicks a refresh.
PLUGIN_NAME"dotbot" — the name the consent gate resolves the dialog's display name from.
DEVICE_CODE / PASTE_CODE"deviceCode" / "pasteCode" — the two sign-in flows, as connect.start names them back.

Tested by src/lua/lib/tests/suites/dotbot_exports.testSuite.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

This asset doesn't declare a schema or structured payload.

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

0items
This part has no composite children. See the Files segment for its leaf payloads.
This part has no leaf payloads.

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.