@cogitator-ai/sandbox

中文文档
0.10.4发布于 2026/10/29MITTypeScriptES Module近 30 天下载 9221

Sandbox execution for Cogitator agents (Docker, WASM, and remote workspaces on E2B, Daytona and Vercel Sandbox)

原始使用文档
文档目录38 节
  1. Installation
  2. Quick Start
  3. Features
  4. Sandbox Manager
  5. Fallback and Initialization
  6. Finding the Docker Daemon
  7. Availability Checks
  8. Shutdown
  9. Docker Executor
  10. Security Features
  11. Container Pool
  12. Pool Options
  13. Docker Workspace
  14. Remote Workspaces
  15. WASM Executor
  16. OS Sandbox
  17. Native Executor
  18. Execution Request
  19. Cancelling an Execution
  20. Execution Result
  21. Resource Limits
  22. Memory
  23. CPU
  24. Process Limits
  25. Network Configuration
  26. Volume Mounts
  27. Utility Functions
  28. Parse Memory
  29. CPU to NanoCPUs
  30. Type Reference
  31. Integration with Cogitator
  32. Examples
  33. Run Python Code
  34. Run Node.js Code
  35. Run Shell Commands
  36. Handle Timeouts
  37. Check Exit Codes
  38. License

@cogitator-ai/sandbox

Secure sandbox execution for Cogitator agents. Run untrusted code in isolated Docker containers, WASM modules, the operating system's own sandbox (Seatbelt on macOS, bubblewrap on Linux) or native fallback with resource limits, network isolation, and timeout enforcement, and give agents a workspace in a Docker container or in a cloud sandbox on E2B, Daytona or Vercel Sandbox.

Installation

pnpm add @cogitator-ai/sandbox

# Optional peer dependencies
pnpm add dockerode       # For Docker sandbox
pnpm add @extism/extism  # For WASM sandbox
pnpm add e2b             # For E2BWorkspace (2.28.1 or later)
pnpm add @daytona/sdk    # For DaytonaWorkspace
pnpm add @vercel/sandbox # For VercelSandboxWorkspace

Quick Start

import { SandboxManager } from '@cogitator-ai/sandbox';

const manager = new SandboxManager();
await manager.initialize();

const result = await manager.execute(
  { command: ['python', '-c', 'print("Hello!")'] },
  { type: 'docker', image: 'python:3.11-alpine' }
);

if (result.success) {
  console.log(result.data.stdout); // "Hello!"
} else {
  console.error(result.error);
}

execute() returns a SandboxResult: { success: true, data } or { success: false, error }. A command that exits non-zero is still a success; check data.exitCode.

Features

  • Docker Sandbox - Full container isolation with dropped capabilities
  • WASM Sandbox - Extism-powered WebAssembly execution
  • OS Sandbox - Local commands under Seatbelt (macOS) or bubblewrap (Linux): writes only in the workspace, reads everywhere but what you deny, network only through a proxy that checks each host, the sandbox settings of Claude Code read as they are
  • Native Fallback - Optional host execution for Docker tools when Docker is unavailable (allowNativeFallback)
  • Container Pool - A fresh, pre-started container for every execution
  • Docker Workspace - One long-lived container per agent workspace, the project mounted, files guarded on the host
  • Remote Workspaces - The whole workspace in an E2B, Daytona or Vercel sandbox, with the same behavior on every provider
  • Daemon Discovery - Finds Docker like the docker CLI: DOCKER_HOST, contexts, Docker Desktop, OrbStack, Colima, Rancher, rootless
  • Resource Limits - Memory, CPU, PID limits
  • Network Isolation - Disabled by default
  • Timeout Enforcement - Kill runaway processes
  • Security Hardening - No privilege escalation, all capabilities dropped

Sandbox Manager

The SandboxManager orchestrates the execution backends and routes each request to the one its type names.

import { SandboxManager } from '@cogitator-ai/sandbox';

const manager = new SandboxManager({
  allowNativeFallback: false,
  pool: {
    maxSize: 10,
    idleTimeoutMs: 120_000,
  },
  defaults: {
    timeout: 30_000,
    resources: {
      memory: '256MB',
      cpus: 1,
    },
    network: { mode: 'none' },
  },
});

await manager.initialize();

const result = await manager.execute(
  {
    command: ['node', '-e', 'console.log(2+2)'],
    timeout: 5000,
    env: { NODE_ENV: 'production' },
    cwd: '/workspace',
  },
  {
    type: 'docker',
    image: 'node:20-alpine',
  }
);

if (result.success) {
  console.log('Output:', result.data.stdout);
  console.log('Exit code:', result.data.exitCode);
  console.log('Duration:', result.data.duration, 'ms');
}

Fallback and Initialization

initialize() is idempotent and safe to call concurrently; execute() calls it automatically.

The manager falls back in one case: a docker request while Docker is unavailable runs on the host through the native executor, with no isolation, and the first such call logs a warning. allowNativeFallback: false refuses those requests with { success: false, error } instead, it defaults to true. The manager defaults apply to the fallback execution as well. A wasm request never falls back: without @extism/extism it fails with WASM sandbox unavailable: install @extism/extism to run WASM tools. An os request never falls back either, whatever allowNativeFallback says: on a machine without the OS sandbox it fails with the reason (unless its policy sets failIfUnavailable: false, see OS Sandbox).

Finding the Docker Daemon

With docker.socketPath or docker.host (+ port) the executor connects there. Otherwise it tries, in order, the first that answers a ping:

  1. DOCKER_HOST when set (handed to Dockerode, nothing else is tried)
  2. The endpoint of the current Docker context (DOCKER_CONTEXT, else currentContext in $DOCKER_CONFIG/config.json, default ~/.docker)
  3. The sockets that exist among /var/run/docker.sock, ~/.docker/run/docker.sock (Docker Desktop), ~/.orbstack/run/docker.sock (OrbStack), ~/.colima/default/docker.sock and ~/.colima/docker.sock (Colima), ~/.rd/docker.sock (Rancher Desktop) and $XDG_RUNTIME_DIR/docker.sock (rootless Docker)
  4. Dockerode's defaults, when none of the above exists

Availability Checks

const dockerAvailable = await manager.isDockerAvailable();
const wasmAvailable = await manager.isWasmAvailable();
const osAvailable = await manager.isOsAvailable();

console.log('Docker:', dockerAvailable);
console.log('WASM:', wasmAvailable);

Shutdown

await manager.shutdown();

Docker Executor

Full container isolation with security hardening.

import { DockerSandboxExecutor } from '@cogitator-ai/sandbox';

const docker = new DockerSandboxExecutor({
  docker: {
    socketPath: '/var/run/docker.sock',
  },
  pool: {
    maxSize: 5,
    idleTimeoutMs: 60_000,
  },
});

const connectResult = await docker.connect();
if (!connectResult.success) {
  console.error('Docker not available:', connectResult.error);
}

const result = await docker.execute(
  {
    command: ['python', '-c', 'print("Hello!")'],
    stdin: 'input data',
    timeout: 10_000,
    cwd: '/app',
    env: { MY_VAR: 'value' },
  },
  {
    type: 'docker',
    image: 'python:3.11-alpine',
    timeout: 30_000,
    resources: {
      memory: '512MB',
      cpus: 2,
      pidsLimit: 50,
    },
    network: {
      mode: 'none',
    },
    mounts: [{ source: '/tmp/data', target: '/data', readOnly: true }],
    env: { GLOBAL_VAR: 'value' },
    workdir: '/workspace',
    user: 'nobody',
  }
);

await docker.disconnect();

Security Features

Docker containers are created with this host configuration:

SettingValue
NetworkModenetwork.mode, default 'none' (no network)
CapDrop['ALL'] (every capability dropped)
SecurityOpt['no-new-privileges']
PidsLimitresources.pidsLimit, default 100
Root FSWritable (not configurable)
Working dir/workspace
Labelai.cogitator.sandbox=true
  • command is executed as an argv array (no shell); use ['sh', '-c', '...'] for shell syntax.
  • Output is demultiplexed frame by frame (frames split across network chunks are reassembled) and capped at 50 000 bytes per stream.
  • Every execution gets a container nothing ran in before (see Container Pool); timed-out containers are always destroyed.
  • network.dns is applied to new containers. network.allowedHosts is rejected because Docker cannot enforce an egress allow-list — use mode: 'none' or a dedicated network.
  • Containers are labeled ai.cogitator.sandbox=true (SANDBOX_CONTAINER_LABEL), so leftovers from a crashed process can be removed with docker rm -f $(docker ps -aq --filter label=ai.cogitator.sandbox).

Container Pool

Keeps containers started ahead of executions. By default a released container is destroyed and a fresh one with the same settings is started in its place, so the next execution gets a warm container nothing ran in before — files and processes never carry over between runs or users.

import { ContainerPool } from '@cogitator-ai/sandbox';

const pool = new ContainerPool(dockerClient, {
  maxSize: 10,
  idleTimeoutMs: 60_000,
});

const container = await pool.acquire('python:3.11-alpine', {
  memory: 256 * 1024 * 1024,
  cpus: 1,
  networkMode: 'none',
  mounts: [],
});

await pool.release(container);

await pool.destroyAll();

A container only serves requests with identical settings (image, resources, network mode, DNS, mounts and user), so a container created with a host mount or network access is never handed to a request that asked for isolation. release(container, { corrupted: true }) always destroys the container. destroyAll() removes every container and closes the pool. The idle-cleanup timer does not keep the Node.js process alive.

Pool Options

OptionTypeDefaultDescription
maxSizenumber5Maximum idle and warming containers; no spare is started beyond it
idleTimeoutMsnumber60000Time before destroying idle containers
reuseContainersbooleanfalseReturn released containers to the pool as they are: faster, but what one execution leaves behind is visible to the next, from any run or user

Pass the same options as pool to SandboxManager, DockerSandboxExecutor or new Cogitator({ sandbox: { pool } }).


Docker Workspace

DockerWorkspace is a workspace whose commands run in one long-lived container: created on first use (or in start()), kept between commands, removed in dispose(). The root of a host workspace (a LocalWorkspace from @cogitator-ai/core) is bind-mounted at /workspace, files are read and written through the host workspace and its path guard (a path under /workspace is mapped to the root), remove and rename run in the container where no symlink reaches the host, and commands run with docker exec in the matching directory. A snapshot restore removes what goes in the container too, then the host writes what changed, and is refused with BUSY while a command runs in the container or on the host.

import { Agent, LocalWorkspace, workspaceTools } from '@cogitator-ai/core';
import { DockerWorkspace } from '@cogitator-ai/sandbox';

const workspace = new DockerWorkspace({
  host: new LocalWorkspace({ root: './project' }),
  image: 'node:24-alpine',
  resources: { memory: '4GB', cpus: 4 },
});
try {
  const agent = new Agent({
    name: 'coder',
    instructions: '...',
    tools: workspaceTools(),
    workspace,
  });
  await cog.run(agent, { input: 'Run the tests and fix what fails' });
} finally {
  await workspace.dispose();
}

The container has the security defaults of the Docker executor, an init process, limits of 2 GB, 2 CPUs and 512 processes unless resources sets others, no network unless network.mode allows it, and on Linux runs commands as your uid and gid. Each command leads its own process group: a timeout or an abort signals the group (SIGTERM, then SIGKILL after killGrace), background and env -i processes included, and the container keeps running. A process that calls setsid escapes the group until dispose(). The image needs sh. Starting removes containers of the same root left by a process of this machine that is gone. Without Docker the first command fails with a WorkspaceError of code UNAVAILABLE, Docker API failures come as code IO. dispose() also disposes the host workspace.

Remote Workspaces

E2BWorkspace, DaytonaWorkspace and VercelSandboxWorkspace are workspaces whose files and commands live in a cloud sandbox: created on first use (or reused by id or name), released in dispose(). Each needs its provider's SDK, an optional peer dependency, and credentials:

WorkspacePackageCredentialsDefault root
E2BWorkspacee2bE2B_API_KEY/home/user
DaytonaWorkspace@daytona/sdkDAYTONA_API_KEY (DAYTONA_API_URL, DAYTONA_TARGET)/home/daytona
VercelSandboxWorkspace@vercel/sandboxVercel's OIDC token, or VERCEL_TOKEN, VERCEL_TEAM_ID, VERCEL_PROJECT_ID/vercel/sandbox
import { Agent, workspaceTools } from '@cogitator-ai/core';
import { E2BWorkspace } from '@cogitator-ai/sandbox';

const workspace = new E2BWorkspace({ root: 'project', lifetime: 30 * 60_000 });
try {
  const agent = new Agent({
    name: 'coder',
    instructions: '...',
    tools: workspaceTools(),
    workspace,
  });
  await cog.run(agent, { input: 'Clone the repo and make its tests pass' });
} finally {
  await workspace.dispose();
}

All three are RemoteShellWorkspace over a small driver (create or connect, read and write bytes, run a program, release), and everything else runs as shell scripts in the sandbox, so they behave the same:

  • Paths are normalized against the root and resolved in the sandbox with realpath -m, a symlink out of the root is refused with OUTSIDE_ROOT
  • Writes upload to a fresh file of the sandbox's temporary directory and move over the file in one step, keeping its mode, reads go through a private copy in the sandbox's temporary directory and never write the root, maxFileBytes caps both
  • list, remove (with the trash in .cogitator/trash) and rename follow the LocalWorkspace rules, and refuse with BUSY while a command runs or kept background processes
  • Commands run under setsid in their own process group, with unforgeable marker lines (the token comes on stdin): a timeout returns 124 and kills the group, background: 'keep' leaves background processes until dispose()
  • snapshot(), restore(), listSnapshots() and deleteSnapshot() keep tar archives in .cogitator/snapshots inside the sandbox, .cogitator, node_modules and .git always excluded, files over maxFileBytes (10 MiB) skipped and listed in skipped, names that are not UTF-8 kept as they are, and maxSnapshotBytes (1 GiB) refusing a bigger one with TOO_LARGE. restore(id, { dryRun }) keeps directories that hold excluded entries and files over the snapshot's size limit, and reports what it restored, deleted and kept, the same API as a local workspace's file checkpoints
  • Reads copy at most one byte over maxFileBytes before they download, listings are sorted, cut at limit and narrowed by glob in the sandbox, and a command that prints more than maxStreamBytes (64 MiB) is stopped, because the SDKs keep whole outputs in memory
  • E2B and Vercel sandboxes get a lifetime (30 minutes) that the workspace pushes back while it is used

onDispose picks what dispose() does with the sandbox: destroy (default for a created one), stop (Daytona and Vercel, the files survive) or keep (default for one given by id or name, stop for a named Vercel sandbox). The image needs sh, GNU coreutils, findutils and tar, and util-linux setsid, as the providers' default images have, and start() names what is missing. Without the SDK or the credentials the first operation fails with a WorkspaceError of code UNAVAILABLE. e2b needs 2.28.1 or later, and from 2.59 it needs Node.js 22.19 or later. A process that starts a session of its own (setsid) escapes the group kill and lives until the sandbox goes. DaytonaWorkspace passes env in a short-lived file of a private directory, so Daytona's record of the command line never holds the values.

Bring another provider by implementing RemoteShellDriver and passing it to new RemoteShellWorkspace({ driver }). shellQuote and shellCommand build a safe shell line from an argv.

WASM Executor

Execute WebAssembly modules via Extism. Each plugin runs in its own worker thread, so a timeout terminates a runaway module instead of blocking the event loop. Idle plugins are pooled per module / WASI / allowed-host combination (up to cacheSize); concurrent executions use separate plugin instances.

import { WasmSandboxExecutor } from '@cogitator-ai/sandbox';

const wasm = new WasmSandboxExecutor({
  wasm: {
    cacheSize: 10,
    wasi: true,
  },
});

await wasm.connect();

const result = await wasm.execute(
  {
    command: ['process'],
    stdin: JSON.stringify({ data: 'input' }),
  },
  {
    type: 'wasm',
    wasmModule: 'https://example.com/plugin.wasm',
    wasmFunction: 'run',
    wasi: true,
    timeout: 5000,
  }
);

await wasm.disconnect();
  • wasmModule takes an http(s) URL, a file path or a package path:
    • a path that is absolute or starts with . is read relative to the working directory
    • anything else is first tried as a file relative to the working directory, then resolved like an import made by your application (for example @cogitator-ai/wasm-tools/wasm/calc.wasm): from the working directory, then from the entry script's directory, then from @cogitator-ai/sandbox itself (which covers NODE_PATH and hoisted installs). This works with pnpm's strict layout, where the sandbox package cannot see your dependencies.
    • when the module ships with a package that exports a path helper, such as getWasmPath() from @cogitator-ai/wasm-tools, pass that absolute path to skip resolution entirely.
  • wasmModule, functionName and wasi from the executor options are used when the per-request config omits them.
  • network.allowedHosts is forwarded to Extism's HTTP allow-list (empty when network.mode is 'none').
  • memoryPages (default 256 = 16 MB) caps the module's own memory: its memory section gets that maximum before the module loads, so memory.grow past it fails inside the module, and a module that needs more to start is refused. Extism's memory for plugin input, output and vars gets the same limit. Modules from a URL are fetched by the executor so the cap applies to them too.

OS Sandbox

OsSandbox runs local commands inside the operating system's sandbox, with the semantics of Claude Code's sandboxed Bash tool: Seatbelt (sandbox-exec with a generated profile) on macOS, bubblewrap (bwrap) on Linux. Linux also needs socat: apt-get install bubblewrap socat. Give it to a LocalWorkspace of @cogitator-ai/core, and every command the workspace runs (run_command) goes through it:

import { LocalWorkspace, loadSandboxPolicy, workspaceTools } from '@cogitator-ai/core';
import { OsSandbox } from '@cogitator-ai/sandbox';

const { policy } = await loadSandboxPolicy('./project'); // the `sandbox` section of .claude/settings.json
const sandbox = new OsSandbox({ policy });
const workspace = new LocalWorkspace({ root: './project', exec: { sandbox } });
await workspace.start(); // fails with SANDBOX_UNAVAILABLE when the sandbox cannot run here

What a sandboxed command can reach:

AccessDefaultChange it with
WritesThe workspace root and a temporary directory for that command alone (TMPDIR)filesystem.allowWrite, filesystem.denyWrite
ReadsEverywhere, credential files such as ~/.ssh includedfilesystem.denyRead, filesystem.allowRead, credentials.files
NetworkNo route out but the sandbox's proxy, which allows nothing until you list hostsnetwork.allowedDomains, network.deniedDomains, onUnknownHost
EnvWhat the workspace passes (no API keys by default)credentials.envVars
  • Filesystem. The narrower rule wins: an allowRead inside a wider denyRead reopens that part, a denyRead inside a wider allowRead still holds. An allowRead that reaches a denied region only through a symlink is ignored, and a file denyRead or credentials.files names with another hard link fails the launch, since a deny holds for the names it lists. A write deny always wins over an allow. Paths are absolute, ~/ for the home directory, or relative to the workspace root. *, ?, ** and [...] work in every list on macOS and in the read lists on Linux, a trailing /** everywhere.
  • Protected paths. Even inside a writable directory a command cannot write the settings, hooks, skills, agents, commands and plugins of Claude Code and Cogitator (in the root and every directory above it, and ~/.claude, ~/.cogitator, ~/.claude.json), the whole .cogitator directory of the root (its trash and snapshots), .mcp.json, shell startup files, .gitconfig, .gitmodules, .vscode, .idea, the files of the repository's git directory that git runs or follows (hooks, config, commondir, config.worktree, of the main repository for a linked worktree, of linked worktrees and submodules too), the root's .git entry itself (no command replaces it with a gitdir: file of its own, git still writes inside), and a HEAD at the root, so no command turns the root into a bare repository whose config git outside the sandbox would read. A symlink at a protected path protects its target too, and a protected file with more than one hard link fails the launch. Only filesystem.disabled lifts them, with the rest of filesystem isolation.
  • Network. Commands get HTTP_PROXY, HTTPS_PROXY and ALL_PROXY (SOCKS5) pointing at a proxy that serves that command alone. A host deniedDomains names is refused, one allowedDomains names goes through (github.com that host only, *.npmjs.org its subdomains, * every host, IPv4 and bracketed IPv6 addresses, each with an optional :port), and any other host is decided by the call's onUnknownHost (a run wires it to its permissions as a WebFetch(domain:...) call), else the sandbox's own onUnknownHost, else refused. strictAllowlist refuses unknown hosts without asking. A host name that resolves only to loopback, link-local, a cloud metadata endpoint or an address of this machine is refused, localhost aside, list the address itself (127.0.0.1:3000) to reach a local server by name. UDP, QUIC and ICMP do not leave, and tools that ignore the proxy variables (plain ssh, most database drivers) cannot connect.
  • Per-command hosts. exec(command, { allowedDomains }) (and run_command's allowedDomains) opens hosts for that command alone, while it runs, and a host it does not list is then refused without asking. deniedDomains still wins, and a strictAllowlist policy refuses such a call.
  • Violations. What the proxy refused, and the file accesses the system refused (Operation not permitted, Read-only file system), are attributed to the command: result.sandbox.violations, and a sanitized <sandbox_violations> block appended to its stderr, so the model sees why it failed. ignoreViolations silences the expected ones ({ "*": ["/etc/hosts"] }). Network violations come from the proxy, file violations are advisory (read from the command's own errors, which it controls), and a <sandbox_violations> tag the command prints itself comes back neutralized.
  • Linux nesting. The command runs in a second bwrap inside the first, with --disable-userns (bubblewrap 0.8.0 and later) and a seccomp filter (x86_64, arm64) that refuses new namespaces, mounts, io_uring and Unix sockets, datagram socket pairs included (any allowUnixSockets entry or allowAllUnixSockets opens them all, the filter cannot tell paths apart), so it cannot undo the read-only binds from a namespace of its own. The seccomp filter is required, as the one thing that keeps commands from pathname Unix sockets: without it the sandbox is unavailable, unless allowAllUnixSockets is set, when --disable-userns alone will do.
  • Outside the sandbox. excludedCommands (the content of Bash(...) rules, such as docker *) run unsandboxed when every command of the line matches and the line has no redirection, cd, substitution or program from a variable. dangerouslyDisableSandbox on a call runs it unsandboxed only when the policy sets allowUnsandboxed (default false, ignored otherwise). Both go through the normal permission flow, and the result says sandboxed: false with the reason.
  • Unavailable. failIfUnavailable (default true) refuses to start, with a SANDBOX_UNAVAILABLE error naming why: no sandbox-exec, no bwrap or socat, user namespaces blocked (a container, Ubuntu's AppArmor restriction). With false commands run unsandboxed with a warning, and each result says so (unsandboxedReason: 'unavailable'). There is no silent fallback.
  • Permissions. A command that runs inside skips the approval it would need (autoAllow, default true): run_command's own and a bare Bash ask rule. Deny rules, ask rules with a pattern (Bash(git push *)), rm and rmdir of a critical path (the root, ~, /, .git) and plan mode still apply, and a command with VAR=x in front, eval, make X=y, source, alias, export, set -a or a shell reading stdin goes through the normal flow, as do commands that name hosts of their own. Only run_command of workspaceTools() running in the sandboxed workspace is auto-allowed, never the core exec tool.

OsSandbox.probe() says whether the sandbox runs here, OsSandbox.create(options) returns a started one, dispose() closes the proxies of commands still running and removes its directories. The os type of the SandboxManager runs an execution under it ({ type: 'os', workdir, os: policy }, the workdir its writable root), always with failIfUnavailable: true.

Not covered, honestly: shell rm deletes for good (the trash is the delete_file tool's, see @cogitator-ai/core), processes a command leaves running in the background lose the network and their TMPDIR once it exits (and are stopped with it on Linux), file violations are read from the command's errors, not from the system log, Linux denies paths a wildcard matches when the command starts (a file made later is not covered), Linux protects a path behind a symlinked directory where the link leads, not at the link, git repositories below the root keep their hooks writable, and no TLS is inspected, so an allowed domain carries whatever goes to it.


Native Executor

Direct execution without isolation (fallback mode).

import { NativeSandboxExecutor } from '@cogitator-ai/sandbox';

const native = new NativeSandboxExecutor();
await native.connect();

const result = await native.execute(
  {
    command: ['ls', '-la'],
    cwd: '/tmp',
    env: { LC_ALL: 'C' },
    timeout: 5000,
  },
  { type: 'native' }
);

if (result.success) console.log(result.data.stdout);

Warning: Native execution has no isolation. Use only when Docker is unavailable.

  • A single-element command (['ls -la | wc -l']) runs through the system shell; longer commands are executed directly with exact argv (no shell), like Docker.
  • stdin is piped to the process.
  • Only PATH, HOME, temp-dir, locale and Windows system variables are inherited from the host environment; secrets in process.env are not passed to executed code. Add variables explicitly via env.
  • On timeout the whole process group is killed (SIGKILL) and the result is returned immediately with exit code 124.
  • Output is capped at 50 000 bytes per stream while the process is still drained.

Execution Request

interface SandboxExecutionRequest {
  command: string[];
  stdin?: string;
  timeout?: number;
  cwd?: string;
  env?: Record<string, string>;
  signal?: AbortSignal;
}

Cancelling an Execution

Aborting signal stops the execution right away: the native executor kills the process group, the Docker executor ends the exec and removes its container (an aborted container is never reused), and the WASM executor closes the plugin, which ends its worker. The result is a failure that names the reason, and a signal that is already aborted runs nothing. Cogitator passes the run's signal, so cancelling a run stops its sandboxed tools.

const controller = new AbortController();
setTimeout(() => controller.abort(new Error('user cancelled')), 1000);

const result = await manager.execute(
  { command: ['sleep', '30'], signal: controller.signal },
  { type: 'docker', image: 'alpine:3.19' }
);
// { success: false, error: 'Execution aborted: user cancelled' }

Execution Result

interface SandboxExecutionResult {
  stdout: string;
  stderr: string;
  exitCode: number;
  timedOut: boolean;
  duration: number;
  truncated?: boolean;
}

Resource Limits

Memory

const result = await manager.execute(request, {
  type: 'docker',
  image: 'alpine',
  resources: {
    memory: '256MB',
  },
});

Supported formats: '256B', '256KB', '256MB', '1GB', '1TB', Docker-style '512m' / '2g' and binary '1GiB' (all binary multiples, case-insensitive). Zero or negative memory and CPU limits are rejected instead of silently running unlimited.

CPU

SandboxConfig is exported as a type (import type { SandboxConfig } from '@cogitator-ai/sandbox').

const config: SandboxConfig = {
  type: 'docker',
  image: 'alpine',
  resources: {
    cpus: 0.5,
    cpuShares: 512,
  },
};

Process Limits

const config: SandboxConfig = {
  type: 'docker',
  image: 'alpine',
  resources: {
    pidsLimit: 50,
  },
};

Network Configuration

const config: SandboxConfig = {
  type: 'docker',
  image: 'alpine',
  network: {
    mode: 'none',
  },
};

Network modes:

  • 'none' - No network access (default, most secure)
  • 'bridge' - Docker bridge network
  • 'host' - Host network (not recommended)

Volume Mounts

Mount host directories into the container:

const config: SandboxConfig = {
  type: 'docker',
  image: 'alpine',
  mounts: [
    { source: '/host/data', target: '/data', readOnly: true },
    { source: '/host/output', target: '/output', readOnly: false },
  ],
};

Utility Functions

Parse Memory

import { parseMemory } from '@cogitator-ai/sandbox';

parseMemory('256MB');
parseMemory('1GB');
parseMemory('512KB');

CPU to NanoCPUs

import { cpusToNanoCpus } from '@cogitator-ai/sandbox';

cpusToNanoCpus(0.5);
cpusToNanoCpus(2);

Type Reference

import type {
  SandboxType,
  SandboxConfig,
  SandboxResourceLimits,
  SandboxNetworkConfig,
  SandboxMount,
  SandboxExecutionRequest,
  SandboxExecutionResult,
  SandboxManagerConfig,
  SandboxPoolConfig,
  SandboxDockerConfig,
  SandboxResult,
  OsSandboxPolicy,
  OsSandboxKind,
  OsSandboxAvailability,
  SandboxViolation,
  SandboxHostRequest,
  SandboxHostDecider,
} from '@cogitator-ai/sandbox';

Integration with Cogitator

Use sandboxed tools in your agents:

import { Cogitator, tool } from '@cogitator-ai/core';
import { z } from 'zod';

const shellTool = tool({
  name: 'run_shell',
  description: 'Execute shell commands safely',
  parameters: z.object({
    command: z.string(),
  }),
  sandbox: {
    type: 'docker',
    image: 'ubuntu:22.04',
    resources: { memory: '256MB' },
    network: { mode: 'none' },
  },
  timeout: 30000,
  execute: async ({ command }) => command,
});

const cog = new Cogitator({
  sandbox: {
    pool: { maxSize: 5 },
    allowNativeFallback: false,
  },
});

The runtime starts a SandboxManager from new Cogitator({ sandbox }) on the first sandboxed tool call. For a Docker tool it does not call execute: the sandbox runs the tool's command argument with sh -c (with optional cwd / env arguments) and the output becomes the tool result. A WASM tool gets its arguments as JSON on stdin and its JSON stdout is the result. When Docker is unavailable — or this package is missing or fails to start — a Docker tool runs its execute unsandboxed on the host with a warning, unless sandbox.allowNativeFallback: false turns the call into a tool error. A WASM tool fails when Extism is missing, and runs its own execute only when this package is missing or fails to start. See Sandbox on the website.


Examples

Run Python Code

const result = await manager.execute(
  {
    command: [
      'python',
      '-c',
      `
import json
data = {"sum": 2 + 2}
print(json.dumps(data))
    `,
    ],
  },
  {
    type: 'docker',
    image: 'python:3.11-alpine',
    timeout: 10_000,
  }
);

if (result.success) {
  const output = JSON.parse(result.data.stdout);
  console.log(output.sum);
}

Run Node.js Code

const result = await manager.execute(
  {
    command: ['node', '-e', 'console.log(JSON.stringify({result: 42}))'],
  },
  {
    type: 'docker',
    image: 'node:20-alpine',
    resources: { memory: '128MB' },
  }
);

Run Shell Commands

const result = await manager.execute(
  {
    command: ['sh', '-c', 'ls -la /workspace && pwd'],
    cwd: '/workspace',
  },
  {
    type: 'docker',
    image: 'alpine:3.19',
  }
);

Handle Timeouts

const result = await manager.execute(
  { command: ['sleep', '60'] },
  { type: 'docker', image: 'alpine', timeout: 5000 }
);

if (result.success && result.data.timedOut) {
  console.log('Command timed out');
}

Check Exit Codes

const result = await manager.execute(
  { command: ['sh', '-c', 'exit 42'] },
  { type: 'docker', image: 'alpine' }
);

if (result.success) console.log('Exit code:', result.data.exitCode);

License

MIT

版本与发布记录

@cogitator-ai/sandbox 共发布 96 个版本,首次发布于 2025/12/31;过去 12 个月发布了 96 个版本。

版本发布日期距今
0.10.4latest2026/10/2912 天前
0.10.32026/10/2912 天前
0.10.22026/10/2912 天前
0.10.12026/10/2912 天前
0.10.02026/10/2812 天前
0.9.22026/10/2813 天前

关于 @cogitator-ai/sandbox 的常见问题

@cogitator-ai/sandbox 是什么?

Sandbox execution for Cogitator agents (Docker, WASM, and remote workspaces on E2B, Daytona and Vercel Sandbox)

如何安装 @cogitator-ai/sandbox?

在项目目录运行 npm install @cogitator-ai/sandbox,或使用 pnpm add / yarn add / bun add。

@cogitator-ai/sandbox 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cogitator-ai/sandbox 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cogitator-ai/sandbox 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cogitator-ai/sandbox 可以商用吗?

@cogitator-ai/sandbox 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cogitator-ai/sandbox 的下载量有多少?

根据 npm 官方下载统计,@cogitator-ai/sandbox 在 2026/10/11 – 2026/11/09 这 30 天内被下载 9,221 次,日均约 307 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cogitator-ai/sandbox 还在维护吗?

最新版本 0.10.4 发布于 2026/10/29(12 天前);至今共发布 96 个版本,其中过去 12 个月发布了 96 个;首次发布于 2025/12/31。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。