refactor: simplify paired review system and add reviewer container isolation
Phase 1 — Strip over-engineering (-3,665 lines): - DB tables: 7 → 3 (remove executions, approvals, artifacts, events) - Task statuses: 11 → 5 (active, review_ready, in_review, merge_ready, completed) - Remove plan governance, event sourcing, gate/verdict, freshness guards - paired-execution-context: 980 → 299 lines - Session commands: remove /risk, /plan, /approve-plan, /request-plan-changes Phase 2 — Container isolation for reviewers: - Add container-runtime.ts (Docker abstraction) - Add credential-proxy.ts (API key injection without container exposure) - Add container-runner.ts (reviewer-specific read-only mount + tmpfs) - Add container/Dockerfile + agent-runner (Chromium, Claude Code, Codex) - agent-runner.ts: auto-route reviewer execution to container mode
This commit is contained in:
103
src/container-runtime.ts
Normal file
103
src/container-runtime.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Container runtime abstraction for EJClaw.
|
||||
* Reviewer agents run inside Docker containers for read-only isolation.
|
||||
* All 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): string[] {
|
||||
return ['--tmpfs', containerPath];
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user