Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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.

KeyTypeDefaultClassEnvironment variable
action_link_keystringnonesecretATRIUM_ACTION_LINKS_ACTION_LINK_KEY

anthropic

KeyTypeDefaultClassEnvironment variable
anthropic_admin_keystringnonesecretATRIUM_ANTHROPIC_ANTHROPIC_ADMIN_KEY

assistant

KeyTypeDefaultClassEnvironment variable
budget_unattended_sharefloat0.8advancedATRIUM_ASSISTANT_BUDGET_UNATTENDED_SHARE
cache_ttl_chatenum (5m, 1h)5muiATRIUM_ASSISTANT_CACHE_TTL_CHAT
max_run_cost_usdfloat2capATRIUM_ASSISTANT_MAX_RUN_COST_USD
max_stepsint24advancedATRIUM_ASSISTANT_MAX_STEPS
monthly_budget_usdfloat75uiATRIUM_ASSISTANT_MONTHLY_BUDGET_USD

core

KeyTypeDefaultClassEnvironment variable
http_timeout_secondsint600advancedATRIUM_CORE_HTTP_TIMEOUT_SECONDS
public_urlstringnoneuiATRIUM_CORE_PUBLIC_URL
tzstringAmerica/Port_of_SpainuiATRIUM_CORE_TZ

db

KeyTypeDefaultClassEnvironment variable
keystringnonesecretATRIUM_DB_KEY

openai

KeyTypeDefaultClassEnvironment variable
openai_admin_keystringnonesecretATRIUM_OPENAI_OPENAI_ADMIN_KEY

web

KeyTypeDefaultClassEnvironment variable
activity_page_sizeint50advancedATRIUM_WEB_ACTIVITY_PAGE_SIZE
activity_snippet_charsint800advancedATRIUM_WEB_ACTIVITY_SNIPPET_CHARS
body_cap_bytesint1048576advancedATRIUM_WEB_BODY_CAP_BYTES
chat_thread_limitint50advancedATRIUM_WEB_CHAT_THREAD_LIMIT
job_historyint10advancedATRIUM_WEB_JOB_HISTORY
learned_limitint50advancedATRIUM_WEB_LEARNED_LIMIT
loop_limitint100advancedATRIUM_WEB_LOOP_LIMIT
page_sizeint50advancedATRIUM_WEB_PAGE_SIZE
preview_bytesint200000advancedATRIUM_WEB_PREVIEW_BYTES
project_thread_limitint100advancedATRIUM_WEB_PROJECT_THREAD_LIMIT
run_historyint30advancedATRIUM_WEB_RUN_HISTORY
snooze_choicesstring1,3,7,14advancedATRIUM_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.

VariableSetting
ATRIUM_ACTION_LINKS_ACTION_LINK_KEYaction_links.action_link_key
ATRIUM_ANTHROPIC_ANTHROPIC_ADMIN_KEYanthropic.anthropic_admin_key
ATRIUM_ASSISTANT_BUDGET_UNATTENDED_SHAREassistant.budget_unattended_share
ATRIUM_ASSISTANT_CACHE_TTL_CHATassistant.cache_ttl_chat
ATRIUM_ASSISTANT_MAX_RUN_COST_USDassistant.max_run_cost_usd
ATRIUM_ASSISTANT_MAX_STEPSassistant.max_steps
ATRIUM_ASSISTANT_MONTHLY_BUDGET_USDassistant.monthly_budget_usd
ATRIUM_CORE_HTTP_TIMEOUT_SECONDScore.http_timeout_seconds
ATRIUM_CORE_PUBLIC_URLcore.public_url
ATRIUM_CORE_TZcore.tz
ATRIUM_DB_KEYdb.key
ATRIUM_OPENAI_OPENAI_ADMIN_KEYopenai.openai_admin_key
ATRIUM_WEB_ACTIVITY_PAGE_SIZEweb.activity_page_size
ATRIUM_WEB_ACTIVITY_SNIPPET_CHARSweb.activity_snippet_chars
ATRIUM_WEB_BODY_CAP_BYTESweb.body_cap_bytes
ATRIUM_WEB_CHAT_THREAD_LIMITweb.chat_thread_limit
ATRIUM_WEB_JOB_HISTORYweb.job_history
ATRIUM_WEB_LEARNED_LIMITweb.learned_limit
ATRIUM_WEB_LOOP_LIMITweb.loop_limit
ATRIUM_WEB_PAGE_SIZEweb.page_size
ATRIUM_WEB_PREVIEW_BYTESweb.preview_bytes
ATRIUM_WEB_PROJECT_THREAD_LIMITweb.project_thread_limit
ATRIUM_WEB_RUN_HISTORYweb.run_history
ATRIUM_WEB_SNOOZE_CHOICESweb.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.

OptionTypeDefaultMeaning
enableboolfalseEnables the module.
packagepackagethe flake’s static Go build for the host platformThe atrium application used by every unit and by atrium-run.
idint880uid 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.
environmentFileslist of string[]EnvironmentFile= paths read by every unit and by atrium-run. They hold credentials and any pinned settings.
extraEnvironmentattribute 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.
modelServerUnitslist of string[]Units that serve chatUrl and embedUrl. The application units start after them and want them.
timeZonenull or stringconfig.time.timeZoneOperator time zone.

Endpoints

Each option is null by default. A null option sets no environment variable for the units.

OptionMeaning
chatUrlOpenAI-compatible chat completions base URL used for mail classification.
embedUrlOpenAI-compatible embeddings base URL.
publicUrlURL the operator reaches the web UI at; used in notifications and signed media links.
grafanaUrlGrafana base URL for the observability tools.
icloudAppleIdApple 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

OptionTypeDefaultMeaning
web.hoststring"0.0.0.0"Address the public listener binds.
web.portport8480Port 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.allowedHostnull or stringnullThe one Host header the public listener accepts. The configuration fails to evaluate with web.auth = "proxy" while it is null.
web.local.enablebooltrueRuns a second listener bound to 127.0.0.1 with --auth local, defined as atrium-web-local.
web.local.portport8481Port of the loopback listener.

Notifications

OptionTypeDefaultMeaning
ntfy.urlnull or stringnullntfy server base URL.
ntfy.topicnull or stringnullTopic for the assistant’s notifications.
ntfy.mediaTopicnull or stringnullTopic for media release notifications.

Media

OptionTypeDefaultMeaning
media.enableboolfalseEnables the Media section: filing, EZTV channels and the library. Adds ffmpeg-headless to the path of the web and scheduler units.
media.groupstring"media"Group that owns the media tree. The web and scheduler units write through it with umask 0002.
media.rootstring"/var/lib/media"Media tree containing downloads/, import/, movies/ and tv/.
media.qbittorrentUrlnull or stringnullqBittorrent Web UI URL, reachable without login.
media.moviesUrlnull or stringnullURL serving root/movies read-only on the private network.
media.tvUrlnull or stringnullURL 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 serve binds 127.0.0.1 unless 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 /healthz must carry an accepted Host header; any other Host receives 400.
  • In proxy mode the request must carry a Remote-Groups header that lists atrium_access, otherwise it receives 403. 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.1 and [::1].
  • The NixOS module runs the public listener in proxy mode and, by default, a second listener bound to 127.0.0.1 in local mode.
  • POST, PUT, PATCH and DELETE requests need the CSRF token that atrium issues in a cookie and a request whose Origin, 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 from media-link.key beside the database, which is created on first use.

Mail safety

Every mailbox write goes through one safety gate.

  • Modes. The mail mode (mail_mode) is shadow (the default) or live; any other value is refused. It is a deploy-time setting of class bootstrap: only an environment variable or the configuration file sets it, and atrium settings set cannot change it. In shadow, atrium records what it would do and makes no automatic mailbox writes: the automatic pass returns without acting.
  • Automatic writes happen only in live mode and only while atrium is not paused (a pause file 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 shadow mode.
  • 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 shadow mode 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 \Deleted and 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 set and unset refuse 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_KEY and ATRIUM_OPENAI_OPENAI_ADMIN_KEY for the keys anthropic_admin_key and openai_admin_key. For the database key (module db, key key) the variable is ATRIUM_DB_KEY. The environment variable list is authoritative. A secret setting’s environment variable also has a _FILE form naming a file whose trimmed contents are the value, for example ATRIUM_DB_KEY_FILE; setting both forms is refused. In the configuration file a secret accepts only its <key>_file form.
  • Settings CLI. atrium settings list, get, set and unset show 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), delete removes it, and status reports each secret as set, unset or pinned. 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 set unless the database is encrypted.
  • Audit log. Every settings write and every secret change appends a row to the append-only settings_audit table with the key, the actor, the source (cli, ui or migration), and the old and new values. For a secret the row records only changed. 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_writes snapshots.

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_file in the configuration file, by ATRIUM_DB_KEY or ATRIUM_DB_KEY_FILE, or by the db_key systemd 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 modernc build refuses any key).
  • atrium db commands.
    • check reports the database path and encryption state, runs PRAGMA integrity_check, and prints row counts of FTS5 and vec0 tables; it fails if the integrity check does not return ok.
    • backup <dest> writes a consistent copy encrypted like the source, verifies it, and refuses a destination that already exists.
    • rekey re-encrypts an encrypted database under a new 64-hex-digit key read from --new-key-file or 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 in db_key_history.
    • shell runs 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 settings or atrium secrets opens 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.