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:
- 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.
- All state lives on disk under
~/.nanobot/. There is no database server; config, workspace, memory, sessions, media, and channel auth are files. - 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
- Channels receive a platform message, check the allow-list, and publish an
InboundMessage. AgentLoopconsumes it, resolves the session, builds context, and coordinates the turn (and intercepts slash commands via the command router).AgentRunnerruns the multi-turn LLM conversation: send messages to the provider, receive tool calls, execute tools, stream responses.- Results are published as
OutboundMessageevents and the originating channel'ssend()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_cursorplus the durable filesSOUL.md,USER.md,memory/MEMORY.md, and makes surgical single-pass edits, version-controlled by aGitStore(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
idleCompactAfterMinutesat its default0so 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+anthropicSDKs. 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_configraises 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
hostis0.0.0.0, the channel refuses to start unlesstokenortokenIssueSecretis 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 explicitreasoningEffortto the Responses API. Force withapiType∈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.apiBaseis mandatory. Install[azure]. - Bedrock uses native
bedrock-runtimeConverse. Model IDs use abedrock/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 asAWS_BEARER_TOKEN_BEDROCK). Claude Opus 4.7 rejectstemperature/top_p/top_k(auto-omitted). - Custom vs Azure: chat-completions-compatible endpoints →
custom; Responses-API-compatible endpoints →azure_openaishape. extraBodyis merged into the request body for almost every provider (vLLM guided decoding, BedrockadditionalModelRequestFields, 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
restrictToWorkspacethat 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(or0.0.0.0bind 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.jsonvia${VAR}+ a0600env file / secret manager. - [ ] Configure
allowFromon every channel; never expose0.0.0.0without 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 deniedandFailed 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)
- No rate limiting — add your own if needed.
- Plain-text config — keys are stored in plain text; use
${VAR}/keyring. - No session expiry management.
- Blocklist-only command filtering — enable bwrap for real isolation.
- 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
-pforwarding cannot reach a container's loopback. To expose the WebUI/ports from a container or to the LAN, set the relevanthostto0.0.0.0in 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_ADMINcapability + 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:
start()must block forever (or untilstop()sets_running = False); if it returns, the channel is considered dead.- Call
self._handle_message(sender_id, chat_id, content, ...)on each inbound message — it runsis_allowed()then publishes to the bus. - Config must be a Pydantic model subclassing
nanobot.config.schema.Base, not a plain dict —is_allowed()readsallow_fromviagetattr, 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:
- Add a
ProviderSpec(...)toPROVIDERSinnanobot/providers/registry.py(name,keywords,env_key,display_name,default_api_base). - Add the field to
ProvidersConfiginnanobot/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.jsonvia${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
execdenylist 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_BACKENDSis 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 —
showReasoningonly renders on CLI + WebUI today; chat channels are no-op until adapted. dream.modelOverrideis reserved but not implemented;maxBatchSize/maxIterationsare 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.