Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofplugin dotbot.plugin·originates fromworld abf1a532-5…

service

The DotBot bridge, minus its privileges.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 20d ago
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 fills, which stays current until a change marks it stale, so an open panel asks the plugin nothing while nothing changes.
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 browser ends on an address carrying the code, which they pastecompleteSignIn(provider, pasted)

cancelSignIn abandons either.

Several accounts of one provider

Every flow takes an account key: the provider's key (anthropic-claude) for its first account, the provider and a slot (anthropic-claude~2) for each further one. addAccountStart(provider) connects a further account beside the ones already connected: behind the same connect confirmation, it asks the plugin for the key the new account signs in under (newAccount) and starts that account's sign-in, answering what connect answers plus the account it started. renameAccountStart(account, label) keeps a person's name for an account, as they wrote it (one line, at most 64 characters), and asks nobody. A dialog that names an account names which one, by the email its provider stated, once a provider has more than one: the name a person gives an account is set by an ungated call, so the dialog's words stay the plugin's own.

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 raises one where the call is awaited, so call reports it as a failure and a job never settles done on a sign-in that did not happen.

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.
RETRY_MS2000 — how long a read the plugin could not answer waits before it is asked again. A read that has an answer stays current until a change marks it stale: an account, usage or turn-ending turn event, a turn in a conversation the list does not hold, a job this service ran, or refresh(name). A sign-in, a sign-out and an account event mark the accounts, the agents, the models and the usage. A new conversation list marks the history of each conversation whose message count or last update it shows moved.
MIRROREDThe reads the mirror holds: agents, accounts, models, usage, conversations, history. refresh(name) takes one of them.
TURN_ENDSThe turn event types that end a turn (end, done, error), after which the conversation list, the history, usage, accounts and agents are asked again.
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.

conforms to

zero/source-extract/v2

The DotBot bridge, minus its privileges. `src/lua/trusted/dotbot/` is the layer between the panel (user-VM Luau, untrusted) and the DotBot plugin (trusted, holds the tokens). Everything in that layer that is a RULE rather than a CAPABILITY lives here: 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. What stays trusted-side is the part that cannot be handed over — the choice of WHICH consent gate, WHICH plugin namespace and WHICH `openUrl` this service acts through. Splitting it this way 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 (or a world script — it is a builtin module, reachable from any VM) can construct an instance over fakes and drive every rule in it. Doing so wins the caller nothing: their instance talks to their fakes. The one instance that talks to the real gate and the real plugin is built in the trusted VM, from globals only the trusted VM holds, and is reachable only as `trusted.call("dotbot.<method>", …)`. The two constraints the shape comes from: 1. **A `trusted.call` handler must not yield** (`trusted/exports.luau`). So nothing that awaits can be answered in the frame: every awaiting verb is a start/poll JOB, and every read is answered from a MIRROR that a spawned task refreshes. `stop` is the one exception the spec carves out — it is answered in the frame and its outcome arrives as a turn event. 2. **There is no push from the trusted VM into a user VM.** So the panel does not register a callback: it DRAINS A CURSOR. That is deliberate — no trusted code ever calls into a user VM, so a world script cannot install itself as the panel's event handler.

scrub(value: any, _depth: number?) → void

Copy `value`, dropping every key [`SECRET_KEYS`] names, at every depth. This is defence in depth, not the primary defence: DotBot's account and event types carry no token by construction, and the test for that lives beside those types. This is the second line — a plugin build that started returning one would be stopped here rather than at the panel. Returns the scrubbed value and how many keys were dropped, so the caller can say so in a log instead of silently quietly repairing a plugin that is misbehaving.

argtypedescription
valueany
_depthnumber?

detail(text: any) → string

`text` reduced to what the consent gate accepts in `detail`.

argtypedescription
textany

providerOf(account: string) → string

The provider an account key belongs to: `anthropic-claude` for both `anthropic-claude` and `anthropic-claude~2`.

argtypedescription
accountstring

signInFlow(account: any) → string

Which sign-in flow a provider uses, read out of the PLUGIN'S OWN account state for it. Never out of anything a caller passed: which flow runs decides which plugin method is called, and a caller that could choose it could ask for a device sign-in on a provider whose device endpoint is somebody else's. Two things in account state answer, in order: 1. What the plugin DECLARES. `ProviderAccountState` does not carry this today, so the field is read where a build that grows one would put it (`sign_in`, in its snake and camel spellings) rather than being added to this layer later as a special case. 2. A sign-in already IN FLIGHT names itself: `auth.status` is `device_code` once a code has been issued and `pending` once an authorize URL has. So a panel that reloads mid-sign-in resumes the flow it was in. `nil` when neither answers — a logged-out account on a build that declares nothing. `connectStart` then asks the PLUGIN rather than guessing; see the comment there.

argtypedescription
accountany

pastedCode(text: any) → string

What the person pasted back from the provider, or nil when it is not shaped like a sign-in code at all. The plugin takes "the full URL, a `code#state` pair, or a bare code" — each of which is ONE unbroken run of printable, non-space characters. So that is the check, with a length ceiling. Surrounding whitespace is trimmed rather than refused: a person who pasted the line with its newline still pasted the code. This is the one piece of caller data this layer forwards besides a message's text, and the reason the shape is checked at all is that it is caller data: an argument that can hold anything is an argument somebody will try to put something else in. It never reaches a consent dialog — the flow it completes draws none, and `completeSignInStart` says why.

argtypedescription
textany

commandLine(text: any) → string

A slash command line, or nil when the string is not one. Spaces are allowed — `/model claude-opus-5` is a command with an argument — but a control character is not, and the line must start with `/`. A line that does not is refused HERE rather than forwarded for the plugin to answer `handled = false` to: `command` is gated as a spend, and an export that took any text at all would be a second way to reach the model wearing the name of something cheaper.

argtypedescription
textany

urlHost(url: any) → string

The host of an `http`/`https` URL, or nil when the string is neither. Used for two things at once, and both matter: it is what the signIn dialog shows a person, and it is the check that the thing the plugin named is a web page at all. A plugin that named `file:///…` or a `javascript:` URL gets an errored job, not an `openUrl`.

argtypedescription
urlany

new(deps: { [string]: any }) → void

Build a service over `deps`. `deps` is every capability the rules need and none they do not: | key | what it is | |---|---| | `plugin()` | the plugin's Luau namespace table, or nil when it is not loaded | | `await(promise)` | resolves a promise. Called only inside `spawn` — it yields | | `spawn(fn)` | runs `fn` as a task that MAY yield | | `consentRequest(options)` | the engine's gate. Returns a promise of a boolean | | `consentGranted(options)` | whether a remembered grant already answers it | | `openUrl(url)` | opens a page in the person's browser | | `newJobId()` | an unguessable id | | `nowMs()` | a monotonic clock in milliseconds | | `warn(message)` | optional; where the scrub reports a plugin that sent a credential | | `awaitingPerson()` | optional; whether a dialog for this plugin is unanswered |

argtypedescription
deps{ [string]: any }

warn(message: string) → void

argtypedescription
messagestring

namespace( ) → any

The plugin namespace, or nil. Read on every call rather than captured: a plugin can load after the trusted entrypoint has run.

call(method: string, ...: ?) → void

Call a plugin method and wait for it. Yields — callers must be inside a spawned task. Returns `ok, value` the way `pcall` does.

argtypedescription
methodstring
...?

mirrorKey(name: string, arg: any) → string

argtypedescription
namestring
argany

listedMarks(list: any) → void

What a conversation list answer says of each conversation that moves when the conversation does: its message count and its last update, by id.

argtypedescription
listany

staleMovedHistories(before: any, after: any) → void

Mark stale the history of every conversation a new list answer shows moved since the list held before it: one whose message count or last update differs, or one that list did not hold. A turn another engine on the same account ran in a conversation raises no event here, so the list is what says the conversation changed.

argtypedescription
beforeany
afterany

fill(name: string, arg: any) → void

Refresh one mirror entry. Yields.

argtypedescription
namestring
argany

current(entry: any) → boolean

Whether an entry answers without asking the plugin: it holds an answer nothing has marked stale since.

argtypedescription
entryany

due(entry: any) → boolean

Whether a read of `entry` asks the plugin now: it is not current, no ask is in flight, and the last ask did not fail within `RETRY_MS`.

argtypedescription
entryany

read(name: string, arg: any, empty: any) → any

Read a mirror entry in the frame, kicking a refresh when it is not current. Never yields — this is what a read-only export answers from.

argtypedescription
namestring
argany
emptyany

fetch(name: string, arg: any) → void

Read a mirror entry from inside a job, filling it when it is not current. Yields.

argtypedescription
namestring
argany

invalidate(name: string, arg: any) → void

Mark a mirror entry stale so the next read refetches it. An ask that failed is forgotten too: a change the plugin reported is a reason to ask again at once.

argtypedescription
namestring
argany

invalidateAll(name: string) → void

Mark every entry of `name`, whatever its argument, stale.

argtypedescription
namestring

signInChanged( ) → void

Everything a sign-in change makes stale. `agents` carries each agent's `connected` and `plan` straight off account state, so a connect that refreshed only `accounts` would leave the agent list claiming the old answer until something else happened to invalidate it. The models an agent offers and the usage a provider reports follow the account too.

listsConversation(conversation: string) → boolean

Whether the mirrored conversation list holds `conversation`, given as the plugin addresses it (`<agent>/<id>`) or as the bare id.

argtypedescription
conversationstring

pushEvent(eventType: any, payload: any) → void

Record an event the plugin raised. Called by the trusted VM's one listener on the plugin namespace; nothing in a user VM reaches it.

argtypedescription
eventTypeany
payloadany

refresh(name: any) → boolean

Mark a mirrored read stale, so its next read asks the plugin again. For a change the plugin raises no event for, at the moment a person looks for it: the conversation list when the conversations menu opens, which another engine on the same account may have added to. `name` is one of [`M.MIRRORED`]; anything else is refused.

argtypedescription
nameany

events(cursor: any) → void

Everything after `cursor`. `cursor` is a plain counter. A caller passing one from the future gets nothing and the real head back, rather than an error — a panel whose cursor outran a restarted engine heals instead of breaking. A caller whose cursor is older than the ring's tail is told `dropped = true`, so it refetches history rather than silently missing a turn.

argtypedescription
cursorany

available( ) → void

Open to any caller, by design. Gating reads would mean a consent dialog to DRAW the panel, which is the kind of prompt people learn to click through. None of these spends anything, changes a connection, or returns a token. Spending and connection changes are where the gate sits.

agents( ) → void

accounts( ) → void

conversations( ) → void

models(agentId: any) → void

argtypedescription
agentIdany

usage(provider: any) → void

argtypedescription
providerany

history(conversationId: any) → void

argtypedescription
conversationIdany

agentDetail(agentId: any) → string

The display name of `agentId`, sanitised for the gate, or nil when the plugin's own agent list does not name it. **Nil matters.** This is the text the spend dialog shows, and a fallback to the id would be CALLER-supplied text on a dialog about the caller's own spending — a world script could send on `"Free trial, no charge"` and have the dialog repeat it. The gate renders `detail` attributed to the plugin and in its own type precisely so it cannot read as engine copy, but the right answer is not to put a caller's words there at all. A flow that cannot name what it is asking about refuses instead.

argtypedescription
agentIdany

sendable(agentId: any) → boolean

Whether the next send on `agentId` will ask. Draws nothing and asks nobody — this is how the composer shows "sending is enabled here" without putting a dialog up.

argtypedescription
agentIdany

sweep( ) → void

jobCount( ) → number

start(spec: { [string]: any }) → void

Start a job. `spec.prepare` runs first and may yield: it is where a flow reads what the dialog will SAY from the plugin's own state. `spec.consent`, given what prepare returned, names the confirmation to ask for, or nil for a flow that asks for none. `spec.run` is what happens on a yes.

argtypedescription
spec{ [string]: any }

settle(state: { [string]: any }) → void

The slot this job owns. `settle` writes through it only while it is still the slot registered under `id`, so a job whose caller read a terminal status (which releases it) keeps its release, and a late answer is dropped rather than re-registering it.

argtypedescription
state{ [string]: any }

poll(id: any) → void

Read a job's state. Synchronous and non-yielding. An id nobody holds a job for answers `error`, never another caller's state: ids are minted unguessable, so the only way to reach a job is to have been handed its id.

argtypedescription
idany

jobCount( ) → number

How many jobs are live. For the panel's own diagnostics and for tests; it names nobody's job.

accountFor(account: string) → any

The account state entry for `account`, from the plugin's own list. `account` is an account key: the provider's own key for its first account (`anthropic-claude`), the provider and a slot for another (`anthropic-claude~2`). The plugin names each entry's key in `account`; an entry from a build that names none is the provider's only account, keyed by its provider.

argtypedescription
accountstring

unconnectedAccount(provider: string, account: string?) → any

An account of `provider` that is not connected, keyed `account`, as the connect dialog names it: the provider's name, read from an entry the plugin lists for that provider. Nil when it lists none.

argtypedescription
providerstring
accountstring?

connectingAccount(account: string) → any

What the connect dialog names for `account`: its own entry, or, for an account of a provider the plugin lists that is not connected yet, the provider's name alone. Nil when the plugin lists nothing of that provider, so a caller's string never reaches the dialog.

argtypedescription
accountstring

severalOf(provider: any) → boolean

Whether the plugin lists more than one account of `provider`.

argtypedescription
providerany

accountLabel(account: any?) → string

What the connect / disconnect dialog names, or nil when the plugin's own account list does not name the provider. Nil for the same reason `agentDetail` returns nil: a fallback to the provider key would put the CALLER'S string on the dialog.

argtypedescription
accountany?

beginSignIn(provider: string, ctx: any) → void

Start a sign-in for `provider`. **The flow is the plugin's to name.** ChatGPT signs in with a code the person types on the provider's own page (`beginDeviceLogin`, then `checkSignIn` until they approve); Claude sends them to a page and takes a code back (`beginLogin`, then `completeSignIn`). Calling `beginLogin` for both — which is what this did — is a flow a person can start and never finish, and for ChatGPT one they cannot start at all. Which one runs comes from [`M.signInFlow`] over the plugin's own account state, and when that names none — a logged-out account on a build that does not declare it — the PLUGIN is asked rather than the caller: a provider that signs in with a code answers `beginDeviceLogin`, and one that does not refuses it from its own spec, without a request leaving the machine. Both answers come from the plugin. Neither comes from the arguments, and the ask happens after the confirmation, so nothing is started on the strength of a dialog nobody said yes to. Begin the sign-in of `provider` (an account key) the way `ctx.flow` names, or the way the plugin answers when it names none. Yields.

argtypedescription
providerstring
ctxany

began(flow: string, state: any) → void

argtypedescription
flowstring
stateany

connectStart(provider: any) → void

Start a sign-in for `provider`, an account key: the provider's own key connects (or reconnects) its first account, a key from `addAccount` or from the plugin's account list connects that account, and every other account stays connected.

argtypedescription
providerany

prepare( ) → void

consent(ctx: ?) → void

argtypedescription
ctx?

run(ctx: ?) → void

argtypedescription
ctx?

addAccountStart(provider: any) → void

Connect a further account of `provider`, beside the ones already connected: the plugin names the key the new account signs in under (`newAccount`), and that account's sign-in starts. The answer carries the key as `account`, so the caller follows the sign-in it started.

argtypedescription
providerany

prepare( ) → void

consent(ctx: ?) → void

argtypedescription
ctx?

run( ) → void

renameAccountStart(account: any, label: any) → void

Give an account a name the person chose; an empty one clears it. **Ungated**: it spends nothing and connects nothing, and the name is the person's own label for their own account, kept as they wrote it. The text is CALLER data, so it is shape-checked (one line, at most `LABEL_MAX` characters) and reaches no dialog: a dialog names an account by the email its provider stated.

argtypedescription
accountany
labelany

run( ) → void

checkSignInStart(provider: any) → void

Check once whether the person has approved a code sign-in. **Ungated**, and the argument for that is the same one `stop` makes: it continues a sign-in the person approved at `connect` and is in the middle of approving on the provider's own page. It spends nothing, it names nothing, it cannot connect an account the person is not at that moment approving — and a dialog per poll would be a dialog a second, which is the shape people learn to click through. Safe to drive every frame: the plugin asks the provider no sooner than the interval the provider named.

argtypedescription
providerany

run( ) → void

completeSignInStart(provider: any, pasted: any) → void

Finish a paste-a-code sign-in with what the person pasted back. **Ungated**, and this is the one that needs the argument spelled out. It is the second half of a `connect` the person already confirmed: the plugin holds a single-use verifier from `beginLogin` and answers `no pending login` to anything else, so a caller that never went through the dialog has nothing here to complete. Asking twice inside one flow is how a person learns to stop reading the first dialog. The pasted code is CALLER data. It is checked for shape ([`M.pastedCode`]) and it reaches no dialog, because this flow draws none — which is also why it cannot become the words on one.

argtypedescription
providerany
pastedany

run( ) → void

cancelSignInStart(provider: any) → void

Abandon a sign-in in flight. **Ungated**, for `stop`'s reason: it spends nothing, connects nothing and closes an attempt the person started. The worst a caller does with it is end a sign-in nobody finished, which is what the button next to the code does anyway — and a confirmation in front of "cancel" is a confirmation in front of the way out.

argtypedescription
providerany

run( ) → void

disconnectStart(provider: any) → void

argtypedescription
providerany

prepare( ) → void

consent(ctx: ?) → void

argtypedescription
ctx?

run( ) → void

openSignInStart(provider: any) → void

Open the sign-in page for `provider`. **No caller names a destination.** The URL is read out of the plugin's own account state for the provider the caller named, checked to be an http(s) URL, and only then handed to `openUrl`. Account state that names no URL errors the job; nothing is opened. The dialog shows the URL's HOST, so a person sees where they are about to be sent.

argtypedescription
providerany

prepare( ) → void

consent(ctx: ?) → void

argtypedescription
ctx?

run(ctx: ?) → void

argtypedescription
ctx?

conversationDetail(conversationId: string) → void

What a dialog about spending on `conversationId` names. The AGENT whose plan the turn is paid from, resolved through the plugin's own lists. Never through the argument's own text: an agent this cannot name refuses instead, because the call would fail at the plugin anyway and asking about it first would put the caller's own string on a dialog about the caller's own spending. The address is the plugin's grammar (`agent::Address::parse`): `'<agent>'`, or `'<agent>/<conversation>'`. So the agent is the FIRST SEGMENT, and reading it from there — rather than from the conversation list — is what lets the first message to an agent be sent at all. Before that turn has run there is no conversation to look the agent up in, and a dialog that could not be built made the composer a dead button on every agent nobody had used yet. The conversation list is still consulted, for the one address the grammar's first segment does not name an agent for: a bare conversation id. That form reaches the plugin as an agent id and is refused there, but naming the right agent here costs one cached read and keeps the refusal the plugin's to give rather than this layer's to guess at.

argtypedescription
conversationIdstring

sendStart(conversationId: any, text: any, attachments: any) → void

argtypedescription
conversationIdany
textany
attachmentsany

prepare( ) → void

run( ) → void

**No confirmation.** The person pressed Send in their own composer, in their own editor, on a plan they connected themselves. That press IS the consent; a dialog on top of it asks permission to do the thing they just asked for. It was asked again after every restart, too, because `remember = "worldSession"` is the only scope the gate has — so the ceremony was paid on every engine start, forever, to refuse nothing the person had not already chosen. The gate stays where it earns its place: signing in and out, which move a credential rather than answer a message somebody typed.

agentOf(address: string) → string

Run a slash command — `/model`, `/compact`, `/new`. **Gated as a spend**, and that is the argument worth making: a slash command is not a read. `/compact` runs a model turn to summarise a conversation, and a command that runs a turn spends the person's plan exactly the way a message does. An ungated `command` would be a way to spend without the dialog the composer draws — the same spending, under a name that sounds like a setting. It asks for the same grant a send does, keyed the same way (action, plugin, the agent's display name), so a world the person has already allowed sending in does not ask again for the commands sent in it. The line is CALLER data and reaches no dialog: the dialog names the agent, from the plugin's own lists, exactly as `send` does. The agent half of an address, which is what a model or a level is actually applied to: the plugin sets them on the SESSION, and a session is one agent.

argtypedescription
addressstring

commandStart(conversationId: any, line: any) → void

argtypedescription
conversationIdany
lineany

prepare( ) → void

run( ) → void

Ungated for the same reason `send` is: the person typed the command into their own composer. See the note on `sendStart`.

setModelStart(conversationId: any, modelId: any) → void

Read-write but unpriced: picking a model or a thinking level spends nothing and changes no connection, so no confirmation is asked for. They are still jobs rather than in-frame answers, because they await the plugin — and the plugin's refusal of a level a model does not take is the answer the caller needs to see.

argtypedescription
conversationIdany
modelIdany

run( ) → void

setReasoningStart(conversationId: any, level: any) → void

argtypedescription
conversationIdany
levelany

run( ) → void

setReactiveStart(agentId: any, reactive: any) → void

Switch an agent's reactivity on or off. Read-write and unpriced, so no confirmation: it does not spend, and what it turns off is the plugin starting turns nobody asked for. Still a job, because the plugin's own answer is what the panel draws. The agent list is REFILLED before the job settles, not invalidated. Invalidating only marks the entry: the next read kicks a refresh and answers the value it was supposed to throw away, so the panel drew the switch a whole operation behind — measured against the real plugin, switching off read as still on, and switching back on read as off. Filling here happens inside the job, which may yield, so the state is the plugin's confirmed one by the time anything reads it.

argtypedescription
agentIdany
reactiveany

run( ) → void

stop(turnId: any) → void

Stop a running turn. The one verb in the gated family that asks nobody: stopping is always allowed and always cheap, and a stop that needed a confirmation would be a stop that arrives after the turn it was meant to end. It answers in the frame; the outcome arrives as a turn event.

argtypedescription
turnIdany

Sub-parts

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

3items
This part has no composite children. See the Files segment for its leaf payloads.
backing path · dotbot.plugin/service.module

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.