feat: add /convert-to-apple-container skill, remove /convert-to-docker (#324)
Docker is now the default runtime. The /convert-to-apple-container skill uses the new skills engine format (manifest.yaml, modify/, intent files, tests/) to switch to Apple Container on macOS. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
175
.claude/skills/convert-to-apple-container/SKILL.md
Normal file
175
.claude/skills/convert-to-apple-container/SKILL.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
name: convert-to-apple-container
|
||||||
|
description: Switch from Docker to Apple Container for macOS-native container isolation. Use when the user wants Apple Container instead of Docker, or is setting up on macOS and prefers the native runtime. Triggers on "apple container", "convert to apple container", "switch to apple container", or "use apple container".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Convert to Apple Container
|
||||||
|
|
||||||
|
This skill switches NanoClaw's container runtime from Docker to Apple Container (macOS-only). It uses the skills engine for deterministic code changes, then walks through verification.
|
||||||
|
|
||||||
|
**What this changes:**
|
||||||
|
- Container runtime binary: `docker` → `container`
|
||||||
|
- Mount syntax: `-v path:path:ro` → `--mount type=bind,source=...,target=...,readonly`
|
||||||
|
- Startup check: `docker info` → `container system status` (with auto-start)
|
||||||
|
- Orphan detection: `docker ps --filter` → `container ls --format json`
|
||||||
|
- Build script default: `docker` → `container`
|
||||||
|
|
||||||
|
**What stays the same:**
|
||||||
|
- Dockerfile (shared by both runtimes)
|
||||||
|
- Container runner code (`src/container-runner.ts`)
|
||||||
|
- Mount security/allowlist validation
|
||||||
|
- All other functionality
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Verify Apple Container is installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
container --version && echo "Apple Container ready" || echo "Install Apple Container first"
|
||||||
|
```
|
||||||
|
|
||||||
|
If not installed:
|
||||||
|
- Download from https://github.com/apple/container/releases
|
||||||
|
- Install the `.pkg` file
|
||||||
|
- Verify: `container --version`
|
||||||
|
|
||||||
|
Apple Container requires macOS. It does not work on Linux.
|
||||||
|
|
||||||
|
## Phase 1: Pre-flight
|
||||||
|
|
||||||
|
### Check if already applied
|
||||||
|
|
||||||
|
Read `.nanoclaw/state.yaml`. If `convert-to-apple-container` is in `applied_skills`, skip to Phase 3 (Verify). The code changes are already in place.
|
||||||
|
|
||||||
|
### Check current runtime
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep "CONTAINER_RUNTIME_BIN" src/container-runtime.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
If it already shows `'container'`, the runtime is already Apple Container. Skip to Phase 3.
|
||||||
|
|
||||||
|
## Phase 2: Apply Code Changes
|
||||||
|
|
||||||
|
Run the skills engine to apply this skill's code package. The package files are in this directory alongside this SKILL.md.
|
||||||
|
|
||||||
|
### Initialize skills system (if needed)
|
||||||
|
|
||||||
|
If `.nanoclaw/` directory doesn't exist yet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/apply-skill.ts --init
|
||||||
|
```
|
||||||
|
|
||||||
|
Or call `initSkillsSystem()` from `skills-engine/migrate.ts`.
|
||||||
|
|
||||||
|
### Apply the skill
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/apply-skill.ts .claude/skills/convert-to-apple-container
|
||||||
|
```
|
||||||
|
|
||||||
|
This deterministically:
|
||||||
|
- Replaces `src/container-runtime.ts` with the Apple Container implementation
|
||||||
|
- Replaces `src/container-runtime.test.ts` with Apple Container-specific tests
|
||||||
|
- Updates `container/build.sh` to default to `container` runtime
|
||||||
|
- Records the application in `.nanoclaw/state.yaml`
|
||||||
|
|
||||||
|
If the apply reports merge conflicts, read the intent files:
|
||||||
|
- `modify/src/container-runtime.ts.intent.md` — what changed and invariants
|
||||||
|
- `modify/container/build.sh.intent.md` — what changed for build script
|
||||||
|
|
||||||
|
### Validate code changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
All tests must pass and build must be clean before proceeding.
|
||||||
|
|
||||||
|
## Phase 3: Verify
|
||||||
|
|
||||||
|
### Ensure Apple Container runtime is running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
container system status || container system start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the container image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./container/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test basic execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo '{}' | container run -i --entrypoint /bin/echo nanoclaw-agent:latest "Container OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test readonly mounts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /tmp/test-ro && echo "test" > /tmp/test-ro/file.txt
|
||||||
|
container run --rm --entrypoint /bin/bash \
|
||||||
|
--mount type=bind,source=/tmp/test-ro,target=/test,readonly \
|
||||||
|
nanoclaw-agent:latest \
|
||||||
|
-c "cat /test/file.txt && touch /test/new.txt 2>&1 || echo 'Write blocked (expected)'"
|
||||||
|
rm -rf /tmp/test-ro
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Read succeeds, write fails with "Read-only file system".
|
||||||
|
|
||||||
|
### Test read-write mounts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /tmp/test-rw
|
||||||
|
container run --rm --entrypoint /bin/bash \
|
||||||
|
-v /tmp/test-rw:/test \
|
||||||
|
nanoclaw-agent:latest \
|
||||||
|
-c "echo 'test write' > /test/new.txt && cat /test/new.txt"
|
||||||
|
cat /tmp/test-rw/new.txt && rm -rf /tmp/test-rw
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Both operations succeed.
|
||||||
|
|
||||||
|
### Full integration test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
Send a message via WhatsApp and verify the agent responds.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Apple Container not found:**
|
||||||
|
- Download from https://github.com/apple/container/releases
|
||||||
|
- Install the `.pkg` file
|
||||||
|
- Verify: `container --version`
|
||||||
|
|
||||||
|
**Runtime won't start:**
|
||||||
|
```bash
|
||||||
|
container system start
|
||||||
|
container system status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Image build fails:**
|
||||||
|
```bash
|
||||||
|
# Clean rebuild — Apple Container caches aggressively
|
||||||
|
container builder stop && container builder rm && container builder start
|
||||||
|
./container/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Container can't write to mounted directories:**
|
||||||
|
Check directory permissions on the host. The container runs as uid 1000.
|
||||||
|
|
||||||
|
## Summary of Changed Files
|
||||||
|
|
||||||
|
| File | Type of Change |
|
||||||
|
|------|----------------|
|
||||||
|
| `src/container-runtime.ts` | Full replacement — Docker → Apple Container API |
|
||||||
|
| `src/container-runtime.test.ts` | Full replacement — tests for Apple Container behavior |
|
||||||
|
| `container/build.sh` | Default runtime: `docker` → `container` |
|
||||||
13
.claude/skills/convert-to-apple-container/manifest.yaml
Normal file
13
.claude/skills/convert-to-apple-container/manifest.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
skill: convert-to-apple-container
|
||||||
|
version: 1.0.0
|
||||||
|
description: "Switch container runtime from Docker to Apple Container (macOS)"
|
||||||
|
core_version: 0.1.0
|
||||||
|
adds: []
|
||||||
|
modifies:
|
||||||
|
- src/container-runtime.ts
|
||||||
|
- src/container-runtime.test.ts
|
||||||
|
- container/build.sh
|
||||||
|
structured: {}
|
||||||
|
conflicts: []
|
||||||
|
depends: []
|
||||||
|
test: "npx vitest run src/container-runtime.test.ts"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the NanoClaw agent container image
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
IMAGE_NAME="nanoclaw-agent"
|
||||||
|
TAG="${1:-latest}"
|
||||||
|
CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-container}"
|
||||||
|
|
||||||
|
echo "Building NanoClaw agent container image..."
|
||||||
|
echo "Image: ${IMAGE_NAME}:${TAG}"
|
||||||
|
|
||||||
|
${CONTAINER_RUNTIME} build -t "${IMAGE_NAME}:${TAG}" .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Build complete!"
|
||||||
|
echo "Image: ${IMAGE_NAME}:${TAG}"
|
||||||
|
echo ""
|
||||||
|
echo "Test with:"
|
||||||
|
echo " echo '{\"prompt\":\"What is 2+2?\",\"groupFolder\":\"test\",\"chatJid\":\"test@g.us\",\"isMain\":false}' | ${CONTAINER_RUNTIME} run -i ${IMAGE_NAME}:${TAG}"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Intent: container/build.sh modifications
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
Changed the default container runtime from `docker` to `container` (Apple Container CLI).
|
||||||
|
|
||||||
|
## Key sections
|
||||||
|
- `CONTAINER_RUNTIME` default: `docker` → `container`
|
||||||
|
- All build/run commands use `${CONTAINER_RUNTIME}` variable (unchanged)
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
- The `CONTAINER_RUNTIME` environment variable override still works
|
||||||
|
- IMAGE_NAME and TAG logic unchanged
|
||||||
|
- Build and test echo commands unchanged
|
||||||
|
|
||||||
|
## Must-keep
|
||||||
|
- The `CONTAINER_RUNTIME` env var override pattern
|
||||||
|
- The test command echo at the end
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock logger
|
||||||
|
vi.mock('./logger.js', () => ({
|
||||||
|
logger: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock child_process — store the mock fn so tests can configure it
|
||||||
|
const mockExecSync = vi.fn();
|
||||||
|
vi.mock('child_process', () => ({
|
||||||
|
execSync: (...args: unknown[]) => mockExecSync(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
CONTAINER_RUNTIME_BIN,
|
||||||
|
readonlyMountArgs,
|
||||||
|
stopContainer,
|
||||||
|
ensureContainerRuntimeRunning,
|
||||||
|
cleanupOrphans,
|
||||||
|
} from './container-runtime.js';
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Pure functions ---
|
||||||
|
|
||||||
|
describe('readonlyMountArgs', () => {
|
||||||
|
it('returns --mount flag with type=bind and readonly', () => {
|
||||||
|
const args = readonlyMountArgs('/host/path', '/container/path');
|
||||||
|
expect(args).toEqual([
|
||||||
|
'--mount',
|
||||||
|
'type=bind,source=/host/path,target=/container/path,readonly',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopContainer', () => {
|
||||||
|
it('returns stop command using CONTAINER_RUNTIME_BIN', () => {
|
||||||
|
expect(stopContainer('nanoclaw-test-123')).toBe(
|
||||||
|
`${CONTAINER_RUNTIME_BIN} stop nanoclaw-test-123`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- ensureContainerRuntimeRunning ---
|
||||||
|
|
||||||
|
describe('ensureContainerRuntimeRunning', () => {
|
||||||
|
it('does nothing when runtime is already running', () => {
|
||||||
|
mockExecSync.mockReturnValueOnce('');
|
||||||
|
|
||||||
|
ensureContainerRuntimeRunning();
|
||||||
|
|
||||||
|
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockExecSync).toHaveBeenCalledWith(
|
||||||
|
`${CONTAINER_RUNTIME_BIN} system status`,
|
||||||
|
{ stdio: 'pipe' },
|
||||||
|
);
|
||||||
|
expect(logger.debug).toHaveBeenCalledWith('Container runtime already running');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto-starts when system status fails', () => {
|
||||||
|
// First call (system status) fails
|
||||||
|
mockExecSync.mockImplementationOnce(() => {
|
||||||
|
throw new Error('not running');
|
||||||
|
});
|
||||||
|
// Second call (system start) succeeds
|
||||||
|
mockExecSync.mockReturnValueOnce('');
|
||||||
|
|
||||||
|
ensureContainerRuntimeRunning();
|
||||||
|
|
||||||
|
expect(mockExecSync).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockExecSync).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
`${CONTAINER_RUNTIME_BIN} system start`,
|
||||||
|
{ stdio: 'pipe', timeout: 30000 },
|
||||||
|
);
|
||||||
|
expect(logger.info).toHaveBeenCalledWith('Container runtime started');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when both status and start fail', () => {
|
||||||
|
mockExecSync.mockImplementation(() => {
|
||||||
|
throw new Error('failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => ensureContainerRuntimeRunning()).toThrow(
|
||||||
|
'Container runtime is required but failed to start',
|
||||||
|
);
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- cleanupOrphans ---
|
||||||
|
|
||||||
|
describe('cleanupOrphans', () => {
|
||||||
|
it('stops orphaned nanoclaw containers from JSON output', () => {
|
||||||
|
// Apple Container ls returns JSON
|
||||||
|
const lsOutput = JSON.stringify([
|
||||||
|
{ status: 'running', configuration: { id: 'nanoclaw-group1-111' } },
|
||||||
|
{ status: 'stopped', configuration: { id: 'nanoclaw-group2-222' } },
|
||||||
|
{ status: 'running', configuration: { id: 'nanoclaw-group3-333' } },
|
||||||
|
{ status: 'running', configuration: { id: 'other-container' } },
|
||||||
|
]);
|
||||||
|
mockExecSync.mockReturnValueOnce(lsOutput);
|
||||||
|
// stop calls succeed
|
||||||
|
mockExecSync.mockReturnValue('');
|
||||||
|
|
||||||
|
cleanupOrphans();
|
||||||
|
|
||||||
|
// ls + 2 stop calls (only running nanoclaw- containers)
|
||||||
|
expect(mockExecSync).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockExecSync).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
`${CONTAINER_RUNTIME_BIN} stop nanoclaw-group1-111`,
|
||||||
|
{ stdio: 'pipe' },
|
||||||
|
);
|
||||||
|
expect(mockExecSync).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
`${CONTAINER_RUNTIME_BIN} stop nanoclaw-group3-333`,
|
||||||
|
{ stdio: 'pipe' },
|
||||||
|
);
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
{ count: 2, names: ['nanoclaw-group1-111', 'nanoclaw-group3-333'] },
|
||||||
|
'Stopped orphaned containers',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when no orphans exist', () => {
|
||||||
|
mockExecSync.mockReturnValueOnce('[]');
|
||||||
|
|
||||||
|
cleanupOrphans();
|
||||||
|
|
||||||
|
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logger.info).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns and continues when ls fails', () => {
|
||||||
|
mockExecSync.mockImplementationOnce(() => {
|
||||||
|
throw new Error('container not available');
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanupOrphans(); // should not throw
|
||||||
|
|
||||||
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ err: expect.any(Error) }),
|
||||||
|
'Failed to clean up orphaned containers',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('continues stopping remaining containers when one stop fails', () => {
|
||||||
|
const lsOutput = JSON.stringify([
|
||||||
|
{ status: 'running', configuration: { id: 'nanoclaw-a-1' } },
|
||||||
|
{ status: 'running', configuration: { id: 'nanoclaw-b-2' } },
|
||||||
|
]);
|
||||||
|
mockExecSync.mockReturnValueOnce(lsOutput);
|
||||||
|
// First stop fails
|
||||||
|
mockExecSync.mockImplementationOnce(() => {
|
||||||
|
throw new Error('already stopped');
|
||||||
|
});
|
||||||
|
// Second stop succeeds
|
||||||
|
mockExecSync.mockReturnValueOnce('');
|
||||||
|
|
||||||
|
cleanupOrphans(); // should not throw
|
||||||
|
|
||||||
|
expect(mockExecSync).toHaveBeenCalledTimes(3);
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
{ count: 2, names: ['nanoclaw-a-1', 'nanoclaw-b-2'] },
|
||||||
|
'Stopped orphaned containers',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Container runtime abstraction for NanoClaw.
|
||||||
|
* All runtime-specific logic lives here so swapping runtimes means changing one file.
|
||||||
|
*/
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
|
/** The container runtime binary name. */
|
||||||
|
export const CONTAINER_RUNTIME_BIN = 'container';
|
||||||
|
|
||||||
|
/** Returns CLI args for a readonly bind mount. */
|
||||||
|
export function readonlyMountArgs(hostPath: string, containerPath: string): string[] {
|
||||||
|
return ['--mount', `type=bind,source=${hostPath},target=${containerPath},readonly`];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the shell command to stop a container by name. */
|
||||||
|
export function stopContainer(name: string): string {
|
||||||
|
return `${CONTAINER_RUNTIME_BIN} stop ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensure the container runtime is running, starting it if needed. */
|
||||||
|
export function ensureContainerRuntimeRunning(): void {
|
||||||
|
try {
|
||||||
|
execSync(`${CONTAINER_RUNTIME_BIN} system status`, { stdio: 'pipe' });
|
||||||
|
logger.debug('Container runtime already running');
|
||||||
|
} catch {
|
||||||
|
logger.info('Starting container runtime...');
|
||||||
|
try {
|
||||||
|
execSync(`${CONTAINER_RUNTIME_BIN} system start`, { stdio: 'pipe', timeout: 30000 });
|
||||||
|
logger.info('Container runtime started');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, 'Failed to start container runtime');
|
||||||
|
console.error(
|
||||||
|
'\n╔════════════════════════════════════════════════════════════════╗',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ FATAL: Container runtime failed to start ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ Agents cannot run without a container runtime. To fix: ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ 1. Ensure Apple Container is installed ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ 2. Run: container system start ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ 3. Restart NanoClaw ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'╚════════════════════════════════════════════════════════════════╝\n',
|
||||||
|
);
|
||||||
|
throw new Error('Container runtime is required but failed to start');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kill orphaned NanoClaw containers from previous runs. */
|
||||||
|
export function cleanupOrphans(): void {
|
||||||
|
try {
|
||||||
|
const output = execSync(`${CONTAINER_RUNTIME_BIN} ls --format json`, {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
const containers: { status: string; configuration: { id: string } }[] = JSON.parse(output || '[]');
|
||||||
|
const orphans = containers
|
||||||
|
.filter((c) => c.status === 'running' && c.configuration.id.startsWith('nanoclaw-'))
|
||||||
|
.map((c) => c.configuration.id);
|
||||||
|
for (const name of orphans) {
|
||||||
|
try {
|
||||||
|
execSync(stopContainer(name), { stdio: 'pipe' });
|
||||||
|
} catch { /* already stopped */ }
|
||||||
|
}
|
||||||
|
if (orphans.length > 0) {
|
||||||
|
logger.info({ count: orphans.length, names: orphans }, 'Stopped orphaned containers');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({ err }, 'Failed to clean up orphaned containers');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Intent: src/container-runtime.ts modifications
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
Replaced Docker runtime with Apple Container runtime. This is a full file replacement — the exported API is identical, only the implementation differs.
|
||||||
|
|
||||||
|
## Key sections
|
||||||
|
|
||||||
|
### CONTAINER_RUNTIME_BIN
|
||||||
|
- Changed: `'docker'` → `'container'` (the Apple Container CLI binary)
|
||||||
|
|
||||||
|
### readonlyMountArgs
|
||||||
|
- Changed: Docker `-v host:container:ro` → Apple Container `--mount type=bind,source=...,target=...,readonly`
|
||||||
|
|
||||||
|
### ensureContainerRuntimeRunning
|
||||||
|
- Changed: `docker info` → `container system status` for checking
|
||||||
|
- Added: auto-start via `container system start` when not running (Apple Container supports this; Docker requires manual start)
|
||||||
|
- Changed: error message references Apple Container instead of Docker
|
||||||
|
|
||||||
|
### cleanupOrphans
|
||||||
|
- Changed: `docker ps --filter name=nanoclaw- --format '{{.Names}}'` → `container ls --format json` with JSON parsing
|
||||||
|
- Apple Container returns JSON with `{ status, configuration: { id } }` structure
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
- All five exports remain identical: `CONTAINER_RUNTIME_BIN`, `readonlyMountArgs`, `stopContainer`, `ensureContainerRuntimeRunning`, `cleanupOrphans`
|
||||||
|
- `stopContainer` implementation is unchanged (`<bin> stop <name>`)
|
||||||
|
- Logger usage pattern is unchanged
|
||||||
|
- Error handling pattern is unchanged
|
||||||
|
|
||||||
|
## Must-keep
|
||||||
|
- The exported function signatures (consumed by container-runner.ts and index.ts)
|
||||||
|
- The error box-drawing output format
|
||||||
|
- The orphan cleanup logic (find + stop pattern)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
describe('convert-to-apple-container skill package', () => {
|
||||||
|
const skillDir = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
it('has a valid manifest', () => {
|
||||||
|
const manifestPath = path.join(skillDir, 'manifest.yaml');
|
||||||
|
expect(fs.existsSync(manifestPath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
||||||
|
expect(content).toContain('skill: convert-to-apple-container');
|
||||||
|
expect(content).toContain('version: 1.0.0');
|
||||||
|
expect(content).toContain('container-runtime.ts');
|
||||||
|
expect(content).toContain('container/build.sh');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has all modified files', () => {
|
||||||
|
const runtimeFile = path.join(skillDir, 'modify', 'src', 'container-runtime.ts');
|
||||||
|
expect(fs.existsSync(runtimeFile)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(runtimeFile, 'utf-8');
|
||||||
|
expect(content).toContain("CONTAINER_RUNTIME_BIN = 'container'");
|
||||||
|
expect(content).toContain('system status');
|
||||||
|
expect(content).toContain('system start');
|
||||||
|
expect(content).toContain('ls --format json');
|
||||||
|
|
||||||
|
const testFile = path.join(skillDir, 'modify', 'src', 'container-runtime.test.ts');
|
||||||
|
expect(fs.existsSync(testFile)).toBe(true);
|
||||||
|
|
||||||
|
const testContent = fs.readFileSync(testFile, 'utf-8');
|
||||||
|
expect(testContent).toContain('system status');
|
||||||
|
expect(testContent).toContain('--mount');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has intent files for modified sources', () => {
|
||||||
|
const runtimeIntent = path.join(skillDir, 'modify', 'src', 'container-runtime.ts.intent.md');
|
||||||
|
expect(fs.existsSync(runtimeIntent)).toBe(true);
|
||||||
|
|
||||||
|
const buildIntent = path.join(skillDir, 'modify', 'container', 'build.sh.intent.md');
|
||||||
|
expect(fs.existsSync(buildIntent)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has build.sh with Apple Container default', () => {
|
||||||
|
const buildFile = path.join(skillDir, 'modify', 'container', 'build.sh');
|
||||||
|
expect(fs.existsSync(buildFile)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(buildFile, 'utf-8');
|
||||||
|
expect(content).toContain('CONTAINER_RUNTIME:-container');
|
||||||
|
expect(content).not.toContain('CONTAINER_RUNTIME:-docker');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses Apple Container API patterns (not Docker)', () => {
|
||||||
|
const runtimeFile = path.join(skillDir, 'modify', 'src', 'container-runtime.ts');
|
||||||
|
const content = fs.readFileSync(runtimeFile, 'utf-8');
|
||||||
|
|
||||||
|
// Apple Container patterns
|
||||||
|
expect(content).toContain('system status');
|
||||||
|
expect(content).toContain('system start');
|
||||||
|
expect(content).toContain('ls --format json');
|
||||||
|
expect(content).toContain('type=bind,source=');
|
||||||
|
|
||||||
|
// Should NOT contain Docker patterns
|
||||||
|
expect(content).not.toContain('docker info');
|
||||||
|
expect(content).not.toContain("'-v'");
|
||||||
|
expect(content).not.toContain('--filter name=');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
---
|
|
||||||
name: convert-to-docker
|
|
||||||
description: Convert NanoClaw from Apple Container to Docker for cross-platform support. Use when user wants to run on Linux, switch to Docker, enable cross-platform deployment, or migrate away from Apple Container. Triggers on "docker", "linux support", "convert to docker", "cross-platform", or "replace apple container".
|
|
||||||
disable-model-invocation: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Convert to Docker
|
|
||||||
|
|
||||||
This skill migrates NanoClaw from Apple Container (macOS-only) to Docker for cross-platform support (macOS and Linux).
|
|
||||||
|
|
||||||
**What this changes:**
|
|
||||||
- Container runtime: Apple Container → Docker
|
|
||||||
- Mount syntax: `--mount type=bind,...,readonly` → `-v path:path:ro`
|
|
||||||
- Startup check: `container system status` → `docker info`
|
|
||||||
- Build commands: `container build/run` → `docker build/run`
|
|
||||||
|
|
||||||
**What stays the same:**
|
|
||||||
- Dockerfile (already Docker-compatible)
|
|
||||||
- Agent runner code
|
|
||||||
- Mount security/allowlist validation
|
|
||||||
- All other functionality
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Verify Docker is installed before starting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker --version && docker info >/dev/null 2>&1 && echo "Docker ready" || echo "Install Docker first"
|
|
||||||
```
|
|
||||||
|
|
||||||
If Docker is not installed:
|
|
||||||
- **macOS**: Download from https://docker.com/products/docker-desktop
|
|
||||||
- **Linux**: `curl -fsSL https://get.docker.com | sh && sudo systemctl start docker`
|
|
||||||
|
|
||||||
## 1. Update Container Runner
|
|
||||||
|
|
||||||
Edit `src/container-runner.ts`:
|
|
||||||
|
|
||||||
### 1a. Update module comment (around line 3)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before:
|
|
||||||
* Spawns agent execution in Apple Container and handles IPC
|
|
||||||
|
|
||||||
// After:
|
|
||||||
* Spawns agent execution in Docker container and handles IPC
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1b. Update directory mount comment (around line 88)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before:
|
|
||||||
// Apple Container only supports directory mounts, not file mounts
|
|
||||||
|
|
||||||
// After:
|
|
||||||
// Docker bind mounts work with both files and directories
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1c. Update env workaround comment (around line 120)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before:
|
|
||||||
// Environment file directory (workaround for Apple Container -i env var bug)
|
|
||||||
|
|
||||||
// After:
|
|
||||||
// Environment file directory (keeps credentials out of process listings)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1d. Update buildContainerArgs function
|
|
||||||
|
|
||||||
Replace the entire function with Docker mount syntax:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function buildContainerArgs(mounts: VolumeMount[]): string[] {
|
|
||||||
const args: string[] = ['run', '-i', '--rm'];
|
|
||||||
|
|
||||||
// Docker: -v with :ro suffix for readonly
|
|
||||||
for (const mount of mounts) {
|
|
||||||
if (mount.readonly) {
|
|
||||||
args.push('-v', `${mount.hostPath}:${mount.containerPath}:ro`);
|
|
||||||
} else {
|
|
||||||
args.push('-v', `${mount.hostPath}:${mount.containerPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(CONTAINER_IMAGE);
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1e. Update spawn command (around line 204)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before:
|
|
||||||
const container = spawn('container', containerArgs, {
|
|
||||||
|
|
||||||
// After:
|
|
||||||
const container = spawn('docker', containerArgs, {
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Update Startup Check
|
|
||||||
|
|
||||||
Edit `src/index.ts`:
|
|
||||||
|
|
||||||
### 2a. Replace the container system check function
|
|
||||||
|
|
||||||
Find `ensureContainerSystemRunning()` and replace entirely with:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function ensureDockerRunning(): void {
|
|
||||||
try {
|
|
||||||
execSync('docker info', { stdio: 'pipe', timeout: 10000 });
|
|
||||||
logger.debug('Docker daemon is running');
|
|
||||||
} catch {
|
|
||||||
logger.error('Docker daemon is not running');
|
|
||||||
console.error('\n╔════════════════════════════════════════════════════════════════╗');
|
|
||||||
console.error('║ FATAL: Docker is not running ║');
|
|
||||||
console.error('║ ║');
|
|
||||||
console.error('║ Agents cannot run without Docker. To fix: ║');
|
|
||||||
console.error('║ macOS: Start Docker Desktop ║');
|
|
||||||
console.error('║ Linux: sudo systemctl start docker ║');
|
|
||||||
console.error('║ ║');
|
|
||||||
console.error('║ Install from: https://docker.com/products/docker-desktop ║');
|
|
||||||
console.error('╚════════════════════════════════════════════════════════════════╝\n');
|
|
||||||
throw new Error('Docker is required but not running');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2b. Update the function call in main()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before:
|
|
||||||
ensureContainerSystemRunning();
|
|
||||||
|
|
||||||
// After:
|
|
||||||
ensureDockerRunning();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Update Build Script
|
|
||||||
|
|
||||||
Edit `container/build.sh`:
|
|
||||||
|
|
||||||
### 3a. Update build command (around line 15-16)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Before:
|
|
||||||
# Build with Apple Container
|
|
||||||
container build -t "${IMAGE_NAME}:${TAG}" .
|
|
||||||
|
|
||||||
# After:
|
|
||||||
# Build with Docker
|
|
||||||
docker build -t "${IMAGE_NAME}:${TAG}" .
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3b. Update test command (around line 23)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Before:
|
|
||||||
echo " echo '{...}' | container run -i ${IMAGE_NAME}:${TAG}"
|
|
||||||
|
|
||||||
# After:
|
|
||||||
echo " echo '{...}' | docker run -i ${IMAGE_NAME}:${TAG}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Update Documentation
|
|
||||||
|
|
||||||
Update references in documentation files:
|
|
||||||
|
|
||||||
| File | Find | Replace |
|
|
||||||
|------|------|---------|
|
|
||||||
| `CLAUDE.md` | "Apple Container (Linux VMs)" | "Docker containers" |
|
|
||||||
| `README.md` | "Apple containers" | "Docker containers" |
|
|
||||||
| `README.md` | "Apple Container" | "Docker" |
|
|
||||||
| `README.md` | Requirements section | Update to show Docker instead |
|
|
||||||
| `docs/REQUIREMENTS.md` | "Apple Container" | "Docker" |
|
|
||||||
| `docs/SPEC.md` | "APPLE CONTAINER" | "DOCKER CONTAINER" |
|
|
||||||
| `docs/SPEC.md` | All Apple Container references | Docker equivalents |
|
|
||||||
|
|
||||||
### Key README.md updates:
|
|
||||||
|
|
||||||
**Requirements section:**
|
|
||||||
```markdown
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- macOS or Linux
|
|
||||||
- Node.js 20+
|
|
||||||
- [Claude Code](https://claude.ai/download)
|
|
||||||
- [Docker](https://docker.com/products/docker-desktop)
|
|
||||||
```
|
|
||||||
|
|
||||||
**FAQ - "Why Docker?":**
|
|
||||||
```markdown
|
|
||||||
**Why Docker?**
|
|
||||||
|
|
||||||
Docker provides cross-platform support (macOS and Linux), a large ecosystem, and mature tooling. Docker Desktop on macOS uses a lightweight Linux VM similar to other container solutions.
|
|
||||||
```
|
|
||||||
|
|
||||||
**FAQ - "Can I run this on Linux?":**
|
|
||||||
```markdown
|
|
||||||
**Can I run this on Linux?**
|
|
||||||
|
|
||||||
Yes. NanoClaw uses Docker, which works on both macOS and Linux. Just install Docker and run `/setup`.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Update Skills
|
|
||||||
|
|
||||||
### 5a. Update `.claude/skills/setup/SKILL.md`
|
|
||||||
|
|
||||||
Replace Section 2 "Install Apple Container" with Docker installation:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## 2. Install Docker
|
|
||||||
|
|
||||||
Check if Docker is installed and running:
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
docker --version && docker info >/dev/null 2>&1 && echo "Docker is running" || echo "Docker not running or not installed"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
If not installed or not running, tell the user:
|
|
||||||
> Docker is required for running agents in isolated environments.
|
|
||||||
>
|
|
||||||
> **macOS:**
|
|
||||||
> 1. Download Docker Desktop from https://docker.com/products/docker-desktop
|
|
||||||
> 2. Install and start Docker Desktop
|
|
||||||
> 3. Wait for the whale icon in the menu bar to stop animating
|
|
||||||
>
|
|
||||||
> **Linux:**
|
|
||||||
> \`\`\`bash
|
|
||||||
> curl -fsSL https://get.docker.com | sh
|
|
||||||
> sudo systemctl start docker
|
|
||||||
> sudo usermod -aG docker $USER # Then log out and back in
|
|
||||||
> \`\`\`
|
|
||||||
>
|
|
||||||
> Let me know when you've completed these steps.
|
|
||||||
|
|
||||||
Wait for user confirmation, then verify:
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
docker run --rm hello-world
|
|
||||||
\`\`\`
|
|
||||||
```
|
|
||||||
|
|
||||||
Update build verification:
|
|
||||||
```markdown
|
|
||||||
Verify the build succeeded:
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
docker images | grep nanoclaw-agent
|
|
||||||
echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "Container OK" || echo "Container build failed"
|
|
||||||
\`\`\`
|
|
||||||
```
|
|
||||||
|
|
||||||
Update troubleshooting section to reference Docker commands.
|
|
||||||
|
|
||||||
### 5b. Update `.claude/skills/debug/SKILL.md`
|
|
||||||
|
|
||||||
Replace all `container` commands with `docker` equivalents:
|
|
||||||
|
|
||||||
| Before | After |
|
|
||||||
|--------|-------|
|
|
||||||
| `container run` | `docker run` |
|
|
||||||
| `container system status` | `docker info` |
|
|
||||||
| `container builder prune` | `docker builder prune` |
|
|
||||||
| `container images` | `docker images` |
|
|
||||||
| `--mount type=bind,source=...,readonly` | `-v ...:ro` |
|
|
||||||
|
|
||||||
Update the architecture diagram header:
|
|
||||||
```
|
|
||||||
Host (macOS/Linux) Container (Docker)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Build and Verify
|
|
||||||
|
|
||||||
After making all changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Compile TypeScript
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Build Docker image
|
|
||||||
./container/build.sh
|
|
||||||
|
|
||||||
# Verify image exists
|
|
||||||
docker images | grep nanoclaw-agent
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Test the Migration
|
|
||||||
|
|
||||||
### 7a. Test basic container execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "Container OK"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7b. Test readonly mounts
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /tmp/test-ro && echo "test" > /tmp/test-ro/file.txt
|
|
||||||
docker run --rm --entrypoint /bin/bash -v /tmp/test-ro:/test:ro nanoclaw-agent:latest \
|
|
||||||
-c "cat /test/file.txt && touch /test/new.txt 2>&1 || echo 'Write blocked (expected)'"
|
|
||||||
rm -rf /tmp/test-ro
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Read succeeds, write fails with "Read-only file system".
|
|
||||||
|
|
||||||
### 7c. Test read-write mounts
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /tmp/test-rw
|
|
||||||
docker run --rm --entrypoint /bin/bash -v /tmp/test-rw:/test nanoclaw-agent:latest \
|
|
||||||
-c "echo 'test write' > /test/new.txt && cat /test/new.txt"
|
|
||||||
cat /tmp/test-rw/new.txt && rm -rf /tmp/test-rw
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Both operations succeed.
|
|
||||||
|
|
||||||
### 7d. Full integration test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# Send @AssistantName hello via WhatsApp
|
|
||||||
# Verify response received
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Docker not running:**
|
|
||||||
- macOS: Start Docker Desktop from Applications
|
|
||||||
- Linux: `sudo systemctl start docker`
|
|
||||||
- Verify: `docker info`
|
|
||||||
|
|
||||||
**Permission denied on Docker socket (Linux):**
|
|
||||||
```bash
|
|
||||||
sudo usermod -aG docker $USER
|
|
||||||
# Log out and back in
|
|
||||||
```
|
|
||||||
|
|
||||||
**Image build fails:**
|
|
||||||
```bash
|
|
||||||
# Clean rebuild
|
|
||||||
docker builder prune -af
|
|
||||||
./container/build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Container can't write to mounted directories:**
|
|
||||||
Check directory permissions on the host. The container runs as uid 1000.
|
|
||||||
|
|
||||||
## Summary of Changed Files
|
|
||||||
|
|
||||||
| File | Type of Change |
|
|
||||||
|------|----------------|
|
|
||||||
| `src/container-runner.ts` | Mount syntax, spawn command, comments |
|
|
||||||
| `src/index.ts` | Startup check function |
|
|
||||||
| `container/build.sh` | Build and run commands |
|
|
||||||
| `CLAUDE.md` | Quick context |
|
|
||||||
| `README.md` | Requirements, FAQ |
|
|
||||||
| `docs/REQUIREMENTS.md` | Architecture references |
|
|
||||||
| `docs/SPEC.md` | Architecture diagram, tech stack |
|
|
||||||
| `.claude/skills/setup/SKILL.md` | Installation instructions |
|
|
||||||
| `.claude/skills/debug/SKILL.md` | Debug commands |
|
|
||||||
@@ -57,7 +57,21 @@ Check the preflight results for `APPLE_CONTAINER` and `DOCKER`.
|
|||||||
- 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
|
- 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.
|
- 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. Build and test
|
### 3b. Apple Container conversion gate (if needed)
|
||||||
|
|
||||||
|
**If the chosen runtime is Apple Container**, you MUST check whether the source code has already been converted from Docker to Apple Container. Do NOT skip this step. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -q "CONTAINER_RUNTIME_BIN = 'container'" src/container-runtime.ts && echo "ALREADY_CONVERTED" || echo "NEEDS_CONVERSION"
|
||||||
|
```
|
||||||
|
|
||||||
|
**If NEEDS_CONVERSION**, the source code still uses Docker as the runtime. You MUST run the `/convert-to-apple-container` skill NOW, before proceeding to the build step.
|
||||||
|
|
||||||
|
**If ALREADY_CONVERTED**, the code already uses Apple Container. Continue to 3c.
|
||||||
|
|
||||||
|
**If the chosen runtime is Docker**, no conversion is needed — Docker is the default. 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.
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/skills-only.yml
vendored
2
.github/workflows/skills-only.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
body: `This PR adds a skill while also modifying source code. A skill PR should not change source files—the skill should contain **instructions** for Claude to follow. See \`/convert-to-docker\` for an example.
|
body: `This PR adds a skill while also modifying source code. A skill PR should not change source files—the skill should contain **instructions** for Claude to follow. See \`/add-telegram\` for an example.
|
||||||
|
|
||||||
If you're fixing a bug or simplifying code, please submit that as a separate PR.
|
If you're fixing a bug or simplifying code, please submit that as a separate PR.
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ A [skill](https://code.claude.com/docs/en/skills) is a markdown file in `.claude
|
|||||||
|
|
||||||
A PR that contributes a skill should not modify any source files.
|
A PR that contributes a skill should not modify any source files.
|
||||||
|
|
||||||
Your skill should contain the **instructions** Claude follows to add the feature—not pre-built code. See `/convert-to-docker` for a good example.
|
Your skill should contain the **instructions** Claude follows to add the feature—not pre-built code. See `/add-telegram` for a good example.
|
||||||
|
|
||||||
### Why?
|
### Why?
|
||||||
|
|
||||||
|
|||||||
@@ -157,13 +157,13 @@ Key files:
|
|||||||
|
|
||||||
Because I use WhatsApp. Fork it and run a skill to change it. That's the whole point.
|
Because I use WhatsApp. Fork it and run a skill to change it. That's the whole point.
|
||||||
|
|
||||||
**Why Apple Container instead of Docker?**
|
**Why Docker?**
|
||||||
|
|
||||||
On macOS, Apple Container is lightweight, fast, and optimized for Apple silicon. But Docker is also fully supported—during `/setup`, you can choose which runtime to use. On Linux, Docker is used automatically.
|
Docker provides cross-platform support (macOS and Linux) and a mature ecosystem. On macOS, you can optionally switch to Apple Container via `/convert-to-apple-container` for a lighter-weight native runtime.
|
||||||
|
|
||||||
**Can I run this on Linux?**
|
**Can I run this on Linux?**
|
||||||
|
|
||||||
Yes. Run `/setup` and it will automatically configure Docker as the container runtime. Thanks to [@dotsetgreg](https://github.com/dotsetgreg) for contributing the `/convert-to-docker` skill.
|
Yes. Docker is the default runtime and works on both macOS and Linux. Just run `/setup`.
|
||||||
|
|
||||||
**Is this secure?**
|
**Is this secure?**
|
||||||
|
|
||||||
|
|||||||
@@ -141,13 +141,13 @@ WhatsApp (baileys) --> SQLite --> 轮询循环 --> 容器 (Claude Agent SDK) -->
|
|||||||
|
|
||||||
因为我用 WhatsApp。fork 这个项目然后运行一个技能来改变它。正是这个项目的核心理念。
|
因为我用 WhatsApp。fork 这个项目然后运行一个技能来改变它。正是这个项目的核心理念。
|
||||||
|
|
||||||
**为什么是 Apple Container 而不是 Docker?**
|
**为什么是 Docker?**
|
||||||
|
|
||||||
在 macOS 上,Apple Container 轻巧、快速,并为 Apple 芯片优化。但 Docker 也完全支持——在 `/setup` 期间,你可以选择使用哪个运行时。在 Linux 上,会自动使用 Docker。
|
Docker 提供跨平台支持(macOS 和 Linux)和成熟的生态系统。在 macOS 上,你可以选择通过 `/convert-to-apple-container` 切换到 Apple Container 以获得更轻量的原生运行时。
|
||||||
|
|
||||||
**我可以在 Linux 上运行吗?**
|
**我可以在 Linux 上运行吗?**
|
||||||
|
|
||||||
可以。运行 `/setup`,它会自动配置 Docker 作为容器运行时。感谢 [@dotsetgreg](https://github.com/dotsetgreg) 贡献了 `/convert-to-docker` 技能。
|
可以。Docker 是默认的容器运行时,在 macOS 和 Linux 上都可以使用。只需运行 `/setup`。
|
||||||
|
|
||||||
**这个安全吗?**
|
**这个安全吗?**
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,8 @@ Skills to add or switch to different messaging platforms:
|
|||||||
- `/convert-to-telegram` - Replace WhatsApp with Telegram entirely
|
- `/convert-to-telegram` - Replace WhatsApp with Telegram entirely
|
||||||
|
|
||||||
### Container Runtime
|
### Container Runtime
|
||||||
The project currently uses Apple Container (macOS-only). We need:
|
The project uses Docker by default (cross-platform). For macOS users who prefer Apple Container:
|
||||||
- `/convert-to-docker` - Replace Apple Container with standard Docker
|
- `/convert-to-apple-container` - Switch from Docker to Apple Container (macOS-only)
|
||||||
- This unlocks Linux support and broader deployment options
|
|
||||||
|
|
||||||
### Platform Support
|
### Platform Support
|
||||||
- `/setup-linux` - Make the full setup work on Linux (depends on Docker conversion)
|
- `/setup-linux` - Make the full setup work on Linux (depends on Docker conversion)
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ nanoclaw/
|
|||||||
│ ├── add-gmail/SKILL.md # /add-gmail - Gmail integration
|
│ ├── add-gmail/SKILL.md # /add-gmail - Gmail integration
|
||||||
│ ├── add-voice-transcription/ # /add-voice-transcription - Whisper
|
│ ├── add-voice-transcription/ # /add-voice-transcription - Whisper
|
||||||
│ ├── x-integration/SKILL.md # /x-integration - X/Twitter
|
│ ├── x-integration/SKILL.md # /x-integration - X/Twitter
|
||||||
│ ├── convert-to-docker/SKILL.md # /convert-to-docker - Docker runtime
|
│ ├── convert-to-apple-container/ # /convert-to-apple-container - Apple Container runtime
|
||||||
│ └── add-parallel/SKILL.md # /add-parallel - Parallel agents
|
│ └── add-parallel/SKILL.md # /add-parallel - Parallel agents
|
||||||
│
|
│
|
||||||
├── groups/
|
├── groups/
|
||||||
|
|||||||
Reference in New Issue
Block a user