# Configuration All runtime configuration for niri lives in two TOML files passed on the command line: ``` coral --config /path/to/config.toml --secrets /path/to/secrets.toml.env ``` Both flags are optional. When omitted, the values below fall back to their defaults. Keys in the secrets file are deep-merged on top of the config file, so a key declared in both resolves to the secrets value. > **Why two files?** Secrets are kept out of the main config (and the process > environment) so spawned subprocesses — including the agent's shell tool — > never inherit credentials. `secrets.toml.env` should be permission-restricted > and excluded from version control. The systemd module (`flake.nix`) exposes matching options: ```nix services.coral = { enable = true; configFile = "/etc/coral/config.toml"; secretsFile = "/etc/coral/secrets.toml.env"; }; ``` Both are nullable; when set, they're appended to the service's `ExecStart`. --- ## `[server]` | Key | Default | Description | |---|---|---| | `port` | `3000` | HTTP port the server listens on. | ## `[agent]` | Key | Default | Description | |---|---|---| | `name` | `"niri"` | Display name injected into the summarizer prompt and system grounding. | | `env` | `"default"` | Runtime tag. Set to `"local"` to skip the primary model entirely and run on the fallback only. | --- ## `[model]` — primary Used for normal agent turns. | Key | Default | Description | |---|---|---| | `provider` | *(required)* | Provider name in the pi registry — e.g. `"openrouter"`, `"openai"`, `"deepseek"`. Resolves base URL and reasoning flags. Required unless `agent.env = "local"`. | | `name` | *(required)* | Model id — e.g. `"deepseek/deepseek-r1"`, `"claude-sonnet-4-5"`. Required unless `agent.env = "local"`. | | `base_url` | *(from registry)* | OpenAI-compatible base URL override for unlisted/self-hosted endpoints. | | `tool_choice` | `"required"` | `"required"`, `"auto"`, or `"none"`. | | `enable_thinking` | `true` | Extended reasoning on models that support it. | | `thinking_effort` | `"low"` | Reasoning budget: `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`. | | `api_key` *(secret)* | *(required)* | API key for the primary endpoint. Place this in `secrets.toml.env`. | ## `[fallback]` Used when the primary endpoint is unreachable or `agent.env = "local"`. Defaults target a small, cheap model suitable for local LM Studio / Ollama. | Key | Default | Description | |---|---|---| | `provider` | *(inferred)* | Provider for the fallback model. Inferred from `base_url` if omitted (`localhost` → `lm-studio`). | | `model` | `"zai-org/glm-4.7-flash"` | Fallback model id. | | `base_url` | *(from registry)* | Base URL for the fallback model. | | `tool_choice` | `"required"` | `"required"`, `"auto"`, or `"none"`. | | `n_ctx` | `4096` | Declared context window size used for context-limit enforcement. | | `context_margin` | `256` | Token headroom reserved before a soft nudge is issued. | | `hard_overflow_tokens` | `1024` | Tokens above the window that trigger a hard prompt truncation. | | `enforce_context_limit` | `true` if base URL is localhost | Whether to enforce the context window limit. | | `referer` | — | `HTTP-Referer` header on fallback requests (OpenRouter attribution). | | `title` | — | `X-Title` header on fallback requests. | | `api_key` *(secret)* | — | API key override for the fallback endpoint. | ## `[summary]` Optional dedicated summarizer model. Falls back to the primary if not configured. | Key | Default | Description | |---|---|---| | `provider` | *(inferred)* | Provider for the summary model. | | `model` | *(uses primary)* | Model id. Leave empty to reuse the primary. | | `base_url` | *(from registry)* | Base URL override. | | `referer` | — | `HTTP-Referer` header on summary requests. | | `title` | — | `X-Title` header on summary requests. | | `api_key` *(secret)* | — | API key override for the summary endpoint. | --- ## `[embeddings]` Embeddings power semantic memory search. Set `api_key` to enable; without it the embedding client is disabled and memory search falls back to keyword matching. | Key | Default | Description | |---|---|---| | `model` | `"google/gemini-embedding-2-preview"` | Embedding model id. | | `dimensions` | `3072` | Output vector dimensions. Must match the database schema; changing it requires rebuilding `memory_chunk_vec`, `memory_prototype_vec`, and embedding meta. | | `base_url` | `"https://openrouter.ai/api/v1"` | OpenAI-compatible base URL. | | `referer` | — | `HTTP-Referer` header on embedding requests. | | `title` | — | `X-Title` header on embedding requests. | | `api_key` *(secret)* | — | API key for the embedding endpoint. | --- ## `[context]` Token / compaction thresholds. | Key | Default | Description | |---|---|---| | `token_nudge_threshold` | `120000` | Tokens at which the runner hints to the primary model to wrap up before compaction. | | `fallback_token_nudge_threshold` | `50000` | Same nudge, but for the fallback model's smaller window. | | `compact_target_tokens` | `65000` | Target post-compaction size. | | `compact_trigger_tokens` | `90000` | Tokens that trigger automatic compaction. | | `compact_recent_messages` | `80` | Recent messages retained verbatim during compaction. | | `compact_chunk_messages` | `32` | Messages summarized per compaction chunk. | | `compact_summary_max_chars` | `16000` | Hard cap on generated summary length. | | `idle_compaction_minutes` | `45` | Idle time before background compaction runs. Decimals supported (e.g. `2.5` for 2m30s). Set to `0` to disable. | ## `[runner]` | Key | Default | Description | |---|---|---| | `max_turns` | `120` | Hard cap on LLM turns per wakeup before the runner forces a rest. | | `max_identical_tool_turns` | `10` | Maximum consecutive turns with the same tool-call signature before the runner bails to break a loop. | --- ## `[container]` and `[ssh]` Three execution backends are available: local shell (default), Docker, and SSH. The two override sections are mutually exclusive — setting fields in both throws at startup. ### `[container]` (Docker) | Key | Default | Description | |---|---|---| | `name` | — | Docker container name. **Both** `name` and `user` must be set to activate Docker mode. Commands run as `docker exec -i -u -w bash -c `. | | `user` | — | Linux username inside the container. Must be set alongside `name`. | The harness home (soul, memories, DB) is anchored to the repo-local `home/` directory, which should be bind-mounted into the container. ### `[ssh]` | Key | Default | Description | |---|---|---| | `target` | — | SSH destination in `user@host` form. Setting this activates SSH mode: shell commands run on the remote host via `ssh bash -c `. | | `identity` | — | Path to the SSH private key passed as `ssh -i `. Omit to use the agent's default key or `~/.ssh/config`. | ## `[image_tool]` | Key | Default | Description | |---|---|---| | `max_bytes` | `150000` | Max image size in bytes accepted by `image_tool`. | | `root` | `home/images` (local/SSH) or `/home//images` (Docker) | Absolute path bounding all `image_tool` operations. Must start with `/`. | --- ## `[discord]` ### Credentials & identity | Key | Default | Description | |---|---|---| | `bot_token` *(secret)* | — | Discord bot token. **Required** for any Discord functionality. Without it the gateway and REST client are both disabled. | | `bot_user_id` | *(auto-detected)* | Bot user id. Auto-populated from the gateway READY payload on first connect. | | `scan_channel_ids` | `[]` | Channel ids the bot treats as its inbox. DMs are always included regardless. Example: `["1234567890", "0987654321"]`. | ### Gateway (WebSocket listener) | Key | Default | Description | |---|---|---| | `gateway_enabled` | `true` | Disable the WebSocket gateway entirely. Useful when only REST polling is wanted. | | `gateway_trace` | `false` | Verbose per-message trace logging (noisy; for debugging). | | `gateway_raw_fallback` | `true` | Use the raw `MESSAGE_CREATE` packet handler as a DM fallback for messages discord.js doesn't surface. | | `gateway_raw_fallback_all` | `false` | Extend the raw fallback to all channel types, not just DMs. | | `wake_on_event` | `false` | Wake the runner on every ingested guild channel message (not just DMs). DMs always wake. | | `wake_on_dm` | `true` | Wake immediately on DMs even when `wake_on_event` is false. | ### Batch digest (REST polling) | Key | Default | Description | |---|---|---| | `batch_interval_ms` | `60000` | How often (ms) the server polls Discord for new messages. Minimum 1 000 ms. | | `batch_max_messages` | `40` | Max messages fetched per channel per poll. Capped at 200. | | `batch_scan` | `true` | Disable REST batch scanning while keeping the gateway running. | | `batch_only_configured` | `true` | When `true`, batch digests only include DMs and channels in `scan_channel_ids`. | | `pending_auto_seen_minutes` | `10` | Age in minutes after which a pending Discord item is demoted to "seen" without a response. | ### REST client | Key | Default | Description | |---|---|---| | `rest_max_attempts` | `3` | Max retry attempts on transient REST failures (rate limits, 502/503/504). Capped at 10. | | `rest_retry_base_ms` | `1000` | Base delay for exponential backoff. Min 100 ms, max 30 000 ms. | --- ## `[metrics]` | Key | Default | Description | |---|---|---| | `retention_days` | `3` | Days to retain metrics rows before they are pruned. |