返回 JoyAI-Echo
configuration.md
根目录 / echo_longvideo / Director_Agent / docs / configuration.md
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
855 lines MARKDOWN