Virtual Infrastructure (Sandboxing)

Sandbox factories bundle a filesystem and shell into a single object — the isolation boundary between the agent and the outside world.

A Sandbox bundles a VirtualFs (filesystem) and VirtualComputer (shell execution) into one object. Every file read/write and shell command the agent executes goes through these interfaces — swap the sandbox to control what the agent can access.

interface Sandbox {
  fs: VirtualFs;
  computer: VirtualComputer;
  dispose?(): Promise<void>;
  init?(sandboxId?: string): Promise<void>;
  sandboxId?(): string | undefined;
}

noumen ships seven sandbox factories. The five remote backends (Sprites, Docker, E2B, Freestyle, SSH) support auto-creation — omit the container/instance ID and the resource is provisioned on first use, persisted across sessions, and disposed automatically. You can also implement your own.

Import paths

sandbox is required on Agent and every preset. Every sandbox factory lives on its own subpath so its optional peer dependency (e.g. dockerode, e2b, ssh2) only enters the module graph when you opt in. import { Agent } from "noumen" stays lean regardless of which backends are installed — even the local adapters are opt-in, so node:child_process and node:fs/promises don't show up in the static import graph of apps that only use a remote sandbox (critical for Next.js NFT and serverless-webpack dependency tracing).

FactoryImportPeer dep
LocalSandboxnoumen/local@anthropic-ai/sandbox-runtime (bundled)
UnsandboxedLocalnoumen/unsandboxed
SpritesSandboxnoumen/sprites— (uses global fetch)
DockerSandboxnoumen/dockerdockerode
E2BSandboxnoumen/e2be2b
FreestyleSandboxnoumen/freestylefreestyle-sandboxes
SshSandboxnoumen/sshssh2

LocalAgent and UnsandboxedAgent shortcuts

For the two local backends, the same subpath also exports a one-step factory that bundles Agent construction with the sandbox. These accept the full AgentOptions shape minus sandbox:

import { LocalAgent } from "noumen/local";
import { UnsandboxedAgent } from "noumen/unsandboxed";

// Equivalent to new Agent({ ..., sandbox: LocalSandbox({ cwd }) })
const sandboxed = LocalAgent({ provider: "anthropic", cwd: "/my/project" });

// Equivalent to new Agent({ ..., sandbox: UnsandboxedLocal({ cwd }) })
const raw = UnsandboxedAgent({ provider: "anthropic", cwd: "/my/project" });

Tune the sandbox via an optional nested block (localSandbox / unsandboxed):

const agent = LocalAgent({
  provider: "anthropic",
  cwd: "/my/project",
  localSandbox: {
    sandbox: {
      filesystem: { denyRead: ["/etc/shadow"] },
      network: { allowedDomains: ["api.openai.com"] },
    },
  },
});

Remote backends stay on the explicit new Agent({ provider, sandbox }) form — there's no DockerAgent shortcut because remote sandboxes carry config (tokens, templates, connection state) that's clearer at the call site.

LocalSandbox — OS-level sandboxing

Uses @anthropic-ai/sandbox-runtime to wrap every shell command with OS-level restrictions. On macOS this uses Seatbelt (sandbox-exec); on Linux it uses bubblewrap (bwrap) + socat.

pnpm add @anthropic-ai/sandbox-runtime
import { LocalSandbox } from "noumen/local";

const sandbox = LocalSandbox({ cwd: "/my/project" });

Filesystem operations (VirtualFs) use the host node:fs — the sandbox boundary is enforced on shell commands (VirtualComputer), which is where the agent executes arbitrary code.

Customizing restrictions

const sandbox = LocalSandbox({
  cwd: "/my/project",
  sandbox: {
    filesystem: {
      allowWrite: ["/my/project", "/tmp"],
      denyRead: ["/etc/shadow", "~/.ssh"],
    },
    network: {
      allowedDomains: ["api.openai.com", "registry.npmjs.org"],
    },
  },
});

Defaults: writes allowed only in cwd, reads allowed everywhere, network unrestricted.

Options

OptionTypeDefaultDescription
cwdstringprocess.cwd()Working directory for file operations and commands
defaultTimeoutnumber30000Command timeout in milliseconds
sandbox.filesystem.allowWritestring[][cwd]Paths the agent may write to
sandbox.filesystem.denyWritestring[][]Paths to deny within allowed write regions
sandbox.filesystem.denyReadstring[][]Paths to deny reading
sandbox.filesystem.allowReadstring[][]Paths to re-allow within denied read regions
sandbox.network.allowedDomainsstring[][]Domains the agent may reach
sandbox.network.deniedDomainsstring[][]Domains to block

Platform requirements

macOS: No extra dependencies — Seatbelt is built in. Linux: Requires bubblewrap and socat (apt install bubblewrap socat). Windows: Not supported natively — use WSL2.

UnsandboxedLocal — no isolation

Backed by node:fs/promises and node:child_process with no OS-level restrictions. The agent can access anything the host process can.

import { UnsandboxedLocal } from "noumen/unsandboxed";

const sandbox = UnsandboxedLocal({ cwd: "/my/project" });

No isolation

UnsandboxedLocal provides no sandboxing. Use it only for development or environments where you fully trust the agent. For production use, prefer LocalSandbox.

Options

OptionTypeDefaultDescription
cwdstringWorking directory for file operations and commands
defaultTimeoutnumber30000Command timeout in milliseconds

SpritesSandbox — remote containers

Run inside a remote sprites.dev container. The agent has no access to the host machine.

Auto-create — omit spriteName and the sprite is provisioned on first use:

import { SpritesSandbox } from "noumen/sprites";

const sandbox = SpritesSandbox({ token: process.env.SPRITE_TOKEN! });
const agent = new Agent({ provider, sandbox });

// Sprite created automatically on first createThread(). Cleaned up by:
await agent.close();

Explicit — attach to a pre-existing sprite (lifecycle is yours):

const sandbox = SpritesSandbox({
  token: process.env.SPRITE_TOKEN!,
  spriteName: "my-sprite",
});

Options

OptionTypeDefaultDescription
tokenstringsprites.dev API token (required)
spriteNamestringAttach to this sprite. When omitted, auto-created on init()
baseURLstringhttps://api.sprites.devAPI base URL
workingDirstring/home/spriteWorking directory inside the container
namePrefixstringnoumen-Prefix for auto-generated sprite names

DockerSandbox — container isolation

Run the agent inside a Docker container. Requires dockerode as an optional peer dependency:

pnpm add dockerode

Auto-create — pass image and the container is created on first use:

import { DockerSandbox } from "noumen/docker";

const sandbox = DockerSandbox({ image: "node:22", cwd: "/workspace" });
const agent = new Agent({ provider, sandbox });

// Container created and started on first createThread(). Cleaned up by:
await agent.close();

Explicit — pass a pre-existing dockerode Container (lifecycle is yours):

import Docker from "dockerode";
import { DockerSandbox } from "noumen/docker";

const docker = new Docker();
const container = await docker.createContainer({
  Image: "node:22",
  Cmd: ["sleep", "infinity"],
  Tty: false,
});
await container.start();

const sandbox = DockerSandbox({ container, cwd: "/workspace" });
const agent = new Agent({ provider, sandbox });

await container.stop();
await container.remove();

Options

OptionTypeDefaultDescription
containerDocker.ContainerAttach to this container. When omitted, auto-created from image
imagestringDocker image for auto-creation (required when container is omitted)
cmdstring[]["sleep", "infinity"]Command for auto-created container
envstring[]Environment variables for auto-created container
dockerOptionsRecordExtra options passed to createContainer
cwdstring/Working directory inside the container
defaultTimeoutnumber30000Command timeout in milliseconds

E2BSandbox — cloud sandbox

Run the agent inside an E2B cloud sandbox. Requires e2b as an optional peer dependency:

pnpm add e2b

Auto-create — omit sandbox and the E2B sandbox is provisioned on first use:

import { E2BSandbox } from "noumen/e2b";

const sandbox = E2BSandbox({ template: "base" });
const agent = new Agent({ provider, sandbox });

// E2B sandbox created on first createThread(). Cleaned up by:
await agent.close();

Explicit — pass a pre-existing Sandbox instance (lifecycle is yours):

import { Sandbox as E2BSandboxSDK } from "e2b";
import { E2BSandbox } from "noumen/e2b";

const e2b = await E2BSandboxSDK.create();
const sandbox = E2BSandbox({ sandbox: e2b, cwd: "/home/user" });
const agent = new Agent({ provider, sandbox });

await e2b.close();

Options

OptionTypeDefaultDescription
sandboxE2B.SandboxAttach to this instance. When omitted, auto-created from template
templatestringbaseE2B template for auto-creation
apiKeystring$E2B_API_KEYE2B API key (falls back to env var)
timeoutMsnumberE2B defaultTimeout for the auto-created sandbox
cwdstringWorking directory inside the sandbox
defaultTimeoutnumber30000Command timeout in milliseconds

FreestyleSandbox — cloud VMs

Run the agent inside a Freestyle VM — full Linux machines with sub-second startup, instant pause/resume, and optional forking. Requires freestyle-sandboxes as an optional peer dependency:

pnpm add freestyle-sandboxes

Auto-create — omit vm and a Freestyle VM is provisioned on first use:

import { FreestyleSandbox } from "noumen/freestyle";

const sandbox = FreestyleSandbox({ cwd: "/workspace" });
const agent = new Agent({ provider, sandbox });

// VM suspended on close — resumes instantly on next session:
await agent.close();

From a snapshot — start from a pre-cached environment for fast startup:

const sandbox = FreestyleSandbox({
  snapshotId: "abc123",
  cwd: "/workspace",
});

Explicit — pass a pre-existing VM instance (lifecycle is yours):

import { freestyle } from "freestyle-sandboxes";
import { FreestyleSandbox } from "noumen/freestyle";

const { vm } = await freestyle.vms.create({ workdir: "/workspace" });
const sandbox = FreestyleSandbox({ vm, cwd: "/workspace" });
const agent = new Agent({ provider, sandbox });

Options

OptionTypeDefaultDescription
vmFreestyleVmInstanceAttach to this VM. When omitted, auto-created
apiKeystring$FREESTYLE_API_KEYFreestyle API key (falls back to env var)
snapshotIdstringCreate VM from this snapshot
specVmSpecConfiguration spec for auto-creation
idleTimeoutSecondsnumber600Auto-suspend after this many seconds of inactivity
cwdstringWorking directory inside the VM
defaultTimeoutnumber30000Command timeout in milliseconds
additionalFilesRecord<string, {content, encoding?}>Files to provision at creation time
gitReposArray<{repo, path, rev?}>Git repos to clone at creation time
disposeStrategy"suspend" | "delete""suspend"What to do on dispose() — suspend preserves memory state

Suspend vs Delete

By default, dispose() suspends the VM rather than deleting it. Suspended VMs preserve full memory state and resume in under 100ms — no reboot, no lost state. Storage-only billing applies while suspended. Set disposeStrategy: "delete" for full cleanup.

SshSandbox — remote SSH hosts

Connect to any remote machine over SSH. Uses ssh2 for command execution (exec channels) and file I/O (SFTP):

pnpm add ssh2

Auto-connect — provide credentials and the connection is established lazily on init():

import { Agent } from "noumen";
import { SshSandbox } from "noumen/ssh";
import fs from "node:fs";

const sandbox = SshSandbox({
  host: "dev.example.com",
  username: "deploy",
  privateKey: fs.readFileSync("/home/deploy/.ssh/id_ed25519"),
  cwd: "/home/deploy/project",
});
const agent = new Agent({ provider, sandbox });

// SSH connection closed on dispose:
await agent.close();

Password auth is also supported — pass password instead of privateKey.

Explicit — pass a pre-connected ssh2 Client (lifecycle is yours):

import { Client } from "ssh2";
import { SshSandbox } from "noumen/ssh";

const client = new Client();
await new Promise<void>((resolve) => {
  client.on("ready", resolve);
  client.connect({ host: "10.0.0.5", username: "root", privateKey: key });
});

const sandbox = SshSandbox({ client, cwd: "/workspace" });
const agent = new Agent({ provider, sandbox });

client.end();

Options

OptionTypeDefaultDescription
clientssh2.ClientAttach to this client. When omitted, auto-connected from host
hoststringSSH hostname (required when client is omitted)
portnumber22SSH port
usernamestring"root"SSH username
passwordstringPassword for password-based auth
privateKeystring | BufferPEM-encoded private key for key-based auth
passphrasestringPassphrase for encrypted private keys
cwdstring/Working directory on the remote host
defaultTimeoutnumber30000Command timeout in milliseconds

Sandbox auto-creation lifecycle

All five remote backends support on-demand provisioning when you omit the explicit resource handle. Here is how the lifecycle works:

  1. Provisioning — the first call to Agent.createThread() triggers sandbox.init(). For auto-create factories this provisions the remote resource (creates a sprite, starts a Docker container, or creates an E2B sandbox).

  2. Session binding — the sandbox identifier is written to a local index file (.noumen/sessions/.sandbox-index.json) keyed by session ID. This mapping lives on the host filesystem so it is always readable, even when the sandbox itself is remote.

  3. Reconnection — when you call agent.resumeThread(sessionId), the agent reads the local index, finds the stored sandbox ID, and passes it to sandbox.init(storedId) so the sandbox reconnects to the same resource instead of creating a new one.

  4. DisposalAgent.close() calls sandbox.dispose(). For auto-created resources this tears down the remote resource (deletes the sprite, stops and removes the Docker container, kills the E2B sandbox, or suspends the Freestyle VM). Resources created by the user (explicit IDs) are never torn down.

  5. Idempotencyinit() uses a single-flight promise. Calling createThread() multiple times reuses the same provisioned resource.

Single-agent, single-sandbox

Each Agent instance has one sandbox shared across all threads. For multi-tenant deployments where each user needs isolated containers, create a separate Agent per user.

Custom sandboxes

Implement VirtualFs and VirtualComputer to target any execution environment — Daytona, cloud VMs, or an in-memory test harness. A custom Sandbox is any object with { fs, computer }:

import type { Sandbox, VirtualFs, VirtualComputer, CommandResult, ExecOptions } from "noumen";

class MyComputer implements VirtualComputer {
  async executeCommand(command: string, opts?: ExecOptions): Promise<CommandResult> {
    // your implementation
  }
}

const sandbox: Sandbox = {
  fs: new MyCustomFs(),
  computer: new MyComputer(),

  // Optional: enable auto-creation and session-aware lifecycle
  async init(sandboxId?: string) {
    // Create a new resource or reconnect to sandboxId
  },
  sandboxId() {
    return "my-resource-id"; // persisted in session metadata
  },
  async dispose() {
    // Tear down auto-created resources
  },
};

The interfaces are intentionally minimal (one method for shell, eight for filesystem) so adapters are straightforward to write. The optional init(), sandboxId(), and dispose() methods enable auto-creation and session-aware lifecycle management.

VirtualFs interface

The filesystem interface that all file tools (ReadFile, WriteFile, EditFile) delegate to:

interface VirtualFs {
  readFile(path: string, opts?: ReadOptions): Promise<string>;
  writeFile(path: string, content: string): Promise<void>;
  appendFile(path: string, content: string): Promise<void>;
  deleteFile(path: string, opts?: { recursive?: boolean }): Promise<void>;
  mkdir(path: string, opts?: { recursive?: boolean }): Promise<void>;
  readdir(path: string, opts?: { recursive?: boolean }): Promise<FileEntry[]>;
  exists(path: string): Promise<boolean>;
  stat(path: string): Promise<FileStat>;
}

VirtualComputer interface

The shell execution interface that all command tools (Bash, Glob, Grep) delegate to:

interface VirtualComputer {
  executeCommand(command: string, opts?: ExecOptions): Promise<CommandResult>;
}

Where ExecOptions supports cwd, timeout, and env, and CommandResult returns exitCode, stdout, and stderr.