@mastra/e2b 0.10.0 → 0.11.0-alpha.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 +61 -0
- package/LICENSE.md +6 -4
- package/dist/code-mode/transport.d.ts.map +1 -1
- package/dist/index.cjs +516 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +511 -40
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +44 -3
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/process-manager.d.ts.map +1 -1
- package/dist/utils/repo-template.d.ts +151 -0
- package/dist/utils/repo-template.d.ts.map +1 -0
- package/dist/utils/template.d.ts +101 -3
- package/dist/utils/template.d.ts.map +1 -1
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _mastra_core_workspace = require("@mastra/core/workspace");
|
|
3
3
|
let e2b = require("e2b");
|
|
4
4
|
let crypto = require("crypto");
|
|
5
|
+
let child_process = require("child_process");
|
|
6
|
+
let util = require("util");
|
|
5
7
|
let _mastra_core_tools = require("@mastra/core/tools");
|
|
6
8
|
let esbuild = require("esbuild");
|
|
7
9
|
/**
|
|
10
|
+
* Node.js version installed into the default template — the current LTS at
|
|
11
|
+
* pin time. An exact version rather than an `lts` alias so the template's
|
|
12
|
+
* contents can never drift under a stable identity hash; bump deliberately
|
|
13
|
+
* (each bump builds new templates).
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_NODE_VERSION = "24.20.0";
|
|
16
|
+
const NODE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
17
|
+
function isNamedTemplateSpec(spec) {
|
|
18
|
+
return typeof spec === "object" && spec !== null && "ref" in spec && "template" in spec;
|
|
19
|
+
}
|
|
20
|
+
function isDeferredNamedTemplateSpec(spec) {
|
|
21
|
+
return typeof spec === "object" && spec !== null && "resolveSpec" in spec && typeof spec.resolveSpec === "function";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
8
24
|
* Create a base template with FUSE mounting dependencies pre-installed.
|
|
9
25
|
*
|
|
10
26
|
* This template includes s3fs and fuse packages required for mounting
|
|
@@ -34,17 +50,28 @@ let esbuild = require("esbuild");
|
|
|
34
50
|
*
|
|
35
51
|
* @returns Object with template builder and deterministic ID
|
|
36
52
|
*/
|
|
37
|
-
function createDefaultMountableTemplate() {
|
|
53
|
+
function createDefaultMountableTemplate(options) {
|
|
38
54
|
const aptPackages = ["s3fs", "fuse"];
|
|
55
|
+
const cpuCount = options?.cpuCount ?? 2;
|
|
56
|
+
const memoryMB = options?.memoryMB ?? 1024;
|
|
57
|
+
const nodeVersion = options?.nodeVersion ?? "24.20.0";
|
|
58
|
+
if (!NODE_VERSION_PATTERN.test(nodeVersion)) throw new Error(`Invalid nodeVersion "${nodeVersion}": expected an exact version like "24.20.0"`);
|
|
39
59
|
const config = {
|
|
40
|
-
version: "
|
|
41
|
-
aptPackages
|
|
60
|
+
version: "v3",
|
|
61
|
+
aptPackages,
|
|
62
|
+
cpuCount,
|
|
63
|
+
memoryMB,
|
|
64
|
+
nodeVersion
|
|
42
65
|
};
|
|
43
66
|
const hash = (0, crypto.createHash)("sha256").update(JSON.stringify(config, Object.keys(config).sort())).digest("hex").slice(0, 16);
|
|
44
67
|
return {
|
|
45
|
-
template: (0, e2b.Template)().fromTemplate("base").aptInstall(aptPackages),
|
|
68
|
+
template: (0, e2b.Template)().fromTemplate("base").aptInstall(aptPackages).runCmd(`curl -fsSL https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-x64.tar.gz | sudo tar -xz -C /usr/local --strip-components=1`).runCmd("sudo corepack enable").runCmd(`echo 'COREPACK_ENABLE_DOWNLOAD_PROMPT=0' | sudo tee -a /etc/environment`).setEnvs({ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" }),
|
|
46
69
|
id: `mastra-${hash}`,
|
|
47
|
-
aptPackages
|
|
70
|
+
aptPackages,
|
|
71
|
+
resources: {
|
|
72
|
+
cpuCount,
|
|
73
|
+
memoryMB
|
|
74
|
+
}
|
|
48
75
|
};
|
|
49
76
|
}
|
|
50
77
|
//#endregion
|
|
@@ -579,6 +606,13 @@ function validateMountPath(mountPath) {
|
|
|
579
606
|
/** Allowlist for marker filenames from ls output — e.g. "mount-abc123" */
|
|
580
607
|
const SAFE_MARKER_NAME = /^mount-[a-z0-9]+$/;
|
|
581
608
|
/**
|
|
609
|
+
* Per-process dedupe of background template rebuild triggers, keyed by
|
|
610
|
+
* template ref. Retained on successful trigger (the ref only ever needs one
|
|
611
|
+
* build; once it exists the exists-check short-circuits before this path),
|
|
612
|
+
* cleared on trigger failure so a later start can retry.
|
|
613
|
+
*/
|
|
614
|
+
const inFlightBackgroundBuilds = /* @__PURE__ */ new Set();
|
|
615
|
+
/**
|
|
582
616
|
* Simplified E2B sandbox implementation.
|
|
583
617
|
*
|
|
584
618
|
* Features:
|
|
@@ -653,10 +687,22 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
|
653
687
|
_preferredSandboxId;
|
|
654
688
|
_instructionsOverride;
|
|
655
689
|
_constructorOptions;
|
|
656
|
-
/**
|
|
690
|
+
/**
|
|
691
|
+
* Resolved template ID after building (if needed). The single cache for
|
|
692
|
+
* template resolution: `resolveTemplate()` returns it when set, and the
|
|
693
|
+
* create-time fallback ladder rewrites it to whichever template actually
|
|
694
|
+
* produced a sandbox.
|
|
695
|
+
*
|
|
696
|
+
* `protected` so a subclass with its own default template (e.g. desktop
|
|
697
|
+
* sandboxes) shares the same cache when it overrides `resolveTemplate()`.
|
|
698
|
+
*/
|
|
657
699
|
_resolvedTemplateId;
|
|
658
|
-
/**
|
|
659
|
-
|
|
700
|
+
/**
|
|
701
|
+
* The named spec a deferred template spec resolved to — kept so the
|
|
702
|
+
* 404-on-create fallback ladder can walk the same name/fallback rungs it
|
|
703
|
+
* would for a plain named spec.
|
|
704
|
+
*/
|
|
705
|
+
_resolvedNamedSpec;
|
|
660
706
|
constructor(options = {}) {
|
|
661
707
|
super({
|
|
662
708
|
...options,
|
|
@@ -678,10 +724,6 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
|
678
724
|
this._preferredSandboxId = options.sandboxId;
|
|
679
725
|
this._instructionsOverride = options.instructions;
|
|
680
726
|
this._constructorOptions = { ...options };
|
|
681
|
-
this._templatePreparePromise = this.resolveTemplate().catch((err) => {
|
|
682
|
-
this.logger.debug(`${LOG_PREFIX} Template preparation error (will retry on start):`, err);
|
|
683
|
-
return "";
|
|
684
|
-
});
|
|
685
727
|
}
|
|
686
728
|
/**
|
|
687
729
|
* Construct a sibling `E2BSandbox` that inherits this sandbox's
|
|
@@ -771,11 +813,7 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
|
771
813
|
this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
|
|
772
814
|
}
|
|
773
815
|
async create() {
|
|
774
|
-
|
|
775
|
-
if (!resolvedTemplateId) {
|
|
776
|
-
this.logger.debug(`${LOG_PREFIX} Template preparation failed earlier, retrying...`);
|
|
777
|
-
resolvedTemplateId = await this.resolveTemplate();
|
|
778
|
-
}
|
|
816
|
+
const resolvedTemplateId = await this.resolveTemplate();
|
|
779
817
|
this.logger.debug(`${LOG_PREFIX} Creating new sandbox for: ${this.id} with template: ${resolvedTemplateId}`);
|
|
780
818
|
const createOpts = {
|
|
781
819
|
...this.connectionOpts,
|
|
@@ -787,17 +825,51 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
|
787
825
|
...this.network && { network: this.network },
|
|
788
826
|
timeoutMs: this.timeout
|
|
789
827
|
};
|
|
828
|
+
const createFromTemplate = (templateId) => this.createSdkSandbox(templateId, createOpts);
|
|
829
|
+
const isTemplateUnusable = (error) => String(error).includes("404");
|
|
790
830
|
let sdkSandbox;
|
|
791
831
|
try {
|
|
792
|
-
sdkSandbox = await
|
|
832
|
+
sdkSandbox = await createFromTemplate(resolvedTemplateId);
|
|
793
833
|
} catch (createError) {
|
|
794
|
-
|
|
795
|
-
|
|
834
|
+
if (!isTemplateUnusable(createError)) throw createError;
|
|
835
|
+
const namedSpec = this.templateSpec && isNamedTemplateSpec(this.templateSpec) ? this.templateSpec : this._resolvedNamedSpec;
|
|
836
|
+
if (namedSpec) {
|
|
837
|
+
this.logger.warn(`${LOG_PREFIX} Creating from '${resolvedTemplateId}' failed, retrying on fallback: ${createError}`);
|
|
838
|
+
this._resolvedTemplateId = void 0;
|
|
839
|
+
const spec = namedSpec;
|
|
840
|
+
const fallbackId = resolvedTemplateId === spec.ref ? await this.resolveFallbackTemplate(spec.fallbackTemplate) : await this.buildOrReuseDefaultTemplate();
|
|
841
|
+
try {
|
|
842
|
+
sdkSandbox = await createFromTemplate(fallbackId);
|
|
843
|
+
this._resolvedTemplateId = fallbackId;
|
|
844
|
+
} catch (fallbackError) {
|
|
845
|
+
if (!isTemplateUnusable(fallbackError)) throw fallbackError;
|
|
846
|
+
const rebuildDefaultAndCreate = async () => {
|
|
847
|
+
this.logger.warn(`${LOG_PREFIX} Default template broken too, rebuilding: ${fallbackError}`);
|
|
848
|
+
const rebuiltId = await this.buildDefaultTemplate();
|
|
849
|
+
const rebuilt = await createFromTemplate(rebuiltId);
|
|
850
|
+
this._resolvedTemplateId = rebuiltId;
|
|
851
|
+
return rebuilt;
|
|
852
|
+
};
|
|
853
|
+
const defaultId = await this.buildOrReuseDefaultTemplate();
|
|
854
|
+
if (defaultId === fallbackId) sdkSandbox = await rebuildDefaultAndCreate();
|
|
855
|
+
else {
|
|
856
|
+
this.logger.warn(`${LOG_PREFIX} Fallback '${fallbackId}' failed too, using default: ${fallbackError}`);
|
|
857
|
+
try {
|
|
858
|
+
sdkSandbox = await createFromTemplate(defaultId);
|
|
859
|
+
this._resolvedTemplateId = defaultId;
|
|
860
|
+
} catch (defaultError) {
|
|
861
|
+
if (!isTemplateUnusable(defaultError)) throw defaultError;
|
|
862
|
+
sdkSandbox = await rebuildDefaultAndCreate();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
this.logger.debug(`${LOG_PREFIX} Created sandbox ${sdkSandbox.sandboxId} from fallback for: ${this.id}`);
|
|
867
|
+
} else if (!this.templateSpec) {
|
|
796
868
|
this.logger.debug(`${LOG_PREFIX} Template not found, rebuilding: ${resolvedTemplateId}`);
|
|
797
869
|
this._resolvedTemplateId = void 0;
|
|
798
870
|
const rebuiltTemplateId = await this.buildDefaultTemplate();
|
|
799
871
|
this.logger.debug(`${LOG_PREFIX} Retrying sandbox creation with rebuilt template: ${rebuiltTemplateId}`);
|
|
800
|
-
sdkSandbox = await
|
|
872
|
+
sdkSandbox = await createFromTemplate(rebuiltTemplateId);
|
|
801
873
|
} else throw createError;
|
|
802
874
|
}
|
|
803
875
|
this._sandbox = sdkSandbox;
|
|
@@ -1237,31 +1309,57 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
|
1237
1309
|
*/
|
|
1238
1310
|
async resolveTemplate() {
|
|
1239
1311
|
if (this._resolvedTemplateId) return this._resolvedTemplateId;
|
|
1240
|
-
if (!this.templateSpec)
|
|
1241
|
-
const { template, id } = createDefaultMountableTemplate();
|
|
1242
|
-
if (await e2b.Template.exists(id, this.connectionOpts)) {
|
|
1243
|
-
this.logger.debug(`${LOG_PREFIX} Using cached mountable template: ${id}`);
|
|
1244
|
-
this._resolvedTemplateId = id;
|
|
1245
|
-
return id;
|
|
1246
|
-
}
|
|
1247
|
-
this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
|
|
1248
|
-
const buildResult = await e2b.Template.build(template, id, this.connectionOpts);
|
|
1249
|
-
this._resolvedTemplateId = buildResult.templateId;
|
|
1250
|
-
this.logger.debug(`${LOG_PREFIX} Template built and cached: ${buildResult.templateId}`);
|
|
1251
|
-
return buildResult.templateId;
|
|
1252
|
-
}
|
|
1312
|
+
if (!this.templateSpec) return await this.buildOrReuseDefaultTemplate();
|
|
1253
1313
|
if (typeof this.templateSpec === "string") {
|
|
1254
1314
|
this._resolvedTemplateId = this.templateSpec;
|
|
1255
1315
|
return this.templateSpec;
|
|
1256
1316
|
}
|
|
1317
|
+
let spec;
|
|
1318
|
+
if (isDeferredNamedTemplateSpec(this.templateSpec)) try {
|
|
1319
|
+
spec = await this.templateSpec.resolveSpec();
|
|
1320
|
+
this._resolvedNamedSpec = spec;
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
this.logger.warn(`${LOG_PREFIX} Deferred template spec resolution failed, falling back: ${error}`);
|
|
1323
|
+
return await this.resolveFallbackTemplate(void 0);
|
|
1324
|
+
}
|
|
1325
|
+
else spec = this.templateSpec;
|
|
1326
|
+
if (isNamedTemplateSpec(spec)) {
|
|
1327
|
+
const { ref, template: namedTemplate, fallbackTemplate, staleRef, buildTags, buildResources } = spec;
|
|
1328
|
+
const buildOpts = {
|
|
1329
|
+
...this.connectionOpts,
|
|
1330
|
+
...buildTags?.length ? { tags: buildTags } : {},
|
|
1331
|
+
...buildResources
|
|
1332
|
+
};
|
|
1333
|
+
try {
|
|
1334
|
+
if (await e2b.Template.exists(ref, this.connectionOpts)) {
|
|
1335
|
+
this.logger.debug(`${LOG_PREFIX} Using cached template: ${ref}`);
|
|
1336
|
+
this._resolvedTemplateId = ref;
|
|
1337
|
+
return ref;
|
|
1338
|
+
}
|
|
1339
|
+
if (staleRef && staleRef !== ref && await e2b.Template.exists(staleRef, this.connectionOpts)) {
|
|
1340
|
+
this.logger.debug(`${LOG_PREFIX} Using stale build ${staleRef}; rebuilding ${ref} in background`);
|
|
1341
|
+
this.triggerBackgroundBuild(namedTemplate, ref, buildOpts);
|
|
1342
|
+
this._resolvedTemplateId = staleRef;
|
|
1343
|
+
return staleRef;
|
|
1344
|
+
}
|
|
1345
|
+
this.logger.debug(`${LOG_PREFIX} Building template: ${ref}...`);
|
|
1346
|
+
const buildResult = await e2b.Template.build(namedTemplate, ref, buildOpts);
|
|
1347
|
+
this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
|
|
1348
|
+
this._resolvedTemplateId = ref;
|
|
1349
|
+
return ref;
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
this.logger.warn(`${LOG_PREFIX} Template '${ref}' resolution failed, falling back: ${error}`);
|
|
1352
|
+
return await this.resolveFallbackTemplate(fallbackTemplate);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1257
1355
|
let template;
|
|
1258
1356
|
let templateName;
|
|
1259
|
-
if (typeof
|
|
1357
|
+
if (typeof spec === "function") {
|
|
1260
1358
|
const { template: baseTemplate } = createDefaultMountableTemplate();
|
|
1261
|
-
template =
|
|
1359
|
+
template = spec(baseTemplate);
|
|
1262
1360
|
templateName = `mastra-custom-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
|
|
1263
1361
|
} else {
|
|
1264
|
-
template =
|
|
1362
|
+
template = spec;
|
|
1265
1363
|
templateName = `mastra-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
|
|
1266
1364
|
}
|
|
1267
1365
|
this.logger.debug(`${LOG_PREFIX} Building custom template: ${templateName}...`);
|
|
@@ -1271,15 +1369,102 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
|
1271
1369
|
return buildResult.templateId;
|
|
1272
1370
|
}
|
|
1273
1371
|
/**
|
|
1372
|
+
* Resolve the default mountable template: reuse when it exists, build once
|
|
1373
|
+
* when it does not.
|
|
1374
|
+
*/
|
|
1375
|
+
/**
|
|
1376
|
+
* Resolve a named spec's fallback template. A named fallback gets its own
|
|
1377
|
+
* exists-then-build resolution; anything failing past that (including
|
|
1378
|
+
* specs without a fallback, e.g. repo templates) lands on the default
|
|
1379
|
+
* mountable template so a broken build never wedges a session.
|
|
1380
|
+
*/
|
|
1381
|
+
/**
|
|
1382
|
+
* Trigger a non-blocking template rebuild via `Template.buildInBackground`
|
|
1383
|
+
* (the build runs on E2B's side, so it outlives this process). Deduped
|
|
1384
|
+
* per-process by ref so concurrent session starts on the same moved head
|
|
1385
|
+
* don't stack duplicate builds; a failed TRIGGER clears the guard so a
|
|
1386
|
+
* later start retries. A build that fails server-side simply never
|
|
1387
|
+
* registers the ref — the next start falls back to the stale build again
|
|
1388
|
+
* and re-triggers.
|
|
1389
|
+
*/
|
|
1390
|
+
triggerBackgroundBuild(template, ref, buildOpts) {
|
|
1391
|
+
if (inFlightBackgroundBuilds.has(ref)) return;
|
|
1392
|
+
inFlightBackgroundBuilds.add(ref);
|
|
1393
|
+
e2b.Template.buildInBackground(template, ref, buildOpts).then((result) => {
|
|
1394
|
+
this.logger.debug(`${LOG_PREFIX} Background template build triggered: ${ref} (${result.buildId})`);
|
|
1395
|
+
}).catch((error) => {
|
|
1396
|
+
inFlightBackgroundBuilds.delete(ref);
|
|
1397
|
+
this.logger.warn(`${LOG_PREFIX} Background template build trigger failed for '${ref}': ${error}`);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
async resolveFallbackTemplate(fallbackTemplate) {
|
|
1401
|
+
if (typeof fallbackTemplate === "string") {
|
|
1402
|
+
this._resolvedTemplateId = fallbackTemplate;
|
|
1403
|
+
return fallbackTemplate;
|
|
1404
|
+
}
|
|
1405
|
+
if (fallbackTemplate && isNamedTemplateSpec(fallbackTemplate)) try {
|
|
1406
|
+
if (await e2b.Template.exists(fallbackTemplate.ref, this.connectionOpts)) {
|
|
1407
|
+
this._resolvedTemplateId = fallbackTemplate.ref;
|
|
1408
|
+
return fallbackTemplate.ref;
|
|
1409
|
+
}
|
|
1410
|
+
const buildResult = await e2b.Template.build(fallbackTemplate.template, fallbackTemplate.ref, {
|
|
1411
|
+
...this.connectionOpts,
|
|
1412
|
+
...fallbackTemplate.buildResources
|
|
1413
|
+
});
|
|
1414
|
+
this._resolvedTemplateId = buildResult.templateId;
|
|
1415
|
+
return buildResult.templateId;
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
this.logger.warn(`${LOG_PREFIX} Fallback template '${fallbackTemplate.ref}' failed too: ${error}`);
|
|
1418
|
+
return await this.buildOrReuseDefaultTemplate();
|
|
1419
|
+
}
|
|
1420
|
+
if (fallbackTemplate) try {
|
|
1421
|
+
const buildResult = await e2b.Template.build(fallbackTemplate, `mastra-fallback-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`, this.connectionOpts);
|
|
1422
|
+
this._resolvedTemplateId = buildResult.templateId;
|
|
1423
|
+
return buildResult.templateId;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
this.logger.warn(`${LOG_PREFIX} Fallback template build failed, using default: ${error}`);
|
|
1426
|
+
return await this.buildOrReuseDefaultTemplate();
|
|
1427
|
+
}
|
|
1428
|
+
return await this.buildOrReuseDefaultTemplate();
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Resources the configured template asked for. The default mountable
|
|
1432
|
+
* template honors them too, so a repo template that falls back never
|
|
1433
|
+
* silently downgrades the machine — a 2 GB session's setup would OOM in
|
|
1434
|
+
* the 1 GB default. Per-size default templates cost one extra build each.
|
|
1435
|
+
*/
|
|
1436
|
+
requestedBuildResources() {
|
|
1437
|
+
return (this.templateSpec && isNamedTemplateSpec(this.templateSpec) ? this.templateSpec : this._resolvedNamedSpec)?.buildResources;
|
|
1438
|
+
}
|
|
1439
|
+
async buildOrReuseDefaultTemplate() {
|
|
1440
|
+
const { template, id, resources } = createDefaultMountableTemplate(this.requestedBuildResources());
|
|
1441
|
+
if (await e2b.Template.exists(id, this.connectionOpts)) {
|
|
1442
|
+
this.logger.debug(`${LOG_PREFIX} Using cached mountable template: ${id}`);
|
|
1443
|
+
this._resolvedTemplateId = id;
|
|
1444
|
+
return id;
|
|
1445
|
+
}
|
|
1446
|
+
this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
|
|
1447
|
+
const buildResult = await e2b.Template.build(template, id, {
|
|
1448
|
+
...this.connectionOpts,
|
|
1449
|
+
...resources
|
|
1450
|
+
});
|
|
1451
|
+
this._resolvedTemplateId = buildResult.templateId;
|
|
1452
|
+
this.logger.debug(`${LOG_PREFIX} Template built and cached: ${buildResult.templateId}`);
|
|
1453
|
+
return buildResult.templateId;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1274
1456
|
* Build the default mountable template (bypasses exists check).
|
|
1275
1457
|
*
|
|
1276
1458
|
* Override point: called from the template-not-found retry path in
|
|
1277
1459
|
* `start()` when no explicit template was configured.
|
|
1278
1460
|
*/
|
|
1279
1461
|
async buildDefaultTemplate() {
|
|
1280
|
-
const { template, id } = createDefaultMountableTemplate();
|
|
1462
|
+
const { template, id, resources } = createDefaultMountableTemplate(this.requestedBuildResources());
|
|
1281
1463
|
this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
|
|
1282
|
-
const buildResult = await e2b.Template.build(template, id,
|
|
1464
|
+
const buildResult = await e2b.Template.build(template, id, {
|
|
1465
|
+
...this.connectionOpts,
|
|
1466
|
+
...resources
|
|
1467
|
+
});
|
|
1283
1468
|
this._resolvedTemplateId = buildResult.templateId;
|
|
1284
1469
|
this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
|
|
1285
1470
|
return buildResult.templateId;
|
|
@@ -1369,6 +1554,292 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
|
1369
1554
|
}
|
|
1370
1555
|
};
|
|
1371
1556
|
//#endregion
|
|
1557
|
+
//#region src/utils/repo-template.ts
|
|
1558
|
+
/**
|
|
1559
|
+
* Sha-tagged repo templates.
|
|
1560
|
+
*
|
|
1561
|
+
* A repo template is an E2B template with the repository already cloned and
|
|
1562
|
+
* its dependencies installed at a known commit. Sessions started from it only
|
|
1563
|
+
* need `git fetch` + checkout of their actual ref plus setup drift, instead
|
|
1564
|
+
* of a cold clone + full install.
|
|
1565
|
+
*
|
|
1566
|
+
* There is exactly ONE template per (repo, setup command, workdir): the
|
|
1567
|
+
* template name is a deterministic `mastra-repo-<hash>` over those inputs,
|
|
1568
|
+
* and the commit sha rides as a docker-style TAG on that name
|
|
1569
|
+
* (`mastra-repo-<hash>:sha-<sha>`). A moved default branch produces a new
|
|
1570
|
+
* tag via a rebuild-in-place of the same template — old sha tags remain as
|
|
1571
|
+
* prunable build history instead of accumulating stale template names.
|
|
1572
|
+
* Builds are lazy: the first `E2BSandbox.start()` that resolves a missing
|
|
1573
|
+
* tag triggers the build; nothing pre-builds templates for idle repos.
|
|
1574
|
+
*
|
|
1575
|
+
* Credential invariant: a build credential may enter the template
|
|
1576
|
+
* DEFINITION (via `setEnvs`, visible to build steps but not persisted into
|
|
1577
|
+
* runtime sandbox environments) and the build process — never the image
|
|
1578
|
+
* filesystem. Clones authenticate through an in-shell computed
|
|
1579
|
+
* `http.extraheader`, so no tokened remote URL or credential file can land
|
|
1580
|
+
* in a captured layer. Callers must supply a short-lived credential (a
|
|
1581
|
+
* GitHub App installation token, which self-expires); never a long-lived
|
|
1582
|
+
* PAT. Without a credential the clone is plain tokenless HTTPS — public
|
|
1583
|
+
* repos build fine; a private repo's build fails and the sandbox falls back
|
|
1584
|
+
* to the fallback template, with the session's runtime setup performing the
|
|
1585
|
+
* full clone using its runtime-injected credential instead.
|
|
1586
|
+
*/
|
|
1587
|
+
const execFileAsync = (0, util.promisify)(child_process.execFile);
|
|
1588
|
+
const ALIAS_VERSION = "v5";
|
|
1589
|
+
/**
|
|
1590
|
+
* Stable tag assigned to every successful repo-template build. Points at the
|
|
1591
|
+
* latest build regardless of sha, so a moved head can boot from the previous
|
|
1592
|
+
* build (`name:current`) while the fresh sha builds in the background.
|
|
1593
|
+
*/
|
|
1594
|
+
const CURRENT_TAG = "current";
|
|
1595
|
+
/**
|
|
1596
|
+
* Env var carrying the repository credential during the build. The same
|
|
1597
|
+
* name a session installs before running setup, so a setup command sees the
|
|
1598
|
+
* same environment in both places. Set via `setEnvs`; the git auth header is
|
|
1599
|
+
* computed from it too.
|
|
1600
|
+
*/
|
|
1601
|
+
const BUILD_TOKEN_ENV = "GH_TOKEN";
|
|
1602
|
+
/**
|
|
1603
|
+
* Clone URLs interpolate into build shell commands, so constrain them to
|
|
1604
|
+
* https plus plain host/path characters. This rejects shell metacharacters
|
|
1605
|
+
* outright rather than escaping them. Every regex here is a single anchored
|
|
1606
|
+
* character class, so matching stays linear on adversarial input; the
|
|
1607
|
+
* structural checks (scheme, host, path segments) go through WHATWG URL
|
|
1608
|
+
* parsing instead of one big backtracking pattern.
|
|
1609
|
+
*/
|
|
1610
|
+
const CLONE_URL_ALLOWED_CHARS = /^[a-z0-9:/._-]+$/i;
|
|
1611
|
+
const CLONE_URL_HOST_PATTERN = /^[a-z0-9.-]+$/i;
|
|
1612
|
+
const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
|
|
1613
|
+
const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
1614
|
+
function isValidCloneUrl(cloneUrl) {
|
|
1615
|
+
if (cloneUrl.length > 2048 || !CLONE_URL_ALLOWED_CHARS.test(cloneUrl)) return false;
|
|
1616
|
+
let url;
|
|
1617
|
+
try {
|
|
1618
|
+
url = new URL(cloneUrl);
|
|
1619
|
+
} catch {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) return false;
|
|
1623
|
+
if (!CLONE_URL_HOST_PATTERN.test(url.hostname)) return false;
|
|
1624
|
+
const segments = url.pathname.split("/").slice(1);
|
|
1625
|
+
return segments.length > 0 && segments.every((segment) => CLONE_URL_SEGMENT_PATTERN.test(segment));
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Compute the deterministic template ref for a set of repo template inputs
|
|
1629
|
+
* without constructing the builder: `mastra-repo-<hash>` named over
|
|
1630
|
+
* (clone URL, setup command, build env), tag-qualified with `:sha-<sha>`
|
|
1631
|
+
* when the sha is known. Exposed so callers (and proofs) can predict which
|
|
1632
|
+
* ref a sandbox will resolve.
|
|
1633
|
+
*/
|
|
1634
|
+
function repoTemplateRef(identity) {
|
|
1635
|
+
const name = repoTemplateName(identity);
|
|
1636
|
+
return identity.sha ? `${name}:${shaTag(identity.sha)}` : `${name}:${CURRENT_TAG}`;
|
|
1637
|
+
}
|
|
1638
|
+
function repoTemplateName(identity) {
|
|
1639
|
+
const cloneUrl = normalizeCloneUrl(identity.cloneUrl);
|
|
1640
|
+
const config = [
|
|
1641
|
+
ALIAS_VERSION,
|
|
1642
|
+
cloneUrl,
|
|
1643
|
+
identity.setupCommand ?? null,
|
|
1644
|
+
identity.buildEnv ? Object.entries(identity.buildEnv).sort(([a], [b]) => a.localeCompare(b)) : null,
|
|
1645
|
+
identity.cpuCount ?? 2,
|
|
1646
|
+
identity.memoryMB ?? 1024
|
|
1647
|
+
];
|
|
1648
|
+
const hash = (0, crypto.createHash)("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 8);
|
|
1649
|
+
const { owner, repo } = parseCloneUrl(cloneUrl);
|
|
1650
|
+
return `mastra-repo-${[owner, repo].map((part) => (part ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-/, "").replace(/-$/, "").slice(0, 24)).filter(Boolean).join("-")}-${hash}`;
|
|
1651
|
+
}
|
|
1652
|
+
function shaTag(sha) {
|
|
1653
|
+
return `sha-${sha.slice(0, 12).toLowerCase()}`;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Create a sha-tagged repo template spec for `E2BSandbox`.
|
|
1657
|
+
*
|
|
1658
|
+
* Returns undefined when {@link RepoTemplateOptions.getRepositoryAccess} is
|
|
1659
|
+
* absent, which is how a session with no repository asks for no template —
|
|
1660
|
+
* so a host can write `template: createRepoTemplate(ctx)` without a
|
|
1661
|
+
* conditional.
|
|
1662
|
+
*
|
|
1663
|
+
* Resolution is deferred: right before the exists-then-build check it
|
|
1664
|
+
* resolves the clone URL and credential, resolves the repository's current
|
|
1665
|
+
* default-branch head (`git ls-remote`, ~100ms, no clone), and keys the
|
|
1666
|
+
* template ref as `mastra-repo-<hash>:sha-<head>` — so a moved default
|
|
1667
|
+
* branch produces a fresh tagged build of the SAME template on the next new
|
|
1668
|
+
* session (rebuild-in-place), and an unmoved head reuses the existing
|
|
1669
|
+
* tagged build. When the head cannot be resolved the ref degrades to the
|
|
1670
|
+
* untagged name and the build clones whatever the default branch is at
|
|
1671
|
+
* build time.
|
|
1672
|
+
*
|
|
1673
|
+
* When the build itself fails — inaccessible repo, registry flake — the
|
|
1674
|
+
* sandbox falls back to its fallback template and the session's runtime
|
|
1675
|
+
* setup performs the full clone, so a broken build never wedges a session.
|
|
1676
|
+
*/
|
|
1677
|
+
function createRepoTemplate(options) {
|
|
1678
|
+
if (!options.getRepositoryAccess) return void 0;
|
|
1679
|
+
return { resolveSpec: async () => (await resolveSpecAtHead(options)).spec };
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Resolve the clone URL and credential, resolve the current default-branch
|
|
1683
|
+
* head, and produce the concrete sha-tagged spec. Shared by the deferred
|
|
1684
|
+
* spec form and {@link refreshRepoTemplate}.
|
|
1685
|
+
*
|
|
1686
|
+
* A failed access call leaves no clone URL and throws, which the sandbox
|
|
1687
|
+
* turns into its default-template fallback rather than a failed start.
|
|
1688
|
+
*/
|
|
1689
|
+
async function resolveSpecAtHead(options) {
|
|
1690
|
+
const access = options.getRepositoryAccess ? await options.getRepositoryAccess().catch(() => void 0) : void 0;
|
|
1691
|
+
const cloneUrl = access?.cloneUrl;
|
|
1692
|
+
if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
|
|
1693
|
+
assertCloneUrl(cloneUrl);
|
|
1694
|
+
const token = access?.authorization?.token;
|
|
1695
|
+
const buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv() : options.buildEnv;
|
|
1696
|
+
const resolved = await resolveDefaultBranchHead(cloneUrl, token).catch(() => void 0);
|
|
1697
|
+
const sha = resolved && SHA_PATTERN.test(resolved) ? resolved : void 0;
|
|
1698
|
+
return {
|
|
1699
|
+
spec: buildRepoTemplateSpec({
|
|
1700
|
+
cloneUrl,
|
|
1701
|
+
...sha ? { sha } : {},
|
|
1702
|
+
...options.setupCommand ? { setupCommand: options.setupCommand } : {},
|
|
1703
|
+
...buildEnv ? { buildEnv } : {},
|
|
1704
|
+
...options.cpuCount !== void 0 ? { cpuCount: options.cpuCount } : {},
|
|
1705
|
+
...options.memoryMB !== void 0 ? { memoryMB: options.memoryMB } : {}
|
|
1706
|
+
}, token),
|
|
1707
|
+
...sha ? { sha } : {}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Ensure the repo template is built at the repository's current
|
|
1712
|
+
* default-branch head, building it (and moving the `current` tag) when it
|
|
1713
|
+
* is not. This is the same resolution the lazy sandbox-start path performs
|
|
1714
|
+
* — exposed standalone so template warming can be driven externally: call
|
|
1715
|
+
* it from a scheduled workflow (cron) or a merge-to-main event handler and
|
|
1716
|
+
* the next session boots warm instead of paying the build.
|
|
1717
|
+
*
|
|
1718
|
+
* The build is awaited; a build failure rejects so callers can observe it.
|
|
1719
|
+
* An unresolvable head degrades to the sha-less `name:current` form, same
|
|
1720
|
+
* as the lazy path.
|
|
1721
|
+
*/
|
|
1722
|
+
async function refreshRepoTemplate(options, connection) {
|
|
1723
|
+
const { spec, sha } = await resolveSpecAtHead(options);
|
|
1724
|
+
const shaField = sha ? { sha } : {};
|
|
1725
|
+
if (await e2b.Template.exists(spec.ref, connection)) return {
|
|
1726
|
+
ref: spec.ref,
|
|
1727
|
+
action: "reused",
|
|
1728
|
+
...shaField
|
|
1729
|
+
};
|
|
1730
|
+
await e2b.Template.build(spec.template, spec.ref, {
|
|
1731
|
+
...connection,
|
|
1732
|
+
...spec.buildTags?.length ? { tags: spec.buildTags } : {},
|
|
1733
|
+
...spec.buildResources
|
|
1734
|
+
});
|
|
1735
|
+
return {
|
|
1736
|
+
ref: spec.ref,
|
|
1737
|
+
action: "built",
|
|
1738
|
+
...shaField
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* The clone URL is the only untrusted input that reaches a build command,
|
|
1743
|
+
* so it is checked before it can be interpolated into one. The workdir is
|
|
1744
|
+
* derived from it rather than supplied, so it needs no separate guard.
|
|
1745
|
+
*/
|
|
1746
|
+
function assertCloneUrl(cloneUrl) {
|
|
1747
|
+
if (!isValidCloneUrl(cloneUrl)) throw new Error(`Invalid cloneUrl '${cloneUrl}': expected an https URL with a plain host and path`);
|
|
1748
|
+
if (parseCloneUrl(cloneUrl).repo === "") throw new Error(`Invalid cloneUrl '${cloneUrl}': expected a repository path such as https://host/owner/repo.git`);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* In-shell git auth flag: computes a basic-auth header from the build env
|
|
1752
|
+
* var at execution time. The stored command contains only the env-var
|
|
1753
|
+
* REFERENCE — the token value never appears in the command string, and no
|
|
1754
|
+
* credential is written to the build filesystem.
|
|
1755
|
+
*/
|
|
1756
|
+
function gitAuthFlag() {
|
|
1757
|
+
return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
|
|
1758
|
+
}
|
|
1759
|
+
function buildRepoTemplateSpec(identity, token) {
|
|
1760
|
+
const { sha, setupCommand, buildEnv } = identity;
|
|
1761
|
+
const cloneUrl = normalizeCloneUrl(identity.cloneUrl);
|
|
1762
|
+
const workdir = defaultWorkdir(cloneUrl);
|
|
1763
|
+
const auth = token ? `${gitAuthFlag()} ` : "";
|
|
1764
|
+
const steps = [`git ${auth}clone ${cloneUrl} "${workdir}"`];
|
|
1765
|
+
if (sha) steps.push(`git -C "${workdir}" ${auth}fetch origin ${sha}`, `git -C "${workdir}" checkout ${sha}`);
|
|
1766
|
+
if (setupCommand) steps.push(`cd "${workdir}" && ${setupCommand}`);
|
|
1767
|
+
let template = createDefaultMountableTemplate().template;
|
|
1768
|
+
const env = { ...buildEnv };
|
|
1769
|
+
if (token) env[BUILD_TOKEN_ENV] = token;
|
|
1770
|
+
if (Object.keys(env).length > 0) template = template.setEnvs(env);
|
|
1771
|
+
template = template.runCmd(steps);
|
|
1772
|
+
return {
|
|
1773
|
+
ref: repoTemplateRef(identity),
|
|
1774
|
+
template,
|
|
1775
|
+
staleRef: `${repoTemplateName(identity)}:${CURRENT_TAG}`,
|
|
1776
|
+
buildTags: [CURRENT_TAG],
|
|
1777
|
+
buildResources: {
|
|
1778
|
+
cpuCount: identity.cpuCount ?? 2,
|
|
1779
|
+
memoryMB: identity.memoryMB ?? 1024
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Resolve the repository's current default-branch head over HTTPS
|
|
1785
|
+
* (`git ls-remote <url> HEAD` — no clone; authenticated via an in-process
|
|
1786
|
+
* `http.extraheader` when a token is provided). Returns undefined when the
|
|
1787
|
+
* head cannot be resolved (inaccessible repo, offline, no git binary);
|
|
1788
|
+
* callers degrade to the untagged template ref.
|
|
1789
|
+
*/
|
|
1790
|
+
async function resolveDefaultBranchHead(cloneUrl, token) {
|
|
1791
|
+
try {
|
|
1792
|
+
const authArgs = token ? ["-c", `http.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`] : [];
|
|
1793
|
+
const { stdout } = await execFileAsync("git", [
|
|
1794
|
+
...authArgs,
|
|
1795
|
+
"ls-remote",
|
|
1796
|
+
"--",
|
|
1797
|
+
cloneUrl,
|
|
1798
|
+
"HEAD"
|
|
1799
|
+
], {
|
|
1800
|
+
timeout: 1e4,
|
|
1801
|
+
env: {
|
|
1802
|
+
...process.env,
|
|
1803
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
const sha = stdout.split(/\s/, 1)[0];
|
|
1807
|
+
return sha && SHA_PATTERN.test(sha) ? sha : void 0;
|
|
1808
|
+
} catch {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Canonical form used for identity and for the build's clone: lowercase
|
|
1814
|
+
* host, no trailing `.git` or slash. Two spellings of one repository must
|
|
1815
|
+
* not produce two templates.
|
|
1816
|
+
*/
|
|
1817
|
+
function normalizeCloneUrl(cloneUrl) {
|
|
1818
|
+
let end = cloneUrl.length;
|
|
1819
|
+
while (end > 0 && cloneUrl[end - 1] === "/") end--;
|
|
1820
|
+
return cloneUrl.slice(0, end).replace(/\.git$/i, "").replace(/^(https:\/\/)([^/]+)/i, (_match, scheme, host) => {
|
|
1821
|
+
return `${scheme.toLowerCase()}${host.toLowerCase()}`;
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Split a normalized clone URL into its host and trailing owner/repo pair.
|
|
1826
|
+
* Hosts that nest groups (GitLab subgroups) keep only the last two path
|
|
1827
|
+
* segments as owner/repo; the full URL still drives identity.
|
|
1828
|
+
*/
|
|
1829
|
+
function parseCloneUrl(cloneUrl) {
|
|
1830
|
+
const [host = "", ...segments] = normalizeCloneUrl(cloneUrl).replace(/^https:\/\//i, "").split("/");
|
|
1831
|
+
const repo = segments.at(-1) ?? "";
|
|
1832
|
+
return {
|
|
1833
|
+
host,
|
|
1834
|
+
owner: segments.length > 1 ? segments.at(-2) ?? "" : "",
|
|
1835
|
+
repo
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function defaultWorkdir(cloneUrl) {
|
|
1839
|
+
const { repo } = parseCloneUrl(cloneUrl);
|
|
1840
|
+
return `$HOME/${repo.replace(/[^\w.-]/g, "-").replace(/^\.+/, "") || "repo"}`;
|
|
1841
|
+
}
|
|
1842
|
+
//#endregion
|
|
1372
1843
|
//#region src/provider.ts
|
|
1373
1844
|
const e2bSandboxProvider = {
|
|
1374
1845
|
id: "e2b",
|
|
@@ -1619,10 +2090,16 @@ var E2BCodeModeTransport = class {
|
|
|
1619
2090
|
}
|
|
1620
2091
|
};
|
|
1621
2092
|
//#endregion
|
|
2093
|
+
exports.DEFAULT_NODE_VERSION = DEFAULT_NODE_VERSION;
|
|
1622
2094
|
exports.E2BCodeModeTransport = E2BCodeModeTransport;
|
|
1623
2095
|
exports.E2BProcessManager = E2BProcessManager;
|
|
1624
2096
|
exports.E2BSandbox = E2BSandbox;
|
|
1625
2097
|
exports.createDefaultMountableTemplate = createDefaultMountableTemplate;
|
|
2098
|
+
exports.createRepoTemplate = createRepoTemplate;
|
|
1626
2099
|
exports.e2bSandboxProvider = e2bSandboxProvider;
|
|
2100
|
+
exports.isDeferredNamedTemplateSpec = isDeferredNamedTemplateSpec;
|
|
2101
|
+
exports.isNamedTemplateSpec = isNamedTemplateSpec;
|
|
2102
|
+
exports.refreshRepoTemplate = refreshRepoTemplate;
|
|
2103
|
+
exports.repoTemplateRef = repoTemplateRef;
|
|
1627
2104
|
|
|
1628
2105
|
//# sourceMappingURL=index.cjs.map
|