All configuration is loaded from .env via pydantic-settings (navi/config.py). The global settings object is imported everywhere as from navi.config import settings.
| Variable | Type | Default | Description |
|---|---|---|---|
NAVI_DEFAULT_PROFILE_ID |
str | "" |
Default profile used when a client creates a session without specifying a profile_id. Empty string means "no default". |
| Variable | Type | Default | Description | |
|---|---|---|---|---|
LLM_CONFIG_FILE |
str | llm.json |
Path to the routing config (see LLM routing). Empty string disables it. | |
OLLAMA_HOST |
str | http://localhost:11434 |
Ollama server URL (used when LLM_CONFIG_FILE is absent) |
|
OLLAMA_API_KEY |
str | "" |
Ollama Cloud API key (used when LLM_CONFIG_FILE is absent) |
|
OLLAMA_DEFAULT_MODEL |
str | gemma4:31b-cloud |
Default model (can be overridden per profile) | |
OLLAMA_NUM_CTX |
int | 65536 |
Context window size in tokens (a route entry's context_window wins) |
|
OLLAMA_THINK |
bool | true |
Enable extended reasoning (thinking) | |
OLLAMA_REQUEST_TIMEOUT |
int | 30 |
Seconds before Ollama request times out (affects fallback speed) | |
EMBEDDING_OLLAMA_HOST |
str | "" |
Ollama server for embedding model (falls back to OLLAMA_HOST if empty) |
|
EMBEDDING_OLLAMA_API_KEY |
str | "" |
API key for embedding Ollama server | |
EMBEDDING_MODEL |
str | nomic-embed-text:latest |
Embedding model for memory vector search | |
EMBEDDING_ROUTE |
str | embedding |
llm.json route the embedding calls walk. Empty means "pass the model name only". |
|
EMBEDDING_DIMENSIONS |
int | 768 |
Vector dimensionality for embeddings | |
OPENAI_API_KEY |
str | "" |
OpenAI API key (if using OpenAI backend) | |
OPENAI_MODEL |
str | "gpt-4" |
Default model for OpenAI backend | |
OPENAI_BASE_URL |
str \ | None | None |
Custom base URL for OpenAI-compatible endpoints (e.g. vLLM, LM Studio) |
ANTHROPIC_API_KEY |
str | "" |
Reserved — no Anthropic backend implemented yet |
Without llm.json (a bare dev checkout), Navi talks to one host built from OLLAMA_HOST/OLLAMA_API_KEY and the per-profile model list, exactly as before.
llm.json)llm.json (repo root, committed — it holds no secrets) declares the hosts and the ordered chains of models to try on them. Backups are a property of the file, not of the profile: when the cloud is unreachable or out of quota, the next entry answers and a local model keeps Navi usable.
{
"targets": {
"cloud": { "host": "https://ollama.com", "api_key_env": "OLLAMA_API_KEY" },
"home": { "host": "http://192.168.1.129:11434" }
},
"routes": {
"default": [
{ "model": "glm-5.3-flash:cloud", "target": "cloud", "context_window": 131072 },
{ "model": "qwen3.6:35b-128k", "target": "home", "context_window": 131072,
"options": { "presence_penalty": 0.0 } }
],
"embedding": [
{ "model": "nomic-embed-text:latest", "target": "home" },
{ "model": "nomic-embed-text:latest", "target": "cloud" }
]
},
"probe": { "interval_sec": 60, "timeout_sec": 5 }
}
targets — named hosts. api_key_env names an environment variable and is read from os.environ or from the .env file itself (the process environment does not receive it), so the deploy branch's .env does not change. Any name works — that is how a second Ollama Cloud account joins a route: give it its own target and its own variable, e.g. cloud2 with "api_key_env": "OLLAMA_API_KEY_2", then list both in the chain. A literal api_key is also accepted, but llm.json is committed: keep secrets in .env and reference them by name.routes — ordered chains; the first entry is the primary, the rest are backups. context_window caps num_ctx for that model and drives the compressor's window, and options are merged over the global Ollama options. When every entry of a route serves the same budget, give them the same context_window: the compressor counts against whichever entry is active, so an uneven window silently changes how much history a failover can hold.probe — how often the background prober re-checks a degraded target.A profile joins a route with "llm_route": "default" (see profiles.md). No llm_route means the profile's model list is used, each name resolved to the target the config says serves it.
| Failure | Classification | Engine behaviour |
|---|---|---|
| connection refused / timeout / first-chunk timeout | LLMConnectionError |
target down for one probe interval, next entry |
| HTTP 429, or a "rate limit"/"quota" message | LLMQuotaExceededError |
target cooling, at least the Retry-After and one interval |
| HTTP 401/403 | LLMAuthError |
target auth, re-checked every 300 s (the key may be rotated) |
| HTTP 5xx | LLMServerUnavailableError |
target cooling for one interval |
| HTTP 404 / model not found | LLMModelNotFoundError |
only that (target, model) pair is marked missing |
| anything else (a malformed request) | LLMBackendError |
not retried — every entry would say no |
LLM_COMPLETE_RETRIES applies only to the last entry left: a dead cloud model must not cost retries × LLM_STREAM_FIRST_CHUNK_TIMEOUT on every call while a healthy local model sits one step down the chain.
For streaming calls the first chunk is awaited before anything is yielded, so a failure is caught before output reaches the client. A stream that breaks after the first chunk is not retried — the turn fails rather than mixing two partial answers from two hosts.
Switch-back never happens mid-turn. RouteHealth probes only degraded targets (once per probe.interval_sec, with probe.timeout_sec), using list() for reachability/auth, show() for a missing model, and a one-token chat for a quota cooldown. When the probe reports the primary healthy again, the next user turn starts on it; the running turn keeps the host it began on.
/health reports which entry serves each route and why a target is degraded — see api.md. POST /admin/ollama/clear-blacklists forgets all recorded failures (and restarts every route at its primary) after a config fix.
A missing llm.json (or LLM_CONFIG_FILE="") means "no routes" and the legacy single-host path. A malformed file raises LLMConfigError at startup, naming the file and the offending target or route: silently routing to the wrong host is worse than refusing to start.
Web search is not a Settings field: it is the navi-web MCP server. Its settings reach it through mcp_servers.d/navi-web.json, which maps SEARXNG_URL (and, optionally, BRAVE_SEARCH_API_KEY) from .env into the server's own environment. SEARXNG_URL is therefore a real .env setting — a self-hosted SearXNG, e.g. http://localhost:8888 — just not one Navi itself reads. Without it the server falls back to DuckDuckGo scraping. See mcp.md.
| Variable | Type | Default | Description |
|---|---|---|---|
LLM_COMPLETE_TIMEOUT |
int | 120 |
Seconds before a non-streaming complete() call times out |
PLANNING_LLM_TIMEOUT_SEC |
int | 240 |
Seconds before a non-streaming planning phase call times out (plan tool / subagent pipeline). Wider than LLM_COMPLETE_TIMEOUT — cloud models prefill big planning prompts for up to a couple of minutes, and a timeout here silently kills the plan. |
LLM_STREAM_FIRST_CHUNK_TIMEOUT |
int | 90 |
Seconds to wait for the first token of a streaming call (prefill phase) |
LLM_STREAM_CHUNK_TIMEOUT |
int | 60 |
Max seconds between consecutive tokens in a streaming call |
LLM_COMPLETE_RETRIES |
int | 2 |
Attempts on the last remaining route entry after a connection error. Entries with a healthy alternative are tried once, so failover stays fast. |
LLM_RETRY_BACKOFF_SEC |
float | 2 |
Base sleep between those attempts (multiplied by the attempt number) |
Large contexts can take 60–90 s to prefill; LLM_STREAM_FIRST_CHUNK_TIMEOUT=90 covers the common case for local Ollama. Increase (e.g. 180) for very large contexts or slow GPUs; reduce for fast cloud endpoints.
| Variable | Type | Default | Description |
|---|---|---|---|
FS_ALLOWED_PATHS |
str | "*" |
Comma-separated paths the filesystem tool can access. "*" = no restriction |
TERMINAL_ALLOWED_COMMANDS |
str | "*" |
Comma-separated allowed executables for terminal. "*" = allow all |
SSH_HOSTS_FILE |
str | ssh_hosts.json |
Path to JSON file with named SSH connections |
settings.fs_allowed_paths_list and settings.terminal_allowed_commands_list are computed properties that parse the comma-separated strings into lists.
| Variable | Type | Default | Description |
|---|---|---|---|
TERMINAL_USER_ALLOWED_COMMANDS |
str | long allowlist | Comma-separated allowed executables for non-admin users. Admin bypasses this restriction. |
settings.terminal_user_allowed_commands_list — computed property parsed from the comma-separated string.
| Variable | Type | Default | Description |
|---|---|---|---|
DATABASE_URL |
str | "" |
PostgreSQL URL (postgresql://user:pass@host:port/db). Required — Navi requires PostgreSQL 15+ with pgvector extension. |
| Variable | Type | Default | Description |
|---|---|---|---|
NAVI_WEBCLIENT_ENABLED |
bool | true |
Master switch for the web UI. false registers none of the web-facing routes (/, /assets, /images, /content-viewers, /content, /admin panel, /debug*) and skips the navi_ui MCP server — a pure API/WS server for terminal clients. The admin JSON API (/admin/*) stays, still gated by require_admin. Toggling requires a server restart. |
NAVI_HOST |
str | 127.0.0.1 |
Bind address for the navi-server launcher (uvicorn). 127.0.0.1 = local-only; remote clients connect via SSH tunnel or reverse proxy. |
NAVI_PORT |
int | 8099 |
Bind port for the navi-server launcher. |
Navi instances can form a swarm: each announces itself to a tiny registry service ("hive", see hive/README.md) running on the main server; agents ask the book who is alive and where. The registry is never a blocking dependency — with HIVE_URL empty, or with the hive down, everything else works and the agent is simply told the book is unreachable.
| Variable | Type | Default | Description |
|---|---|---|---|
HIVE_URL |
str | "" |
Hive registry address (e.g. http://192.168.1.168:8087). Empty = standalone navi, no announcements. |
ANNOUNCE_INTERVAL_SEC |
int | 120 |
How often this navi re-announces itself to the hive. |
SWARM_KEY_FILE |
str | .swarm-key |
Shared PSK file (gitignored, 0600, generated by deploy/install.sh). |
INSTANCE_FILE |
str | instance.json |
This navi's identity for the swarm: generated name (single female name from Japanese/American/Spanish pools) + instance id. Rename = edit the file, restart. |
PEER_ASK_PROFILE |
str | server_admin |
Which profile answers incoming /peer/ask questions (one-shot agent run). |
PEER_ASK_TIMEOUT_SEC |
int | 120 |
Hard ceiling for a single peer ask — the LLM turn inside /peer/ask. |
See deploy/README.md for the one-command server deployment (bash deploy/install.sh): dockerized PostgreSQL + systemd + navi-code in PATH, web UI and auth off by default.
| Variable | Type | Default | Description |
|---|---|---|---|
LOG_LEVEL |
str | INFO |
Python logging level (DEBUG, INFO, WARNING, ERROR) |
| Variable | Type | Default | Description |
|---|---|---|---|
TOOLS_DIR |
str | tools |
Directory for user-defined tools (auto-discovered at startup) |
CONTEXT_PROVIDERS_DIR |
str | context_providers |
Directory for user-defined context providers (auto-discovered at startup) |
user_key (BYOK)Per-server JSON config (mcp_servers.d/<name>.json) — where a user's personal credential is injected. Details in mcp.md.
// HTTP server: key goes into an Authorization-style header
{ "transport": "streamable_http", "url": "https://…",
"user_key": { "header": "Authorization", "prefix": "Bearer " } }
// stdio server: key goes into an env var
{ "transport": "stdio", "command": "uvx", "env": { "API_KEY": "default" },
"user_key": { "env": "API_KEY" } }
Keys are stored Fernet-encrypted (NAVI_AUTH_ENCRYPTION_KEY, see Authentication above). A user without a saved key is served the default credential from the config file only if their role is admin; every other role is refused, and the server is left out of their tool list entirely. See mcp.md.
| Variable | Type | Default | Description |
|---|---|---|---|
SESSION_FILES_DIR |
str | session_files |
Directory for uploaded session files |
SESSION_FILES_MAX_SIZE_MB |
int | 200 |
Max upload size per file in megabytes |
SHARE_FILE_MAX_SIZE_MB |
int | 1024 |
Max file size share_file may copy into session files, in megabytes |
SESSION_MESSAGES_WINDOW |
int | 1000 |
Max hot (non-archived) messages per session |
WS_REPLAY_BUFFER_SIZE |
int | 500 |
Max events retained per WebSocket turn for reconnect replay |
| Variable | Type | Default | Description |
|---|---|---|---|
PUBLIC_URL |
str | http://localhost:8099 |
Base URL used by share_file to build download links. Set this when behind a reverse proxy. |
| Variable | Type | Default | Description |
|---|---|---|---|
CONTEXT_COMPRESSION_ENABLED |
bool | true |
Enable/disable automatic context compression |
CONTEXT_COMPRESSION_THRESHOLD |
float | 0.90 |
Trigger compression at this fraction of OLLAMA_NUM_CTX |
CONTEXT_KEEP_RECENT |
int | 8 |
Number of recent conversation turns to keep verbatim |
CONTEXT_SUMMARY_TEMPERATURE |
float | 0.3 |
Temperature for the summarization LLM call |
CONTEXT_SUMMARY_MAX_TOKENS |
int | 6000 |
Max output tokens for the summary LLM call |
OUTPUT_RESERVE_TOKENS |
int | 2048 |
Headroom reserved for model response in context size checks |
CONTEXT_MESSAGE_TOKEN_BUDGET |
int | 0 |
Per-message token budget for the LLM context view. A single tool/assistant message whose estimated size exceeds this is head/tail-truncated in the built context only (stored history is never mutated) so one huge tool result cannot alone blow the window. 0 = auto (OLLAMA_NUM_CTX // 6). |
| Variable | Type | Default | Description |
|---|---|---|---|
GMAIL_ADDRESS |
str | "" |
Gmail address for the email_manager tool (IMAP/SMTP with App Password) |
GMAIL_APP_PASSWORD |
str | "" |
Gmail App Password (not the account password — generate at myaccount.google.com) |
| Variable | Type | Default | Description |
|---|---|---|---|
NAVI_AUTH_ENABLED |
bool | true |
Master auth switch. Set false to disable OAuth/API-token auth entirely. Every request is treated as the local anonymous admin user. Use only for trusted single-user/local deployments. |
GNAUTH_BASE_URL |
str | http://gnexus-auth.local |
gnexus-auth server base URL |
GNAUTH_CLIENT_ID |
str | "" |
OAuth client ID |
GNAUTH_CLIENT_SECRET |
str | "" |
OAuth client secret |
GNAUTH_REDIRECT_URI |
str | http://localhost:8099/auth/callback |
Must match redirect URI registered in gnexus-auth |
GNAUTH_ADMIN_ROLE_SLUG |
str | navi_admin |
Role slug that maps to Navi admin role |
GNAUTH_USER_ROLE_SLUG |
str | navi_user |
Role slug that maps to Navi user role |
GNAUTH_PROFILE_PATH |
str | /account/profile |
Path appended to gnauth_base_url for profile links |
NAVI_AUTH_ENCRYPTION_KEY |
str | "" |
Fernet key (base64, 32 bytes). Generate once with python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". Never change after first launch. |
NAVI_AUTH_COOKIE_NAME |
str | navi_auth_session |
Session cookie name |
NAVI_AUTH_COOKIE_SECURE |
bool | False |
Set True behind HTTPS |
NAVI_AUTH_COOKIE_SAMESITE |
str | lax |
SameSite policy |
NAVI_AUTH_COOKIE_MAX_AGE_DAYS |
int | 30 |
Cookie lifetime |
Full auth setup guide: docs/auth.md.
| Variable | Type | Default | Description |
|---|---|---|---|
NAVI_PUSH_VAPID_PUBLIC_KEY |
str | "" |
VAPID public key for web push. Empty = push fully disabled (endpoints 503, no notifications sent). |
NAVI_PUSH_VAPID_PRIVATE_KEY |
str | "" |
VAPID private key. Generate the pair once: pip install py_vapid && python -m py_vapid gen. |
NAVI_PUSH_VAPID_SUBJECT |
str | mailto:admin@navi.local |
Contact for the push service (a mailto: or https: URL). |
NAVI_PUSH_COOLDOWN_SEC |
int | 30 |
Minimum seconds between two pushes for the same session. |
Push notifications fire when an agent turn (or a scheduled recall) completes and no browser tab is watching that session. See docs/push.md.
| Variable | Type | Default | Description |
|---|---|---|---|
SYNAPSE_SOURCE_URL |
str | "" |
GNEXUS Synapse base URL for the outgoing source API. Empty = nothing is ever emitted. |
SYNAPSE_SOURCE_API_KEY |
str | "" |
syn_* source key registered in the Synapse admin panel (write it into .env yourself). |
SYNAPSE_SOURCE_NAME |
str | navi |
Source name on Synapse's side. |
Without both URL and key, Synapse-linked outgoing events are silently skipped and the UI disables Synapse-linked options (source_ready: false). See docs/synapse.md.
| Variable | Type | Default | Description |
|---|---|---|---|
NAVI_PERSONA |
str | "" |
Global personality prompt prepended to every profile's system prompt |
NAVI_PERSONA_FILE |
str | "" |
Path to a .txt file containing the persona (preferred over inline NAVI_PERSONA) |
Recommended: use NAVI_PERSONA_FILE=persona.txt rather than inlining the persona in .env, because multi-line values don't parse reliably in .env files.
The _load_persona_from_file validator reads the file on startup if NAVI_PERSONA is empty and NAVI_PERSONA_FILE is set.
| Variable | Type | Default | Description |
|---|---|---|---|
TASKS_MAX_PER_SESSION |
int | 5 |
Concurrent running background tasks per session |
TASKS_MAX_GLOBAL |
int | 20 |
Concurrent running background tasks server-wide |
TASKS_MAX_SPAWN |
int | 2 |
Concurrent background spawn_agent per session |
TASKS_TTL_SEC |
int | 3600 |
Finished-task retention before reaping |
TASKS_EVENT_BUFFER_SIZE |
int | 50 |
Per-task event ring (spawn tasks use 200) |
TASKS_RATE_LIMIT |
int | 10 |
Max task spawns per 5 min per session |
TASK_NOTES_MAX_PENDING |
int | 20 |
Pending completion notes per session |
TASK_NOTES_PER_TURN |
int | 5 |
Max notes coalesced into one turn injection |
MESSAGE_QUEUE_MAX |
int | 5 |
User messages queued while a run is active |
BACKGROUNDABLE_TOOLS |
str | terminal,ssh_exec,peer,spawn_agent,code_exec |
Tools accepting "background": true |
PARALLEL_TOOL_CALLS |
bool | false |
Execute a multi-tool-call batch concurrently (per-profile override: AgentProfile.parallel_tool_calls) |
See docs/tasks.md for the full mechanism.
When a terminal (run), code_exec or ssh_exec call is detached and the agent did not pass timeout, the executor lifts it to 300 s for the detached run only — the tools' short foreground defaults (20/30/60 s) would otherwise mark long commands "completed" with partial output while the process is still running.
.env# LLM — Ollama (primary) OLLAMA_HOST=http://localhost:11434 OLLAMA_API_KEY= OLLAMA_DEFAULT_MODEL=gemma4:31b-cloud OLLAMA_NUM_CTX=65536 OLLAMA_THINK=true OLLAMA_REQUEST_TIMEOUT=30 # LLM routing (hosts + backup chains). Committed; no secrets inside: # LLM_CONFIG_FILE=llm.json # LLM — OpenAI (optional) # OPENAI_API_KEY=sk-... # OPENAI_MODEL=gpt-4 # OPENAI_BASE_URL=https://api.openai.com/v1 # Database (PostgreSQL 15+ with pgvector) DATABASE_URL=postgresql://user:pass@localhost:5432/navidb # Security / sandboxing FS_ALLOWED_PATHS=* TERMINAL_ALLOWED_COMMANDS=* # Misc LOG_LEVEL=INFO TOOLS_DIR=tools SHARE_FILE_MAX_SIZE_MB=1024 SESSION_MESSAGES_WINDOW=1000 WS_REPLAY_BUFFER_SIZE=500 # Context compression CONTEXT_COMPRESSION_ENABLED=true CONTEXT_COMPRESSION_THRESHOLD=0.90 CONTEXT_KEEP_RECENT=8 # Persona NAVI_PERSONA_FILE=persona.txt NAVI_DEFAULT_PROFILE_ID=navi_code