@pai/sandbox-e2b
E2B-backed SandboxProvider implementation.
Package: @pai/sandbox-e2b
Implements the SandboxProvider contract on E2B Firecracker microVMs. Each sandbox is a real Linux VM with a POSIX shell, suitable for production agent workloads.
Quick start
Bind the provider where the runtime binds storage and files:
import { createAgentRuntime } from "@pai/core";
import { sandboxCapability } from "@pai/sandbox";
import { createE2BSandboxProvider } from "@pai/sandbox-e2b";
const runtime = createAgentRuntime({
agent, // tools: { ...sandboxTools() }
scopeKey: (identity) => identity.workspaceId,
capabilities: {
sandbox: sandboxCapability({ provider: createE2BSandboxProvider() }),
},
});The provider authenticates with the E2B_API_KEY environment variable when apiKey is not passed.
Options
type E2BSandboxProviderOptions = {
/** E2B API key. Falls back to the `E2B_API_KEY` environment variable. */
apiKey?: string;
/** Default sandbox template. E2B's own default (`base`) applies when unset. */
template?: string;
/**
* Default idle lifetime for created sandboxes. E2B's own default
* (5 minutes) applies when unset.
*/
defaultTtlMs?: number;
/**
* Network policy applied when `create` receives no `network`. Defaults to
* `{ access: "none" }` — the inverse of E2B's internet-on default.
*/
defaultNetwork?: SandboxNetworkConfig;
/**
* Pause idle sandboxes instead of killing them, and resume them
* transparently on the next operation. Billing stops while paused.
* Defaults to true.
*/
autoPause?: boolean;
};Behaviour notes
- Network is off by default. Sandboxes are created with
{ access: "none" }unless the caller ordefaultNetworksays otherwise.allowlistmaps to E2BallowOutrules plus a deny-all rule; E2B can only domain-filter HTTP (port 80) and TLS SNI (port 443) traffic. - Auto-pause. With
autoPause: true(the default), idle sandboxes are paused rather than killed.connect— or any auto-resumed operation — wakes them with memory intact. WithautoPause: false, callers must reconnect throughprovider.connectafter an explicitpause(). - Timeouts and aborts are provider-enforced. A command exceeding
timeoutMsis killed and rejects withSandboxCommandTimeoutError; aborting therunCommandsignal kills the remote process before rejecting with the abort reason. - Dead sandboxes are normalized. E2B reports a reclaimed sandbox differently per API surface (404s, 502s, generic timeouts). The provider maps all of them: session operations throw
SandboxNotFoundError,connectreturns null (after one retry for E2B's known pause/resume flakiness), and backend or auth failures throwSandboxUnavailableError. keepAlive({ ttlMs })only extends. The idle deadline is pushed out to at leastttlMsfrom now and never pulled in, so a short keepalive cannot truncate a longer lifetime granted at create time.probe()lists one sandbox. That is the cheapest authenticated call E2B offers, so a status check proves the API key and the control plane without provisioning anything. A rejected key arrives asSandboxUnavailableErrorwithreason: "auth", which is not retryable.resourcesis rejected. E2B sizes sandboxes through templates, not per-create configuration; passtemplateinstead.bindingKeyis ignored. E2B has no atomic one-sandbox-per-key primitive, so replicas creating for one key at once can each get a sandbox; the session manager's metadata rediscovery still reconnects after a restart.
Conformance
createE2BSandboxProvider passes the full @pai/sandbox/test conformance suite against real E2B sandboxes. The package's conformance and live tests run only when E2B_API_KEY is set; without it they skip.
import { createSandboxProviderConformanceSuite } from "@pai/sandbox/test";
import { createE2BSandboxProvider } from "@pai/sandbox-e2b";
createSandboxProviderConformanceSuite({
name: "createE2BSandboxProvider",
createProvider: () => createE2BSandboxProvider(),
});