| 1 | # Configuration |
| 2 | |
| 3 | Release config: `.config.local.json` |
| 4 | |
| 5 | > [!NOTE] |
| 6 | > Copy `.config.local.example.json` to `.config.local.json`, then pass it with `--config` when running |
| 7 | > the gateway. The public template enables only the WebUI WebSocket and Director callback channels. |
| 8 | |
| 9 | ## Environment Variables for Secrets |
| 10 | |
| 11 | Instead of storing secrets directly in `config.json`, you can use `${VAR_NAME}` references that are resolved from environment variables at startup: |
| 12 | |
| 13 | ```json |
| 14 | { |
| 15 | "providers": { |
| 16 | "custom": { "apiKey": "${NANOBOT_MODEL_API_KEY}" } |
| 17 | }, |
| 18 | "tools": { |
| 19 | "fileStorage": { |
| 20 | "outbound": { |
| 21 | "s3": { |
| 22 | "accessKeyId": "${FILE_STORAGE_ACCESS_KEY_ID}", |
| 23 | "secretAccessKey": "${FILE_STORAGE_SECRET_ACCESS_KEY}" |
| 24 | } |
| 25 | } |
| 26 | } |
| 27 | } |
| 28 | } |
| 29 | ``` |
| 30 | |
| 31 | For **systemd** deployments, use `EnvironmentFile=` in the service unit to load variables from a file that only the deploying user can read: |
| 32 | |
| 33 | ```ini |
| 34 | # /etc/systemd/system/nanobot.service (excerpt) |
| 35 | [Service] |
| 36 | EnvironmentFile=/home/youruser/nanobot_secrets.env |
| 37 | User=nanobot |
| 38 | ExecStart=... |
| 39 | ``` |
| 40 | |
| 41 | ```bash |
| 42 | # /home/youruser/nanobot_secrets.env (mode 600, owned by youruser) |
| 43 | NANOBOT_MODEL_API_KEY=your-key-here |
| 44 | FILE_STORAGE_ACCESS_KEY_ID=optional-storage-key |
| 45 | FILE_STORAGE_SECRET_ACCESS_KEY=optional-storage-secret |
| 46 | ``` |
| 47 | |
| 48 | ## Echo Director |
| 49 | |
| 50 | Keep generic agent settings under `agents.defaults` and Echo service connectivity under |
| 51 | `tools.echoGenerator`: |
| 52 | |
| 53 | ```json |
| 54 | { |
| 55 | "agents": { |
| 56 | "defaults": { |
| 57 | "maxConcurrentRequests": 3 |
| 58 | } |
| 59 | }, |
| 60 | "tools": { |
| 61 | "echoGenerator": { |
| 62 | "baseUrl": "http://127.0.0.1:8221", |
| 63 | "callbackBaseUrl": "http://127.0.0.1:18791" |
| 64 | } |
| 65 | } |
| 66 | } |
| 67 | ``` |
| 68 | |
| 69 | `maxConcurrentRequests` limits simultaneous agent turns; set it to `0` for no limit. |
| 70 | Generation and merge jobs require the Echo service and complete asynchronously through callbacks. |
| 71 | |
| 72 | ## Providers |
| 73 | |
| 74 | > [!TIP] |
| 75 | > - **MiniMax (Mainland China)**: If your API key is from MiniMax's mainland China platform (minimaxi.com), set `"apiBase": "https://api.minimaxi.com/v1"` in your minimax provider config. |
| 76 | > - **MiniMax thinking mode**: Use `providers.minimaxAnthropic` when you want `reasoningEffort` / thinking mode. MiniMax exposes that capability through its Anthropic-compatible endpoint, so nanobot keeps it as a separate provider instead of guessing MiniMax-specific thinking parameters on the generic OpenAI-compatible `minimax` endpoint. It uses the same `MINIMAX_API_KEY`. Default Anthropic-compatible base URL: `https://api.minimax.io/anthropic`; for mainland China use `https://api.minimaxi.com/anthropic`. |
| 77 | > - **VolcEngine / BytePlus Coding Plan**: Use dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan` instead of the pay-per-use `volcengine` / `byteplus` providers. |
| 78 | > - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config. |
| 79 | > - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config. |
| 80 | > - **Step Fun (Mainland China)**: If your API key is from Step Fun's mainland China platform (stepfun.com), set `"apiBase": "https://api.stepfun.com/v1"` in your stepfun provider config. |
| 81 | |
| 82 | | Provider | Purpose | Get API Key | |
| 83 | |----------|---------|-------------| |
| 84 | | `custom` | Any OpenAI-compatible endpoint | — | |
| 85 | | `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) | |
| 86 | | `volcengine` | LLM (VolcEngine, pay-per-use) | [Coding Plan](https://www.volcengine.com/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) · [volcengine.com](https://www.volcengine.com) | |
| 87 | | `byteplus` | LLM (VolcEngine international, pay-per-use) | [Coding Plan](https://www.byteplus.com/en/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) · [byteplus.com](https://www.byteplus.com) | |
| 88 | | `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) | |
| 89 | | `azure_openai` | LLM (Azure OpenAI) | [portal.azure.com](https://portal.azure.com) | |
| 90 | | `openai` | LLM + Voice transcription (Whisper) | [platform.openai.com](https://platform.openai.com) | |
| 91 | | `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) | |
| 92 | | `groq` | LLM + Voice transcription (Whisper, default) | [console.groq.com](https://console.groq.com) | |
| 93 | | `minimax` | LLM (MiniMax direct) | [platform.minimaxi.com](https://platform.minimaxi.com) | |
| 94 | | `minimax_anthropic` | LLM (MiniMax Anthropic-compatible endpoint, thinking mode) | [platform.minimaxi.com](https://platform.minimaxi.com) | |
| 95 | | `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) | |
| 96 | | `aihubmix` | LLM (API gateway, access to all models) | [aihubmix.com](https://aihubmix.com) | |
| 97 | | `siliconflow` | LLM (SiliconFlow/硅基流动) | [siliconflow.cn](https://siliconflow.cn) | |
| 98 | | `dashscope` | LLM (Qwen) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) | |
| 99 | | `moonshot` | LLM (Moonshot/Kimi) | [platform.moonshot.cn](https://platform.moonshot.cn) | |
| 100 | | `zhipu` | LLM (Zhipu GLM) | [open.bigmodel.cn](https://open.bigmodel.cn) | |
| 101 | | `mimo` | LLM (MiMo) | [platform.xiaomimimo.com](https://platform.xiaomimimo.com) | |
| 102 | | `ollama` | LLM (local, Ollama) | — | |
| 103 | | `lm_studio` | LLM (local, LM Studio) | — | |
| 104 | | `mistral` | LLM | [docs.mistral.ai](https://docs.mistral.ai/) | |
| 105 | | `stepfun` | LLM (Step Fun/阶跃星辰) | [platform.stepfun.com](https://platform.stepfun.com) | |
| 106 | | `ovms` | LLM (local, OpenVINO Model Server) | [docs.openvino.ai](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html) | |
| 107 | | `vllm` | LLM (local, any OpenAI-compatible server) | — | |
| 108 | | `openai_codex` | LLM (Codex, OAuth) | `nanobot provider login openai-codex` | |
| 109 | | `github_copilot` | LLM (GitHub Copilot, OAuth) | `nanobot provider login github-copilot` | |
| 110 | | `qianfan` | LLM (Baidu Qianfan) | [cloud.baidu.com](https://cloud.baidu.com/doc/qianfan/s/Hmh4suq26) | |
| 111 | |
| 112 | |
| 113 | <details> |
| 114 | <summary><b>OpenAI Codex (OAuth)</b></summary> |
| 115 | |
| 116 | Codex uses OAuth instead of API keys. Requires a ChatGPT Plus or Pro account. |
| 117 | No `providers.openaiCodex` block is needed in `config.json`; `nanobot provider login` stores the OAuth session outside config. |
| 118 | |
| 119 | **1. Login:** |
| 120 | ```bash |
| 121 | nanobot provider login openai-codex |
| 122 | ``` |
| 123 | |
| 124 | **2. Set model** (merge into `~/.nanobot/config.json`): |
| 125 | ```json |
| 126 | { |
| 127 | "agents": { |
| 128 | "defaults": { |
| 129 | "model": "openai-codex/gpt-5.1-codex" |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 | ``` |
| 134 | |
| 135 | **3. Chat:** |
| 136 | ```bash |
| 137 | nanobot agent -m "Hello!" |
| 138 | |
| 139 | # Target a specific workspace/config locally |
| 140 | nanobot agent -c .config.local.json -m "Hello!" |
| 141 | |
| 142 | # One-off workspace override on top of that config |
| 143 | nanobot agent -c .config.local.json -w /tmp/echo-director-test -m "Hello!" |
| 144 | ``` |
| 145 | |
| 146 | </details> |
| 147 | |
| 148 | |
| 149 | <details> |
| 150 | <summary><b>GitHub Copilot (OAuth)</b></summary> |
| 151 | |
| 152 | GitHub Copilot uses OAuth instead of API keys. Requires a [GitHub account with a plan](https://github.com/features/copilot/plans) configured. |
| 153 | No `providers.githubCopilot` block is needed in `config.json`; `nanobot provider login` stores the OAuth session outside config. |
| 154 | |
| 155 | **1. Login:** |
| 156 | ```bash |
| 157 | nanobot provider login github-copilot |
| 158 | ``` |
| 159 | |
| 160 | **2. Set model** (merge into `~/.nanobot/config.json`): |
| 161 | ```json |
| 162 | { |
| 163 | "agents": { |
| 164 | "defaults": { |
| 165 | "model": "github-copilot/gpt-4.1" |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | ``` |
| 170 | |
| 171 | **3. Chat:** |
| 172 | ```bash |
| 173 | nanobot agent -m "Hello!" |
| 174 | |
| 175 | # Target a specific workspace/config locally |
| 176 | nanobot agent -c .config.local.json -m "Hello!" |
| 177 | |
| 178 | # One-off workspace override on top of that config |
| 179 | nanobot agent -c .config.local.json -w /tmp/echo-director-test -m "Hello!" |
| 180 | ``` |
| 181 | |
| 182 | </details> |
| 183 | |
| 184 | <details> |
| 185 | <summary><b>Custom Provider (Any OpenAI-compatible API)</b></summary> |
| 186 | |
| 187 | Connects directly to any OpenAI-compatible endpoint — llama.cpp, Together AI, Fireworks, Azure OpenAI, or any self-hosted server. Model name is passed as-is. |
| 188 | |
| 189 | ```json |
| 190 | { |
| 191 | "providers": { |
| 192 | "custom": { |
| 193 | "apiKey": "your-api-key", |
| 194 | "apiBase": "https://api.your-provider.com/v1" |
| 195 | } |
| 196 | }, |
| 197 | "agents": { |
| 198 | "defaults": { |
| 199 | "model": "your-model-name" |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | ``` |
| 204 | |
| 205 | > For local servers that don't require authentication, set `apiKey` to `null`. |
| 206 | > |
| 207 | > `custom` is the right choice for providers that expose an OpenAI-compatible **chat completions** API. It does **not** force third-party endpoints onto the OpenAI/Azure **Responses API**. |
| 208 | > |
| 209 | > If your proxy or gateway is specifically Responses-API-compatible, use the `azure_openai` provider shape instead and point `apiBase` at that endpoint: |
| 210 | > |
| 211 | > ```json |
| 212 | > { |
| 213 | > "providers": { |
| 214 | > "azure_openai": { |
| 215 | > "apiKey": "your-api-key", |
| 216 | > "apiBase": "https://api.your-provider.com", |
| 217 | > "defaultModel": "your-model-name" |
| 218 | > } |
| 219 | > }, |
| 220 | > "agents": { |
| 221 | > "defaults": { |
| 222 | > "provider": "azure_openai", |
| 223 | > "model": "your-model-name" |
| 224 | > } |
| 225 | > } |
| 226 | > } |
| 227 | > ``` |
| 228 | > |
| 229 | > In short: **chat-completions-compatible endpoint → `custom`**; **Responses-compatible endpoint → `azure_openai`**. |
| 230 | |
| 231 | </details> |
| 232 | |
| 233 | <details> |
| 234 | <summary><b>Ollama (local)</b></summary> |
| 235 | |
| 236 | Run a local model with Ollama, then add to config: |
| 237 | |
| 238 | **1. Start Ollama** (example): |
| 239 | ```bash |
| 240 | ollama run llama3.2 |
| 241 | ``` |
| 242 | |
| 243 | **2. Add to config** (partial — merge into `~/.nanobot/config.json`): |
| 244 | ```json |
| 245 | { |
| 246 | "providers": { |
| 247 | "ollama": { |
| 248 | "apiBase": "http://localhost:11434" |
| 249 | } |
| 250 | }, |
| 251 | "agents": { |
| 252 | "defaults": { |
| 253 | "provider": "ollama", |
| 254 | "model": "llama3.2" |
| 255 | } |
| 256 | } |
| 257 | } |
| 258 | ``` |
| 259 | |
| 260 | > `provider: "auto"` also works when `providers.ollama.apiBase` is configured, but setting `"provider": "ollama"` is the clearest option. |
| 261 | |
| 262 | </details> |
| 263 | |
| 264 | <details> |
| 265 | <summary><b>LM Studio (local)</b></summary> |
| 266 | |
| 267 | [LM Studio](https://lmstudio.ai/) provides a local OpenAI-compatible server for running LLMs. Download models through the LM Studio UI, then start the local server. |
| 268 | |
| 269 | **1. Start LM Studio server:** |
| 270 | - Launch LM Studio |
| 271 | - Go to the "Local Server" tab |
| 272 | - Load a model (e.g., Llama, Mistral, Qwen) |
| 273 | - Click "Start Server" (default port: 1234) |
| 274 | |
| 275 | **2. Add to config** (partial — merge into `~/.nanobot/config.json`): |
| 276 | ```json |
| 277 | { |
| 278 | "providers": { |
| 279 | "lm_studio": { |
| 280 | "apiKey": null, |
| 281 | "apiBase": "http://localhost:1234/v1" |
| 282 | } |
| 283 | }, |
| 284 | "agents": { |
| 285 | "defaults": { |
| 286 | "provider": "lm_studio", |
| 287 | "model": "local-model" |
| 288 | } |
| 289 | } |
| 290 | } |
| 291 | ``` |
| 292 | |
| 293 | > **Note:** Set `apiKey` to `null` for LM Studio since it runs locally and doesn't require authentication. The model name should match what's shown in the LM Studio UI. |
| 294 | > `provider: "auto"` also works when `providers.lm_studio.apiBase` is configured, but setting `"provider": "lm_studio"` is the clearest option. |
| 295 | |
| 296 | </details> |
| 297 | |
| 298 | <details> |
| 299 | <summary><b>OpenVINO Model Server (local / OpenAI-compatible)</b></summary> |
| 300 | |
| 301 | Run LLMs locally on Intel GPUs using [OpenVINO Model Server](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html). OVMS exposes an OpenAI-compatible API at `/v3`. |
| 302 | |
| 303 | > Requires Docker and an Intel GPU with driver access (`/dev/dri`). |
| 304 | |
| 305 | **1. Pull the model** (example): |
| 306 | |
| 307 | ```bash |
| 308 | mkdir -p ov/models && cd ov |
| 309 | |
| 310 | docker run -d \ |
| 311 | --rm \ |
| 312 | --user $(id -u):$(id -g) \ |
| 313 | -v $(pwd)/models:/models \ |
| 314 | openvino/model_server:latest-gpu \ |
| 315 | --pull \ |
| 316 | --model_name openai/gpt-oss-20b \ |
| 317 | --model_repository_path /models \ |
| 318 | --source_model OpenVINO/gpt-oss-20b-int4-ov \ |
| 319 | --task text_generation \ |
| 320 | --tool_parser gptoss \ |
| 321 | --reasoning_parser gptoss \ |
| 322 | --enable_prefix_caching true \ |
| 323 | --target_device GPU |
| 324 | ``` |
| 325 | |
| 326 | > This downloads the model weights. Wait for the container to finish before proceeding. |
| 327 | |
| 328 | **2. Start the server** (example): |
| 329 | |
| 330 | ```bash |
| 331 | docker run -d \ |
| 332 | --rm \ |
| 333 | --name ovms \ |
| 334 | --user $(id -u):$(id -g) \ |
| 335 | -p 8000:8000 \ |
| 336 | -v $(pwd)/models:/models \ |
| 337 | --device /dev/dri \ |
| 338 | --group-add=$(stat -c "%g" /dev/dri/render* | head -n 1) \ |
| 339 | openvino/model_server:latest-gpu \ |
| 340 | --rest_port 8000 \ |
| 341 | --model_name openai/gpt-oss-20b \ |
| 342 | --model_repository_path /models \ |
| 343 | --source_model OpenVINO/gpt-oss-20b-int4-ov \ |
| 344 | --task text_generation \ |
| 345 | --tool_parser gptoss \ |
| 346 | --reasoning_parser gptoss \ |
| 347 | --enable_prefix_caching true \ |
| 348 | --target_device GPU |
| 349 | ``` |
| 350 | |
| 351 | **3. Add to config** (partial — merge into `~/.nanobot/config.json`): |
| 352 | |
| 353 | ```json |
| 354 | { |
| 355 | "providers": { |
| 356 | "ovms": { |
| 357 | "apiBase": "http://localhost:8000/v3" |
| 358 | } |
| 359 | }, |
| 360 | "agents": { |
| 361 | "defaults": { |
| 362 | "provider": "ovms", |
| 363 | "model": "openai/gpt-oss-20b" |
| 364 | } |
| 365 | } |
| 366 | } |
| 367 | ``` |
| 368 | |
| 369 | > OVMS is a local server — no API key required. Supports tool calling (`--tool_parser gptoss`), reasoning (`--reasoning_parser gptoss`), and streaming. |
| 370 | > See the [official OVMS docs](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html) for more details. |
| 371 | </details> |
| 372 | |
| 373 | <details> |
| 374 | <summary><b>vLLM (local / OpenAI-compatible)</b></summary> |
| 375 | |
| 376 | Run your own model with vLLM or any OpenAI-compatible server, then add to config: |
| 377 | |
| 378 | **1. Start the server** (example): |
| 379 | ```bash |
| 380 | vllm serve meta-llama/Llama-3.1-8B-Instruct --port 8000 |
| 381 | ``` |
| 382 | |
| 383 | **2. Add to config** (partial — merge into `~/.nanobot/config.json`): |
| 384 | |
| 385 | *Provider (set API key to null for local servers):* |
| 386 | ```json |
| 387 | { |
| 388 | "providers": { |
| 389 | "vllm": { |
| 390 | "apiKey": null, |
| 391 | "apiBase": "http://localhost:8000/v1" |
| 392 | } |
| 393 | } |
| 394 | } |
| 395 | ``` |
| 396 | |
| 397 | *Model:* |
| 398 | ```json |
| 399 | { |
| 400 | "agents": { |
| 401 | "defaults": { |
| 402 | "model": "meta-llama/Llama-3.1-8B-Instruct" |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | ``` |
| 407 | |
| 408 | </details> |
| 409 | |
| 410 | <details> |
| 411 | <summary><b>Adding a New Provider (Developer Guide)</b></summary> |
| 412 | |
| 413 | nanobot uses a **Provider Registry** (`nanobot/providers/registry.py`) as the single source of truth. |
| 414 | Adding a new provider only takes **2 steps** — no if-elif chains to touch. |
| 415 | |
| 416 | **Step 1.** Add a `ProviderSpec` entry to `PROVIDERS` in `nanobot/providers/registry.py`: |
| 417 | |
| 418 | ```python |
| 419 | ProviderSpec( |
| 420 | name="myprovider", # config field name |
| 421 | keywords=("myprovider", "mymodel"), # model-name keywords for auto-matching |
| 422 | env_key="MYPROVIDER_API_KEY", # env var name |
| 423 | display_name="My Provider", # shown in `nanobot status` |
| 424 | default_api_base="https://api.myprovider.com/v1", # OpenAI-compatible endpoint |
| 425 | ) |
| 426 | ``` |
| 427 | |
| 428 | **Step 2.** Add a field to `ProvidersConfig` in `nanobot/config/schema.py`: |
| 429 | |
| 430 | ```python |
| 431 | class ProvidersConfig(BaseModel): |
| 432 | ... |
| 433 | myprovider: ProviderConfig = ProviderConfig() |
| 434 | ``` |
| 435 | |
| 436 | That's it! Environment variables, model routing, config matching, and `nanobot status` display will all work automatically. |
| 437 | |
| 438 | **Common `ProviderSpec` options:** |
| 439 | |
| 440 | | Field | Description | Example | |
| 441 | |-------|-------------|---------| |
| 442 | | `default_api_base` | OpenAI-compatible base URL | `"https://api.deepseek.com"` | |
| 443 | | `env_extras` | Additional env vars to set | `(("ZHIPUAI_API_KEY", "{api_key}"),)` | |
| 444 | | `model_overrides` | Per-model parameter overrides | `(("kimi-k2.5", {"temperature": 1.0}), ("kimi-k2.6", {"temperature": 1.0}),)` | |
| 445 | | `is_gateway` | Can route any model (like OpenRouter) | `True` | |
| 446 | | `detect_by_key_prefix` | Detect gateway by API key prefix | `"sk-or-"` | |
| 447 | | `detect_by_base_keyword` | Detect gateway by API base URL | `"openrouter"` | |
| 448 | | `strip_model_prefix` | Strip provider prefix before sending to gateway | `True` (for AiHubMix) | |
| 449 | | `supports_max_completion_tokens` | Use `max_completion_tokens` instead of `max_tokens`; required for providers that reject both being set simultaneously (e.g. VolcEngine) | `True` | |
| 450 | |
| 451 | </details> |
| 452 | |
| 453 | ## Release Channels |
| 454 | |
| 455 | The personal release uses the WebUI WebSocket channel and the Director callback channel. Configure |
| 456 | both under `channels` in `.config.local.json`: |
| 457 | |
| 458 | ```json |
| 459 | { |
| 460 | "channels": { |
| 461 | "sendProgress": true, |
| 462 | "sendToolHints": false, |
| 463 | "sendMaxRetries": 3, |
| 464 | "websocket": { |
| 465 | "enabled": true, |
| 466 | "host": "127.0.0.1", |
| 467 | "port": 8765, |
| 468 | "websocketRequiresToken": false |
| 469 | }, |
| 470 | "director_callback": { |
| 471 | "enabled": true, |
| 472 | "host": "127.0.0.1", |
| 473 | "port": 18791, |
| 474 | "path_prefix": "/api/director", |
| 475 | "secret": "" |
| 476 | } |
| 477 | } |
| 478 | } |
| 479 | ``` |
| 480 | |
| 481 | Echo jobs use callback-only completion. Point the Echo client back to the local callback channel: |
| 482 | |
| 483 | ```json |
| 484 | { |
| 485 | "tools": { |
| 486 | "echoGenerator": { |
| 487 | "baseUrl": "http://127.0.0.1:8221", |
| 488 | "callbackBaseUrl": "http://127.0.0.1:18791", |
| 489 | "httpTimeoutSec": 30 |
| 490 | } |
| 491 | } |
| 492 | } |
| 493 | ``` |
| 494 | |
| 495 | Keep `callbackBaseUrl` aligned with the `director_callback` host and port. `GET /version/{id}` remains |
| 496 | available on Echo Server for diagnostics, but the Agent does not run a status-polling loop. |
| 497 | |
| 498 | | Setting | Default | Description | |
| 499 | |---------|---------|-------------| |
| 500 | | `sendProgress` | `true` | Stream agent's text progress to the channel | |
| 501 | | `sendToolHints` | `false` | Stream tool-call hints (e.g. `read_file("…")`) | |
| 502 | | `sendMaxRetries` | `3` | Max delivery attempts per outbound message, including the initial send (0-10 configured, minimum 1 actual attempt) | |
| 503 | |
| 504 | ### Retry Behavior |
| 505 | |
| 506 | Retry is intentionally simple. |
| 507 | |
| 508 | When a channel `send()` raises, nanobot retries at the channel-manager layer. By default, `channels.sendMaxRetries` is `3`, and that count includes the initial send. |
| 509 | |
| 510 | - **Attempt 1**: Send immediately |
| 511 | - **Attempt 2**: Retry after `1s` |
| 512 | - **Attempt 3**: Retry after `2s` |
| 513 | - **Higher retry budgets**: Backoff continues as `1s`, `2s`, `4s`, then stays capped at `4s` |
| 514 | - **Transient failures**: Network hiccups and temporary API limits often recover on the next attempt |
| 515 | - **Permanent failures**: Invalid tokens, revoked access, or banned channels will exhaust the retry budget and fail cleanly |
| 516 | |
| 517 | > [!NOTE] |
| 518 | > This design is deliberate: channel implementations should raise on delivery failure, and the channel manager owns the shared retry policy. |
| 519 | > |
| 520 | > If a channel is completely unreachable, nanobot cannot notify the user through that same channel. Watch logs for `Failed to send to {channel} after N attempts` to spot persistent delivery failures. |
| 521 | |
| 522 | ## Web Search |
| 523 | |
| 524 | > [!TIP] |
| 525 | > Use `proxy` in `tools.web` to route all web requests (search + fetch) through a proxy: |
| 526 | > ```json |
| 527 | > { "tools": { "web": { "proxy": "http://127.0.0.1:7890" } } } |
| 528 | > ``` |
| 529 | |
| 530 | nanobot supports multiple web search providers. Configure in `~/.nanobot/config.json` under `tools.web.search`. |
| 531 | |
| 532 | By default, web tools are enabled and web search uses `duckduckgo`, so search works out of the box without an API key. |
| 533 | |
| 534 | If you want to disable all built-in web tools entirely, set `tools.web.enable` to `false`. This removes both `web_search` and `web_fetch` from the tool list sent to the LLM. |
| 535 | |
| 536 | If you need to allow trusted private ranges such as Tailscale / CGNAT addresses, you can explicitly exempt them from SSRF blocking with `tools.ssrfWhitelist`: |
| 537 | |
| 538 | ```json |
| 539 | { |
| 540 | "tools": { |
| 541 | "ssrfWhitelist": ["100.64.0.0/10"] |
| 542 | } |
| 543 | } |
| 544 | ``` |
| 545 | |
| 546 | | Provider | Config fields | Env var fallback | Free | |
| 547 | |----------|--------------|------------------|------| |
| 548 | | `brave` | `apiKey` | `BRAVE_API_KEY` | No | |
| 549 | | `tavily` | `apiKey` | `TAVILY_API_KEY` | No | |
| 550 | | `jina` | `apiKey` | `JINA_API_KEY` | Free tier (10M tokens) | |
| 551 | | `kagi` | `apiKey` | `KAGI_API_KEY` | No | |
| 552 | | `searxng` | `baseUrl` | `SEARXNG_BASE_URL` | Yes (self-hosted) | |
| 553 | | `duckduckgo` (default) | — | — | Yes | |
| 554 | |
| 555 | **Disable all built-in web tools:** |
| 556 | ```json |
| 557 | { |
| 558 | "tools": { |
| 559 | "web": { |
| 560 | "enable": false |
| 561 | } |
| 562 | } |
| 563 | } |
| 564 | ``` |
| 565 | |
| 566 | **Brave:** |
| 567 | ```json |
| 568 | { |
| 569 | "tools": { |
| 570 | "web": { |
| 571 | "search": { |
| 572 | "provider": "brave", |
| 573 | "apiKey": "BSA..." |
| 574 | } |
| 575 | } |
| 576 | } |
| 577 | } |
| 578 | ``` |
| 579 | |
| 580 | **Tavily:** |
| 581 | ```json |
| 582 | { |
| 583 | "tools": { |
| 584 | "web": { |
| 585 | "search": { |
| 586 | "provider": "tavily", |
| 587 | "apiKey": "tvly-..." |
| 588 | } |
| 589 | } |
| 590 | } |
| 591 | } |
| 592 | ``` |
| 593 | |
| 594 | **Jina** (free tier with 10M tokens): |
| 595 | ```json |
| 596 | { |
| 597 | "tools": { |
| 598 | "web": { |
| 599 | "search": { |
| 600 | "provider": "jina", |
| 601 | "apiKey": "jina_..." |
| 602 | } |
| 603 | } |
| 604 | } |
| 605 | } |
| 606 | ``` |
| 607 | |
| 608 | **Kagi:** |
| 609 | ```json |
| 610 | { |
| 611 | "tools": { |
| 612 | "web": { |
| 613 | "search": { |
| 614 | "provider": "kagi", |
| 615 | "apiKey": "your-kagi-api-key" |
| 616 | } |
| 617 | } |
| 618 | } |
| 619 | } |
| 620 | ``` |
| 621 | |
| 622 | **SearXNG** (self-hosted, no API key needed): |
| 623 | ```json |
| 624 | { |
| 625 | "tools": { |
| 626 | "web": { |
| 627 | "search": { |
| 628 | "provider": "searxng", |
| 629 | "baseUrl": "https://searx.example" |
| 630 | } |
| 631 | } |
| 632 | } |
| 633 | } |
| 634 | ``` |
| 635 | |
| 636 | **DuckDuckGo** (zero config): |
| 637 | ```json |
| 638 | { |
| 639 | "tools": { |
| 640 | "web": { |
| 641 | "search": { |
| 642 | "provider": "duckduckgo" |
| 643 | } |
| 644 | } |
| 645 | } |
| 646 | } |
| 647 | ``` |
| 648 | |
| 649 | | Option | Type | Default | Description | |
| 650 | |--------|------|---------|-------------| |
| 651 | | `enable` | boolean | `true` | Enable or disable all built-in web tools (`web_search` + `web_fetch`) | |
| 652 | | `proxy` | string or null | `null` | Proxy for all web requests, for example `http://127.0.0.1:7890` | |
| 653 | |
| 654 | ### `tools.web.search` |
| 655 | |
| 656 | | Option | Type | Default | Description | |
| 657 | |--------|------|---------|-------------| |
| 658 | | `provider` | string | `"duckduckgo"` | Search backend: `brave`, `tavily`, `jina`, `searxng`, `duckduckgo` | |
| 659 | | `apiKey` | string | `""` | API key for Brave or Tavily | |
| 660 | | `baseUrl` | string | `""` | Base URL for SearXNG | |
| 661 | | `maxResults` | integer | `5` | Results per search (1–10) | |
| 662 | |
| 663 | ## MCP (Model Context Protocol) |
| 664 | |
| 665 | > [!TIP] |
| 666 | > The config format is compatible with Claude Desktop / Cursor. You can copy MCP server configs directly from any MCP server's README. |
| 667 | |
| 668 | nanobot supports [MCP](https://modelcontextprotocol.io/) — connect external tool servers and use them as native agent tools. |
| 669 | |
| 670 | Add MCP servers to your `config.json`: |
| 671 | |
| 672 | ```json |
| 673 | { |
| 674 | "tools": { |
| 675 | "mcpServers": { |
| 676 | "filesystem": { |
| 677 | "command": "npx", |
| 678 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"] |
| 679 | }, |
| 680 | "my-remote-mcp": { |
| 681 | "url": "https://example.com/mcp/", |
| 682 | "headers": { |
| 683 | "Authorization": "Bearer xxxxx" |
| 684 | } |
| 685 | } |
| 686 | } |
| 687 | } |
| 688 | } |
| 689 | ``` |
| 690 | |
| 691 | Two transport modes are supported: |
| 692 | |
| 693 | | Mode | Config | Example | |
| 694 | |------|--------|---------| |
| 695 | | **Stdio** | `command` + `args` | Local process via `npx` / `uvx` | |
| 696 | | **HTTP** | `url` + `headers` (optional) | Remote endpoint (`https://mcp.example.com/sse`) | |
| 697 | |
| 698 | Use `toolTimeout` to override the default 30s per-call timeout for slow servers: |
| 699 | |
| 700 | ```json |
| 701 | { |
| 702 | "tools": { |
| 703 | "mcpServers": { |
| 704 | "my-slow-server": { |
| 705 | "url": "https://example.com/mcp/", |
| 706 | "toolTimeout": 120 |
| 707 | } |
| 708 | } |
| 709 | } |
| 710 | } |
| 711 | ``` |
| 712 | |
| 713 | Use `enabledTools` to register only a subset of tools from an MCP server: |
| 714 | |
| 715 | ```json |
| 716 | { |
| 717 | "tools": { |
| 718 | "mcpServers": { |
| 719 | "filesystem": { |
| 720 | "command": "npx", |
| 721 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], |
| 722 | "enabledTools": ["read_file", "mcp_filesystem_write_file"] |
| 723 | } |
| 724 | } |
| 725 | } |
| 726 | } |
| 727 | ``` |
| 728 | |
| 729 | `enabledTools` accepts either the raw MCP tool name (for example `read_file`) or the wrapped nanobot tool name (for example `mcp_filesystem_write_file`). |
| 730 | |
| 731 | - Omit `enabledTools`, or set it to `["*"]`, to register all tools. |
| 732 | - Set `enabledTools` to `[]` to register no tools from that server. |
| 733 | - Set `enabledTools` to a non-empty list of names to register only that subset. |
| 734 | |
| 735 | MCP tools are automatically discovered and registered on startup. The LLM can use them alongside built-in tools — no extra configuration needed. |
| 736 | |
| 737 | |
| 738 | |
| 739 | |
| 740 | ## Security |
| 741 | |
| 742 | > [!TIP] |
| 743 | > For production deployments, set `"restrictToWorkspace": true` and `"tools.exec.sandbox": "bwrap"` in your config to sandbox the agent. |
| 744 | > In `v0.1.4.post3` and earlier, an empty `allowFrom` allowed all senders. Since `v0.1.4.post4`, empty `allowFrom` denies all access by default. To allow all senders, set `"allowFrom": ["*"]`. |
| 745 | |
| 746 | | Option | Default | Description | |
| 747 | |--------|---------|-------------| |
| 748 | | `tools.restrictToWorkspace` | `false` | When `true`, restricts **all** agent tools (shell, file read/write/edit, list) to the workspace directory. Prevents path traversal and out-of-scope access. | |
| 749 | | `tools.exec.sandbox` | `""` | Sandbox backend for shell commands. Set to `"bwrap"` to wrap exec calls in a [bubblewrap](https://github.com/containers/bubblewrap) sandbox — the process can only see the workspace (read-write) and media directory (read-only); config files and API keys are hidden. Automatically enables `restrictToWorkspace` for file tools. **Linux only** — requires `bwrap` to be installed. Not available on macOS or Windows (bwrap depends on Linux kernel namespaces). | |
| 750 | | `tools.exec.enable` | `true` | When `false`, the shell `exec` tool is not registered at all. Use this to completely disable shell command execution. | |
| 751 | | `tools.exec.pathAppend` | `""` | Extra directories to append to `PATH` when running shell commands (e.g. `/usr/sbin` for `ufw`). | |
| 752 | | `channels.*.allowFrom` | `[]` (deny all) | Whitelist of user IDs. Empty denies all; use `["*"]` to allow everyone. | |
| 753 | |
| 754 | ## Auto Compact |
| 755 | |
| 756 | When a user is idle for longer than a configured threshold, nanobot **proactively** compresses the older part of the session context into a summary while keeping a recent legal suffix of live messages. This reduces token cost and first-token latency when the user returns — instead of re-processing a long stale context with an expired KV cache, the model receives a compact summary, the most recent live context, and fresh input. |
| 757 | |
| 758 | ```json |
| 759 | { |
| 760 | "agents": { |
| 761 | "defaults": { |
| 762 | "idleCompactAfterMinutes": 15 |
| 763 | } |
| 764 | } |
| 765 | } |
| 766 | ``` |
| 767 | |
| 768 | | Option | Default | Description | |
| 769 | |--------|---------|-------------| |
| 770 | | `agents.defaults.idleCompactAfterMinutes` | `0` (disabled) | Minutes of idle time before auto-compaction starts. Set to `0` to disable. Recommended: `15` — close to a typical LLM KV cache expiry window, so stale sessions get compacted before the user returns. | |
| 771 | |
| 772 | `sessionTtlMinutes` remains accepted as a legacy alias for backward compatibility, but `idleCompactAfterMinutes` is the preferred config key going forward. |
| 773 | |
| 774 | How it works: |
| 775 | 1. **Idle detection**: On each idle tick (~1 s), checks all sessions for expiration. |
| 776 | 2. **Background compaction**: Idle sessions summarize the older live prefix via LLM and keep the most recent legal suffix (currently 8 messages). |
| 777 | 3. **Summary injection**: When the user returns, the summary is injected as runtime context (one-shot, not persisted) alongside the retained recent suffix. |
| 778 | 4. **Restart-safe resume**: The summary is also mirrored into session metadata so it can still be recovered after a process restart. |
| 779 | |
| 780 | > [!NOTE] |
| 781 | > Mental model: "summarize older context, keep the freshest live turns, **and overwrite the session file with the compact form.**" It is not a full `session.clear()`, but it is a write — not a soft cursor move. |
| 782 | > |
| 783 | > Concretely, auto compact rewrites `sessions/<key>.jsonl` in place: older messages (including their structured `tool_calls` / `tool_call_id` / `reasoning_content`) are replaced by just the retained recent suffix (currently 8 messages), while the archived prefix is preserved only as a plain-text summary appended to `memory/history.jsonl` (or a `[RAW] ...` flattened dump if LLM summarization fails). The original structured JSON of those turns is no longer recoverable from the session file. |
| 784 | > |
| 785 | > This differs from the **token-driven soft consolidation** that fires when a prompt exceeds the context budget: that path only advances an internal `last_consolidated` cursor and leaves the session file untouched, so the raw tool-call trail stays on disk and can still be replayed or audited. If you rely on that trail for debugging or auditing, leave `idleCompactAfterMinutes` at the default `0` and let only the token-driven path run. |
| 786 | |
| 787 | ## Timezone |
| 788 | |
| 789 | Time is context. Context should be precise. |
| 790 | |
| 791 | By default, nanobot uses `UTC` for runtime time context. If you want the agent to think in your local time, set `agents.defaults.timezone` to a valid [IANA timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones): |
| 792 | |
| 793 | ```json |
| 794 | { |
| 795 | "agents": { |
| 796 | "defaults": { |
| 797 | "timezone": "Asia/Shanghai" |
| 798 | } |
| 799 | } |
| 800 | } |
| 801 | ``` |
| 802 | |
| 803 | This affects runtime time strings shown to the model, such as runtime context and heartbeat prompts. It also becomes the default timezone for cron schedules when a cron expression omits `tz`, and for one-shot `at` times when the ISO datetime has no explicit offset. |
| 804 | |
| 805 | Common examples: `UTC`, `America/New_York`, `America/Los_Angeles`, `Europe/London`, `Europe/Berlin`, `Asia/Tokyo`, `Asia/Shanghai`, `Asia/Singapore`, `Australia/Sydney`. |
| 806 | |
| 807 | > Need another timezone? Browse the full [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). |
| 808 | |
| 809 | ## Unified Session |
| 810 | |
| 811 | By default, each channel and chat ID combination gets its own session. To share one conversation |
| 812 | across multiple configured transports, enable `unifiedSession`: |
| 813 | |
| 814 | ```json |
| 815 | { |
| 816 | "agents": { |
| 817 | "defaults": { |
| 818 | "unifiedSession": true |
| 819 | } |
| 820 | } |
| 821 | } |
| 822 | ``` |
| 823 | |
| 824 | When enabled, all incoming messages are routed into a single shared session. |
| 825 | |
| 826 | | Behavior | `false` (default) | `true` | |
| 827 | |----------|-------------------|--------| |
| 828 | | Session key | `channel:chat_id` | `unified:default` | |
| 829 | | Cross-channel continuity | No | Yes | |
| 830 | | `/new` clears | Current channel session | Shared session | |
| 831 | | `/stop` finds tasks | By channel session | By shared session | |
| 832 | | Existing `session_key_override` | Respected | Still respected — not overwritten | |
| 833 | |
| 834 | > This is designed for single-user, multi-device setups. It is **off by default** — existing users see zero behavior change. |
| 835 | |
| 836 | ## Disabled Skills |
| 837 | |
| 838 | nanobot ships with built-in skills, and your workspace can also define custom skills under `skills/`. If you want to hide specific skills from the agent, set `agents.defaults.disabledSkills` to a list of skill directory names: |
| 839 | |
| 840 | ```json |
| 841 | { |
| 842 | "agents": { |
| 843 | "defaults": { |
| 844 | "disabledSkills": ["github", "weather"] |
| 845 | } |
| 846 | } |
| 847 | } |
| 848 | ``` |
| 849 | |
| 850 | Disabled skills are excluded from the main agent's skill summary, from always-on skill injection, and from subagent skill summaries. This is useful when some bundled skills are unnecessary for your deployment or should not be exposed to end users. |
| 851 | |
| 852 | | Option | Default | Description | |
| 853 | |--------|---------|-------------| |
| 854 | | `agents.defaults.disabledSkills` | `[]` | List of skill directory names to exclude from loading. Applies to both built-in skills and workspace skills. | |
| 855 |