@agentforge-ai/sandbox 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +70 -1
- package/dist/index.js +279 -7
- package/dist/index.js.map +1 -1
- package/dist/sandbox-manager.js +275 -8
- package/dist/sandbox-manager.js.map +1 -1
- package/dist/types.d.ts +36 -2
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/docker-sandbox.ts","../src/security.ts","../src/container-pool.ts","../src/sandbox-manager.ts"],"sourcesContent":["/**\n * @module docker-sandbox\n *\n * DockerSandbox — a container-backed {@link SandboxProvider} for AgentForge.\n *\n * Each instance manages exactly one Docker container. The container is\n * created lazily on the first call to {@link DockerSandbox.start}, executed\n * via the Docker exec API, and destroyed via {@link DockerSandbox.destroy}.\n *\n * @example\n * ```ts\n * const sandbox = new DockerSandbox({\n * scope: 'agent',\n * workspaceAccess: 'ro',\n * workspacePath: '/home/user/project',\n * resourceLimits: { memoryMb: 512, cpuShares: 512 },\n * });\n * await sandbox.start();\n * const result = await sandbox.exec('echo hello');\n * console.log(result.stdout); // \"hello\\n\"\n * await sandbox.destroy();\n * ```\n */\n\nimport Dockerode from 'dockerode';\nimport { randomUUID } from 'node:crypto';\nimport type { DockerSandboxConfig, ExecOptions, ExecResult, SandboxProvider } from './types.js';\nimport { DEFAULT_CAP_DROP, SecurityError, validateBinds, validateCommand, validateImageName } from './security.js';\n\n/** Default Docker image used when none is specified. */\nconst DEFAULT_IMAGE = 'node:22-slim';\n\n/** Default per-exec timeout in milliseconds. */\nconst DEFAULT_EXEC_TIMEOUT_MS = 30_000;\n\n/** Default container workspace mount point. */\nconst DEFAULT_CONTAINER_WORKSPACE = '/workspace';\n\n// ---------------------------------------------------------------------------\n// Stream demuxing\n// ---------------------------------------------------------------------------\n\n/**\n * Decodes the multiplexed stream that Docker returns for exec output.\n *\n * Docker prefixes every chunk with an 8-byte header:\n * [stream_type(1)] [0(3)] [size(4 BE)]\n * where stream_type is 1 = stdout, 2 = stderr.\n */\nfunction demuxDockerStream(buffer: Buffer): { stdout: string; stderr: string } {\n let stdout = '';\n let stderr = '';\n let offset = 0;\n\n while (offset + 8 <= buffer.length) {\n const streamType = buffer[offset];\n const frameSize = buffer.readUInt32BE(offset + 4);\n offset += 8;\n\n if (offset + frameSize > buffer.length) break;\n\n const chunk = buffer.slice(offset, offset + frameSize).toString('utf8');\n offset += frameSize;\n\n if (streamType === 1) {\n stdout += chunk;\n } else if (streamType === 2) {\n stderr += chunk;\n }\n }\n\n return { stdout, stderr };\n}\n\n// ---------------------------------------------------------------------------\n// DockerSandbox\n// ---------------------------------------------------------------------------\n\n/**\n * Container-based sandbox using the Docker engine.\n *\n * Implements the {@link SandboxProvider} interface for full lifecycle\n * management, command execution, and file I/O within an isolated container.\n */\nexport class DockerSandbox implements SandboxProvider {\n private readonly config: Required<\n Pick<DockerSandboxConfig, 'scope' | 'workspaceAccess' | 'image' | 'containerWorkspacePath'>\n > &\n DockerSandboxConfig;\n\n private readonly docker: Dockerode;\n private container: Dockerode.Container | null = null;\n private containerId: string | null = null;\n private killTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * @param config - Sandbox configuration.\n * @param docker - Optional pre-configured Dockerode instance (useful in tests).\n */\n constructor(config: DockerSandboxConfig, docker?: Dockerode) {\n // Validate security constraints eagerly\n const image = config.image ?? DEFAULT_IMAGE;\n validateImageName(image);\n\n if (config.binds && config.binds.length > 0) {\n validateBinds(config.binds);\n }\n\n this.config = {\n ...config,\n image,\n containerWorkspacePath: config.containerWorkspacePath ?? DEFAULT_CONTAINER_WORKSPACE,\n };\n\n this.docker =\n docker ??\n new Dockerode(\n process.env['DOCKER_HOST']\n ? { host: process.env['DOCKER_HOST'] }\n : { socketPath: '/var/run/docker.sock' },\n );\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Create and start the Docker container.\n * Idempotent — calling start() on an already-running sandbox is a no-op.\n */\n async start(): Promise<void> {\n if (this.container) return;\n\n const { image, scope, resourceLimits, binds, env, timeout, workspaceAccess, workspacePath, containerWorkspacePath } = this.config;\n\n const name = `agentforge-${scope}-${randomUUID().slice(0, 8)}`;\n\n const envArray = Object.entries(env ?? {}).map(([k, v]) => `${k}=${v}`);\n\n // Build bind mounts\n const allBinds: string[] = [...(binds ?? [])];\n\n // Mount workspace if configured\n if (workspaceAccess !== 'none' && workspacePath) {\n const mode = workspaceAccess === 'ro' ? 'ro' : 'rw';\n allBinds.push(`${workspacePath}:${containerWorkspacePath}:${mode}`);\n }\n\n const hostConfig: Dockerode.HostConfig = {\n // Resource limits\n CpuShares: resourceLimits?.cpuShares,\n Memory: resourceLimits?.memoryMb ? resourceLimits.memoryMb * 1024 * 1024 : undefined,\n PidsLimit: resourceLimits?.pidsLimit ?? 256,\n\n // Security hardening\n CapDrop: [...DEFAULT_CAP_DROP],\n SecurityOpt: ['no-new-privileges:true'],\n ReadonlyRootfs: false,\n\n // Bind mounts\n Binds: allBinds.length > 0 ? allBinds : undefined,\n };\n\n this.container = await this.docker.createContainer({\n name,\n Image: image,\n // Keep container alive — we run commands via exec\n Cmd: ['/bin/sh', '-c', 'while true; do sleep 3600; done'],\n Env: envArray,\n AttachStdin: false,\n AttachStdout: false,\n AttachStderr: false,\n Tty: false,\n NetworkDisabled: resourceLimits?.networkDisabled ?? false,\n WorkingDir: containerWorkspacePath,\n Labels: {\n 'agentforge.scope': scope,\n 'agentforge.managed': 'true',\n },\n HostConfig: hostConfig,\n });\n\n await this.container.start();\n this.containerId = this.container.id;\n\n // Auto-kill after configured timeout\n if (timeout && timeout > 0) {\n this.killTimer = setTimeout(() => {\n void this.destroy();\n }, timeout * 1000);\n }\n }\n\n /**\n * Stop the container gracefully (10 s grace period then SIGKILL).\n * The container is kept for potential restart.\n */\n async stop(): Promise<void> {\n this._clearKillTimer();\n if (!this.container) return;\n\n try {\n const info = await this.container.inspect();\n if (info.State.Running) {\n await this.container.stop({ t: 10 });\n }\n } catch {\n // Container may already be stopped — ignore\n }\n }\n\n /**\n * Destroy the container and release all resources.\n * Safe to call multiple times.\n */\n async destroy(): Promise<void> {\n this._clearKillTimer();\n const container = this.container;\n this.container = null;\n this.containerId = null;\n\n if (!container) return;\n\n try {\n await container.remove({ force: true });\n } catch {\n // Already gone — ignore\n }\n }\n\n // ---------------------------------------------------------------------------\n // Execution\n // ---------------------------------------------------------------------------\n\n /**\n * Execute a shell command inside the running container.\n *\n * @param command - Shell command string passed to `/bin/sh -c`.\n * @param options - Per-call options (timeout, cwd, env overrides).\n */\n async exec(command: string, options: ExecOptions = {}): Promise<ExecResult> {\n if (!this.container) {\n throw new Error('DockerSandbox: container is not running. Call start() first.');\n }\n\n // Defense-in-depth command validation\n validateCommand(command);\n\n const timeoutMs = options.timeout ?? DEFAULT_EXEC_TIMEOUT_MS;\n const envOverride = Object.entries(options.env ?? {}).map(([k, v]) => `${k}=${v}`);\n\n const execInstance = await this.container.exec({\n Cmd: ['/bin/sh', '-c', command],\n AttachStdout: true,\n AttachStderr: true,\n Tty: false,\n WorkingDir: options.cwd,\n Env: envOverride.length > 0 ? envOverride : undefined,\n });\n\n return new Promise<ExecResult>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new Error(`DockerSandbox: exec timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n execInstance.start({ hijack: true, stdin: false }, (err, stream) => {\n if (err) {\n clearTimeout(timer);\n reject(err);\n return;\n }\n\n if (!stream) {\n clearTimeout(timer);\n reject(new Error('DockerSandbox: no stream returned from exec'));\n return;\n }\n\n const chunks: Buffer[] = [];\n\n stream.on('data', (chunk: Buffer) => chunks.push(chunk));\n\n stream.on('end', async () => {\n clearTimeout(timer);\n try {\n const raw = Buffer.concat(chunks);\n const { stdout, stderr } = demuxDockerStream(raw);\n\n // Inspect exec to get exit code\n const inspectResult = await execInstance.inspect();\n const exitCode = inspectResult.ExitCode ?? 0;\n\n resolve({ stdout, stderr, exitCode });\n } catch (inspectErr) {\n reject(inspectErr);\n }\n });\n\n stream.on('error', (streamErr: Error) => {\n clearTimeout(timer);\n reject(streamErr);\n });\n });\n });\n }\n\n /**\n * Read a file from the container filesystem by running `cat`.\n *\n * @param path - Absolute path inside the container.\n */\n async readFile(path: string): Promise<string> {\n const result = await this.exec(`cat \"${path.replace(/\"/g, '\\\\\"')}\"`);\n if (result.exitCode !== 0) {\n throw new Error(\n `DockerSandbox.readFile: failed to read \"${path}\" (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n return result.stdout;\n }\n\n /**\n * Write content to a file inside the container using base64 encoding\n * to avoid shell quoting issues.\n *\n * @param path - Absolute path inside the container.\n * @param content - UTF-8 string content.\n */\n async writeFile(path: string, content: string): Promise<void> {\n const b64 = Buffer.from(content, 'utf8').toString('base64');\n const cmd = `printf '%s' \"${b64}\" | base64 -d > \"${path.replace(/\"/g, '\\\\\"')}\"`;\n const result = await this.exec(cmd);\n if (result.exitCode !== 0) {\n throw new Error(\n `DockerSandbox.writeFile: failed to write \"${path}\" (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n }\n\n // ---------------------------------------------------------------------------\n // Health\n // ---------------------------------------------------------------------------\n\n /**\n * Returns true if the underlying Docker container is running.\n */\n async isRunning(): Promise<boolean> {\n if (!this.container) return false;\n try {\n const info = await this.container.inspect();\n return info.State.Running === true;\n } catch {\n return false;\n }\n }\n\n /**\n * Returns the Docker container ID or null if not yet started.\n */\n getContainerId(): string | null {\n return this.containerId;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _clearKillTimer(): void {\n if (this.killTimer !== null) {\n clearTimeout(this.killTimer);\n this.killTimer = null;\n }\n }\n}\n","/**\n * @module security\n *\n * Security helpers for the Docker sandbox implementation.\n *\n * Centralised in one module so policy changes propagate everywhere.\n * All validation functions throw {@link SecurityError} on violations.\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Host-side paths that must never be bind-mounted into a sandbox container.\n * Mounting these paths would allow container-escape or privilege escalation.\n */\nexport const BLOCKED_BIND_PREFIXES: readonly string[] = [\n '/var/run/docker.sock',\n '/etc',\n '/proc',\n '/sys',\n '/dev',\n '/boot',\n '/root',\n];\n\n/**\n * Linux capabilities dropped by default for every sandbox container.\n * We start with no capabilities at all (drop \"ALL\") then add nothing back.\n */\nexport const DEFAULT_CAP_DROP: readonly string[] = ['ALL'];\n\n/**\n * Allowed image name patterns (non-arbitrary image names in production).\n * Only images that match one of these prefixes are permitted.\n * In test / dev the list can be extended via AGENTFORGE_ALLOWED_IMAGES env var.\n */\nconst BASE_ALLOWED_IMAGE_PREFIXES: readonly string[] = [\n 'node:',\n 'python:',\n 'ubuntu:',\n 'debian:',\n 'alpine:',\n 'agentforge/',\n];\n\n// ---------------------------------------------------------------------------\n// Validation functions\n// ---------------------------------------------------------------------------\n\n/**\n * Throws if the provided bind-mount spec contains a blocked host path.\n *\n * @param bind - A bind-mount spec in `host:container[:mode]` format.\n * @throws {SecurityError} when the host path is on the block-list.\n */\nexport function validateBind(bind: string): void {\n const hostPath = bind.split(':')[0];\n if (!hostPath) {\n throw new SecurityError(`Invalid bind mount spec: \"${bind}\"`);\n }\n\n for (const blocked of BLOCKED_BIND_PREFIXES) {\n if (hostPath === blocked || hostPath.startsWith(blocked + '/') || hostPath.startsWith(blocked)) {\n throw new SecurityError(\n `Bind mount \"${bind}\" is blocked. Host path \"${hostPath}\" matches blocked prefix \"${blocked}\".`,\n );\n }\n }\n}\n\n/**\n * Validate all bind mounts in the provided array.\n * @throws {SecurityError} on the first violation found.\n */\nexport function validateBinds(binds: string[]): void {\n for (const bind of binds) {\n validateBind(bind);\n }\n}\n\n/**\n * Validate that an image name is on the allow-list.\n *\n * In production (NODE_ENV === 'production') only known safe images are allowed.\n * In development/test any image name that passes format validation is accepted.\n *\n * Additional allowed prefixes can be injected via the\n * `AGENTFORGE_ALLOWED_IMAGES` env var (comma-separated prefixes).\n *\n * @param image - Docker image name, e.g. `node:22-slim`.\n * @throws {SecurityError} if the image is not permitted.\n */\nexport function validateImageName(image: string): void {\n if (!image || typeof image !== 'string') {\n throw new SecurityError('Image name must be a non-empty string.');\n }\n\n // Reject obviously dangerous patterns (shell metacharacters)\n if (/[;&|`$(){}[\\]<>]/.test(image)) {\n throw new SecurityError(`Image name \"${image}\" contains forbidden characters.`);\n }\n\n if (process.env['NODE_ENV'] !== 'production') {\n // In dev/test, just ensure the name is a plausible Docker image reference\n return;\n }\n\n // Build the full allow-list (base + env-configured extras)\n const extraPrefixes = (process.env['AGENTFORGE_ALLOWED_IMAGES'] ?? '')\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n const allowedPrefixes = [...BASE_ALLOWED_IMAGE_PREFIXES, ...extraPrefixes];\n\n const allowed = allowedPrefixes.some((prefix) => image.startsWith(prefix));\n if (!allowed) {\n throw new SecurityError(\n `Image \"${image}\" is not on the allow-list. ` +\n `Allowed prefixes: ${allowedPrefixes.join(', ')}. ` +\n `Add custom prefixes via AGENTFORGE_ALLOWED_IMAGES env var.`,\n );\n }\n}\n\n/**\n * Validate that a command does not contain obvious escape attempts.\n * This is a defense-in-depth measure — the container itself is the primary boundary.\n *\n * @param command - The shell command to validate.\n * @throws {SecurityError} if the command contains dangerous patterns.\n */\nexport function validateCommand(command: string): void {\n if (!command || typeof command !== 'string') {\n throw new SecurityError('Command must be a non-empty string.');\n }\n\n // Block attempts to access the Docker socket from within the container\n const dangerousPatterns = [\n /docker\\.sock/i,\n /nsenter\\s/i,\n /mount\\s+-t\\s+proc/i,\n ];\n\n for (const pattern of dangerousPatterns) {\n if (pattern.test(command)) {\n throw new SecurityError(\n `Command contains a potentially dangerous pattern: ${pattern.source}`,\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error class\n// ---------------------------------------------------------------------------\n\n/**\n * A structured error type for sandbox security violations.\n */\nexport class SecurityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SecurityError';\n }\n}\n","/**\n * @module container-pool\n *\n * ContainerPool — maintains a pool of warm {@link DockerSandbox} instances\n * to amortise container cold-start latency.\n *\n * Design:\n * - A fixed-size pool of pre-started containers (warm slots).\n * - {@link ContainerPool.acquire} returns the least-recently-used idle\n * container or starts a new one if the pool has not yet reached capacity.\n * - {@link ContainerPool.release} marks a container as idle so it can be reused.\n * - An idle-eviction sweep runs periodically and destroys containers that have\n * been idle longer than `idleTimeoutSeconds`.\n * - LRU eviction is applied when the pool is at capacity and a new container is\n * requested but none are idle.\n */\n\nimport type Dockerode from 'dockerode';\nimport { DockerSandbox } from './docker-sandbox.js';\nimport type { DockerSandboxConfig, PoolConfig, PoolEntry, SandboxProvider } from './types.js';\nimport { randomUUID } from 'node:crypto';\n\nconst DEFAULT_MAX_SIZE = 3;\nconst DEFAULT_IDLE_TIMEOUT_SECONDS = 300;\nconst SWEEP_INTERVAL_MS = 30_000;\n\n/**\n * A pool of warm Docker containers that can be acquired and released for reuse.\n *\n * @example\n * ```ts\n * const pool = new ContainerPool({ image: 'node:22-slim', scope: 'agent' });\n * await pool.warmUp();\n *\n * const sb = await pool.acquire();\n * await sb.exec('node -e \"console.log(1+1)\"');\n * await pool.release(sb);\n *\n * await pool.drain();\n * ```\n */\nexport class ContainerPool {\n private readonly config: Required<PoolConfig>;\n private readonly docker?: Dockerode;\n private readonly entries: Map<string, PoolEntry> = new Map();\n private sweepTimer: ReturnType<typeof setInterval> | null = null;\n private draining = false;\n\n constructor(config: PoolConfig, docker?: Dockerode) {\n this.config = {\n maxSize: DEFAULT_MAX_SIZE,\n idleTimeoutSeconds: DEFAULT_IDLE_TIMEOUT_SECONDS,\n ...config,\n };\n this.docker = docker;\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /**\n * Pre-warm the pool up to `maxSize` containers.\n * Resolves once all warm containers are started.\n */\n async warmUp(): Promise<void> {\n const needed = this.config.maxSize - this.entries.size;\n if (needed <= 0) return;\n\n await Promise.all(\n Array.from({ length: needed }).map(async () => {\n await this._addEntry();\n }),\n );\n\n this._startSweep();\n }\n\n /**\n * Acquire an idle sandbox from the pool.\n *\n * If an idle entry is available, it is marked in-use and returned immediately.\n * If the pool is not yet at capacity, a new container is started and returned.\n * If the pool is at capacity and all containers are in use, the LRU idle\n * container is evicted and a fresh one is started.\n *\n * @throws If the pool is draining.\n */\n async acquire(): Promise<SandboxProvider> {\n if (this.draining) {\n throw new Error('ContainerPool: pool is draining — cannot acquire new sandboxes.');\n }\n\n // 1. Find an idle entry (LRU = smallest lastUsedAt)\n const idle = this._lruIdle();\n if (idle) {\n idle.inUse = true;\n idle.lastUsedAt = Date.now();\n return idle.sandbox;\n }\n\n // 2. Pool has spare capacity — start a fresh container\n if (this.entries.size < this.config.maxSize) {\n const entry = await this._addEntry();\n entry.inUse = true;\n entry.lastUsedAt = Date.now();\n return entry.sandbox;\n }\n\n // 3. Pool at capacity, all in use — evict LRU entry and replace\n const lruKey = this._lruAnyKey();\n if (lruKey) {\n const evicted = this.entries.get(lruKey)!;\n this.entries.delete(lruKey);\n void evicted.sandbox.destroy().catch(() => {/* best-effort */});\n }\n\n const entry = await this._addEntry();\n entry.inUse = true;\n entry.lastUsedAt = Date.now();\n return entry.sandbox;\n }\n\n /**\n * Release a previously acquired sandbox back to the pool.\n * The sandbox is reset to an idle state for future reuse.\n */\n async release(sandbox: SandboxProvider): Promise<void> {\n for (const entry of this.entries.values()) {\n if (entry.sandbox === sandbox) {\n entry.inUse = false;\n entry.lastUsedAt = Date.now();\n return;\n }\n }\n // Sandbox not found in pool — destroy it to avoid leaks\n await sandbox.destroy();\n }\n\n /**\n * Drain the pool: stop acquiring and destroy all containers.\n * After draining, the pool stays in a drained state and rejects new acquires.\n */\n async drain(): Promise<void> {\n if (this.draining && this.entries.size === 0) return;\n this.draining = true;\n this._stopSweep();\n\n await Promise.all(\n Array.from(this.entries.values()).map((entry) =>\n entry.sandbox.destroy().catch(() => {/* best-effort */}),\n ),\n );\n this.entries.clear();\n }\n\n /**\n * Returns the current number of entries (in-use + idle).\n */\n get size(): number {\n return this.entries.size;\n }\n\n /**\n * Returns the number of idle (available) entries.\n */\n get idleCount(): number {\n let count = 0;\n for (const entry of this.entries.values()) {\n if (!entry.inUse) count++;\n }\n return count;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private async _addEntry(): Promise<PoolEntry> {\n const sandboxConfig: DockerSandboxConfig = {\n image: this.config.image,\n scope: this.config.scope,\n workspaceAccess: 'none',\n };\n\n const sandbox = new DockerSandbox(sandboxConfig, this.docker);\n await sandbox.start();\n\n const id = randomUUID();\n const entry: PoolEntry = {\n sandbox,\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n inUse: false,\n };\n\n this.entries.set(id, entry);\n return entry;\n }\n\n /** Return the idle entry with the smallest lastUsedAt (LRU idle). */\n private _lruIdle(): PoolEntry | null {\n let best: PoolEntry | null = null;\n for (const entry of this.entries.values()) {\n if (!entry.inUse) {\n if (!best || entry.lastUsedAt < best.lastUsedAt) {\n best = entry;\n }\n }\n }\n return best;\n }\n\n /** Return the key of the entry with the smallest lastUsedAt (LRU any). */\n private _lruAnyKey(): string | null {\n let bestKey: string | null = null;\n let bestTime = Infinity;\n for (const [key, entry] of this.entries) {\n if (entry.lastUsedAt < bestTime) {\n bestTime = entry.lastUsedAt;\n bestKey = key;\n }\n }\n return bestKey;\n }\n\n /** Start periodic idle-eviction sweep. */\n private _startSweep(): void {\n if (this.sweepTimer) return;\n this.sweepTimer = setInterval(() => void this._sweep(), SWEEP_INTERVAL_MS);\n if (this.sweepTimer.unref) this.sweepTimer.unref();\n }\n\n private _stopSweep(): void {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = null;\n }\n }\n\n /** Remove and destroy containers that have been idle past the timeout. */\n private async _sweep(): Promise<void> {\n if (this.draining) return;\n\n const nowMs = Date.now();\n const idleTimeoutMs = this.config.idleTimeoutSeconds * 1000;\n\n const evictions: [string, PoolEntry][] = [];\n\n for (const [key, entry] of this.entries) {\n if (!entry.inUse && nowMs - entry.lastUsedAt > idleTimeoutMs) {\n evictions.push([key, entry]);\n }\n }\n\n for (const [key, entry] of evictions) {\n this.entries.delete(key);\n await entry.sandbox.destroy().catch(() => {/* best-effort */});\n }\n }\n}\n","/**\n * @module sandbox-manager\n *\n * SandboxManager — factory + registry for sandbox instances.\n *\n * Responsibilities:\n * - Create the correct sandbox type (Docker | E2B) from a unified config.\n * - Apply the `agentforge-{scope}-{id}` naming convention.\n * - Track active sandboxes and clean them up on process exit.\n * - Verify Docker availability on startup; print a friendly message if absent.\n */\n\nimport Dockerode from 'dockerode';\nimport { randomUUID } from 'node:crypto';\nimport { DockerSandbox } from './docker-sandbox.js';\nimport type {\n DockerSandboxConfig,\n ExecOptions,\n ExecResult,\n SandboxManagerConfig,\n SandboxProvider,\n} from './types.js';\n\n/**\n * A thin E2B stub that satisfies {@link SandboxProvider} but throws at runtime.\n * The real E2B implementation lives in @agentforge-ai/core.\n * This stub lets the manager compile and be tested without the E2B dependency.\n */\nclass E2BProviderStub implements SandboxProvider {\n async start(): Promise<void> {\n throw new Error(\n 'SandboxManager: E2B provider is not bundled in @agentforge-ai/sandbox. ' +\n 'Use the SandboxManager from @agentforge-ai/core instead.',\n );\n }\n async stop(): Promise<void> { /* noop until started */ }\n async destroy(): Promise<void> { /* noop */ }\n async exec(_cmd: string, _opts?: ExecOptions): Promise<ExecResult> {\n throw new Error('E2BProviderStub: not implemented');\n }\n async readFile(_path: string): Promise<string> {\n throw new Error('E2BProviderStub: not implemented');\n }\n async writeFile(_path: string, _content: string): Promise<void> {\n throw new Error('E2BProviderStub: not implemented');\n }\n async isRunning(): Promise<boolean> { return false; }\n getContainerId(): string | null { return null; }\n}\n\n/**\n * Checks whether the Docker daemon is reachable.\n *\n * @param docker - Dockerode instance to ping.\n * @returns `true` if Docker is available, `false` otherwise.\n */\nexport async function isDockerAvailable(docker: Dockerode): Promise<boolean> {\n try {\n await docker.ping();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Central factory and lifecycle manager for sandbox instances.\n *\n * @example\n * ```ts\n * const manager = new SandboxManager({ provider: 'docker' });\n * await manager.initialize();\n *\n * const sb = await manager.create({ scope: 'agent', workspaceAccess: 'none' });\n * const result = await sb.exec('echo hello');\n * await manager.destroy(sb);\n *\n * await manager.shutdown();\n * ```\n */\nexport class SandboxManager {\n private readonly config: SandboxManagerConfig;\n private readonly docker: Dockerode;\n private readonly active = new Map<string, SandboxProvider>();\n private shutdownRegistered = false;\n\n constructor(config: SandboxManagerConfig = {}) {\n this.config = { provider: 'docker', ...config };\n\n const dockerHostCfg = config.dockerHost;\n if (dockerHostCfg?.host) {\n this.docker = new Dockerode({\n host: dockerHostCfg.host,\n port: dockerHostCfg.port ?? 2376,\n protocol: dockerHostCfg.protocol ?? 'http',\n });\n } else {\n this.docker = new Dockerode({\n socketPath: dockerHostCfg?.socketPath ?? '/var/run/docker.sock',\n });\n }\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /**\n * Initialize the manager. For the Docker provider this verifies that the\n * Docker daemon is reachable. Call this once at application startup.\n *\n * @throws Error if the Docker daemon cannot be reached (Docker provider only).\n */\n async initialize(): Promise<void> {\n if (this.config.provider === 'docker') {\n const available = await isDockerAvailable(this.docker);\n if (!available) {\n console.warn(\n '[SandboxManager] Docker daemon is not reachable. ' +\n 'Agent tool execution will fail until Docker is started. ' +\n 'Install Docker: https://docs.docker.com/get-docker/',\n );\n }\n }\n\n this._registerShutdownHandlers();\n }\n\n /**\n * Create and start a new sandbox.\n *\n * The sandbox is registered internally; call {@link SandboxManager.destroy}\n * or {@link SandboxManager.shutdown} to release it.\n *\n * @param overrides - Per-sandbox config overrides merged with manager defaults.\n */\n async create(\n overrides: Pick<DockerSandboxConfig, 'scope' | 'workspaceAccess'> &\n Partial<DockerSandboxConfig>,\n ): Promise<SandboxProvider> {\n if (this.config.provider === 'e2b') {\n const stub = new E2BProviderStub();\n const id = this._generateId(overrides.scope);\n this.active.set(id, stub);\n return stub;\n }\n\n const mergedConfig: DockerSandboxConfig = {\n image: 'node:22-slim',\n ...this.config.dockerConfig,\n ...overrides,\n };\n\n const sandbox = new DockerSandbox(mergedConfig, this.docker);\n await sandbox.start();\n\n const id = this._generateId(overrides.scope);\n this.active.set(id, sandbox);\n return sandbox;\n }\n\n /**\n * Destroy a specific sandbox and remove it from the active registry.\n */\n async destroy(sandbox: SandboxProvider): Promise<void> {\n await sandbox.destroy();\n for (const [key, value] of this.active) {\n if (value === sandbox) {\n this.active.delete(key);\n break;\n }\n }\n }\n\n /**\n * Destroy all active sandboxes and shut the manager down.\n * Called automatically on SIGTERM / SIGINT when registered.\n */\n async shutdown(): Promise<void> {\n const destroyAll = Array.from(this.active.values()).map((sb) =>\n sb.destroy().catch((err: unknown) => {\n console.error('[SandboxManager] Error destroying sandbox during shutdown:', err);\n }),\n );\n await Promise.all(destroyAll);\n this.active.clear();\n }\n\n /**\n * Returns the number of currently active sandboxes.\n */\n get activeCount(): number {\n return this.active.size;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _generateId(scope: string): string {\n return `agentforge-${scope}-${randomUUID().slice(0, 8)}`;\n }\n\n private _registerShutdownHandlers(): void {\n if (this.shutdownRegistered) return;\n this.shutdownRegistered = true;\n\n const handler = () => {\n void this.shutdown().finally(() => process.exit(0));\n };\n\n process.once('SIGTERM', handler);\n process.once('SIGINT', handler);\n process.once('beforeExit', () => void this.shutdown());\n }\n}\n"],"mappings":";AAwBA,OAAO,eAAe;AACtB,SAAS,kBAAkB;;;ACRpB,IAAM,wBAA2C;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,IAAM,mBAAsC,CAAC,KAAK;AAOzD,IAAM,8BAAiD;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,aAAa,MAAoB;AAC/C,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AAClC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,cAAc,6BAA6B,IAAI,GAAG;AAAA,EAC9D;AAEA,aAAW,WAAW,uBAAuB;AAC3C,QAAI,aAAa,WAAW,SAAS,WAAW,UAAU,GAAG,KAAK,SAAS,WAAW,OAAO,GAAG;AAC9F,YAAM,IAAI;AAAA,QACR,eAAe,IAAI,4BAA4B,QAAQ,6BAA6B,OAAO;AAAA,MAC7F;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,cAAc,OAAuB;AACnD,aAAW,QAAQ,OAAO;AACxB,iBAAa,IAAI;AAAA,EACnB;AACF;AAcO,SAAS,kBAAkB,OAAqB;AACrD,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,UAAM,IAAI,cAAc,wCAAwC;AAAA,EAClE;AAGA,MAAI,mBAAmB,KAAK,KAAK,GAAG;AAClC,UAAM,IAAI,cAAc,eAAe,KAAK,kCAAkC;AAAA,EAChF;AAEA,MAAI,QAAQ,IAAI,UAAU,MAAM,cAAc;AAE5C;AAAA,EACF;AAGA,QAAM,iBAAiB,QAAQ,IAAI,2BAA2B,KAAK,IAChE,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AAEjB,QAAM,kBAAkB,CAAC,GAAG,6BAA6B,GAAG,aAAa;AAEzE,QAAM,UAAU,gBAAgB,KAAK,CAAC,WAAW,MAAM,WAAW,MAAM,CAAC;AACzE,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR,UAAU,KAAK,iDACQ,gBAAgB,KAAK,IAAI,CAAC;AAAA,IAEnD;AAAA,EACF;AACF;AASO,SAAS,gBAAgB,SAAuB;AACrD,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,cAAc,qCAAqC;AAAA,EAC/D;AAGA,QAAM,oBAAoB;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,WAAW,mBAAmB;AACvC,QAAI,QAAQ,KAAK,OAAO,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,qDAAqD,QAAQ,MAAM;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AACF;AASO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;;ADzIA,IAAM,gBAAgB;AAGtB,IAAM,0BAA0B;AAGhC,IAAM,8BAA8B;AAapC,SAAS,kBAAkB,QAAoD;AAC7E,MAAI,SAAS;AACb,MAAI,SAAS;AACb,MAAI,SAAS;AAEb,SAAO,SAAS,KAAK,OAAO,QAAQ;AAClC,UAAM,aAAa,OAAO,MAAM;AAChC,UAAM,YAAY,OAAO,aAAa,SAAS,CAAC;AAChD,cAAU;AAEV,QAAI,SAAS,YAAY,OAAO,OAAQ;AAExC,UAAM,QAAQ,OAAO,MAAM,QAAQ,SAAS,SAAS,EAAE,SAAS,MAAM;AACtE,cAAU;AAEV,QAAI,eAAe,GAAG;AACpB,gBAAU;AAAA,IACZ,WAAW,eAAe,GAAG;AAC3B,gBAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAYO,IAAM,gBAAN,MAA+C;AAAA,EACnC;AAAA,EAKA;AAAA,EACT,YAAwC;AAAA,EACxC,cAA6B;AAAA,EAC7B,YAAkD;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1D,YAAY,QAA6B,QAAoB;AAE3D,UAAM,QAAQ,OAAO,SAAS;AAC9B,sBAAkB,KAAK;AAEvB,QAAI,OAAO,SAAS,OAAO,MAAM,SAAS,GAAG;AAC3C,oBAAc,OAAO,KAAK;AAAA,IAC5B;AAEA,SAAK,SAAS;AAAA,MACZ,GAAG;AAAA,MACH;AAAA,MACA,wBAAwB,OAAO,0BAA0B;AAAA,IAC3D;AAEA,SAAK,SACH,UACA,IAAI;AAAA,MACF,QAAQ,IAAI,aAAa,IACrB,EAAE,MAAM,QAAQ,IAAI,aAAa,EAAE,IACnC,EAAE,YAAY,uBAAuB;AAAA,IAC3C;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,QAAuB;AAC3B,QAAI,KAAK,UAAW;AAEpB,UAAM,EAAE,OAAO,OAAO,gBAAgB,OAAO,KAAK,SAAS,iBAAiB,eAAe,uBAAuB,IAAI,KAAK;AAE3H,UAAM,OAAO,cAAc,KAAK,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AAE5D,UAAM,WAAW,OAAO,QAAQ,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE;AAGtE,UAAM,WAAqB,CAAC,GAAI,SAAS,CAAC,CAAE;AAG5C,QAAI,oBAAoB,UAAU,eAAe;AAC/C,YAAM,OAAO,oBAAoB,OAAO,OAAO;AAC/C,eAAS,KAAK,GAAG,aAAa,IAAI,sBAAsB,IAAI,IAAI,EAAE;AAAA,IACpE;AAEA,UAAM,aAAmC;AAAA;AAAA,MAEvC,WAAW,gBAAgB;AAAA,MAC3B,QAAQ,gBAAgB,WAAW,eAAe,WAAW,OAAO,OAAO;AAAA,MAC3E,WAAW,gBAAgB,aAAa;AAAA;AAAA,MAGxC,SAAS,CAAC,GAAG,gBAAgB;AAAA,MAC7B,aAAa,CAAC,wBAAwB;AAAA,MACtC,gBAAgB;AAAA;AAAA,MAGhB,OAAO,SAAS,SAAS,IAAI,WAAW;AAAA,IAC1C;AAEA,SAAK,YAAY,MAAM,KAAK,OAAO,gBAAgB;AAAA,MACjD;AAAA,MACA,OAAO;AAAA;AAAA,MAEP,KAAK,CAAC,WAAW,MAAM,iCAAiC;AAAA,MACxD,KAAK;AAAA,MACL,aAAa;AAAA,MACb,cAAc;AAAA,MACd,cAAc;AAAA,MACd,KAAK;AAAA,MACL,iBAAiB,gBAAgB,mBAAmB;AAAA,MACpD,YAAY;AAAA,MACZ,QAAQ;AAAA,QACN,oBAAoB;AAAA,QACpB,sBAAsB;AAAA,MACxB;AAAA,MACA,YAAY;AAAA,IACd,CAAC;AAED,UAAM,KAAK,UAAU,MAAM;AAC3B,SAAK,cAAc,KAAK,UAAU;AAGlC,QAAI,WAAW,UAAU,GAAG;AAC1B,WAAK,YAAY,WAAW,MAAM;AAChC,aAAK,KAAK,QAAQ;AAAA,MACpB,GAAG,UAAU,GAAI;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAsB;AAC1B,SAAK,gBAAgB;AACrB,QAAI,CAAC,KAAK,UAAW;AAErB,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,UAAU,QAAQ;AAC1C,UAAI,KAAK,MAAM,SAAS;AACtB,cAAM,KAAK,UAAU,KAAK,EAAE,GAAG,GAAG,CAAC;AAAA,MACrC;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAyB;AAC7B,SAAK,gBAAgB;AACrB,UAAM,YAAY,KAAK;AACvB,SAAK,YAAY;AACjB,SAAK,cAAc;AAEnB,QAAI,CAAC,UAAW;AAEhB,QAAI;AACF,YAAM,UAAU,OAAO,EAAE,OAAO,KAAK,CAAC;AAAA,IACxC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,KAAK,SAAiB,UAAuB,CAAC,GAAwB;AAC1E,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,IAAI,MAAM,8DAA8D;AAAA,IAChF;AAGA,oBAAgB,OAAO;AAEvB,UAAM,YAAY,QAAQ,WAAW;AACrC,UAAM,cAAc,OAAO,QAAQ,QAAQ,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE;AAEjF,UAAM,eAAe,MAAM,KAAK,UAAU,KAAK;AAAA,MAC7C,KAAK,CAAC,WAAW,MAAM,OAAO;AAAA,MAC9B,cAAc;AAAA,MACd,cAAc;AAAA,MACd,KAAK;AAAA,MACL,YAAY,QAAQ;AAAA,MACpB,KAAK,YAAY,SAAS,IAAI,cAAc;AAAA,IAC9C,CAAC;AAED,WAAO,IAAI,QAAoB,CAAC,SAAS,WAAW;AAClD,YAAM,QAAQ,WAAW,MAAM;AAC7B,eAAO,IAAI,MAAM,uCAAuC,SAAS,IAAI,CAAC;AAAA,MACxE,GAAG,SAAS;AAEZ,mBAAa,MAAM,EAAE,QAAQ,MAAM,OAAO,MAAM,GAAG,CAAC,KAAK,WAAW;AAClE,YAAI,KAAK;AACP,uBAAa,KAAK;AAClB,iBAAO,GAAG;AACV;AAAA,QACF;AAEA,YAAI,CAAC,QAAQ;AACX,uBAAa,KAAK;AAClB,iBAAO,IAAI,MAAM,6CAA6C,CAAC;AAC/D;AAAA,QACF;AAEA,cAAM,SAAmB,CAAC;AAE1B,eAAO,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AAEvD,eAAO,GAAG,OAAO,YAAY;AAC3B,uBAAa,KAAK;AAClB,cAAI;AACF,kBAAM,MAAM,OAAO,OAAO,MAAM;AAChC,kBAAM,EAAE,QAAQ,OAAO,IAAI,kBAAkB,GAAG;AAGhD,kBAAM,gBAAgB,MAAM,aAAa,QAAQ;AACjD,kBAAM,WAAW,cAAc,YAAY;AAE3C,oBAAQ,EAAE,QAAQ,QAAQ,SAAS,CAAC;AAAA,UACtC,SAAS,YAAY;AACnB,mBAAO,UAAU;AAAA,UACnB;AAAA,QACF,CAAC;AAED,eAAO,GAAG,SAAS,CAAC,cAAqB;AACvC,uBAAa,KAAK;AAClB,iBAAO,SAAS;AAAA,QAClB,CAAC;AAAA,MACH,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,SAAS,MAA+B;AAC5C,UAAM,SAAS,MAAM,KAAK,KAAK,QAAQ,KAAK,QAAQ,MAAM,KAAK,CAAC,GAAG;AACnE,QAAI,OAAO,aAAa,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,2CAA2C,IAAI,WAAW,OAAO,QAAQ,MAAM,OAAO,MAAM;AAAA,MAC9F;AAAA,IACF;AACA,WAAO,OAAO;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,UAAU,MAAc,SAAgC;AAC5D,UAAM,MAAM,OAAO,KAAK,SAAS,MAAM,EAAE,SAAS,QAAQ;AAC1D,UAAM,MAAM,gBAAgB,GAAG,oBAAoB,KAAK,QAAQ,MAAM,KAAK,CAAC;AAC5E,UAAM,SAAS,MAAM,KAAK,KAAK,GAAG;AAClC,QAAI,OAAO,aAAa,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,6CAA6C,IAAI,WAAW,OAAO,QAAQ,MAAM,OAAO,MAAM;AAAA,MAChG;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,YAA8B;AAClC,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,UAAU,QAAQ;AAC1C,aAAO,KAAK,MAAM,YAAY;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAgC;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAwB;AAC9B,QAAI,KAAK,cAAc,MAAM;AAC3B,mBAAa,KAAK,SAAS;AAC3B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AACF;;;AElWA,SAAS,cAAAA,mBAAkB;AAE3B,IAAM,mBAAmB;AACzB,IAAM,+BAA+B;AACrC,IAAM,oBAAoB;AAiBnB,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EACA;AAAA,EACA,UAAkC,oBAAI,IAAI;AAAA,EACnD,aAAoD;AAAA,EACpD,WAAW;AAAA,EAEnB,YAAY,QAAoB,QAAoB;AAClD,SAAK,SAAS;AAAA,MACZ,SAAS;AAAA,MACT,oBAAoB;AAAA,MACpB,GAAG;AAAA,IACL;AACA,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,SAAwB;AAC5B,UAAM,SAAS,KAAK,OAAO,UAAU,KAAK,QAAQ;AAClD,QAAI,UAAU,EAAG;AAEjB,UAAM,QAAQ;AAAA,MACZ,MAAM,KAAK,EAAE,QAAQ,OAAO,CAAC,EAAE,IAAI,YAAY;AAC7C,cAAM,KAAK,UAAU;AAAA,MACvB,CAAC;AAAA,IACH;AAEA,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,UAAoC;AACxC,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI,MAAM,sEAAiE;AAAA,IACnF;AAGA,UAAM,OAAO,KAAK,SAAS;AAC3B,QAAI,MAAM;AACR,WAAK,QAAQ;AACb,WAAK,aAAa,KAAK,IAAI;AAC3B,aAAO,KAAK;AAAA,IACd;AAGA,QAAI,KAAK,QAAQ,OAAO,KAAK,OAAO,SAAS;AAC3C,YAAMC,SAAQ,MAAM,KAAK,UAAU;AACnC,MAAAA,OAAM,QAAQ;AACd,MAAAA,OAAM,aAAa,KAAK,IAAI;AAC5B,aAAOA,OAAM;AAAA,IACf;AAGA,UAAM,SAAS,KAAK,WAAW;AAC/B,QAAI,QAAQ;AACV,YAAM,UAAU,KAAK,QAAQ,IAAI,MAAM;AACvC,WAAK,QAAQ,OAAO,MAAM;AAC1B,WAAK,QAAQ,QAAQ,QAAQ,EAAE,MAAM,MAAM;AAAA,MAAkB,CAAC;AAAA,IAChE;AAEA,UAAM,QAAQ,MAAM,KAAK,UAAU;AACnC,UAAM,QAAQ;AACd,UAAM,aAAa,KAAK,IAAI;AAC5B,WAAO,MAAM;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAQ,SAAyC;AACrD,eAAW,SAAS,KAAK,QAAQ,OAAO,GAAG;AACzC,UAAI,MAAM,YAAY,SAAS;AAC7B,cAAM,QAAQ;AACd,cAAM,aAAa,KAAK,IAAI;AAC5B;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,QAAQ;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAuB;AAC3B,QAAI,KAAK,YAAY,KAAK,QAAQ,SAAS,EAAG;AAC9C,SAAK,WAAW;AAChB,SAAK,WAAW;AAEhB,UAAM,QAAQ;AAAA,MACZ,MAAM,KAAK,KAAK,QAAQ,OAAO,CAAC,EAAE;AAAA,QAAI,CAAC,UACrC,MAAM,QAAQ,QAAQ,EAAE,MAAM,MAAM;AAAA,QAAkB,CAAC;AAAA,MACzD;AAAA,IACF;AACA,SAAK,QAAQ,MAAM;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAAoB;AACtB,QAAI,QAAQ;AACZ,eAAW,SAAS,KAAK,QAAQ,OAAO,GAAG;AACzC,UAAI,CAAC,MAAM,MAAO;AAAA,IACpB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,YAAgC;AAC5C,UAAM,gBAAqC;AAAA,MACzC,OAAO,KAAK,OAAO;AAAA,MACnB,OAAO,KAAK,OAAO;AAAA,MACnB,iBAAiB;AAAA,IACnB;AAEA,UAAM,UAAU,IAAI,cAAc,eAAe,KAAK,MAAM;AAC5D,UAAM,QAAQ,MAAM;AAEpB,UAAM,KAAKD,YAAW;AACtB,UAAM,QAAmB;AAAA,MACvB;AAAA,MACA,WAAW,KAAK,IAAI;AAAA,MACpB,YAAY,KAAK,IAAI;AAAA,MACrB,OAAO;AAAA,IACT;AAEA,SAAK,QAAQ,IAAI,IAAI,KAAK;AAC1B,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,WAA6B;AACnC,QAAI,OAAyB;AAC7B,eAAW,SAAS,KAAK,QAAQ,OAAO,GAAG;AACzC,UAAI,CAAC,MAAM,OAAO;AAChB,YAAI,CAAC,QAAQ,MAAM,aAAa,KAAK,YAAY;AAC/C,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,aAA4B;AAClC,QAAI,UAAyB;AAC7B,QAAI,WAAW;AACf,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,SAAS;AACvC,UAAI,MAAM,aAAa,UAAU;AAC/B,mBAAW,MAAM;AACjB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,cAAoB;AAC1B,QAAI,KAAK,WAAY;AACrB,SAAK,aAAa,YAAY,MAAM,KAAK,KAAK,OAAO,GAAG,iBAAiB;AACzE,QAAI,KAAK,WAAW,MAAO,MAAK,WAAW,MAAM;AAAA,EACnD;AAAA,EAEQ,aAAmB;AACzB,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,SAAwB;AACpC,QAAI,KAAK,SAAU;AAEnB,UAAM,QAAQ,KAAK,IAAI;AACvB,UAAM,gBAAgB,KAAK,OAAO,qBAAqB;AAEvD,UAAM,YAAmC,CAAC;AAE1C,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,SAAS;AACvC,UAAI,CAAC,MAAM,SAAS,QAAQ,MAAM,aAAa,eAAe;AAC5D,kBAAU,KAAK,CAAC,KAAK,KAAK,CAAC;AAAA,MAC7B;AAAA,IACF;AAEA,eAAW,CAAC,KAAK,KAAK,KAAK,WAAW;AACpC,WAAK,QAAQ,OAAO,GAAG;AACvB,YAAM,MAAM,QAAQ,QAAQ,EAAE,MAAM,MAAM;AAAA,MAAkB,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;;;ACxPA,OAAOE,gBAAe;AACtB,SAAS,cAAAC,mBAAkB;AAe3B,IAAM,kBAAN,MAAiD;AAAA,EAC/C,MAAM,QAAuB;AAC3B,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAAA,EACA,MAAM,OAAsB;AAAA,EAA2B;AAAA,EACvD,MAAM,UAAyB;AAAA,EAAa;AAAA,EAC5C,MAAM,KAAK,MAAc,OAA0C;AACjE,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAAA,EACA,MAAM,SAAS,OAAgC;AAC7C,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAAA,EACA,MAAM,UAAU,OAAe,UAAiC;AAC9D,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAAA,EACA,MAAM,YAA8B;AAAE,WAAO;AAAA,EAAO;AAAA,EACpD,iBAAgC;AAAE,WAAO;AAAA,EAAM;AACjD;AAQA,eAAsB,kBAAkB,QAAqC;AAC3E,MAAI;AACF,UAAM,OAAO,KAAK;AAClB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBO,IAAM,iBAAN,MAAqB;AAAA,EACT;AAAA,EACA;AAAA,EACA,SAAS,oBAAI,IAA6B;AAAA,EACnD,qBAAqB;AAAA,EAE7B,YAAY,SAA+B,CAAC,GAAG;AAC7C,SAAK,SAAS,EAAE,UAAU,UAAU,GAAG,OAAO;AAE9C,UAAM,gBAAgB,OAAO;AAC7B,QAAI,eAAe,MAAM;AACvB,WAAK,SAAS,IAAIC,WAAU;AAAA,QAC1B,MAAM,cAAc;AAAA,QACpB,MAAM,cAAc,QAAQ;AAAA,QAC5B,UAAU,cAAc,YAAY;AAAA,MACtC,CAAC;AAAA,IACH,OAAO;AACL,WAAK,SAAS,IAAIA,WAAU;AAAA,QAC1B,YAAY,eAAe,cAAc;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,aAA4B;AAChC,QAAI,KAAK,OAAO,aAAa,UAAU;AACrC,YAAM,YAAY,MAAM,kBAAkB,KAAK,MAAM;AACrD,UAAI,CAAC,WAAW;AACd,gBAAQ;AAAA,UACN;AAAA,QAGF;AAAA,MACF;AAAA,IACF;AAEA,SAAK,0BAA0B;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,OACJ,WAE0B;AAC1B,QAAI,KAAK,OAAO,aAAa,OAAO;AAClC,YAAM,OAAO,IAAI,gBAAgB;AACjC,YAAMC,MAAK,KAAK,YAAY,UAAU,KAAK;AAC3C,WAAK,OAAO,IAAIA,KAAI,IAAI;AACxB,aAAO;AAAA,IACT;AAEA,UAAM,eAAoC;AAAA,MACxC,OAAO;AAAA,MACP,GAAG,KAAK,OAAO;AAAA,MACf,GAAG;AAAA,IACL;AAEA,UAAM,UAAU,IAAI,cAAc,cAAc,KAAK,MAAM;AAC3D,UAAM,QAAQ,MAAM;AAEpB,UAAM,KAAK,KAAK,YAAY,UAAU,KAAK;AAC3C,SAAK,OAAO,IAAI,IAAI,OAAO;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ,SAAyC;AACrD,UAAM,QAAQ,QAAQ;AACtB,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,QAAQ;AACtC,UAAI,UAAU,SAAS;AACrB,aAAK,OAAO,OAAO,GAAG;AACtB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAA0B;AAC9B,UAAM,aAAa,MAAM,KAAK,KAAK,OAAO,OAAO,CAAC,EAAE;AAAA,MAAI,CAAC,OACvD,GAAG,QAAQ,EAAE,MAAM,CAAC,QAAiB;AACnC,gBAAQ,MAAM,8DAA8D,GAAG;AAAA,MACjF,CAAC;AAAA,IACH;AACA,UAAM,QAAQ,IAAI,UAAU;AAC5B,SAAK,OAAO,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,cAAsB;AACxB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAMQ,YAAY,OAAuB;AACzC,WAAO,cAAc,KAAK,IAAIC,YAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,EACxD;AAAA,EAEQ,4BAAkC;AACxC,QAAI,KAAK,mBAAoB;AAC7B,SAAK,qBAAqB;AAE1B,UAAM,UAAU,MAAM;AACpB,WAAK,KAAK,SAAS,EAAE,QAAQ,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,IACpD;AAEA,YAAQ,KAAK,WAAW,OAAO;AAC/B,YAAQ,KAAK,UAAU,OAAO;AAC9B,YAAQ,KAAK,cAAc,MAAM,KAAK,KAAK,SAAS,CAAC;AAAA,EACvD;AACF;","names":["randomUUID","entry","Dockerode","randomUUID","Dockerode","id","randomUUID"]}
|
|
1
|
+
{"version":3,"sources":["../src/docker-sandbox.ts","../src/security.ts","../src/container-pool.ts","../src/sandbox-manager.ts","../src/native-sandbox.ts"],"sourcesContent":["/**\n * @module docker-sandbox\n *\n * DockerSandbox — a container-backed {@link SandboxProvider} for AgentForge.\n *\n * Each instance manages exactly one Docker container. The container is\n * created lazily on the first call to {@link DockerSandbox.start}, executed\n * via the Docker exec API, and destroyed via {@link DockerSandbox.destroy}.\n *\n * @example\n * ```ts\n * const sandbox = new DockerSandbox({\n * scope: 'agent',\n * workspaceAccess: 'ro',\n * workspacePath: '/home/user/project',\n * resourceLimits: { memoryMb: 512, cpuShares: 512 },\n * });\n * await sandbox.start();\n * const result = await sandbox.exec('echo hello');\n * console.log(result.stdout); // \"hello\\n\"\n * await sandbox.destroy();\n * ```\n */\n\nimport Dockerode from 'dockerode';\nimport { randomUUID } from 'node:crypto';\nimport type { DockerSandboxConfig, ExecOptions, ExecResult, SandboxProvider } from './types.js';\nimport { DEFAULT_CAP_DROP, SecurityError, validateBinds, validateCommand, validateImageName } from './security.js';\n\n/** Default Docker image used when none is specified. */\nconst DEFAULT_IMAGE = 'node:22-slim';\n\n/** Default per-exec timeout in milliseconds. */\nconst DEFAULT_EXEC_TIMEOUT_MS = 30_000;\n\n/** Default container workspace mount point. */\nconst DEFAULT_CONTAINER_WORKSPACE = '/workspace';\n\n// ---------------------------------------------------------------------------\n// Stream demuxing\n// ---------------------------------------------------------------------------\n\n/**\n * Decodes the multiplexed stream that Docker returns for exec output.\n *\n * Docker prefixes every chunk with an 8-byte header:\n * [stream_type(1)] [0(3)] [size(4 BE)]\n * where stream_type is 1 = stdout, 2 = stderr.\n */\nfunction demuxDockerStream(buffer: Buffer): { stdout: string; stderr: string } {\n let stdout = '';\n let stderr = '';\n let offset = 0;\n\n while (offset + 8 <= buffer.length) {\n const streamType = buffer[offset];\n const frameSize = buffer.readUInt32BE(offset + 4);\n offset += 8;\n\n if (offset + frameSize > buffer.length) break;\n\n const chunk = buffer.slice(offset, offset + frameSize).toString('utf8');\n offset += frameSize;\n\n if (streamType === 1) {\n stdout += chunk;\n } else if (streamType === 2) {\n stderr += chunk;\n }\n }\n\n return { stdout, stderr };\n}\n\n// ---------------------------------------------------------------------------\n// DockerSandbox\n// ---------------------------------------------------------------------------\n\n/**\n * Container-based sandbox using the Docker engine.\n *\n * Implements the {@link SandboxProvider} interface for full lifecycle\n * management, command execution, and file I/O within an isolated container.\n */\nexport class DockerSandbox implements SandboxProvider {\n private readonly config: Required<\n Pick<DockerSandboxConfig, 'scope' | 'workspaceAccess' | 'image' | 'containerWorkspacePath'>\n > &\n DockerSandboxConfig;\n\n private readonly docker: Dockerode;\n private container: Dockerode.Container | null = null;\n private containerId: string | null = null;\n private killTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * @param config - Sandbox configuration.\n * @param docker - Optional pre-configured Dockerode instance (useful in tests).\n */\n constructor(config: DockerSandboxConfig, docker?: Dockerode) {\n // Validate security constraints eagerly\n const image = config.image ?? DEFAULT_IMAGE;\n validateImageName(image);\n\n if (config.binds && config.binds.length > 0) {\n validateBinds(config.binds);\n }\n\n this.config = {\n ...config,\n image,\n containerWorkspacePath: config.containerWorkspacePath ?? DEFAULT_CONTAINER_WORKSPACE,\n };\n\n this.docker =\n docker ??\n new Dockerode(\n process.env['DOCKER_HOST']\n ? { host: process.env['DOCKER_HOST'] }\n : { socketPath: '/var/run/docker.sock' },\n );\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Create and start the Docker container.\n * Idempotent — calling start() on an already-running sandbox is a no-op.\n */\n async start(): Promise<void> {\n if (this.container) return;\n\n const { image, scope, resourceLimits, binds, env, timeout, workspaceAccess, workspacePath, containerWorkspacePath } = this.config;\n\n const name = `agentforge-${scope}-${randomUUID().slice(0, 8)}`;\n\n const envArray = Object.entries(env ?? {}).map(([k, v]) => `${k}=${v}`);\n\n // Build bind mounts\n const allBinds: string[] = [...(binds ?? [])];\n\n // Mount workspace if configured\n if (workspaceAccess !== 'none' && workspacePath) {\n const mode = workspaceAccess === 'ro' ? 'ro' : 'rw';\n allBinds.push(`${workspacePath}:${containerWorkspacePath}:${mode}`);\n }\n\n const hostConfig: Dockerode.HostConfig = {\n // Resource limits\n CpuShares: resourceLimits?.cpuShares,\n Memory: resourceLimits?.memoryMb ? resourceLimits.memoryMb * 1024 * 1024 : undefined,\n PidsLimit: resourceLimits?.pidsLimit ?? 256,\n\n // Security hardening\n CapDrop: [...DEFAULT_CAP_DROP],\n SecurityOpt: ['no-new-privileges:true'],\n ReadonlyRootfs: false,\n\n // Bind mounts\n Binds: allBinds.length > 0 ? allBinds : undefined,\n };\n\n this.container = await this.docker.createContainer({\n name,\n Image: image,\n // Keep container alive — we run commands via exec\n Cmd: ['/bin/sh', '-c', 'while true; do sleep 3600; done'],\n Env: envArray,\n AttachStdin: false,\n AttachStdout: false,\n AttachStderr: false,\n Tty: false,\n NetworkDisabled: resourceLimits?.networkDisabled ?? false,\n WorkingDir: containerWorkspacePath,\n Labels: {\n 'agentforge.scope': scope,\n 'agentforge.managed': 'true',\n },\n HostConfig: hostConfig,\n });\n\n await this.container.start();\n this.containerId = this.container.id;\n\n // Auto-kill after configured timeout\n if (timeout && timeout > 0) {\n this.killTimer = setTimeout(() => {\n void this.destroy();\n }, timeout * 1000);\n }\n }\n\n /**\n * Stop the container gracefully (10 s grace period then SIGKILL).\n * The container is kept for potential restart.\n */\n async stop(): Promise<void> {\n this._clearKillTimer();\n if (!this.container) return;\n\n try {\n const info = await this.container.inspect();\n if (info.State.Running) {\n await this.container.stop({ t: 10 });\n }\n } catch {\n // Container may already be stopped — ignore\n }\n }\n\n /**\n * Destroy the container and release all resources.\n * Safe to call multiple times.\n */\n async destroy(): Promise<void> {\n this._clearKillTimer();\n const container = this.container;\n this.container = null;\n this.containerId = null;\n\n if (!container) return;\n\n try {\n await container.remove({ force: true });\n } catch {\n // Already gone — ignore\n }\n }\n\n // ---------------------------------------------------------------------------\n // Execution\n // ---------------------------------------------------------------------------\n\n /**\n * Execute a shell command inside the running container.\n *\n * @param command - Shell command string passed to `/bin/sh -c`.\n * @param options - Per-call options (timeout, cwd, env overrides).\n */\n async exec(command: string, options: ExecOptions = {}): Promise<ExecResult> {\n if (!this.container) {\n throw new Error('DockerSandbox: container is not running. Call start() first.');\n }\n\n // Defense-in-depth command validation\n validateCommand(command);\n\n const timeoutMs = options.timeout ?? DEFAULT_EXEC_TIMEOUT_MS;\n const envOverride = Object.entries(options.env ?? {}).map(([k, v]) => `${k}=${v}`);\n\n const execInstance = await this.container.exec({\n Cmd: ['/bin/sh', '-c', command],\n AttachStdout: true,\n AttachStderr: true,\n Tty: false,\n WorkingDir: options.cwd,\n Env: envOverride.length > 0 ? envOverride : undefined,\n });\n\n return new Promise<ExecResult>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new Error(`DockerSandbox: exec timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n execInstance.start({ hijack: true, stdin: false }, (err, stream) => {\n if (err) {\n clearTimeout(timer);\n reject(err);\n return;\n }\n\n if (!stream) {\n clearTimeout(timer);\n reject(new Error('DockerSandbox: no stream returned from exec'));\n return;\n }\n\n const chunks: Buffer[] = [];\n\n stream.on('data', (chunk: Buffer) => chunks.push(chunk));\n\n stream.on('end', async () => {\n clearTimeout(timer);\n try {\n const raw = Buffer.concat(chunks);\n const { stdout, stderr } = demuxDockerStream(raw);\n\n // Inspect exec to get exit code\n const inspectResult = await execInstance.inspect();\n const exitCode = inspectResult.ExitCode ?? 0;\n\n resolve({ stdout, stderr, exitCode });\n } catch (inspectErr) {\n reject(inspectErr);\n }\n });\n\n stream.on('error', (streamErr: Error) => {\n clearTimeout(timer);\n reject(streamErr);\n });\n });\n });\n }\n\n /**\n * Read a file from the container filesystem by running `cat`.\n *\n * @param path - Absolute path inside the container.\n */\n async readFile(path: string): Promise<string> {\n const result = await this.exec(`cat \"${path.replace(/\"/g, '\\\\\"')}\"`);\n if (result.exitCode !== 0) {\n throw new Error(\n `DockerSandbox.readFile: failed to read \"${path}\" (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n return result.stdout;\n }\n\n /**\n * Write content to a file inside the container using base64 encoding\n * to avoid shell quoting issues.\n *\n * @param path - Absolute path inside the container.\n * @param content - UTF-8 string content.\n */\n async writeFile(path: string, content: string): Promise<void> {\n const b64 = Buffer.from(content, 'utf8').toString('base64');\n const cmd = `printf '%s' \"${b64}\" | base64 -d > \"${path.replace(/\"/g, '\\\\\"')}\"`;\n const result = await this.exec(cmd);\n if (result.exitCode !== 0) {\n throw new Error(\n `DockerSandbox.writeFile: failed to write \"${path}\" (exit ${result.exitCode}): ${result.stderr}`,\n );\n }\n }\n\n // ---------------------------------------------------------------------------\n // Health\n // ---------------------------------------------------------------------------\n\n /**\n * Returns true if the underlying Docker container is running.\n */\n async isRunning(): Promise<boolean> {\n if (!this.container) return false;\n try {\n const info = await this.container.inspect();\n return info.State.Running === true;\n } catch {\n return false;\n }\n }\n\n /**\n * Returns the Docker container ID or null if not yet started.\n */\n getContainerId(): string | null {\n return this.containerId;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _clearKillTimer(): void {\n if (this.killTimer !== null) {\n clearTimeout(this.killTimer);\n this.killTimer = null;\n }\n }\n}\n","/**\n * @module security\n *\n * Security helpers for the Docker sandbox implementation.\n *\n * Centralised in one module so policy changes propagate everywhere.\n * All validation functions throw {@link SecurityError} on violations.\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Host-side paths that must never be bind-mounted into a sandbox container.\n * Mounting these paths would allow container-escape or privilege escalation.\n */\nexport const BLOCKED_BIND_PREFIXES: readonly string[] = [\n '/var/run/docker.sock',\n '/etc',\n '/proc',\n '/sys',\n '/dev',\n '/boot',\n '/root',\n];\n\n/**\n * Linux capabilities dropped by default for every sandbox container.\n * We start with no capabilities at all (drop \"ALL\") then add nothing back.\n */\nexport const DEFAULT_CAP_DROP: readonly string[] = ['ALL'];\n\n/**\n * Allowed image name patterns (non-arbitrary image names in production).\n * Only images that match one of these prefixes are permitted.\n * In test / dev the list can be extended via AGENTFORGE_ALLOWED_IMAGES env var.\n */\nconst BASE_ALLOWED_IMAGE_PREFIXES: readonly string[] = [\n 'node:',\n 'python:',\n 'ubuntu:',\n 'debian:',\n 'alpine:',\n 'agentforge/',\n];\n\n// ---------------------------------------------------------------------------\n// Validation functions\n// ---------------------------------------------------------------------------\n\n/**\n * Throws if the provided bind-mount spec contains a blocked host path.\n *\n * @param bind - A bind-mount spec in `host:container[:mode]` format.\n * @throws {SecurityError} when the host path is on the block-list.\n */\nexport function validateBind(bind: string): void {\n const hostPath = bind.split(':')[0];\n if (!hostPath) {\n throw new SecurityError(`Invalid bind mount spec: \"${bind}\"`);\n }\n\n for (const blocked of BLOCKED_BIND_PREFIXES) {\n if (hostPath === blocked || hostPath.startsWith(blocked + '/') || hostPath.startsWith(blocked)) {\n throw new SecurityError(\n `Bind mount \"${bind}\" is blocked. Host path \"${hostPath}\" matches blocked prefix \"${blocked}\".`,\n );\n }\n }\n}\n\n/**\n * Validate all bind mounts in the provided array.\n * @throws {SecurityError} on the first violation found.\n */\nexport function validateBinds(binds: string[]): void {\n for (const bind of binds) {\n validateBind(bind);\n }\n}\n\n/**\n * Validate that an image name is on the allow-list.\n *\n * In production (NODE_ENV === 'production') only known safe images are allowed.\n * In development/test any image name that passes format validation is accepted.\n *\n * Additional allowed prefixes can be injected via the\n * `AGENTFORGE_ALLOWED_IMAGES` env var (comma-separated prefixes).\n *\n * @param image - Docker image name, e.g. `node:22-slim`.\n * @throws {SecurityError} if the image is not permitted.\n */\nexport function validateImageName(image: string): void {\n if (!image || typeof image !== 'string') {\n throw new SecurityError('Image name must be a non-empty string.');\n }\n\n // Reject obviously dangerous patterns (shell metacharacters)\n if (/[;&|`$(){}[\\]<>]/.test(image)) {\n throw new SecurityError(`Image name \"${image}\" contains forbidden characters.`);\n }\n\n if (process.env['NODE_ENV'] !== 'production') {\n // In dev/test, just ensure the name is a plausible Docker image reference\n return;\n }\n\n // Build the full allow-list (base + env-configured extras)\n const extraPrefixes = (process.env['AGENTFORGE_ALLOWED_IMAGES'] ?? '')\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n const allowedPrefixes = [...BASE_ALLOWED_IMAGE_PREFIXES, ...extraPrefixes];\n\n const allowed = allowedPrefixes.some((prefix) => image.startsWith(prefix));\n if (!allowed) {\n throw new SecurityError(\n `Image \"${image}\" is not on the allow-list. ` +\n `Allowed prefixes: ${allowedPrefixes.join(', ')}. ` +\n `Add custom prefixes via AGENTFORGE_ALLOWED_IMAGES env var.`,\n );\n }\n}\n\n/**\n * Validate that a command does not contain obvious escape attempts.\n * This is a defense-in-depth measure — the container itself is the primary boundary.\n *\n * @param command - The shell command to validate.\n * @throws {SecurityError} if the command contains dangerous patterns.\n */\nexport function validateCommand(command: string): void {\n if (!command || typeof command !== 'string') {\n throw new SecurityError('Command must be a non-empty string.');\n }\n\n // Block attempts to access the Docker socket from within the container\n const dangerousPatterns = [\n /docker\\.sock/i,\n /nsenter\\s/i,\n /mount\\s+-t\\s+proc/i,\n ];\n\n for (const pattern of dangerousPatterns) {\n if (pattern.test(command)) {\n throw new SecurityError(\n `Command contains a potentially dangerous pattern: ${pattern.source}`,\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error class\n// ---------------------------------------------------------------------------\n\n/**\n * A structured error type for sandbox security violations.\n */\nexport class SecurityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SecurityError';\n }\n}\n","/**\n * @module container-pool\n *\n * ContainerPool — maintains a pool of warm {@link DockerSandbox} instances\n * to amortise container cold-start latency.\n *\n * Design:\n * - A fixed-size pool of pre-started containers (warm slots).\n * - {@link ContainerPool.acquire} returns the least-recently-used idle\n * container or starts a new one if the pool has not yet reached capacity.\n * - {@link ContainerPool.release} marks a container as idle so it can be reused.\n * - An idle-eviction sweep runs periodically and destroys containers that have\n * been idle longer than `idleTimeoutSeconds`.\n * - LRU eviction is applied when the pool is at capacity and a new container is\n * requested but none are idle.\n */\n\nimport type Dockerode from 'dockerode';\nimport { DockerSandbox } from './docker-sandbox.js';\nimport type { DockerSandboxConfig, PoolConfig, PoolEntry, SandboxProvider } from './types.js';\nimport { randomUUID } from 'node:crypto';\n\nconst DEFAULT_MAX_SIZE = 3;\nconst DEFAULT_IDLE_TIMEOUT_SECONDS = 300;\nconst SWEEP_INTERVAL_MS = 30_000;\n\n/**\n * A pool of warm Docker containers that can be acquired and released for reuse.\n *\n * @example\n * ```ts\n * const pool = new ContainerPool({ image: 'node:22-slim', scope: 'agent' });\n * await pool.warmUp();\n *\n * const sb = await pool.acquire();\n * await sb.exec('node -e \"console.log(1+1)\"');\n * await pool.release(sb);\n *\n * await pool.drain();\n * ```\n */\nexport class ContainerPool {\n private readonly config: Required<PoolConfig>;\n private readonly docker?: Dockerode;\n private readonly entries: Map<string, PoolEntry> = new Map();\n private sweepTimer: ReturnType<typeof setInterval> | null = null;\n private draining = false;\n\n constructor(config: PoolConfig, docker?: Dockerode) {\n this.config = {\n maxSize: DEFAULT_MAX_SIZE,\n idleTimeoutSeconds: DEFAULT_IDLE_TIMEOUT_SECONDS,\n ...config,\n };\n this.docker = docker;\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /**\n * Pre-warm the pool up to `maxSize` containers.\n * Resolves once all warm containers are started.\n */\n async warmUp(): Promise<void> {\n const needed = this.config.maxSize - this.entries.size;\n if (needed <= 0) return;\n\n await Promise.all(\n Array.from({ length: needed }).map(async () => {\n await this._addEntry();\n }),\n );\n\n this._startSweep();\n }\n\n /**\n * Acquire an idle sandbox from the pool.\n *\n * If an idle entry is available, it is marked in-use and returned immediately.\n * If the pool is not yet at capacity, a new container is started and returned.\n * If the pool is at capacity and all containers are in use, the LRU idle\n * container is evicted and a fresh one is started.\n *\n * @throws If the pool is draining.\n */\n async acquire(): Promise<SandboxProvider> {\n if (this.draining) {\n throw new Error('ContainerPool: pool is draining — cannot acquire new sandboxes.');\n }\n\n // 1. Find an idle entry (LRU = smallest lastUsedAt)\n const idle = this._lruIdle();\n if (idle) {\n idle.inUse = true;\n idle.lastUsedAt = Date.now();\n return idle.sandbox;\n }\n\n // 2. Pool has spare capacity — start a fresh container\n if (this.entries.size < this.config.maxSize) {\n const entry = await this._addEntry();\n entry.inUse = true;\n entry.lastUsedAt = Date.now();\n return entry.sandbox;\n }\n\n // 3. Pool at capacity, all in use — evict LRU entry and replace\n const lruKey = this._lruAnyKey();\n if (lruKey) {\n const evicted = this.entries.get(lruKey)!;\n this.entries.delete(lruKey);\n void evicted.sandbox.destroy().catch(() => {/* best-effort */});\n }\n\n const entry = await this._addEntry();\n entry.inUse = true;\n entry.lastUsedAt = Date.now();\n return entry.sandbox;\n }\n\n /**\n * Release a previously acquired sandbox back to the pool.\n * The sandbox is reset to an idle state for future reuse.\n */\n async release(sandbox: SandboxProvider): Promise<void> {\n for (const entry of this.entries.values()) {\n if (entry.sandbox === sandbox) {\n entry.inUse = false;\n entry.lastUsedAt = Date.now();\n return;\n }\n }\n // Sandbox not found in pool — destroy it to avoid leaks\n await sandbox.destroy();\n }\n\n /**\n * Drain the pool: stop acquiring and destroy all containers.\n * After draining, the pool stays in a drained state and rejects new acquires.\n */\n async drain(): Promise<void> {\n if (this.draining && this.entries.size === 0) return;\n this.draining = true;\n this._stopSweep();\n\n await Promise.all(\n Array.from(this.entries.values()).map((entry) =>\n entry.sandbox.destroy().catch(() => {/* best-effort */}),\n ),\n );\n this.entries.clear();\n }\n\n /**\n * Returns the current number of entries (in-use + idle).\n */\n get size(): number {\n return this.entries.size;\n }\n\n /**\n * Returns the number of idle (available) entries.\n */\n get idleCount(): number {\n let count = 0;\n for (const entry of this.entries.values()) {\n if (!entry.inUse) count++;\n }\n return count;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private async _addEntry(): Promise<PoolEntry> {\n const sandboxConfig: DockerSandboxConfig = {\n image: this.config.image,\n scope: this.config.scope,\n workspaceAccess: 'none',\n };\n\n const sandbox = new DockerSandbox(sandboxConfig, this.docker);\n await sandbox.start();\n\n const id = randomUUID();\n const entry: PoolEntry = {\n sandbox,\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n inUse: false,\n };\n\n this.entries.set(id, entry);\n return entry;\n }\n\n /** Return the idle entry with the smallest lastUsedAt (LRU idle). */\n private _lruIdle(): PoolEntry | null {\n let best: PoolEntry | null = null;\n for (const entry of this.entries.values()) {\n if (!entry.inUse) {\n if (!best || entry.lastUsedAt < best.lastUsedAt) {\n best = entry;\n }\n }\n }\n return best;\n }\n\n /** Return the key of the entry with the smallest lastUsedAt (LRU any). */\n private _lruAnyKey(): string | null {\n let bestKey: string | null = null;\n let bestTime = Infinity;\n for (const [key, entry] of this.entries) {\n if (entry.lastUsedAt < bestTime) {\n bestTime = entry.lastUsedAt;\n bestKey = key;\n }\n }\n return bestKey;\n }\n\n /** Start periodic idle-eviction sweep. */\n private _startSweep(): void {\n if (this.sweepTimer) return;\n this.sweepTimer = setInterval(() => void this._sweep(), SWEEP_INTERVAL_MS);\n if (this.sweepTimer.unref) this.sweepTimer.unref();\n }\n\n private _stopSweep(): void {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = null;\n }\n }\n\n /** Remove and destroy containers that have been idle past the timeout. */\n private async _sweep(): Promise<void> {\n if (this.draining) return;\n\n const nowMs = Date.now();\n const idleTimeoutMs = this.config.idleTimeoutSeconds * 1000;\n\n const evictions: [string, PoolEntry][] = [];\n\n for (const [key, entry] of this.entries) {\n if (!entry.inUse && nowMs - entry.lastUsedAt > idleTimeoutMs) {\n evictions.push([key, entry]);\n }\n }\n\n for (const [key, entry] of evictions) {\n this.entries.delete(key);\n await entry.sandbox.destroy().catch(() => {/* best-effort */});\n }\n }\n}\n","/**\n * @module sandbox-manager\n *\n * SandboxManager — factory + registry for sandbox instances.\n *\n * Responsibilities:\n * - Create the correct sandbox type (Docker | E2B) from a unified config.\n * - Apply the `agentforge-{scope}-{id}` naming convention.\n * - Track active sandboxes and clean them up on process exit.\n * - Verify Docker availability on startup; print a friendly message if absent.\n */\n\nimport Dockerode from 'dockerode';\nimport { randomUUID } from 'node:crypto';\nimport { DockerSandbox } from './docker-sandbox.js';\nimport { NativeSandbox, isNativeSandboxAvailable } from './native-sandbox.js';\nimport type {\n DockerSandboxConfig,\n ExecOptions,\n ExecResult,\n NativeSandboxConfig,\n SandboxManagerConfig,\n SandboxProvider,\n} from './types.js';\n\n/**\n * A thin E2B stub that satisfies {@link SandboxProvider} but throws at runtime.\n * The real E2B implementation lives in @agentforge-ai/core.\n * This stub lets the manager compile and be tested without the E2B dependency.\n */\nclass E2BProviderStub implements SandboxProvider {\n async start(): Promise<void> {\n throw new Error(\n 'SandboxManager: E2B provider is not bundled in @agentforge-ai/sandbox. ' +\n 'Use the SandboxManager from @agentforge-ai/core instead.',\n );\n }\n async stop(): Promise<void> { /* noop until started */ }\n async destroy(): Promise<void> { /* noop */ }\n async exec(_cmd: string, _opts?: ExecOptions): Promise<ExecResult> {\n throw new Error('E2BProviderStub: not implemented');\n }\n async readFile(_path: string): Promise<string> {\n throw new Error('E2BProviderStub: not implemented');\n }\n async writeFile(_path: string, _content: string): Promise<void> {\n throw new Error('E2BProviderStub: not implemented');\n }\n async isRunning(): Promise<boolean> { return false; }\n getContainerId(): string | null { return null; }\n}\n\n/**\n * Checks whether the Docker daemon is reachable.\n *\n * @param docker - Dockerode instance to ping.\n * @returns `true` if Docker is available, `false` otherwise.\n */\nexport async function isDockerAvailable(docker: Dockerode): Promise<boolean> {\n try {\n await docker.ping();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Central factory and lifecycle manager for sandbox instances.\n *\n * @example\n * ```ts\n * const manager = new SandboxManager({ provider: 'docker' });\n * await manager.initialize();\n *\n * const sb = await manager.create({ scope: 'agent', workspaceAccess: 'none' });\n * const result = await sb.exec('echo hello');\n * await manager.destroy(sb);\n *\n * await manager.shutdown();\n * ```\n */\nexport class SandboxManager {\n private readonly config: SandboxManagerConfig;\n private readonly docker: Dockerode;\n private readonly active = new Map<string, SandboxProvider>();\n private shutdownRegistered = false;\n\n constructor(config: SandboxManagerConfig = {}) {\n this.config = { provider: 'docker', ...config };\n\n const dockerHostCfg = config.dockerHost;\n if (dockerHostCfg?.host) {\n this.docker = new Dockerode({\n host: dockerHostCfg.host,\n port: dockerHostCfg.port ?? 2376,\n protocol: dockerHostCfg.protocol ?? 'http',\n });\n } else {\n this.docker = new Dockerode({\n socketPath: dockerHostCfg?.socketPath ?? '/var/run/docker.sock',\n });\n }\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /**\n * Initialize the manager. For the Docker provider this verifies that the\n * Docker daemon is reachable. Call this once at application startup.\n *\n * @throws Error if the Docker daemon cannot be reached (Docker provider only).\n */\n async initialize(): Promise<void> {\n if (this.config.provider === 'docker') {\n const available = await isDockerAvailable(this.docker);\n if (!available) {\n console.warn(\n '[SandboxManager] Docker daemon is not reachable. ' +\n 'Agent tool execution will fail until Docker is started. ' +\n 'Install Docker: https://docs.docker.com/get-docker/',\n );\n }\n }\n\n if (this.config.provider === 'native') {\n const { available, method } = await isNativeSandboxAvailable();\n if (!available) {\n console.warn(\n '[SandboxManager] No native isolation method found (sandbox-exec / bwrap). ' +\n 'Sandboxes will run with limited isolation (timeout + env filtering only).',\n );\n } else {\n console.info(`[SandboxManager] Native isolation available: ${method}`);\n }\n }\n\n this._registerShutdownHandlers();\n }\n\n /**\n * Create and start a new sandbox.\n *\n * The sandbox is registered internally; call {@link SandboxManager.destroy}\n * or {@link SandboxManager.shutdown} to release it.\n *\n * @param overrides - Per-sandbox config overrides merged with manager defaults.\n */\n async create(\n overrides: Pick<DockerSandboxConfig, 'scope' | 'workspaceAccess'> &\n Partial<DockerSandboxConfig>,\n ): Promise<SandboxProvider> {\n if (this.config.provider === 'e2b') {\n const stub = new E2BProviderStub();\n const id = this._generateId(overrides.scope);\n this.active.set(id, stub);\n return stub;\n }\n\n if (this.config.provider === 'native') {\n const nativeConfig: NativeSandboxConfig = {\n scope: overrides.scope,\n profile: this.config.nativeConfig?.profile,\n workingDirectory: this.config.nativeConfig?.workingDirectory,\n };\n const sandbox = new NativeSandbox(nativeConfig);\n await sandbox.start();\n const id = this._generateId(overrides.scope);\n this.active.set(id, sandbox);\n return sandbox;\n }\n\n const mergedConfig: DockerSandboxConfig = {\n image: 'node:22-slim',\n ...this.config.dockerConfig,\n ...overrides,\n };\n\n const sandbox = new DockerSandbox(mergedConfig, this.docker);\n await sandbox.start();\n\n const id = this._generateId(overrides.scope);\n this.active.set(id, sandbox);\n return sandbox;\n }\n\n /**\n * Destroy a specific sandbox and remove it from the active registry.\n */\n async destroy(sandbox: SandboxProvider): Promise<void> {\n await sandbox.destroy();\n for (const [key, value] of this.active) {\n if (value === sandbox) {\n this.active.delete(key);\n break;\n }\n }\n }\n\n /**\n * Destroy all active sandboxes and shut the manager down.\n * Called automatically on SIGTERM / SIGINT when registered.\n */\n async shutdown(): Promise<void> {\n const destroyAll = Array.from(this.active.values()).map((sb) =>\n sb.destroy().catch((err: unknown) => {\n console.error('[SandboxManager] Error destroying sandbox during shutdown:', err);\n }),\n );\n await Promise.all(destroyAll);\n this.active.clear();\n }\n\n /**\n * Returns the number of currently active sandboxes.\n */\n get activeCount(): number {\n return this.active.size;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _generateId(scope: string): string {\n return `agentforge-${scope}-${randomUUID().slice(0, 8)}`;\n }\n\n private _registerShutdownHandlers(): void {\n if (this.shutdownRegistered) return;\n this.shutdownRegistered = true;\n\n const handler = () => {\n void this.shutdown().finally(() => process.exit(0));\n };\n\n process.once('SIGTERM', handler);\n process.once('SIGINT', handler);\n process.once('beforeExit', () => void this.shutdown());\n }\n}\n","/**\n * @module native-sandbox\n *\n * NativeSandbox — a lightweight process-level sandbox using platform-native\n * isolation mechanisms as an alternative to Docker.\n *\n * Platform support:\n * - macOS: `sandbox-exec` with Apple Seatbelt profiles (.sb)\n * - Linux: `bwrap` (Bubblewrap) if available\n * - Fallback: plain child_process with timeout + env filtering (no isolation)\n *\n * Security note: spawn() is used intentionally here because this module is the\n * sandbox itself. Commands are passed through isolation wrappers (sandbox-exec /\n * bwrap) that enforce the security policy. The env is sanitized before use.\n */\n\nimport { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport os from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport type { ExecOptions, ExecResult, SandboxProvider } from './types.js';\nimport type { NativeSandboxConfig, SandboxProfile } from './types.js';\n\nconst execFileAsync = promisify(execFile);\n\n// ---------------------------------------------------------------------------\n// Default profile\n// ---------------------------------------------------------------------------\n\n/**\n * Conservative default profile for native sandboxes.\n * Network access is disabled; only /tmp is accessible.\n */\nexport const DEFAULT_SANDBOX_PROFILE: SandboxProfile = {\n allowNetwork: false,\n allowFS: ['/tmp'],\n timeout: 30,\n memory: 512,\n};\n\n// ---------------------------------------------------------------------------\n// Availability detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detects which native isolation method is available on the current platform.\n *\n * @returns An object indicating availability and the method found.\n */\nexport async function isNativeSandboxAvailable(): Promise<{\n available: boolean;\n method: 'sandbox-exec' | 'bwrap' | 'none';\n}> {\n if (process.platform === 'darwin') {\n try {\n // sandbox-exec is a built-in macOS tool; 'which' confirms it is on PATH\n await execFileAsync('which', ['sandbox-exec'], { timeout: 5000 });\n return { available: true, method: 'sandbox-exec' };\n } catch {\n // fall through\n }\n }\n\n if (process.platform === 'linux') {\n try {\n await execFileAsync('which', ['bwrap'], { timeout: 5000 });\n return { available: true, method: 'bwrap' };\n } catch {\n // fall through\n }\n }\n\n return { available: false, method: 'none' };\n}\n\n// ---------------------------------------------------------------------------\n// Seatbelt profile generation (macOS)\n// ---------------------------------------------------------------------------\n\n/**\n * Generates an Apple Seatbelt (.sb) profile string from a {@link SandboxProfile}.\n *\n * The profile uses deny-default policy and selectively allows operations\n * based on the provided configuration.\n */\nfunction generateSeatbeltProfile(profile: SandboxProfile): string {\n const lines: string[] = [\n '(version 1)',\n '(deny default)',\n // Always allow process execution so /bin/sh -c works\n '(allow process-exec)',\n '(allow process-fork)',\n // Allow reading system libraries and frameworks\n '(allow file-read* (subpath \"/usr/lib\"))',\n '(allow file-read* (subpath \"/usr/libexec\"))',\n '(allow file-read* (subpath \"/System/Library\"))',\n '(allow file-read* (subpath \"/Library/Preferences\"))',\n '(allow file-read* (literal \"/dev/null\"))',\n '(allow file-read* (literal \"/dev/urandom\"))',\n '(allow file-read* (literal \"/dev/random\"))',\n // Allow sysctl reads (needed by many programs)\n '(allow sysctl-read)',\n // Allow signal sending to self\n '(allow signal (target self))',\n // Allow mach operations needed for basic process functionality\n '(allow mach-lookup)',\n ];\n\n // Filesystem access\n for (const allowedPath of profile.allowFS) {\n lines.push(`(allow file-read* (subpath \"${allowedPath}\"))`);\n lines.push(`(allow file-write* (subpath \"${allowedPath}\"))`);\n }\n\n // Network access\n if (profile.allowNetwork) {\n lines.push('(allow network*)');\n }\n\n return lines.join('\\n');\n}\n\n// ---------------------------------------------------------------------------\n// bwrap argument generation (Linux)\n// ---------------------------------------------------------------------------\n\n/**\n * Builds the bwrap (Bubblewrap) argument list for the given profile and command.\n */\nfunction buildBwrapArgs(profile: SandboxProfile, command: string, cwd?: string): string[] {\n const args: string[] = [\n '--ro-bind', '/', '/',\n '--dev', '/dev',\n '--proc', '/proc',\n '--tmpfs', '/tmp',\n ];\n\n // Bind allowed writable paths\n for (const allowedPath of profile.allowFS) {\n // Skip /tmp since we already have --tmpfs /tmp\n if (allowedPath === '/tmp') continue;\n args.push('--bind', allowedPath, allowedPath);\n }\n\n // Network isolation\n if (!profile.allowNetwork) {\n args.push('--unshare-net');\n }\n\n // User namespace isolation\n args.push('--unshare-user', '--unshare-pid', '--unshare-ipc', '--unshare-uts');\n\n if (cwd) {\n args.push('--chdir', cwd);\n }\n\n args.push('--', '/bin/sh', '-c', command);\n\n return args;\n}\n\n// ---------------------------------------------------------------------------\n// NativeSandbox\n// ---------------------------------------------------------------------------\n\n/**\n * A sandbox provider that uses platform-native process isolation instead of Docker.\n *\n * Isolation strategy (in priority order):\n * 1. macOS: `sandbox-exec` with Apple Seatbelt profiles\n * 2. Linux: `bwrap` (Bubblewrap)\n * 3. Fallback: plain child_process with timeout + env filtering (no isolation)\n */\nexport class NativeSandbox implements SandboxProvider {\n private readonly config: NativeSandboxConfig;\n private readonly profile: SandboxProfile;\n private readonly id: string;\n private running = false;\n private isolationMethod: 'sandbox-exec' | 'bwrap' | 'none' = 'none';\n\n constructor(config: NativeSandboxConfig) {\n this.config = config;\n this.profile = config.profile ?? { ...DEFAULT_SANDBOX_PROFILE };\n this.id = `native-${randomUUID()}`;\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n async start(): Promise<void> {\n const { method } = await isNativeSandboxAvailable();\n this.isolationMethod = method;\n\n if (method === 'none') {\n console.warn(\n '[NativeSandbox] No native isolation available (sandbox-exec / bwrap not found). ' +\n 'Running with limited isolation (timeout + env filtering only). ' +\n 'For stronger isolation, install Docker or use bwrap on Linux.',\n );\n } else {\n console.info(`[NativeSandbox] Using isolation method: ${method}`);\n }\n\n // Ensure working directory exists if specified\n if (this.config.workingDirectory) {\n await fs.mkdir(this.config.workingDirectory, { recursive: true });\n }\n\n this.running = true;\n }\n\n async stop(): Promise<void> {\n this.running = false;\n }\n\n async destroy(): Promise<void> {\n this.running = false;\n }\n\n // ---------------------------------------------------------------------------\n // Execution\n // ---------------------------------------------------------------------------\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n // timeout in ExecOptions is in milliseconds; profile.timeout is in seconds\n const timeoutMs = options?.timeout ?? this.profile.timeout * 1000;\n const cwd = options?.cwd ?? this.config.workingDirectory ?? os.tmpdir();\n const extraEnv = options?.env ?? {};\n\n return new Promise((resolve, reject) => {\n let stdout = '';\n let stderr = '';\n\n const env = this._buildSafeEnv(extraEnv);\n\n // spawn() is used intentionally — this IS the sandbox layer. Commands\n // are wrapped by platform isolation (sandbox-exec / bwrap) that enforce\n // the security policy before the shell ever runs.\n let proc: ReturnType<typeof spawn>;\n\n if (this.isolationMethod === 'sandbox-exec') {\n const sbProfile = generateSeatbeltProfile(this.profile);\n proc = spawn('sandbox-exec', ['-p', sbProfile, '/bin/sh', '-c', command], {\n cwd,\n env,\n timeout: timeoutMs,\n });\n } else if (this.isolationMethod === 'bwrap') {\n const bwrapArgs = buildBwrapArgs(this.profile, command, cwd);\n proc = spawn('bwrap', bwrapArgs, {\n env,\n timeout: timeoutMs,\n });\n } else {\n // Fallback: no OS-level isolation, but env is sanitized and timeout enforced\n proc = spawn('/bin/sh', ['-c', command], {\n cwd,\n env,\n timeout: timeoutMs,\n });\n }\n\n proc.stdout?.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });\n proc.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });\n\n proc.on('error', (err) => {\n reject(new Error(`[NativeSandbox] exec error: ${err.message}`));\n });\n\n proc.on('close', (code) => {\n resolve({\n stdout,\n stderr,\n exitCode: code ?? 1,\n });\n });\n });\n }\n\n // ---------------------------------------------------------------------------\n // File I/O (with path validation)\n // ---------------------------------------------------------------------------\n\n async readFile(filePath: string): Promise<string> {\n this._validatePath(filePath);\n return fs.readFile(filePath, 'utf-8');\n }\n\n async writeFile(filePath: string, content: string): Promise<void> {\n this._validatePath(filePath);\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, content, 'utf-8');\n }\n\n // ---------------------------------------------------------------------------\n // Health\n // ---------------------------------------------------------------------------\n\n async isRunning(): Promise<boolean> {\n return this.running;\n }\n\n getContainerId(): string | null {\n return this.running ? this.id : null;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Validates that a file path is within the allowed filesystem paths defined\n * in the sandbox profile. Throws if the path is not allowed.\n */\n private _validatePath(filePath: string): void {\n const resolved = path.resolve(filePath);\n const allowed = this.profile.allowFS.some((allowedPath) => {\n const resolvedAllowed = path.resolve(allowedPath);\n return resolved.startsWith(resolvedAllowed + path.sep) || resolved === resolvedAllowed;\n });\n\n if (!allowed) {\n throw new Error(\n `[NativeSandbox] Access denied: path \"${filePath}\" is not within allowed filesystem paths: ` +\n JSON.stringify(this.profile.allowFS),\n );\n }\n }\n\n /**\n * Builds a sanitized environment for child processes.\n * Removes sensitive variables (secrets, tokens, credentials) and merges\n * any additional environment variables provided by the caller.\n */\n private _buildSafeEnv(extra: Record<string, string>): Record<string, string> {\n const SENSITIVE_PATTERNS = [\n /SECRET/i,\n /TOKEN/i,\n /PASSWORD/i,\n /PASSWD/i,\n /API_KEY/i,\n /PRIVATE_KEY/i,\n /CREDENTIAL/i,\n /AUTH/i,\n /AWS_/i,\n /GCP_/i,\n /AZURE_/i,\n ];\n\n const safeEnv: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue;\n const isSensitive = SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));\n if (!isSensitive) {\n safeEnv[key] = value;\n }\n }\n\n // Merge caller-provided env vars\n Object.assign(safeEnv, extra);\n\n return safeEnv;\n }\n}\n"],"mappings":"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","names":["path","randomUUID","entry","Dockerode","randomUUID","randomUUID","Dockerode","id","sandbox","randomUUID"]}
|
package/dist/sandbox-manager.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/sandbox-manager.ts
|
|
2
2
|
import Dockerode2 from "dockerode";
|
|
3
|
-
import { randomUUID as
|
|
3
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4
4
|
|
|
5
5
|
// src/docker-sandbox.ts
|
|
6
6
|
import Dockerode from "dockerode";
|
|
@@ -284,11 +284,11 @@ var DockerSandbox = class {
|
|
|
284
284
|
*
|
|
285
285
|
* @param path - Absolute path inside the container.
|
|
286
286
|
*/
|
|
287
|
-
async readFile(
|
|
288
|
-
const result = await this.exec(`cat "${
|
|
287
|
+
async readFile(path2) {
|
|
288
|
+
const result = await this.exec(`cat "${path2.replace(/"/g, '\\"')}"`);
|
|
289
289
|
if (result.exitCode !== 0) {
|
|
290
290
|
throw new Error(
|
|
291
|
-
`DockerSandbox.readFile: failed to read "${
|
|
291
|
+
`DockerSandbox.readFile: failed to read "${path2}" (exit ${result.exitCode}): ${result.stderr}`
|
|
292
292
|
);
|
|
293
293
|
}
|
|
294
294
|
return result.stdout;
|
|
@@ -300,13 +300,13 @@ var DockerSandbox = class {
|
|
|
300
300
|
* @param path - Absolute path inside the container.
|
|
301
301
|
* @param content - UTF-8 string content.
|
|
302
302
|
*/
|
|
303
|
-
async writeFile(
|
|
303
|
+
async writeFile(path2, content) {
|
|
304
304
|
const b64 = Buffer.from(content, "utf8").toString("base64");
|
|
305
|
-
const cmd = `printf '%s' "${b64}" | base64 -d > "${
|
|
305
|
+
const cmd = `printf '%s' "${b64}" | base64 -d > "${path2.replace(/"/g, '\\"')}"`;
|
|
306
306
|
const result = await this.exec(cmd);
|
|
307
307
|
if (result.exitCode !== 0) {
|
|
308
308
|
throw new Error(
|
|
309
|
-
`DockerSandbox.writeFile: failed to write "${
|
|
309
|
+
`DockerSandbox.writeFile: failed to write "${path2}" (exit ${result.exitCode}): ${result.stderr}`
|
|
310
310
|
);
|
|
311
311
|
}
|
|
312
312
|
}
|
|
@@ -342,6 +342,251 @@ var DockerSandbox = class {
|
|
|
342
342
|
}
|
|
343
343
|
};
|
|
344
344
|
|
|
345
|
+
// src/native-sandbox.ts
|
|
346
|
+
import { execFile, spawn } from "child_process";
|
|
347
|
+
import { promisify } from "util";
|
|
348
|
+
import fs from "fs/promises";
|
|
349
|
+
import path from "path";
|
|
350
|
+
import os from "os";
|
|
351
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
352
|
+
var execFileAsync = promisify(execFile);
|
|
353
|
+
var DEFAULT_SANDBOX_PROFILE = {
|
|
354
|
+
allowNetwork: false,
|
|
355
|
+
allowFS: ["/tmp"],
|
|
356
|
+
timeout: 30,
|
|
357
|
+
memory: 512
|
|
358
|
+
};
|
|
359
|
+
async function isNativeSandboxAvailable() {
|
|
360
|
+
if (process.platform === "darwin") {
|
|
361
|
+
try {
|
|
362
|
+
await execFileAsync("which", ["sandbox-exec"], { timeout: 5e3 });
|
|
363
|
+
return { available: true, method: "sandbox-exec" };
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (process.platform === "linux") {
|
|
368
|
+
try {
|
|
369
|
+
await execFileAsync("which", ["bwrap"], { timeout: 5e3 });
|
|
370
|
+
return { available: true, method: "bwrap" };
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return { available: false, method: "none" };
|
|
375
|
+
}
|
|
376
|
+
function generateSeatbeltProfile(profile) {
|
|
377
|
+
const lines = [
|
|
378
|
+
"(version 1)",
|
|
379
|
+
"(deny default)",
|
|
380
|
+
// Always allow process execution so /bin/sh -c works
|
|
381
|
+
"(allow process-exec)",
|
|
382
|
+
"(allow process-fork)",
|
|
383
|
+
// Allow reading system libraries and frameworks
|
|
384
|
+
'(allow file-read* (subpath "/usr/lib"))',
|
|
385
|
+
'(allow file-read* (subpath "/usr/libexec"))',
|
|
386
|
+
'(allow file-read* (subpath "/System/Library"))',
|
|
387
|
+
'(allow file-read* (subpath "/Library/Preferences"))',
|
|
388
|
+
'(allow file-read* (literal "/dev/null"))',
|
|
389
|
+
'(allow file-read* (literal "/dev/urandom"))',
|
|
390
|
+
'(allow file-read* (literal "/dev/random"))',
|
|
391
|
+
// Allow sysctl reads (needed by many programs)
|
|
392
|
+
"(allow sysctl-read)",
|
|
393
|
+
// Allow signal sending to self
|
|
394
|
+
"(allow signal (target self))",
|
|
395
|
+
// Allow mach operations needed for basic process functionality
|
|
396
|
+
"(allow mach-lookup)"
|
|
397
|
+
];
|
|
398
|
+
for (const allowedPath of profile.allowFS) {
|
|
399
|
+
lines.push(`(allow file-read* (subpath "${allowedPath}"))`);
|
|
400
|
+
lines.push(`(allow file-write* (subpath "${allowedPath}"))`);
|
|
401
|
+
}
|
|
402
|
+
if (profile.allowNetwork) {
|
|
403
|
+
lines.push("(allow network*)");
|
|
404
|
+
}
|
|
405
|
+
return lines.join("\n");
|
|
406
|
+
}
|
|
407
|
+
function buildBwrapArgs(profile, command, cwd) {
|
|
408
|
+
const args = [
|
|
409
|
+
"--ro-bind",
|
|
410
|
+
"/",
|
|
411
|
+
"/",
|
|
412
|
+
"--dev",
|
|
413
|
+
"/dev",
|
|
414
|
+
"--proc",
|
|
415
|
+
"/proc",
|
|
416
|
+
"--tmpfs",
|
|
417
|
+
"/tmp"
|
|
418
|
+
];
|
|
419
|
+
for (const allowedPath of profile.allowFS) {
|
|
420
|
+
if (allowedPath === "/tmp") continue;
|
|
421
|
+
args.push("--bind", allowedPath, allowedPath);
|
|
422
|
+
}
|
|
423
|
+
if (!profile.allowNetwork) {
|
|
424
|
+
args.push("--unshare-net");
|
|
425
|
+
}
|
|
426
|
+
args.push("--unshare-user", "--unshare-pid", "--unshare-ipc", "--unshare-uts");
|
|
427
|
+
if (cwd) {
|
|
428
|
+
args.push("--chdir", cwd);
|
|
429
|
+
}
|
|
430
|
+
args.push("--", "/bin/sh", "-c", command);
|
|
431
|
+
return args;
|
|
432
|
+
}
|
|
433
|
+
var NativeSandbox = class {
|
|
434
|
+
config;
|
|
435
|
+
profile;
|
|
436
|
+
id;
|
|
437
|
+
running = false;
|
|
438
|
+
isolationMethod = "none";
|
|
439
|
+
constructor(config) {
|
|
440
|
+
this.config = config;
|
|
441
|
+
this.profile = config.profile ?? { ...DEFAULT_SANDBOX_PROFILE };
|
|
442
|
+
this.id = `native-${randomUUID2()}`;
|
|
443
|
+
}
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// Lifecycle
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
async start() {
|
|
448
|
+
const { method } = await isNativeSandboxAvailable();
|
|
449
|
+
this.isolationMethod = method;
|
|
450
|
+
if (method === "none") {
|
|
451
|
+
console.warn(
|
|
452
|
+
"[NativeSandbox] No native isolation available (sandbox-exec / bwrap not found). Running with limited isolation (timeout + env filtering only). For stronger isolation, install Docker or use bwrap on Linux."
|
|
453
|
+
);
|
|
454
|
+
} else {
|
|
455
|
+
console.info(`[NativeSandbox] Using isolation method: ${method}`);
|
|
456
|
+
}
|
|
457
|
+
if (this.config.workingDirectory) {
|
|
458
|
+
await fs.mkdir(this.config.workingDirectory, { recursive: true });
|
|
459
|
+
}
|
|
460
|
+
this.running = true;
|
|
461
|
+
}
|
|
462
|
+
async stop() {
|
|
463
|
+
this.running = false;
|
|
464
|
+
}
|
|
465
|
+
async destroy() {
|
|
466
|
+
this.running = false;
|
|
467
|
+
}
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Execution
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
async exec(command, options) {
|
|
472
|
+
const timeoutMs = options?.timeout ?? this.profile.timeout * 1e3;
|
|
473
|
+
const cwd = options?.cwd ?? this.config.workingDirectory ?? os.tmpdir();
|
|
474
|
+
const extraEnv = options?.env ?? {};
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
let stdout = "";
|
|
477
|
+
let stderr = "";
|
|
478
|
+
const env = this._buildSafeEnv(extraEnv);
|
|
479
|
+
let proc;
|
|
480
|
+
if (this.isolationMethod === "sandbox-exec") {
|
|
481
|
+
const sbProfile = generateSeatbeltProfile(this.profile);
|
|
482
|
+
proc = spawn("sandbox-exec", ["-p", sbProfile, "/bin/sh", "-c", command], {
|
|
483
|
+
cwd,
|
|
484
|
+
env,
|
|
485
|
+
timeout: timeoutMs
|
|
486
|
+
});
|
|
487
|
+
} else if (this.isolationMethod === "bwrap") {
|
|
488
|
+
const bwrapArgs = buildBwrapArgs(this.profile, command, cwd);
|
|
489
|
+
proc = spawn("bwrap", bwrapArgs, {
|
|
490
|
+
env,
|
|
491
|
+
timeout: timeoutMs
|
|
492
|
+
});
|
|
493
|
+
} else {
|
|
494
|
+
proc = spawn("/bin/sh", ["-c", command], {
|
|
495
|
+
cwd,
|
|
496
|
+
env,
|
|
497
|
+
timeout: timeoutMs
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
proc.stdout?.on("data", (chunk) => {
|
|
501
|
+
stdout += chunk.toString();
|
|
502
|
+
});
|
|
503
|
+
proc.stderr?.on("data", (chunk) => {
|
|
504
|
+
stderr += chunk.toString();
|
|
505
|
+
});
|
|
506
|
+
proc.on("error", (err) => {
|
|
507
|
+
reject(new Error(`[NativeSandbox] exec error: ${err.message}`));
|
|
508
|
+
});
|
|
509
|
+
proc.on("close", (code) => {
|
|
510
|
+
resolve({
|
|
511
|
+
stdout,
|
|
512
|
+
stderr,
|
|
513
|
+
exitCode: code ?? 1
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// File I/O (with path validation)
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
async readFile(filePath) {
|
|
522
|
+
this._validatePath(filePath);
|
|
523
|
+
return fs.readFile(filePath, "utf-8");
|
|
524
|
+
}
|
|
525
|
+
async writeFile(filePath, content) {
|
|
526
|
+
this._validatePath(filePath);
|
|
527
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
528
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
529
|
+
}
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// Health
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
async isRunning() {
|
|
534
|
+
return this.running;
|
|
535
|
+
}
|
|
536
|
+
getContainerId() {
|
|
537
|
+
return this.running ? this.id : null;
|
|
538
|
+
}
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
// Private helpers
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
/**
|
|
543
|
+
* Validates that a file path is within the allowed filesystem paths defined
|
|
544
|
+
* in the sandbox profile. Throws if the path is not allowed.
|
|
545
|
+
*/
|
|
546
|
+
_validatePath(filePath) {
|
|
547
|
+
const resolved = path.resolve(filePath);
|
|
548
|
+
const allowed = this.profile.allowFS.some((allowedPath) => {
|
|
549
|
+
const resolvedAllowed = path.resolve(allowedPath);
|
|
550
|
+
return resolved.startsWith(resolvedAllowed + path.sep) || resolved === resolvedAllowed;
|
|
551
|
+
});
|
|
552
|
+
if (!allowed) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`[NativeSandbox] Access denied: path "${filePath}" is not within allowed filesystem paths: ` + JSON.stringify(this.profile.allowFS)
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Builds a sanitized environment for child processes.
|
|
560
|
+
* Removes sensitive variables (secrets, tokens, credentials) and merges
|
|
561
|
+
* any additional environment variables provided by the caller.
|
|
562
|
+
*/
|
|
563
|
+
_buildSafeEnv(extra) {
|
|
564
|
+
const SENSITIVE_PATTERNS = [
|
|
565
|
+
/SECRET/i,
|
|
566
|
+
/TOKEN/i,
|
|
567
|
+
/PASSWORD/i,
|
|
568
|
+
/PASSWD/i,
|
|
569
|
+
/API_KEY/i,
|
|
570
|
+
/PRIVATE_KEY/i,
|
|
571
|
+
/CREDENTIAL/i,
|
|
572
|
+
/AUTH/i,
|
|
573
|
+
/AWS_/i,
|
|
574
|
+
/GCP_/i,
|
|
575
|
+
/AZURE_/i
|
|
576
|
+
];
|
|
577
|
+
const safeEnv = {};
|
|
578
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
579
|
+
if (value === void 0) continue;
|
|
580
|
+
const isSensitive = SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
581
|
+
if (!isSensitive) {
|
|
582
|
+
safeEnv[key] = value;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
Object.assign(safeEnv, extra);
|
|
586
|
+
return safeEnv;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
345
590
|
// src/sandbox-manager.ts
|
|
346
591
|
var E2BProviderStub = class {
|
|
347
592
|
async start() {
|
|
@@ -415,6 +660,16 @@ var SandboxManager = class {
|
|
|
415
660
|
);
|
|
416
661
|
}
|
|
417
662
|
}
|
|
663
|
+
if (this.config.provider === "native") {
|
|
664
|
+
const { available, method } = await isNativeSandboxAvailable();
|
|
665
|
+
if (!available) {
|
|
666
|
+
console.warn(
|
|
667
|
+
"[SandboxManager] No native isolation method found (sandbox-exec / bwrap). Sandboxes will run with limited isolation (timeout + env filtering only)."
|
|
668
|
+
);
|
|
669
|
+
} else {
|
|
670
|
+
console.info(`[SandboxManager] Native isolation available: ${method}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
418
673
|
this._registerShutdownHandlers();
|
|
419
674
|
}
|
|
420
675
|
/**
|
|
@@ -432,6 +687,18 @@ var SandboxManager = class {
|
|
|
432
687
|
this.active.set(id2, stub);
|
|
433
688
|
return stub;
|
|
434
689
|
}
|
|
690
|
+
if (this.config.provider === "native") {
|
|
691
|
+
const nativeConfig = {
|
|
692
|
+
scope: overrides.scope,
|
|
693
|
+
profile: this.config.nativeConfig?.profile,
|
|
694
|
+
workingDirectory: this.config.nativeConfig?.workingDirectory
|
|
695
|
+
};
|
|
696
|
+
const sandbox2 = new NativeSandbox(nativeConfig);
|
|
697
|
+
await sandbox2.start();
|
|
698
|
+
const id2 = this._generateId(overrides.scope);
|
|
699
|
+
this.active.set(id2, sandbox2);
|
|
700
|
+
return sandbox2;
|
|
701
|
+
}
|
|
435
702
|
const mergedConfig = {
|
|
436
703
|
image: "node:22-slim",
|
|
437
704
|
...this.config.dockerConfig,
|
|
@@ -478,7 +745,7 @@ var SandboxManager = class {
|
|
|
478
745
|
// Private helpers
|
|
479
746
|
// ---------------------------------------------------------------------------
|
|
480
747
|
_generateId(scope) {
|
|
481
|
-
return `agentforge-${scope}-${
|
|
748
|
+
return `agentforge-${scope}-${randomUUID3().slice(0, 8)}`;
|
|
482
749
|
}
|
|
483
750
|
_registerShutdownHandlers() {
|
|
484
751
|
if (this.shutdownRegistered) return;
|