mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f896caefa0 | |||
| 55c003c5f4 | |||
| 20f2bae5cd | |||
| d6b82e6473 | |||
| fea5ac5f2c | |||
| 2b86bbef19 | |||
| 9dc0a7e62f | |||
| 2035033397 | |||
| 1c294ff9a5 | |||
| 43198310e1 | |||
| b7d6eebf4d | |||
| 3906104960 |
@@ -80,7 +80,7 @@ See [docs/v1-to-v2-changes.md](docs/v1-to-v2-changes.md) for what's different an
|
||||
- **Per-agent workspace** — each agent group has its own `CLAUDE.md`, its own memory, its own container, and only the mounts you allow. Nothing crosses the boundary unless you wire it to.
|
||||
- **Scheduled tasks** — recurring jobs that run Claude and can message you back
|
||||
- **Web access** — search and fetch content from the web
|
||||
- **Container isolation** — agents are sandboxed in Docker (macOS/Linux/WSL2), with optional [Docker Sandboxes](docs/docker-sandboxes.md) micro-VM isolation or Apple Container as a macOS-native opt-in
|
||||
- **Container isolation** — agents are sandboxed in Docker (macOS/Linux/WSL2), with optional Docker Sandboxes micro-VM isolation
|
||||
- **Credential security** — agents never hold raw API keys. Outbound requests route through [OneCLI's Agent Vault](https://github.com/onecli/onecli), which injects credentials at request time and enforces per-agent policies and rate limits.
|
||||
- **Agent templates**: stamp a ready-to-run agent (instructions + MCP tools + skills, no secrets) from a reusable bundle, via the setup wizard or `ncl groups create --template <ref>`. Load from the [public library](https://github.com/nanocoai/nanoclaw-templates), a local folder, or any git repo. See [docs/templates.md](docs/templates.md).
|
||||
|
||||
@@ -165,7 +165,7 @@ Key files:
|
||||
|
||||
**Why Docker?**
|
||||
|
||||
Docker provides cross-platform support (macOS, Linux and Windows via WSL2) and a mature ecosystem. On macOS, Apple Container is also supported as a lighter-weight native runtime. For additional isolation, [Docker Sandboxes](docs/docker-sandboxes.md) run each container inside a micro VM.
|
||||
Docker provides cross-platform support (macOS, Linux and Windows via WSL2) and a mature ecosystem. For additional isolation, Docker Sandboxes run each container inside a micro VM.
|
||||
|
||||
**Can I run this on Linux or Windows?**
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# Apple Container Networking Setup (macOS 26)
|
||||
|
||||
Apple Container's vmnet networking requires manual configuration for containers to access the internet. Without this, containers can communicate with the host but cannot reach external services (DNS, HTTPS, APIs).
|
||||
|
||||
## Quick Setup
|
||||
|
||||
Run these two commands (requires `sudo`):
|
||||
|
||||
```bash
|
||||
# 1. Enable IP forwarding so the host routes container traffic
|
||||
sudo sysctl -w net.inet.ip.forwarding=1
|
||||
|
||||
# 2. Enable NAT so container traffic gets masqueraded through your internet interface
|
||||
echo "nat on en0 from 192.168.64.0/24 to any -> (en0)" | sudo pfctl -ef -
|
||||
```
|
||||
|
||||
> **Note:** Replace `en0` with your active internet interface. Check with: `route get 8.8.8.8 | grep interface`
|
||||
|
||||
## Making It Persistent
|
||||
|
||||
These settings reset on reboot. To make them permanent:
|
||||
|
||||
**IP Forwarding** — add to `/etc/sysctl.conf`:
|
||||
```
|
||||
net.inet.ip.forwarding=1
|
||||
```
|
||||
|
||||
**NAT Rules** — add to `/etc/pf.conf` (before any existing rules):
|
||||
```
|
||||
nat on en0 from 192.168.64.0/24 to any -> (en0)
|
||||
```
|
||||
|
||||
Then reload: `sudo pfctl -f /etc/pf.conf`
|
||||
|
||||
## IPv6 DNS Issue
|
||||
|
||||
By default, DNS resolvers return IPv6 (AAAA) records before IPv4 (A) records. Since our NAT only handles IPv4, Node.js applications inside containers will try IPv6 first and fail.
|
||||
|
||||
The container image and runner are configured to prefer IPv4 via:
|
||||
```
|
||||
NODE_OPTIONS=--dns-result-order=ipv4first
|
||||
```
|
||||
|
||||
This is set both in the `Dockerfile` and passed via `-e` flag in `container-runner.ts`.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Check IP forwarding is enabled
|
||||
sysctl net.inet.ip.forwarding
|
||||
# Expected: net.inet.ip.forwarding: 1
|
||||
|
||||
# Test container internet access
|
||||
container run --rm --entrypoint curl nanoclaw-agent:latest \
|
||||
-s4 --connect-timeout 5 -o /dev/null -w "%{http_code}" https://api.anthropic.com
|
||||
# Expected: 404
|
||||
|
||||
# Check bridge interface (only exists when a container is running)
|
||||
ifconfig bridge100
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `curl: (28) Connection timed out` | IP forwarding disabled | `sudo sysctl -w net.inet.ip.forwarding=1` |
|
||||
| HTTP works, HTTPS times out | IPv6 DNS resolution | Add `NODE_OPTIONS=--dns-result-order=ipv4first` |
|
||||
| `Could not resolve host` | DNS not forwarded | Check bridge100 exists, verify pfctl NAT rules |
|
||||
| Container hangs after output | Missing `process.exit(0)` in agent-runner | Rebuild container image |
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Container VM (192.168.64.x)
|
||||
│
|
||||
├── eth0 → gateway 192.168.64.1
|
||||
│
|
||||
bridge100 (192.168.64.1) ← host bridge, created by vmnet when container runs
|
||||
│
|
||||
├── IP forwarding (sysctl) routes packets from bridge100 → en0
|
||||
│
|
||||
├── NAT (pfctl) masquerades 192.168.64.0/24 → en0's IP
|
||||
│
|
||||
en0 (your WiFi/Ethernet) → Internet
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [apple/container#469](https://github.com/apple/container/issues/469) — No network from container on macOS 26
|
||||
- [apple/container#656](https://github.com/apple/container/issues/656) — Cannot access internet URLs during building
|
||||
@@ -9,5 +9,3 @@ The files in this directory are original design documents and developer referenc
|
||||
| [SPEC.md](SPEC.md) | [Architecture](https://docs.nanoclaw.dev/concepts/architecture) |
|
||||
| [SECURITY.md](SECURITY.md) | [Security model](https://docs.nanoclaw.dev/concepts/security) |
|
||||
| [REQUIREMENTS.md](REQUIREMENTS.md) | [Introduction](https://docs.nanoclaw.dev/introduction) |
|
||||
| [docker-sandboxes.md](docker-sandboxes.md) | [Docker Sandboxes](https://docs.nanoclaw.dev/advanced/docker-sandboxes) |
|
||||
| [APPLE-CONTAINER-NETWORKING.md](APPLE-CONTAINER-NETWORKING.md) | [Container runtime](https://docs.nanoclaw.dev/advanced/container-runtime) |
|
||||
|
||||
+102
-60
@@ -2,69 +2,101 @@
|
||||
|
||||
## Trust Model
|
||||
|
||||
Privilege is **user-level**, persisted in the `user_roles` table (owner /
|
||||
admin, global or scoped to an agent group) plus `agent_group_members` (the
|
||||
unprivileged access gate).
|
||||
|
||||
| Entity | Trust Level | Rationale |
|
||||
|--------|-------------|-----------|
|
||||
| Main group | Trusted | Private self-chat, admin control |
|
||||
| Non-main groups | Untrusted | Other users may be malicious |
|
||||
| Container agents | Sandboxed | Isolated execution environment |
|
||||
| Incoming messages | User input | Potential prompt injection |
|
||||
| Owners / admins (`user_roles`) | Trusted | Hold owner/admin roles; gate admin commands and approve credentialed actions |
|
||||
| Group members (`agent_group_members`) | Access-gated | Membership grants access to an agent group, but their messages are still untrusted input |
|
||||
| Unregistered senders | Untrusted | Subject to each messaging group's `unknown_sender_policy` |
|
||||
| Agent containers | Sandboxed | Long-lived per-session container; isolated by mounts, non-root, no host reach |
|
||||
| Incoming messages | User input | Potential prompt injection regardless of who sent them |
|
||||
|
||||
## Security Boundaries
|
||||
|
||||
### 1. Container Isolation (Primary Boundary)
|
||||
|
||||
Agents execute in containers (lightweight Linux VMs), providing:
|
||||
- **Process isolation** - Container processes cannot affect the host
|
||||
- **Filesystem isolation** - Only explicitly mounted directories are visible
|
||||
- **Non-root execution** - Runs as unprivileged `node` user (uid 1000)
|
||||
- **Ephemeral containers** - Fresh environment per invocation (`--rm`)
|
||||
Agents execute in containers (Docker), providing:
|
||||
- **Process isolation** — container processes cannot affect the host
|
||||
- **Filesystem isolation** — only explicitly mounted directories are visible
|
||||
- **Non-root execution** — runs as an unprivileged user (`node`, uid 1000, or the host uid remapped in)
|
||||
- **Per-session containers** — one long-lived container per session polls that session's DBs and handles many messages, then is torn down (`--rm`) when the session goes idle.
|
||||
|
||||
This is the primary security boundary. Rather than relying on application-level permission checks, the attack surface is limited by what's mounted.
|
||||
This is the primary security boundary. Rather than relying on application-level
|
||||
permission checks, the attack surface is limited by what's mounted.
|
||||
|
||||
### 2. Mount Security
|
||||
|
||||
**External Allowlist** - Mount permissions stored at `~/.config/nanoclaw/mount-allowlist.json`, which is:
|
||||
- Outside project root
|
||||
- Never mounted into containers
|
||||
- Cannot be modified by agents
|
||||
`buildMounts` (`src/container-runner.ts`) composes a fixed set of mounts per
|
||||
spawn. For the default (Claude) provider these are:
|
||||
|
||||
**Default Blocked Patterns:**
|
||||
| Container path | Host source | Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/workspace` | `data/v2-sessions/<group>/<session>/` | RW | Session folder — `inbound.db`, `outbound.db`, `outbox/`, `.claude/` |
|
||||
| `/workspace/agent` | `groups/<folder>/` | RW | Agent group working files + `CLAUDE.local.md` |
|
||||
| `/workspace/agent/container.json` | group `container.json` | RO | Container config — readable, not writable |
|
||||
| `/workspace/agent/CLAUDE.md` | composed `CLAUDE.md` | RO | Regenerated every spawn; agent edits would be clobbered |
|
||||
| `/workspace/agent/.claude-fragments` | group `.claude-fragments/` | RO | Composer skill/MCP fragments |
|
||||
| `/workspace/global` | `groups/global/` | RO | Shared global memory |
|
||||
| `/app/CLAUDE.md` | `container/CLAUDE.md` | RO | Shared base doc imported by the composed entry point |
|
||||
| `/home/node/.claude` | `data/v2-sessions/<group>/.claude-shared/` | RW | Claude state, settings, skill symlinks |
|
||||
| `/app/src` | `container/agent-runner/src/` | RO | Shared agent-runner source (same for all groups) |
|
||||
| `/app/skills` | `container/skills/` | RO | Shared container skills |
|
||||
| `/workspace/extra/<name>` | allowlisted host dir | RO (RW only if allowed) | Operator-configured additional mounts |
|
||||
|
||||
The config mounts (`container.json`, `CLAUDE.md`, `.claude-fragments`) are
|
||||
**nested read-only mounts on top of the read-write group dir** — the agent can
|
||||
read its config but cannot modify it. The project root is **never mounted**: the
|
||||
container only ever sees the paths above plus any provider-contributed mounts
|
||||
(e.g. an OpenCode XDG dir). Host application source (`src/`, `dist/`,
|
||||
`package.json`) is not reachable.
|
||||
|
||||
**Additional-mount allowlist** — extra mounts from a group's container config
|
||||
are validated against an allowlist at `~/.config/nanoclaw/mount-allowlist.json`,
|
||||
which is:
|
||||
- Outside the project root
|
||||
- Never mounted into containers
|
||||
- Not modifiable by agents
|
||||
|
||||
Its schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"allowedRoots": [
|
||||
{ "path": "~/projects", "allowReadWrite": true, "description": "Dev projects" },
|
||||
{ "path": "~/Documents/work", "allowReadWrite": false, "description": "Read-only" }
|
||||
],
|
||||
"blockedPatterns": ["password", "secret", "token"]
|
||||
}
|
||||
```
|
||||
.ssh, .gnupg, .aws, .azure, .gcloud, .kube, .docker,
|
||||
credentials, .env, .netrc, .npmrc, id_rsa, id_ed25519,
|
||||
|
||||
**Default blocked patterns** (merged with any in the file):
|
||||
```
|
||||
.ssh, .gnupg, .gpg, .aws, .azure, .gcloud, .kube, .docker,
|
||||
credentials, .env, .netrc, .npmrc, .pypirc, id_rsa, id_ed25519,
|
||||
private_key, .secret
|
||||
```
|
||||
|
||||
**Protections:**
|
||||
- Symlink resolution before validation (prevents traversal attacks)
|
||||
- Container path validation (rejects `..` and absolute paths)
|
||||
- `nonMainReadOnly` option forces read-only for non-main groups
|
||||
|
||||
**Read-Only Project Root:**
|
||||
|
||||
The main group's project root is mounted read-only. Writable paths the agent needs (store, group folder, IPC, `.claude/`) are mounted separately. This prevents the agent from modifying host application code (`src/`, `dist/`, `package.json`, etc.) which would bypass the sandbox entirely on next restart. The `store/` directory is mounted read-write so the main agent can access the SQLite database directly.
|
||||
**Enforcement** (`src/modules/mount-security/index.ts`):
|
||||
- **No allowlist file ⇒ every additional mount is blocked** — the fixed mounts above are unaffected, but nothing extra is granted until the operator creates the file.
|
||||
- Symlinks are resolved to their real path (`realpathSync`) before any check, defeating traversal via symlink.
|
||||
- The real path is rejected if it matches a blocked pattern, and rejected unless it sits under one of `allowedRoots`.
|
||||
- The container path is validated: relative, non-empty, no `..`, no leading `/`, no `:` (blocks Docker `-v` option injection). It is mounted under `/workspace/extra/`.
|
||||
- **Read-write is granted only when the mount requests it (`readonly: false`) *and* the matched root has `allowReadWrite: true`.** Otherwise the mount is forced read-only.
|
||||
|
||||
### 3. Session Isolation
|
||||
|
||||
Each group has isolated Claude sessions at `data/sessions/{group}/.claude/`:
|
||||
- Groups cannot see other groups' conversation history
|
||||
- Session data includes full message history and file contents read
|
||||
- Prevents cross-group information disclosure
|
||||
Per-session state lives under `data/v2-sessions/<agent-group>/<session>/`
|
||||
(`inbound.db`, `outbound.db`, `outbox/`, `.claude/`). Claude state
|
||||
(`.claude-shared`) and the working folder are scoped to the agent group, so:
|
||||
- Different agent groups cannot see each other's conversation history or files.
|
||||
- A group's sessions share that group's memory but keep separate message DBs.
|
||||
|
||||
### 4. IPC Authorization
|
||||
This prevents cross-group information disclosure.
|
||||
|
||||
Messages and task operations are verified against group identity:
|
||||
|
||||
| Operation | Main Group | Non-Main Group |
|
||||
|-----------|------------|----------------|
|
||||
| Send message to own chat | ✓ | ✓ |
|
||||
| Send message to other chats | ✓ | ✗ |
|
||||
| Schedule task for self | ✓ | ✓ |
|
||||
| Schedule task for others | ✓ | ✗ |
|
||||
| View all tasks | ✓ | Own only |
|
||||
| Manage other groups | ✓ | ✗ |
|
||||
|
||||
### 5. Credential Isolation (OneCLI Agent Vault)
|
||||
### 4. Credential Isolation (OneCLI Agent Vault)
|
||||
|
||||
Real API credentials **never enter containers**. NanoClaw uses [OneCLI's Agent Vault](https://github.com/onecli/onecli) to proxy outbound requests and inject credentials at the gateway level.
|
||||
|
||||
@@ -77,13 +109,12 @@ Real API credentials **never enter containers**. NanoClaw uses [OneCLI's Agent V
|
||||
**Per-agent policies:**
|
||||
Each NanoClaw group gets its own OneCLI agent identity. This allows different credential policies per group (e.g. your sales agent vs. support agent). OneCLI supports rate limits, and time-bound access and approval flows are on the roadmap.
|
||||
|
||||
**NOT Mounted:**
|
||||
- Channel auth sessions (`store/auth/`) — host only
|
||||
- Mount allowlist — external, never mounted
|
||||
- Any credentials matching blocked patterns
|
||||
- `.env` is shadowed with `/dev/null` in the project root mount
|
||||
**Never on the container filesystem:**
|
||||
- The project root and `.env` — never mounted; the container only receives the paths in the mount table above.
|
||||
- The mount allowlist — external (`~/.config/nanoclaw/…`), never mounted.
|
||||
- Real credentials — injected per request by the OneCLI gateway, never written into any mount.
|
||||
|
||||
### 6. Egress Lockdown (Forced Proxy)
|
||||
### 5. Egress Lockdown (Forced Proxy)
|
||||
|
||||
The `HTTPS_PROXY` env var only redirects *proxy-aware* clients — a tool that
|
||||
ignores it (or a raw socket) could reach the internet directly and bypass
|
||||
@@ -111,31 +142,42 @@ no `host-gateway` route).
|
||||
exception: a heal failure there is logged but not fatal, since already-running
|
||||
agents stay on the internal net (no leak) until the gateway returns.
|
||||
|
||||
**Default: egress is open.** Lockdown is **off** unless you opt in; by default
|
||||
the agent reaches the OneCLI gateway over the host-gateway path and outbound
|
||||
traffic is not confined to the internal network.
|
||||
|
||||
**Configuration:**
|
||||
|
||||
| Env | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `NANOCLAW_EGRESS_LOCKDOWN` | `false` | Set `true` to opt in (otherwise the host-gateway path is used). Enabled automatically by `/add-golden-registry`. |
|
||||
| `NANOCLAW_EGRESS_LOCKDOWN` | `false` | Set `true` to opt in (otherwise the host-gateway path is used). |
|
||||
| `NANOCLAW_EGRESS_NETWORK` | `nanoclaw-egress` | Network name. |
|
||||
| `ONECLI_GATEWAY_CONTAINER` | `onecli` | Gateway container to attach. |
|
||||
|
||||
These variables are read from the **host process** environment (the service's
|
||||
environment / `.env`), not from inside the container. The agent container is
|
||||
started with only `TZ` and any provider-declared variables — host environment
|
||||
variables, including secrets, are never forwarded into the agent.
|
||||
|
||||
**⚠ Behavior when enabled:** with lockdown on, agents have **no direct
|
||||
internet** — all traffic must go through OneCLI. Proxy-aware clients (npm, pnpm,
|
||||
pip, curl, node/bun with the proxy env) are unaffected. Any workflow that relies
|
||||
on a **non-proxy-aware** tool reaching the internet directly will fail by design.
|
||||
Lockdown is **off by default**; opt in with `NANOCLAW_EGRESS_LOCKDOWN=true`.
|
||||
|
||||
## Privilege Comparison
|
||||
## Resource Limits
|
||||
|
||||
| Capability | Main Group | Non-Main Group |
|
||||
|------------|------------|----------------|
|
||||
| Project root access | `/workspace/project` (ro) | None |
|
||||
| Store (SQLite DB) | `/workspace/project/store` (rw) | None |
|
||||
| Group folder | `/workspace/group` (rw) | `/workspace/group` (rw) |
|
||||
| Global memory | Implicit via project | `/workspace/global` (ro) |
|
||||
| Additional mounts | Configurable | Read-only unless allowed |
|
||||
| Network access | Unrestricted | Unrestricted |
|
||||
| MCP tools | All | All |
|
||||
Per-container CPU and memory caps are **opt-in and unset by default** — a runaway
|
||||
agent is not throttled unless the operator configures a limit:
|
||||
|
||||
| Env | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `CONTAINER_CPU_LIMIT` | *(empty — unbounded)* | Passed to `--cpus` when set (e.g. `2`). |
|
||||
| `CONTAINER_MEMORY_LIMIT` | *(empty — unbounded)* | Passed to `--memory` when set (e.g. `8g`). |
|
||||
|
||||
Only `--memory` is a container-level cap; whether it's a *hard* cap depends on
|
||||
the host having no swap (a deployment concern). On a swapless host a runaway is
|
||||
OOM-killed at the limit.
|
||||
|
||||
## Security Architecture Diagram
|
||||
|
||||
@@ -149,7 +191,7 @@ Lockdown is **off by default**; opt in with `NANOCLAW_EGRESS_LOCKDOWN=true`.
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ HOST PROCESS (TRUSTED) │
|
||||
│ • Message routing │
|
||||
│ • IPC authorization │
|
||||
│ • Role / access checks (user_roles, agent_group_members) │
|
||||
│ • Mount validation (external allowlist) │
|
||||
│ • Container lifecycle │
|
||||
│ • OneCLI Agent Vault (injects credentials, enforces policies) │
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
# Running NanoClaw in Docker Sandboxes (Manual Setup)
|
||||
|
||||
This guide walks through setting up NanoClaw inside a [Docker Sandbox](https://docs.docker.com/ai/sandboxes/) from scratch — no install script, no pre-built fork. You'll clone the upstream repo, apply the necessary patches, and have agents running in full hypervisor-level isolation.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Host (macOS / Windows WSL)
|
||||
└── Docker Sandbox (micro VM with isolated kernel)
|
||||
├── NanoClaw process (Node.js)
|
||||
│ ├── Channel adapters (WhatsApp, Telegram, etc.)
|
||||
│ └── Container spawner → nested Docker daemon
|
||||
└── Docker-in-Docker
|
||||
└── nanoclaw-agent containers
|
||||
└── Claude Agent SDK
|
||||
```
|
||||
|
||||
Each agent runs in its own container, inside a micro VM that is fully isolated from your host. Two layers of isolation: per-agent containers + the VM boundary.
|
||||
|
||||
The sandbox provides a MITM proxy at `host.docker.internal:3128` that handles network access and injects your Anthropic API key automatically.
|
||||
|
||||
> **Note:** This guide is based on a validated setup running on macOS (Apple Silicon) with WhatsApp. Other channels (Telegram, Slack, etc.) and environments (Windows WSL) may require additional proxy patches for their specific HTTP/WebSocket clients. The core patches (container runner, credential proxy, Dockerfile) apply universally — channel-specific proxy configuration varies.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Docker Desktop v4.40+** with Sandbox support
|
||||
- **Anthropic API key** (the sandbox proxy manages injection)
|
||||
- For **Telegram**: a bot token from [@BotFather](https://t.me/BotFather) and your chat ID
|
||||
- For **WhatsApp**: a phone with WhatsApp installed
|
||||
|
||||
Verify sandbox support:
|
||||
```bash
|
||||
docker sandbox version
|
||||
```
|
||||
|
||||
## Step 1: Create the Sandbox
|
||||
|
||||
On your host machine:
|
||||
|
||||
```bash
|
||||
# Create a workspace directory
|
||||
mkdir -p ~/nanoclaw-workspace
|
||||
|
||||
# Create a shell sandbox with the workspace mounted
|
||||
docker sandbox create shell ~/nanoclaw-workspace
|
||||
```
|
||||
|
||||
If you're using WhatsApp, configure proxy bypass so WhatsApp's Noise protocol isn't MITM-inspected:
|
||||
|
||||
```bash
|
||||
docker sandbox network proxy shell-nanoclaw-workspace \
|
||||
--bypass-host web.whatsapp.com \
|
||||
--bypass-host "*.whatsapp.com" \
|
||||
--bypass-host "*.whatsapp.net"
|
||||
```
|
||||
|
||||
Telegram does not need proxy bypass.
|
||||
|
||||
Enter the sandbox:
|
||||
```bash
|
||||
docker sandbox run shell-nanoclaw-workspace
|
||||
```
|
||||
|
||||
## Step 2: Install Prerequisites
|
||||
|
||||
Inside the sandbox:
|
||||
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install -y build-essential python3
|
||||
npm config set strict-ssl false
|
||||
```
|
||||
|
||||
## Step 3: Clone and Install NanoClaw
|
||||
|
||||
NanoClaw must live inside the workspace directory — Docker-in-Docker can only bind-mount from the shared workspace path.
|
||||
|
||||
```bash
|
||||
# Clone to home first (virtiofs can corrupt git pack files during clone)
|
||||
cd ~
|
||||
git clone https://github.com/nanocoai/nanoclaw.git
|
||||
|
||||
# Replace with YOUR workspace path (the host path you passed to `docker sandbox create`)
|
||||
WORKSPACE=/Users/you/nanoclaw-workspace
|
||||
|
||||
# Move into workspace so DinD mounts work
|
||||
mv nanoclaw "$WORKSPACE/nanoclaw"
|
||||
cd "$WORKSPACE/nanoclaw"
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
pnpm install https-proxy-agent
|
||||
```
|
||||
|
||||
## Step 4: Apply Proxy and Sandbox Patches
|
||||
|
||||
NanoClaw needs several patches to work inside a Docker Sandbox. These handle proxy routing, CA certificates, and Docker-in-Docker mount restrictions.
|
||||
|
||||
### 4a. Dockerfile — proxy args for container image build
|
||||
|
||||
`pnpm install` inside `docker build` fails with `SELF_SIGNED_CERT_IN_CHAIN` because the sandbox's MITM proxy presents its own certificate. Add proxy build args to `container/Dockerfile`:
|
||||
|
||||
Add these lines after the `FROM` line:
|
||||
|
||||
```dockerfile
|
||||
# Accept proxy build args
|
||||
ARG http_proxy
|
||||
ARG https_proxy
|
||||
ARG no_proxy
|
||||
ARG NODE_EXTRA_CA_CERTS
|
||||
ARG npm_config_strict_ssl=true
|
||||
RUN npm config set strict-ssl ${npm_config_strict_ssl}
|
||||
```
|
||||
|
||||
And after the `RUN pnpm install` line:
|
||||
|
||||
```dockerfile
|
||||
RUN npm config set strict-ssl true
|
||||
```
|
||||
|
||||
### 4b. Build script — forward proxy args
|
||||
|
||||
Patch `container/build.sh` to pass proxy env vars to `docker build`:
|
||||
|
||||
Add these `--build-arg` flags to the `docker build` command:
|
||||
|
||||
```bash
|
||||
--build-arg http_proxy="${http_proxy:-$HTTP_PROXY}" \
|
||||
--build-arg https_proxy="${https_proxy:-$HTTPS_PROXY}" \
|
||||
--build-arg no_proxy="${no_proxy:-$NO_PROXY}" \
|
||||
--build-arg npm_config_strict_ssl=false \
|
||||
```
|
||||
|
||||
### 4c. Container runner — proxy forwarding, CA cert mount, /dev/null fix
|
||||
|
||||
Three changes to `src/container-runner.ts`:
|
||||
|
||||
**Replace `/dev/null` shadow mount.** The sandbox rejects `/dev/null` bind mounts. Find where `.env` is shadow-mounted to `/dev/null` and replace it with an empty file:
|
||||
|
||||
```typescript
|
||||
// Create an empty file to shadow .env (Docker Sandbox rejects /dev/null mounts)
|
||||
const emptyEnvPath = path.join(DATA_DIR, 'empty-env');
|
||||
if (!fs.existsSync(emptyEnvPath)) fs.writeFileSync(emptyEnvPath, '');
|
||||
// Use emptyEnvPath instead of '/dev/null' in the mount
|
||||
```
|
||||
|
||||
**Forward proxy env vars** to spawned agent containers. Add `-e` flags for `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` and their lowercase variants.
|
||||
|
||||
**Mount CA certificate.** If `NODE_EXTRA_CA_CERTS` or `SSL_CERT_FILE` is set, copy the cert into the project directory and mount it into agent containers:
|
||||
|
||||
```typescript
|
||||
const caCertSrc = process.env.NODE_EXTRA_CA_CERTS || process.env.SSL_CERT_FILE;
|
||||
if (caCertSrc) {
|
||||
const certDir = path.join(DATA_DIR, 'ca-cert');
|
||||
fs.mkdirSync(certDir, { recursive: true });
|
||||
fs.copyFileSync(caCertSrc, path.join(certDir, 'proxy-ca.crt'));
|
||||
// Mount: certDir -> /workspace/ca-cert (read-only)
|
||||
// Set NODE_EXTRA_CA_CERTS=/workspace/ca-cert/proxy-ca.crt in the container
|
||||
}
|
||||
```
|
||||
|
||||
### 4d. Container runtime — prevent self-termination
|
||||
|
||||
In `src/container-runtime.ts`, the `cleanupOrphans()` function matches containers by the `nanoclaw-` prefix. Inside a sandbox, the sandbox container itself may match (e.g., `nanoclaw-docker-sandbox`). Filter out the current hostname:
|
||||
|
||||
```typescript
|
||||
// In cleanupOrphans(), filter out os.hostname() from the list of containers to stop
|
||||
```
|
||||
|
||||
### 4e. Credential proxy — route through MITM proxy
|
||||
|
||||
In `src/credential-proxy.ts`, upstream API requests need to go through the sandbox proxy. Add `HttpsProxyAgent` to outbound requests:
|
||||
|
||||
```typescript
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
|
||||
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||
const upstreamAgent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;
|
||||
// Pass upstreamAgent to https.request() options
|
||||
```
|
||||
|
||||
### 4f. Setup script — proxy build args
|
||||
|
||||
Patch `setup/container.ts` to pass the same proxy `--build-arg` flags as `build.sh` (Step 4b).
|
||||
|
||||
## Step 5: Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
bash container/build.sh
|
||||
```
|
||||
|
||||
## Step 6: Add a Channel
|
||||
|
||||
### Telegram
|
||||
|
||||
```bash
|
||||
# Apply the Telegram skill
|
||||
pnpm exec tsx scripts/apply-skill.ts .claude/skills/add-telegram
|
||||
|
||||
# Rebuild after applying the skill
|
||||
pnpm run build
|
||||
|
||||
# Configure .env
|
||||
cat > .env << EOF
|
||||
TELEGRAM_BOT_TOKEN=<your-token-from-botfather>
|
||||
ASSISTANT_NAME=nanoclaw
|
||||
ANTHROPIC_API_KEY=proxy-managed
|
||||
EOF
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
|
||||
# Register your chat
|
||||
pnpm exec tsx setup/index.ts --step register \
|
||||
--jid "tg:<your-chat-id>" \
|
||||
--name "My Chat" \
|
||||
--trigger "@nanoclaw" \
|
||||
--folder "telegram_main" \
|
||||
--channel telegram \
|
||||
--assistant-name "nanoclaw" \
|
||||
--is-main \
|
||||
--no-trigger-required
|
||||
```
|
||||
|
||||
**To find your chat ID:** Send any message to your bot, then:
|
||||
```bash
|
||||
curl -s --proxy $HTTPS_PROXY "https://api.telegram.org/bot<TOKEN>/getUpdates" | python3 -m json.tool
|
||||
```
|
||||
|
||||
**Telegram in groups:** Disable Group Privacy in @BotFather (`/mybots` > Bot Settings > Group Privacy > Turn off), then remove and re-add the bot.
|
||||
|
||||
**Important:** If the Telegram skill creates `src/channels/telegram.ts`, you'll need to patch it for proxy support. Add an `HttpsProxyAgent` and pass it to grammy's `Bot` constructor via `baseFetchConfig.agent`. Then rebuild.
|
||||
|
||||
### WhatsApp
|
||||
|
||||
Make sure you configured proxy bypass in [Step 1](#step-1-create-the-sandbox) first.
|
||||
|
||||
```bash
|
||||
# Apply the WhatsApp skill
|
||||
pnpm exec tsx scripts/apply-skill.ts .claude/skills/add-whatsapp
|
||||
|
||||
# Rebuild
|
||||
pnpm run build
|
||||
|
||||
# Configure .env
|
||||
cat > .env << EOF
|
||||
ASSISTANT_NAME=nanoclaw
|
||||
ANTHROPIC_API_KEY=proxy-managed
|
||||
EOF
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
|
||||
# Authenticate (choose one):
|
||||
|
||||
# QR code — scan with WhatsApp camera:
|
||||
pnpm exec tsx src/whatsapp-auth.ts
|
||||
|
||||
# OR pairing code — enter code in WhatsApp > Linked Devices > Link with phone number:
|
||||
pnpm exec tsx src/whatsapp-auth.ts --pairing-code --phone <phone-number-no-plus>
|
||||
|
||||
# Register your chat (JID = your phone number + @s.whatsapp.net)
|
||||
pnpm exec tsx setup/index.ts --step register \
|
||||
--jid "<phone>@s.whatsapp.net" \
|
||||
--name "My Chat" \
|
||||
--trigger "@nanoclaw" \
|
||||
--folder "whatsapp_main" \
|
||||
--channel whatsapp \
|
||||
--assistant-name "nanoclaw" \
|
||||
--is-main \
|
||||
--no-trigger-required
|
||||
```
|
||||
|
||||
**Important:** The WhatsApp skill files (`src/channels/whatsapp.ts` and `src/whatsapp-auth.ts`) also need proxy patches — add `HttpsProxyAgent` for WebSocket connections and a proxy-aware version fetch. Then rebuild.
|
||||
|
||||
### Both Channels
|
||||
|
||||
Apply both skills, patch both for proxy support, combine the `.env` variables, and register each chat separately.
|
||||
|
||||
## Step 7: Run
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
You don't need to set `ANTHROPIC_API_KEY` manually. The sandbox proxy intercepts requests and replaces `proxy-managed` with your real key automatically.
|
||||
|
||||
## Networking Details
|
||||
|
||||
### How the proxy works
|
||||
|
||||
All traffic from the sandbox routes through the host proxy at `host.docker.internal:3128`:
|
||||
|
||||
```
|
||||
Agent container → DinD bridge → Sandbox VM → host.docker.internal:3128 → Host proxy → api.anthropic.com
|
||||
```
|
||||
|
||||
**"Bypass" does not mean traffic skips the proxy.** It means the proxy passes traffic through without MITM inspection. Node.js doesn't automatically use `HTTP_PROXY` env vars — you need explicit `HttpsProxyAgent` configuration in every HTTP/WebSocket client.
|
||||
|
||||
### Shared paths for DinD mounts
|
||||
|
||||
Only the workspace directory is available for Docker-in-Docker bind mounts. Paths outside the workspace fail with "path not shared":
|
||||
- `/dev/null` → replace with an empty file in the project dir
|
||||
- `/usr/local/share/ca-certificates/` → copy cert to project dir
|
||||
- `/home/agent/` → clone to workspace instead
|
||||
|
||||
### Git clone and virtiofs
|
||||
|
||||
The workspace is mounted via virtiofs. Git's pack file handling can corrupt over virtiofs during clone. Workaround: clone to `/home/agent` first, then `mv` into the workspace.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### pnpm install fails with SELF_SIGNED_CERT_IN_CHAIN
|
||||
```bash
|
||||
npm config set strict-ssl false
|
||||
```
|
||||
|
||||
### Container build fails with proxy errors
|
||||
```bash
|
||||
docker build \
|
||||
--build-arg http_proxy=$http_proxy \
|
||||
--build-arg https_proxy=$https_proxy \
|
||||
-t nanoclaw-agent:latest container/
|
||||
```
|
||||
|
||||
### Agent containers fail with "path not shared"
|
||||
All bind-mounted paths must be under the workspace directory. Check:
|
||||
- Is NanoClaw cloned into the workspace? (not `/home/agent/`)
|
||||
- Is the CA cert copied to the project root?
|
||||
- Has the empty `.env` shadow file been created?
|
||||
|
||||
### Agent containers can't reach Anthropic API
|
||||
Verify proxy env vars are forwarded to agent containers. Check container logs for `HTTP_PROXY=http://host.docker.internal:3128`.
|
||||
|
||||
### WhatsApp error 405
|
||||
The version fetch is returning a stale version. Make sure the proxy-aware `fetchWaVersionViaProxy` patch is applied — it fetches `sw.js` through `HttpsProxyAgent` and parses `client_revision`.
|
||||
|
||||
### WhatsApp "Connection failed" immediately
|
||||
Proxy bypass not configured. From the **host**, run:
|
||||
```bash
|
||||
docker sandbox network proxy <sandbox-name> \
|
||||
--bypass-host web.whatsapp.com \
|
||||
--bypass-host "*.whatsapp.com" \
|
||||
--bypass-host "*.whatsapp.net"
|
||||
```
|
||||
|
||||
### Telegram bot doesn't receive messages
|
||||
1. Check the grammy proxy patch is applied (look for `HttpsProxyAgent` in `src/channels/telegram.ts`)
|
||||
2. Check Group Privacy is disabled in @BotFather if using in groups
|
||||
|
||||
### Git clone fails with "inflate: data stream error"
|
||||
Clone to a non-workspace path first, then move:
|
||||
```bash
|
||||
cd ~ && git clone https://github.com/nanocoai/nanoclaw.git && mv nanoclaw /path/to/workspace/nanoclaw
|
||||
```
|
||||
|
||||
### WhatsApp QR code doesn't display
|
||||
Run the auth command interactively inside the sandbox (not piped through `docker sandbox exec`):
|
||||
```bash
|
||||
docker sandbox run shell-nanoclaw-workspace
|
||||
# Then inside:
|
||||
pnpm exec tsx src/whatsapp-auth.ts
|
||||
```
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nanoclaw",
|
||||
"version": "2.1.35",
|
||||
"version": "2.1.36",
|
||||
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
|
||||
Reference in New Issue
Block a user