Merge branch 'main' into docs/k8s-image-gc-known-issue
This commit is contained in:
@@ -47,16 +47,16 @@ launchctl list | grep nanoclaw
|
||||
# Expected: PID 0 com.nanoclaw (PID = running, "-" = not running, non-zero exit = crashed)
|
||||
|
||||
# 2. Any running containers?
|
||||
container ls --format '{{.Names}} {{.Status}}' 2>/dev/null | grep nanoclaw
|
||||
docker ps --format '{{.Names}} {{.Status}}' 2>/dev/null | grep nanoclaw
|
||||
|
||||
# 3. Any stopped/orphaned containers?
|
||||
container ls -a --format '{{.Names}} {{.Status}}' 2>/dev/null | grep nanoclaw
|
||||
docker ps -a --format '{{.Names}} {{.Status}}' 2>/dev/null | grep nanoclaw
|
||||
|
||||
# 4. Recent errors in service log?
|
||||
grep -E 'ERROR|WARN' logs/nanoclaw.log | tail -20
|
||||
|
||||
# 5. Is WhatsApp connected? (look for last connection event)
|
||||
grep -E 'Connected to WhatsApp|Connection closed|connection.*close' logs/nanoclaw.log | tail -5
|
||||
# 5. Are channels connected? (look for last connection event)
|
||||
grep -E 'Connected|Connection closed|connection.*close|channel.*ready' logs/nanoclaw.log | tail -5
|
||||
|
||||
# 6. Are groups loaded?
|
||||
grep 'groupCount' logs/nanoclaw.log | tail -3
|
||||
@@ -105,7 +105,7 @@ grep -E 'Scheduling retry|retry|Max retries' logs/nanoclaw.log | tail -10
|
||||
## Agent Not Responding
|
||||
|
||||
```bash
|
||||
# Check if messages are being received from WhatsApp
|
||||
# Check if messages are being received from channels
|
||||
grep 'New messages' logs/nanoclaw.log | tail -10
|
||||
|
||||
# Check if messages are being processed (container spawned)
|
||||
@@ -135,10 +135,10 @@ sqlite3 store/messages.db "SELECT name, container_config FROM registered_groups;
|
||||
|
||||
# Test-run a container to check mounts (dry run)
|
||||
# Replace <group-folder> with the group's folder name
|
||||
container run -i --rm --entrypoint ls nanoclaw-agent:latest /workspace/extra/
|
||||
docker run -i --rm --entrypoint ls nanoclaw-agent:latest /workspace/extra/
|
||||
```
|
||||
|
||||
## WhatsApp Auth Issues
|
||||
## Channel Auth Issues
|
||||
|
||||
```bash
|
||||
# Check if QR code was requested (means auth expired)
|
||||
|
||||
15
docs/README.md
Normal file
15
docs/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# NanoClaw Documentation
|
||||
|
||||
The official documentation is at **[docs.nanoclaw.dev](https://docs.nanoclaw.dev)**.
|
||||
|
||||
The files in this directory are original design documents and developer references. For the most current and accurate information, use the documentation site.
|
||||
|
||||
| This directory | Documentation site |
|
||||
|---|---|
|
||||
| [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) |
|
||||
| [skills-as-branches.md](skills-as-branches.md) | [Skills system](https://docs.nanoclaw.dev/integrations/skills-system) |
|
||||
| [DEBUG_CHECKLIST.md](DEBUG_CHECKLIST.md) | [Troubleshooting](https://docs.nanoclaw.dev/advanced/troubleshooting) |
|
||||
| [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) |
|
||||
@@ -22,9 +22,9 @@ The entire codebase should be something you can read and understand. One Node.js
|
||||
|
||||
Instead of application-level permission systems trying to prevent agents from accessing things, agents run in actual Linux containers. The isolation is at the OS level. Agents can only see what's explicitly mounted. Bash access is safe because commands run inside the container, not on your Mac.
|
||||
|
||||
### Built for One User
|
||||
### Built for the Individual User
|
||||
|
||||
This isn't a framework or a platform. It's working software for my specific needs. I use WhatsApp and Email, so it supports WhatsApp and Email. I don't use Telegram, so it doesn't support Telegram. I add the integrations I actually want, not every possible integration.
|
||||
This isn't a framework or a platform. It's software that fits each user's exact needs. You fork the repo, add the channels you want (WhatsApp, Telegram, Discord, Slack, Gmail), and end up with clean code that does exactly what you need.
|
||||
|
||||
### Customization = Code Changes
|
||||
|
||||
@@ -44,41 +44,31 @@ When people contribute, they shouldn't add "Telegram support alongside WhatsApp.
|
||||
|
||||
## RFS (Request for Skills)
|
||||
|
||||
Skills we'd love contributors to build:
|
||||
Skills we'd like to see contributed:
|
||||
|
||||
### Communication Channels
|
||||
Skills to add or switch to different messaging platforms:
|
||||
- `/add-telegram` - Add Telegram as an input channel
|
||||
- `/add-slack` - Add Slack as an input channel
|
||||
- `/add-discord` - Add Discord as an input channel
|
||||
- `/add-sms` - Add SMS via Twilio or similar
|
||||
- `/convert-to-telegram` - Replace WhatsApp with Telegram entirely
|
||||
- `/add-signal` - Add Signal as a channel
|
||||
- `/add-matrix` - Add Matrix integration
|
||||
|
||||
### Container Runtime
|
||||
The project uses Docker by default (cross-platform). For macOS users who prefer Apple Container:
|
||||
- `/convert-to-apple-container` - Switch from Docker to Apple Container (macOS-only)
|
||||
|
||||
### Platform Support
|
||||
- `/setup-linux` - Make the full setup work on Linux (depends on Docker conversion)
|
||||
- `/setup-windows` - Windows support via WSL2 + Docker
|
||||
> **Note:** Telegram, Slack, Discord, Gmail, and Apple Container skills already exist. See the [skills documentation](https://docs.nanoclaw.dev/integrations/skills-system) for the full list.
|
||||
|
||||
---
|
||||
|
||||
## Vision
|
||||
|
||||
A personal Claude assistant accessible via WhatsApp, with minimal custom code.
|
||||
A personal Claude assistant accessible via messaging, with minimal custom code.
|
||||
|
||||
**Core components:**
|
||||
- **Claude Agent SDK** as the core agent
|
||||
- **Containers** for isolated agent execution (Linux VMs)
|
||||
- **WhatsApp** as the primary I/O channel
|
||||
- **Multi-channel messaging** (WhatsApp, Telegram, Discord, Slack, Gmail) — add exactly the channels you need
|
||||
- **Persistent memory** per conversation and globally
|
||||
- **Scheduled tasks** that run Claude and can message back
|
||||
- **Web access** for search and browsing
|
||||
- **Browser automation** via agent-browser
|
||||
|
||||
**Implementation approach:**
|
||||
- Use existing tools (WhatsApp connector, Claude Agent SDK, MCP servers)
|
||||
- Use existing tools (channel libraries, Claude Agent SDK, MCP servers)
|
||||
- Minimal glue code
|
||||
- File-based systems where possible (CLAUDE.md for memory, folders for groups)
|
||||
|
||||
@@ -87,7 +77,7 @@ A personal Claude assistant accessible via WhatsApp, with minimal custom code.
|
||||
## Architecture Decisions
|
||||
|
||||
### Message Routing
|
||||
- A router listens to WhatsApp and routes messages based on configuration
|
||||
- A router listens to connected channels and routes messages based on configuration
|
||||
- Only messages from registered groups are processed
|
||||
- Trigger: `@Andy` prefix (case insensitive), configurable via `ASSISTANT_NAME` env var
|
||||
- Unregistered groups are ignored completely
|
||||
@@ -136,10 +126,11 @@ A personal Claude assistant accessible via WhatsApp, with minimal custom code.
|
||||
|
||||
## Integration Points
|
||||
|
||||
### WhatsApp
|
||||
- Using baileys library for WhatsApp Web connection
|
||||
### Channels
|
||||
- WhatsApp (baileys), Telegram (grammy), Discord (discord.js), Slack (@slack/bolt), Gmail (googleapis)
|
||||
- Each channel lives in a separate fork repo and is added via skills (e.g., `/add-whatsapp`, `/add-telegram`)
|
||||
- Messages stored in SQLite, polled by router
|
||||
- QR code authentication during setup
|
||||
- Channels self-register at startup — unconfigured channels are skipped with a warning
|
||||
|
||||
### Scheduler
|
||||
- Built-in scheduler runs on the host, spawns containers for task execution
|
||||
@@ -170,12 +161,12 @@ A personal Claude assistant accessible via WhatsApp, with minimal custom code.
|
||||
- Each user gets a custom setup matching their exact needs
|
||||
|
||||
### Skills
|
||||
- `/setup` - Install dependencies, authenticate WhatsApp, configure scheduler, start services
|
||||
- `/customize` - General-purpose skill for adding capabilities (new channels like Telegram, new integrations, behavior changes)
|
||||
- `/update` - Pull upstream changes, merge with customizations, run migrations
|
||||
- `/setup` - Install dependencies, configure channels, start services
|
||||
- `/customize` - General-purpose skill for adding capabilities
|
||||
- `/update-nanoclaw` - Pull upstream changes, merge with customizations
|
||||
|
||||
### Deployment
|
||||
- Runs on local Mac via launchd
|
||||
- Runs on macOS (launchd), Linux (systemd), or Windows (WSL2)
|
||||
- Single Node.js process handles everything
|
||||
|
||||
---
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Main group | Trusted | Private self-chat, admin control |
|
||||
| Non-main groups | Untrusted | Other users may be malicious |
|
||||
| Container agents | Sandboxed | Isolated execution environment |
|
||||
| WhatsApp messages | User input | Potential prompt injection |
|
||||
| Incoming messages | User input | Potential prompt injection |
|
||||
|
||||
## Security Boundaries
|
||||
|
||||
@@ -64,23 +64,24 @@ Messages and task operations are verified against group identity:
|
||||
| View all tasks | ✓ | Own only |
|
||||
| Manage other groups | ✓ | ✗ |
|
||||
|
||||
### 5. Credential Handling
|
||||
### 5. Credential Isolation (OneCLI Agent Vault)
|
||||
|
||||
**Mounted Credentials:**
|
||||
- Claude auth tokens (filtered from `.env`, read-only)
|
||||
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.
|
||||
|
||||
**How it works:**
|
||||
1. Credentials are registered once with `onecli secrets create`, stored and managed by OneCLI
|
||||
2. When NanoClaw spawns a container, it calls `applyContainerConfig()` to route outbound HTTPS through the OneCLI gateway
|
||||
3. The gateway matches requests by host and path, injects the real credential, and forwards
|
||||
4. Agents cannot discover real credentials — not in environment, stdin, files, or `/proc`
|
||||
|
||||
**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:**
|
||||
- WhatsApp session (`store/auth/`) - host only
|
||||
- Mount allowlist - external, never mounted
|
||||
- Channel auth sessions (`store/auth/`) — host only
|
||||
- Mount allowlist — external, never mounted
|
||||
- Any credentials matching blocked patterns
|
||||
|
||||
**Credential Filtering:**
|
||||
Only these environment variables are exposed to containers:
|
||||
```typescript
|
||||
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
||||
```
|
||||
|
||||
> **Note:** Anthropic credentials are mounted so that Claude Code can authenticate when the agent runs. However, this means the agent itself can discover these credentials via Bash or file operations. Ideally, Claude Code would authenticate without exposing credentials to the agent's execution environment, but I couldn't figure this out. **PRs welcome** if you have ideas for credential isolation.
|
||||
- `.env` is shadowed with `/dev/null` in the project root mount
|
||||
|
||||
## Privilege Comparison
|
||||
|
||||
@@ -98,7 +99,7 @@ const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ UNTRUSTED ZONE │
|
||||
│ WhatsApp Messages (potentially malicious) │
|
||||
│ Incoming Messages (potentially malicious) │
|
||||
└────────────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼ Trigger check, input escaping
|
||||
@@ -108,16 +109,16 @@ const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
||||
│ • IPC authorization │
|
||||
│ • Mount validation (external allowlist) │
|
||||
│ • Container lifecycle │
|
||||
│ • Credential filtering │
|
||||
│ • OneCLI Agent Vault (injects credentials, enforces policies) │
|
||||
└────────────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼ Explicit mounts only
|
||||
▼ Explicit mounts only, no secrets
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ CONTAINER (ISOLATED/SANDBOXED) │
|
||||
│ • Agent execution │
|
||||
│ • Bash commands (sandboxed) │
|
||||
│ • File operations (limited to mounts) │
|
||||
│ • Network access (unrestricted) │
|
||||
│ • Cannot modify security config │
|
||||
│ • API calls routed through OneCLI Agent Vault │
|
||||
│ • No real credentials in environment or filesystem │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
290
docs/SPEC.md
290
docs/SPEC.md
@@ -1,79 +1,81 @@
|
||||
# NanoClaw Specification
|
||||
|
||||
A personal Claude assistant accessible via WhatsApp, with persistent memory per conversation, scheduled tasks, and email integration.
|
||||
A personal Claude assistant with multi-channel support, persistent memory per conversation, scheduled tasks, and container-isolated agent execution.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture](#architecture)
|
||||
2. [Folder Structure](#folder-structure)
|
||||
3. [Configuration](#configuration)
|
||||
4. [Memory System](#memory-system)
|
||||
5. [Session Management](#session-management)
|
||||
6. [Message Flow](#message-flow)
|
||||
7. [Commands](#commands)
|
||||
8. [Scheduled Tasks](#scheduled-tasks)
|
||||
9. [MCP Servers](#mcp-servers)
|
||||
10. [Deployment](#deployment)
|
||||
11. [Security Considerations](#security-considerations)
|
||||
2. [Architecture: Channel System](#architecture-channel-system)
|
||||
3. [Folder Structure](#folder-structure)
|
||||
4. [Configuration](#configuration)
|
||||
5. [Memory System](#memory-system)
|
||||
6. [Session Management](#session-management)
|
||||
7. [Message Flow](#message-flow)
|
||||
8. [Commands](#commands)
|
||||
9. [Scheduled Tasks](#scheduled-tasks)
|
||||
10. [MCP Servers](#mcp-servers)
|
||||
11. [Deployment](#deployment)
|
||||
12. [Security Considerations](#security-considerations)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ HOST (macOS) │
|
||||
│ (Main Node.js Process) │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌────────────────────┐ │
|
||||
│ │ WhatsApp │────────────────────▶│ SQLite Database │ │
|
||||
│ │ (baileys) │◀────────────────────│ (messages.db) │ │
|
||||
│ └──────────────┘ store/send └─────────┬──────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
|
||||
│ │ Message Loop │ │ Scheduler Loop │ │ IPC Watcher │ │
|
||||
│ │ (polls SQLite) │ │ (checks tasks) │ │ (file-based) │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ └───────────────┘ │
|
||||
│ │ │ │
|
||||
│ └───────────┬───────────┘ │
|
||||
│ │ spawns container │
|
||||
│ ▼ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ CONTAINER (Linux VM) │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ AGENT RUNNER │ │
|
||||
│ │ │ │
|
||||
│ │ Working directory: /workspace/group (mounted from host) │ │
|
||||
│ │ Volume mounts: │ │
|
||||
│ │ • groups/{name}/ → /workspace/group │ │
|
||||
│ │ • groups/global/ → /workspace/global/ (non-main only) │ │
|
||||
│ │ • data/sessions/{group}/.claude/ → /home/node/.claude/ │ │
|
||||
│ │ • Additional dirs → /workspace/extra/* │ │
|
||||
│ │ │ │
|
||||
│ │ Tools (all groups): │ │
|
||||
│ │ • Bash (safe - sandboxed in container!) │ │
|
||||
│ │ • Read, Write, Edit, Glob, Grep (file operations) │ │
|
||||
│ │ • WebSearch, WebFetch (internet access) │ │
|
||||
│ │ • agent-browser (browser automation) │ │
|
||||
│ │ • mcp__nanoclaw__* (scheduler tools via IPC) │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ HOST (macOS / Linux) │
|
||||
│ (Main Node.js Process) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌────────────────────┐ │
|
||||
│ │ Channels │─────────────────▶│ SQLite Database │ │
|
||||
│ │ (self-register │◀────────────────│ (messages.db) │ │
|
||||
│ │ at startup) │ store/send └─────────┬──────────┘ │
|
||||
│ └──────────────────┘ │ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
|
||||
│ │ Message Loop │ │ Scheduler Loop │ │ IPC Watcher │ │
|
||||
│ │ (polls SQLite) │ │ (checks tasks) │ │ (file-based) │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ └───────────────┘ │
|
||||
│ │ │ │
|
||||
│ └───────────┬───────────┘ │
|
||||
│ │ spawns container │
|
||||
│ ▼ │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ CONTAINER (Linux VM) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ AGENT RUNNER │ │
|
||||
│ │ │ │
|
||||
│ │ Working directory: /workspace/group (mounted from host) │ │
|
||||
│ │ Volume mounts: │ │
|
||||
│ │ • groups/{name}/ → /workspace/group │ │
|
||||
│ │ • groups/global/ → /workspace/global/ (non-main only) │ │
|
||||
│ │ • data/sessions/{group}/.claude/ → /home/node/.claude/ │ │
|
||||
│ │ • Additional dirs → /workspace/extra/* │ │
|
||||
│ │ │ │
|
||||
│ │ Tools (all groups): │ │
|
||||
│ │ • Bash (safe - sandboxed in container!) │ │
|
||||
│ │ • Read, Write, Edit, Glob, Grep (file operations) │ │
|
||||
│ │ • WebSearch, WebFetch (internet access) │ │
|
||||
│ │ • agent-browser (browser automation) │ │
|
||||
│ │ • mcp__nanoclaw__* (scheduler tools via IPC) │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Component | Technology | Purpose |
|
||||
|-----------|------------|---------|
|
||||
| WhatsApp Connection | Node.js (@whiskeysockets/baileys) | Connect to WhatsApp, send/receive messages |
|
||||
| Channel System | Channel registry (`src/channels/registry.ts`) | Channels self-register at startup |
|
||||
| Message Storage | SQLite (better-sqlite3) | Store messages for polling |
|
||||
| Container Runtime | Containers (Linux VMs) | Isolated environments for agent execution |
|
||||
| Agent | @anthropic-ai/claude-agent-sdk (0.2.29) | Run Claude with tools and MCP servers |
|
||||
@@ -82,6 +84,158 @@ A personal Claude assistant accessible via WhatsApp, with persistent memory per
|
||||
|
||||
---
|
||||
|
||||
## Architecture: Channel System
|
||||
|
||||
The core ships with no channels built in — each channel (WhatsApp, Telegram, Slack, Discord, Gmail) is installed as a [Claude Code skill](https://code.claude.com/docs/en/skills) that adds the channel code to your fork. Channels self-register at startup; installed channels with missing credentials emit a WARN log and are skipped.
|
||||
|
||||
### System Diagram
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Channels["Channels"]
|
||||
WA[WhatsApp]
|
||||
TG[Telegram]
|
||||
SL[Slack]
|
||||
DC[Discord]
|
||||
New["Other Channel (Signal, Gmail...)"]
|
||||
end
|
||||
|
||||
subgraph Orchestrator["Orchestrator — index.ts"]
|
||||
ML[Message Loop]
|
||||
GQ[Group Queue]
|
||||
RT[Router]
|
||||
TS[Task Scheduler]
|
||||
DB[(SQLite)]
|
||||
end
|
||||
|
||||
subgraph Execution["Container Execution"]
|
||||
CR[Container Runner]
|
||||
LC["Linux Container"]
|
||||
IPC[IPC Watcher]
|
||||
end
|
||||
|
||||
%% Flow
|
||||
WA & TG & SL & DC & New -->|onMessage| ML
|
||||
ML --> GQ
|
||||
GQ -->|concurrency| CR
|
||||
CR --> LC
|
||||
LC -->|filesystem IPC| IPC
|
||||
IPC -->|tasks & messages| RT
|
||||
RT -->|Channel.sendMessage| Channels
|
||||
TS -->|due tasks| CR
|
||||
|
||||
%% DB Connections
|
||||
DB <--> ML
|
||||
DB <--> TS
|
||||
|
||||
%% Styling for the dynamic channel
|
||||
style New stroke-dasharray: 5 5,stroke-width:2px
|
||||
```
|
||||
|
||||
### Channel Registry
|
||||
|
||||
The channel system is built on a factory registry in `src/channels/registry.ts`:
|
||||
|
||||
```typescript
|
||||
export type ChannelFactory = (opts: ChannelOpts) => Channel | null;
|
||||
|
||||
const registry = new Map<string, ChannelFactory>();
|
||||
|
||||
export function registerChannel(name: string, factory: ChannelFactory): void {
|
||||
registry.set(name, factory);
|
||||
}
|
||||
|
||||
export function getChannelFactory(name: string): ChannelFactory | undefined {
|
||||
return registry.get(name);
|
||||
}
|
||||
|
||||
export function getRegisteredChannelNames(): string[] {
|
||||
return [...registry.keys()];
|
||||
}
|
||||
```
|
||||
|
||||
Each factory receives `ChannelOpts` (callbacks for `onMessage`, `onChatMetadata`, and `registeredGroups`) and returns either a `Channel` instance or `null` if that channel's credentials are not configured.
|
||||
|
||||
### Channel Interface
|
||||
|
||||
Every channel implements this interface (defined in `src/types.ts`):
|
||||
|
||||
```typescript
|
||||
interface Channel {
|
||||
name: string;
|
||||
connect(): Promise<void>;
|
||||
sendMessage(jid: string, text: string): Promise<void>;
|
||||
isConnected(): boolean;
|
||||
ownsJid(jid: string): boolean;
|
||||
disconnect(): Promise<void>;
|
||||
setTyping?(jid: string, isTyping: boolean): Promise<void>;
|
||||
syncGroups?(force: boolean): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Self-Registration Pattern
|
||||
|
||||
Channels self-register using a barrel-import pattern:
|
||||
|
||||
1. Each channel skill adds a file to `src/channels/` (e.g. `whatsapp.ts`, `telegram.ts`) that calls `registerChannel()` at module load time:
|
||||
|
||||
```typescript
|
||||
// src/channels/whatsapp.ts
|
||||
import { registerChannel, ChannelOpts } from './registry.js';
|
||||
|
||||
export class WhatsAppChannel implements Channel { /* ... */ }
|
||||
|
||||
registerChannel('whatsapp', (opts: ChannelOpts) => {
|
||||
// Return null if credentials are missing
|
||||
if (!existsSync(authPath)) return null;
|
||||
return new WhatsAppChannel(opts);
|
||||
});
|
||||
```
|
||||
|
||||
2. The barrel file `src/channels/index.ts` imports all channel modules, triggering registration:
|
||||
|
||||
```typescript
|
||||
import './whatsapp.js';
|
||||
import './telegram.js';
|
||||
// ... each skill adds its import here
|
||||
```
|
||||
|
||||
3. At startup, the orchestrator (`src/index.ts`) loops through registered channels and connects whichever ones return a valid instance:
|
||||
|
||||
```typescript
|
||||
for (const name of getRegisteredChannelNames()) {
|
||||
const factory = getChannelFactory(name);
|
||||
const channel = factory?.(channelOpts);
|
||||
if (channel) {
|
||||
await channel.connect();
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/channels/registry.ts` | Channel factory registry |
|
||||
| `src/channels/index.ts` | Barrel imports that trigger channel self-registration |
|
||||
| `src/types.ts` | `Channel` interface, `ChannelOpts`, message types |
|
||||
| `src/index.ts` | Orchestrator — instantiates channels, runs message loop |
|
||||
| `src/router.ts` | Finds the owning channel for a JID, formats messages |
|
||||
|
||||
### Adding a New Channel
|
||||
|
||||
To add a new channel, contribute a skill to `.claude/skills/add-<name>/` that:
|
||||
|
||||
1. Adds a `src/channels/<name>.ts` file implementing the `Channel` interface
|
||||
2. Calls `registerChannel(name, factory)` at module load
|
||||
3. Returns `null` from the factory if credentials are missing
|
||||
4. Adds an import line to `src/channels/index.ts`
|
||||
|
||||
See existing skills (`/add-whatsapp`, `/add-telegram`, `/add-slack`, `/add-discord`, `/add-gmail`) for the pattern.
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
@@ -100,7 +254,8 @@ nanoclaw/
|
||||
├── src/
|
||||
│ ├── index.ts # Orchestrator: state, message loop, agent invocation
|
||||
│ ├── channels/
|
||||
│ │ └── whatsapp.ts # WhatsApp connection, auth, send/receive
|
||||
│ │ ├── registry.ts # Channel factory registry
|
||||
│ │ └── index.ts # Barrel imports for channel self-registration
|
||||
│ ├── ipc.ts # IPC watcher and task processing
|
||||
│ ├── router.ts # Message formatting and outbound routing
|
||||
│ ├── config.ts # Configuration constants
|
||||
@@ -141,10 +296,10 @@ nanoclaw/
|
||||
│
|
||||
├── groups/
|
||||
│ ├── CLAUDE.md # Global memory (all groups read this)
|
||||
│ ├── main/ # Self-chat (main control channel)
|
||||
│ ├── {channel}_main/ # Main control channel (e.g., whatsapp_main/)
|
||||
│ │ ├── CLAUDE.md # Main channel memory
|
||||
│ │ └── logs/ # Task execution logs
|
||||
│ └── {Group Name}/ # Per-group folders (created on registration)
|
||||
│ └── {channel}_{group-name}/ # Per-group folders (created on registration)
|
||||
│ ├── CLAUDE.md # Group-specific memory
|
||||
│ ├── logs/ # Task logs for this group
|
||||
│ └── *.md # Files created by the agent
|
||||
@@ -203,9 +358,9 @@ export const TRIGGER_PATTERN = new RegExp(`^@${ASSISTANT_NAME}\\b`, 'i');
|
||||
Groups can have additional directories mounted via `containerConfig` in the SQLite `registered_groups` table (stored as JSON in the `container_config` column). Example registration:
|
||||
|
||||
```typescript
|
||||
registerGroup("1234567890@g.us", {
|
||||
setRegisteredGroup("1234567890@g.us", {
|
||||
name: "Dev Team",
|
||||
folder: "dev-team",
|
||||
folder: "whatsapp_dev-team",
|
||||
trigger: "@Andy",
|
||||
added_at: new Date().toISOString(),
|
||||
containerConfig: {
|
||||
@@ -221,6 +376,8 @@ registerGroup("1234567890@g.us", {
|
||||
});
|
||||
```
|
||||
|
||||
Folder names follow the convention `{channel}_{group-name}` (e.g., `whatsapp_family-chat`, `telegram_dev-team`). The main group has `isMain: true` set during registration.
|
||||
|
||||
Additional mounts appear at `/workspace/extra/{containerPath}` inside the container.
|
||||
|
||||
**Mount syntax note:** Read-write mounts use `-v host:container`, but readonly mounts require `--mount "type=bind,source=...,target=...,readonly"` (the `:ro` suffix may not work on all runtimes).
|
||||
@@ -314,10 +471,10 @@ Sessions enable conversation continuity - Claude remembers what you talked about
|
||||
### Incoming Message Flow
|
||||
|
||||
```
|
||||
1. User sends WhatsApp message
|
||||
1. User sends a message via any connected channel
|
||||
│
|
||||
▼
|
||||
2. Baileys receives message via WhatsApp Web protocol
|
||||
2. Channel receives message (e.g. Baileys for WhatsApp, Bot API for Telegram)
|
||||
│
|
||||
▼
|
||||
3. Message stored in SQLite (store/messages.db)
|
||||
@@ -349,7 +506,7 @@ Sessions enable conversation continuity - Claude remembers what you talked about
|
||||
└── Uses tools as needed (search, email, etc.)
|
||||
│
|
||||
▼
|
||||
9. Router prefixes response with assistant name and sends via WhatsApp
|
||||
9. Router prefixes response with assistant name and sends via the owning channel
|
||||
│
|
||||
▼
|
||||
10. Router updates last agent timestamp and saves session ID
|
||||
@@ -473,7 +630,7 @@ The `nanoclaw` MCP server is created dynamically per agent call with the current
|
||||
| `pause_task` | Pause a task |
|
||||
| `resume_task` | Resume a paused task |
|
||||
| `cancel_task` | Delete a task |
|
||||
| `send_message` | Send a WhatsApp message to the group |
|
||||
| `send_message` | Send a message to the group via its channel |
|
||||
|
||||
---
|
||||
|
||||
@@ -487,7 +644,8 @@ When NanoClaw starts, it:
|
||||
1. **Ensures container runtime is running** - Automatically starts it if needed; kills orphaned NanoClaw containers from previous runs
|
||||
2. Initializes the SQLite database (migrates from JSON files if they exist)
|
||||
3. Loads state from SQLite (registered groups, sessions, router state)
|
||||
4. Connects to WhatsApp (on `connection.open`):
|
||||
4. **Connects channels** — loops through registered channels, instantiates those with credentials, calls `connect()` on each
|
||||
5. Once at least one channel is connected:
|
||||
- Starts the scheduler loop
|
||||
- Starts the IPC watcher for container messages
|
||||
- Sets up the per-group queue with `processGroupMessages`
|
||||
|
||||
359
docs/docker-sandboxes.md
Normal file
359
docs/docker-sandboxes.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 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/qwibitai/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
|
||||
npm install
|
||||
npm 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
|
||||
|
||||
`npm 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 npm 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
|
||||
npm run build
|
||||
bash container/build.sh
|
||||
```
|
||||
|
||||
## Step 6: Add a Channel
|
||||
|
||||
### Telegram
|
||||
|
||||
```bash
|
||||
# Apply the Telegram skill
|
||||
npx tsx scripts/apply-skill.ts .claude/skills/add-telegram
|
||||
|
||||
# Rebuild after applying the skill
|
||||
npm 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
|
||||
npx 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
|
||||
npx tsx scripts/apply-skill.ts .claude/skills/add-whatsapp
|
||||
|
||||
# Rebuild
|
||||
npm 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:
|
||||
npx tsx src/whatsapp-auth.ts
|
||||
|
||||
# OR pairing code — enter code in WhatsApp > Linked Devices > Link with phone number:
|
||||
npx tsx src/whatsapp-auth.ts --pairing-code --phone <phone-number-no-plus>
|
||||
|
||||
# Register your chat (JID = your phone number + @s.whatsapp.net)
|
||||
npx 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
|
||||
npm 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
|
||||
|
||||
### npm 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/qwibitai/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:
|
||||
npx tsx src/whatsapp-auth.ts
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,168 +0,0 @@
|
||||
# NanoClaw Skills Architecture
|
||||
|
||||
## What Skills Are For
|
||||
|
||||
NanoClaw's core is intentionally minimal. Skills are how users extend it: adding channels, integrations, cross-platform support, or replacing internals entirely. Examples: add Telegram alongside WhatsApp, switch from Apple Container to Docker, add Gmail integration, add voice message transcription. Each skill modifies the actual codebase, adding channel handlers, updating the message router, changing container configuration, and adding dependencies, rather than working through a plugin API or runtime hooks.
|
||||
|
||||
## Why This Architecture
|
||||
|
||||
The problem: users need to combine multiple modifications to a shared codebase, keep those modifications working across core updates, and do all of this without becoming git experts or losing their custom changes. A plugin system would be simpler but constrains what skills can do. Giving skills full codebase access means they can change anything, but that creates merge conflicts, update breakage, and state tracking challenges.
|
||||
|
||||
This architecture solves that by making skill application fully programmatic using standard git mechanics, with AI as a fallback for conflicts git can't resolve, and a shared resolution cache so most users never hit those conflicts at all. The result: users compose exactly the features they want, customizations survive core updates automatically, and the system is always recoverable.
|
||||
|
||||
## Core Principle
|
||||
|
||||
Skills are self-contained, auditable packages applied via standard git merge mechanics. Claude Code orchestrates the process — running git commands, reading skill manifests, and stepping in only when git can't resolve a conflict. The system uses existing git features (`merge-file`, `rerere`, `apply`) rather than custom merge infrastructure.
|
||||
|
||||
## Three-Level Resolution Model
|
||||
|
||||
Every operation follows this escalation:
|
||||
|
||||
1. **Git** — deterministic. `git merge-file` merges, `git rerere` replays cached resolutions, structured operations apply without merging. No AI. Handles the vast majority of cases.
|
||||
2. **Claude Code** — reads `SKILL.md`, `.intent.md`, and `state.yaml` to resolve conflicts git can't handle. Caches resolutions via `git rerere` so the same conflict never needs resolving twice.
|
||||
3. **Claude Code + user input** — when Claude Code lacks sufficient context to determine intent (e.g., two features genuinely conflict at an application level), it asks the user for a decision, then uses that input to perform the resolution. Claude Code still does the work — the user provides direction, not code.
|
||||
|
||||
**Important**: A clean merge doesn't guarantee working code. Semantic conflicts can produce clean text merges that break at runtime. **Tests run after every operation.**
|
||||
|
||||
## Backup/Restore Safety
|
||||
|
||||
Before any operation, all affected files are copied to `.nanoclaw/backup/`. On success, backup is deleted. On failure, backup is restored. Works safely for users who don't use git.
|
||||
|
||||
## The Shared Base
|
||||
|
||||
`.nanoclaw/base/` holds a clean copy of the core codebase. This is the single common ancestor for all three-way merges, only updated during core updates.
|
||||
|
||||
## Two Types of Changes
|
||||
|
||||
### Code Files (Three-Way Merge)
|
||||
Source code where skills weave in logic. Merged via `git merge-file` against the shared base. Skills carry full modified files.
|
||||
|
||||
### Structured Data (Deterministic Operations)
|
||||
Files like `package.json`, `docker-compose.yml`, `.env.example`. Skills declare requirements in the manifest; the system applies them programmatically. Multiple skills' declarations are batched — dependencies merged, `package.json` written once, `npm install` run once.
|
||||
|
||||
```yaml
|
||||
structured:
|
||||
npm_dependencies:
|
||||
whatsapp-web.js: "^2.1.0"
|
||||
env_additions:
|
||||
- WHATSAPP_TOKEN
|
||||
docker_compose_services:
|
||||
whatsapp-redis:
|
||||
image: redis:alpine
|
||||
ports: ["6380:6379"]
|
||||
```
|
||||
|
||||
Structured conflicts (version incompatibilities, port collisions) follow the same three-level resolution model.
|
||||
|
||||
## Skill Package Structure
|
||||
|
||||
A skill contains only the files it adds or modifies. Modified code files carry the **full file** (clean core + skill's changes), making `git merge-file` straightforward and auditable.
|
||||
|
||||
```
|
||||
skills/add-whatsapp/
|
||||
SKILL.md # What this skill does and why
|
||||
manifest.yaml # Metadata, dependencies, structured ops
|
||||
tests/whatsapp.test.ts # Integration tests
|
||||
add/src/channels/whatsapp.ts # New files
|
||||
modify/src/server.ts # Full modified file for merge
|
||||
modify/src/server.ts.intent.md # Structured intent for conflict resolution
|
||||
```
|
||||
|
||||
### Intent Files
|
||||
Each modified file has a `.intent.md` with structured headings: **What this skill adds**, **Key sections**, **Invariants**, and **Must-keep sections**. These give Claude Code specific guidance during conflict resolution.
|
||||
|
||||
### Manifest
|
||||
Declares: skill metadata, core version compatibility, files added/modified, file operations, structured operations, skill relationships (conflicts, depends, tested_with), post-apply commands, and test command.
|
||||
|
||||
## Customization and Layering
|
||||
|
||||
**One skill, one happy path** — a skill implements the reasonable default for 80% of users.
|
||||
|
||||
**Customization is more patching.** Apply the skill, then modify via tracked patches, direct editing, or additional layered skills. Custom modifications are recorded in `state.yaml` and replayable.
|
||||
|
||||
**Skills layer via `depends`.** Extension skills build on base skills (e.g., `telegram-reactions` depends on `add-telegram`).
|
||||
|
||||
## File Operations
|
||||
|
||||
Renames, deletes, and moves are declared in the manifest and run **before** code merges. When core renames a file, a **path remap** resolves skill references at apply time — skill packages are never mutated.
|
||||
|
||||
## The Apply Flow
|
||||
|
||||
1. Pre-flight checks (compatibility, dependencies, untracked changes)
|
||||
2. Backup
|
||||
3. File operations + path remapping
|
||||
4. Copy new files
|
||||
5. Merge modified code files (`git merge-file`)
|
||||
6. Conflict resolution (shared cache → `git rerere` → Claude Code → Claude Code + user input)
|
||||
7. Apply structured operations (batched)
|
||||
8. Post-apply commands, update `state.yaml`
|
||||
9. **Run tests** (mandatory, even if all merges were clean)
|
||||
10. Clean up (delete backup on success, restore on failure)
|
||||
|
||||
## Shared Resolution Cache
|
||||
|
||||
`.nanoclaw/resolutions/` ships pre-computed, verified conflict resolutions with **hash enforcement** — a cached resolution only applies if base, current, and skill input hashes match exactly. This means most users never encounter unresolved conflicts for common skill combinations.
|
||||
|
||||
### rerere Adapter
|
||||
`git rerere` requires unmerged index entries that `git merge-file` doesn't create. An adapter sets up the required index state after `merge-file` produces a conflict, enabling rerere caching. This requires the project to be a git repository; users without `.git/` lose caching but not functionality.
|
||||
|
||||
## State Tracking
|
||||
|
||||
`.nanoclaw/state.yaml` records: core version, all applied skills (with per-file hashes for base/skill/merged), structured operation outcomes, custom patches, and path remaps. This makes drift detection instant and replay deterministic.
|
||||
|
||||
## Untracked Changes
|
||||
|
||||
Direct edits are detected via hash comparison before any operation. Users can record them as tracked patches, continue untracked, or abort. The three-level model can always recover coherent state from any starting point.
|
||||
|
||||
## Core Updates
|
||||
|
||||
Most changes propagate automatically through three-way merge. **Breaking changes** require a **migration skill** — a regular skill that preserves the old behavior, authored against the new core. Migrations are declared in `migrations.yaml` and applied automatically during updates.
|
||||
|
||||
### Update Flow
|
||||
1. Preview changes (git-only, no files modified)
|
||||
2. Backup → file operations → three-way merge → conflict resolution
|
||||
3. Re-apply custom patches (`git apply --3way`)
|
||||
4. **Update base** to new core
|
||||
5. Apply migration skills (preserves user's setup automatically)
|
||||
6. Re-apply updated skills (version-changed skills only)
|
||||
7. Re-run structured operations → run all tests → clean up
|
||||
|
||||
The user sees no prompts during updates. To accept a new default later, they remove the migration skill.
|
||||
|
||||
## Skill Removal
|
||||
|
||||
Uninstall is **replay without the skill**: read `state.yaml`, remove the target skill, replay all remaining skills from clean base using the resolution cache. Backup for safety.
|
||||
|
||||
## Rebase
|
||||
|
||||
Flatten accumulated layers into a clean starting point. Updates base, regenerates diffs, clears old patches and stale cache entries. Trades individual skill history for simpler future merges.
|
||||
|
||||
## Replay
|
||||
|
||||
Given `state.yaml`, reproduce the exact installation on a fresh machine with no AI (assuming cached resolutions). Apply skills in order, merge, apply custom patches, batch structured operations, run tests.
|
||||
|
||||
## Skill Tests
|
||||
|
||||
Each skill includes integration tests. Tests run **always** — after apply, after update, after uninstall, during replay, in CI. CI tests all official skills individually and pairwise combinations for skills sharing modified files or structured operations.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Use git, don't reinvent it.**
|
||||
2. **Three-level resolution: git → Claude Code → Claude Code + user input.**
|
||||
3. **Clean merges aren't enough.** Tests run after every operation.
|
||||
4. **All operations are safe.** Backup/restore, no half-applied state.
|
||||
5. **One shared base**, only updated on core updates.
|
||||
6. **Code merges vs. structured operations.** Source code is merged; configs are aggregated.
|
||||
7. **Resolutions are learned and shared** with hash enforcement.
|
||||
8. **One skill, one happy path.** Customization is more patching.
|
||||
9. **Skills layer and compose.**
|
||||
10. **Intent is first-class and structured.**
|
||||
11. **State is explicit and complete.** Replay is deterministic.
|
||||
12. **Always recoverable.**
|
||||
13. **Uninstall is replay.**
|
||||
14. **Core updates are the maintainers' responsibility.** Breaking changes require migration skills.
|
||||
15. **File operations and path remapping are first-class.**
|
||||
16. **Skills are tested.** CI tests pairwise by overlap.
|
||||
17. **Deterministic serialization.** No noisy diffs.
|
||||
18. **Rebase when needed.**
|
||||
19. **Progressive core slimming** via migration skills.
|
||||
677
docs/skills-as-branches.md
Normal file
677
docs/skills-as-branches.md
Normal file
@@ -0,0 +1,677 @@
|
||||
# Skills as Branches
|
||||
|
||||
## Overview
|
||||
|
||||
This document covers **feature skills** — skills that add capabilities via git branch merges. This is the most complex skill type and the primary way NanoClaw is extended.
|
||||
|
||||
NanoClaw has four types of skills overall. See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full taxonomy:
|
||||
|
||||
| Type | Location | How it works |
|
||||
|------|----------|-------------|
|
||||
| **Feature** (this doc) | `.claude/skills/` + `skill/*` branch | SKILL.md has instructions; code lives on a branch, applied via `git merge` |
|
||||
| **Utility** | `.claude/skills/<name>/` with code files | Self-contained tools; code in skill directory, copied into place on install |
|
||||
| **Operational** | `.claude/skills/` on `main` | Instruction-only workflows (setup, debug, update) |
|
||||
| **Container** | `container/skills/` | Loaded inside agent containers at runtime |
|
||||
|
||||
---
|
||||
|
||||
Feature skills are distributed as git branches on the upstream repository. Applying a skill is a `git merge`. Updating core is a `git merge`. Everything is standard git.
|
||||
|
||||
This replaces the previous `skills-engine/` system (three-way file merging, `.nanoclaw/` state, manifest files, replay, backup/restore) with plain git operations and Claude for conflict resolution.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Repository structure
|
||||
|
||||
The upstream repo (`qwibitai/nanoclaw`) maintains:
|
||||
|
||||
- `main` — core NanoClaw (no skill code)
|
||||
- `skill/discord` — main + Discord integration
|
||||
- `skill/telegram` — main + Telegram integration
|
||||
- `skill/slack` — main + Slack integration
|
||||
- `skill/gmail` — main + Gmail integration
|
||||
- etc.
|
||||
|
||||
Each skill branch contains all the code changes for that skill: new files, modified source files, updated `package.json` dependencies, `.env.example` additions — everything. No manifest, no structured operations, no separate `add/` and `modify/` directories.
|
||||
|
||||
### Skill discovery and installation
|
||||
|
||||
Skills are split into two categories:
|
||||
|
||||
**Operational skills** (on `main`, always available):
|
||||
- `/setup`, `/debug`, `/update-nanoclaw`, `/customize`, `/update-skills`
|
||||
- These are instruction-only SKILL.md files — no code changes, just workflows
|
||||
- Live in `.claude/skills/` on `main`, immediately available to every user
|
||||
|
||||
**Feature skills** (in marketplace, installed on demand):
|
||||
- `/add-discord`, `/add-telegram`, `/add-slack`, `/add-gmail`, etc.
|
||||
- Each has a SKILL.md with setup instructions and a corresponding `skill/*` branch with code
|
||||
- Live in the marketplace repo (`qwibitai/nanoclaw-skills`)
|
||||
|
||||
Users never interact with the marketplace directly. The operational skills `/setup` and `/customize` handle plugin installation transparently:
|
||||
|
||||
```bash
|
||||
# Claude runs this behind the scenes — users don't see it
|
||||
claude plugin install nanoclaw-skills@nanoclaw-skills --scope project
|
||||
```
|
||||
|
||||
Skills are hot-loaded after `claude plugin install` — no restart needed. This means `/setup` can install the marketplace plugin, then immediately run any feature skill, all in one session.
|
||||
|
||||
### Selective skill installation
|
||||
|
||||
`/setup` asks users what channels they want, then only offers relevant skills:
|
||||
|
||||
1. "Which messaging channels do you want to use?" → Discord, Telegram, Slack, WhatsApp
|
||||
2. User picks Telegram → Claude installs the plugin and runs `/add-telegram`
|
||||
3. After Telegram is set up: "Want to add Agent Swarm support for Telegram?" → offers `/add-telegram-swarm`
|
||||
4. "Want to enable community skills?" → installs community marketplace plugins
|
||||
|
||||
Dependent skills (e.g., `telegram-swarm` depends on `telegram`) are only offered after their parent is installed. `/customize` follows the same pattern for post-setup additions.
|
||||
|
||||
### Marketplace configuration
|
||||
|
||||
NanoClaw's `.claude/settings.json` registers the official marketplace:
|
||||
|
||||
```json
|
||||
{
|
||||
"extraKnownMarketplaces": {
|
||||
"nanoclaw-skills": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "qwibitai/nanoclaw-skills"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The marketplace repo uses Claude Code's plugin structure:
|
||||
|
||||
```
|
||||
qwibitai/nanoclaw-skills/
|
||||
.claude-plugin/
|
||||
marketplace.json # Plugin catalog
|
||||
plugins/
|
||||
nanoclaw-skills/ # Single plugin bundling all official skills
|
||||
.claude-plugin/
|
||||
plugin.json # Plugin manifest
|
||||
skills/
|
||||
add-discord/
|
||||
SKILL.md # Setup instructions; step 1 is "merge the branch"
|
||||
add-telegram/
|
||||
SKILL.md
|
||||
add-slack/
|
||||
SKILL.md
|
||||
...
|
||||
```
|
||||
|
||||
Multiple skills are bundled in one plugin — installing `nanoclaw-skills` makes all feature skills available at once. Individual skills don't need separate installation.
|
||||
|
||||
Each SKILL.md tells Claude to merge the corresponding skill branch as step 1, then walks through interactive setup (env vars, bot creation, etc.).
|
||||
|
||||
### Applying a skill
|
||||
|
||||
User runs `/add-discord` (discovered via marketplace). Claude follows the SKILL.md:
|
||||
|
||||
1. `git fetch upstream skill/discord`
|
||||
2. `git merge upstream/skill/discord`
|
||||
3. Interactive setup (create bot, get token, configure env vars, etc.)
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
git fetch upstream skill/discord
|
||||
git merge upstream/skill/discord
|
||||
```
|
||||
|
||||
### Applying multiple skills
|
||||
|
||||
```bash
|
||||
git merge upstream/skill/discord
|
||||
git merge upstream/skill/telegram
|
||||
```
|
||||
|
||||
Git handles the composition. If both skills modify the same lines, it's a real conflict and Claude resolves it.
|
||||
|
||||
### Updating core
|
||||
|
||||
```bash
|
||||
git fetch upstream main
|
||||
git merge upstream/main
|
||||
```
|
||||
|
||||
Since skill branches are kept merged-forward with main (see CI section), the user's merged-in skill changes and upstream changes have proper common ancestors.
|
||||
|
||||
### Checking for skill updates
|
||||
|
||||
Users who previously merged a skill branch can check for updates. For each `upstream/skill/*` branch, check whether the branch has commits that aren't in the user's HEAD:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
for branch in $(git branch -r | grep 'upstream/skill/'); do
|
||||
# Check if user has merged this skill at some point
|
||||
merge_base=$(git merge-base HEAD "$branch" 2>/dev/null) || continue
|
||||
# Check if the skill branch has new commits beyond what the user has
|
||||
if ! git merge-base --is-ancestor "$branch" HEAD 2>/dev/null; then
|
||||
echo "$branch has updates available"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
This requires no state — it uses git history to determine which skills were previously merged and whether they have new commits.
|
||||
|
||||
This logic is available in two ways:
|
||||
- Built into `/update-nanoclaw` — after merging main, optionally check for skill updates
|
||||
- Standalone `/update-skills` — check and merge skill updates independently
|
||||
|
||||
### Conflict resolution
|
||||
|
||||
At any merge step, conflicts may arise. Claude resolves them — reading the conflicted files, understanding the intent of both sides, and producing the correct result. This is what makes the branch approach viable at scale: conflict resolution that previously required human judgment is now automated.
|
||||
|
||||
### Skill dependencies
|
||||
|
||||
Some skills depend on other skills. E.g., `skill/telegram-swarm` requires `skill/telegram`. Dependent skill branches are branched from their parent skill branch, not from `main`.
|
||||
|
||||
This means `skill/telegram-swarm` includes all of telegram's changes plus its own additions. When a user merges `skill/telegram-swarm`, they get both — no need to merge telegram separately.
|
||||
|
||||
Dependencies are implicit in git history — `git merge-base --is-ancestor` determines whether one skill branch is an ancestor of another. No separate dependency file is needed.
|
||||
|
||||
### Uninstalling a skill
|
||||
|
||||
```bash
|
||||
# Find the merge commit
|
||||
git log --merges --oneline | grep discord
|
||||
|
||||
# Revert it
|
||||
git revert -m 1 <merge-commit>
|
||||
```
|
||||
|
||||
This creates a new commit that undoes the skill's changes. Claude can handle the whole flow.
|
||||
|
||||
If the user has modified the skill's code since merging (custom changes on top), the revert might conflict — Claude resolves it.
|
||||
|
||||
If the user later wants to re-apply the skill, they need to revert the revert first (git treats reverted changes as "already applied and undone"). Claude handles this too.
|
||||
|
||||
## CI: Keeping Skill Branches Current
|
||||
|
||||
A GitHub Action runs on every push to `main`:
|
||||
|
||||
1. List all `skill/*` branches
|
||||
2. For each skill branch, merge `main` into it (merge-forward, not rebase)
|
||||
3. Run build and tests on the merged result
|
||||
4. If tests pass, push the updated skill branch
|
||||
5. If a skill fails (conflict, build error, test failure), open a GitHub issue for manual resolution
|
||||
|
||||
**Why merge-forward instead of rebase:**
|
||||
- No force-push — preserves history for users who already merged the skill
|
||||
- Users can re-merge a skill branch to pick up skill updates (bug fixes, improvements)
|
||||
- Git has proper common ancestors throughout the merge graph
|
||||
|
||||
**Why this scales:** With a few hundred skills and a few commits to main per day, the CI cost is trivial. Haiku is fast and cheap. The approach that wouldn't have been feasible a year or two ago is now practical because Claude can resolve conflicts at scale.
|
||||
|
||||
## Installation Flow
|
||||
|
||||
### New users (recommended)
|
||||
|
||||
1. Fork `qwibitai/nanoclaw` on GitHub (click the Fork button)
|
||||
2. Clone your fork:
|
||||
```bash
|
||||
git clone https://github.com/<you>/nanoclaw.git
|
||||
cd nanoclaw
|
||||
```
|
||||
3. Run Claude Code:
|
||||
```bash
|
||||
claude
|
||||
```
|
||||
4. Run `/setup` — Claude handles dependencies, authentication, container setup, service configuration, and adds `upstream` remote if not present
|
||||
|
||||
Forking is recommended because it gives users a remote to push their customizations to. Clone-only works for trying things out but provides no remote backup.
|
||||
|
||||
### Existing users migrating from clone
|
||||
|
||||
Users who previously ran `git clone https://github.com/qwibitai/nanoclaw.git` and have local customizations:
|
||||
|
||||
1. Fork `qwibitai/nanoclaw` on GitHub
|
||||
2. Reroute remotes:
|
||||
```bash
|
||||
git remote rename origin upstream
|
||||
git remote add origin https://github.com/<you>/nanoclaw.git
|
||||
git push --force origin main
|
||||
```
|
||||
The `--force` is needed because the fresh fork's main is at upstream's latest, but the user wants their (possibly behind) version. The fork was just created so there's nothing to lose.
|
||||
3. From this point, `origin` = their fork, `upstream` = qwibitai/nanoclaw
|
||||
|
||||
### Existing users migrating from the old skills engine
|
||||
|
||||
Users who previously applied skills via the `skills-engine/` system have skill code in their tree but no merge commits linking to skill branches. Git doesn't know these changes came from a skill, so merging a skill branch on top would conflict or duplicate.
|
||||
|
||||
**For new skills going forward:** just merge skill branches as normal. No issue.
|
||||
|
||||
**For existing old-engine skills**, two migration paths:
|
||||
|
||||
**Option A: Per-skill reapply (keep your fork)**
|
||||
1. For each old-engine skill: identify and revert the old changes, then merge the skill branch fresh
|
||||
2. Claude assists with identifying what to revert and resolving any conflicts
|
||||
3. Custom modifications (non-skill changes) are preserved
|
||||
|
||||
**Option B: Fresh start (cleanest)**
|
||||
1. Create a new fork from upstream
|
||||
2. Merge the skill branches you want
|
||||
3. Manually re-apply your custom (non-skill) changes
|
||||
4. Claude assists by diffing your old fork against the new one to identify custom changes
|
||||
|
||||
In both cases:
|
||||
- Delete the `.nanoclaw/` directory (no longer needed)
|
||||
- The `skills-engine/` code will be removed from upstream once all skills are migrated
|
||||
- `/update-skills` only tracks skills applied via branch merge — old-engine skills won't appear in update checks
|
||||
|
||||
## User Workflows
|
||||
|
||||
### Custom changes
|
||||
|
||||
Users make custom changes directly on their main branch. This is the standard fork workflow — their `main` IS their customized version.
|
||||
|
||||
```bash
|
||||
# Make changes
|
||||
vim src/config.ts
|
||||
git commit -am "change trigger word to @Bob"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Custom changes, skills, and core updates all coexist on their main branch. Git handles the three-way merging at each merge step because it can trace common ancestors through the merge history.
|
||||
|
||||
### Applying a skill
|
||||
|
||||
Run `/add-discord` in Claude Code (discovered via the marketplace plugin), or manually:
|
||||
|
||||
```bash
|
||||
git fetch upstream skill/discord
|
||||
git merge upstream/skill/discord
|
||||
# Follow setup instructions for configuration
|
||||
git push origin main
|
||||
```
|
||||
|
||||
If the user is behind upstream's main when they merge a skill branch, the merge might bring in some core changes too (since skill branches are merged-forward with main). This is generally fine — they get a compatible version of everything.
|
||||
|
||||
### Updating core
|
||||
|
||||
```bash
|
||||
git fetch upstream main
|
||||
git merge upstream/main
|
||||
git push origin main
|
||||
```
|
||||
|
||||
This is the same as the existing `/update-nanoclaw` skill's merge path.
|
||||
|
||||
### Updating skills
|
||||
|
||||
Run `/update-skills` or let `/update-nanoclaw` check after a core update. For each previously-merged skill branch that has new commits, Claude offers to merge the updates.
|
||||
|
||||
### Contributing back to upstream
|
||||
|
||||
Users who want to submit a PR to upstream:
|
||||
|
||||
```bash
|
||||
git fetch upstream main
|
||||
git checkout -b my-fix upstream/main
|
||||
# Make changes
|
||||
git push origin my-fix
|
||||
# Create PR from my-fix to qwibitai/nanoclaw:main
|
||||
```
|
||||
|
||||
Standard fork contribution workflow. Their custom changes stay on their main and don't leak into the PR.
|
||||
|
||||
## Contributing a Skill
|
||||
|
||||
The flow below is for **feature skills** (branch-based). For utility skills (self-contained tools) and container skills, the contributor opens a PR that adds files directly to `.claude/skills/<name>/` or `container/skills/<name>/` — no branch extraction needed. See [CONTRIBUTING.md](../CONTRIBUTING.md) for all skill types.
|
||||
|
||||
### Contributor flow (feature skills)
|
||||
|
||||
1. Fork `qwibitai/nanoclaw`
|
||||
2. Branch from `main`
|
||||
3. Make the code changes (new channel file, modified integration points, updated package.json, .env.example additions, etc.)
|
||||
4. Open a PR to `main`
|
||||
|
||||
The contributor opens a normal PR — they don't need to know about skill branches or marketplace repos. They just make code changes and submit.
|
||||
|
||||
### Maintainer flow
|
||||
|
||||
When a skill PR is reviewed and approved:
|
||||
|
||||
1. Create a `skill/<name>` branch from the PR's commits:
|
||||
```bash
|
||||
git fetch origin pull/<PR_NUMBER>/head:skill/<name>
|
||||
git push origin skill/<name>
|
||||
```
|
||||
2. Force-push to the contributor's PR branch, replacing it with a single commit that adds the contributor to `CONTRIBUTORS.md` (removing all code changes)
|
||||
3. Merge the slimmed PR into `main` (just the contributor addition)
|
||||
4. Add the skill's SKILL.md to the marketplace repo (`qwibitai/nanoclaw-skills`)
|
||||
|
||||
This way:
|
||||
- The contributor gets merge credit (their PR is merged)
|
||||
- They're added to CONTRIBUTORS.md automatically by the maintainer
|
||||
- The skill branch is created from their work
|
||||
- `main` stays clean (no skill code)
|
||||
- The contributor only had to do one thing: open a PR with code changes
|
||||
|
||||
**Note:** GitHub PRs from forks have "Allow edits from maintainers" checked by default, so the maintainer can push to the contributor's PR branch.
|
||||
|
||||
### Skill SKILL.md
|
||||
|
||||
The contributor can optionally provide a SKILL.md (either in the PR or separately). This goes into the marketplace repo and contains:
|
||||
|
||||
1. Frontmatter (name, description, triggers)
|
||||
2. Step 1: Merge the skill branch
|
||||
3. Steps 2-N: Interactive setup (create bot, get token, configure env vars, verify)
|
||||
|
||||
If the contributor doesn't provide a SKILL.md, the maintainer writes one based on the PR.
|
||||
|
||||
## Community Marketplaces
|
||||
|
||||
Anyone can maintain their own fork with skill branches and their own marketplace repo. This enables a community-driven skill ecosystem without requiring write access to the upstream repo.
|
||||
|
||||
### How it works
|
||||
|
||||
A community contributor:
|
||||
|
||||
1. Maintains a fork of NanoClaw (e.g., `alice/nanoclaw`)
|
||||
2. Creates `skill/*` branches on their fork with their custom skills
|
||||
3. Creates a marketplace repo (e.g., `alice/nanoclaw-skills`) with a `.claude-plugin/marketplace.json` and plugin structure
|
||||
|
||||
### Adding a community marketplace
|
||||
|
||||
If the community contributor is trusted, they can open a PR to add their marketplace to NanoClaw's `.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"extraKnownMarketplaces": {
|
||||
"nanoclaw-skills": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "qwibitai/nanoclaw-skills"
|
||||
}
|
||||
},
|
||||
"alice-nanoclaw-skills": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "alice/nanoclaw-skills"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once merged, all NanoClaw users automatically discover the community marketplace alongside the official one.
|
||||
|
||||
### Installing community skills
|
||||
|
||||
`/setup` and `/customize` ask users whether they want to enable community skills. If yes, Claude installs community marketplace plugins via `claude plugin install`:
|
||||
|
||||
```bash
|
||||
claude plugin install alice-skills@alice-nanoclaw-skills --scope project
|
||||
```
|
||||
|
||||
Community skills are hot-loaded and immediately available — no restart needed. Dependent skills are only offered after their prerequisites are met (e.g., community Telegram add-ons only after Telegram is installed).
|
||||
|
||||
Users can also browse and install community plugins manually via `/plugin`.
|
||||
|
||||
### Properties of this system
|
||||
|
||||
- **No gatekeeping required.** Anyone can create skills on their fork without permission. They only need approval to be listed in the auto-discovered marketplaces.
|
||||
- **Multiple marketplaces coexist.** Users see skills from all trusted marketplaces in `/plugin`.
|
||||
- **Community skills use the same merge pattern.** The SKILL.md just points to a different remote:
|
||||
```bash
|
||||
git remote add alice https://github.com/alice/nanoclaw.git
|
||||
git fetch alice skill/my-cool-feature
|
||||
git merge alice/skill/my-cool-feature
|
||||
```
|
||||
- **Users can also add marketplaces manually.** Even without being listed in settings.json, users can run `/plugin marketplace add alice/nanoclaw-skills` to discover skills from any source.
|
||||
- **CI is per-fork.** Each community maintainer runs their own CI to keep their skill branches merged-forward. They can use the same GitHub Action as the upstream repo.
|
||||
|
||||
## Flavors
|
||||
|
||||
A flavor is a curated fork of NanoClaw — a combination of skills, custom changes, and configuration tailored for a specific use case (e.g., "NanoClaw for Sales," "NanoClaw Minimal," "NanoClaw for Developers").
|
||||
|
||||
### Creating a flavor
|
||||
|
||||
1. Fork `qwibitai/nanoclaw`
|
||||
2. Merge in the skills you want
|
||||
3. Make custom changes (trigger word, prompts, integrations, etc.)
|
||||
4. Your fork's `main` IS the flavor
|
||||
|
||||
### Installing a flavor
|
||||
|
||||
During `/setup`, users are offered a choice of flavors before any configuration happens. The setup skill reads `flavors.yaml` from the repo (shipped with upstream, always up to date) and presents options:
|
||||
|
||||
AskUserQuestion: "Start with a flavor or default NanoClaw?"
|
||||
- Default NanoClaw
|
||||
- NanoClaw for Sales — Gmail + Slack + CRM (maintained by alice)
|
||||
- NanoClaw Minimal — Telegram-only, lightweight (maintained by bob)
|
||||
|
||||
If a flavor is chosen:
|
||||
|
||||
```bash
|
||||
git remote add <flavor-name> https://github.com/alice/nanoclaw.git
|
||||
git fetch <flavor-name> main
|
||||
git merge <flavor-name>/main
|
||||
```
|
||||
|
||||
Then setup continues normally (dependencies, auth, container, service).
|
||||
|
||||
**This choice is only offered on a fresh fork** — when the user's main matches or is close to upstream's main with no local commits. If `/setup` detects significant local changes (re-running setup on an existing install), it skips the flavor selection and goes straight to configuration.
|
||||
|
||||
After installation, the user's fork has three remotes:
|
||||
- `origin` — their fork (push customizations here)
|
||||
- `upstream` — `qwibitai/nanoclaw` (core updates)
|
||||
- `<flavor-name>` — the flavor fork (flavor updates)
|
||||
|
||||
### Updating a flavor
|
||||
|
||||
```bash
|
||||
git fetch <flavor-name> main
|
||||
git merge <flavor-name>/main
|
||||
```
|
||||
|
||||
The flavor maintainer keeps their fork updated (merging upstream, updating skills). Users pull flavor updates the same way they pull core updates.
|
||||
|
||||
### Flavors registry
|
||||
|
||||
`flavors.yaml` lives in the upstream repo:
|
||||
|
||||
```yaml
|
||||
flavors:
|
||||
- name: NanoClaw for Sales
|
||||
repo: alice/nanoclaw
|
||||
description: Gmail + Slack + CRM integration, daily pipeline summaries
|
||||
maintainer: alice
|
||||
|
||||
- name: NanoClaw Minimal
|
||||
repo: bob/nanoclaw
|
||||
description: Telegram-only, no container overhead
|
||||
maintainer: bob
|
||||
```
|
||||
|
||||
Anyone can PR to add their flavor. The file is available locally when `/setup` runs since it's part of the cloned repo.
|
||||
|
||||
### Discoverability
|
||||
|
||||
- **During setup** — flavor selection is offered as part of the initial setup flow
|
||||
- **`/browse-flavors` skill** — reads `flavors.yaml` and presents options at any time
|
||||
- **GitHub topics** — flavor forks can tag themselves with `nanoclaw-flavor` for searchability
|
||||
- **Discord / website** — community-curated lists
|
||||
|
||||
## Migration
|
||||
|
||||
Migration from the old skills engine to branches is complete. All feature skills now live on `skill/*` branches, and the skills engine has been removed.
|
||||
|
||||
### Skill branches
|
||||
|
||||
| Branch | Base | Description |
|
||||
|--------|------|-------------|
|
||||
| `skill/whatsapp` | `main` | WhatsApp channel |
|
||||
| `skill/telegram` | `main` | Telegram channel |
|
||||
| `skill/slack` | `main` | Slack channel |
|
||||
| `skill/discord` | `main` | Discord channel |
|
||||
| `skill/gmail` | `main` | Gmail channel |
|
||||
| `skill/voice-transcription` | `skill/whatsapp` | OpenAI Whisper voice transcription |
|
||||
| `skill/image-vision` | `skill/whatsapp` | Image attachment processing |
|
||||
| `skill/pdf-reader` | `skill/whatsapp` | PDF attachment reading |
|
||||
| `skill/local-whisper` | `skill/voice-transcription` | Local whisper.cpp transcription |
|
||||
| `skill/ollama-tool` | `main` | Ollama MCP server for local models |
|
||||
| `skill/apple-container` | `main` | Apple Container runtime |
|
||||
| `skill/reactions` | `main` | WhatsApp emoji reactions |
|
||||
|
||||
### What was removed
|
||||
|
||||
- `skills-engine/` directory (entire engine)
|
||||
- `scripts/apply-skill.ts`, `scripts/uninstall-skill.ts`, `scripts/rebase.ts`
|
||||
- `scripts/fix-skill-drift.ts`, `scripts/validate-all-skills.ts`
|
||||
- `.github/workflows/skill-drift.yml`, `.github/workflows/skill-pr.yml`
|
||||
- All `add/`, `modify/`, `tests/`, and `manifest.yaml` from skill directories
|
||||
- `.nanoclaw/` state directory
|
||||
|
||||
Operational skills (`setup`, `debug`, `update-nanoclaw`, `customize`, `update-skills`) remain on main in `.claude/skills/`.
|
||||
|
||||
## What Changes
|
||||
|
||||
### README Quick Start
|
||||
|
||||
Before:
|
||||
```bash
|
||||
git clone https://github.com/qwibitai/NanoClaw.git
|
||||
cd NanoClaw
|
||||
claude
|
||||
```
|
||||
|
||||
After:
|
||||
```
|
||||
1. Fork qwibitai/nanoclaw on GitHub
|
||||
2. git clone https://github.com/<you>/nanoclaw.git
|
||||
3. cd nanoclaw
|
||||
4. claude
|
||||
5. /setup
|
||||
```
|
||||
|
||||
### Setup skill (`/setup`)
|
||||
|
||||
Updates to the setup flow:
|
||||
|
||||
- Check if `upstream` remote exists; if not, add it: `git remote add upstream https://github.com/qwibitai/nanoclaw.git`
|
||||
- Check if `origin` points to the user's fork (not qwibitai). If it points to qwibitai, guide them through the fork migration.
|
||||
- **Install marketplace plugin:** `claude plugin install nanoclaw-skills@nanoclaw-skills --scope project` — makes all feature skills available (hot-loaded, no restart)
|
||||
- **Ask which channels to add:** present channel options (Discord, Telegram, Slack, WhatsApp, Gmail), run corresponding `/add-*` skills for selected channels
|
||||
- **Offer dependent skills:** after a channel is set up, offer relevant add-ons (e.g., Agent Swarm after Telegram, voice transcription after WhatsApp)
|
||||
- **Optionally enable community marketplaces:** ask if the user wants community skills, install those marketplace plugins too
|
||||
|
||||
### `.claude/settings.json`
|
||||
|
||||
Marketplace configuration so the official marketplace is auto-registered:
|
||||
|
||||
```json
|
||||
{
|
||||
"extraKnownMarketplaces": {
|
||||
"nanoclaw-skills": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "qwibitai/nanoclaw-skills"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Skills directory on main
|
||||
|
||||
The `.claude/skills/` directory on `main` retains only operational skills (setup, debug, update-nanoclaw, customize, update-skills). Feature skills (add-discord, add-telegram, etc.) live in the marketplace repo, installed via `claude plugin install` during `/setup` or `/customize`.
|
||||
|
||||
### Skills engine removal
|
||||
|
||||
The following can be removed:
|
||||
|
||||
- `skills-engine/` — entire directory (apply, merge, replay, state, backup, etc.)
|
||||
- `scripts/apply-skill.ts`
|
||||
- `scripts/uninstall-skill.ts`
|
||||
- `scripts/fix-skill-drift.ts`
|
||||
- `scripts/validate-all-skills.ts`
|
||||
- `.nanoclaw/` — state directory
|
||||
- `add/` and `modify/` subdirectories from all skill directories
|
||||
- Feature skill SKILL.md files from `.claude/skills/` on main (they now live in the marketplace)
|
||||
|
||||
Operational skills (`setup`, `debug`, `update-nanoclaw`, `customize`, `update-skills`) remain on main in `.claude/skills/`.
|
||||
|
||||
### New infrastructure
|
||||
|
||||
- **Marketplace repo** (`qwibitai/nanoclaw-skills`) — single Claude Code plugin bundling SKILL.md files for all feature skills
|
||||
- **CI GitHub Action** — merge-forward `main` into all `skill/*` branches on every push to `main`, using Claude (Haiku) for conflict resolution
|
||||
- **`/update-skills` skill** — checks for and applies skill branch updates using git history
|
||||
- **`CONTRIBUTORS.md`** — tracks skill contributors
|
||||
|
||||
### Update skill (`/update-nanoclaw`)
|
||||
|
||||
The update skill gets simpler with the branch-based approach. The old skills engine required replaying all applied skills after merging core updates — that entire step disappears. Skill changes are already in the user's git history, so `git merge upstream/main` just works.
|
||||
|
||||
**What stays the same:**
|
||||
- Preflight (clean working tree, upstream remote)
|
||||
- Backup branch + tag
|
||||
- Preview (git log, git diff, file buckets)
|
||||
- Merge/cherry-pick/rebase options
|
||||
- Conflict preview (dry-run merge)
|
||||
- Conflict resolution
|
||||
- Build + test validation
|
||||
- Rollback instructions
|
||||
|
||||
**What's removed:**
|
||||
- Skill replay step (was needed by the old skills engine to re-apply skills after core update)
|
||||
- Re-running structured operations (npm deps, env vars — these are part of git history now)
|
||||
|
||||
**What's added:**
|
||||
- Optional step at the end: "Check for skill updates?" which runs the `/update-skills` logic
|
||||
- This checks whether any previously-merged skill branches have new commits (bug fixes, improvements to the skill itself — not just merge-forwards from main)
|
||||
|
||||
**Why users don't need to re-merge skills after a core update:**
|
||||
When the user merged a skill branch, those changes became part of their git history. When they later merge `upstream/main`, git performs a normal three-way merge — the skill changes in their tree are untouched, and only core changes are brought in. The merge-forward CI ensures skill branches stay compatible with latest main, but that's for new users applying the skill fresh. Existing users who already merged the skill don't need to do anything.
|
||||
|
||||
Users only need to re-merge a skill branch if the skill itself was updated (not just merged-forward with main). The `/update-skills` check detects this.
|
||||
|
||||
## Discord Announcement
|
||||
|
||||
### For existing users
|
||||
|
||||
> **Skills are now git branches**
|
||||
>
|
||||
> We've simplified how skills work in NanoClaw. Instead of a custom skills engine, skills are now git branches that you merge in.
|
||||
>
|
||||
> **What this means for you:**
|
||||
> - Applying a skill: `git fetch upstream skill/discord && git merge upstream/skill/discord`
|
||||
> - Updating core: `git fetch upstream main && git merge upstream/main`
|
||||
> - Checking for skill updates: `/update-skills`
|
||||
> - No more `.nanoclaw/` state directory or skills engine
|
||||
>
|
||||
> **We now recommend forking instead of cloning.** This gives you a remote to push your customizations to.
|
||||
>
|
||||
> **If you currently have a clone with local changes**, migrate to a fork:
|
||||
> 1. Fork `qwibitai/nanoclaw` on GitHub
|
||||
> 2. Run:
|
||||
> ```
|
||||
> git remote rename origin upstream
|
||||
> git remote add origin https://github.com/<you>/nanoclaw.git
|
||||
> git push --force origin main
|
||||
> ```
|
||||
> This works even if you're way behind — just push your current state.
|
||||
>
|
||||
> **If you previously applied skills via the old system**, your code changes are already in your working tree — nothing to redo. You can delete the `.nanoclaw/` directory. Future skills and updates use the branch-based approach.
|
||||
>
|
||||
> **Discovering skills:** Skills are now available through Claude Code's plugin marketplace. Run `/plugin` in Claude Code to browse and install available skills.
|
||||
|
||||
### For skill contributors
|
||||
|
||||
> **Contributing skills**
|
||||
>
|
||||
> To contribute a skill:
|
||||
> 1. Fork `qwibitai/nanoclaw`
|
||||
> 2. Branch from `main` and make your code changes
|
||||
> 3. Open a regular PR
|
||||
>
|
||||
> That's it. We'll create a `skill/<name>` branch from your PR, add you to CONTRIBUTORS.md, and add the SKILL.md to the marketplace. CI automatically keeps skill branches merged-forward with `main` using Claude to resolve any conflicts.
|
||||
>
|
||||
> **Want to run your own skill marketplace?** Maintain skill branches on your fork and create a marketplace repo. Open a PR to add it to NanoClaw's auto-discovered marketplaces — or users can add it manually via `/plugin marketplace add`.
|
||||
Reference in New Issue
Block a user