import { execFile } from 'child_process'; export interface DockerSandboxConfig { sessionId: string; image: string; workspaceDir: string; network: 'none' | 'bridge' | 'host'; memoryLimit: string; cpuLimit: string; timeoutSeconds: number; } export interface ExecOptions { cwd?: string; timeout?: number; } export interface ExecResult { stdout: string; stderr: string; } /** * Manages a single Docker container for sandboxed tool execution. * Uses the Docker CLI directly (no SDK dependency). */ export class DockerSandbox { private config: DockerSandboxConfig; private _containerId: string | null = null; private _hostWorkdir: string; constructor(config: DockerSandboxConfig) { this.config = config; const sanitizedId = config.sessionId.replace(/[^a-zA-Z0-9_-]/g, '_'); this._hostWorkdir = `/tmp/flynn-sandbox-${sanitizedId}`; } get containerId(): string | null { return this._containerId; } get containerName(): string { const sanitizedId = this.config.sessionId.replace(/[^a-zA-Z0-9_-]/g, '_'); return `flynn-${sanitizedId}`; } /** Create and start the sandbox container. */ async create(): Promise { const args = [ 'create', '--name', this.containerName, '--memory', this.config.memoryLimit, '--cpus', this.config.cpuLimit, '--network', this.config.network, '-v', `${this._hostWorkdir}:${this.config.workspaceDir}`, this.config.image, 'sleep', 'infinity', ]; const createResult = await this.dockerCmd(args); this._containerId = createResult.stdout.trim(); await this.dockerCmd(['start', this._containerId]); } /** Execute a command inside the container. */ async exec(command: string, opts?: ExecOptions): Promise { if (!this._containerId) { throw new Error('Sandbox container not created. Call create() first.'); } const args = ['exec']; if (opts?.cwd) { args.push('-w', opts.cwd); } args.push(this._containerId, 'bash', '-c', command); const timeout = opts?.timeout ?? this.config.timeoutSeconds * 1000; return this.dockerCmd(args, timeout); } /** Force-remove the container. */ async destroy(): Promise { if (!this._containerId) return; try { await this.dockerCmd(['rm', '-f', this._containerId]); } catch { // Ignore errors during cleanup } this._containerId = null; } /** Check if Docker is available on this host. */ static async isAvailable(): Promise { try { await new Promise((resolve, reject) => { execFile('docker', ['version', '--format', '{{.Server.Version}}'], { timeout: 5000, }, (error, stdout) => { if (error) reject(error); else resolve(stdout); }); }); return true; } catch { return false; } } /** Run a docker CLI command. */ private dockerCmd(args: string[], timeout = 30_000): Promise { return new Promise((resolve, reject) => { execFile('docker', args, { timeout, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(error); return; } resolve({ stdout, stderr }); }); }); } }