Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Microsandbox sandbox

Execute OCI images in local hardware-isolated microVMs without Docker daemon coupling

The Microsandbox backend runs commands inside local hardware-isolated microVMs through the optional microsandbox SDK. It implements the same DisposableSandbox contract as the other DeepAgents backends: executeCommand, spawn, readFile, writeFiles, and dispose.

Use it when you want standard OCI images and real process streaming, but do not want your app coupled to a Docker daemon. The backend boots a microVM from an image, creates the working directory, and returns a sandbox that can plug directly into createBashTool.

Microsandbox is an optional peer dependency. It requires Node.js 22+ and hardware virtualization: Apple silicon, Linux with KVM, or Windows with WHP.

Installation

npm install microsandbox@^0.7.8

If the package or runtime is unavailable, createMicrosandboxSandbox() throws MicrosandboxNotAvailableError. This includes an incomplete runtime installation.

Basic usage

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

await using sandbox = await createMicrosandboxSandbox();

const result = await sandbox.executeCommand('echo "Hello from Microsandbox"');
console.log(result.stdout);

The default image is bash:5.3-alpine3.24. The default working directory is /workspace, and the factory creates it during boot because common images do not ship that directory.

AI agent integration

import {
  MICROSANDBOX_DEFAULT_DESTINATION,
  createBashTool,
  createMicrosandboxSandbox,
} from '@deepagents/context';

const backend = await createMicrosandboxSandbox({
  name: 'chat-123',
  configure: (builder) => builder.image('node:lts-bookworm-slim'),
});

const sandbox = await createBashTool({
  sandbox: backend,
  workingDirectory: MICROSANDBOX_DEFAULT_DESTINATION,
});

Pass the sandbox.tools object to an agent the same way you would with Docker, Apple Container, Daytona, or a virtual sandbox.

Named sandboxes

Pass name when a chat or workflow should resume the same microVM:

const sandbox = await createMicrosandboxSandbox({
  name: 'analytics-session',
  configure: (builder) => builder.image('python:3.12-slim').memory(2048),
});

Named creation uses get-or-create semantics:

  • If a sandbox with that name is running, the factory connects to it.
  • If it exists but is stopped, the factory starts it and preserves rootfs state.
  • If it does not exist, the factory creates it.

dispose() stops a named sandbox but does not remove it, so the next createMicrosandboxSandbox({ name }) can resume it. For a fresh named sandbox, pass replace: true.

Unnamed sandboxes are ephemeral: the factory generates a name, and dispose() stops and removes the microVM.

Options

OptionTypeDescription
namestringStable sandbox name for get-or-create reuse
workdirstringDefault working directory; defaults to /workspace
replacebooleanReplace an existing named sandbox instead of attaching
commandTimeoutnumberPer-command timeout for executeCommand, in milliseconds
readiness(sandbox) => void | Promise<void>Hook that must pass before the factory returns
configure(builder) => builderFluent access to the SDK builder: image (defaults to bash:5.3-alpine3.24), cpus, memory, env, volumes, network policy, secrets, user, idle timeout

The options are the inputs the factory itself reads. Everything else is set fluently on the SDK builder through configure, which runs on every connect; the SDK applies builder options only when creating, so an existing named sandbox keeps its configuration.

The factory applies workdir, the persistence implied by name, and the Bash shell after configure. A named sandbox therefore stays persistent even if configure calls ephemeral(true), and the factory's workdir wins over one set in configure.

Command execution APIs

executeCommand is buffered and accepts an optional AbortSignal:

const controller = new AbortController();
const pending = sandbox.executeCommand('sleep 30', {
  signal: controller.signal,
});
setTimeout(() => controller.abort(), 1_000);
await pending;

For live byte streams, use spawn:

const proc = await sandbox.spawn?.('node long-task.js', { cwd: '/workspace' });
if (!proc) throw new Error('spawn is unavailable');

const decoder = new TextDecoder();
for await (const chunk of proc.stdout) {
  process.stdout.write(decoder.decode(chunk, { stream: true }));
}
const exit = await proc.exit;

executeCommand lowers onto the same streaming path as spawn, so cancellation kills the guest process instead of abandoning it.

Readiness hooks

Use readiness when the image needs a daemon or service started before the agent gets the sandbox:

const sandbox = await createMicrosandboxSandbox({
  name: 'daemon-backed-chat',
  configure: (builder) => builder.image('my-daemon-image:0.1.0'),
  readiness: async (sandbox) => {
    const result = await sandbox.executeCommand('start-daemon && wait-ready');
    if (result.exitCode !== 0) throw new Error(result.stderr);
  },
});

If readiness throws, the factory disposes the sandbox and rethrows the error.

Image cache demo

The Text2SQL demo shows the local image-cache flow:

node demo/microsandbox-text2sql/bootstrap.ts
node demo/microsandbox-text2sql/demo-microsandbox.ts

bootstrap.ts exports the shared daemon image from Buildx as an OCI archive and loads it directly into the local Microsandbox cache with the SDK's Image.load(). The demo then uses createMicrosandboxSandbox({ configure }) with image(...) and pullPolicy('never') so a missing local image fails clearly instead of trying a registry pull.

Error handling

All package-owned errors extend MicrosandboxSandboxError:

ErrorWhen
MicrosandboxNotAvailableErrorThe optional dependency is unavailable, or the runtime is missing or incomplete
MicrosandboxCreationErrorSandbox creation fails outside a known SDK error
MicrosandboxCommandErrorFile reads or writes through the VM filesystem fail

The adapter wraps the SDK's LibkrunfwNotFoundError, RuntimeNotInstalledError, and RuntimeIncompleteError in MicrosandboxNotAvailableError, retaining the SDK error in cause. Other MicrosandboxError subclasses pass through unchanged, so callers can handle those SDK failure classes directly.

Next steps

On this page