@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/CHANGELOG.md +34 -0
- package/dist/index.cjs +358 -290
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +359 -292
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +81 -66
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/process-manager.d.ts +28 -0
- package/dist/sandbox/process-manager.d.ts.map +1 -0
- package/package.json +10 -10
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
|
-
|
|
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({
|
|
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
|
|
240
|
-
* await
|
|
241
|
-
* const content = await
|
|
242
|
-
* const files = await
|
|
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
|
|
248
|
-
* const url =
|
|
353
|
+
* const e2b = sandbox.e2b;
|
|
354
|
+
* const url = e2b.getHost(3000);
|
|
249
355
|
* ```
|
|
250
356
|
*/
|
|
251
|
-
get
|
|
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
|
-
//
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
//
|
|
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
|
-
*
|
|
510
|
-
*
|
|
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
|
|
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
|
-
|
|
725
|
+
const paginator = Sandbox.list({
|
|
540
726
|
...this.connectionOpts,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
},
|
|
546
|
-
timeoutMs: this.timeout
|
|
727
|
+
query: {
|
|
728
|
+
metadata: { "mastra-sandbox-id": this.id },
|
|
729
|
+
state: ["running", "paused"]
|
|
730
|
+
}
|
|
547
731
|
});
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
this.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
630
|
-
* Returns the connected sandbox if found, null otherwise.
|
|
793
|
+
* Build the default mountable template (bypasses exists check).
|
|
631
794
|
*/
|
|
632
|
-
async
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
*
|
|
657
|
-
*
|
|
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
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
*
|
|
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
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
|
767
|
-
*
|
|
768
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
892
|
+
await this.ensureRunning();
|
|
893
|
+
return await fn();
|
|
809
894
|
} finally {
|
|
810
895
|
this._isRetrying = false;
|
|
811
896
|
}
|
|
812
897
|
}
|
|
813
|
-
|
|
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
|