Files
EJClaw/src/container-runtime.ts
2026-04-08 04:27:50 +09:00

112 lines
3.1 KiB
TypeScript

/**
* Container runtime abstraction for EJClaw verification.
* Verification profiles and related host evidence still use Docker.
* Runtime-specific logic lives here so swapping runtimes means changing one file.
*/
import { execSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import { logger } from './logger.js';
export const CONTAINER_RUNTIME_BIN = 'docker';
export const CONTAINER_HOST_GATEWAY = 'host.docker.internal';
/**
* Address the credential proxy binds to.
* Docker Desktop (macOS): 127.0.0.1
* Docker (Linux): docker0 bridge IP (containers can reach it via host.docker.internal)
*/
export const PROXY_BIND_HOST =
process.env.CREDENTIAL_PROXY_HOST || detectProxyBindHost();
function detectProxyBindHost(): string {
if (os.platform() === 'darwin') return '127.0.0.1';
if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop')) return '127.0.0.1';
const ifaces = os.networkInterfaces();
const docker0 = ifaces['docker0'];
if (docker0) {
const ipv4 = docker0.find((a) => a.family === 'IPv4');
if (ipv4) return ipv4.address;
}
return '0.0.0.0';
}
export function hostGatewayArgs(): string[] {
if (os.platform() === 'linux') {
return ['--add-host=host.docker.internal:host-gateway'];
}
return [];
}
export function readonlyMountArgs(
hostPath: string,
containerPath: string,
): string[] {
return ['-v', `${hostPath}:${containerPath}:ro`];
}
export function writableMountArgs(
hostPath: string,
containerPath: string,
): string[] {
return ['-v', `${hostPath}:${containerPath}`];
}
export function tmpfsMountArgs(
containerPath: string,
options?: string | string[],
): string[] {
if (!options || (Array.isArray(options) && options.length === 0)) {
return ['--tmpfs', containerPath];
}
const optionText = Array.isArray(options) ? options.join(',') : options;
return ['--tmpfs', `${containerPath}:${optionText}`];
}
export function stopContainer(name: string): string {
return `${CONTAINER_RUNTIME_BIN} stop ${name}`;
}
export function ensureContainerRuntimeRunning(): void {
try {
execSync(`${CONTAINER_RUNTIME_BIN} info`, {
stdio: 'pipe',
timeout: 10000,
});
logger.debug('Container runtime already running');
} catch (err) {
logger.error({ err }, 'Failed to reach container runtime');
throw new Error(
'Container runtime (Docker) is required for reviewer isolation but failed to start. Run: docker info',
);
}
}
export function cleanupOrphans(): void {
try {
const output = execSync(
`${CONTAINER_RUNTIME_BIN} ps --filter name=ejclaw-reviewer- --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' });
} catch {
/* already stopped */
}
}
if (orphans.length > 0) {
logger.info(
{ count: orphans.length, names: orphans },
'Stopped orphaned reviewer containers',
);
}
} catch (err) {
logger.warn({ err }, 'Failed to clean up orphaned containers');
}
}