@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/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: "v1",
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
- /** Resolved template ID after building (if needed) */
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
- /** Promise for template preparation (started in constructor) */
659
- _templatePreparePromise;
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
- let resolvedTemplateId = await (this._templatePreparePromise || this.resolveTemplate());
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 this.createSdkSandbox(resolvedTemplateId, createOpts);
832
+ sdkSandbox = await createFromTemplate(resolvedTemplateId);
793
833
  } catch (createError) {
794
- const errorStr = String(createError);
795
- if (errorStr.includes("404") && errorStr.includes("not found") && !this.templateSpec) {
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 this.createSdkSandbox(rebuiltTemplateId, createOpts);
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 this.templateSpec === "function") {
1357
+ if (typeof spec === "function") {
1260
1358
  const { template: baseTemplate } = createDefaultMountableTemplate();
1261
- template = this.templateSpec(baseTemplate);
1359
+ template = spec(baseTemplate);
1262
1360
  templateName = `mastra-custom-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
1263
1361
  } else {
1264
- template = this.templateSpec;
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, this.connectionOpts);
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