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