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).
| Factory | Import | Peer dep |
|---|---|---|
LocalSandbox | noumen/local | @anthropic-ai/sandbox-runtime (bundled) |
UnsandboxedLocal | noumen/unsandboxed | — |
SpritesSandbox | noumen/sprites | — (uses global fetch) |
DockerSandbox | noumen/docker | dockerode |
E2BSandbox | noumen/e2b | e2b |
FreestyleSandbox | noumen/freestyle | freestyle-sandboxes |
SshSandbox | noumen/ssh | ssh2 |
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-runtimeimport { 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
| Option | Type | Default | Description |
|---|---|---|---|
cwd | string | process.cwd() | Working directory for file operations and commands |
defaultTimeout | number | 30000 | Command timeout in milliseconds |
sandbox.filesystem.allowWrite | string[] | [cwd] | Paths the agent may write to |
sandbox.filesystem.denyWrite | string[] | [] | Paths to deny within allowed write regions |
sandbox.filesystem.denyRead | string[] | [] | Paths to deny reading |
sandbox.filesystem.allowRead | string[] | [] | Paths to re-allow within denied read regions |
sandbox.network.allowedDomains | string[] | [] | Domains the agent may reach |
sandbox.network.deniedDomains | string[] | [] | 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
| Option | Type | Default | Description |
|---|---|---|---|
cwd | string | — | Working directory for file operations and commands |
defaultTimeout | number | 30000 | Command 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
| Option | Type | Default | Description |
|---|---|---|---|
token | string | — | sprites.dev API token (required) |
spriteName | string | — | Attach to this sprite. When omitted, auto-created on init() |
baseURL | string | https://api.sprites.dev | API base URL |
workingDir | string | /home/sprite | Working directory inside the container |
namePrefix | string | noumen- | 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 dockerodeAuto-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
| Option | Type | Default | Description |
|---|---|---|---|
container | Docker.Container | — | Attach to this container. When omitted, auto-created from image |
image | string | — | Docker image for auto-creation (required when container is omitted) |
cmd | string[] | ["sleep", "infinity"] | Command for auto-created container |
env | string[] | — | Environment variables for auto-created container |
dockerOptions | Record | — | Extra options passed to createContainer |
cwd | string | / | Working directory inside the container |
defaultTimeout | number | 30000 | Command timeout in milliseconds |
E2BSandbox — cloud sandbox
Run the agent inside an E2B cloud sandbox. Requires e2b as an optional peer dependency:
pnpm add e2bAuto-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
| Option | Type | Default | Description |
|---|---|---|---|
sandbox | E2B.Sandbox | — | Attach to this instance. When omitted, auto-created from template |
template | string | base | E2B template for auto-creation |
apiKey | string | $E2B_API_KEY | E2B API key (falls back to env var) |
timeoutMs | number | E2B default | Timeout for the auto-created sandbox |
cwd | string | — | Working directory inside the sandbox |
defaultTimeout | number | 30000 | Command 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-sandboxesAuto-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
| Option | Type | Default | Description |
|---|---|---|---|
vm | FreestyleVmInstance | — | Attach to this VM. When omitted, auto-created |
apiKey | string | $FREESTYLE_API_KEY | Freestyle API key (falls back to env var) |
snapshotId | string | — | Create VM from this snapshot |
spec | VmSpec | — | Configuration spec for auto-creation |
idleTimeoutSeconds | number | 600 | Auto-suspend after this many seconds of inactivity |
cwd | string | — | Working directory inside the VM |
defaultTimeout | number | 30000 | Command timeout in milliseconds |
additionalFiles | Record<string, {content, encoding?}> | — | Files to provision at creation time |
gitRepos | Array<{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 ssh2Auto-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
| Option | Type | Default | Description |
|---|---|---|---|
client | ssh2.Client | — | Attach to this client. When omitted, auto-connected from host |
host | string | — | SSH hostname (required when client is omitted) |
port | number | 22 | SSH port |
username | string | "root" | SSH username |
password | string | — | Password for password-based auth |
privateKey | string | Buffer | — | PEM-encoded private key for key-based auth |
passphrase | string | — | Passphrase for encrypted private keys |
cwd | string | / | Working directory on the remote host |
defaultTimeout | number | 30000 | Command 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:
-
Provisioning — the first call to
Agent.createThread()triggerssandbox.init(). For auto-create factories this provisions the remote resource (creates a sprite, starts a Docker container, or creates an E2B sandbox). -
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. -
Reconnection — when you call
agent.resumeThread(sessionId), the agent reads the local index, finds the stored sandbox ID, and passes it tosandbox.init(storedId)so the sandbox reconnects to the same resource instead of creating a new one. -
Disposal —
Agent.close()callssandbox.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. -
Idempotency —
init()uses a single-flight promise. CallingcreateThread()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.