diff --git a/container/build.sh b/container/build.sh index fbdef31..8321fdf 100755 --- a/container/build.sh +++ b/container/build.sh @@ -8,7 +8,7 @@ cd "$SCRIPT_DIR" IMAGE_NAME="nanoclaw-agent" TAG="${1:-latest}" -CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-container}" +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}" echo "Building NanoClaw agent container image..." echo "Image: ${IMAGE_NAME}:${TAG}" diff --git a/src/container-runtime.test.ts b/src/container-runtime.test.ts index ab2dec5..de45361 100644 --- a/src/container-runtime.test.ts +++ b/src/container-runtime.test.ts @@ -32,12 +32,9 @@ beforeEach(() => { // --- Pure functions --- describe('readonlyMountArgs', () => { - it('returns --mount flag with bind,source,target,readonly', () => { + it('returns -v flag with :ro suffix', () => { const args = readonlyMountArgs('/host/path', '/container/path'); - expect(args).toEqual([ - '--mount', - 'type=bind,source=/host/path,target=/container/path,readonly', - ]); + expect(args).toEqual(['-v', '/host/path:/container/path:ro']); }); }); @@ -53,41 +50,21 @@ describe('stopContainer', () => { describe('ensureContainerRuntimeRunning', () => { it('does nothing when runtime is already running', () => { - // system status succeeds mockExecSync.mockReturnValueOnce(''); ensureContainerRuntimeRunning(); expect(mockExecSync).toHaveBeenCalledTimes(1); expect(mockExecSync).toHaveBeenCalledWith( - `${CONTAINER_RUNTIME_BIN} system status`, - { stdio: 'pipe' }, + `${CONTAINER_RUNTIME_BIN} info`, + { stdio: 'pipe', timeout: 10000 }, ); expect(logger.debug).toHaveBeenCalledWith('Container runtime already running'); }); - it('starts runtime when status check fails', () => { - // system status fails + it('throws when docker info fails', () => { mockExecSync.mockImplementationOnce(() => { - throw new Error('not running'); - }); - // 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'); + throw new Error('Cannot connect to the Docker daemon'); }); expect(() => ensureContainerRuntimeRunning()).toThrow( @@ -101,21 +78,14 @@ describe('ensureContainerRuntimeRunning', () => { describe('cleanupOrphans', () => { it('stops orphaned nanoclaw containers', () => { - const containers = [ - { status: 'running', configuration: { id: 'nanoclaw-group1-111' } }, - { status: 'running', configuration: { id: 'nanoclaw-group2-222' } }, - { status: 'running', configuration: { id: 'other-container' } }, - { status: 'stopped', configuration: { id: 'nanoclaw-old-333' } }, - ]; - - // ls returns container list - mockExecSync.mockReturnValueOnce(JSON.stringify(containers)); + // docker ps returns container names, one per line + mockExecSync.mockReturnValueOnce('nanoclaw-group1-111\nnanoclaw-group2-222\n'); // stop calls succeed mockExecSync.mockReturnValue(''); cleanupOrphans(); - // ls + 2 stop calls (only running nanoclaw- containers) + // ps + 2 stop calls expect(mockExecSync).toHaveBeenCalledTimes(3); expect(mockExecSync).toHaveBeenNthCalledWith( 2, @@ -134,7 +104,7 @@ describe('cleanupOrphans', () => { }); it('does nothing when no orphans exist', () => { - mockExecSync.mockReturnValueOnce(JSON.stringify([])); + mockExecSync.mockReturnValueOnce(''); cleanupOrphans(); @@ -142,18 +112,9 @@ describe('cleanupOrphans', () => { expect(logger.info).not.toHaveBeenCalled(); }); - it('handles empty output from ls', () => { - mockExecSync.mockReturnValueOnce(''); - - cleanupOrphans(); - - expect(mockExecSync).toHaveBeenCalledTimes(1); - expect(logger.warn).not.toHaveBeenCalled(); - }); - - it('warns and continues when ls fails', () => { + it('warns and continues when ps fails', () => { mockExecSync.mockImplementationOnce(() => { - throw new Error('runtime not available'); + throw new Error('docker not available'); }); cleanupOrphans(); // should not throw @@ -165,12 +126,7 @@ describe('cleanupOrphans', () => { }); it('continues stopping remaining containers when one stop fails', () => { - const containers = [ - { status: 'running', configuration: { id: 'nanoclaw-a-1' } }, - { status: 'running', configuration: { id: 'nanoclaw-b-2' } }, - ]; - - mockExecSync.mockReturnValueOnce(JSON.stringify(containers)); + mockExecSync.mockReturnValueOnce('nanoclaw-a-1\nnanoclaw-b-2\n'); // First stop fails mockExecSync.mockImplementationOnce(() => { throw new Error('already stopped'); diff --git a/src/container-runtime.ts b/src/container-runtime.ts index 507769f..592c4cc 100644 --- a/src/container-runtime.ts +++ b/src/container-runtime.ts @@ -1,17 +1,17 @@ /** * Container runtime abstraction for NanoClaw. - * Swap this file to switch between container runtimes (e.g. Docker). + * 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'; +export const CONTAINER_RUNTIME_BIN = 'docker'; /** 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`]; + return ['-v', `${hostPath}:${containerPath}:ro`]; } /** Returns the shell command to stop a container by name. */ @@ -22,52 +22,46 @@ export function stopContainer(name: string): string { /** Ensure the container runtime is running, starting it if needed. */ export function ensureContainerRuntimeRunning(): void { try { - execSync(`${CONTAINER_RUNTIME_BIN} system status`, { stdio: 'pipe' }); + execSync(`${CONTAINER_RUNTIME_BIN} info`, { stdio: 'pipe', timeout: 10000 }); 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 your container runtime is installed ║', - ); - console.error( - '║ 2. Start the runtime, then restart NanoClaw ║', - ); - console.error( - '╚════════════════════════════════════════════════════════════════╝\n', - ); - throw new Error('Container runtime is required but failed to start'); - } + } catch (err) { + logger.error({ err }, 'Failed to reach 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 Docker is installed and running ║', + ); + console.error( + '║ 2. Run: docker info ║', + ); + 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); + const output = execSync( + `${CONTAINER_RUNTIME_BIN} ps --filter name=nanoclaw- --format '{{.Names}}'`, + { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8' }, + ); + const orphans = output.trim().split('\n').filter(Boolean); for (const name of orphans) { try { execSync(stopContainer(name), { stdio: 'pipe' });