From f02761eddb0ace29a00fd43b0e3a4575bc5b7f87 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Tue, 10 Mar 2026 23:26:08 +0900 Subject: [PATCH] refactor: remove container references from setup, use host process runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setup/container.ts: replace container build with npm run build:runners - setup/index.ts: rename step container → runners - setup/environment.ts: remove Docker/Apple Container detection - setup/service.ts: remove Docker group stale check, add nodeBin and npm-global to service PATH for codex CLI - setup/verify.ts: remove container runtime check - SKILL.md: rewrite for container-free setup, add Codex auth step --- .claude/skills/setup/SKILL.md | 71 +++++----------- setup/container.ts | 148 +++++++--------------------------- setup/environment.ts | 26 +----- setup/index.ts | 2 +- setup/service.ts | 40 +-------- setup/verify.ts | 23 +----- 6 files changed, 58 insertions(+), 252 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 544ee1d..703fc1d 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -11,6 +11,8 @@ Run setup steps automatically. Only pause when user action is required (channel **UX Note:** Use `AskUserQuestion` for all user-facing questions. +**Architecture:** NanoClaw runs agents as direct host processes (no Docker/containers). Each agent type (Claude Code, Codex) has its own runner in `container/agent-runner/` and `container/codex-runner/`. + ## 0. Git & Fork Setup Check the git remote configuration to ensure the user has a fork and upstream is configured. @@ -68,49 +70,18 @@ Run `npx tsx setup/index.ts --step environment` and parse the status block. - If HAS_AUTH=true → WhatsApp is already configured, note for step 5 - If HAS_REGISTERED_GROUPS=true → note existing config, offer to skip or reconfigure -- Record APPLE_CONTAINER and DOCKER values for step 3 -## 3. Container Runtime +## 3. Build Agent Runners -### 3a. Choose runtime +Run `npx tsx setup/index.ts --step runners` and parse the status block. -Check the preflight results for `APPLE_CONTAINER` and `DOCKER`, and the PLATFORM from step 1. +This builds the agent runners that execute as host processes: +- `container/agent-runner/` — Claude Code agent (via Claude Agent SDK) +- `container/codex-runner/` — Codex agent (via Codex CLI) -- PLATFORM=linux → Docker (only option) -- PLATFORM=macos + APPLE_CONTAINER=installed → Use `AskUserQuestion: Docker (cross-platform) or Apple Container (native macOS)?` If Apple Container, run `/convert-to-apple-container` now, then skip to 3c. -- PLATFORM=macos + APPLE_CONTAINER=not_found → Docker +**If BUILD_OK=false:** Read `logs/setup.log` for the error. Common fix: `cd container/agent-runner && npm install && npm run build`. -### 3a-docker. Install Docker - -- DOCKER=running → continue to 4b -- DOCKER=installed_not_running → start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Wait 15s, re-check with `docker info`. -- DOCKER=not_found → Use `AskUserQuestion: Docker is required for running agents. Would you like me 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. - -### 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. Continue to 3c. - -### 3c. Build and test - -Run `npx tsx setup/index.ts --step container -- --runtime ` and parse the status block. - -**If BUILD_OK=false:** Read `logs/setup.log` tail for the build error. -- Cache issue (stale layers): `docker builder prune -f` (Docker) or `container builder stop && container builder rm && container builder start` (Apple Container). Retry. -- Dockerfile syntax or missing files: diagnose from the log and fix, then retry. - -**If TEST_OK=false but BUILD_OK=true:** The image built but won't run. Check logs — common cause is runtime not fully started. Wait a moment and retry the test. +**If CODEX_RUNNER=false but AGENT_RUNNER=true:** Codex runner failed to build. Not critical if you only use Claude Code. Fix: `cd container/codex-runner && npm install && npm run build`. ## 4. Claude Authentication (No Script) @@ -122,6 +93,12 @@ AskUserQuestion: Claude subscription (Pro/Max) vs Anthropic API key? **API key:** Tell user to add `ANTHROPIC_API_KEY=` to `.env`. +### Codex Authentication (Optional) + +AskUserQuestion: Do you also want to use Codex (OpenAI) agents? + +If yes: Tell user to add `OPENAI_API_KEY=` (or `CODEX_OPENAI_API_KEY=`) to `.env`. The Codex runner will use this key. + ## 5. Install Skills Marketplace Register and install the NanoClaw skills marketplace plugin so all feature skills (channel integrations, add-ons) are available: @@ -183,20 +160,6 @@ Run `npx tsx setup/index.ts --step service` and parse the status block. **If FALLBACK=wsl_no_systemd:** WSL without systemd detected. Tell user they can either enable systemd in WSL (`echo -e "[boot]\nsystemd=true" | sudo tee /etc/wsl.conf` then restart WSL) or use the generated `start-nanoclaw.sh` wrapper. -**If DOCKER_GROUP_STALE=true:** The user was added to the docker group after their session started — the systemd service can't reach the Docker socket. Ask user to run these two commands: - -1. Immediate fix: `sudo setfacl -m u:$(whoami):rw /var/run/docker.sock` -2. Persistent fix (re-applies after every Docker restart): -```bash -sudo mkdir -p /etc/systemd/system/docker.service.d -sudo tee /etc/systemd/system/docker.service.d/socket-acl.conf << 'EOF' -[Service] -ExecStartPost=/usr/bin/setfacl -m u:USERNAME:rw /var/run/docker.sock -EOF -sudo systemctl daemon-reload -``` -Replace `USERNAME` with the actual username (from `whoami`). Run the two `sudo` commands separately — the `tee` heredoc first, then `daemon-reload`. After user confirms setfacl ran, re-run the service step. - **If SERVICE_LOADED=false:** - Read `logs/setup.log` for the error. - macOS: check `launchctl list | grep nanoclaw`. If PID=`-` and status non-zero, read `logs/nanoclaw.error.log`. @@ -221,7 +184,9 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ **Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 8), missing `.env` (step 4), missing channel credentials (re-invoke channel skill). -**Container agent fails ("Claude Code process exited with code 1"):** Ensure the container runtime is running — `open -a Docker` (macOS Docker), `container system start` (Apple Container), or `sudo systemctl start docker` (Linux). Check container logs in `groups/main/logs/container-*.log`. +**Agent fails ("Claude Code process exited with code 1"):** Check agent logs in `groups//logs/agent-*.log`. Common causes: missing credentials in `.env`, `codex` CLI not in PATH, stale session IDs. + +**Codex agent not responding:** Ensure `OPENAI_API_KEY` is set in `.env` and `codex` CLI is installed globally (`npm install -g @openai/codex`). The runner needs `codex` in PATH. **No response to messages:** Check trigger pattern. Main channel doesn't need prefix. Check DB: `npx tsx setup/index.ts --step verify`. Check `logs/nanoclaw.log`. diff --git a/setup/container.ts b/setup/container.ts index cc44350..cdcb92d 100644 --- a/setup/container.ts +++ b/setup/container.ts @@ -1,141 +1,55 @@ /** - * Step: container — Build container image and verify with test run. - * Replaces 03-setup-container.sh + * Step: runners — Build agent runners (no container needed). + * Agents run as direct host processes. */ import { execSync } from 'child_process'; +import fs from 'fs'; import path from 'path'; import { logger } from '../src/logger.js'; -import { commandExists } from './platform.js'; import { emitStatus } from './status.js'; -function parseArgs(args: string[]): { runtime: string } { - let runtime = ''; - for (let i = 0; i < args.length; i++) { - if (args[i] === '--runtime' && args[i + 1]) { - runtime = args[i + 1]; - i++; - } - } - return { runtime }; -} - -export async function run(args: string[]): Promise { +export async function run(_args: string[]): Promise { const projectRoot = process.cwd(); - const { runtime } = parseArgs(args); - const image = 'nanoclaw-agent:latest'; - const logFile = path.join(projectRoot, 'logs', 'setup.log'); - if (!runtime) { - emitStatus('SETUP_CONTAINER', { - RUNTIME: 'unknown', - IMAGE: image, - BUILD_OK: false, - TEST_OK: false, - STATUS: 'failed', - ERROR: 'missing_runtime_flag', - LOG: 'logs/setup.log', - }); - process.exit(4); - } - - // Validate runtime availability - if (runtime === 'apple-container' && !commandExists('container')) { - emitStatus('SETUP_CONTAINER', { - RUNTIME: runtime, - IMAGE: image, - BUILD_OK: false, - TEST_OK: false, - STATUS: 'failed', - ERROR: 'runtime_not_available', - LOG: 'logs/setup.log', - }); - process.exit(2); - } - - if (runtime === 'docker') { - if (!commandExists('docker')) { - emitStatus('SETUP_CONTAINER', { - RUNTIME: runtime, - IMAGE: image, - BUILD_OK: false, - TEST_OK: false, - STATUS: 'failed', - ERROR: 'runtime_not_available', - LOG: 'logs/setup.log', - }); - process.exit(2); - } - try { - execSync('docker info', { stdio: 'ignore' }); - } catch { - emitStatus('SETUP_CONTAINER', { - RUNTIME: runtime, - IMAGE: image, - BUILD_OK: false, - TEST_OK: false, - STATUS: 'failed', - ERROR: 'runtime_not_available', - LOG: 'logs/setup.log', - }); - process.exit(2); - } - } - - if (!['apple-container', 'docker'].includes(runtime)) { - emitStatus('SETUP_CONTAINER', { - RUNTIME: runtime, - IMAGE: image, - BUILD_OK: false, - TEST_OK: false, - STATUS: 'failed', - ERROR: 'unknown_runtime', - LOG: 'logs/setup.log', - }); - process.exit(4); - } - - const buildCmd = - runtime === 'apple-container' ? 'container build' : 'docker build'; - const runCmd = runtime === 'apple-container' ? 'container' : 'docker'; - - // Build + // Build agent runners let buildOk = false; - logger.info({ runtime }, 'Building container'); + logger.info('Building agent runners'); try { - execSync(`${buildCmd} -t ${image} .`, { - cwd: path.join(projectRoot, 'container'), + execSync('npm run build:runners', { + cwd: projectRoot, stdio: ['ignore', 'pipe', 'pipe'], }); buildOk = true; - logger.info('Container build succeeded'); + logger.info('Agent runners build succeeded'); } catch (err) { - logger.error({ err }, 'Container build failed'); + logger.error({ err }, 'Agent runners build failed'); } - // Test - let testOk = false; - if (buildOk) { - logger.info('Testing container'); - try { - const output = execSync( - `echo '{}' | ${runCmd} run -i --rm --entrypoint /bin/echo ${image} "Container OK"`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, - ); - testOk = output.includes('Container OK'); - logger.info({ testOk }, 'Container test result'); - } catch { - logger.error('Container test failed'); - } - } + // Verify runner entry points exist + const agentRunner = path.join( + projectRoot, + 'container', + 'agent-runner', + 'dist', + 'index.js', + ); + const codexRunner = path.join( + projectRoot, + 'container', + 'codex-runner', + 'dist', + 'index.js', + ); + const agentRunnerOk = fs.existsSync(agentRunner); + const codexRunnerOk = fs.existsSync(codexRunner); - const status = buildOk && testOk ? 'success' : 'failed'; + const status = buildOk && agentRunnerOk ? 'success' : 'failed'; - emitStatus('SETUP_CONTAINER', { - RUNTIME: runtime, - IMAGE: image, + emitStatus('SETUP_RUNNERS', { BUILD_OK: buildOk, - TEST_OK: testOk, + AGENT_RUNNER: agentRunnerOk, + CODEX_RUNNER: codexRunnerOk, STATUS: status, LOG: 'logs/setup.log', }); diff --git a/setup/environment.ts b/setup/environment.ts index b9814ee..5dd0d21 100644 --- a/setup/environment.ts +++ b/setup/environment.ts @@ -1,5 +1,5 @@ /** - * Step: environment — Detect OS, Node, container runtimes, existing config. + * Step: environment — Detect OS, Node, existing config. * Replaces 01-check-environment.sh */ import fs from 'fs'; @@ -9,7 +9,7 @@ import Database from 'better-sqlite3'; import { STORE_DIR } from '../src/config.js'; import { logger } from '../src/logger.js'; -import { commandExists, getPlatform, isHeadless, isWSL } from './platform.js'; +import { getPlatform, isHeadless, isWSL } from './platform.js'; import { emitStatus } from './status.js'; export async function run(_args: string[]): Promise { @@ -21,24 +21,6 @@ export async function run(_args: string[]): Promise { const wsl = isWSL(); const headless = isHeadless(); - // Check Apple Container - let appleContainer: 'installed' | 'not_found' = 'not_found'; - if (commandExists('container')) { - appleContainer = 'installed'; - } - - // Check Docker - let docker: 'running' | 'installed_not_running' | 'not_found' = 'not_found'; - if (commandExists('docker')) { - try { - const { execSync } = await import('child_process'); - execSync('docker info', { stdio: 'ignore' }); - docker = 'running'; - } catch { - docker = 'installed_not_running'; - } - } - // Check existing config const hasEnv = fs.existsSync(path.join(projectRoot, '.env')); @@ -70,8 +52,6 @@ export async function run(_args: string[]): Promise { { platform, wsl, - appleContainer, - docker, hasEnv, hasAuth, hasRegisteredGroups, @@ -83,8 +63,6 @@ export async function run(_args: string[]): Promise { PLATFORM: platform, IS_WSL: wsl, IS_HEADLESS: headless, - APPLE_CONTAINER: appleContainer, - DOCKER: docker, HAS_ENV: hasEnv, HAS_AUTH: hasAuth, HAS_REGISTERED_GROUPS: hasRegisteredGroups, diff --git a/setup/index.ts b/setup/index.ts index 7ac13e2..8757880 100644 --- a/setup/index.ts +++ b/setup/index.ts @@ -10,7 +10,7 @@ const STEPS: Record< () => Promise<{ run: (args: string[]) => Promise }> > = { environment: () => import('./environment.js'), - container: () => import('./container.js'), + runners: () => import('./container.js'), groups: () => import('./groups.js'), register: () => import('./register.js'), mounts: () => import('./mounts.js'), diff --git a/setup/service.ts b/setup/service.ts index 643c8c9..ffdeb9f 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -101,7 +101,7 @@ function setupLaunchd( EnvironmentVariables PATH - /usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin + ${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin HOME ${homeDir} @@ -174,33 +174,6 @@ function killOrphanedProcesses(projectRoot: string): void { } } -/** - * Detect stale docker group membership in the user systemd session. - * - * When a user is added to the docker group mid-session, the user systemd - * daemon (user@UID.service) keeps the old group list from login time. - * Docker works in the terminal but not in the service context. - * - * Only relevant on Linux with user-level systemd (not root, not macOS, not WSL nohup). - */ -function checkDockerGroupStale(): boolean { - try { - execSync('systemd-run --user --pipe --wait docker info', { - stdio: 'pipe', - timeout: 10000, - }); - return false; // Docker works from systemd session - } catch { - // Check if docker works from the current shell (to distinguish stale group vs broken docker) - try { - execSync('docker info', { stdio: 'pipe', timeout: 5000 }); - return true; // Works in shell but not systemd session → stale group - } catch { - return false; // Docker itself is not working, different issue - } - } -} - function setupSystemd( projectRoot: string, nodePath: string, @@ -244,7 +217,7 @@ WorkingDirectory=${projectRoot} Restart=always RestartSec=5 Environment=HOME=${homeDir} -Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin +Environment=PATH=${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin StandardOutput=append:${projectRoot}/logs/nanoclaw.log StandardError=append:${projectRoot}/logs/nanoclaw.error.log @@ -254,14 +227,6 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`; fs.writeFileSync(unitPath, unit); logger.info({ unitPath }, 'Wrote systemd unit'); - // Detect stale docker group before starting (user systemd only) - const dockerGroupStale = !runningAsRoot && checkDockerGroupStale(); - if (dockerGroupStale) { - logger.warn( - 'Docker group not active in systemd session — user was likely added to docker group mid-session', - ); - } - // Kill orphaned nanoclaw processes to avoid channel connection conflicts killOrphanedProcesses(projectRoot); @@ -299,7 +264,6 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`; PROJECT_PATH: projectRoot, UNIT_PATH: unitPath, SERVICE_LOADED: serviceLoaded, - ...(dockerGroupStale ? { DOCKER_GROUP_STALE: true } : {}), STATUS: 'success', LOG: 'logs/setup.log', }); diff --git a/setup/verify.ts b/setup/verify.ts index f64e4d0..5b1eba6 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -82,21 +82,7 @@ export async function run(_args: string[]): Promise { } logger.info({ service }, 'Service status'); - // 2. Check container runtime - let containerRuntime = 'none'; - try { - execSync('command -v container', { stdio: 'ignore' }); - containerRuntime = 'apple-container'; - } catch { - try { - execSync('docker info', { stdio: 'ignore' }); - containerRuntime = 'docker'; - } catch { - // No runtime - } - } - - // 3. Check credentials + // 2. Check credentials let credentials = 'missing'; const envFile = path.join(projectRoot, '.env'); if (fs.existsSync(envFile)) { @@ -106,7 +92,7 @@ export async function run(_args: string[]): Promise { } } - // 4. Check channel auth (detect configured channels by credentials) + // 3. Check channel auth (detect configured channels by credentials) const envVars = readEnvFile([ 'TELEGRAM_BOT_TOKEN', 'SLACK_BOT_TOKEN', @@ -139,7 +125,7 @@ export async function run(_args: string[]): Promise { const configuredChannels = Object.keys(channelAuth); const anyChannelConfigured = configuredChannels.length > 0; - // 5. Check registered groups (using better-sqlite3, not sqlite3 CLI) + // 4. Check registered groups (using better-sqlite3, not sqlite3 CLI) let registeredGroups = 0; const dbPath = path.join(STORE_DIR, 'messages.db'); if (fs.existsSync(dbPath)) { @@ -155,7 +141,7 @@ export async function run(_args: string[]): Promise { } } - // 6. Check mount allowlist + // 5. Check mount allowlist let mountAllowlist = 'missing'; if ( fs.existsSync( @@ -178,7 +164,6 @@ export async function run(_args: string[]): Promise { emitStatus('VERIFY', { SERVICE: service, - CONTAINER_RUNTIME: containerRuntime, CREDENTIALS: credentials, CONFIGURED_CHANNELS: configuredChannels.join(','), CHANNEL_AUTH: JSON.stringify(channelAuth),