Skip to content

Configuration

Two files, and the split is strict.

.nouride/config.tomleverything structural. Safe to commit, print and render in a browser
.envcredentials and per-host values. 0600

No credentials in config.toml. Where one is needed, the setting names a secret, and the value is resolved from the environment or the secret store at the moment it is used:

api_key = "nougate" → $NOUGATE
→ $NOURIDE_SECRET_NOUGATE
→ data/secrets.json

Environment wins over the store, so a container platform can always pin a value. The store is what the dashboard writes to, which is why adding a bot token does not need a redeploy.

Every value in config.example.toml matches a built-in default, so a missing file boots the same daemon. Delete anything you are not changing.

Terminal window
nouride config check [file] # would this configuration start? Nothing is launched
nouride config schema [section] # every valid key, as JSON Schema
nouride config explain <path> # e.g. security.web.search_endpoint

An annotated copy ships beside the binary as config.example.toml, and it is the fullest reference there is — this page is the map.


[daemon]

name = "nouride"
log_level = "info" # debug | info | warn | error
log_pretty = false # human-readable console output — dev only
data_dir = "./.nouride/data"
agents_dir = "./.nouride/agents"
workspace_dir = "./.nouride/workspace"
shutdown_timeout_ms = 30000
timezone = "UTC"
backup_keep = 7
# default_agent = "nouva"

Three paths rather than one root, because the three hold different kinds of thing: daemon state, identity worth committing, and output that can grow large. workspace_dir on another disk is a supported arrangement.

backup_keep is dated copies of the persona packs under data/backups/agents/. MEMORY.md is the only thing in an install that is not in git and cannot be rebuilt.

default_agent answers when a message mentions nobody, arrives on a gateway with no owner, and matches no channel binding.

[agents]

mentions_enabled = true
mention_max_hops = 5
job_wake_max_chain = 100
groups_require_mention = false
group_context_messages = 20

What each of these does in practice →

[providers.<name>]

[providers.nougate-anthropic]
kind = "anthropic" # the wire format, not the vendor
base_url = "https://nougate.nouverse.tech/anthropic/v1"
api_key = "nougate" # a secret NAME
# headers = { "HTTP-Referer" = "https://nouverse.tech" }

No profiles exist by default beyond anthropic and openai, which point at the real APIs and name the conventional environment variables — so a minimal setup defines no profiles at all. Provider profiles →

[gateways.<platform>.<name>]

[gateways.telegram.nouva]
enabled = true
token = "telegram-nouva" # a secret NAME
agent = "nouva"
mode = "polling" # webhook is not implemented
[gateways.discord.main]
enabled = true
token = "discord-main"
[gateways.discord.main.cache]
messages = 0 # per channel
users = 0 # global
members = 0 # per guild
[gateways.whatsapp.main]
enabled = false
encryption_key = "wa-encryption-key"
session_dir = "./data/whatsapp"
bridge_transport = "stdio" # stdio | ws | inproc

None are defined by default, so a fresh install brings up the dashboard and nothing else rather than retry-looping against a platform it has no token for. Gateways →

[security]

approvals = "risky" # normal | risky | none
tool_timeout_ms = 30000
tool_max_output_bytes = 10485760
approval_timeout_ms = 300000 # unanswered requests expire as DENIALS
max_conversation_turns = 200
sender_allowlist = []
credentials_dir = "" # empty = <data_dir>/credentials, created 0700

credentials_dir is deliberately not a workspace root: file tools cannot reach it, only a command can, through $NOURIDE_CREDENTIALS_DIR. The daemon refuses to start if it lands inside any readable root.

[security.access]

mode = "pair" # pair | allowlist | open
allowlist = []
code_ttl_ms = 900000
notify_cooldown_ms = 300000

Who may talk to it →

[security.exec]

mode = "full" # deny | allowlist | full
ask = "on-miss" # off | on-miss | always
ask_fallback = "deny" # deny | allowlist — when nobody can be reached
allowlist = ["ls", "pwd", "cat", ""]
blocked = []
max_memory_mb = 0 # 0 = unlimited. Linux only, via prlimit
max_cpu_seconds = 0

Approvals & permissions →

[security.web]

allow_private_urls = false
search_kind = "brave" # brave | searxng | duckduckgo
search_api_key = "brave_search_api_key"
search_endpoint = ""
fallback_enabled = true
fallback_backends = ["duckduckgo"]

[security.rate_limit]

messages_per_minute = 10 # per sender, per gateway
cooldown_reply = "Please slow down."

[media]

image_model = "" # empty = the agent's own model
read_images = true
max_image_bytes = 8388608

Documents are not sent to a model at all. They land in the agent’s workspace and it opens them with read or a command, so a PDF costs nothing until the agent looks.

[alerts]

gateway = "" # e.g. "telegram:nouva"
chat_id = ""

Where the daemon reports on itself — a gateway that gave up reconnecting, a session that needs relinking. Boot warns when chat gateways are enabled and this is empty.

[dashboard] and [control]

[dashboard]
enabled = true
host = "127.0.0.1" # $HOST overrides. The container image sets 0.0.0.0
port = 18254 # $PORT overrides
public_url = "" # empty rather than a guess
[control]
enabled = true
chat = true # registers the in-process gateway `nouride chat` uses

[llm.defaults], [llm.limits], [llm.streaming], [llm.context]

[llm.defaults]
provider = ""
model = ""
max_tokens = 8192
temperature = 0.7
[llm.limits]
max_tokens_per_turn = 16384
max_tool_iterations = 25
max_turn_duration_ms = 300000
max_daily_cost_usd = 0 # 0 = off
cost_alert_threshold_pct = 80
[llm.streaming]
enabled = true
typing_indicator = true
progress_in_chat = true
[llm.context]
max_context_ratio = 0.8
truncation_strategy = "summary" # summary | sliding_window
preserve_recent_turns = 4
compact_at_ratio = 0.8
compact_at_tokens = 120000
preserve_recent_tool_results = 3
# summary_model = "claude-haiku-4-5-20251001"

What each does, and why →

[queue]

max_concurrent_per_agent = 1 # per chat. The loader refuses anything higher
max_queue_depth = 20
max_concurrent_turns = 8 # across every conversation. 0 lifts the ceiling
queue_timeout_ms = 300000
dedup_window_ms = 1000

[attachments] and [retention]

[attachments]
max_size_mb = 50
[retention]
session_days = 30 # the conversation AND the files that belong to it
audit_days = 30
job_log_days = 14
tool_output_days = 2
scratchpad_days = 3

How long a file is kept is not in [attachments]. It is session_days, the same window as the transcript that mentions it — a file is part of what was said. It used to have its own key, seven days against a thirty-day transcript, and the result was a conversation that mentioned a screenshot for another twenty-three days after the screenshot was deleted.

0 on any of these keeps everything: the off position, not “delete all”.

[session_memory]

active_dir = "./.nouride/memories"
messages = 2000
retention_days = 3
[session_memory.reset]
mode = "none" # none | daily | idle

What this is for →

[cron]

enabled = true
tick_interval_ms = 1000
min_interval_ms = 60000
max_jobs = 100
run_retention_days = 14
catchup_window_ms = 3600000
fail_streak_limit = 10
unattended_approval = "auto" # auto | deny
command_timeout_ms = 600000

Scheduled work →

[health]

heartbeat_interval_ms = 60000
disk_warn_threshold_mb = 1024
wal_warn_threshold_mb = 100

[skills] and [rsi]

[skills]
off = [] # names nobody loads, whatever their own skills.toml says
[rsi]
mode = "require_approval" # off | require_approval | always_allow
min_tool_failures = 3
reflect_after = 8
rapid_follow_up_ms = 180000
model = ""
cooldown_ms = 60000
max_pending = 20
notify_chat = true

Anything unrecognised in rsi.mode is read as require_approval, never as always_allow: a bad value should cost an approval, not grant autonomy. Skills & learning →

[nougate]

in_process = false
port = 18256

in_process = true needs a binary built with --bundle-nougate. Nougate →


Environment variables

Only these override the config file. Everything else structural belongs in config.toml.

HOSTdashboard bind address. Required as 0.0.0.0 in a container
PORTdashboard port
LOG_LEVELdebug | info | warn | error
TZIANA timezone for the agent’s runtime context
NOURIDE_DATA_DIRoverrides [daemon] data_dir
NOURIDE_AGENTS_DIRoverrides [daemon] agents_dir
NOURIDE_CONFIGwhere to find the config file
INITIAL_ADMIN_USERNAME / INITIAL_ADMIN_PASSWORDthe bootstrap account, read only while the user table is empty. Both, or neither
SESSION_SECRETgenerated into data/.session-secret if unset
NOURIDE_URL / NOURIDE_TOKENpoint the CLI at a remote daemon
NOURIDE_TYPECHECK_JOBSdevelopment only — cap the typecheck’s parallelism
NOURIDE_WA_LOG_LEVELturn the WhatsApp bridge’s logging up

Everything else in .env is a credential, named by your own config:

Terminal window
# [providers.nougate-anthropic] api_key = "nougate" →
NOUGATE=
# [gateways.telegram.nouva] token = "telegram-nouva" →
TELEGRAM_NOUVA=
# The two built-in profiles name these:
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
# 64 hex characters — openssl rand -hex 32
WA_ENCRYPTION_KEY=
# Without this, web_search is not offered at all, rather than offered and always failing.
BRAVE_SEARCH_API_KEY=

There is no fixed list. Every variable exists because something in your config.toml names it — delete the ones your config does not reference.

If a name is too generic for your environment, NOURIDE_SECRET_<NAME> is read as well.

Per-agent config

.nouride/agents/<id>/config.toml and skills.toml. Agents & persona packs →