docs: update skills to use Docker commands after runtime migration (#325)

All skills now reference Docker CLI instead of Apple Container CLI.
Setup skill defaults to Docker with optional /convert-to-apple-container.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-20 14:06:31 +02:00
committed by GitHub
parent a7faac664a
commit 6b9b3a12c9
7 changed files with 41 additions and 161 deletions

View File

@@ -33,7 +33,7 @@ Added Discord as a channel option alongside WhatsApp, introducing multi-channel
- The `runAgent` function is completely unchanged - The `runAgent` function is completely unchanged
- State management (loadState/saveState) is unchanged - State management (loadState/saveState) is unchanged
- Recovery logic is unchanged - Recovery logic is unchanged
- Apple Container check is unchanged (ensureContainerSystemRunning) - Container runtime check is unchanged (ensureContainerSystemRunning)
## Must-keep ## Must-keep
- The `escapeXml` and `formatMessages` re-exports - The `escapeXml` and `formatMessages` re-exports

View File

@@ -13,7 +13,7 @@ Adds Parallel AI MCP integration to NanoClaw for advanced web research capabilit
User must have: User must have:
1. Parallel AI API key from https://platform.parallel.ai 1. Parallel AI API key from https://platform.parallel.ai
2. NanoClaw already set up and running 2. NanoClaw already set up and running
3. Container system working (Apple Container or Docker) 3. Docker installed and running
## Implementation Steps ## Implementation Steps
@@ -224,14 +224,9 @@ Build the container with updated agent runner:
./container/build.sh ./container/build.sh
``` ```
The build script will automatically:
- Try Apple Container first
- Fall back to Docker if Rosetta is required
- Import to Apple Container
Verify the build: Verify the build:
```bash ```bash
echo '{}' | container run -i --entrypoint /bin/echo nanoclaw-agent:latest "Container OK" echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "Container OK"
``` ```
### 7. Restart Service ### 7. Restart Service

View File

@@ -40,7 +40,7 @@ Refactored from single WhatsApp channel to multi-channel architecture using the
- The `runAgent` function is completely unchanged - The `runAgent` function is completely unchanged
- State management (loadState/saveState) is unchanged - State management (loadState/saveState) is unchanged
- Recovery logic is unchanged - Recovery logic is unchanged
- Apple Container check is unchanged (ensureContainerSystemRunning) - Container runtime check is unchanged (ensureContainerSystemRunning)
## Must-keep ## Must-keep
- The `escapeXml` and `formatMessages` re-exports - The `escapeXml` and `formatMessages` re-exports

View File

@@ -296,86 +296,20 @@ if (registeredGroups[chatJid]) {
} }
``` ```
### Step 6: Fix Orphan Container Cleanup (CRITICAL) ### Step 6: Build and Restart
**This step is essential.** When the NanoClaw service restarts (e.g., `launchctl kickstart -k`), the running container is detached but NOT killed. The new service instance spawns a fresh container, but the orphan keeps running and shares the same IPC mount directory. Both containers race to read IPC input files, causing the new container to randomly miss messages — making it appear like the agent doesn't respond.
The existing cleanup code in `ensureContainerSystemRunning()` in `src/index.ts` uses `container ls --format {{.Names}}` which **silently fails** on Apple Container (only `json` and `table` are valid format options). The catch block swallows the error, so orphans are never cleaned up.
Find the orphan cleanup block in `ensureContainerSystemRunning()` (the section starting with `// Kill and clean up orphaned NanoClaw containers from previous runs`) and replace it with:
```typescript
// Kill and clean up orphaned NanoClaw containers from previous runs
try {
const listJson = execSync('container ls -a --format json', {
stdio: ['pipe', 'pipe', 'pipe'],
encoding: 'utf-8',
});
const containers = JSON.parse(listJson) as Array<{ configuration: { id: string }; status: string }>;
const nanoclawContainers = containers.filter(
(c) => c.configuration.id.startsWith('nanoclaw-'),
);
const running = nanoclawContainers
.filter((c) => c.status === 'running')
.map((c) => c.configuration.id);
if (running.length > 0) {
execSync(`container stop ${running.join(' ')}`, { stdio: 'pipe' });
logger.info({ count: running.length }, 'Stopped orphaned containers');
}
const allNames = nanoclawContainers.map((c) => c.configuration.id);
if (allNames.length > 0) {
execSync(`container rm ${allNames.join(' ')}`, { stdio: 'pipe' });
logger.info({ count: allNames.length }, 'Cleaned up stopped containers');
}
} catch {
// No containers or cleanup not supported
}
```
### Step 7: Build and Restart
```bash ```bash
npm run build npm run build
```
Before restarting the service, kill any orphaned containers manually to ensure a clean slate:
```bash
container ls -a --format json | python3 -c "
import sys, json
data = json.load(sys.stdin)
nc = [c['configuration']['id'] for c in data if c['configuration']['id'].startswith('nanoclaw-')]
if nc: print(' '.join(nc))
" | xargs -r container stop 2>/dev/null
container ls -a --format json | python3 -c "
import sys, json
data = json.load(sys.stdin)
nc = [c['configuration']['id'] for c in data if c['configuration']['id'].startswith('nanoclaw-')]
if nc: print(' '.join(nc))
" | xargs -r container rm 2>/dev/null
echo "Orphaned containers cleaned"
```
Now restart the service:
```bash
launchctl kickstart -k gui/$(id -u)/com.nanoclaw launchctl kickstart -k gui/$(id -u)/com.nanoclaw
``` ```
Verify it started with exactly one (or zero, before first message) nanoclaw container: Verify it started:
```bash ```bash
sleep 3 && launchctl list | grep nanoclaw sleep 3 && launchctl list | grep nanoclaw
container ls -a --format json | python3 -c "
import sys, json
data = json.load(sys.stdin)
nc = [c for c in data if c['configuration']['id'].startswith('nanoclaw-')]
print(f'{len(nc)} nanoclaw container(s)')
for c in nc: print(f' {c[\"configuration\"][\"id\"]} - {c[\"status\"]}')
"
``` ```
### Step 8: Test Voice Transcription ### Step 7: Test Voice Transcription
Tell the user: Tell the user:
@@ -431,41 +365,6 @@ The architecture supports multiple providers. To add Groq, Deepgram, or local Wh
## Troubleshooting ## Troubleshooting
### Agent doesn't respond to voice messages (or any messages after a voice note)
**Most likely cause: orphaned containers.** When the service restarts, the previous container keeps running and races to consume IPC messages. Check:
```bash
container ls -a --format json | python3 -c "
import sys, json
data = json.load(sys.stdin)
nc = [c for c in data if c['configuration']['id'].startswith('nanoclaw-')]
print(f'{len(nc)} nanoclaw container(s):')
for c in nc: print(f' {c[\"configuration\"][\"id\"]} - {c[\"status\"]}')
"
```
If you see more than one running container, kill the orphans:
```bash
# Stop all nanoclaw containers, then restart the service
container ls -a --format json | python3 -c "
import sys, json
data = json.load(sys.stdin)
running = [c['configuration']['id'] for c in data if c['configuration']['id'].startswith('nanoclaw-') and c['status'] == 'running']
if running: print(' '.join(running))
" | xargs -r container stop 2>/dev/null
container ls -a --format json | python3 -c "
import sys, json
data = json.load(sys.stdin)
nc = [c['configuration']['id'] for c in data if c['configuration']['id'].startswith('nanoclaw-')]
if nc: print(' '.join(nc))
" | xargs -r container rm 2>/dev/null
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
**Root cause:** The `ensureContainerSystemRunning()` function previously used `container ls --format {{.Names}}` which silently fails on Apple Container (only `json` and `table` formats are supported). Step 6 of this skill fixes this. If you haven't applied Step 6, the orphan problem will recur on every restart.
### "Transcription unavailable" or "Transcription failed" ### "Transcription unavailable" or "Transcription failed"
Check logs for specific errors: Check logs for specific errors:

View File

@@ -86,8 +86,8 @@ cat .env # Should show one of:
To verify env vars are reaching the container: To verify env vars are reaching the container:
```bash ```bash
echo '{}' | container run -i \ echo '{}' | docker run -i \
--mount type=bind,source=$(pwd)/data/env,target=/workspace/env-dir,readonly \ -v $(pwd)/data/env:/workspace/env-dir:ro \
--entrypoint /bin/bash nanoclaw-agent:latest \ --entrypoint /bin/bash nanoclaw-agent:latest \
-c 'export $(cat /workspace/env-dir/env | xargs); echo "OAuth: ${#CLAUDE_CODE_OAUTH_TOKEN} chars, API: ${#ANTHROPIC_API_KEY} chars"' -c 'export $(cat /workspace/env-dir/env | xargs); echo "OAuth: ${#CLAUDE_CODE_OAUTH_TOKEN} chars, API: ${#ANTHROPIC_API_KEY} chars"'
``` ```
@@ -95,19 +95,19 @@ echo '{}' | container run -i \
### 3. Mount Issues ### 3. Mount Issues
**Container mount notes:** **Container mount notes:**
- Only mounts directories, not individual files - Docker supports both `-v` and `--mount` syntax
- `-v` syntax may NOT support `:ro` suffix - use `--mount` for readonly: - Use `:ro` suffix for readonly mounts:
```bash ```bash
# Readonly: use --mount # Readonly
--mount "type=bind,source=/path,target=/container/path,readonly" -v /path:/container/path:ro
# Read-write: use -v # Read-write
-v /path:/container/path -v /path:/container/path
``` ```
To check what's mounted inside a container: To check what's mounted inside a container:
```bash ```bash
container run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c 'ls -la /workspace/' docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c 'ls -la /workspace/'
``` ```
Expected structure: Expected structure:
@@ -129,7 +129,7 @@ Expected structure:
The container runs as user `node` (uid 1000). Check ownership: The container runs as user `node` (uid 1000). Check ownership:
```bash ```bash
container run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c ' docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c '
whoami whoami
ls -la /workspace/ ls -la /workspace/
ls -la /app/ ls -la /app/
@@ -152,7 +152,7 @@ grep -A3 "Claude sessions" src/container-runner.ts
**Verify sessions are accessible:** **Verify sessions are accessible:**
```bash ```bash
container run --rm --entrypoint /bin/bash \ docker run --rm --entrypoint /bin/bash \
-v ~/.claude:/home/node/.claude \ -v ~/.claude:/home/node/.claude \
nanoclaw-agent:latest -c ' nanoclaw-agent:latest -c '
echo "HOME=$HOME" echo "HOME=$HOME"
@@ -183,8 +183,8 @@ cp .env data/env/env
# Run test query # Run test query
echo '{"prompt":"What is 2+2?","groupFolder":"test","chatJid":"test@g.us","isMain":false}' | \ echo '{"prompt":"What is 2+2?","groupFolder":"test","chatJid":"test@g.us","isMain":false}' | \
container run -i \ docker run -i \
--mount "type=bind,source=$(pwd)/data/env,target=/workspace/env-dir,readonly" \ -v $(pwd)/data/env:/workspace/env-dir:ro \
-v $(pwd)/groups/test:/workspace/group \ -v $(pwd)/groups/test:/workspace/group \
-v $(pwd)/data/ipc:/workspace/ipc \ -v $(pwd)/data/ipc:/workspace/ipc \
nanoclaw-agent:latest nanoclaw-agent:latest
@@ -192,8 +192,8 @@ echo '{"prompt":"What is 2+2?","groupFolder":"test","chatJid":"test@g.us","isMai
### Test Claude Code directly: ### Test Claude Code directly:
```bash ```bash
container run --rm --entrypoint /bin/bash \ docker run --rm --entrypoint /bin/bash \
--mount "type=bind,source=$(pwd)/data/env,target=/workspace/env-dir,readonly" \ -v $(pwd)/data/env:/workspace/env-dir:ro \
nanoclaw-agent:latest -c ' nanoclaw-agent:latest -c '
export $(cat /workspace/env-dir/env | xargs) export $(cat /workspace/env-dir/env | xargs)
claude -p "Say hello" --dangerously-skip-permissions --allowedTools "" claude -p "Say hello" --dangerously-skip-permissions --allowedTools ""
@@ -202,7 +202,7 @@ container run --rm --entrypoint /bin/bash \
### Interactive shell in container: ### Interactive shell in container:
```bash ```bash
container run --rm -it --entrypoint /bin/bash nanoclaw-agent:latest docker run --rm -it --entrypoint /bin/bash nanoclaw-agent:latest
``` ```
## SDK Options Reference ## SDK Options Reference
@@ -235,7 +235,7 @@ npm run build
./container/build.sh ./container/build.sh
# Or force full rebuild # Or force full rebuild
container builder prune -af docker builder prune -af
./container/build.sh ./container/build.sh
``` ```
@@ -243,10 +243,10 @@ container builder prune -af
```bash ```bash
# List images # List images
container images docker images
# Check what's in the image # Check what's in the image
container run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c ' docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c '
echo "=== Node version ===" echo "=== Node version ==="
node --version node --version
@@ -327,10 +327,10 @@ echo -e "\n2. Env file copied for container?"
[ -f data/env/env ] && echo "OK" || echo "MISSING - will be created on first run" [ -f data/env/env ] && echo "OK" || echo "MISSING - will be created on first run"
echo -e "\n3. Container runtime running?" echo -e "\n3. Container runtime running?"
container system status &>/dev/null && echo "OK" || echo "NOT RUNNING - NanoClaw should auto-start it; check logs" docker info &>/dev/null && echo "OK" || echo "NOT RUNNING - start Docker Desktop (macOS) or sudo systemctl start docker (Linux)"
echo -e "\n4. Container image exists?" echo -e "\n4. Container image exists?"
echo '{}' | container run -i --entrypoint /bin/echo nanoclaw-agent:latest "OK" 2>/dev/null || echo "MISSING - run ./container/build.sh" echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "OK" 2>/dev/null || echo "MISSING - run ./container/build.sh"
echo -e "\n5. Session mount path correct?" echo -e "\n5. Session mount path correct?"
grep -q "/home/node/.claude" src/container-runner.ts 2>/dev/null && echo "OK" || echo "WRONG - should mount to /home/node/.claude/, not /root/.claude/" grep -q "/home/node/.claude" src/container-runner.ts 2>/dev/null && echo "OK" || echo "WRONG - should mount to /home/node/.claude/, not /root/.claude/"

View File

@@ -43,40 +43,26 @@ Only ask the user for help if multiple retries fail with the same error.
### 3a. Choose runtime ### 3a. Choose runtime
Use the environment check results from step 1 to decide which runtime to use: Check the preflight results for `APPLE_CONTAINER` and `DOCKER`.
- PLATFORM=linux → Docker **If APPLE_CONTAINER=installed** (macOS only): Ask the user which runtime they'd like to use — Docker (default, cross-platform) or Apple Container (native macOS). If they choose Apple Container, run `/convert-to-apple-container` now before continuing, then skip to 3b.
- PLATFORM=macos + APPLE_CONTAINER=installed → apple-container
- PLATFORM=macos + DOCKER=running + APPLE_CONTAINER=not_found → Docker
- PLATFORM=macos + DOCKER=installed_not_running → start Docker: `open -a Docker`. Wait 15s, re-check with `docker info`. If still not running, tell the user Docker is starting up and poll a few more times.
- Neither available → AskUserQuestion: Apple Container (recommended for macOS) vs Docker?
- Apple Container: tell user to download from https://github.com/apple/container/releases and install the .pkg. Wait for confirmation, then verify with `container --version`.
- Docker on macOS: install via `brew install --cask docker`, then `open -a Docker` and wait for it to start. If brew not available, direct to Docker Desktop download.
- Docker on Linux: install with `curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER`. Note: user may need to log out/in for group membership.
### 3b. Docker conversion gate (REQUIRED before building) **If APPLE_CONTAINER=not_found**: Use Docker (the default). Proceed to install/start Docker below.
**If the chosen runtime is Docker**, you MUST check whether the source code has already been converted from Apple Container to Docker. Do NOT skip this step. Run: ### 3a-docker. Install Docker
```bash - DOCKER=running → continue to 3b
grep -q 'container system status' src/index.ts && echo "NEEDS_CONVERSION" || echo "ALREADY_CONVERTED" - DOCKER=installed_not_running → start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Wait 15s, re-check with `docker info`. If still not running, tell the user Docker is starting up and poll a few more times.
``` - DOCKER=not_found → **ask the user for confirmation before installing.** Tell them Docker is required for running agents and ask if they'd like you to install it. If confirmed:
- macOS: install via `brew install --cask docker`, then `open -a Docker` and wait for it to start. If brew not available, direct to Docker Desktop download at https://docker.com/products/docker-desktop
- Linux: install with `curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER`. Note: user may need to log out/in for group membership.
Check these three files for Apple Container references: ### 3b. Build and test
- `src/index.ts` — look for `container system status` or `ensureContainerSystemRunning`
- `src/container-runner.ts` — look for `spawn('container'`
- `container/build.sh` — look for `container build`
**If ANY of those Apple Container references exist**, the source code has NOT been converted. You MUST run the `/convert-to-docker` skill NOW, before proceeding to the build step. Do not attempt to build the container image until the conversion is complete.
**If none of those references exist** (i.e. the code already uses `docker info`, `spawn('docker'`, `docker build`), the conversion has already been done. Continue to 3c.
### 3c. Build and test
Run `./.claude/skills/setup/scripts/03-setup-container.sh --runtime <chosen>` and parse the status block. Run `./.claude/skills/setup/scripts/03-setup-container.sh --runtime <chosen>` and parse the status block.
**If BUILD_OK=false:** Read `logs/setup.log` tail for the build error. **If BUILD_OK=false:** Read `logs/setup.log` tail for the build error.
- If it's a cache issue (stale layers): run `container builder stop && container builder rm && container builder start` (Apple Container) or `docker builder prune -f` (Docker), then retry. - If it's a cache issue (stale layers): run `docker builder prune -f`, then retry.
- If Dockerfile syntax or missing files: diagnose from the log and fix. - If Dockerfile syntax or missing files: diagnose from the log and fix.
- Retry the build script after fixing. - Retry the build script after fixing.

View File

@@ -200,11 +200,11 @@ Add to the end of tools array (before the closing `]`):
Change build context from `container/` to project root (required to access `.claude/skills/`): Change build context from `container/` to project root (required to access `.claude/skills/`):
```bash ```bash
# Find: # Find:
container build -t "${IMAGE_NAME}:${TAG}" . docker build -t "${IMAGE_NAME}:${TAG}" .
# Replace with: # Replace with:
cd "$SCRIPT_DIR/.." cd "$SCRIPT_DIR/.."
container build -t "${IMAGE_NAME}:${TAG}" -f container/Dockerfile . docker build -t "${IMAGE_NAME}:${TAG}" -f container/Dockerfile .
``` ```
--- ---
@@ -402,7 +402,7 @@ If MCP tools not found in container:
./container/build.sh 2>&1 | grep -i skill ./container/build.sh 2>&1 | grep -i skill
# Check container has the file # Check container has the file
container run nanoclaw-agent ls -la /app/src/skills/ docker run nanoclaw-agent ls -la /app/src/skills/
``` ```
## Security ## Security