Bots and their connectors
A bot is an account of its own, added to a space like a member. A host can
run it, or a member can, with minca agent --bot. What a bot does is decided
by the space's automations that name it in by.
This page covers who may configure a bot, the instructions it follows, and how to connect it to a service outside the space, like GitHub.
Spec 08 §3 covers bots on a host.
Who may configure a bot
Someone who holds /std.automation/instruct in the space, and every role the
bot holds there. The second half means nobody reaches past their own grants
through a bot. In the community preset, founders, admins and moderators
hold the grant.
Ask with mayConfigure:
import { mayConfigure } from '@minca/core/schemas';
async function openSettings(bot: string, person: string) {
if (await mayConfigure(node, space.id, bot, person)) {
// show the bot's settings
}
}The bot, its host and the app all ask this one function, about whoever wrote
the version in question (its root):
- A bot follows what a record asks only when its writer passes.
- A bot follows instructions (
std.bot) only from someone who passes. - A bot acts on GitHub only for an automation whose current words were written by someone who passes, whoever sets it off.
- A host connects or disconnects a bot only for someone who passes.
Bots don't configure bots
A bot never passes for itself. Steered once, it could leave itself
instructions that count. Nor does another bot, just as no chat app lets one
integration reprogram the next. A host never counts a bot it runs (isBot in
runAutomations), and the add-bot dialog offers a bot no role that grants
/std.automation/instruct.
Instructions
A std.bot record says how a community wants its bot to be: a name to go by,
a tone, what to know.
{
"bot": "did:key:zDnaeYffVz7N…",
"instructions": "You are Ferris, the team's helper. Answer briefly, and link issues as #N."
}| Collection | Body | Rules |
|---|---|---|
std.bot | bot DID; instructions string ≤ 2000 | onePer: [@author, bot] |
Each person keeps their own, so nobody's words overwrite another's. The bot reads the newest one whose current version was written by someone who may configure it, as a person and not through an agent. It adds them to its system prompt after its own rules, saying whose words they are.
Instructions can't undo the bot's rules. What set an automation off is still data, and deleting is still refused. Everyone in the space can read them.
An app that shows a bot's instructions must pick them the same way, as the
CLI's personaIn does.
Connecting a bot
A host can connect its bots to outside services, like GitHub, a calendar or a drive. The person never handles a key. They go through the service's own consent screen, and the host keeps what reaches the service. Your app only needs each service's id and name.
- Read what the host offers from
node.hosting.space(space.id). Each view hasconnectors, a list of{ id, name }, and the host'surl. - Call
askToConnect(node, space, bot, id). It makes a random 32-byte secret, keeps it, and writes astd.connectnaming the bot, the service and the secret's hash. The request is valid for nine minutes. - Open
connectPage(hostUrl, ask)in a new window. Open it in the click handler, so the browser doesn't block it. The host checks the request through the bot and sends the window on to the service. - When the service sends it back, the bot writes its own
std.connectorsaying what it is connected to. The window posts{ minca: 'connected' }to your app and closes.
import type { SpaceHostingView } from '@minca/core';
import { askToConnect, connectPage } from '@minca/core/schemas';
async function connect(host: SpaceHostingView, bot: string) {
// Opened in the click, before any await, or the browser blocks it.
const page = window.open('about:blank', 'minca-connect');
const ask = await askToConnect(node, space.id, bot, 'github');
if (page) page.location.href = connectPage(host.url, ask);
}Define std.connect and std.connector in the space before you write a
request. Show a std.connector only when its writer is the bot it names.
Disconnecting
Write the request with disconnect: true, then send it with
askToDisconnect. The host checks it the same way, forgets the connection,
and the bot deletes its std.connector.
import type { SpaceHostingView } from '@minca/core';
import { askToConnect, askToDisconnect } from '@minca/core/schemas';
async function disconnect(host: SpaceHostingView, bot: string) {
const ask = await askToConnect(node, space.id, bot, 'github', { disconnect: true });
await askToDisconnect(host.url, ask);
}The records
| Collection | Body | Rules |
|---|---|---|
std.connect | bot DID; kind service id, 1–40 chars; verifier; expires date-time; disconnect bool | |
std.connector | bot DID; kind 1–40 chars; account ≤ 100; reaches; by DID; since date-time | onePer: [@author, kind] |
verifieris base64url(SHA-256(secret)), 43–64 characters.accountis whose account at the service it is.reachesis what it reaches: up to 100 strings of up to 200 characters.
A request, for the secret AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (43 As):
{
"bot": "did:key:zDnaeYffVz7N…",
"kind": "github",
"verifier": "DwBzhbb51LfusnSGBa_hqYSgo7-j8BTQnip4TOnlzRo",
"expires": "2026-10-15T12:09:00Z"
}Spec 08 §3.1 covers how the host checks it.
Writing a connector
Each host decides which services it connects its bots to. In the reference
host, a service is one module implementing HostConnector, passed to
startHost as connectors. It lives in the CLI, which the API reference
doesn't cover, so here are its members:
| Member | Does |
|---|---|
id, name | What the host's description names |
consentUrl(state, callback) | Where the browser goes to consent |
complete(query, callback) | Handles the browser coming back, and says what was connected |
tools(connection, { mayWrite, budget }) | What the bot may do on a connection |
hook(body, headers) | Optional. Handles calls from the service itself |
consentUrl must send the browser back to callback,
<host>/host/connect/<id>/callback, with state. Set the host's public
address (MINCA_HOST_URL) when a connector sends a redirect_uri. Otherwise
callback is worked out from each request.
complete gets the callback's query. It answers what was connected
(account, reaches, and data the host keeps and never shows, like a
refresh token), or why not. The host has already checked the state, the
browser and the request. The connector checks what only the service can say,
like whether this person may give that account to a bot.
tools that write must refuse without mayWrite, and each call to the
service takes one from budget. What the service returns is someone else's
words: mark it as data, as the GitHub tools do.
hook gets calls at <host>/host/connect/<id>/hook. It answers unsigned
for a call that isn't the service's, forget with the connections that are
gone, or reaches with what they now reach at most. The host only ever
narrows what a connection reaches, since one account at a service may serve
several communities.
Whatever the connector, the reference host:
- stops acting through a connection, and forgets it, once the bot holds no role in the space or its maker may no longer configure the bot;
- only ever narrows what a connection reaches;
- refuses two connectors with one id.
A Google Calendar connector, say, would send the browser to Google's consent
screen, exchange the code for tokens in complete, and keep the refresh token
in data. It would offer calendar_events, and with mayWrite,
calendar_add.
GitHub, in the reference host
The reference host's GitHub connector is a GitHub App. Whoever runs the host registers it once, with:
- Callback URL
<host>/host/connect/github/callback, with "Request user authorization (OAuth) during installation" ticked. - Webhook URL
<host>/host/connect/github/hook, with a secret, subscribed to installation events. - Permissions: Issues and Pull requests read and write. Contents and Metadata read.
MINCA_GITHUB_APP_ID,MINCA_GITHUB_APP_SLUG,MINCA_GITHUB_CLIENT_ID,MINCA_GITHUB_CLIENT_SECRET,MINCA_GITHUB_PRIVATE_KEY(the PEM, or a path to it) andMINCA_GITHUB_WEBHOOK_SECRETin the host's environment, withMINCA_HOST_BOTS=1.
Without the webhook secret, the host takes no webhook calls, and learns of an uninstall only when a call fails.
Installing
The person picks the organisation and its repositories on GitHub's install
screen. The bot then acts on GitHub as the App, <slug>[bot], on those
repositories only, with tokens that last an hour.
GitHub doesn't vouch for the installation id it sends back, and lists an installation to anyone with access to one of its repositories. So the connector exchanges GitHub's code for the user's own token, and keeps the installation only if it is on that user's account, or on an organisation they are an active admin of.
When the App is uninstalled or suspended, the host forgets every connection through it. After a disconnect the App stays installed on GitHub, where whoever installed it can remove it.
Which repositories
One installation may serve several communities of the same account. So a
connection keeps the repositories it was made with. When GitHub says they
changed, the host only narrows them, and each bot says so in its
std.connector. To reach a repository added later, connect again.
Repositories are kept by name. If one is renamed and another is made under its old name, in an installation for all repositories, the new one is reached in its place.
The bot's tools
A bot connected in a space has these there, beside its node actions:
| Tool | Does |
|---|---|
github_search | Issues and pull requests in its repositories, in GitHub's search syntax |
github_get | One issue or pull request: text, labels, latest comments, a pull request's files |
github_file | A file at a branch, tag or commit |
github_comment | Comments on an issue or pull request |
github_issue | Opens an issue |
It works like a chat app a workspace installs:
- The two that write run for an automation made by someone who may configure the bot, whoever sets it off. What sets it off reaches the model as data, and can still steer it. An automation whose current words are by someone who may not instruct the bot doesn't run at all.
- The three that read run for anyone whose record sets an automation off. A member can ask the bot what is in the connected repositories, private ones included. So connect only what everyone in the space may read.
- Every call names a repository the installation reaches, checked in code.
- A search is scoped to those repositories, and shows only results whose own repository is one of them, whatever the query says. An issue moved out of them isn't read at its old address.
- A run keeps the tools it started with until it ends, even through a disconnect or uninstall.
- Of several people's instructions, the bot takes the one written last by its writer's clock. Whoever may configure the bot is trusted with that, as with a bot's settings in any chat app.
- The model never sees a token. Each call adds one.
- What GitHub returns was written by people, possibly outside the space, and reaches the model marked as data.
- A bot makes at most 200 calls a day across its connectors, so a loop can't use up a service's limits.
Planned
- GitHub events as records ("PR #12 opened"), so automations can answer them.
- Connectors for a bot a member runs, which has no public address for a service to send people back to.
Automations
An automation does something when records in a space look a certain way. "When a poll has 10 votes, close it." "When someone mentions the bot, answer them." "When a channel passes 100 messages,…
Node actions
Actions are the node's operations written down as data: a name, a description and an input schema for each. The CLI, MCP and WebMCP all build their tools from this one list, so an agent sees the…