errand-bot 0.2.0

Run a coding agent from a chat channel, in a sandbox it cannot escape.
{
  "type": "object",
  "description": "The fully resolved configuration the daemon runs on.",
  "additionalProperties": false,
  "properties": {
    "chat": {
      "type": "object",
      "description": "Chat connection and who may drive the bot.",
      "additionalProperties": false,
      "properties": {
        "token": {
          "type": "string",
          "description": "Bot token. Secret. Never enters a sandbox."
        },
        "channelId": {
          "type": "string",
          "description": "The single channel the daemon serves. Everything else is ignored."
        },
        "allowedUserIds": {
          "description": "Accounts permitted to drive sessions. Must not be empty."
        },
        "blockedUserIds": {
          "description": "Accounts refused before anything else is considered."
        },
        "operatorUserIds": {
          "description": "Accounts that may control any session, not only their own."
        },
        "startOnMention": {
          "type": "boolean",
          "description": "Require a message to mention the bot before it starts a session."
        }
      },
      "required": [
        "token",
        "channelId",
        "allowedUserIds",
        "blockedUserIds",
        "operatorUserIds"
      ]
    },
    "agent": {
      "type": "object",
      "description": "Which model the agent talks to, and the credential it reaches it with.",
      "additionalProperties": false,
      "properties": {
        "provider": {
          "type": "string",
          "description": "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": {
          "type": "string",
          "description": "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": {
          "type": "string",
          "description": "Model an image is shown to when the working model cannot see one."
        },
        "delegate": {
          "type": "object",
          "description": "A cheaper model to ask about one artefact, or none.",
          "additionalProperties": false,
          "properties": {
            "model": {
              "type": "string",
              "description": "The model asked. Must be one the provider serves under the same key."
            },
            "perTurn": {
              "type": "number",
              "description": "How many delegations one turn may make before the rest stay at home."
            },
            "deadlineMs": {
              "type": "number",
              "description": "How long one delegation may take before it is abandoned."
            },
            "baseUrl": {
              "type": "string",
              "description": "Where the provider is reached, when the model store does not say."
            }
          },
          "required": [
            "model",
            "perTurn",
            "deadlineMs"
          ]
        },
        "rulesPath": {
          "type": "string",
          "description": "File of standing instructions given to every session, or none."
        },
        "providers": {
          "description": "Every provider, the one a session starts on included.  Each entry carries the credential errand reaches that provider with, optionally the `credentialName` the agent reads it from, and whatever else the agent's own configuration takes: `baseUrl`, `api`, `models`. Written into the agent's configuration verbatim apart from the credential, because that schema belongs to the agent.  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."
        },
        "aliases": {
          "description": "Short names for models, so a session is started without spelling one."
        }
      },
      "required": [
        "provider",
        "providers",
        "aliases"
      ]
    },
    "github": {
      "type": "object",
      "description": "How a session reaches GitHub, or none when unconfigured.",
      "additionalProperties": false,
      "properties": {
        "token": {
          "type": "string",
          "description": "Token the agent authenticates with. Secret, and reachable by the agent."
        },
        "userName": {
          "type": "string",
          "description": "Name commits are authored with."
        },
        "userEmail": {
          "type": "string",
          "description": "Email commits are authored with."
        }
      },
      "required": [
        "token",
        "userName",
        "userEmail"
      ]
    },
    "projectRoot": {
      "type": "string",
      "description": "Absolute path under which every session's project directory lives."
    },
    "stateDir": {
      "type": "string",
      "description": "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."
    },
    "sandbox": {
      "type": "object",
      "description": "What a session may consume, and what the backend enforces.",
      "additionalProperties": false,
      "properties": {
        "backend": {
          "type": "string",
          "enum": [
            "podman",
            "bailey"
          ],
          "description": "Which backend confines sessions."
        },
        "requireFullEnforcement": {
          "type": "boolean",
          "description": "Refuse to start when the backend cannot enforce every configured guarantee on this host, rather than reporting the gap and continuing."
        },
        "network": {
          "type": "string",
          "enum": [
            "restricted",
            "none"
          ],
          "description": "Network exposure granted to a session."
        },
        "egressPorts": {
          "description": "Ports a session may open outbound, when the network is not `none`."
        },
        "egress": {
          "type": "object",
          "description": "What a session may reach outbound, and whether it is brokered.",
          "additionalProperties": false,
          "properties": {
            "mode": {
              "description": "Whether egress is port-only or forced through the broker."
            },
            "allow": {
              "description": "Hosts the broker permits, on top of the provider. A leading `*.` matches subdomains; a lone `*` admits any host."
            },
            "allowInternal": {
              "type": "boolean",
              "description": "Whether the broker may dial an address on the host's own network."
            }
          },
          "required": [
            "mode",
            "allow",
            "allowInternal"
          ]
        },
        "image": {
          "type": "string",
          "description": "Container image the podman backend runs. Inert under bailey."
        },
        "memory": {
          "type": "string",
          "description": "Memory ceiling per session, in size syntax such as `4g`."
        },
        "cpus": {
          "type": "number",
          "description": "CPU ceiling per session, in cores."
        },
        "pids": {
          "type": "number",
          "description": "Process count ceiling per session."
        },
        "fileMax": {
          "type": "string",
          "description": "Largest single file a session may write, in size syntax."
        },
        "disk": {
          "type": "string",
          "description": "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": {
          "type": "number",
          "description": "How often a session's disk use is measured, in milliseconds."
        },
        "gracePeriodMs": {
          "type": "number",
          "description": "How long a sandbox may take to stop before it is killed."
        },
        "hideHostAddress": {
          "type": "boolean",
          "description": "Hide the host's network identity from a session, when it has a network."
        },
        "policyExtra": {
          "type": "object",
          "description": "Paths granted on top of the generated policy, or none.",
          "additionalProperties": false,
          "properties": {
            "read": {
              "description": "Directories or files a session may read."
            },
            "write": {
              "description": "Directories or files a session may write."
            },
            "execute": {
              "description": "Directories a session may execute from."
            }
          },
          "required": [
            "read",
            "write",
            "execute"
          ]
        },
        "pathExtra": {
          "description": "Directories added to a session's PATH, or none."
        },
        "env": {
          "description": "Variables set in every session's environment, or none."
        }
      },
      "required": [
        "backend"
      ]
    },
    "output": {
      "type": "object",
      "description": "How much of the agent's activity reaches the thread.",
      "additionalProperties": false,
      "properties": {
        "forwardToolOutput": {
          "type": "boolean",
          "description": "Whether tool output bodies are posted, not just that a tool ran."
        },
        "maxToolOutputChars": {
          "type": "number",
          "description": "Longest tool output posted before it is truncated and marked as such."
        },
        "maxAttachmentBytes": {
          "type": "number",
          "description": "Largest attached file taken into a session, in bytes."
        },
        "maxAttachmentsPerMessage": {
          "type": "number",
          "description": "Most attached files taken from one message. The rest are refused."
        },
        "postDiffs": {
          "type": "boolean",
          "description": "Post a diff after the agent changes a file."
        }
      }
    },
    "shutdown": {
      "type": "object",
      "description": "Who may power off the host. Empty means nobody, which is the default.",
      "additionalProperties": false,
      "properties": {
        "allowedUserIds": {
          "description": "Account ids permitted to power off the host."
        }
      },
      "required": [
        "allowedUserIds"
      ]
    },
    "web": {
      "type": "object",
      "description": "The web interface, or none when one is not served.",
      "additionalProperties": false,
      "properties": {
        "host": {
          "type": "string",
          "description": "Address to bind to. Must be loopback, private, or a tailnet address."
        },
        "port": {
          "type": "number",
          "description": "Port to listen on."
        },
        "observer": {
          "type": "boolean",
          "description": "When true the interface may watch and read but not start, prompt, or control anything."
        },
        "publicUrl": {
          "type": "string",
          "description": "Where the interface is reachable from outside, such as behind a tunnel."
        }
      }
    },
    "limits": {
      "type": "object",
      "description": "Bounds on how much work exists at once.",
      "additionalProperties": false,
      "properties": {
        "maxConcurrentTurns": {
          "type": "number",
          "description": "Sessions that may have a model turn in flight simultaneously."
        },
        "maxLiveSessions": {
          "type": "number",
          "description": "Sessions that may exist at all."
        },
        "maxQueueLength": {
          "type": "number",
          "description": "Prompts that may wait for a turn slot."
        },
        "maxQueueWaitMs": {
          "type": "number",
          "description": "How long a queued prompt may wait before it expires unsent."
        }
      }
    },
    "timeouts": {
      "type": "object",
      "description": "Deadlines that end or unblock a session.",
      "additionalProperties": false,
      "properties": {
        "idleMs": {
          "type": "number",
          "description": "No message and no agent activity for this long ends the session."
        },
        "startupMs": {
          "type": "number",
          "description": "How long the agent has to become ready before it is abandoned."
        },
        "questionMs": {
          "type": "number",
          "description": "How long a question posted to a thread waits for an answer."
        },
        "abortMs": {
          "type": "number",
          "description": "How long an abort waits for the agent before it is forced."
        }
      },
      "required": [
        "startupMs",
        "questionMs",
        "abortMs"
      ]
    },
    "problems": {
      "description": "Every problem found, in the order the checks ran."
    },
    "$schema": {
      "type": "string",
      "description": "Where this schema lives."
    }
  },
  "required": [
    "chat",
    "agent",
    "projectRoot",
    "stateDir",
    "sandbox",
    "output",
    "shutdown",
    "limits",
    "timeouts",
    "problems"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/QaidVoid/errand/main/config.schema.json",
  "title": "errand configuration"
}