Jesse A. Green / notebook

nanobot Admin & Architecture Guide

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.

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.
  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:

                 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). 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

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:

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 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.


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)

{
  "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.

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:

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)

# ~/.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
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.

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.
[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.


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/)

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.