@mastra/e2b 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,12 +5,6 @@ var e2b = require('e2b');
5
5
  var crypto = require('crypto');
6
6
 
7
7
  // src/sandbox/index.ts
8
-
9
- // src/utils/shell-quote.ts
10
- function shellQuote(arg) {
11
- if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
12
- return "'" + arg.replace(/'/g, "'\\''") + "'";
13
- }
14
8
  var MOUNTABLE_TEMPLATE_VERSION = "v1";
15
9
  function createDefaultMountableTemplate() {
16
10
  const aptPackages = ["s3fs", "fuse"];
@@ -177,6 +171,117 @@ async function mountGCS(mountPath, config, ctx) {
177
171
  throw new Error(`Failed to mount GCS bucket: ${stderr || stdout || error}`);
178
172
  }
179
173
  }
174
+ var E2BProcessHandle = class extends workspace.ProcessHandle {
175
+ pid;
176
+ _e2bHandle;
177
+ _sandbox;
178
+ _startTime;
179
+ constructor(e2bHandle, sandbox, startTime, options) {
180
+ super(options);
181
+ this.pid = e2bHandle.pid;
182
+ this._e2bHandle = e2bHandle;
183
+ this._sandbox = sandbox;
184
+ this._startTime = startTime;
185
+ }
186
+ /** Delegates to E2B's handle so exitCode reflects server-side state without needing wait(). */
187
+ get exitCode() {
188
+ return this._e2bHandle.exitCode;
189
+ }
190
+ async wait() {
191
+ try {
192
+ const result = await this._e2bHandle.wait();
193
+ return {
194
+ success: result.exitCode === 0,
195
+ exitCode: result.exitCode,
196
+ stdout: this.stdout,
197
+ stderr: this.stderr,
198
+ executionTimeMs: Date.now() - this._startTime
199
+ };
200
+ } catch (error) {
201
+ const errorObj = error;
202
+ const exitCode = errorObj.result?.exitCode ?? errorObj.exitCode ?? this.exitCode ?? 1;
203
+ if (errorObj.result?.stdout) this.emitStdout(errorObj.result.stdout);
204
+ if (errorObj.result?.stderr) this.emitStderr(errorObj.result.stderr);
205
+ return {
206
+ success: false,
207
+ exitCode,
208
+ stdout: this.stdout,
209
+ stderr: this.stderr || (error instanceof Error ? error.message : String(error)),
210
+ executionTimeMs: Date.now() - this._startTime
211
+ };
212
+ }
213
+ }
214
+ async kill() {
215
+ if (this.exitCode !== void 0) return false;
216
+ return this._e2bHandle.kill();
217
+ }
218
+ async sendStdin(data) {
219
+ if (this.exitCode !== void 0) {
220
+ throw new Error(`Process ${this.pid} has already exited with code ${this.exitCode}`);
221
+ }
222
+ await this._sandbox.commands.sendStdin(this.pid, data);
223
+ }
224
+ };
225
+ var E2BProcessManager = class extends workspace.SandboxProcessManager {
226
+ async spawn(command, options = {}) {
227
+ return this.sandbox.retryOnDead(async () => {
228
+ const e2b = this.sandbox.e2b;
229
+ const mergedEnv = { ...this.env, ...options.env };
230
+ const envs = Object.fromEntries(
231
+ Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
232
+ );
233
+ let handle;
234
+ const e2bHandle = await e2b.commands.run(command, {
235
+ background: true,
236
+ stdin: true,
237
+ cwd: options.cwd,
238
+ envs,
239
+ timeoutMs: options.timeout,
240
+ onStdout: (data) => handle.emitStdout(data),
241
+ onStderr: (data) => handle.emitStderr(data)
242
+ });
243
+ handle = new E2BProcessHandle(e2bHandle, e2b, Date.now(), options);
244
+ this._tracked.set(handle.pid, handle);
245
+ return handle;
246
+ });
247
+ }
248
+ /**
249
+ * List processes by querying E2B's commands API.
250
+ * E2B manages all state server-side — no local tracking needed.
251
+ */
252
+ async list() {
253
+ const e2b = this.sandbox.e2b;
254
+ const procs = await e2b.commands.list();
255
+ return procs.map((proc) => ({
256
+ pid: proc.pid,
257
+ command: [proc.cmd, ...proc.args].join(" "),
258
+ running: true
259
+ // E2B only lists running processes
260
+ }));
261
+ }
262
+ /**
263
+ * Get a handle to a process by PID.
264
+ * Checks base class tracking first, then falls back to commands.connect()
265
+ * for processes spawned externally or before reconnection.
266
+ */
267
+ async get(pid) {
268
+ const tracked = this._tracked.get(pid);
269
+ if (tracked) return tracked;
270
+ const e2b = this.sandbox.e2b;
271
+ let handle;
272
+ try {
273
+ const e2bHandle = await e2b.commands.connect(pid, {
274
+ onStdout: (data) => handle.emitStdout(data),
275
+ onStderr: (data) => handle.emitStderr(data)
276
+ });
277
+ handle = new E2BProcessHandle(e2bHandle, e2b, Date.now());
278
+ this._tracked.set(pid, handle);
279
+ return handle;
280
+ } catch {
281
+ return void 0;
282
+ }
283
+ }
284
+ };
180
285
 
181
286
  // src/sandbox/index.ts
182
287
  var SAFE_MOUNT_PATH = /^\/[a-zA-Z0-9_.\-/]+$/;
@@ -192,7 +297,6 @@ var E2BSandbox = class extends workspace.MastraSandbox {
192
297
  id;
193
298
  name = "E2BSandbox";
194
299
  provider = "e2b";
195
- // Status is managed by base class lifecycle methods
196
300
  status = "pending";
197
301
  _sandbox = null;
198
302
  _createdAt = null;
@@ -202,13 +306,17 @@ var E2BSandbox = class extends workspace.MastraSandbox {
202
306
  env;
203
307
  metadata;
204
308
  connectionOpts;
205
- // Non-optional (initialized by BaseSandbox)
309
+ _instructionsOverride;
206
310
  /** Resolved template ID after building (if needed) */
207
311
  _resolvedTemplateId;
208
312
  /** Promise for template preparation (started in constructor) */
209
313
  _templatePreparePromise;
210
314
  constructor(options = {}) {
211
- super({ ...options, name: "E2BSandbox" });
315
+ super({
316
+ ...options,
317
+ name: "E2BSandbox",
318
+ processes: new E2BProcessManager({ env: options.env ?? {} })
319
+ });
212
320
  this.id = options.id ?? this.generateId();
213
321
  this.timeout = options.timeout ?? 3e5;
214
322
  this.templateSpec = options.template;
@@ -220,14 +328,12 @@ var E2BSandbox = class extends workspace.MastraSandbox {
220
328
  ...options.apiKey && { apiKey: options.apiKey },
221
329
  ...options.accessToken && { accessToken: options.accessToken }
222
330
  };
331
+ this._instructionsOverride = options.instructions;
223
332
  this._templatePreparePromise = this.resolveTemplate().catch((err) => {
224
333
  this.logger.debug(`${LOG_PREFIX} Template preparation error (will retry on start):`, err);
225
334
  return "";
226
335
  });
227
336
  }
228
- generateId() {
229
- return `e2b-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
230
- }
231
337
  /**
232
338
  * Get the underlying E2B Sandbox instance for direct access to E2B APIs.
233
339
  *
@@ -238,26 +344,168 @@ var E2BSandbox = class extends workspace.MastraSandbox {
238
344
  *
239
345
  * @example Direct file operations
240
346
  * ```typescript
241
- * const e2bSandbox = sandbox.instance;
242
- * await e2bSandbox.files.write('/tmp/test.txt', 'Hello');
243
- * const content = await e2bSandbox.files.read('/tmp/test.txt');
244
- * const files = await e2bSandbox.files.list('/tmp');
347
+ * const e2b = sandbox.e2b;
348
+ * await e2b.files.write('/tmp/test.txt', 'Hello');
349
+ * const content = await e2b.files.read('/tmp/test.txt');
350
+ * const files = await e2b.files.list('/tmp');
245
351
  * ```
246
352
  *
247
353
  * @example Access ports
248
354
  * ```typescript
249
- * const e2bSandbox = sandbox.instance;
250
- * const url = e2bSandbox.getHost(3000);
355
+ * const e2b = sandbox.e2b;
356
+ * const url = e2b.getHost(3000);
251
357
  * ```
252
358
  */
253
- get instance() {
359
+ get e2b() {
254
360
  if (!this._sandbox) {
255
361
  throw new workspace.SandboxNotReadyError(this.id);
256
362
  }
257
363
  return this._sandbox;
258
364
  }
259
365
  // ---------------------------------------------------------------------------
260
- // Mount Support
366
+ // Lifecycle
367
+ // ---------------------------------------------------------------------------
368
+ /**
369
+ * Start the E2B sandbox.
370
+ * Handles template preparation, existing sandbox reconnection, and new sandbox creation.
371
+ *
372
+ * Status management and mount processing are handled by the base class.
373
+ */
374
+ async start() {
375
+ if (this._sandbox) {
376
+ return;
377
+ }
378
+ const [existingSandbox, templateId] = await Promise.all([
379
+ this.findExistingSandbox(),
380
+ this._templatePreparePromise || this.resolveTemplate()
381
+ ]);
382
+ if (existingSandbox) {
383
+ this._sandbox = existingSandbox;
384
+ this._createdAt = /* @__PURE__ */ new Date();
385
+ this.logger.debug(`${LOG_PREFIX} Reconnected to existing sandbox for: ${this.id}`);
386
+ const expectedPaths = Array.from(this.mounts.entries.keys());
387
+ this.logger.debug(`${LOG_PREFIX} Running mount reconciliation...`);
388
+ await this.reconcileMounts(expectedPaths);
389
+ this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
390
+ return;
391
+ }
392
+ let resolvedTemplateId = templateId;
393
+ if (!resolvedTemplateId) {
394
+ this.logger.debug(`${LOG_PREFIX} Template preparation failed earlier, retrying...`);
395
+ resolvedTemplateId = await this.resolveTemplate();
396
+ }
397
+ this.logger.debug(`${LOG_PREFIX} Creating new sandbox for: ${this.id} with template: ${resolvedTemplateId}`);
398
+ try {
399
+ this._sandbox = await e2b.Sandbox.betaCreate(resolvedTemplateId, {
400
+ ...this.connectionOpts,
401
+ autoPause: true,
402
+ metadata: {
403
+ ...this.metadata,
404
+ "mastra-sandbox-id": this.id
405
+ },
406
+ timeoutMs: this.timeout
407
+ });
408
+ } catch (createError) {
409
+ const errorStr = String(createError);
410
+ if (errorStr.includes("404") && errorStr.includes("not found") && !this.templateSpec) {
411
+ this.logger.debug(`${LOG_PREFIX} Template not found, rebuilding: ${templateId}`);
412
+ this._resolvedTemplateId = void 0;
413
+ const rebuiltTemplateId = await this.buildDefaultTemplate();
414
+ this.logger.debug(`${LOG_PREFIX} Retrying sandbox creation with rebuilt template: ${rebuiltTemplateId}`);
415
+ this._sandbox = await e2b.Sandbox.betaCreate(rebuiltTemplateId, {
416
+ ...this.connectionOpts,
417
+ autoPause: true,
418
+ metadata: {
419
+ ...this.metadata,
420
+ "mastra-sandbox-id": this.id
421
+ },
422
+ timeoutMs: this.timeout
423
+ });
424
+ } else {
425
+ throw createError;
426
+ }
427
+ }
428
+ this.logger.debug(`${LOG_PREFIX} Created sandbox ${this._sandbox.sandboxId} for logical ID: ${this.id}`);
429
+ this._createdAt = /* @__PURE__ */ new Date();
430
+ }
431
+ /**
432
+ * Stop the E2B sandbox.
433
+ * Unmounts all filesystems and releases the sandbox reference.
434
+ * Status management is handled by the base class.
435
+ */
436
+ async stop() {
437
+ try {
438
+ const procs = await this.processes.list();
439
+ await Promise.all(procs.map((p) => this.processes.kill(p.pid)));
440
+ } catch {
441
+ }
442
+ for (const mountPath of [...this.mounts.entries.keys()]) {
443
+ try {
444
+ await this.unmount(mountPath);
445
+ } catch {
446
+ }
447
+ }
448
+ this._sandbox = null;
449
+ }
450
+ /**
451
+ * Destroy the E2B sandbox and clean up all resources.
452
+ * Unmounts filesystems, kills the sandbox, and clears mount state.
453
+ * Status management is handled by the base class.
454
+ */
455
+ async destroy() {
456
+ if (this._sandbox) {
457
+ try {
458
+ const procs = await this.processes.list();
459
+ await Promise.all(procs.map((p) => this.processes.kill(p.pid)));
460
+ } catch {
461
+ }
462
+ for (const mountPath of [...this.mounts.entries.keys()]) {
463
+ try {
464
+ await this.unmount(mountPath);
465
+ } catch {
466
+ }
467
+ }
468
+ try {
469
+ await this._sandbox.kill();
470
+ } catch {
471
+ }
472
+ this._sandbox = null;
473
+ }
474
+ this.mounts.clear();
475
+ }
476
+ async getInfo() {
477
+ return {
478
+ id: this.id,
479
+ name: this.name,
480
+ provider: this.provider,
481
+ status: this.status,
482
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
483
+ mounts: Array.from(this.mounts.entries).map(([path, entry]) => ({
484
+ path,
485
+ filesystem: entry.filesystem?.provider ?? entry.config?.type ?? "unknown"
486
+ })),
487
+ metadata: {
488
+ ...this.metadata
489
+ }
490
+ };
491
+ }
492
+ /**
493
+ * Get instructions describing this E2B sandbox.
494
+ * Used by agents to understand the execution environment.
495
+ */
496
+ getInstructions(opts) {
497
+ if (this._instructionsOverride === void 0) return this._getDefaultInstructions();
498
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
499
+ const defaultInstructions = this._getDefaultInstructions();
500
+ return this._instructionsOverride({ defaultInstructions, requestContext: opts?.requestContext });
501
+ }
502
+ _getDefaultInstructions() {
503
+ const mountCount = this.mounts.entries.size;
504
+ const mountInfo = mountCount > 0 ? ` ${mountCount} filesystem(s) mounted via FUSE.` : "";
505
+ return `Cloud sandbox with /home/user as working directory.${mountInfo}`;
506
+ }
507
+ // ---------------------------------------------------------------------------
508
+ // Mounting
261
509
  // ---------------------------------------------------------------------------
262
510
  /**
263
511
  * Mount a filesystem at a path in the sandbox.
@@ -359,23 +607,6 @@ var E2BSandbox = class extends workspace.MastraSandbox {
359
607
  this.logger.debug(`${LOG_PREFIX} Mounted ${mountPath}`);
360
608
  return { success: true, mountPath };
361
609
  }
362
- /**
363
- * Write marker file for detecting config changes on reconnect.
364
- * Stores both the mount path and config hash in the file.
365
- */
366
- async writeMarkerFile(mountPath) {
367
- if (!this._sandbox) return;
368
- const markerContent = this.mounts.getMarkerContent(mountPath);
369
- if (!markerContent) return;
370
- const filename = this.mounts.markerFilename(mountPath);
371
- const markerPath = `/tmp/.mastra-mounts/${filename}`;
372
- try {
373
- await this._sandbox.commands.run("mkdir -p /tmp/.mastra-mounts");
374
- await this._sandbox.files.write(markerPath, markerContent);
375
- } catch {
376
- this.logger.debug(`${LOG_PREFIX} Warning: Could not write marker file at ${markerPath}`);
377
- }
378
- }
379
610
  /**
380
611
  * Unmount a filesystem from a path in the sandbox.
381
612
  */
@@ -470,116 +701,49 @@ var E2BSandbox = class extends workspace.MastraSandbox {
470
701
  this.logger.debug(`${LOG_PREFIX} Error during orphan cleanup (non-fatal)`);
471
702
  }
472
703
  }
473
- /**
474
- * Check if a path is already mounted and if the config matches.
475
- *
476
- * @param mountPath - The mount path to check
477
- * @param newConfig - The new config to compare against the stored config
478
- * @returns 'not_mounted' | 'matching' | 'mismatched'
479
- */
480
- async checkExistingMount(mountPath, newConfig) {
481
- if (!this._sandbox) throw new workspace.SandboxNotReadyError(this.id);
482
- const mountCheck = await this._sandbox.commands.run(
483
- `mountpoint -q "${mountPath}" && echo "mounted" || echo "not mounted"`
484
- );
485
- if (mountCheck.stdout.trim() !== "mounted") {
486
- return "not_mounted";
487
- }
488
- const filename = this.mounts.markerFilename(mountPath);
489
- const markerPath = `/tmp/.mastra-mounts/${filename}`;
490
- try {
491
- const markerResult = await this._sandbox.commands.run(`cat "${markerPath}" 2>/dev/null || echo ""`);
492
- const parsed = this.mounts.parseMarkerContent(markerResult.stdout.trim());
493
- if (!parsed) {
494
- return "mismatched";
495
- }
496
- const newConfigHash = this.mounts.computeConfigHash(newConfig);
497
- this.logger.debug(
498
- `${LOG_PREFIX} Marker check - stored hash: "${parsed.configHash}", new config hash: "${newConfigHash}"`
499
- );
500
- if (parsed.path === mountPath && parsed.configHash === newConfigHash) {
501
- return "matching";
502
- }
503
- } catch {
504
- }
505
- return "mismatched";
704
+ // ---------------------------------------------------------------------------
705
+ // Deprecated
706
+ // ---------------------------------------------------------------------------
707
+ /** @deprecated Use `e2b` instead. */
708
+ get instance() {
709
+ return this.e2b;
710
+ }
711
+ /** @deprecated Use `status === 'running'` instead. */
712
+ async isReady() {
713
+ return this.status === "running" && this._sandbox !== null;
506
714
  }
507
715
  // ---------------------------------------------------------------------------
508
- // Lifecycle (overrides base class protected methods)
716
+ // Internal Helpers
509
717
  // ---------------------------------------------------------------------------
718
+ generateId() {
719
+ return `e2b-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
720
+ }
510
721
  /**
511
- * Start the E2B sandbox.
512
- * Handles template preparation, existing sandbox reconnection, and new sandbox creation.
513
- *
514
- * Status management and mount processing are handled by the base class.
722
+ * Find an existing sandbox with matching mastra-sandbox-id metadata.
723
+ * Returns the connected sandbox if found, null otherwise.
515
724
  */
516
- async start() {
517
- if (this._sandbox) {
518
- return;
519
- }
520
- const [existingSandbox, templateId] = await Promise.all([
521
- this.findExistingSandbox(),
522
- this._templatePreparePromise || this.resolveTemplate()
523
- ]);
524
- if (existingSandbox) {
525
- this._sandbox = existingSandbox;
526
- this._createdAt = /* @__PURE__ */ new Date();
527
- this.logger.debug(`${LOG_PREFIX} Reconnected to existing sandbox for: ${this.id}`);
528
- const expectedPaths = Array.from(this.mounts.entries.keys());
529
- this.logger.debug(`${LOG_PREFIX} Running mount reconciliation...`);
530
- await this.reconcileMounts(expectedPaths);
531
- this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
532
- return;
533
- }
534
- let resolvedTemplateId = templateId;
535
- if (!resolvedTemplateId) {
536
- this.logger.debug(`${LOG_PREFIX} Template preparation failed earlier, retrying...`);
537
- resolvedTemplateId = await this.resolveTemplate();
538
- }
539
- this.logger.debug(`${LOG_PREFIX} Creating new sandbox for: ${this.id} with template: ${resolvedTemplateId}`);
725
+ async findExistingSandbox() {
540
726
  try {
541
- this._sandbox = await e2b.Sandbox.betaCreate(resolvedTemplateId, {
727
+ const paginator = e2b.Sandbox.list({
542
728
  ...this.connectionOpts,
543
- autoPause: true,
544
- metadata: {
545
- ...this.metadata,
546
- "mastra-sandbox-id": this.id
547
- },
548
- timeoutMs: this.timeout
729
+ query: {
730
+ metadata: { "mastra-sandbox-id": this.id },
731
+ state: ["running", "paused"]
732
+ }
549
733
  });
550
- } catch (createError) {
551
- const errorStr = String(createError);
552
- if (errorStr.includes("404") && errorStr.includes("not found") && !this.templateSpec) {
553
- this.logger.debug(`${LOG_PREFIX} Template not found, rebuilding: ${templateId}`);
554
- this._resolvedTemplateId = void 0;
555
- const rebuiltTemplateId = await this.buildDefaultTemplate();
556
- this.logger.debug(`${LOG_PREFIX} Retrying sandbox creation with rebuilt template: ${rebuiltTemplateId}`);
557
- this._sandbox = await e2b.Sandbox.betaCreate(rebuiltTemplateId, {
558
- ...this.connectionOpts,
559
- autoPause: true,
560
- metadata: {
561
- ...this.metadata,
562
- "mastra-sandbox-id": this.id
563
- },
564
- timeoutMs: this.timeout
565
- });
566
- } else {
567
- throw createError;
734
+ const sandboxes = await paginator.nextItems();
735
+ this.logger.debug(`${LOG_PREFIX} sandboxes:`, sandboxes);
736
+ if (sandboxes.length > 0) {
737
+ const existingSandbox = sandboxes[0];
738
+ this.logger.debug(
739
+ `${LOG_PREFIX} Found existing sandbox for ${this.id}: ${existingSandbox.sandboxId} (state: ${existingSandbox.state})`
740
+ );
741
+ return await e2b.Sandbox.connect(existingSandbox.sandboxId, this.connectionOpts);
568
742
  }
743
+ } catch (e) {
744
+ this.logger.debug(`${LOG_PREFIX} Error querying for existing sandbox:`, e);
569
745
  }
570
- this.logger.debug(`${LOG_PREFIX} Created sandbox ${this._sandbox.sandboxId} for logical ID: ${this.id}`);
571
- this._createdAt = /* @__PURE__ */ new Date();
572
- }
573
- /**
574
- * Build the default mountable template (bypasses exists check).
575
- */
576
- async buildDefaultTemplate() {
577
- const { template, id } = createDefaultMountableTemplate();
578
- this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
579
- const buildResult = await e2b.Template.build(template, id, this.connectionOpts);
580
- this._resolvedTemplateId = buildResult.templateId;
581
- this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
582
- return buildResult.templateId;
746
+ return null;
583
747
  }
584
748
  /**
585
749
  * Resolve the template specification to a template ID.
@@ -628,112 +792,62 @@ var E2BSandbox = class extends workspace.MastraSandbox {
628
792
  return buildResult.templateId;
629
793
  }
630
794
  /**
631
- * Find an existing sandbox with matching mastra-sandbox-id metadata.
632
- * Returns the connected sandbox if found, null otherwise.
795
+ * Build the default mountable template (bypasses exists check).
633
796
  */
634
- async findExistingSandbox() {
635
- try {
636
- const paginator = e2b.Sandbox.list({
637
- ...this.connectionOpts,
638
- query: {
639
- metadata: { "mastra-sandbox-id": this.id },
640
- state: ["running", "paused"]
641
- }
642
- });
643
- const sandboxes = await paginator.nextItems();
644
- this.logger.debug(`${LOG_PREFIX} sandboxes:`, sandboxes);
645
- if (sandboxes.length > 0) {
646
- const existingSandbox = sandboxes[0];
647
- this.logger.debug(
648
- `${LOG_PREFIX} Found existing sandbox for ${this.id}: ${existingSandbox.sandboxId} (state: ${existingSandbox.state})`
649
- );
650
- return await e2b.Sandbox.connect(existingSandbox.sandboxId, this.connectionOpts);
651
- }
652
- } catch (e) {
653
- this.logger.debug(`${LOG_PREFIX} Error querying for existing sandbox:`, e);
654
- }
655
- return null;
797
+ async buildDefaultTemplate() {
798
+ const { template, id } = createDefaultMountableTemplate();
799
+ this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
800
+ const buildResult = await e2b.Template.build(template, id, this.connectionOpts);
801
+ this._resolvedTemplateId = buildResult.templateId;
802
+ this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
803
+ return buildResult.templateId;
656
804
  }
657
805
  /**
658
- * Stop the E2B sandbox.
659
- * Unmounts all filesystems and releases the sandbox reference.
660
- * Status management is handled by the base class.
806
+ * Write marker file for detecting config changes on reconnect.
807
+ * Stores both the mount path and config hash in the file.
661
808
  */
662
- async stop() {
663
- for (const mountPath of [...this.mounts.entries.keys()]) {
664
- try {
665
- await this.unmount(mountPath);
666
- } catch {
667
- }
809
+ async writeMarkerFile(mountPath) {
810
+ if (!this._sandbox) return;
811
+ const markerContent = this.mounts.getMarkerContent(mountPath);
812
+ if (!markerContent) return;
813
+ const filename = this.mounts.markerFilename(mountPath);
814
+ const markerPath = `/tmp/.mastra-mounts/${filename}`;
815
+ try {
816
+ await this._sandbox.commands.run("mkdir -p /tmp/.mastra-mounts");
817
+ await this._sandbox.files.write(markerPath, markerContent);
818
+ } catch {
819
+ this.logger.debug(`${LOG_PREFIX} Warning: Could not write marker file at ${markerPath}`);
668
820
  }
669
- this._sandbox = null;
670
821
  }
671
822
  /**
672
- * Destroy the E2B sandbox and clean up all resources.
673
- * Unmounts filesystems, kills the sandbox, and clears mount state.
674
- * Status management is handled by the base class.
823
+ * Check if a path is already mounted and if the config matches.
675
824
  */
676
- async destroy() {
677
- for (const mountPath of [...this.mounts.entries.keys()]) {
678
- try {
679
- await this.unmount(mountPath);
680
- } catch {
681
- }
825
+ async checkExistingMount(mountPath, newConfig) {
826
+ if (!this._sandbox) throw new workspace.SandboxNotReadyError(this.id);
827
+ const mountCheck = await this._sandbox.commands.run(
828
+ `mountpoint -q "${mountPath}" && echo "mounted" || echo "not mounted"`
829
+ );
830
+ if (mountCheck.stdout.trim() !== "mounted") {
831
+ return "not_mounted";
682
832
  }
683
- if (this._sandbox) {
684
- try {
685
- await this._sandbox.kill();
686
- } catch {
833
+ const filename = this.mounts.markerFilename(mountPath);
834
+ const markerPath = `/tmp/.mastra-mounts/${filename}`;
835
+ try {
836
+ const markerResult = await this._sandbox.commands.run(`cat "${markerPath}" 2>/dev/null || echo ""`);
837
+ const parsed = this.mounts.parseMarkerContent(markerResult.stdout.trim());
838
+ if (!parsed) {
839
+ return "mismatched";
687
840
  }
688
- }
689
- this._sandbox = null;
690
- this.mounts.clear();
691
- }
692
- /**
693
- * Check if the sandbox is ready for operations.
694
- */
695
- async isReady() {
696
- return this.status === "running" && this._sandbox !== null;
697
- }
698
- /**
699
- * Get information about the current state of the sandbox.
700
- */
701
- async getInfo() {
702
- return {
703
- id: this.id,
704
- name: this.name,
705
- provider: this.provider,
706
- status: this.status,
707
- createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
708
- mounts: Array.from(this.mounts.entries).map(([path, entry]) => ({
709
- path,
710
- filesystem: entry.filesystem?.provider ?? entry.config?.type ?? "unknown"
711
- })),
712
- metadata: {
713
- ...this.metadata
841
+ const newConfigHash = this.mounts.computeConfigHash(newConfig);
842
+ this.logger.debug(
843
+ `${LOG_PREFIX} Marker check - stored hash: "${parsed.configHash}", new config hash: "${newConfigHash}"`
844
+ );
845
+ if (parsed.path === mountPath && parsed.configHash === newConfigHash) {
846
+ return "matching";
714
847
  }
715
- };
716
- }
717
- /**
718
- * Get instructions describing this E2B sandbox.
719
- * Used by agents to understand the execution environment.
720
- */
721
- getInstructions() {
722
- const mountCount = this.mounts.entries.size;
723
- const mountInfo = mountCount > 0 ? ` ${mountCount} filesystem(s) mounted via FUSE.` : "";
724
- return `Cloud sandbox with /home/user as working directory.${mountInfo}`;
725
- }
726
- // ---------------------------------------------------------------------------
727
- // Internal Helpers
728
- // ---------------------------------------------------------------------------
729
- /**
730
- * Ensure the sandbox is started and return the E2B Sandbox instance.
731
- * Uses base class ensureRunning() for status management and error handling.
732
- * @throws {SandboxNotReadyError} if sandbox fails to start
733
- */
734
- async ensureSandbox() {
735
- await this.ensureRunning();
736
- return this._sandbox;
848
+ } catch {
849
+ }
850
+ return "mismatched";
737
851
  }
738
852
  /**
739
853
  * Check if an error indicates the sandbox itself is dead/gone.
@@ -761,76 +875,29 @@ var E2BSandbox = class extends workspace.MastraSandbox {
761
875
  }
762
876
  this.status = "stopped";
763
877
  }
764
- // ---------------------------------------------------------------------------
765
- // Command Execution
766
- // ---------------------------------------------------------------------------
767
878
  /**
768
- * Execute a shell command in the sandbox.
769
- * Automatically starts the sandbox if not already running.
770
- * Retries once if the sandbox is found to be dead.
879
+ * Execute an operation with automatic retry if the sandbox is found to be dead.
880
+ *
881
+ * When the E2B sandbox times out or crashes mid-operation, this method
882
+ * resets sandbox state, restarts it, and retries the operation once.
883
+ *
884
+ * @internal Used by E2BProcessManager to handle dead sandboxes during spawn.
771
885
  */
772
- async executeCommand(command, args = [], options = {}) {
773
- this.logger.debug(`${LOG_PREFIX} Executing: ${command} ${args.join(" ")}`, options);
774
- const sandbox = await this.ensureSandbox();
775
- const startTime = Date.now();
776
- const fullCommand = args.length > 0 ? `${command} ${args.map(shellQuote).join(" ")}` : command;
777
- this.logger.debug(`${LOG_PREFIX} Executing: ${fullCommand}`);
886
+ async retryOnDead(fn) {
778
887
  try {
779
- const mergedEnv = { ...this.env, ...options.env };
780
- const envs = Object.fromEntries(
781
- Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
782
- );
783
- const result = await sandbox.commands.run(fullCommand, {
784
- cwd: options.cwd,
785
- envs,
786
- timeoutMs: options.timeout,
787
- onStdout: options.onStdout,
788
- onStderr: options.onStderr
789
- });
790
- const executionTimeMs = Date.now() - startTime;
791
- this.logger.debug(`${LOG_PREFIX} Exit code: ${result.exitCode} (${executionTimeMs}ms)`);
792
- if (result.stdout) this.logger.debug(`${LOG_PREFIX} stdout:
793
- ${result.stdout}`);
794
- if (result.stderr) this.logger.debug(`${LOG_PREFIX} stderr:
795
- ${result.stderr}`);
796
- return {
797
- success: result.exitCode === 0,
798
- exitCode: result.exitCode,
799
- stdout: result.stdout,
800
- stderr: result.stderr,
801
- executionTimeMs,
802
- command,
803
- args
804
- };
888
+ return await fn();
805
889
  } catch (error) {
806
890
  if (this.isSandboxDeadError(error) && !this._isRetrying) {
807
891
  this.handleSandboxTimeout();
808
892
  this._isRetrying = true;
809
893
  try {
810
- return await this.executeCommand(command, args, options);
894
+ await this.ensureRunning();
895
+ return await fn();
811
896
  } finally {
812
897
  this._isRetrying = false;
813
898
  }
814
899
  }
815
- const executionTimeMs = Date.now() - startTime;
816
- const errorObj = error;
817
- const stdout = errorObj.result?.stdout || "";
818
- const stderr = errorObj.result?.stderr || (error instanceof Error ? error.message : String(error));
819
- const exitCode = errorObj.result?.exitCode ?? 1;
820
- this.logger.debug(`${LOG_PREFIX} Exit code: ${exitCode} (${executionTimeMs}ms) [error]`);
821
- if (stdout) this.logger.debug(`${LOG_PREFIX} stdout:
822
- ${stdout}`);
823
- if (stderr) this.logger.debug(`${LOG_PREFIX} stderr:
824
- ${stderr}`);
825
- return {
826
- success: false,
827
- exitCode,
828
- stdout,
829
- stderr,
830
- executionTimeMs,
831
- command,
832
- args
833
- };
900
+ throw error;
834
901
  }
835
902
  }
836
903
  };
@@ -864,6 +931,7 @@ var e2bSandboxProvider = {
864
931
  createSandbox: (config) => new E2BSandbox(config)
865
932
  };
866
933
 
934
+ exports.E2BProcessManager = E2BProcessManager;
867
935
  exports.E2BSandbox = E2BSandbox;
868
936
  exports.createDefaultMountableTemplate = createDefaultMountableTemplate;
869
937
  exports.e2bSandboxProvider = e2bSandboxProvider;