# Configuration reference
All configuration is hand-edited TOML under the config root
(`~/.config/roster`), read **live**: edits take effect on the next read,
there is no deploy step, and `roster server validate` runs the exact
loader the daemon uses and prints every error, not just the first. Broken
config fails closed — the gateway denies, dispatch pauses, `server start`
refuses to boot.
The files:
```
org.toml org-wide policy: grants, actions, trust, budgets,
knowledge/context policy
workers/<name>/worker.toml one worker: channels, heartbeat, overlays
workers/<name>/identity.md who the worker is (prose, not config)
connections/<name>.toml one service capability (usually wizard-written)
providers.toml optional overlay on the built-in provider registry
```
One rule to know: `scope` is never hand-written. The loader tags everything
in `org.toml` as org-wide and everything in a worker's file as that worker's,
and rules, budgets, and trust all match subjects by ancestry.
Config that grants nothing is safe by default: no grants means no egress,
no actions means no proposals, no limits means no caps (the default-deny
judge is the security floor; budgets are an opt-in ceiling).
## org.toml
### `[engine]`
| `image` | `ghcr.io/manasgarg/roster-box:latest` | The box image workers run in. The server pulls it at start (re-pulled on every restart, so `:latest` stays current). Point it at a locally built tag (e.g. `roster-box`) to run your own build — a bare tag with no registry component is never pulled, only expected to exist. |
| `dir` | unset | Dev override: mount this checkout read-only over the engine baked into the box image. Unset is the normal, production posture. |
### `[[grant]]` — egress rules
Ordered; first match wins; no match denies. See [gateway.md](gateway.md)
for the full match vocabulary and semantics.
```toml
[[grant]]
name = "model-api"
match = { host = ["chatgpt.com", "api.anthropic.com"], port = 443 }
```
| `name` | yes | The rule's name — budgets, trust, and audit lines bind to it |
| `match` | no (default: match everything) | Nested table: `protocol`, `host`, `port`, `method`, `pathPrefix`, `headerContains`, `maxBodySize`, `mcp = { method, tool }`; scalars or arrays |
| `verdict` | yes | `allow`, `deny`, or `tunnel` |
| `inject` | no | `{ credential = "<vault name>", provider?, headers? }` — swap the box's sentinel for the real credential in transit |
### `[[action]]` — what a worker may propose
```toml
[[action]]
name = "email-send"
executor = "email"
trust = "gate" # default; "auto" skips the desk
wake_on_resolve = true # file a continuation task when the gate resolves
```
Executors: `message-user`, `email`, `identity`, `purpose`,
`discord`, `slack`, `task`, `term`, `knowledge`. An intent with no grant is
refused, not gated. See [actions-and-trust.md](actions-and-trust.md).
(Memory has no executor: the worker writes `store/memory/` directly —
[memory.md](memory.md).)
### `[[trust]]` — the ladder
```toml
[[trust]]
intent = "email-send"
match = { to = "*@ourco.com" } # glob predicates over the payload
```
First matching rule decides. Every `match` field must hold, and a list
payload field matches only if *every* element does. `earned` promotes to
auto after `after` (default 5) executed gates with zero denials — one
denial revokes it. No rule → the action grant's own `trust` default.
### `[budget]` — currencies, meters, limits
```toml
[budget]
currencies = ["usd", "model_calls"]
vars = { price = { model_call = 0.05 } }
[[budget.meter]]
match = 'decision.rule == "model-api"'
spend = { model_calls = "1", usd = "vars.price.model_call" }
[[budget.limit]]
currency = "usd"
```
Meters are CEL over `request`, `decision`, `subject`, `vars`. Limits at org
scope cap the whole fleet; per-worker limits go in the worker's file. Details and
enforcement semantics in [gateway.md](gateway.md).
### `[[expose]]` — sentinel env vars
```toml
[[expose]]
credential = "github" # must exist in the vault (fail-closed)
env = "GH_TOKEN" # set in the box, to the sentinel value
```
Gives box tools an env var that *looks* authenticated; the gateway injects
the real value only where a grant's injection applies. Reserved names
(`HOME`, the proxy/CA variables, anything starting `ROSTER_`) are
rejected, as are overlapping-scope duplicates. Connections write these for
you — hand-written exposes are for credentials outside the connection
model.
### `[context]` — prompt budgets (characters)
| `max_injected_chars` | 48000 |
| `identity_max_chars` | 12000 |
| `purpose_max_chars` | 8000 |
| `briefing_max_chars` | 4000 |
| `task_max_chars` | 24000 |
| `history_max_messages` | 25 |
| `history_max_chars` | 6000 |
Mandatory blocks fail rather than truncate; see [context.md](context.md).
`history_*` bound the recent-channel-history block a fresh session's first
turn carries (newest kept, oldest dropped; `0` disables it) — history
truncates rather than fails, since the full record stays readable at
`$HOME/channel`.
### `[memory]` — recall bounds
| `enabled` | `true` | compile the memory block at all |
| `recall_max_notes` | 20 | active notes recalled per run (pinned first, then newest) |
| `recall_char_budget` | 6000 | rendered block cap; oldest unpinned drop first |
Recall is a bounded window into the worker's own
`store/memory/memory.jsonl` ([memory.md](memory.md)) — the worker writes
and curates the file; the host only reads it into each run's input.
Worker overlays allowed, like `[context]`.
### `[knowledge]`
| `enabled` | `true` | |
| `write_from` | `"clean-room"` | the default write contract for gated repos — `"clean-room"`: only runs without person-data may write; `"any-run"`: scan-only. A `write_from` in a gated connection file beats this default for that repo ([repos.md](repos.md)) |
| `max_file_chars` | 200000 | per-file cap |
| `max_repo_bytes` | 1000000000 | repo size cap |
| `max_deletions_ungated` | 20 | a `repo_push` deleting more files than this waits for a human gate |
### `[store]`
| `snapshots` | 14 | rotating store snapshots kept per worker (0 disables — the store is then unrecoverable after a bad run) |
## workers/\<name\>/worker.toml
```toml
name = "dobby" # must equal the folder name
[channels]
discord = "discord" # vault credential for its bot (also: slack = "…")
heartbeat = "every 30m" # the curation pulse; default 30m, "off" disables
[[budget.limit]] # per-worker cap (limits only; currencies,
currency = "model_calls" # vars, and meters are org-level)
window = "hour"
max = 60
```
A worker's file may also carry its own `[[grant]]`, `[[action]]`,
`[[trust]]`, `[[expose]]`, and `[context]`/`[knowledge]`
overlays. Overlays merge over org defaults; the knowledge overlay can only
narrow (disable features, reduce limits) — it cannot re-enable, raise, or
relax what the org set. Two workers cannot bind the same channel credential.
`identity.md` sits next to it: owner-authored prose defining who the worker
is, composed into every run, editable by admins — worker-proposed edits always
gate ([actions-and-trust.md](actions-and-trust.md)).
## connections/\<name\>.toml
Usually written by `roster connection add`; hand-editable after. The file
stem is the vault credential name.
| `provider` | yes | registry entry (login flow + inject template), or any name if inline inject is given |
| `[grant.<worker>]` | no | availability edge, one per worker (`[grant.org]` = fleet-wide); its keys scope the edge to provider dimensions. None = connected, granted to no one. Legacy `workers = ["dobby"]` / `scope = "org"` + `[restrict]` still parse as identical edges |
| `hosts` | yes | allowed hostnames |
| `methods` | no (default `["*"]` — all) | allowed HTTP methods; list verbs (e.g. `["GET"]`) to narrow the grant |
| `env` | yes | the sentinel env var the box sees |
| `inject_header` / `inject_value` | together | custom injection, e.g. `"private-token"` / `"{token}"` |
A connection whose secret is missing from the vault is **disabled** — grant
and exposure omitted, loud warning in `validate` and `connection ls` — not
a fatal config error. See [connections.md](connections.md).
## providers.toml
The binary ships a provider registry: `openai-codex` and `anthropic`
(OAuth model providers), `github`, `gitlab`, `notion`, `linear` (token
capabilities), `slack` (channel *and* capability from one login), and
`discord`, `smtp` (host-side channel/email infrastructure). An entry's
supported uses come from its `use` array when present and are inferred
otherwise (a `connection` block → capability; channel auth kinds →
channel; else model). An optional `brief` string is surfaced to workers in
their run context's connections brief — the place to say how the service
is meant to be used (see [context.md](context.md)); `hidden = true` keeps an entry compiling without
showing it in the catalog. `providers.toml` overlays the registry — one
top-level table per provider, each entry **replacing** that provider's
default wholesale (`roster connection add --declare` writes these for
you):
```toml
[acme]
auth = "api_key"
inject = [{ header = "authorization", value = "Bearer {key}" }]
connection = { hosts = ["api.acme.com"], env = "ACME_TOKEN" }
```
## Environment variables
| `ROSTER_ROOT` | self-contained mode: everything under `$ROSTER_ROOT/{config,data,state}` — tests, scratch deployments, side-by-side instances |
| `XDG_CONFIG_HOME` / `XDG_DATA_HOME` / `XDG_STATE_HOME` | standard XDG overrides for the three roots |
| `ROSTER_VAULT_DIR`, `ROSTER_CA_DIR` | relocate the vault or CA individually |
| `ROSTER_EMAIL_SINK` | testing: write outbound email to `state/outbox/` instead of SMTP |