/** * 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'); } }