# nanobot Admin & Architecture Guide

This guide is the exhaustive operational and architectural reference for **nanobot v0.2.1** — an open-source, lightweight personal AI-agent framework (PyPI: `nanobot-ai`, MIT licensed, Python ≥ 3.11). It is written for administrators, operators, architects, and developers who deploy, secure, extend, or study nanobot. Written from the source at commit `6a0368b`.

For end-user installation and day-to-day usage, see the companion **User Guide**.

---

## 1. Executive Summary

nanobot is a single Python process that runs **one autonomous LLM agent** and simultaneously fronts it across a dozen+ messaging channels, a browser WebUI, an OpenAI-compatible HTTP API, and a Python SDK. Its design philosophy is a *small, readable core*: messages flow through an async message bus into a compact agent loop that calls an LLM, executes tools, and manages layered memory — rather than a heavy orchestration framework.

**What it is:**

- An agent **runtime** you self-host and own end to end.
- A **multi-channel** front end (Telegram, Discord, Slack, Feishu, WhatsApp, Matrix, Email, QQ, WeChat, WeCom, DingTalk, Teams, Signal, Mochat, WebSocket/WebUI).
- A **multi-provider** model router (~40 LLM providers + any OpenAI-compatible endpoint, with presets and fallback chains).
- An **extensible** platform: channels, tools, and skills are all discovered via Python entry points and a workspace directory.

**What admins must understand most:**

1. **The trust boundary is the host.** When the agent runs in autonomous mode it can read/write files, run shell commands, and fetch the web. The deployment host's security posture *is* the system's security posture. See [Section 7](#7-security).
2. **All state lives on disk under `~/.nanobot/`.** There is no database server; config, workspace, memory, sessions, media, and channel auth are files.
3. **Config is the control plane.** A single `config.json` (Pydantic-validated, camelCase) drives every behavior.

| Attribute | Value |
|-----------|-------|
| Version | 0.2.1 |
| Language | Python 3.11+ (asyncio throughout); React/TypeScript WebUI; TypeScript WhatsApp bridge |
| Package | `nanobot-ai` on PyPI; command is `nanobot` |
| License | MIT |
| Config | `~/.nanobot/config.json` (Pydantic, camelCase aliases) |
| Persistence | Filesystem under `~/.nanobot/` (no DB) |
| Entry points | CLI `nanobot/cli/commands.py`; SDK `nanobot/nanobot.py` |
| Build | Hatchling; WebUI bundled into the wheel; bridge force-included |

---

## 2. Architecture

### 2.1 Core data flow

nanobot decouples channels from the agent core with an async **MessageBus**. The flow for a single turn:

```text
                 External platforms (Telegram, Discord, WebUI, API, ...)
                                      │  inbound
                                      ▼
   ┌──────────────────────────────────────────────────────────────────┐
   │                          Channels layer                            │
   │  nanobot/channels/*  (auto-discovered: pkgutil scan + entry pts)   │
   │  _handle_message()  →  is_allowed()  →  publish InboundMessage     │
   └──────────────────────────────┬───────────────────────────────────┘
                                   │  InboundMessage
                                   ▼
                      ┌────────────────────────┐
                      │   MessageBus (async)    │   nanobot/bus/queue.py
                      └───────────┬────────────┘
                                  ▼
   ┌──────────────────────────────────────────────────────────────────┐
   │   AgentLoop  (nanobot/agent/loop.py)                               │
   │   • resolves session key       • builds context/prompt            │
   │   • applies hooks              • routes slash commands            │
   └──────────────────────────────┬───────────────────────────────────┘
                                   ▼
   ┌──────────────────────────────────────────────────────────────────┐
   │   AgentRunner (nanobot/agent/runner.py)  — the ReAct loop          │
   │   send → LLM provider → tool_calls → execute tools → repeat        │
   │     │                    │                  │                       │
   │     ▼                    ▼                  ▼                       │
   │  Providers           Tools registry     Session/Memory             │
   │  nanobot/providers/  nanobot/agent/     nanobot/session/           │
   │                      tools/             nanobot/agent/memory.py     │
   └──────────────────────────────┬───────────────────────────────────┘
                                   │  OutboundMessage
                                   ▼
                          back onto the MessageBus
                                   │  outbound
                                   ▼
                      Channels.send() → external platform
```

1. **Channels** receive a platform message, check the allow-list, and publish an `InboundMessage`.
2. **`AgentLoop`** consumes it, resolves the session, builds context, and coordinates the turn (and intercepts slash commands via the command router).
3. **`AgentRunner`** runs the multi-turn LLM conversation: send messages to the provider, receive tool calls, execute tools, stream responses.
4. Results are published as `OutboundMessage` events and the originating channel's `send()` delivers them.

### 2.2 Component inventory

| Subsystem | Location | Responsibility |
|-----------|----------|----------------|
| **Message bus** | `nanobot/bus/queue.py`, `events.py` | Async pub/sub decoupling channels from the agent; `InboundMessage` / `OutboundMessage` events |
| **Agent loop** | `nanobot/agent/loop.py` | Session keys, hooks, context building, slash-command routing |
| **Agent runner** | `nanobot/agent/runner.py` | The ReAct conversation loop: LLM call → tool execution → stream |
| **Providers** | `nanobot/providers/` | LLM backends on a common `base.py`; `factory.py` + `registry.py` for instantiation & model discovery; image gen + transcription |
| **Channels** | `nanobot/channels/` | 18 platform integrations on `base.py`; `manager.py` discovers & coordinates |
| **Tools** | `nanobot/agent/tools/` | Agent capabilities; `registry.py` + `loader.py` auto-discover |
| **Memory** | `nanobot/agent/memory.py` | History persistence + Dream two-phase consolidation; atomic writes |
| **Sessions** | `nanobot/session/` | Per-session history, compaction, TTL auto-compaction (`manager.py`), goal state (`goal_state.py`) |
| **Config** | `nanobot/config/schema.py`, `loader.py` | Pydantic config, camelCase aliases, `${VAR}` resolution |
| **Command router** | `nanobot/command/` | Slash-command routing + built-in handlers |
| **Cron** | `nanobot/cron/` | Schedules; powers the Heartbeat periodic-task mechanism |
| **Pairing** | `nanobot/pairing/` | DM sender approval store + persistent pairing codes per channel |
| **Skills** | `nanobot/skills/` | Built-in skills (markdown + YAML frontmatter) loaded into context |
| **Security** | `nanobot/security/` | SSRF/network guard (`network.py`), PTH file guard |
| **API server** | `nanobot/api/server.py` | OpenAI-compatible HTTP API |
| **CLI** | `nanobot/cli/commands.py` | Typer/Click app, the `nanobot` entry point |
| **SDK** | `nanobot/nanobot.py` | `Nanobot` library facade |
| **WebUI** | `webui/` (React/Vite) → built into `nanobot/web/dist/` | Browser workbench over the WebSocket multiplex protocol |
| **Bridge** | `bridge/` (TypeScript) → force-included as `nanobot/bridge` | WhatsApp bridge (Baileys) |
| **Templates** | `nanobot/templates/` | Jinja2 prompt templates (`identity.md`, `SOUL.md`, `HEARTBEAT.md`, …) |

### 2.3 The agent loop (ReAct) and hooks

`AgentRunner` implements a bounded think→act loop: the model is asked for the next action, tool calls are executed, results are fed back, and the loop repeats until the model produces a final answer or hits the iteration cap (`max_iterations`, default 40, adjustable at runtime via the `my` tool, range 1–100). Concurrency is bounded by `NANOBOT_MAX_CONCURRENT_REQUESTS` (default 3).

The loop exposes a **hook** interface (`nanobot/agent/hook.py`) usable from the SDK for observability and light customization:

| Hook method | Fires |
|-------------|-------|
| `wants_streaming()` | Declare interest in token streaming |
| `before_iteration(ctx)` | Before each LLM call |
| `on_stream(ctx, delta)` | Per streamed token |
| `on_stream_end(ctx, *, resuming)` | When a stream segment ends |
| `before_execute_tools(ctx)` | Before tool execution |
| `after_iteration(ctx)` | After each iteration |
| `finalize_content(ctx, content)` | Transform final text (pipeline across hooks) |

Async hook methods are fan-out with error isolation; `finalize_content` is a pipeline (each hook sees the previous output).

### 2.4 Provider abstraction (model freedom)

Providers share a common base (`nanobot/providers/base.py`) and are registered in a **Provider Registry** (`registry.py`) — the single source of truth. Two special clients exist (Anthropic native, OpenAI Responses/Azure); everything else routes through an OpenAI-compatible client pointed at the provider's `apiBase`. This registry pattern is why **adding a provider takes two steps** (see [Section 9.2](#92-add-a-new-provider)). `factory.py` instantiates the right client for the active model; `fallback_provider.py` implements the failover chain.

Native/special-cased provider clients: `anthropic_provider.py`, `azure_openai_provider.py`, `bedrock_provider.py` (Converse API), `github_copilot_provider.py` (OAuth), `openai_codex_provider.py` (OAuth), `openai_responses/` (Responses API), `openai_compat_provider.py` (the catch-all for the long tail). Plus `image_generation.py` and `transcription.py`.

### 2.5 Channel abstraction (chat-native reach)

Channels subclass `BaseChannel` (`nanobot/channels/base.py`). `manager.py` discovers built-ins by `pkgutil` scan and third-party channels via the `nanobot.channels` entry-point group, instantiates any with `enabled: true`, and runs each one's `start()` (which must block forever). Inbound messages go through `_handle_message()` → `is_allowed()` → bus. Streaming is opt-in (config `streaming: true` **and** the subclass overrides `send_delta`).

Channels split cleanly by connectivity, which matters for firewall planning:

- **No inbound port** (WebSocket/long-conn/outbound): Telegram (long-poll), Discord, Slack (Socket Mode), Feishu/Lark (WebSocket), DingTalk (Stream), QQ, WeCom, WeChat (long-poll), Matrix, Signal (signal-cli daemon), Mochat (Socket.IO), Napcat (forward WS), WhatsApp (local bridge).
- **Inbound webhook (needs a public HTTPS endpoint / reverse proxy)**: Microsoft Teams (`/api/messages`), Telegram *in optional webhook mode*.

### 2.6 Tool system

Tools live in `nanobot/agent/tools/` and are auto-discovered (`loader.py` + `registry.py`) plus the `nanobot.tools` entry-point group. The agent is the only caller; users never invoke tools directly.

| Tool name | Source | Capability |
|-----------|--------|------------|
| `read_file`, `write_file`, `edit_file`, `list_dir` | `filesystem.py` | Workspace file I/O (path-guarded) |
| `apply_patch` | `apply_patch.py` | Structured multi-file patch application |
| `exec` | `shell.py` (+ `sandbox.py`, `exec_session.py`) | Shell command execution, optionally sandboxed |
| `web_search`, `web_fetch` | `web.py`, `search.py` | SSRF-guarded web search & page fetch |
| `spawn` | `spawn.py`, `subagent.py` | Launch subagents (bounded by `maxConcurrentSubagents`) |
| `long_task` / `task`, `goal` / `complete_goal` | `long_task.py` | Sustained multi-turn objectives |
| `cron` | `cron.py` | Agent-managed schedules/reminders |
| `message` | `message.py` | Proactive cross-channel delivery |
| `generate_image` | `image_generation.py` | Image generation/editing (opt-in) |
| `my` | `self.py`, `runtime_state.py` | Inspect/tune runtime state (model, context window, iterations) |
| `cli_apps` / `run_cli_app` | `cli_apps.py` | Drive installed CLI apps |
| MCP tools | `mcp.py` | Tools proxied from external MCP servers |

### 2.7 Memory architecture

Two-stage layered memory (see User Guide §10 for the user view):

- **Consolidator** summarizes the oldest safe slice of an over-long session and appends a JSONL line to `memory/history.jsonl` (append-only, cursor-based via `.cursor`).
- **Dream** (`/dream`, or scheduled via cron) reads new history since `.dream_cursor` plus the durable files `SOUL.md`, `USER.md`, `memory/MEMORY.md`, and makes surgical single-pass edits, version-controlled by a `GitStore` (`memory/.git`).

There are **two distinct compaction paths** an admin must not confuse:

| Path | Trigger | Effect on `sessions/<key>.jsonl` | Auditability |
|------|---------|----------------------------------|--------------|
| **Token-driven soft consolidation** | Prompt exceeds context budget | Untouched — only advances an internal `last_consolidated` cursor | Raw tool-call trail stays on disk; replayable/auditable |
| **Idle auto-compact** (`idleCompactAfterMinutes`) | User idle past threshold | **Rewrites the session file in place** — keeps the recent 8-message suffix, archives the prefix to `history.jsonl` as plain-text summary | Original structured turns are **not** recoverable from the session file |

> [!IMPORTANT]
> If you rely on the raw session trail for debugging or compliance auditing, leave `idleCompactAfterMinutes` at its default `0` so only the non-destructive token-driven path runs.

Session writes use atomic temp-file + fsync + rename + directory-fsync for crash durability (`nanobot/agent/memory.py`) — do not replace with plain writes.

---

## 3. System Requirements

| Component | Requirement |
|-----------|-------------|
| Python | ≥ 3.11 (3.11 / 3.12 classified; Docker image uses 3.12) |
| OS | Linux, macOS, Windows. Exceptions: **Matrix** unsupported on Windows; **bwrap sandbox** Linux-only |
| Node.js | ≥ 18 (only for WhatsApp; ≥ 20 in the Docker image) |
| Memory | Modest; the agent process is lightweight (compose limits it to 1 CPU / 1 GB) |
| Network | Outbound HTTPS to your LLM provider(s) and any enabled channel's platform |
| Disk | Workspace + memory + media; grows with sessions and generated images |

### Core dependencies (from `pyproject.toml`)

`typer`, `anthropic`, `openai`, `pydantic` / `pydantic-settings`, `websockets` / `websocket-client`, `httpx`, `ddgs` (DuckDuckGo search), `mcp`, `tiktoken`, `jinja2`, `dulwich` (GitStore), `croniter`, `loguru`, `rich`, `prompt-toolkit` / `questionary`, document readers (`pypdf`, `python-docx`, `openpyxl`, `python-pptx`), and channel SDKs (`python-telegram-bot`, `lark-oapi`, `slack-sdk`, `dingtalk-stream`, `qq-botpy`, `boto3`, …). Optional extras gate Matrix, Discord, WeChat, WeCom, Teams, Azure, PDF (PyMuPDF), Olostep, LangSmith, and the API server.

> [!NOTE]
> The litellm dependency was removed in v0.1.4.post6 in favor of native `openai` + `anthropic` SDKs. Older security notes that mention keeping litellm updated are historical.

---

## 4. Configuration Mastery

All configuration is one Pydantic-validated JSON file: `~/.nanobot/config.json` (the schema is `nanobot/config/schema.py`, loaded by `loader.py`). The base model uses `alias_generator=to_camel` with `populate_by_name=True`, so **both camelCase (`allowFrom`) and snake_case (`allow_from`) are accepted**; docs use camelCase.

### 4.1 Config refresh without data loss

```bash
nanobot onboard      # answer "N" when asked to overwrite
```

This merges in any new default fields while preserving your existing values — the safe way to pick up schema additions after an upgrade.

### 4.2 Secrets via environment variables

Any string value may use a `${VAR_NAME}` reference, resolved from `os.environ` **once at startup, in memory only** (never written back). A missing variable fails fast:

```text
ValueError: Environment variable 'NAME' referenced in config is not set
```

> [!CAUTION]
> `${VAR}` is **not** shell default-value syntax — there is no `${VAR:-default}` fallback. If the variable is unset, `load_config` raises and the agent falls back to default configuration. Verify every referenced variable exists in the gateway's process environment.

Loading mechanisms (any that populate `os.environ` works): systemd `EnvironmentFile=`, Docker `--env-file` / `-e`, direnv `.envrc`, or secret managers (`op run`, `pass`, `bw`).

### 4.3 Top-level config sections

| Section | Purpose |
|---------|---------|
| `providers` | Per-provider credentials and endpoints |
| `agents.defaults` | Model, generation params, behavioral toggles |
| `modelPresets` | Named model configurations switchable at runtime |
| `channels` | Per-channel settings + global channel defaults |
| `tools` | Web, exec, MCP, image generation, `my`, security toggles |
| `gateway` | Gateway bind host/port + health endpoint |

### 4.4 `agents.defaults` reference

| Key | Default | Description |
|-----|---------|-------------|
| `provider` | `"auto"` | Provider name or `auto` (detect from model/keys) |
| `model` | — | Model id (e.g. `anthropic/claude-opus-4-6`) |
| `maxTokens` | provider-dependent | Max completion tokens |
| `contextWindowTokens` | provider-dependent | Window used for prompt building & consolidation |
| `temperature` | provider-dependent | Sampling temperature |
| `reasoningEffort` | unset | `none`/`low`/`medium`/`high`/`max`/`adaptive` (provider support varies) |
| `modelPreset` | `null` | Active preset name (`null` = implicit default) |
| `fallbackModels` | `[]` | Ordered failover chain (preset names or inline `{provider, model}`) |
| `timezone` | `"UTC"` | IANA tz for runtime time context, cron, and `at` times |
| `unifiedSession` | `false` | Route all channels into one shared session |
| `idleCompactAfterMinutes` | `0` (off) | Idle minutes before destructive auto-compaction (`sessionTtlMinutes` is a legacy alias) |
| `maxConcurrentSubagents` | `1` | Max simultaneous spawned subagents |
| `disabledSkills` | `[]` | Skill directory names to hide from the agent |
| `toolHintMaxLength` | `40` | Truncation length for tool-hint display (20–500) |
| `dream` | — | Dream config: `intervalH`, `cron`, `modelOverride`*, `maxBatchSize`†, `maxIterations`† |

\* reserved for a future release. † deprecated, accepted for compat, no effect.

### 4.5 Global channel settings (`channels`)

| Setting | Default | Description |
|---------|---------|-------------|
| `sendProgress` | `true` | Stream the agent's text progress to the channel |
| `sendToolHints` | `false` | Stream tool-call breadcrumbs |
| `showReasoning` | `true` | Allow channels to surface model reasoning/thinking (CLI + WebUI render it; most chat channels are no-op until adapted) |
| `extractDocumentText` | `true` | Extract document/text attachments into the prompt |
| `sendMaxRetries` | `3` | Outbound delivery attempts incl. the first (1–10) |
| `transcriptionProvider` | `"groq"` | Voice transcription backend (`groq` free / `openai`) |
| `transcriptionLanguage` | `null` | Optional ISO-639-1 hint |

`sendProgress`, `sendToolHints`, and `showReasoning` can be overridden per channel. **Retry backoff:** attempt 1 immediate, then `1s, 2s, 4s, …` capped at `4s`.

### 4.6 Web tools (`tools.web`)

| Option | Default | Description |
|--------|---------|-------------|
| `enable` | `true` | Master switch for `web_search` + `web_fetch` |
| `proxy` | `null` | Proxy for all web requests |
| `userAgent` | `null` | UA header; browser-like if null |
| `search.provider` | `"duckduckgo"` | `brave`/`tavily`/`jina`/`kagi`/`olostep`/`volcengine`/`searxng`/`duckduckgo` |
| `search.apiKey` | `""` | Key for API-backed search |
| `search.maxResults` | `5` | Results per search (1–10) |
| `fetch.useJinaReader` | `true` | Prefer Jina Reader over local readability-lxml |
| `ssrfWhitelist` (under `tools`) | `[]` | CIDRs exempted from SSRF blocking (e.g. Tailscale `100.64.0.0/10`) |

### 4.7 Security-relevant config

| Option | Default | Description |
|--------|---------|-------------|
| `tools.restrictToWorkspace` | `false` | Confine all file/shell tools to the workspace |
| `tools.exec.enable` | `true` | If `false`, the `exec` tool is not registered |
| `tools.exec.sandbox` | `""` | `"bwrap"` to sandbox shell in bubblewrap (Linux only; auto-enables `restrictToWorkspace`) |
| `tools.exec.timeout` | `60` | Shell hard timeout seconds (`0` disables) |
| `tools.exec.pathAppend` | `""` | Extra `PATH` dirs for shell commands |
| `tools.ssrfWhitelist` | `[]` | SSRF allow-list CIDRs |
| `channels.*.allowFrom` | omitted | Per-channel access control (omit = pairing-only; `["*"]` = all; list = specific IDs) |
| `tools.my.enable` / `tools.my.allowSet` | `true` / `false` | `my` tool read / write modes |

### 4.8 `tools.my` (self-runtime tool)

The `my` tool lets the agent inspect (and, if `allowSet: true`, tune) its own runtime state — model, context window, iteration budget, scratchpad. **All changes are in-memory only; restart restores defaults.** Protected ranges: `max_iterations` 1–100, `context_window_tokens` 4,096–1,000,000, `model` non-empty. Sensitive fields (`api_key`, `password`, `secret`, `token`, security boundaries, Python internals) are blocked from both check and set regardless of dot-path — preventing credential leaks and sandbox escape.

### 4.9 Image generation (`tools.imageGeneration`)

| Option | Default | Description |
|--------|---------|-------------|
| `enabled` | `false` | Register the `generate_image` tool |
| `provider` | `"openrouter"` | `openrouter`/`custom`/`aihubmix`/`minimax`/`gemini`/`ollama`/`stepfun`/`zhipu` |
| `model` | `"openai/gpt-5.4-image-2"` | Provider model |
| `defaultAspectRatio` | `"1:1"` | Default ratio |
| `defaultImageSize` | `"1K"` | Default size hint |
| `maxImagesPerTurn` | `4` | Max `count` per call (1–8) |
| `saveDir` | `"generated"` | Subdir under the media directory |

Artifacts: `~/.nanobot/media/generated/YYYY-MM-DD/img_<id>.<ext>` plus a JSON sidecar (id, path, mime, prompt, model, provider, source_images, created_at).

### 4.10 MCP servers (`tools.mcpServers`)

| Mode | Fields | Notes |
|------|--------|-------|
| stdio | `command` + `args` (+ `env`) | Local process (`npx`/`uvx`); paths normalized on Windows |
| HTTP | `url` + optional `headers` | Remote endpoint (e.g. SSE) |

Extra knobs: `toolTimeout` (override the 30s per-call default) and `enabledTools` (subset selection — accepts raw or wrapped names; `["*"]` all, `[]` none).

---

## 5. Channel Reference

Every channel is configured under `channels.<name>` with `enabled: true` plus `allowFrom`. Connectivity column indicates firewall implications.

| Channel | Key fields | Connectivity | Extra install |
|---------|-----------|--------------|---------------|
| `telegram` | `token`, `allowFrom`; optional webhook (`mode`, `webhookUrl`, `webhookSecretToken`, …) | Long-poll (default) or inbound webhook | core |
| `discord` | `token`, `allowFrom`, `allowChannels`, `groupPolicy`, `streaming` | Outbound gateway | `[discord]` |
| `slack` | `botToken`, `appToken`, `groupPolicy` | Socket Mode (outbound) | core |
| `feishu` | `appId`, `appSecret`, `streaming`, `domain` (`feishu`/`lark`), `reactEmoji`, `doneEmoji` | WebSocket | core |
| `whatsapp` | `allowFrom` (phone numbers) | Local Node bridge → `127.0.0.1:3001` | Node ≥ 18 |
| `weixin` | `allowFrom`, optional `token`, `routeTag`, `stateDir` | HTTP long-poll + QR login | `[weixin]` |
| `qq` | `appId`, `secret`, `msgFormat` | botpy WebSocket; private msgs only | core |
| `napcat` | `wsUrl`, `accessToken`, `groupPolicy`, `groupPolicyOverrides`, `welcomeNewMembers` | Forward WebSocket (OneBot v11) | core |
| `dingtalk` | `clientId`, `clientSecret`, `groupUserIsolation` | Stream Mode | core |
| `matrix` | `homeserver`, `userId`, `password`, `e2eeEnabled`, `sasVerification`, `maxMediaBytes` | Sync (outbound) | `[matrix]` (not Windows) |
| `email` | `consentGranted`, IMAP/SMTP host/port/creds, `fromAddress`, `autoReplyEnabled`, attachment caps | IMAP poll + SMTP | core |
| `wecom` | `botId`, `secret` | WebSocket | `[wecom]` |
| `msteams` | `appId`, `appPassword`, `tenantId`, `host`/`port`/`path`, `validateInboundAuth`, ref-pruning | **Inbound HTTPS webhook** | `[msteams]` |
| `mochat` | `claw_token`, `base_url`, `socket_url`, `sessions`, `panels` | Socket.IO | core |
| `signal` | `phoneNumber`, `daemonHost`/`daemonPort`, `dm`/`group` policies | signal-cli daemon (SSE in / JSON-RPC out) | signal-cli |
| `websocket` | see [§5.1](#51-websocket-channel--webui-transport) | WebSocket server | core |

Common `groupPolicy` values: `"mention"` (default — reply only when @mentioned), `"open"` (reply to all), `"allowlist"` (specific channels/rooms); Napcat additionally accepts a float probability. DMs always reply for allowed users.

### 5.1 WebSocket channel — WebUI transport

The WebSocket channel (`channels.websocket`) both serves the bundled WebUI and exposes a programmatic protocol. Key fields:

| Field | Default | Description |
|-------|---------|-------------|
| `enabled` | `false` | Enable the server |
| `host` | `"127.0.0.1"` | Bind address; `"0.0.0.0"` for external/Docker |
| `port` | `8765` | Listen port |
| `path` | `"/"` | Upgrade path (trailing slash normalized) |
| `maxMessageBytes` | `37748736` (36 MB) | Inbound size cap (1 KB–40 MB) |
| `token` | `""` | Static shared secret (timing-safe compare) |
| `websocketRequiresToken` | `true` | Require an issued token when no static `token` |
| `tokenIssuePath` / `tokenIssueSecret` / `tokenTtlS` | `""` / `""` / `300` | Short-lived token issuance |
| `allowFrom` | `["*"]` | Allowed `client_id` values |
| `streaming` | `true` | `delta`/`stream_end` frames vs single `message` |
| `pingIntervalS` / `pingTimeoutS` | `20.0` / `20.0` | Keep-alive (5–300) |
| `sslCertfile` / `sslKeyfile` | `""` / `""` | WSS (enforced TLSv1.2 minimum) |

> [!WARNING]
> When `host` is `0.0.0.0`, the channel **refuses to start** unless `token` or `tokenIssueSecret` is set. Treat the chat_id as a *capability*: any caller passing auth can attach to a chat_id it knows. nanobot's model is single-user/local; multi-tenant deployments must namespace chat_ids per user or add a per-tenant gate.

Protocol summary (all frames JSON with an `event` field): server emits `ready`, `message`, `delta`/`stream_end`, `reasoning_delta`/`reasoning_end`, `runtime_model_updated`, `attached`, `error`; clients send plain text / `{content}` (legacy default chat) or typed envelopes (`new_chat`, `attach`, `message`) for multi-chat multiplexing. `chat_id` format: `^[A-Za-z0-9_:-]{1,64}$`.

---

## 6. Provider Reference

nanobot supports ~40 providers. Set `providers.<name>.apiKey` (prefer `${VAR}`) and an `agents.defaults.model`. `provider: "auto"` detects the provider from the model name keywords and configured keys; pinning the provider is clearer.

### 6.1 Provider categories

| Category | Providers |
|----------|-----------|
| **Gateways** (any model, one key) | `openrouter`, `aihubmix`, `siliconflow`, `novita`, `skywork` |
| **Direct frontier** | `anthropic`, `openai`, `gemini`, `deepseek`, `groq`, `mistral`, `moonshot` (Kimi), `zhipu` (GLM), `dashscope` (Qwen), `minimax`, `minimax_anthropic`, `stepfun`, `longcat`, `ant_ling`, `mimo`/`xiaomi_mimo`, `qianfan`, `volcengine`/`byteplus` (+ coding-plan variants) |
| **Cloud** | `azure_openai` (API key or Entra ID), `bedrock` (Converse API) |
| **OAuth** | `openai_codex`, `github_copilot` (login via `nanobot provider login <name>`) |
| **Local / self-hosted** | `ollama`, `lm_studio`, `vllm`, `ovms` (OpenVINO), `atomic_chat`, `custom` (any OpenAI-compatible) |

### 6.2 Notable provider behaviors

- **OpenAI** defaults to `apiType: "auto"` — Chat Completions normally, routing GPT-5/o-series or explicit `reasoningEffort` to the Responses API. Force with `apiType` ∈ `auto`/`chat_completions`/`responses`.
- **Azure OpenAI** talks via the Responses API; model names map to **deployment** names; supports static key or `DefaultAzureCredential` (Entra ID) — the signing identity needs the *Cognitive Services OpenAI User* role. `apiBase` is mandatory. Install `[azure]`.
- **Bedrock** uses native `bedrock-runtime` Converse. Model IDs use a `bedrock/` prefix (stripped before the AWS call), e.g. `bedrock/global.anthropic.claude-opus-4-7`. Standard AWS credential chain or a Bedrock API key (exported as `AWS_BEARER_TOKEN_BEDROCK`). Claude Opus 4.7 rejects `temperature`/`top_p`/`top_k` (auto-omitted).
- **Custom vs Azure**: chat-completions-compatible endpoints → `custom`; Responses-API-compatible endpoints → `azure_openai` shape.
- **`extraBody`** is merged into the request body for almost every provider (vLLM guided decoding, Bedrock `additionalModelRequestFields`, OpenAI Responses tools/include, etc.).

### 6.3 ProviderSpec options (for `registry.py`)

`default_api_base`, `keywords` (auto-match), `env_key`, `env_extras`, `model_overrides`, `is_gateway`, `detect_by_key_prefix`, `detect_by_base_keyword`, `strip_model_prefix`, `supports_max_completion_tokens`. See [§9.2](#92-add-a-new-provider).

---

## 7. Security

> [!CAUTION]
> **The headline admin concern.** In autonomous mode nanobot is an LLM driving file I/O, shell execution, and web access **on the host**. The host's trust boundary *is* the system's. Treat the deployment host as if the model (and anyone who can message it) can run code on it — because, by design, it can.

### 7.1 Threat model

- **Shell execution (`exec`)** is in the default tool set. It blocks obvious destructive patterns (`rm -rf /`, fork bombs, `mkfs.*`, raw disk writes) but pattern-blocking is **not** a sandbox. A capable model — or a prompt-injected one — can do real damage outside an allow-list.
- **Filesystem tools** can read/write anywhere allowed; without `restrictToWorkspace` that can include sensitive files.
- **Web fetch** is an SSRF vector; nanobot mitigates this (below) but admins must understand the escape hatch.
- **Prompt injection persistence:** anything written to memory/session/history can be **replayed** into future LLM calls. A malicious instruction captured into memory can resurface later. Bound and sanitize what enters memory.
- **Channel exposure:** a misconfigured `allowFrom` (or `0.0.0.0` bind without a token) puts an OS-capable agent on the open network.

### 7.2 Built-in guards (and their limits)

| Guard | Mechanism | Limit |
|-------|-----------|-------|
| **Workspace restriction** | `_resolve_path` in `filesystem.py` confines file tools to `allowed_dir` (+ media + `extra_allowed_dirs`); `ExecTool` rejects out-of-workspace `working_dir` | Off by default (`restrictToWorkspace: false`) |
| **SSRF protection** | `validate_url_target` in `security/network.py` blocks RFC1918, link-local, and cloud metadata (`169.254.169.254`) | Escape hatch: `tools.ssrfWhitelist` CIDRs |
| **Shell sandbox** | `tools/sandbox.py` `bwrap` backend: workspace RW, media RO, system RO, config/keys hidden via tmpfs | **Linux only**; no-op on macOS/Windows/bare-metal-without-bwrap |
| **Command pattern blocklist** | Dangerous-pattern detection in `shell.py` | Blocklist, not allowlist — obfuscation can evade |
| **Access control** | `allowFrom` allow-list + pairing | Empty `allowFrom` denies all (since v0.1.4.post4) |
| **WebSocket auth** | static token (timing-safe `hmac.compare_digest`) or single-use issued tokens; allow-list checked at handshake + message; TLSv1.2 min | chat_id is a capability; not multi-tenant |
| **`my` tool** | sensitive-field and security-boundary blocklist; in-memory only | — |
| **Resource caps** | exec timeout (60s), output truncation (~10KB), HTTP timeouts (10–30s) | — |

### 7.3 Hardening recipe (production)

```json
{
  "tools": {
    "restrictToWorkspace": true,
    "exec": { "sandbox": "bwrap" }
  }
}
```

Plus, at the OS/deployment layer:

- [ ] Run as a **dedicated non-root user** (`useradd -m nanobot`); never run as root.
- [ ] Set file permissions: `chmod 700 ~/.nanobot`, `chmod 600 ~/.nanobot/config.json`, `chmod 700 ~/.nanobot/whatsapp-auth`.
- [ ] Enable the **bwrap sandbox** on Linux for kernel-namespace isolation of shell commands.
- [ ] Keep secrets out of `config.json` via `${VAR}` + a `0600` env file / secret manager.
- [ ] Configure `allowFrom` on **every** channel; never expose `0.0.0.0` without a token.
- [ ] Run in a **container or VM** for an extra isolation layer; restrict outbound egress if feasible.
- [ ] Set **spending limits** on LLM provider keys (no built-in rate limiting).
- [ ] Monitor logs for `Access denied` and `Failed to send to {channel} after N attempts`.
- [ ] Security-review any custom skills/tools/MCP servers — installing them is arbitrary code execution.

### 7.4 Known security limitations (from `SECURITY.md`)

1. **No rate limiting** — add your own if needed.
2. **Plain-text config** — keys are stored in plain text; use `${VAR}`/keyring.
3. **No session expiry** management.
4. **Blocklist-only command filtering** — enable bwrap for real isolation.
5. **Limited audit trail** — enhance logging as needed.

### 7.5 WhatsApp bridge security

The bridge binds to `127.0.0.1:3001` (localhost only). Set `bridgeToken` for shared-secret auth between the Python and Node processes; keep `~/.nanobot/whatsapp-auth` at mode `0700`.

### 7.6 Reporting vulnerabilities

Do **not** open a public issue. Use a private GitHub security advisory or email `[email removed]`. Stated response target: 48 hours.

---

## 8. Operations & Deployment

### 8.1 Ports

| Port | Service | Bind default |
|------|---------|--------------|
| 18790 | Gateway health endpoint (`GET /health` → `{"status":"ok"}`) | `127.0.0.1` |
| 8765 | WebSocket channel / WebUI | `127.0.0.1` |
| 8900 | OpenAI-compatible API (`nanobot serve`) | `127.0.0.1` |
| 3001 | WhatsApp bridge (internal) | `127.0.0.1` |

> [!IMPORTANT]
> Docker `-p` forwarding cannot reach a container's loopback. To expose the WebUI/ports from a container or to the LAN, set the relevant `host` to `0.0.0.0` in config **and** add authentication.

### 8.2 Docker

The official image is built from the repo `Dockerfile` (uv + Python 3.12 base; installs Node 20 + bubblewrap; runs as non-root `nanobot` UID 1000; entrypoint validates `~/.nanobot` is writable). Compose drops all Linux capabilities except `SYS_ADMIN` (needed by bwrap user namespaces) with unconfined apparmor/seccomp.

```bash
docker compose run --rm nanobot-cli onboard   # first-time setup
vim ~/.nanobot/config.json                     # add API keys
docker compose up -d nanobot-gateway           # start gateway
docker compose logs -f nanobot-gateway         # logs
```

Manual `docker run` (mirroring compose caps) for the gateway:

```bash
docker run \
  --cap-drop ALL --cap-add SYS_ADMIN \
  --security-opt apparmor=unconfined --security-opt seccomp=unconfined \
  -v ~/.nanobot:/home/nanobot/.nanobot \
  -p 18790:18790 -p 8765:8765 \
  nanobot gateway
```

> [!CAUTION]
> The `SYS_ADMIN` capability + unconfined apparmor/seccomp are required for the bwrap sandbox. They also widen the container's privileges — weigh this against your isolation needs. Third-party Docker Hub images are **not** maintained by HKUDS; don't mount keys into untrusted images.

Permissions: if you hit `Permission denied`, `sudo chown -R 1000:1000 ~/.nanobot` or pass `--user $(id -u):$(id -g)` (Podman `--userns=keep-id`). Always mount to `/home/nanobot/.nanobot`.

### 8.3 systemd (Linux user service)

```ini
# ~/.config/systemd/user/nanobot-gateway.service
[Unit]
Description=Nanobot Gateway
After=network.target
[Service]
Type=simple
ExecStart=%h/.local/bin/nanobot gateway
Restart=always
RestartSec=10
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=%h
[Install]
WantedBy=default.target
```

```bash
systemctl --user daemon-reload
systemctl --user enable --now nanobot-gateway
loginctl enable-linger $USER     # keep running after logout
journalctl --user -u nanobot-gateway -f
```

Use `EnvironmentFile=` (mode 600) to inject `${VAR}` secrets.

### 8.4 macOS LaunchAgent

Create `~/Library/LaunchAgents/ai.nanobot.gateway.plist` invoking the absolute `nanobot` path with `gateway` + `--workspace`, `RunAtLoad`, and `KeepAlive`. Manage with `launchctl bootstrap`/`kickstart`/`bootout` (full plist in `docs/deployment.md`). If startup fails with "address already in use", stop any manually started gateway first.

### 8.5 Multiple instances

`--config` selects the config file and derives the runtime dir (cron, media, state) from its location; `--workspace` (or `agents.defaults.workspace`) selects the workspace. Each concurrent instance needs a distinct port.

```bash
nanobot onboard --config ~/.nanobot-telegram/config.json --workspace ~/.nanobot-telegram/workspace
nanobot gateway --config ~/.nanobot-telegram/config.json
nanobot gateway --config ~/.nanobot-discord/config.json --port 18792
```

### 8.6 Backup & state

Everything is under `~/.nanobot/` (or the per-instance config directory): `config.json`, `workspace/` (`SOUL.md`, `USER.md`, `memory/` incl. the `.git` history, `sessions/`, `skills/`, `HEARTBEAT.md`), `media/`, `cron/`, and channel auth (`whatsapp-auth`, Matrix store, Weixin state). Back up the whole tree; memory's `GitStore` already versions the durable files.

### 8.7 Heartbeat (periodic tasks)

The running gateway wakes every ~30 minutes and runs any tasks under `## Active Tasks` in `~/.nanobot/workspace/HEARTBEAT.md`, delivering results to the most recently active channel. Empty task lists skip silently. (The legacy dedicated heartbeat service was removed; it now rides on `cron`.)

---

## 9. Extending nanobot

Channels, tools, and providers are all pluggable. Skills add capability as *know-how* rather than code.

### 9.1 Add a channel plugin

Subclass `BaseChannel`, register a `nanobot.channels` entry point, install, configure. Three rules that bite:

1. `start()` **must block forever** (or until `stop()` sets `_running = False`); if it returns, the channel is considered dead.
2. Call `self._handle_message(sender_id, chat_id, content, ...)` on each inbound message — it runs `is_allowed()` then publishes to the bus.
3. **Config must be a Pydantic model** subclassing `nanobot.config.schema.Base`, not a plain dict — `is_allowed()` reads `allow_from` via `getattr`, which silently returns `[]` (deny-all) for dicts.

```toml
[project.entry-points."nanobot.channels"]
webhook = "nanobot_channel_webhook:WebhookChannel"
```

Naming convention: PyPI `nanobot-channel-{name}`, entry-point key `{name}`, config section `channels.{name}`, package `nanobot_channel_{name}`. Verify with `nanobot plugins list`. Optional streaming via `send_delta`; optional reasoning via `send_reasoning_delta`/`send_reasoning_end`; interactive login via `login(force=False)` (QR flows).

### 9.2 Add a new provider

Two steps, no if-elif chains:

1. Add a `ProviderSpec(...)` to `PROVIDERS` in `nanobot/providers/registry.py` (`name`, `keywords`, `env_key`, `display_name`, `default_api_base`).
2. Add the field to `ProvidersConfig` in `nanobot/config/schema.py`.

Env vars, model routing, config matching, and `nanobot status` display then work automatically.

### 9.3 Skills

Built-in skills live in `nanobot/skills/` as markdown + YAML frontmatter (e.g. `long-goal`, `cron`, `github`, `image-generation`, `memory`, `my`, `skill-creator`, `summarize`, `tmux`, `weather`, `clawhub`, `update-setup`). Workspace skills go under `<workspace>/skills/`. Hide skills with `agents.defaults.disabledSkills`. External skills can be published to / installed from **ClawHub**. Add capability that is "know-how" as a skill, not hardcoded loop logic.

### 9.4 Prompt templates

System prompts and scenario instructions are Jinja2 markdown in `nanobot/templates/` (`identity.md`, `platform_policy.md`, `SOUL.md`, `HEARTBEAT.md`, …), loaded by `utils/prompt_templates.py`. **Editing them changes agent behavior as directly as code** — keep changes narrow and avoid teaching the model to echo internal markers, local paths, or tool-call text.

### 9.5 Python SDK

`Nanobot.from_config(config_path=None, *, workspace=None)` → `await bot.run(message, *, session_key="sdk:default", hooks=None)` → `RunResult(content, tools_used, messages)`. See User Guide §9.4 and the hook table in [§2.3](#23-the-agent-loop-react-and-hooks).

---

## 10. Developer Internals — Module Reference

| Module | Notes for a future maintainer |
|--------|-------------------------------|
| `nanobot/cli/commands.py` | Typer/Click app; the `nanobot = nanobot.cli.commands:app` entry point. Forces UTF-8 stdout/stderr on startup for emoji/multilingual input. Holds `onboard`, `agent`, `gateway`, `serve`, `status`, `provider login`, `channels login/status`, `plugins list`. |
| `nanobot/agent/loop.py` | `AgentLoop`: session resolution, hooks, context build, slash routing. |
| `nanobot/agent/runner.py` | `AgentRunner`: the ReAct loop; `NANOBOT_LLM_TIMEOUT_S` (300). |
| `nanobot/agent/memory.py` | History + Dream; atomic JSONL writes (temp+fsync+rename+dir-fsync). |
| `nanobot/agent/autocompact.py` | Idle auto-compaction logic. |
| `nanobot/agent/subagent.py` / `tools/spawn.py` | Subagent spawning + bounding. |
| `nanobot/providers/registry.py` / `factory.py` / `base.py` | Provider source of truth, instantiation, common client. |
| `nanobot/providers/fallback_provider.py` | Failover chain across `fallbackModels`. |
| `nanobot/channels/manager.py` / `base.py` | Discovery (pkgutil + entry points) + `BaseChannel`. |
| `nanobot/config/schema.py` / `loader.py` | Pydantic schema, camelCase aliases, `${VAR}` resolution (raises on missing). |
| `nanobot/security/network.py` | `validate_url_target` SSRF guard + `configure_ssrf_whitelist`. |
| `nanobot/agent/tools/sandbox.py` | bwrap wrapping; add backends as `_wrap_<name>` in `_BACKENDS`. |
| `nanobot/api/server.py` | OpenAI-compatible API (note: routes are not decorator-extractable; see below). |
| `bridge/src/` | TypeScript WhatsApp bridge (Baileys); `BRIDGE_PORT`/`AUTH_DIR`/`BRIDGE_TOKEN`. |
| `webui/` | React/Vite SPA; dev server proxies `/api`, `/webui`, `/auth`, WS to the gateway; build → `nanobot/web/dist/` bundled into the wheel. |

### 10.1 Runtime environment variables

| Variable | Default | Effect |
|----------|---------|--------|
| `NANOBOT_MAX_CONCURRENT_REQUESTS` | `3` | Max concurrent agent requests (`agent/loop.py`) |
| `NANOBOT_LLM_TIMEOUT_S` | `300` | LLM call timeout (`agent/runner.py`) |
| `NANOBOT_STREAM_IDLE_TIMEOUT_S` | `90` | Anthropic stream idle timeout |
| `NANOBOT_SKIP_WEBUI_BUILD` / `NANOBOT_FORCE_WEBUI_BUILD` | — | Control WebUI build during `pip install` (`hatch_build.py`) |
| Provider keys | — | `OPENAI_API_KEY`, `GROQ_API_KEY`, `AWS_REGION`/`AWS_DEFAULT_REGION`, `AWS_BEARER_TOKEN_BEDROCK`, `TAVILY_API_KEY`, `BRAVE_API_KEY`, `LANGFUSE_SECRET_KEY`, and per-provider `*_API_KEY` env keys defined in the registry |

> These are runtime/operational env vars; **secrets** should be referenced from `config.json` via `${VAR}` rather than relied on implicitly.

### 10.2 Windows specifics

`ExecTool` uses `cmd /c` (vs `sh -c`); MCP stdio commands are path-normalized; always `pathlib.Path`. Matrix is unavailable (no `python-olm` wheel); bwrap sandbox is Linux-only.

### 10.3 Extraction note for future automated re-docs

The AST extractor reported `api_endpoints = 0` even though `nanobot/api/server.py` exposes `/health`, `/v1/models`, and `/v1/chat/completions`. Those routes are not standard decorator routes the extractor recognizes — read the API source directly. Similarly, the click CLI surface is split across `nanobot/cli/commands.py` (and channel subcommands), and the real config surface is the Pydantic schema in `nanobot/config/schema.py`, not any env-scrape TSV. The repo's own `docs/` directory is the authoritative user spec.

---

## 11. Troubleshooting (Operations)

Community-reported issues map to the rows below (source: GitHub issues).

### Error: `Environment variable 'NAME' referenced in config is not set`
**Cause:** `${NAME}` placeholder with the variable unset; nanobot fails fast.
**Solution:** Provide the variable (systemd `EnvironmentFile`, Docker `--env-file`, direnv, secret manager) or inline the value.
**Prevention:** No default-value syntax exists — audit all `${VAR}` references.

### Gateway/WebUI unreachable from LAN or Docker
**Cause:** Default `127.0.0.1` bind; Docker `-p` can't reach loopback.
**Solution:** Set `gateway.host` and `channels.websocket.host` to `0.0.0.0` **and** add `token`/`tokenIssueSecret`.
**Prevention:** Pair every `0.0.0.0` bind with auth.

### `bwrap: clone3: Operation not permitted` in Docker
**Cause:** Container lacks `CAP_SYS_ADMIN` / has restrictive seccomp/apparmor for user namespaces.
**Solution:** Run with `--cap-add SYS_ADMIN --security-opt apparmor=unconfined --security-opt seccomp=unconfined` (as compose does).
**Prevention:** Use the provided `docker-compose.yml` caps when `exec.sandbox: "bwrap"` is on.

### All users denied on a channel
**Cause:** Empty `allowFrom` denies all (since v0.1.4.post4).
**Solution:** `["*"]` for all, or list IDs, or rely on pairing (DM only).

### WhatsApp broken after upgrade
**Cause:** Bridge not rebuilt on upgrade.
**Solution:** `rm -rf ~/.nanobot/bridge && nanobot channels login whatsapp`.

### Matrix fails to start on Windows
**Cause:** `python-olm` has no Windows wheel; `matrix` extra skips it on win32.
**Solution:** Use macOS/Linux/WSL2.

### Feishu streaming not working / CardKit errors
**Cause:** Missing `cardkit:card:write` permission.
**Solution:** Grant + publish a new app version, or set `channels.feishu.streaming: false`.

### MCP stdio server leaves a hang at shutdown
**Cause:** A known SDK issue ("exit cancel scope in a different task") for stdio MCP at shutdown (GitHub #4211).
**Solution:** Prefer HTTP MCP transports for long-lived servers where possible; track upstream.

### Tool results / messages dropped after a turn
**Cause:** Edge cases in legal-message-start detection / browser-refresh history loss (GitHub #4203, #4200).
**Solution:** Keep on the latest release; report reproductions.

### Permission denied on `~/.nanobot` (Docker)
**Cause:** UID mismatch with the `nanobot` user (1000).
**Solution:** `chown -R 1000:1000 ~/.nanobot` or `--user $(id -u):$(id -g)`.

---

## 12. Potential Improvements & Tech Debt

Observed from the source — candidates for hardening:

- **No built-in rate limiting** — an OS-capable agent with `allowFrom: ["*"]` and no per-user throttle is exposed to abuse. Add a reverse-proxy rate limit or per-channel quota.
- **Blocklist-only command filtering** — the `exec` denylist is bypassable; the only real isolation is bwrap (Linux). A first-class allow-list mode and non-Linux sandbox backend would close the macOS/Windows gap (`sandbox.py` `_BACKENDS` is designed for this).
- **Plain-text secrets in config** — `${VAR}` mitigates but a native keyring backend is listed as a recommendation, not a feature.
- **Destructive idle auto-compact** — the in-place session rewrite is irreversible for the structured trail; an opt-in archival mode that preserves raw turns would help auditing.
- **chat_id-as-capability** — fine for single-user; multi-tenant deployments need per-user namespacing or a per-tenant auth gate before exposure.
- **Reasoning rendering coverage** — `showReasoning` only renders on CLI + WebUI today; chat channels are no-op until adapted.
- **`dream.modelOverride`** is reserved but not implemented; `maxBatchSize`/`maxIterations` are accepted-but-ignored — schema clarity debt.

---

## 13. Quick Reference

### Service commands
| Command | Purpose |
|---------|---------|
| `nanobot onboard [--wizard] [-c <cfg>] [-w <ws>]` | Init/refresh config + workspace |
| `nanobot agent [-m] [-c] [-w] [--no-markdown] [--logs]` | Local chat |
| `nanobot gateway [-c] [-w] [--port]` | Channels + WebUI service |
| `nanobot serve [--host] [-w]` | OpenAI-compatible API |
| `nanobot status` | Status |
| `nanobot provider login <name>` | OAuth (Codex, Copilot) |
| `nanobot channels login <ch> [--force]` | Interactive channel login |
| `nanobot channels status` | Channel status |
| `nanobot plugins list` | List built-in + plugin channels |

### Filesystem layout (`~/.nanobot/`)
```text
config.json                  # the control plane
workspace/
  SOUL.md  USER.md  HEARTBEAT.md
  memory/  (MEMORY.md, history.jsonl, .cursor, .dream_cursor, .git/)
  sessions/<key>.jsonl
  skills/
media/  (generated/YYYY-MM-DD/...)
cron/
bridge/  whatsapp-auth/  (channel auth & state)
logs/
```

### Default ports
| 18790 | gateway health · 8765 | WebUI/WS · 8900 | API · 3001 | WhatsApp bridge (internal) |

### Security defaults to change for production
`tools.restrictToWorkspace: true` · `tools.exec.sandbox: "bwrap"` (Linux) · `allowFrom` on every channel · token on any `0.0.0.0` bind · non-root user · `chmod 600 config.json`.

---

_Generated June 2026 from HKUDS/nanobot at 6a0368b. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
