@mastra/e2b 0.10.0 → 0.11.0-alpha.1

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