errand-box runs code an AI agent writes, on a host that also holds the agent's credentials. This page states what protects what, and what does not.
Use GitHub's private vulnerability reporting on this repository ("Security", then "Report a vulnerability"). Do not open a public issue for it.
| side | what runs there | trusted? |
|---|---|---|
| host, root | incusd, incus-user, lxcfs, s6 or systemd, errand-box setup, up, maint |
yes |
| host, the errand account | errand, the bailey shim, the watchdog |
yes, but holds credentials (below) |
| guest | everything in a session: kage, the agent's commands, Docker and podman inside, the guest's root | no |
A guest is an unprivileged system container. Its boundary is the shared host kernel, user namespaces with a uid range of its own, seccomp, and the cgroup limits Incus sets. It is not a virtual machine. A kernel vulnerability, used from inside a guest, gives control of the host.
| credential | where it lives | can a guest read it? |
|---|---|---|
| chat bot token | errand's config (~errand/.config/errand/config.json, mode 0600) |
no |
| model provider key | errand's config; errand's broker adds it to requests | no: under egress.mode = proxy the guest gets a per-session nonce, and a provider the broker cannot route gets no variable at all (docs/research/review.md) |
| GitHub token | errand's config | yes: errand puts it in the session's environment as GH_TOKEN. Use a token of an account that owns nothing of value. |
the operator's creds.env |
wherever the operator keeps it; setup only reads it | no |
errand-box never prints a credential. setup names which keys it read,
never their values. The shim passes the session's environment into the guest
through the Incus API, not on a host command line.
Give each credential before the step that needs it. Put it in creds.env
(mode 0600) on the host, or in the repository's secrets for a workflow.
Never in a chat, an issue, or a file in a repository.
| for | credential | permissions it needs |
|---|---|---|
| errand: chat | ERRAND_CHAT_TOKEN, a Discord bot token, and the channel id and user ids in errand-box.env |
the bot's privileged intent Message Content, set in the Discord developer portal (errand asks for Guilds, Guild Messages and Message Content); in the channel: view, send messages, create public threads, send messages in threads, manage threads (errand archives them), attach files, add reactions, read message history |
| errand: the model | ERRAND_PROVIDER_CREDENTIAL, and ERRAND_PROVIDER_BASE_URL in errand-box.env |
a key for that endpoint only. With a base URL the key stays in errand's broker |
| sessions: GitHub (optional) | ERRAND_GITHUB_TOKEN, and ERRAND_GITHUB_USER_NAME and ERRAND_GITHUB_USER_EMAIL in errand-box.env |
a fine-grained token of an account that owns nothing of value, limited to the repositories the agent may change: Contents read and write, Pull requests read and write, Metadata read. Every session can read it. errand-box does not configure errand's GitHub trigger, which reads notifications and needs a classic token |
| the scheduled workflows | the workflow token, given by GitHub | as each workflow declares. For freshness.yml to open pull requests, set "Allow GitHub Actions to create and approve pull requests" in the repository's Actions settings |
| CI on those pull requests (optional) | a repository secret FRESHNESS_TOKEN |
a fine-grained token for this repository only: contents and pull requests, read and write. A pull request opened with the workflow token starts no workflow; one opened with this token does |
| a release | none beyond the workflow token | release.yml asks for contents: write |
| component taken over | reaches |
|---|---|
| a guest's root | its own instance; the two host directories errand granted it (the project and the session's state); errand's broker, with errand's allowlist. Not the host network, other guests, or the host's other files. |
| a guest that leaves its container through a setup mistake | an unprivileged host uid of its own range; files of the errand account through the agent's mapped uid 1000. |
| a guest that leaves its container through a kernel vulnerability | the host. |
| the errand account | errand's credentials. Its restricted project forbids privileged containers, raw LXC settings, disk devices outside its home, and proxy devices (T-020), so the broker link is a shared unix socket. No device can reach a host socket the project does not intend (E-61 is closed). A guest cannot add devices, so only the errand account itself reaches this. |
| incusd, incus-user, lxcfs | root. |
- The host never acts on a file a guest can write. The session's state
directory is writable from the guest. So the shim checks the policy
errand wrote there against the digest it recorded at
trust, and every Incus client call names errand-box's own client directory instead of theHOMEerrand points into that state directory. - No signal by name. Daemons stop through their supervisor, by pid.
- No unverified download runs. Every fetched archive is checked against
pins.tomlbefore use. - A session gets no network device. Its only way out is errand's broker.
- errand's chat service is Discord outside a test. Only the variable
ERRAND_DISCORD_MOCK_URLpoints errand at a local stand-in, and errand refuses it unless the URL and the stand-in's gateway are loopback IP addresses and the token begins withmock.(errand/src/chat/endpoint.rs). Nothing errand-box writes sets it.
errand-box doctor and setup report these, each as DEGRADED:
- no AppArmor in the kernel: Incus loses its AppArmor profile; seccomp and per-guest uid ranges remain (edge case E-17);
- no usable lxcfs: guests see the host's memory and CPU count (E-08);
- a
dirstorage pool: the disk quota is not enforced (E-05); - Incus 6.0.0: new instances rewrite their rootfs, which is slow but not less safe (E-20).