refactor: extract runtime-specific code into src/container-runtime.ts (#321)

Move all container-runtime-specific logic (binary name, mount args,
stop command, startup check, orphan cleanup) into a single file so
swapping runtimes only requires replacing this one file.

Neutralize "Apple Container" references in comments and docs that
would become incorrect after a runtime swap. References that list
both runtimes as options are left unchanged.

No behavior change — Apple Container remains the default runtime.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-20 13:13:55 +02:00
committed by GitHub
parent 8fd67916b3
commit c6e1bfecc6
11 changed files with 305 additions and 101 deletions

View File

@@ -1,6 +1,6 @@
/**
* Container Runner for NanoClaw
* Spawns agent execution in Apple Container and handles IPC
* Spawns agent execution in containers and handles IPC
*/
import { ChildProcess, exec, spawn } from 'child_process';
import fs from 'fs';
@@ -17,6 +17,7 @@ import {
} from './config.js';
import { readEnvFile } from './env.js';
import { logger } from './logger.js';
import { CONTAINER_RUNTIME_BIN, readonlyMountArgs, stopContainer } from './container-runtime.js';
import { validateAdditionalMounts } from './mount-security.js';
import { RegisteredGroup } from './types.js';
@@ -88,7 +89,7 @@ function buildVolumeMounts(
});
// Global memory directory (read-only for non-main)
// Apple Container only supports directory mounts, not file mounts
// Only directory mounts are supported, not file mounts
const globalDir = path.join(GROUPS_DIR, 'global');
if (fs.existsSync(globalDir)) {
mounts.push({
@@ -160,7 +161,7 @@ function buildVolumeMounts(
});
// Mount agent-runner source from host — recompiled on container startup.
// Bypasses Apple Container's sticky build cache for code changes.
// Bypasses sticky build cache for code changes.
const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src');
mounts.push({
hostPath: agentRunnerSrc,
@@ -202,13 +203,9 @@ function buildContainerArgs(mounts: VolumeMount[], containerName: string): strin
args.push('-e', 'HOME=/home/node');
}
// Apple Container: --mount for readonly, -v for read-write
for (const mount of mounts) {
if (mount.readonly) {
args.push(
'--mount',
`type=bind,source=${mount.hostPath},target=${mount.containerPath},readonly`,
);
args.push(...readonlyMountArgs(mount.hostPath, mount.containerPath));
} else {
args.push('-v', `${mount.hostPath}:${mount.containerPath}`);
}
@@ -262,7 +259,7 @@ export async function runContainerAgent(
fs.mkdirSync(logsDir, { recursive: true });
return new Promise((resolve) => {
const container = spawn('container', containerArgs, {
const container = spawn(CONTAINER_RUNTIME_BIN, containerArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
});
@@ -369,7 +366,7 @@ export async function runContainerAgent(
const killOnTimeout = () => {
timedOut = true;
logger.error({ group: group.name, containerName }, 'Container timeout, stopping gracefully');
exec(`container stop ${containerName}`, { timeout: 15000 }, (err) => {
exec(stopContainer(containerName), { timeout: 15000 }, (err) => {
if (err) {
logger.warn({ group: group.name, containerName, err }, 'Graceful stop failed, force killing');
container.kill('SIGKILL');