Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Sandbox

Execute real sandbox commands across Docker, Apple Container, Microsandbox, Daytona, WASM, virtual, and custom backends

The sandbox system lets AI agents execute real commands in isolated environments. Choose between Docker containers for portable full binary execution, Apple Container lightweight VMs for stronger local isolation on Apple silicon, Microsandbox microVMs for local hardware-isolated OCI images, caller-owned Daytona sandboxes for managed remote workspaces, WASM virtual machines for lightweight in-process execution, virtual file systems for simulation, custom Sandbox implementations for specialized policy, or binary bridges to combine approaches.

When to Use What

ApproachUse CaseRequires Docker?
createDockerSandbox + createBashToolAgent needs real binaries in isolated containerYes
createDockerSandboxDirect container control without AI agent wiringYes
createAppleContainerSandbox + createBashToolAgent needs real binaries in Apple Container lightweight VMsNo
createAppleContainerSandboxDirect VM-backed container control on Apple silicon/macOS 26+No
createMicrosandboxSandbox + createBashToolAgent needs OCI images in local microVMs with streaming process outputNo
createMicrosandboxSandboxDirect local microVM control with get-or-create named sandboxesNo
createDaytonaSandbox(client, options) + createBashToolManaged remote sandbox with caller-owned Daytona clientNo
createAgentOsSandboxLightweight WASM execution, no Docker neededNo
createVirtualSandboxIn-process bash with a virtual filesystemNo
createBinaryBridgesBridge specific host binaries into just-bash virtual FSNo

All AI-agent-facing surfaces compose the same way: pick a backend factory (createDockerSandbox / createAppleContainerSandbox / createMicrosandboxSandbox / createDaytonaSandbox / createAgentOsSandbox / createVirtualSandbox), then wrap it with createBashTool({ sandbox }) to get the bash tool and file tools. Skill provisioning and discovery remain the application's responsibility. For per-command file-change tracking, first decorate a real backend with withStraceFileChanges(...), then pass that tracked backend into createBashTool.

All backends expose the buffered command + file API (executeCommand, readFile, writeFiles, dispose). Backends that can stream real processes, including Docker, Apple Container, Microsandbox, Agent OS, and Daytona sandboxes, additionally expose an optional spawn(command, options?) for live streaming stdio. Feature-detect it with if (!sandbox.spawn) { ... } when writing backend-agnostic code.

Every backend is also AsyncDisposable: it implements [Symbol.asyncDispose] (which delegates to dispose()), so you can bind it with await using and let the sandbox release at scope exit instead of writing try/finally:

await using backend = await createDockerSandbox({ installers: [pkg(['jq'])] });
await backend.executeCommand('jq --version');
// backend.dispose() runs automatically when this scope exits

Agent OS is a v0.1.0 preview. Requires optional peer dependencies @rivet-dev/agent-os-core and @rivet-dev/agent-os-common.

Quick Start: Docker + Bash Tool

The fastest path for giving an AI agent a bash tool that runs in Docker:

import { groq } from '@ai-sdk/groq';
import { printer } from '@deepagents/agent';
import {
  type AvailableSkill,
  ContextEngine,
  InMemoryContextStore,
  agent,
  createBashTool,
  createDockerSandbox,
  pkg,
  role,
  skills,
  user,
} from '@deepagents/context';

const backend = await createDockerSandbox({
  installers: [pkg(['curl', 'jq'])],
});
const sandbox = await createBashTool({ sandbox: backend });

// The application separately mounts or copies these files into the backend
// and builds the catalog from its own manifest, database, or discovery logic.
const availableSkills: AvailableSkill[] = [
  {
    name: 'deploy',
    description: 'Deploy services safely.',
    path: '/workspace/skills/deploy/SKILL.md',
  },
];

try {
  const context = new ContextEngine({
    chatId: 'demo',
    userId: 'user-1',
    store: new InMemoryContextStore(),
  });

  context.set(
    role('You are a helpful assistant with bash access.'),
    skills(availableSkills),
    user('Use the most relevant skill to help me.'),
  );

  const assistant = agent({
    name: 'Assistant',
    sandbox,
    model: groq('gpt-oss-20b'),
    context,
  });

  await printer.stdout(await assistant.stream({}));
} finally {
  await sandbox.sandbox.dispose();
}

Quick Start: Docker Sandbox

For direct container control without AI agent wiring:

import { createDockerSandbox, pkg } from '@deepagents/context';

const sandbox = await createDockerSandbox({
  image: 'alpine:latest',
  installers: [pkg(['curl', 'jq'])],
  volumes: [
    {
      type: 'bind',
      hostPath: process.cwd(),
      containerPath: '/workspace',
      readOnly: true,
    },
  ],
  resources: { memory: '512m', cpus: 1 },
});

try {
  const result = await sandbox.executeCommand('curl --version');
  console.log(result.stdout);

  await sandbox.writeFiles([
    { path: '/tmp/hello.txt', content: 'Hello from Docker!' },
  ]);

  const content = await sandbox.readFile('/tmp/hello.txt');
  console.log(content);
} finally {
  await sandbox.dispose();
}

Or use useSandbox for automatic cleanup:

import { useSandbox } from '@deepagents/context';

const output = await useSandbox(
  { installers: [pkg(['curl'])] },
  async (sandbox) => {
    const result = await sandbox.executeCommand('curl --version');
    return result.stdout;
  },
);

Quick Start: Binary Bridges

Bridge specific host binaries into a just-bash virtual file system (no Docker needed):

import { Bash, ReadWriteFs } from 'just-bash';
import { createBashTool, createBinaryBridges } from '@deepagents/context';

const bridges = createBinaryBridges(
  'node',
  { name: 'python', binaryPath: 'python3' },
  { name: 'git', allowedArgs: /^(status|log|diff|show)/ },
);

const { bash } = await createBashTool({
  sandbox: new Bash({
    fs: new ReadWriteFs({ root: process.cwd() }),
    customCommands: bridges,
  }),
});

Binary bridges resolve virtual paths to real host paths and use the host's PATH for binary resolution, while restricting access via optional allowedArgs regex.

Quick Start: Agent OS

WASM-based execution with no Docker dependency and ~6ms cold start:

import common from '@rivet-dev/agent-os-common';
import { createAgentOsSandbox } from '@deepagents/context';

const sandbox = await createAgentOsSandbox({
  software: [common],
});

try {
  const result = await sandbox.executeCommand('echo "Hello from WASM!"');
  console.log(result.stdout);

  await sandbox.writeFiles([
    { path: '/tmp/hello.txt', content: 'Written inside WASM' },
  ]);

  const content = await sandbox.readFile('/tmp/hello.txt');
  console.log(content);
} finally {
  await sandbox.dispose();
}

Or use useAgentOsSandbox for automatic cleanup:

import common from '@rivet-dev/agent-os-common';
import { useAgentOsSandbox } from '@deepagents/context';

const output = await useAgentOsSandbox(
  { software: [common] },
  async (sandbox) => {
    const result = await sandbox.executeCommand('ls /');
    return result.stdout;
  },
);

Quick Start: Virtual Sandbox

Use the virtual sandbox when an agent needs the standard bash/read/write tool surface without Docker:

import { InMemoryFs } from 'just-bash';
import { createBashTool, createVirtualSandbox } from '@deepagents/context';

const sandbox = await createBashTool({
  sandbox: await createVirtualSandbox({ fs: new InMemoryFs() }),
});

const result = await sandbox.sandbox.executeCommand('echo "hello"');
console.log(result.stdout); // hello

The virtual backend uses just-bash, so command execution and file IO stay in-process while still exercising the same createBashTool path used by agents.

When you want command groups such as sql run, tool validate, or marker remind inside a virtual sandbox, pass customCommands to createVirtualSandbox() and build them with Subcommand Builders.

Bash Tool Schema

createBashTool() from @deepagents/context returns a bash tool whose input schema requires two fields: command and reasoning. The LLM must provide a brief reason on every call; the owned tool enforces it at the Zod schema level.

interface BashToolInput {
  command: string;
  reasoning: string;
}

type WrappedBashTool = Tool<BashToolInput, CommandResult>;

The reasoning field is stripped before the command runs — it never influences execution. The AI SDK records it as part of the tool-call step input, so it remains visible in the response's content array for auditing. Calls missing reasoning fail schema validation before execute is invoked.

const sandbox = await createBashTool({
  sandbox: await createVirtualSandbox({ fs: new InMemoryFs() }),
});

// ✅ Accepted by the tool schema
await sandbox.tools.bash.execute(
  { command: 'ls /workspace', reasoning: 'List files to find the entry point' },
  {} as never,
);

// ❌ Rejected — AI SDK emits a tool-error with /reasoning/
await sandbox.tools.bash.execute(
  { command: 'ls /workspace' } as never,
  {} as never,
);

Type-safe consumers can import WrappedBashTool and BashToolInput directly from @deepagents/context.

Command Metadata

A bash hook can return host-only metadata alongside an optional replacement result. createBashTool() merges it with metadata already returned by sandbox decorators such as withStraceFileChanges().

const sandbox = await createBashTool({
  sandbox: backend,
  onAfterBashCall: ({ command }) =>
    command.startsWith('sql ')
      ? { meta: { formattedSql: 'SELECT 1' } }
      : undefined,
});

meta is preserved in the raw tool result for host-side consumers and stripped from model-visible output by the toolkit's shared toolset projection.

Custom Errors: BashException

Subclass BashException when you want a command or transform hook to fail with an explicit CommandResult shape.

import { BashException, type CommandResult } from '@deepagents/context';

class RateLimitError extends BashException {
  constructor(private readonly retryAfterMs: number) {
    super(`rate limited; retry in ${retryAfterMs}ms`);
  }

  format(): CommandResult {
    return {
      stdout: '',
      stderr: `${this.message}\n`,
      exitCode: 1,
    };
  }
}

createBashTool() catches BashException instances around sandbox execution and returns format() to the caller. Other error types still propagate normally.

Debug Logging

Gate bash logging behind your own environment variable when you construct the tool:

DEBUG_BASH=1 node scripts/run-demo.ts
# [bash] sql run main "SELECT 1"
# [bash] exit 1

The env var is gate-only — not a magic config:

const debug = Boolean(process.env.DEBUG_BASH);

await createBashTool({
  sandbox: bashInstance,
  onBeforeBashCall: ({ command }) => {
    if (debug) console.log(`[bash] ${command}`);
    return { command };
  },
  onAfterBashCall: ({ result }) => {
    if (debug && result.exitCode !== 0) {
      console.log(`[bash] exit ${result.exitCode}`);
    }
    return { result };
  },
});

Next Steps

On this page