/** * E2B Sandbox Provider * * A simplified E2B sandbox implementation that supports mounting * cloud filesystems (S3, GCS, R2) via FUSE. * * @see https://e2b.dev/docs */ import type { RequestContext } from '@mastra/core/di'; import type { SandboxInfo, WorkspaceFilesystem, MountResult, ProviderStatus, MountManager, MastraSandboxOptions, SandboxFileInput, SandboxNetworking, SandboxCloneOptions } from '@mastra/core/workspace'; /** * Inlined from `@mastra/core/workspace` to avoid requiring a newer core peer dep. */ type InstructionsOption = string | ((opts: { defaultInstructions: string; requestContext?: RequestContext; }) => string); import { MastraSandbox } from '@mastra/core/workspace'; import { Sandbox } from 'e2b'; import type { SandboxConnectOpts, SandboxLifecycle, SandboxNetworkOpts, SandboxOpts } from 'e2b'; import type { TemplateSpec } from '../utils/template.js'; import { E2BProcessManager } from './process-manager.js'; /** * E2B sandbox provider configuration. */ export interface E2BSandboxOptions extends Omit { /** Unique identifier for this sandbox instance */ id?: string; /** * Persisted E2B provider sandbox ID to reattach to deterministically. * * When set, `start()` first queries this exact sandbox and connects to it * (resuming it if paused) instead of discovering by logical `id` metadata. * Only a typed "sandbox gone" error (not found / killed / not running) * falls through to the usual logical-id lookup and create ladder; * auth, quota, rate-limit, timeout, and network errors propagate without * creating a new sandbox. */ sandboxId?: string; /** * Sandbox template specification. * * - `string` - Use an existing template by ID * - `TemplateBuilder` - Use a custom template (e.g., from `createMountableTemplate()`) * - `(base) => base.aptInstall([...])` - Customize the default mountable template * * If not provided and mounting is used, a default template with s3fs will be built. * For best performance, pre-build your template and use the template ID. * * @see createDefaultMountableTemplate */ template?: TemplateSpec; /** Execution timeout in milliseconds * * @default 300_000 // 5 minutes */ timeout?: number; /** Environment variables to set in the sandbox */ env?: Record; /** Custom metadata */ metadata?: Record; /** Network configuration to use when creating the E2B sandbox */ network?: SandboxNetworkOpts; /** * Sandbox lifecycle behavior when the `timeout` is reached. * * Defaults to `{ onTimeout: 'pause' }`, which snapshots the sandbox so the * next `start()` reconnects and resumes it. Pass `{ onTimeout: 'kill' }` for * stateless workspaces whose data lives outside the sandbox (e.g. mounted * from S3) — idle sandboxes are then destroyed and recreated on next use * instead of retained as paused snapshots. * * Note: an explicit `stop()` always pauses, regardless of this setting. */ lifecycle?: SandboxLifecycle; /** Domain for self-hosted E2B. Falls back to E2B_DOMAIN env var. */ domain?: string; /** API URL for self-hosted E2B. Falls back to E2B_API_URL env var. */ apiUrl?: string; /** API key for authentication. Falls back to E2B_API_KEY env var. */ apiKey?: string; /** Access token for authentication. Falls back to E2B_ACCESS_TOKEN env var. */ accessToken?: string; /** * Custom instructions that override the default instructions * returned by `getInstructions()`. * * - `string` — Fully replaces the default instructions. * Pass an empty string to suppress instructions entirely. * - `(opts) => string` — Receives the default instructions and * optional request context so you can extend or customise per-request. */ instructions?: InstructionsOption; } /** * Simplified E2B sandbox implementation. * * Features: * - Single sandbox instance lifecycle * - Supports mounting cloud filesystems (S3, GCS, R2) via FUSE * - Automatic sandbox timeout handling with retry * * @example Basic usage * ```typescript * import { Workspace } from '@mastra/core/workspace'; * import { E2BSandbox } from '@mastra/e2b'; * * const sandbox = new E2BSandbox({ * timeout: 60000, * }); * * const workspace = new Workspace({ sandbox }); * const result = await workspace.executeCode('console.log("Hello!")'); * ``` * * @example With S3 filesystem mounting * ```typescript * import { Workspace } from '@mastra/core/workspace'; * import { E2BSandbox } from '@mastra/e2b'; * import { S3Filesystem } from '@mastra/s3'; * * const workspace = new Workspace({ * mounts: { * '/bucket': new S3Filesystem({ * bucket: 'my-bucket', * region: 'us-east-1', * }), * }, * sandbox: new E2BSandbox({ timeout: 60000 }), * }); * * ``` */ export declare class E2BSandbox extends MastraSandbox { readonly id: string; readonly name: string; readonly provider: string; status: ProviderStatus; readonly mounts: MountManager; readonly processes: E2BProcessManager; /** * Networking capability: public HTTPS URLs for sandbox ports. * E2B exposes every port via `getHost(port)` — no upfront declaration needed. * * When not attached in this process, the URL is resolved by looking up the * existing sandbox by identity (without resuming it) and deriving the host * (`{port}-{sandboxId}.{domain}`), so other processes can resolve * deployments without waking a paused sandbox. */ readonly networking: SandboxNetworking; protected _sandbox: Sandbox | null; private _createdAt; private _isRetrying; private readonly timeout; protected readonly templateSpec?: TemplateSpec; private readonly metadata; private readonly network?; private readonly lifecycle; protected readonly connectionOpts: Record; private readonly _preferredSandboxId?; private readonly _instructionsOverride?; private readonly _constructorOptions; /** * Resolved template ID after building (if needed). The single cache for * template resolution: `resolveTemplate()` returns it when set, and the * create-time fallback ladder rewrites it to whichever template actually * produced a sandbox. * * `protected` so a subclass with its own default template (e.g. desktop * sandboxes) shares the same cache when it overrides `resolveTemplate()`. */ protected _resolvedTemplateId?: string; /** * The named spec a deferred template spec resolved to — kept so the * 404-on-create fallback ladder can walk the same name/fallback rungs it * would for a plain named spec. */ private _resolvedNamedSpec?; constructor(options?: E2BSandboxOptions); /** * Construct a sibling `E2BSandbox` that inherits this sandbox's * configuration (credentials, template, network, metadata, instructions) * with per-instance overrides. * * Performs no I/O — the sandbox clone provisions (or reconnects to an * existing E2B sandbox with the same logical `id`) on its own `start()`. * Use it when one configured sandbox acts as the template for a fleet of * independent sandboxes (e.g. one per project). * * `options.idleTimeoutMinutes` maps to the E2B sandbox `timeout` (ms); * `options.sandboxId` reattaches the clone to that exact E2B sandbox on * `start()`. The parent's own preferred provider sandbox ID is never * inherited — physical identity is per-instance. */ clone(options?: SandboxCloneOptions): E2BSandbox; /** * Get the underlying E2B Sandbox instance for direct access to E2B APIs. * * Use this when you need to access E2B features not exposed through the * WorkspaceSandbox interface (e.g., files API, ports, etc.). * * @throws {SandboxNotReadyError} If the sandbox has not been started * * @example Direct file operations * ```typescript * const e2b = sandbox.e2b; * await e2b.files.write('/tmp/test.txt', 'Hello'); * const content = await e2b.files.read('/tmp/test.txt'); * const files = await e2b.files.list('/tmp'); * ``` * * @example Access ports * ```typescript * const e2b = sandbox.e2b; * const url = e2b.getHost(3000); * ``` */ get e2b(): Sandbox; /** * The E2B provider sandbox ID resolved after connect or create. * * Persist this to reattach deterministically later via the `sandboxId` * option (or `clone({ sandboxId })`). Undefined until the sandbox has been * started (attached) in this process. */ get sandboxId(): string | undefined; /** * Acquisition primitives (base-orchestrated start): the base derives * `outcome: 'created'` only when a brand-new sandbox VM was created; * reconnecting (including resuming a paused sandbox) is `outcome: 'connected'`. * * `find` returns an already-connected E2B handle: `Sandbox.connect` * resumes paused sandboxes, and its failures are deliberately swallowed * (unusable handle → create fresh) — that forgiveness is this provider's * policy, so it lives here rather than in `connect`. The exception is the * `sandboxId` reattach inside {@link acquireExistingSandbox}, which is * fail-closed: only a "sandbox gone" error falls through to discovery. */ protected find(): Promise; protected connect(existingSandbox: Sandbox): Promise; protected create(): Promise; /** * Stop the E2B sandbox by pausing it (snapshot-stop). * * Pausing freezes the whole VM — filesystem, memory, and running processes — * and stops billing immediately. The next `start()` reconnects and resumes it, * with background processes still running. Filesystem mounts are unmounted * first (FUSE mounts don't survive pause) and reconciled again on start. * * Status management is handled by the base class. */ stop(): Promise; /** * Destroy the E2B sandbox and clean up all resources. * Unmounts filesystems, kills the sandbox, and clears mount state. * Status management is handled by the base class. */ destroy(): Promise; getInfo(): Promise; /** * Bulk-write files into the sandbox filesystem via the SDK's native upload. * * Per-file permission modes are not supported; an explicit `mode` is * rejected rather than silently discarded. */ writeFiles(files: SandboxFileInput[]): Promise; /** * Get instructions describing this E2B sandbox. * Used by agents to understand the execution environment. */ getInstructions(opts?: { requestContext?: RequestContext; }): string; private _getDefaultInstructions; /** * Mount a filesystem at a path in the sandbox. * Uses FUSE tools (s3fs, gcsfuse) to mount cloud storage. */ mount(filesystem: WorkspaceFilesystem, mountPath: string): Promise; /** * Unmount a filesystem from a path in the sandbox. */ unmount(mountPath: string): Promise; /** * Unmount all stale mounts that are not in the expected mounts list. * Also cleans up orphaned directories and marker files from failed mount attempts. * Call this after reconnecting to an existing sandbox to clean up old mounts. */ reconcileMounts(expectedMountPaths: string[]): Promise; /** @deprecated Use `e2b` instead. */ get instance(): Sandbox; /** @deprecated Use `status === 'running'` instead. */ isReady(): Promise; private generateId; /** Domain used to derive public sandbox hosts (self-hosted E2B or e2b.app). */ private get sandboxDomain(); /** * Look up an existing sandbox with matching mastra-sandbox-id metadata * WITHOUT connecting or resuming it. Returns its list info or null. */ private lookupExistingSandboxInfo; /** * Acquire an existing sandbox: try the preferred provider sandbox ID first * (deterministic reattach), then fall back to logical-id metadata discovery. */ private acquireExistingSandbox; /** * Deterministically reattach to a sandbox by its E2B provider ID. * * Fail-closed: only a typed "sandbox gone" error (not found / killed / * not running) returns null so the caller can fall through to logical-id * discovery or creation. Any other error (auth, quota, rate limit, * timeout, network) propagates so a duplicate sandbox is never created. * * Ownership is validated before connecting: a sandbox tagged with a * different `mastra-sandbox-id` is refused (without resuming it). * Sandboxes without the tag (created outside Mastra) are attachable. */ private connectToPreferredSandbox; /** * Find an existing sandbox with matching mastra-sandbox-id metadata. * Returns the connected sandbox if found, null otherwise. * Connecting to a paused sandbox resumes it. */ private findExistingSandbox; /** * Create a new SDK sandbox from a resolved template ID. * * Override point for providers layered on the E2B SDK whose `Sandbox` * class extends `e2b`'s (e.g. `@e2b/desktop`): override to call their * `Sandbox.create`. Connection options are already spread into `opts`. */ protected createSdkSandbox(templateId: string, opts: SandboxOpts): Promise; /** * Connect to (and resume) an existing SDK sandbox by its E2B sandbox ID. * Override point — see {@link createSdkSandbox}. */ protected connectSdkSandbox(sandboxId: string, opts: SandboxConnectOpts): Promise; /** * Resolve the template specification to a template ID. * * - String: Use as-is (template ID) * - TemplateBuilder: Build and return the template ID * - Function: Apply to base mountable template, then build * - undefined: Use default mountable template (cached) * * Override point: subclasses with a different default template (e.g. * desktop sandboxes) override this and {@link buildDefaultTemplate}. */ protected resolveTemplate(): Promise; /** * Resolve the default mountable template: reuse when it exists, build once * when it does not. */ /** * Resolve a named spec's fallback template. A named fallback gets its own * exists-then-build resolution; anything failing past that (including * specs without a fallback, e.g. repo templates) lands on the default * mountable template so a broken build never wedges a session. */ /** * Trigger a non-blocking template rebuild via `Template.buildInBackground` * (the build runs on E2B's side, so it outlives this process). Deduped * per-process by ref so concurrent session starts on the same moved head * don't stack duplicate builds; a failed TRIGGER clears the guard so a * later start retries. A build that fails server-side simply never * registers the ref — the next start falls back to the stale build again * and re-triggers. */ private triggerBackgroundBuild; private resolveFallbackTemplate; /** * Resources the configured template asked for. The default mountable * template honors them too, so a repo template that falls back never * silently downgrades the machine — a 2 GB session's setup would OOM in * the 1 GB default. Per-size default templates cost one extra build each. */ private requestedBuildResources; private buildOrReuseDefaultTemplate; /** * Build the default mountable template (bypasses exists check). * * Override point: called from the template-not-found retry path in * `start()` when no explicit template was configured. */ protected buildDefaultTemplate(): Promise; /** * Write marker file for detecting config changes on reconnect. * Stores both the mount path and config hash in the file. */ private writeMarkerFile; /** * Check if a path is already mounted and if the config matches. */ private checkExistingMount; /** * Check if an error indicates the sandbox itself is dead/gone. * Does NOT include code execution timeouts (those are the user's code taking too long). * Does NOT include "port is not open" - that needs sandbox kill, not reconnect. */ private isSandboxDeadError; /** * Handle sandbox timeout by clearing the instance and resetting state. * * Bypasses the normal stop() lifecycle because the sandbox is already dead — * we can't unmount filesystems or run cleanup commands. Instead we reset * mount states to 'pending' so they get re-mounted when start() runs again. */ private handleSandboxTimeout; /** * Execute an operation with automatic retry if the sandbox is found to be dead. * * When the E2B sandbox times out or crashes mid-operation, this method * resets sandbox state, restarts it, and retries the operation once. * * @internal Used by E2BProcessManager to handle dead sandboxes during spawn. */ retryOnDead(fn: () => Promise): Promise; } export {}; //# sourceMappingURL=index.d.ts.map