@anvia/sandbox

Docker-backed sandbox sessions for running untrusted code.

@anvia/sandbox provides ephemeral Docker containers for running model-generated or untrusted code outside the host process. It also exposes sandbox operations as agent tools.

Install

pnpm add @anvia/sandbox

Docker must be installed and running on the host machine.

Quick Start

import { DockerSandbox } from "@anvia/sandbox";

const sandbox = new DockerSandbox();
const session = await sandbox.createSession();

// Run a command
const result = await session.exec({ command: "node", args: ["-e", "console.log(42)"] });
console.log(result.stdout); // "42\n"

// Clean up
await session.destroy();

Sandbox Configuration

type DockerSandboxOptions = {
  image?: string;          // Docker image (default: "node:22-bookworm")
  pull?: "missing" | "always" | "never";  // pull policy (default: "missing")
  workdir?: string;        // workspace directory (default: "/workspace")
  workspace?: SandboxWorkspaceOptions;  // ephemeral or persistent volume
  lifecycle?: SandboxLifecycleOptions;  // ttl and idle cleanup
  network?: boolean | "none" | "host" | string | { mode: boolean | "none" | "host" | string };
  user?: string;           // container user
  dockerPath?: string;     // path to docker CLI
  labels?: Record<string, string>;  // Docker container labels
  limits?: SandboxLimits;  // resource limits
  security?: DockerSandboxSecurityOptions;
  hooks?: SandboxHooks;
};
// Custom image and limits
const sandbox = new DockerSandbox({
  image: "python:3.12-slim",
  limits: { timeoutMs: 30_000, maxFileBytes: 5_000_000, memoryMb: 512, cpus: 1 },
});

// Network access enabled
const sandbox = new DockerSandbox({
  network: { mode: "host" },
});

Presets

const nodeSandbox = DockerSandbox.node();
const pythonSandbox = DockerSandbox.python();
const denoSandbox = DockerSandbox.deno();

Security Defaults

By default, sandboxes run with:

  • Network access disabled
  • noNewPrivileges enabled
  • All Linux capabilities dropped (dropCapabilities: ["ALL"])

Override with the security option:

const sandbox = new DockerSandbox({
  security: {
    readonlyRootfs: false,      // allow writes to root filesystem
    noNewPrivileges: true,
    dropCapabilities: ["ALL"],
  },
});

Sessions

Each session creates one Docker container and one Docker volume mounted at the workdir.

Creating Sessions

const session = await sandbox.createSession({
  id: "my-session",           // optional custom ID
  workspace: { mode: "ephemeral" },
  metadata: { userId: "123" }, // optional metadata
  manifest: {
    files: {
      "main.ts": 'console.log("hello")',
      "data.json": '{"key": "value"}',
    },
    directories: ["src", "tests"],
    env: { API_KEY: "secret" },
  },
});

The manifest seeds the workspace with files and directories before any commands run.

Use a persistent workspace only when continuity is explicit:

const session = await sandbox.createSession({
  workspace: {
    mode: "persistent",
    id: `user-${userId}`,
  },
});

Executing Commands

const result = await session.exec({
  command: "node",
  args: ["main.ts"],
  cwd: "src",                   // working directory (relative to workdir)
  env: { DEBUG: "true" },       // additional environment variables
  timeoutMs: 10_000,            // per-command timeout
  input: "stdin data",          // stdin input
  signal: abortController.signal,  // abort signal
  onStdout: (chunk) => {},      // streaming stdout callback
  onStderr: (chunk) => {},      // streaming stderr callback
});

console.log(result.stdout);
console.log(result.stderr);
console.log(result.exitCode);
console.log(result.durationMs);
console.log(result.timedOut);

Stream command output with execStream(...):

for await (const event of session.execStream({ command: "npm", args: ["test"] })) {
  if (event.type === "stdout" || event.type === "stderr") {
    process.stdout.write(event.text);
  }
}

File Operations

// Write files
await session.writeTextFile("output.txt", "Hello, world!");
await session.writeFile("binary.bin", uint8Array);

// Read files
const content = await session.readTextFile("output.txt");
const bytes = await session.readFile("binary.bin");

// List files
const files = await session.listFiles("src");
// [{ path: "src/index.ts", type: "file", size: 1234 }]

Cleanup

await session.destroy();

This removes both the container and the Docker volume.

Agent Tools

Expose a sandbox session as agent tools:

import { createSandboxTools } from "@anvia/sandbox";
import { AgentBuilder } from "@anvia/core";

const session = await sandbox.createSession();
const tools = createSandboxTools(session, {
  allow: ["exec_command", "read_file", "write_file", "list_files"],
  exec: {
    allowedCommands: ["node", "npm"],
    maxTimeoutMs: 30_000,
  },
});

const agent = new AgentBuilder("coder", model)
  .instructions("Write and run code to solve problems.")
  .tool(...tools)
  .defaultMaxTurns(5)
  .build();

Available Tools

ToolDescription
exec_commandRun a shell command in the sandbox
read_fileRead a file from the workspace
write_fileWrite a file to the workspace
list_filesList files in a directory

Tool Options

const tools = createSandboxTools(session, {
  include: ["exec_command", "read_file"],  // only include specific tools
  execTimeoutMs: 30_000,                   // default timeout for exec_command
});

Resource Limits

type SandboxLimits = {
  timeoutMs?: number;      // session-level timeout
  maxOutputBytes?: number; // max stdout/stderr bytes
  memoryMb?: number;       // container memory limit
  cpus?: number;           // container CPU limit
  pidsLimit?: number;      // max processes
};

Error Types

ErrorWhen
SandboxDockerUnavailableErrorDocker CLI not found
SandboxDockerCommandErrorDocker setup command failed
SandboxSessionDestroyedErrorUsing a session after destroy()
SandboxPathErrorPath traversal outside workspace
SandboxTimeoutErrorCommand exceeded timeout