Operator reference
atrium is a single-operator assistant: one Go binary with an embedded web UI and SQLite state, deployed through a NixOS module. The host supplies credentials, external services, the reverse proxy, persistence and backups.
This reference covers two things: how an instance is configured, and the behavior and boundaries an operator can rely on today.
Configuration
- Configuration reference lists every setting with its type, default and class, and describes the TOML file,
<key>_filesecrets,${NAME}interpolation and precedence. Settings and secrets summarizes how they behave. - Environment variables lists every setting pinned through the environment; the database section of the behavior page covers the database key.
- The same settings are described as a JSON Schema in
atrium.schema.json. - NixOS module options lists the
services.atriumoptions. They configure the deployment (units, listeners, deployment variables), not the settings registry.
The two config pages and the schema are generated from the settings registry and are not edited by hand.
Current behavior
Operator-visible behavior and boundaries states what the running application does and refuses to do: access, mail safety, assistant limits, settings and secrets and the database.
Configuration reference
Generated by just settingsgen. Do not edit.
The file named by ATRIUM_CONFIG is TOML with one table per module. Unknown keys are refused. A string value may contain ${NAME} references to environment variables; an unset variable is refused. <key>_file reads the value from a file and trims it; secret settings accept only <key>_file. A secret setting’s environment variable also has a _FILE form naming a file whose trimmed contents are the value; setting both forms is refused. Every key set here or through its environment variable is pinned: it shows read-only and cannot be changed with atrium settings. Precedence: environment variable, configuration file, stored value, default.
action_links
| Key | Type | Default | Class | Environment variable |
|---|---|---|---|---|
action_link_key | string | none | secret | ATRIUM_ACTION_LINKS_ACTION_LINK_KEY |
anthropic
| Key | Type | Default | Class | Environment variable |
|---|---|---|---|---|
anthropic_admin_key | string | none | secret | ATRIUM_ANTHROPIC_ANTHROPIC_ADMIN_KEY |
assistant
| Key | Type | Default | Class | Environment variable |
|---|---|---|---|---|
budget_unattended_share | float | 0.8 | advanced | ATRIUM_ASSISTANT_BUDGET_UNATTENDED_SHARE |
cache_ttl_chat | enum (5m, 1h) | 5m | ui | ATRIUM_ASSISTANT_CACHE_TTL_CHAT |
max_run_cost_usd | float | 2 | cap | ATRIUM_ASSISTANT_MAX_RUN_COST_USD |
max_steps | int | 24 | advanced | ATRIUM_ASSISTANT_MAX_STEPS |
monthly_budget_usd | float | 75 | ui | ATRIUM_ASSISTANT_MONTHLY_BUDGET_USD |
core
| Key | Type | Default | Class | Environment variable |
|---|---|---|---|---|
http_timeout_seconds | int | 600 | advanced | ATRIUM_CORE_HTTP_TIMEOUT_SECONDS |
public_url | string | none | ui | ATRIUM_CORE_PUBLIC_URL |
tz | string | America/Port_of_Spain | ui | ATRIUM_CORE_TZ |
db
| Key | Type | Default | Class | Environment variable |
|---|---|---|---|---|
key | string | none | secret | ATRIUM_DB_KEY |
openai
| Key | Type | Default | Class | Environment variable |
|---|---|---|---|---|
openai_admin_key | string | none | secret | ATRIUM_OPENAI_OPENAI_ADMIN_KEY |
web
| Key | Type | Default | Class | Environment variable |
|---|---|---|---|---|
activity_page_size | int | 50 | advanced | ATRIUM_WEB_ACTIVITY_PAGE_SIZE |
activity_snippet_chars | int | 800 | advanced | ATRIUM_WEB_ACTIVITY_SNIPPET_CHARS |
body_cap_bytes | int | 1048576 | advanced | ATRIUM_WEB_BODY_CAP_BYTES |
chat_thread_limit | int | 50 | advanced | ATRIUM_WEB_CHAT_THREAD_LIMIT |
job_history | int | 10 | advanced | ATRIUM_WEB_JOB_HISTORY |
learned_limit | int | 50 | advanced | ATRIUM_WEB_LEARNED_LIMIT |
loop_limit | int | 100 | advanced | ATRIUM_WEB_LOOP_LIMIT |
page_size | int | 50 | advanced | ATRIUM_WEB_PAGE_SIZE |
preview_bytes | int | 200000 | advanced | ATRIUM_WEB_PREVIEW_BYTES |
project_thread_limit | int | 100 | advanced | ATRIUM_WEB_PROJECT_THREAD_LIMIT |
run_history | int | 30 | advanced | ATRIUM_WEB_RUN_HISTORY |
snooze_choices | string | 1,3,7,14 | advanced | ATRIUM_WEB_SNOOZE_CHOICES |
Environment variables
Generated by just settingsgen. Do not edit.
An environment variable pins its setting and wins over the configuration file and the stored value. The file named by ATRIUM_CONFIG is the only other source of pins. A secret setting also accepts <variable>_FILE, a path to a file whose trimmed contents are the value; setting both forms is refused.
| Variable | Setting |
|---|---|
ATRIUM_ACTION_LINKS_ACTION_LINK_KEY | action_links.action_link_key |
ATRIUM_ANTHROPIC_ANTHROPIC_ADMIN_KEY | anthropic.anthropic_admin_key |
ATRIUM_ASSISTANT_BUDGET_UNATTENDED_SHARE | assistant.budget_unattended_share |
ATRIUM_ASSISTANT_CACHE_TTL_CHAT | assistant.cache_ttl_chat |
ATRIUM_ASSISTANT_MAX_RUN_COST_USD | assistant.max_run_cost_usd |
ATRIUM_ASSISTANT_MAX_STEPS | assistant.max_steps |
ATRIUM_ASSISTANT_MONTHLY_BUDGET_USD | assistant.monthly_budget_usd |
ATRIUM_CORE_HTTP_TIMEOUT_SECONDS | core.http_timeout_seconds |
ATRIUM_CORE_PUBLIC_URL | core.public_url |
ATRIUM_CORE_TZ | core.tz |
ATRIUM_DB_KEY | db.key |
ATRIUM_OPENAI_OPENAI_ADMIN_KEY | openai.openai_admin_key |
ATRIUM_WEB_ACTIVITY_PAGE_SIZE | web.activity_page_size |
ATRIUM_WEB_ACTIVITY_SNIPPET_CHARS | web.activity_snippet_chars |
ATRIUM_WEB_BODY_CAP_BYTES | web.body_cap_bytes |
ATRIUM_WEB_CHAT_THREAD_LIMIT | web.chat_thread_limit |
ATRIUM_WEB_JOB_HISTORY | web.job_history |
ATRIUM_WEB_LEARNED_LIMIT | web.learned_limit |
ATRIUM_WEB_LOOP_LIMIT | web.loop_limit |
ATRIUM_WEB_PAGE_SIZE | web.page_size |
ATRIUM_WEB_PREVIEW_BYTES | web.preview_bytes |
ATRIUM_WEB_PROJECT_THREAD_LIMIT | web.project_thread_limit |
ATRIUM_WEB_RUN_HISTORY | web.run_history |
ATRIUM_WEB_SNOOZE_CHOICES | web.snooze_choices |
NixOS module options
The flake’s nixosModules.default defines services.atrium. All options below sit under that prefix. Nothing is created unless services.atrium.enable is true.
These are deployment options. They produce systemd units and environment variables for them. They are separate from the settings in the configuration reference, which are pinned through ATRIUM_<MODULE>_<KEY> variables or the TOML file named by ATRIUM_CONFIG; those can be supplied through environmentFiles or extraEnvironment.
Core
The module still passes several deployment values under their Phase A variable names. A value whose setting has moved into the registry, such as the mail mode and the time zone, takes effect only through that setting’s ATRIUM_<MODULE>_<KEY> pin, which environmentFiles or extraEnvironment can supply.
| Option | Type | Default | Meaning |
|---|---|---|---|
enable | bool | false | Enables the module. |
package | package | the flake’s static Go build for the host platform | The atrium application used by every unit and by atrium-run. |
id | int | 880 | uid and gid of the static atrium user and group that own /var/lib/atrium. |
mode | "shadow" or "live" | "shadow" | Mail mode. Only live lets atrium change mailboxes on its own. The module passes it as the Phase A variable ATRIUM_MODE, which atrium no longer reads: the mail mode is the mail_mode setting, set only through its ATRIUM_<MODULE>_<KEY> pin or the configuration file. |
environmentFiles | list of string | [] | EnvironmentFile= paths read by every unit and by atrium-run. They hold credentials and any pinned settings. |
extraEnvironment | attribute set of string | {} | Environment added to every unit and to atrium-run. It is merged after the module’s own variables, so an entry replaces a module-set variable of the same name. |
modelServerUnits | list of string | [] | Units that serve chatUrl and embedUrl. The application units start after them and want them. |
timeZone | null or string | config.time.timeZone | Operator time zone. |
Endpoints
Each option is null by default. A null option sets no environment variable for the units.
| Option | Meaning |
|---|---|
chatUrl | OpenAI-compatible chat completions base URL used for mail classification. |
embedUrl | OpenAI-compatible embeddings base URL. |
publicUrl | URL the operator reaches the web UI at; used in notifications and signed media links. |
grafanaUrl | Grafana base URL for the observability tools. |
icloudAppleId | Apple ID for iCloud CalDAV and CardDAV. Setting it also defines the vdirsyncer unit and timer that mirror calendars and contacts, and points atrium at the mirrored directories. |
Web listeners
| Option | Type | Default | Meaning |
|---|---|---|---|
web.host | string | "0.0.0.0" | Address the public listener binds. |
web.port | port | 8480 | Port of the public listener. |
web.auth | "proxy" or "local" | "proxy" | proxy (the default) trusts the Remote-Groups header from the reverse proxy and requires web.allowedHost. local is possible and trusts every request. |
web.allowedHost | null or string | null | The one Host header the public listener accepts. The configuration fails to evaluate with web.auth = "proxy" while it is null. |
web.local.enable | bool | true | Runs a second listener bound to 127.0.0.1 with --auth local, defined as atrium-web-local. |
web.local.port | port | 8481 | Port of the loopback listener. |
Notifications
| Option | Type | Default | Meaning |
|---|---|---|---|
ntfy.url | null or string | null | ntfy server base URL. |
ntfy.topic | null or string | null | Topic for the assistant’s notifications. |
ntfy.mediaTopic | null or string | null | Topic for media release notifications. |
Media
| Option | Type | Default | Meaning |
|---|---|---|---|
media.enable | bool | false | Enables the Media section: filing, EZTV channels and the library. Adds ffmpeg-headless to the path of the web and scheduler units. |
media.group | string | "media" | Group that owns the media tree. The web and scheduler units write through it with umask 0002. |
media.root | string | "/var/lib/media" | Media tree containing downloads/, import/, movies/ and tv/. |
media.qbittorrentUrl | null or string | null | qBittorrent Web UI URL, reachable without login. |
media.moviesUrl | null or string | null | URL serving root/movies read-only on the private network. |
media.tvUrl | null or string | null | URL serving root/tv read-only on the private network. |
Fixed by the module
Regardless of options, the module places the database at /var/lib/atrium/atrium.db and the message cache at /var/lib/atrium/cache/messages, and installs the atrium-run command, which runs the atrium binary as the atrium user with the same environment files and variables as the units.
The module defines these units: atrium-web, atrium-watch, atrium-agent-scheduler, the oneshot services atrium-sync, atrium-notion-sync and atrium-scan with their timers, and the code-mode worker socket and service. atrium-web-local exists only with web.local.enable, and atrium-vdirsyncer-sync with its timer only when icloudAppleId is set.
Operator-visible behavior and boundaries
This page states what atrium does and refuses to do today. It describes current behavior only. Setting names, types and defaults live in the generated setting reference and are not repeated here.
Access
atrium servebinds127.0.0.1unless told otherwise. Proxy authentication is its default mode; local mode is selected with--auth local. A listener in proxy mode refuses to start without an accepted Host (--allowed-host, or the public host setting).- Every request except
GET /healthzmust carry an accepted Host header; any other Host receives400. - In proxy mode the request must carry a
Remote-Groupsheader that listsatrium_access, otherwise it receives403. Static assets under/static/and valid signed media links are exempt from the group check. The Host check still applies to them. - In local mode there is no group check. Without an explicit allowed Host, the accepted Hosts are
localhost,127.0.0.1and[::1]. - The NixOS module runs the public listener in proxy mode and, by default, a second listener bound to
127.0.0.1in local mode. POST,PUT,PATCHandDELETErequests need the CSRF token that atrium issues in a cookie and a request whoseOrigin, when present, matches the Host. Responses carry a Content-Security-Policy.- A signed media link grants one file under
/media/file/. It carries an expiry and an HMAC-SHA256 signature, and is valid for 12 hours from issue. A missing, altered or expired signature is not accepted and the request falls back to the normal group check. The signing key is the media link key setting when it is set; otherwise it is read frommedia-link.keybeside the database, which is created on first use.
Mail safety
Every mailbox write goes through one safety gate.
- Modes. The mail mode (
mail_mode) isshadow(the default) orlive; any other value is refused. It is a deploy-time setting of classbootstrap: only an environment variable or the configuration file sets it, andatrium settings setcannot change it. Inshadow, atrium records what it would do and makes no automatic mailbox writes: the automatic pass returns without acting. - Automatic writes happen only in
livemode and only while atrium is not paused (apausefile beside the database). They are limited to: filing or trashing read, unflagged inbox mail according to recorded decisions; flagging unread inbox mail from known contacts; applying the operator’s folder corrections made after go-live; and rule-based deletions, which run at most once a day per account. Mail that was already read and in the inbox when live mode first started is never touched by the inbox pass. - Approval-required writes are every other mailbox change, such as the proposals from rebalance, deletion and label scans. They are applied only when the operator approves the proposal; the approval is the only scope in which they pass the gate in
shadowmode. - Operator send. The assistant can save and edit drafts but has no tool to send them. Sending is an operator action from the Drafts page. In
shadowmode the only append the gate allows outside an approved proposal is the copy of a sent message into the account’s Sent mailbox. - Write caps. A pass makes at most the write cap of automatic writes per account (setting
write_cap, default 100, range 1 to 1000); the remainder is deferred to a later pass and logged. - Verification. Before each write atrium re-reads the message on the server. If the message changed (for example it was read, flagged or moved in the meantime), the write is skipped and recorded as skipped with its reason.
- Protected folders. Trash, Sent, Drafts and Junk (identified by their special-use flags or well-known names such as Bin, Deleted Items, Sent Items and Spam) are never a source or a destination of automatic filing. Destinations must be existing, safely named folders; Gmail labels beginning with
\or[Gmail]and system labels are refused. - Retries. A message whose automatic action failed or was skipped is attempted again until the configured number of such outcomes (setting
write_attempts, default 3) is recorded for it. A message already acted on is not acted on again. - Outcome unknown stays blocked. atrium records the intent before it writes. If the connection is lost or the process is interrupted so that the result is unknown, the action is marked interrupted and is never retried automatically. An undo that copied the message back but did not expunge the original, or whose outcome is unknown, is likewise recorded as blocking; further undo of that action is refused until the operator resolves the mailbox by hand.
- Undo. Every applied automatic action that has an undo plan can be undone by the operator, which moves the message back, restores labels, restores the trash state or removes the flag. Undo runs under its own approval scope and also records its outcome.
- Message handling. Moves use COPY, STORE
\Deletedand UID EXPUNGE, not IMAP MOVE.
Assistant limits
- Scheduled runs (cron) cannot call any tool whose effect is a write. They can read, and they can create proposals for the operator to approve.
- Nightly runs have the same read access and may in addition create, update, move, archive and alias Notion pages, and update the profile. Profile changes are logged and undoable.
- Chat is the only run kind that can create calendar events, edit drafts, or change money, media or schedule records.
- Research subagents are limited to read-only tools, at most 12 steps and at most $0.25 of spend per run, with at most two running at once. They cannot write, notify, schedule or start other agents.
- Per-run limits. Each run stops after the configured maximum number of steps (default 24) and maximum cost (default $2); both are settings.
- Monthly budget. Spend for the calendar month in the operator’s time zone is compared with the monthly budget. Chat stops starting new work at 100% of the budget. Scheduled and nightly runs stop at the configured unattended share of the budget (default 80%). A research subagent does not start once a budget limit has been reached.
- Sending mail is an operator action only; see Mail safety.
- Tool permissions, run-kind restrictions and spending limits are enforced in code, not in prompts. Mail, files, pages and tool results are treated as untrusted data and cannot grant URL access or extend skills. Arbitrary web fetches use an allowlist and SSRF protection on every redirect.
- Code execution is unavailable.
Settings and secrets
- Precedence. For a setting: an environment variable, then the configuration file, then a value stored in the database, then the default. An environment variable or configuration file value pins the setting.
- Pins are read-only. A pinned setting cannot be changed or unset through the settings registry;
atrium settings setandunsetrefuse it and name the source of the pin. - Naming. The environment variable for a setting is
ATRIUM_<MODULE>_<KEY>, where<KEY>is the whole registry key. When the key already begins with the module name, the module appears twice:ATRIUM_ANTHROPIC_ANTHROPIC_ADMIN_KEYandATRIUM_OPENAI_OPENAI_ADMIN_KEYfor the keysanthropic_admin_keyandopenai_admin_key. For the database key (moduledb, keykey) the variable isATRIUM_DB_KEY. The environment variable list is authoritative. A secret setting’s environment variable also has a_FILEform naming a file whose trimmed contents are the value, for exampleATRIUM_DB_KEY_FILE; setting both forms is refused. In the configuration file a secret accepts only its<key>_fileform. - Settings CLI.
atrium settings list,get,setandunsetshow or change instance-level values; each shows the effective value, its source (pin,db,default) and its class. Secret settings are neither shown nor changed here. - Secrets CLI.
atrium secrets set <name>reads the value from standard input (the command line never carries it),deleteremoves it, andstatusreports each secret asset,unsetorpinned. Secrets are write-only: no command returns a stored secret. Setting a secret that the deployment pins is refused. A pin-only secret, such as the database key, can be neither stored nor deleted through the CLI. - Encrypted database required for stored admin keys. The Anthropic and OpenAI admin keys are refused by
atrium secrets setunless the database is encrypted. - Audit log. Every settings write and every secret change appends a row to the append-only
settings_audittable with the key, the actor, the source (cli,uiormigration), and the old and new values. For a secret the row records onlychanged. The table rejects updates and deletes, and the rows are listed read-only on the Activity page. - Redaction. Secret values of at least eight characters are scrubbed from log output, including their URL- and JSON-escaped forms, from audit values and from the recorded
agent_writessnapshots.
Database
- Encryption at rest. With a database key, every command opens the database encrypted with SQLCipher, and a database created under that key is encrypted. The key is 64 hexadecimal digits, supplied by
db.key_filein the configuration file, byATRIUM_DB_KEYorATRIUM_DB_KEY_FILE, or by thedb_keysystemd credential. Without a key the database stays plaintext. - Refusals. atrium refuses to start or run a command, and changes nothing, when:
- a key is configured but the file is plaintext;
- the file is encrypted but no key is configured;
- the key does not open the file;
- a key is configured but the binary has no SQLCipher engine (the
moderncbuild refuses any key).
atrium dbcommands.checkreports the database path and encryption state, runsPRAGMA integrity_check, and prints row counts of FTS5 and vec0 tables; it fails if the integrity check does not returnok.backup <dest>writes a consistent copy encrypted like the source, verifies it, and refuses a destination that already exists.rekeyre-encrypts an encrypted database under a new 64-hex-digit key read from--new-key-fileor standard input, never from the command line. It refuses when no key is configured, when the new key equals the current key, when another process holds the database (the atrium units and timers must be stopped), and when the database records an unknown newer schema. It writes and verifies a re-encrypted copy before replacing the file, so a failed rekey leaves the database unchanged, and it records the retired key’s id indb_key_history.shellruns SQL statements read from standard input against the database.
- Migrations. Pending schema migrations are applied under an exclusive lock when the web service or a command such as
atrium settingsoratrium secretsopens the database. atrium does not make a backup first. - Newer database, older binary. A binary refuses a database that records a migration it does not know, before writing anything. Going back to an older release therefore needs a database backup taken before the upgrade, and loses every write made since; changing the binary alone does not reverse a migration.
- What the database is not. The database holds records, stored settings and assistant history. Uploaded files under
files/beside it, the Notion wiki and the rebuildable message and media caches are separate, so the database alone is not a complete backup.