@mastra/e2b 0.10.0-alpha.3 → 0.10.0-alpha.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @mastra/e2b
2
2
 
3
+ ## 0.10.0-alpha.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Improved `E2BSandbox` extensibility by adding protected SDK creation, connection, and template resolution hooks. Providers built on E2B can now select a specialized SDK sandbox without changing existing E2B behavior. ([#21707](https://github.com/mastra-ai/mastra/pull/21707))
8
+
9
+ ## 0.10.0-alpha.4
10
+
11
+ ### Minor Changes
12
+
13
+ - Added deterministic reattach to E2B sandboxes by provider sandbox ID. Pass the persisted E2B sandbox ID via the new `sandboxId` option (or `clone({ sandboxId })`) and `start()` connects to that exact sandbox — resuming it if paused — instead of discovering by logical id metadata. Only a typed "sandbox gone" error falls back to the usual lookup-or-create path; auth, quota, rate-limit, timeout, and network errors now propagate instead of silently creating a duplicate sandbox. The resolved provider ID is exposed via the new `sandbox.sandboxId` property so it can be persisted across restarts. ([#22316](https://github.com/mastra-ai/mastra/pull/22316))
14
+
15
+ ```ts
16
+ const sandbox = new E2BSandbox({ id: 'my-workspace', sandboxId: persistedId });
17
+ await sandbox.start();
18
+ await save(sandbox.sandboxId); // persist for the next process
19
+ ```
20
+
21
+ Fixes https://github.com/mastra-ai/mastra/issues/22300
22
+
23
+ ### Patch Changes
24
+
25
+ - Starting a sandbox now reports whether it created a fresh sandbox or reconnected to an existing one, so an `onStart` handler can run first-time setup only when it's actually needed: ([#21984](https://github.com/mastra-ai/mastra/pull/21984))
26
+
27
+ ```typescript
28
+ new E2BSandbox({
29
+ id: 'session-1',
30
+ onStart: async ({ outcome }) => {
31
+ if (outcome === 'created') await cloneRepo();
32
+ },
33
+ });
34
+ ```
35
+
36
+ - Updated dependencies [[`4ff3ee2`](https://github.com/mastra-ai/mastra/commit/4ff3ee2bff7ed07528b4817f8f49639031c72a4d), [`c24754c`](https://github.com/mastra-ai/mastra/commit/c24754c1fb6fe144e5051e536e98c8a18b0214ac), [`45dd6ee`](https://github.com/mastra-ai/mastra/commit/45dd6ee089bd7df0d0c98a10098e483fd388e04a), [`32d3583`](https://github.com/mastra-ai/mastra/commit/32d358332cb8ac2306b83b73cf3536e74dbd435e), [`aca2869`](https://github.com/mastra-ai/mastra/commit/aca2869b2031982f3c4a2f52525c9be7cf123ef8)]:
37
+ - @mastra/core@1.62.0-alpha.11
38
+
3
39
  ## 0.10.0-alpha.3
4
40
 
5
41
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -650,6 +650,7 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
650
650
  network;
651
651
  lifecycle;
652
652
  connectionOpts;
653
+ _preferredSandboxId;
653
654
  _instructionsOverride;
654
655
  _constructorOptions;
655
656
  /** Resolved template ID after building (if needed) */
@@ -674,6 +675,7 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
674
675
  ...options.apiKey && { apiKey: options.apiKey },
675
676
  ...options.accessToken && { accessToken: options.accessToken }
676
677
  };
678
+ this._preferredSandboxId = options.sandboxId;
677
679
  this._instructionsOverride = options.instructions;
678
680
  this._constructorOptions = { ...options };
679
681
  this._templatePreparePromise = this.resolveTemplate().catch((err) => {
@@ -692,13 +694,16 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
692
694
  * independent sandboxes (e.g. one per project).
693
695
  *
694
696
  * `options.idleTimeoutMinutes` maps to the E2B sandbox `timeout` (ms);
695
- * `options.sandboxId` is ignored because E2B reconnects by logical `id`.
697
+ * `options.sandboxId` reattaches the clone to that exact E2B sandbox on
698
+ * `start()`. The parent's own preferred provider sandbox ID is never
699
+ * inherited — physical identity is per-instance.
696
700
  */
697
701
  clone(options = {}) {
698
- const { id: _id, ...base } = this._constructorOptions;
702
+ const { id: _id, sandboxId: _sandboxId, ...base } = this._constructorOptions;
699
703
  return new E2BSandbox({
700
704
  ...base,
701
705
  ...options.id !== void 0 && { id: options.id },
706
+ ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
702
707
  ...options.env !== void 0 && { env: options.env },
703
708
  ...options.idleTimeoutMinutes !== void 0 && { timeout: options.idleTimeoutMinutes * 6e4 }
704
709
  });
@@ -730,61 +735,73 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
730
735
  return this._sandbox;
731
736
  }
732
737
  /**
733
- * Start the E2B sandbox.
734
- * Handles template preparation, existing sandbox reconnection, and new sandbox creation.
738
+ * The E2B provider sandbox ID resolved after connect or create.
735
739
  *
736
- * Status management and mount processing are handled by the base class.
740
+ * Persist this to reattach deterministically later via the `sandboxId`
741
+ * option (or `clone({ sandboxId })`). Undefined until the sandbox has been
742
+ * started (attached) in this process.
737
743
  */
738
- async start() {
739
- if (this._sandbox) return;
740
- const [existingSandbox, templateId] = await Promise.all([this.findExistingSandbox(), this._templatePreparePromise || this.resolveTemplate()]);
741
- if (existingSandbox) {
742
- this._sandbox = existingSandbox;
743
- this._createdAt = /* @__PURE__ */ new Date();
744
- this.logger.debug(`${LOG_PREFIX} Reconnected to existing sandbox for: ${this.id}`);
745
- const expectedPaths = Array.from(this.mounts.entries.keys());
746
- this.logger.debug(`${LOG_PREFIX} Running mount reconciliation...`);
747
- await this.reconcileMounts(expectedPaths);
748
- this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
749
- return;
750
- }
751
- let resolvedTemplateId = templateId;
744
+ get sandboxId() {
745
+ return this._sandbox?.sandboxId;
746
+ }
747
+ /**
748
+ * Acquisition primitives (base-orchestrated start): the base derives
749
+ * `outcome: 'created'` only when a brand-new sandbox VM was created;
750
+ * reconnecting (including resuming a paused sandbox) is `outcome: 'connected'`.
751
+ *
752
+ * `find` returns an already-connected E2B handle: `Sandbox.connect`
753
+ * resumes paused sandboxes, and its failures are deliberately swallowed
754
+ * (unusable handle → create fresh) — that forgiveness is this provider's
755
+ * policy, so it lives here rather than in `connect`. The exception is the
756
+ * `sandboxId` reattach inside {@link acquireExistingSandbox}, which is
757
+ * fail-closed: only a "sandbox gone" error falls through to discovery.
758
+ */
759
+ async find() {
760
+ if (this._sandbox) return this._sandbox;
761
+ return await this.acquireExistingSandbox() ?? void 0;
762
+ }
763
+ async connect(existingSandbox) {
764
+ if (existingSandbox === this._sandbox) return;
765
+ this._sandbox = existingSandbox;
766
+ this._createdAt = /* @__PURE__ */ new Date();
767
+ this.logger.debug(`${LOG_PREFIX} Reconnected to existing sandbox for: ${this.id}`);
768
+ const expectedPaths = Array.from(this.mounts.entries.keys());
769
+ this.logger.debug(`${LOG_PREFIX} Running mount reconciliation...`);
770
+ await this.reconcileMounts(expectedPaths);
771
+ this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
772
+ }
773
+ async create() {
774
+ let resolvedTemplateId = await (this._templatePreparePromise || this.resolveTemplate());
752
775
  if (!resolvedTemplateId) {
753
776
  this.logger.debug(`${LOG_PREFIX} Template preparation failed earlier, retrying...`);
754
777
  resolvedTemplateId = await this.resolveTemplate();
755
778
  }
756
779
  this.logger.debug(`${LOG_PREFIX} Creating new sandbox for: ${this.id} with template: ${resolvedTemplateId}`);
780
+ const createOpts = {
781
+ ...this.connectionOpts,
782
+ lifecycle: this.lifecycle,
783
+ metadata: {
784
+ ...this.metadata,
785
+ "mastra-sandbox-id": this.id
786
+ },
787
+ ...this.network && { network: this.network },
788
+ timeoutMs: this.timeout
789
+ };
790
+ let sdkSandbox;
757
791
  try {
758
- this._sandbox = await e2b.Sandbox.create(resolvedTemplateId, {
759
- ...this.connectionOpts,
760
- lifecycle: this.lifecycle,
761
- metadata: {
762
- ...this.metadata,
763
- "mastra-sandbox-id": this.id
764
- },
765
- ...this.network && { network: this.network },
766
- timeoutMs: this.timeout
767
- });
792
+ sdkSandbox = await this.createSdkSandbox(resolvedTemplateId, createOpts);
768
793
  } catch (createError) {
769
794
  const errorStr = String(createError);
770
795
  if (errorStr.includes("404") && errorStr.includes("not found") && !this.templateSpec) {
771
- this.logger.debug(`${LOG_PREFIX} Template not found, rebuilding: ${templateId}`);
796
+ this.logger.debug(`${LOG_PREFIX} Template not found, rebuilding: ${resolvedTemplateId}`);
772
797
  this._resolvedTemplateId = void 0;
773
798
  const rebuiltTemplateId = await this.buildDefaultTemplate();
774
799
  this.logger.debug(`${LOG_PREFIX} Retrying sandbox creation with rebuilt template: ${rebuiltTemplateId}`);
775
- this._sandbox = await e2b.Sandbox.create(rebuiltTemplateId, {
776
- ...this.connectionOpts,
777
- lifecycle: this.lifecycle,
778
- metadata: {
779
- ...this.metadata,
780
- "mastra-sandbox-id": this.id
781
- },
782
- ...this.network && { network: this.network },
783
- timeoutMs: this.timeout
784
- });
800
+ sdkSandbox = await this.createSdkSandbox(rebuiltTemplateId, createOpts);
785
801
  } else throw createError;
786
802
  }
787
- this.logger.debug(`${LOG_PREFIX} Created sandbox ${this._sandbox.sandboxId} for logical ID: ${this.id}`);
803
+ this._sandbox = sdkSandbox;
804
+ this.logger.debug(`${LOG_PREFIX} Created sandbox ${sdkSandbox.sandboxId} for logical ID: ${this.id}`);
788
805
  this._createdAt = /* @__PURE__ */ new Date();
789
806
  }
790
807
  /**
@@ -849,7 +866,10 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
849
866
  path,
850
867
  filesystem: entry.filesystem?.provider ?? entry.config?.type ?? "unknown"
851
868
  })),
852
- metadata: { ...this.metadata }
869
+ metadata: {
870
+ ...this.metadata,
871
+ ...this._sandbox && { sandboxId: this._sandbox.sandboxId }
872
+ }
853
873
  };
854
874
  }
855
875
  /**
@@ -1127,6 +1147,52 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
1127
1147
  return null;
1128
1148
  }
1129
1149
  /**
1150
+ * Acquire an existing sandbox: try the preferred provider sandbox ID first
1151
+ * (deterministic reattach), then fall back to logical-id metadata discovery.
1152
+ */
1153
+ async acquireExistingSandbox() {
1154
+ if (this._preferredSandboxId) {
1155
+ const preferred = await this.connectToPreferredSandbox(this._preferredSandboxId);
1156
+ if (preferred) return preferred;
1157
+ }
1158
+ return this.findExistingSandbox();
1159
+ }
1160
+ /**
1161
+ * Deterministically reattach to a sandbox by its E2B provider ID.
1162
+ *
1163
+ * Fail-closed: only a typed "sandbox gone" error (not found / killed /
1164
+ * not running) returns null so the caller can fall through to logical-id
1165
+ * discovery or creation. Any other error (auth, quota, rate limit,
1166
+ * timeout, network) propagates so a duplicate sandbox is never created.
1167
+ *
1168
+ * Ownership is validated before connecting: a sandbox tagged with a
1169
+ * different `mastra-sandbox-id` is refused (without resuming it).
1170
+ * Sandboxes without the tag (created outside Mastra) are attachable.
1171
+ */
1172
+ async connectToPreferredSandbox(preferredSandboxId) {
1173
+ let info;
1174
+ try {
1175
+ info = await e2b.Sandbox.getInfo(preferredSandboxId, this.connectionOpts);
1176
+ } catch (e) {
1177
+ if (this.isSandboxDeadError(e)) {
1178
+ this.logger.debug(`${LOG_PREFIX} Preferred sandbox ${preferredSandboxId} is gone, falling back to logical-id discovery:`, e);
1179
+ return null;
1180
+ }
1181
+ throw e;
1182
+ }
1183
+ const owner = info.metadata?.["mastra-sandbox-id"];
1184
+ if (owner !== void 0 && owner !== this.id) throw new Error(`${LOG_PREFIX} Provider sandbox ${preferredSandboxId} belongs to logical sandbox id "${owner}", refusing to attach it to "${this.id}"`);
1185
+ try {
1186
+ return await e2b.Sandbox.connect(preferredSandboxId, this.connectionOpts);
1187
+ } catch (e) {
1188
+ if (this.isSandboxDeadError(e)) {
1189
+ this.logger.debug(`${LOG_PREFIX} Preferred sandbox ${preferredSandboxId} vanished before connect, falling back:`, e);
1190
+ return null;
1191
+ }
1192
+ throw e;
1193
+ }
1194
+ }
1195
+ /**
1130
1196
  * Find an existing sandbox with matching mastra-sandbox-id metadata.
1131
1197
  * Returns the connected sandbox if found, null otherwise.
1132
1198
  * Connecting to a paused sandbox resumes it.
@@ -1135,19 +1201,39 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
1135
1201
  const info = await this.lookupExistingSandboxInfo();
1136
1202
  if (!info) return null;
1137
1203
  try {
1138
- return await e2b.Sandbox.connect(info.sandboxId, this.connectionOpts);
1204
+ return await this.connectSdkSandbox(info.sandboxId, this.connectionOpts);
1139
1205
  } catch (e) {
1140
1206
  this.logger.debug(`${LOG_PREFIX} Error connecting to existing sandbox:`, e);
1141
1207
  return null;
1142
1208
  }
1143
1209
  }
1144
1210
  /**
1211
+ * Create a new SDK sandbox from a resolved template ID.
1212
+ *
1213
+ * Override point for providers layered on the E2B SDK whose `Sandbox`
1214
+ * class extends `e2b`'s (e.g. `@e2b/desktop`): override to call their
1215
+ * `Sandbox.create`. Connection options are already spread into `opts`.
1216
+ */
1217
+ async createSdkSandbox(templateId, opts) {
1218
+ return e2b.Sandbox.create(templateId, opts);
1219
+ }
1220
+ /**
1221
+ * Connect to (and resume) an existing SDK sandbox by its E2B sandbox ID.
1222
+ * Override point — see {@link createSdkSandbox}.
1223
+ */
1224
+ async connectSdkSandbox(sandboxId, opts) {
1225
+ return e2b.Sandbox.connect(sandboxId, opts);
1226
+ }
1227
+ /**
1145
1228
  * Resolve the template specification to a template ID.
1146
1229
  *
1147
1230
  * - String: Use as-is (template ID)
1148
1231
  * - TemplateBuilder: Build and return the template ID
1149
1232
  * - Function: Apply to base mountable template, then build
1150
1233
  * - undefined: Use default mountable template (cached)
1234
+ *
1235
+ * Override point: subclasses with a different default template (e.g.
1236
+ * desktop sandboxes) override this and {@link buildDefaultTemplate}.
1151
1237
  */
1152
1238
  async resolveTemplate() {
1153
1239
  if (this._resolvedTemplateId) return this._resolvedTemplateId;
@@ -1186,6 +1272,9 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
1186
1272
  }
1187
1273
  /**
1188
1274
  * Build the default mountable template (bypasses exists check).
1275
+ *
1276
+ * Override point: called from the template-not-found retry path in
1277
+ * `start()` when no explicit template was configured.
1189
1278
  */
1190
1279
  async buildDefaultTemplate() {
1191
1280
  const { template, id } = createDefaultMountableTemplate();