refactor: extract runtime-specific code into src/container-runtime.ts (#321)

Move all container-runtime-specific logic (binary name, mount args,
stop command, startup check, orphan cleanup) into a single file so
swapping runtimes only requires replacing this one file.

Neutralize "Apple Container" references in comments and docs that
would become incorrect after a runtime swap. References that list
both runtimes as options are left unchanged.

No behavior change — Apple Container remains the default runtime.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-20 13:13:55 +02:00
committed by GitHub
parent 8fd67916b3
commit c6e1bfecc6
11 changed files with 305 additions and 101 deletions

View File

@@ -20,7 +20,7 @@ The entire codebase should be something you can read and understand. One Node.js
### Security Through True Isolation
Instead of application-level permission systems trying to prevent agents from accessing things, agents run in actual Linux containers (Apple Container). 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.
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
@@ -71,7 +71,7 @@ A personal Claude assistant accessible via WhatsApp, with minimal custom code.
**Core components:**
- **Claude Agent SDK** as the core agent
- **Apple Container** for isolated agent execution (Linux VMs)
- **Containers** for isolated agent execution (Linux VMs)
- **WhatsApp** as the primary I/O channel
- **Persistent memory** per conversation and globally
- **Scheduled tasks** that run Claude and can message back
@@ -104,7 +104,7 @@ A personal Claude assistant accessible via WhatsApp, with minimal custom code.
- Sessions auto-compact when context gets too long, preserving critical information
### Container Isolation
- All agents run inside Apple Container (lightweight Linux VMs)
- All agents run inside containers (lightweight Linux VMs)
- Each agent invocation spawns a container with mounted directories
- Containers provide filesystem isolation - agents can only see mounted paths
- Bash access is safe because commands run inside the container, not on the host

View File

@@ -13,7 +13,7 @@
### 1. Container Isolation (Primary Boundary)
Agents execute in Apple Container (lightweight Linux VMs), providing:
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)

View File

@@ -45,7 +45,7 @@ A personal Claude assistant accessible via WhatsApp, with persistent memory per
│ │ spawns container │
│ ▼ │
├─────────────────────────────────────────────────────────────────────┤
APPLE CONTAINER (Linux VM) │
CONTAINER (Linux VM)
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ AGENT RUNNER │ │
@@ -75,7 +75,7 @@ A personal Claude assistant accessible via WhatsApp, with persistent memory per
|-----------|------------|---------|
| WhatsApp Connection | Node.js (@whiskeysockets/baileys) | Connect to WhatsApp, send/receive messages |
| Message Storage | SQLite (better-sqlite3) | Store messages for polling |
| Container Runtime | Apple Container | Isolated Linux VMs for agent execution |
| 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 |
| Browser Automation | agent-browser + Chromium | Web interaction and screenshots |
| Runtime | Node.js 20+ | Host process for routing and scheduling |
@@ -111,7 +111,7 @@ nanoclaw/
│ ├── mount-security.ts # Mount allowlist validation for containers
│ ├── whatsapp-auth.ts # Standalone WhatsApp authentication
│ ├── task-scheduler.ts # Runs scheduled tasks when due
│ └── container-runner.ts # Spawns agents in Apple Containers
│ └── container-runner.ts # Spawns agents in containers
├── container/
│ ├── Dockerfile # Container image (runs as 'node' user, includes Claude Code CLI)
@@ -196,7 +196,7 @@ export const MAX_CONCURRENT_CONTAINERS = Math.max(1, parseInt(process.env.MAX_CO
export const TRIGGER_PATTERN = new RegExp(`^@${ASSISTANT_NAME}\\b`, 'i');
```
**Note:** Paths must be absolute for Apple Container volume mounts to work correctly.
**Note:** Paths must be absolute for container volume mounts to work correctly.
### Container Configuration
@@ -223,7 +223,7 @@ registerGroup("1234567890@g.us", {
Additional mounts appear at `/workspace/extra/{containerPath}` inside the container.
**Apple Container mount syntax note:** Read-write mounts use `-v host:container`, but readonly mounts require `--mount "type=bind,source=...,target=...,readonly"` (the `:ro` suffix doesn't work).
**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).
### Claude Authentication
@@ -240,7 +240,7 @@ The token can be extracted from `~/.claude/.credentials.json` if you're logged i
ANTHROPIC_API_KEY=sk-ant-api03-...
```
Only the authentication variables (`CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY`) are extracted from `.env` and written to `data/env/env`, then mounted into the container at `/workspace/env-dir/env` and sourced by the entrypoint script. This ensures other environment variables in `.env` are not exposed to the agent. This workaround is needed because Apple Container loses `-e` environment variables when using `-i` (interactive mode with piped stdin).
Only the authentication variables (`CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY`) are extracted from `.env` and written to `data/env/env`, then mounted into the container at `/workspace/env-dir/env` and sourced by the entrypoint script. This ensures other environment variables in `.env` are not exposed to the agent. This workaround is needed because some container runtimes lose `-e` environment variables when using `-i` (interactive mode with piped stdin).
### Changing the Assistant Name
@@ -484,7 +484,7 @@ NanoClaw runs as a single macOS launchd service.
### Startup Sequence
When NanoClaw starts, it:
1. **Ensures Apple Container system is running** - Automatically starts it if needed; kills orphaned NanoClaw containers from previous runs
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`):
@@ -557,7 +557,7 @@ tail -f logs/nanoclaw.log
### Container Isolation
All agents run inside Apple Container (lightweight Linux VMs), providing:
All agents run inside containers (lightweight Linux VMs), providing:
- **Filesystem isolation**: Agents can only access mounted directories
- **Safe Bash access**: Commands run inside the container, not on your Mac
- **Network isolation**: Can be configured per-container if needed
@@ -605,7 +605,7 @@ chmod 700 groups/
| Issue | Cause | Solution |
|-------|-------|----------|
| No response to messages | Service not running | Check `launchctl list | grep nanoclaw` |
| "Claude Code process exited with code 1" | Apple Container failed to start | Check logs; NanoClaw auto-starts container system but may fail |
| "Claude Code process exited with code 1" | Container runtime failed to start | Check logs; NanoClaw auto-starts container runtime but may fail |
| "Claude Code process exited with code 1" | Session mount path wrong | Ensure mount is to `/home/node/.claude/` not `/root/.claude/` |
| Session not continuing | Session ID not saved | Check SQLite: `sqlite3 store/messages.db "SELECT * FROM sessions"` |
| Session not continuing | Mount path mismatch | Container user is `node` with HOME=/home/node; sessions must be at `/home/node/.claude/` |