Configuration
Every field the daemon accepts, projected from src/config/schema.rs. A key that is not one of these is a refusal to start rather than a setting quietly ignored, so a misspelling says so. Inside a provider definition, which may carry keys of the operator's own, only a misspelling of a route key is refused, and anything else is ignored.
The file is read from ~/.config/errand/config.json, then /etc/errand/config.json, then config.json in the working directory. ERRAND_CONFIG names one outright.
There is a file to copy in the repository, config.example.json, and a schema beside it. Naming the schema in your own file gives an editor completion and checking as you type:
{ "$schema": "https://raw.githubusercontent.com/QaidVoid/errand/main/config.schema.json" }top level
The fully resolved configuration the daemon runs on.
| field | type | default | what it does |
|---|---|---|---|
projectRoot | string | required | Absolute path under which every session's project directory lives. |
stateDir | string | required | Where per-session state directories are created on the host. Neither this nor projectRoot may sit inside the other. A session may write both, and the transcript is kept beside this directory precisely so that it cannot. |
chat
Chat connection and who may drive the bot.
| field | type | default | what it does |
|---|---|---|---|
token | string | required | Bot token. Secret. Never enters a sandbox. At least 8 bytes, so it can be scrubbed from what is posted. |
channelId | string | required | The single channel the daemon serves. Everything else is ignored. |
allowedUserIds | string[] | required | Accounts permitted to drive sessions. Must not be empty. |
blockedUserIds | string[] | none | Accounts refused before anything else is considered. |
operatorUserIds | string[] | none | Accounts that may control any session, not only their own. |
startOnMention | boolean | false | Require a message to mention the bot before it starts a session. |
agent
Which model the agent talks to, and the credential it reaches it with.
| field | type | default | what it does |
|---|---|---|---|
provider | string | required | Provider id a session starts on, such as anthropic or zai-coding-cn. Only where model does not name one of its own, which is the more specific thing to have written and settles it instead. |
model | string or undefined | none | Model pattern or id. Omitted to use the provider's default. May name its provider as provider/id, may carry a thinking level as id:max, and may be a short name from aliases. A leading segment is only read as a provider when it is one this configuration defines, so a model id holding a slash of its own stays whole. |
visionModel | string or undefined | none | Model an image is shown to when the working model cannot see one. |
imageGen | string or undefined | none | Model !gen-image draws with, or none. Named as !model names them: an alias, provider/id, or a bare model id, with the effort to use as a level on the end, as image:max. The level stays on the model, because it is part of the name the images endpoint is asked for. It is the standing effort: one asked for on the command, as --effort <level> or a level on a model named there, replaces it for that picture. A name nothing lists is refused when the command is used, not here. The command calls the provider's own POST /images/generations with this model and the prompt, and posts what comes back as an upload. The provider is the one that lists the model, so its baseUrl is where the request goes, and a name two providers list is refused rather than guessed at. The provider a session runs on is preferred when it lists the name too. A provider with no baseUrl cannot draw. |
speechGen | string or undefined | none | Model !gen-speech reads aloud with, or none. Named as !model names them: an alias, provider/id, or a bare model id. It takes no effort level, because the speech tiers are model names on the server. A name nothing lists is refused when the command is used, not here. |
rulesPath | string or undefined | none | File of standing instructions given to every session, or none. |
providers | Map<String, serde_json::Value> | none | Every provider, the one a session starts on included. A provider is named with lowercase letters, digits and dashes, which is all kage accepts in a provider id. One marked extension: true may be named anything, since errand never writes it into kage's configuration. Each entry carries the credential errand reaches that provider with, optionally the credentialName the agent reads it from, and whatever else locates it: baseUrl, kind, models. A kind of openai or anthropic says which wire protocol a provider kage does not already know speaks, in kage's own words; omitted means openai. A provider kage knows needs no kind, since its name already says it. Endpoints reach the agent's own configuration translated, because that schema belongs to the agent; the credential never enters it, traveling in the environment instead. Under egress.mode = proxy, a provider the broker routes, unless marked broker: false, is given to the session as a stand-in, errand-session- and 64 hex digits, minted for that session alone and honoured only by its broker until the session stops. It authenticates the session to its broker, which puts the real credential on; it is worth nothing to the provider, and the session's reported text is scrubbed of it. An entry of models naming a model the host's store already defines for that provider lends its name and context to the declaration, so the agent can address it. discover asks the provider which models it serves, at startup, after a change to this file, and on !models refresh, under its baseUrl. true reads the usual /models listing. An object moves it: path, list for where the array sits, fields naming where id, name, contextWindow, and maxTokens sit in an entry, and defaults for fields every model found carries. An entry of models naming a model found overrides the fields it names and keeps the rest. usage says how to read what is left of the provider's usage window, for !usage, the bot's status, and refusing a session the window cannot serve: "zai" for z.ai's quota endpoint, "gateway" for /usage under the baseUrl, or an object with path, auth (bearer or raw), percent and percentIs (used or left), and resets and resetsAs (iso, ms, or s). Several windows go under windows, the one to show first: a missing or stale one gives way to the next, and a spent one outranks them all. A path may pick a list entry by a field, as windows[id=5h].usedPercent. spentRefuses: false is for a plan that bills past a spent window to a credit balance: the window still shows, but no longer refuses a session. A provider without usage is not metered, whatever it is called. A defaultThinkingLevel on a provider says how hard its models think when nobody says, and one on an entry of models says it for that model alone. Naming a model the host's store already lists says it for that model without listing it twice. An images object on one provider, naming a model, gives sessions kage's generate_image tool through it, for an endpoint that answers POST /images/generations. A provider the agent already reaches for chat lends the tool its endpoint and key. One kage does not know that lists no models is an images endpoint of its own, written for the tool alone so its image models are never offered as chat models, and it needs a baseUrl. An editModel beside the model also gives sessions kage's edit_image tool on the same endpoint, which answers POST /images/edits with that model. A speech object on one provider, naming a model and an optional voice, gives sessions kage's generate_speech tool, for an endpoint that answers POST /audio/speech. The three shapes are the same as for images: a provider the agent already reaches for chat lends the tool its endpoint and key, and one kage does not know that lists no models is a speech endpoint of its own and needs a baseUrl. The voice is the one asked for when the agent names none. It needs a kage newer than 0.5.1. A music object on one provider, naming a model, gives sessions kage's generate_music tool, for an endpoint that answers POST /music/generations, in the same three shapes. It needs a kage newer than 0.5.1. On a brokered provider, placement (bearer or x-api-key) says which header its key travels in, following an explicit kind when left out, and capabilities, a list of method and path or "*", says which calls the key may make. A list only warns until strictCapabilities is true; the sandboxing page describes both. A key that reads as a misspelling of one of these three is refused. Any other key errand does not read is ignored. A provider entry may instead be marked extension: true, which means a kage plugin registers it. Errand writes nothing for it into the agent's configuration and does not broker it: the plugin holds the endpoint, and a signing plugin needs the real credential, so that travels whole in the environment under credentialName. Its models are read only so a session can list and switch to them. |
plugins | Plugin[] | [] | Kage plugins handed to every session. |
aliases | Record<string, string> | none | Short names for models, so a session is started without spelling one. |
fallback | string[] | none | Models to start a session on instead, in order, when the provider it would start on has spent its usage window. Named as !model names them, aliases included. The first whose provider has room is used; a session is turned away only when every one is spent too. |
deny | string[] | none | Commands the agent may not run, whatever else is permitted. Sessions run with every tool allowed, since the chat cannot serve a prompt the agent waits on and a permission ask nobody sees is a session that stalls. This is what stands in for that prompt: glob patterns matched against the command line, the same patterns kage's own rules use. A match is refused by the agent itself and the reason reaches the thread, so a refusal is visible rather than silent. Patterns are checked against the whole command line, so rm -rf * covers the form that is actually typed. To refuse one tool outright whatever it is asked to do, use deniedTools. |
deniedTools | string[] | none | Tools the agent may not call at all. Named by the tool kage calls it, such as shell or write. Refused whatever their input carries, which is the blunt end: deny refuses particular commands, this refuses a whole capability. |
maxProviderRetries | number | 10 | How many times a transient provider failure is retried before the agent gives up on it. Written into the agent's [loop] table as max_provider_retries. |
agent.delegate
A cheaper model the session's model may ask about one artefact. Absent means no delegation at all: the session's own model does everything.
| field | type | default | what it does |
|---|---|---|---|
model | string | required | The model asked. Must be one the provider serves under the same key. |
perTurn | number | none | How many delegations one turn may make before the rest stay at home. |
deadlineMs | number | none | How long one delegation may take before it is abandoned. |
baseUrl | string or undefined | none | Where the provider is reached, when the model store does not say. |
agent.plugins[]
One kage plugin handed to every session. The file is copied into the agent's own plugin directory at launch, where the sandbox mounts it, and its capabilities are granted in the configuration written for the agent. Capabilities are the operator's grant and are never guessed: a plugin left without the ones it needs disables itself at load.
| field | type | default | what it does |
|---|---|---|---|
path | string | required | Host path of the plugin file. Must be an absolute path to a .lua file. |
capabilities | string[] | [] | Capabilities granted to the plugin, by kage's capability names. |
forge
The forge identity a session works with, when one is configured. A session with no forge configuration still runs, and simply has no credential to reach a repository with.
For Forgejo, set type to forgejo and url to the instance address. Use HTTPS: HTTP sends the forge token and repository traffic in cleartext, exposing them to anyone who can observe or alter the connection.
Forgejo assignment triggers only cover direct assignments to the bot, not team assignments. Its notification floor is kept in memory only. On restart, unread activity from before the restart is skipped, including activity from failed reads.
{
"forge": {
"type": "forgejo",
"url": "https://forge.example.com",
"token": "PUT A FORGE TOKEN HERE",
"userName": "Commit Author",
"userEmail": "author@example.com"
}
}| field | type | default | what it does |
|---|---|---|---|
type | ForgeKind | "github" | Which forge this is. GitHub unless type says otherwise, so the section reads the same as the github spelling it replaces. |
url | string or undefined | none | The instance's address, such as https://forge.example.com. Forgejo is the only forge configured by one: GitHub is always github.com, and naming a url there is refused. |
token | string | required | The daemon's token, which it reads notifications, composes pull requests and pushes with. A session holds it too unless app is set, which only GitHub has. Secret, and at least 8 bytes, so it can be scrubbed from what is posted. |
userName | string | required | Name commits are authored with. |
userEmail | string | required | Email commits are authored with. |
forge.trigger
Who may ask for work from the forge.
| field | type | default | what it does |
|---|---|---|---|
allowedUsers | string[] | required | The forge logins whose mentions and assignments start a session, and whose comments continue one. Nobody else is heard: on a public repository anyone can comment, and every session runs code. Required and non-empty. A wildcard * is refused. |
repositories | string[] | [] | The repositories listened to, as owner/repo, or owner/* for every one an account or organisation holds. Absent means wherever the bot is mentioned or assigned. |
on | string[] or undefined | ["mention","assign"] | Events that start a session: mention, assign, or both. Must not be empty. Omitted means both events. |
forge.app
The GitHub App a session's credential is minted from.
| field | type | default | what it does |
|---|---|---|---|
appId | number | required | The App's id. |
installationId | number | required | The installation tokens are minted for. |
privateKeyPath | string | required | The absolute path of the App's private key, as GitHub gave it. |
permissions | Record<string, string> | {} | What each token may do, by permission, as read or write. Empty asks for everything the installation was granted. |
repositories | string[] | [] | The repositories each token covers, by name without their owner. Empty covers every repository of the installation. |
github
Deprecated but still supported: the legacy GitHub identity. Rename github to forge; forge.type defaults to github. Do not set both sections. Only token, userName, userEmail, trigger, and app are accepted here, not type or url. The optional trigger and app objects use the fields documented under forge.trigger and forge.app.
| field | type | default | what it does |
|---|---|---|---|
token | string | required | The daemon's token, which it reads notifications, composes pull requests and pushes with. A session holds it too unless app is set, which only GitHub has. Secret, and at least 8 bytes, so it can be scrubbed from what is posted. |
userName | string | required | Name commits are authored with. |
userEmail | string | required | Email commits are authored with. |
sandbox
What a session may consume, and what the backend enforces.
| field | type | default | what it does |
|---|---|---|---|
backend | SandboxBackend | none | Which backend confines sessions. |
requireFullEnforcement | boolean | true | Refuse to start when the backend cannot enforce every configured guarantee on this host, rather than reporting the gap and continuing. |
network | NetworkMode | "restricted" | Network exposure granted to a session. |
egressPorts | number[] | [443] | Ports a session may open outbound, when the network is not none. |
image | string | "localhost/errand-agent:latest" | Container image the podman backend runs. Inert under bailey and incus. |
memory | string | "4g" | Memory ceiling per session, in size syntax such as 4g. |
cpus | number | 2 | CPU ceiling per session, in cores. |
pids | number | 512 | Process count ceiling per session. |
fileMax | string | "1g" | Largest single file a session may write, in size syntax. |
tmpSize | string | none | Size of a session's private /tmp, in size syntax such as 512m. Backed by memory, so it is charged against memory when used, not against the host disk. The backend's small default starves a build that unpacks or compiles under /tmp, which is most of them. |
shmSize | string | none | Size of a session's private /dev/shm, in size syntax. |
diskTmp | boolean | none | Put a session's /tmp on disk instead of the memory-backed tmpfs, so a build's scratch is bounded by disk rather than by memory. The backend binds a directory under /state at /tmp, which still masks the host's /tmp as the tmpfs did, so the whole of /tmp is on disk, not only what honours TMPDIR. Replaces the tmpfs, so tmpSize has no effect while this is on. The directory is emptied at every launch, so a continued session starts on a clean /tmp. |
disk | string | "5g" | How much a session may add to its project and state together. Measured rather than enforced, because no backend caps what a process tree writes in aggregate without root. Passing it ends the session. |
diskCheckMs | number | 30000 | How often a session's disk use is measured, in milliseconds. |
gracePeriodMs | number | 10000 | How long a sandbox may take to stop before it is killed. |
hideHostAddress | boolean | false | Hide the host's network identity from a session, when it has a network. |
pathExtra | string[] or undefined | none | Directories added to a session's PATH, or none. |
env | Record<string, string> or undefined | none | Variables set in every session's environment, or none. |
sandbox.egress
What a session may reach outbound, and how that is enforced.
| field | type | default | what it does |
|---|---|---|---|
mode | EgressMode | "proxy" | Whether egress is port-only or forced through the broker. |
allow | string[] | [] | Hosts the broker permits, on top of the provider. A leading *. matches subdomains, and a lone * admits any host. A host not named here is refused, and a thread's owner can grant it with !grant. |
allowInternal | boolean | false | Whether the broker may dial an address on the host's own network. Only an address written as a literal: a name that resolves somewhere internal stays refused, since what it points at is not the operator's to decide. |
sandbox.incus
How the Incus backend starts a session's system container. Inert under any other backend.
| field | type | default | what it does |
|---|---|---|---|
project | string | "errand" | The Incus project instances and images live in. |
image | string | "errand-agent" | The image alias a session starts from when no golden is set. |
golden | string or undefined | none | A stopped instance to copy for each session, or none to create from the image each time. |
rootSize | string | "20g" | The size of a session's root filesystem, in size syntax such as 20g. |
guestUid | number | 1000 | The uid the agent runs as inside the guest. |
sandbox.policyExtra
Paths granted to a session on top of what the daemon already grants. Additive only. Every path is absolute, since a relative one in a policy means nothing.
| field | type | default | what it does |
|---|---|---|---|
read | string[] | none | Directories or files a session may read. |
write | string[] | none | Directories or files a session may write. |
execute | string[] | none | Directories a session may execute from. |
output
How much of the agent's activity reaches the thread.
| field | type | default | what it does |
|---|---|---|---|
forwardToolOutput | boolean | false | Whether tool output bodies are posted, not just that a tool ran. |
maxToolOutputChars | number | 1500 | Longest tool output posted before it is truncated and marked as such. |
maxAttachmentBytes | number | 5242880 | Largest attached file taken into a session, in bytes. |
maxAttachmentsPerMessage | number | 4 | Most attached files taken from one message. The rest are refused. |
maxUploadBytes | number | 20971520 | Largest file uploaded, on request with !file or as an image the agent generated, in bytes. |
postDiffs | boolean | true | Post a diff after the agent changes a file. |
postImages | boolean | true | Post each image the agent generates to the thread, as an upload with a caption. A turn posts at most four, and generating a fifth stops the turn, whether or not images are posted. |
postAudio | boolean | true | Post each clip the agent generates to the thread, as an upload with a caption. |
web
The local web interface. Absent means no listener at all. The address it binds to is always checked rather than trusted. Without login that address is the whole access control; with it, changing anything takes a Discord login.
| field | type | default | what it does |
|---|---|---|---|
host | string | "127.0.0.1" | Address to bind to. Must be loopback, private, or a tailnet address. |
port | number | 8787 | Port to listen on. |
observer | boolean | false | When true the interface may watch and read but not start, prompt, or control anything. |
publicUrl | string or undefined | none | Where the interface is reachable from outside, such as behind a tunnel. Set, it needs login or observer. |
web.login
The Discord application a visitor logs in through.
| field | type | default | what it does |
|---|---|---|---|
clientId | string | required | The application's OAuth2 client id. |
clientSecret | string | required | The application's OAuth2 client secret. Secret, and at least 8 bytes, so it can be scrubbed from what is posted. |
shutdown
Who may power off the host from a chat message. Off unless the list has somebody in it. This is the one command that acts on the machine rather than on a session, so it is not covered by any session role.
| field | type | default | what it does |
|---|---|---|---|
allowedUserIds | string[] | none | Account ids permitted to power off the host. |
limits
Bounds on how much work exists at once.
| field | type | default | what it does |
|---|---|---|---|
maxConcurrentTurns | number | 2 | Sessions that may have a model turn in flight simultaneously. |
maxLiveSessions | number | 4 | Sessions that may exist at all. |
maxQueueLength | number | 32 | Prompts that may wait for a turn slot. |
maxQueueWaitMs | number | 900000 | How long a queued prompt may wait before it expires unsent. |
timeouts
Deadlines that end or unblock a session.
| field | type | default | what it does |
|---|---|---|---|
idleMs | number | 1800000 | No message and no agent activity for this long ends the session. A tool call in flight is activity, however long it runs and whatever it says: a swarm or agent call reports nothing to the session until it returns, so silence during one is not idleness. |
startupMs | number | none | How long the agent has to become ready before it is abandoned. |
questionMs | number | none | How long a question posted to a thread waits for an answer. |
abortMs | number | none | How long an abort waits for the agent before it is forced. |
retention
When what a cold session left on disk is collected. A session that has not been used for olderThan loses its state directory, its record, its index entry and the receipts of its provider calls, and its own project too. A project named by hand in an older errand is never collected. A live session is never collected, however old.
| field | type | default | what it does |
|---|---|---|---|
enabled | boolean | true | Whether the daemon collects at all. errand threads gc works either way. |
olderThan | string | "30d" | How long a session goes unused before it is collected, such as 30d, 2w or 7d 12h. A month is 30 days and a year 365. |
every | string | "1d" | How often the daemon looks, as well as once when it starts. |