@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.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
|
-
|
|
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({
|
|
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
|
|
242
|
-
* await
|
|
243
|
-
* const content = await
|
|
244
|
-
* const files = await
|
|
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
|
|
250
|
-
* const url =
|
|
355
|
+
* const e2b = sandbox.e2b;
|
|
356
|
+
* const url = e2b.getHost(3000);
|
|
251
357
|
* ```
|
|
252
358
|
*/
|
|
253
|
-
get
|
|
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
|
-
//
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
//
|
|
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
|
-
*
|
|
512
|
-
*
|
|
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
|
|
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
|
-
|
|
727
|
+
const paginator = e2b.Sandbox.list({
|
|
542
728
|
...this.connectionOpts,
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
},
|
|
548
|
-
timeoutMs: this.timeout
|
|
729
|
+
query: {
|
|
730
|
+
metadata: { "mastra-sandbox-id": this.id },
|
|
731
|
+
state: ["running", "paused"]
|
|
732
|
+
}
|
|
549
733
|
});
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
this.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
632
|
-
* Returns the connected sandbox if found, null otherwise.
|
|
795
|
+
* Build the default mountable template (bypasses exists check).
|
|
633
796
|
*/
|
|
634
|
-
async
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
*
|
|
659
|
-
*
|
|
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
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
*
|
|
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
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
|
769
|
-
*
|
|
770
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
894
|
+
await this.ensureRunning();
|
|
895
|
+
return await fn();
|
|
811
896
|
} finally {
|
|
812
897
|
this._isRetrying = false;
|
|
813
898
|
}
|
|
814
899
|
}
|
|
815
|
-
|
|
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;
|