@mastra/platform-workspace 1.4.1 → 1.5.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
@@ -26,6 +26,8 @@ let path = require("path");
26
26
  path = __toESM(path, 1);
27
27
  let _mastra_core_workspace = require("@mastra/core/workspace");
28
28
  let e2b = require("e2b");
29
+ let child_process = require("child_process");
30
+ let util = require("util");
29
31
  //#region src/client.ts
30
32
  const DEFAULT_PROXY_URL = "https://workspaces.mastra.ai";
31
33
  /**
@@ -39,19 +41,19 @@ function requireOption(value, name) {
39
41
  return value;
40
42
  }
41
43
  function resolveSandboxProvider(value) {
42
- const provider = value?.trim() || "railway";
44
+ const provider = value?.trim() || "e2b";
43
45
  if (provider !== "railway" && provider !== "e2b") throw new Error("SANDBOX_PROVIDER must be either \"railway\" or \"e2b\"");
44
46
  return provider;
45
47
  }
46
48
  function resolvePlatformOptions(options) {
47
- const configuredSandboxProvider = process.env.SANDBOX_PROVIDER?.trim();
49
+ const environmentSandboxProvider = process.env.SANDBOX_PROVIDER?.trim();
50
+ const configuredSandboxProvider = options.sandboxProvider ?? environmentSandboxProvider;
48
51
  return {
49
52
  accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_ACCESS_TOKEN, "accessToken"),
50
53
  projectId: requireOption(options.projectId ?? process.env.MASTRA_PROJECT_ID, "projectId"),
51
54
  actingUserId: options.actingUserId?.trim() || void 0,
52
55
  proxyUrl: (process.env.MASTRA_WORKSPACE_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/$/, ""),
53
56
  sandboxProvider: resolveSandboxProvider(configuredSandboxProvider),
54
- useLegacyRoutes: !configuredSandboxProvider,
55
57
  sessionId: options.sessionId,
56
58
  threadId: options.threadId,
57
59
  fetch: options.fetch ?? fetch
@@ -99,7 +101,6 @@ var PlatformClient = class {
99
101
  actingUserId;
100
102
  proxyUrl;
101
103
  sandboxProvider;
102
- useLegacyRoutes;
103
104
  /** Advisory session correlation id — see {@link PlatformClientOptions.sessionId}. */
104
105
  sessionId;
105
106
  /** Advisory thread correlation id — see {@link PlatformClientOptions.threadId}. */
@@ -112,13 +113,17 @@ var PlatformClient = class {
112
113
  this.actingUserId = resolved.actingUserId;
113
114
  this.proxyUrl = resolved.proxyUrl;
114
115
  this.sandboxProvider = resolved.sandboxProvider;
115
- this.useLegacyRoutes = resolved.useLegacyRoutes;
116
116
  this.sessionId = resolved.sessionId;
117
117
  this.threadId = resolved.threadId;
118
118
  this.fetch = resolved.fetch;
119
119
  }
120
120
  async request(path, options = {}) {
121
- const providerPath = this.useLegacyRoutes ? "" : `/${this.sandboxProvider}`;
121
+ return this.requestAtPath(`/${this.sandboxProvider}`, path, options);
122
+ }
123
+ async requestProvider(path, options = {}) {
124
+ return this.requestAtPath(`/${this.sandboxProvider}`, path, options);
125
+ }
126
+ async requestAtPath(providerPath, path, options) {
122
127
  const url = new URL(`${this.proxyUrl}/v1${providerPath}/projects/${encodeURIComponent(this.projectId)}${path}`);
123
128
  for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, String(value));
124
129
  const headers = new Headers(options.headers);
@@ -386,6 +391,185 @@ function matchesExtension(name, extension) {
386
391
  return (Array.isArray(extension) ? extension : [extension]).some((ext) => name.endsWith(ext));
387
392
  }
388
393
  //#endregion
394
+ //#region src/template.ts
395
+ const SERIALIZE_TEMPLATE = Symbol("serializeTemplate");
396
+ const GET_TEMPLATE_BUILD_ENVS = Symbol("getTemplateBuildEnvs");
397
+ const MAX_OPERATIONS = 256;
398
+ const MAX_SERIALIZED_BYTES = 256 * 1024;
399
+ const MAX_STRING_LENGTH = 32 * 1024;
400
+ const MAX_COLLECTION_ITEMS = 512;
401
+ const MAX_FAMILY_LENGTH = 200;
402
+ var SerializableSandboxTemplateBuilder = class SerializableSandboxTemplateBuilder {
403
+ #operations;
404
+ #family;
405
+ #buildEnvs;
406
+ constructor(operations = [], family, buildEnvs = {}) {
407
+ this.#operations = operations;
408
+ this.#family = family;
409
+ this.#buildEnvs = buildEnvs;
410
+ }
411
+ cpuCount(count) {
412
+ return this.#append("cpuCount", [validateResourceValue(count, "count")]);
413
+ }
414
+ memoryMB(memoryMB) {
415
+ return this.#append("memoryMB", [validateResourceValue(memoryMB, "memoryMB")]);
416
+ }
417
+ async build(options = {}) {
418
+ return buildSandboxTemplate(this, options);
419
+ }
420
+ runCmd(command) {
421
+ return this.#append("runCmd", [validateStringOrStrings(command, "command")]);
422
+ }
423
+ setWorkdir(path) {
424
+ return this.#append("setWorkdir", [validateString(path, "path")]);
425
+ }
426
+ setEnvs(envs, options) {
427
+ const copy = validateStringRecord(envs, "envs", "environment variable");
428
+ if ((options === void 0 ? void 0 : validateBooleanOptions(options, ["ephemeral"]))?.ephemeral === true) return new SerializableSandboxTemplateBuilder(this.#operations, this.#family, {
429
+ ...this.#buildEnvs,
430
+ ...copy
431
+ });
432
+ return this.#append("setEnvs", [copy]);
433
+ }
434
+ aptInstall(packages, options) {
435
+ const args = [validateStringOrStrings(packages, "packages")];
436
+ if (options !== void 0) args.push(validateBooleanOptions(options, ["noInstallRecommends", "fixMissing"]));
437
+ return this.#append("aptInstall", args);
438
+ }
439
+ pipInstall(packages, options) {
440
+ return this.#appendOptionalInstall("pipInstall", packages, options, ["g"]);
441
+ }
442
+ npmInstall(packages, options) {
443
+ return this.#appendOptionalInstall("npmInstall", packages, options, ["g", "dev"]);
444
+ }
445
+ withFamily(family) {
446
+ if (typeof family !== "string" || family.length === 0) throw new TypeError("family must be a non-empty string");
447
+ if (family.length > MAX_FAMILY_LENGTH) throw new RangeError(`family cannot exceed ${MAX_FAMILY_LENGTH} characters`);
448
+ assertSerializedSize(this.#operations, family);
449
+ return new SerializableSandboxTemplateBuilder(this.#operations, family, this.#buildEnvs);
450
+ }
451
+ [GET_TEMPLATE_BUILD_ENVS]() {
452
+ return Object.keys(this.#buildEnvs).length > 0 ? { ...this.#buildEnvs } : void 0;
453
+ }
454
+ [SERIALIZE_TEMPLATE]() {
455
+ return {
456
+ schemaVersion: 1,
457
+ operations: this.#operations.map((operation) => ({
458
+ method: operation.method,
459
+ args: cloneJson(operation.args)
460
+ })),
461
+ ...this.#family !== void 0 && { family: this.#family }
462
+ };
463
+ }
464
+ #appendOptionalInstall(method, packages, options, optionKeys) {
465
+ const args = [];
466
+ if (packages !== void 0) args.push(validateStringOrStrings(packages, "packages"));
467
+ if (options !== void 0) {
468
+ if (packages === void 0) args.push(null);
469
+ args.push(validateBooleanOptions(options, optionKeys));
470
+ }
471
+ return this.#append(method, args);
472
+ }
473
+ #append(method, args) {
474
+ if (this.#operations.length >= MAX_OPERATIONS) throw new RangeError(`Sandbox template cannot contain more than ${MAX_OPERATIONS} operations`);
475
+ const operation = {
476
+ method,
477
+ args: cloneJson(args)
478
+ };
479
+ const operations = [...this.#operations, operation];
480
+ assertSerializedSize(operations, this.#family);
481
+ return new SerializableSandboxTemplateBuilder(operations, this.#family, this.#buildEnvs);
482
+ }
483
+ };
484
+ function assertSerializedSize(operations, family) {
485
+ const serialized = JSON.stringify({
486
+ schemaVersion: 1,
487
+ operations,
488
+ ...family !== void 0 && { family }
489
+ });
490
+ if (new TextEncoder().encode(serialized).byteLength > MAX_SERIALIZED_BYTES) throw new RangeError(`Serialized sandbox template cannot exceed ${MAX_SERIALIZED_BYTES} bytes`);
491
+ }
492
+ function Template() {
493
+ return new SerializableSandboxTemplateBuilder();
494
+ }
495
+ function isSandboxTemplateBuilder(value) {
496
+ return value instanceof SerializableSandboxTemplateBuilder;
497
+ }
498
+ function serializeSandboxTemplate(template) {
499
+ if (!isSandboxTemplateBuilder(template)) throw new TypeError("template must be created with Template()");
500
+ return template[SERIALIZE_TEMPLATE]();
501
+ }
502
+ function getSandboxTemplateBuildEnvs(template) {
503
+ if (!isSandboxTemplateBuilder(template)) throw new TypeError("template must be created with Template()");
504
+ return template[GET_TEMPLATE_BUILD_ENVS]();
505
+ }
506
+ async function buildSandboxTemplate(template, options) {
507
+ const environmentId = options.environmentId ?? process.env.MASTRA_ENVIRONMENT_ID;
508
+ if (!environmentId) throw new Error("environmentId is required. Pass it or set MASTRA_ENVIRONMENT_ID.");
509
+ const client = new PlatformClient(options);
510
+ const templateBuildEnvs = getSandboxTemplateBuildEnvs(template);
511
+ return await (await client.requestProvider("/sandbox/templates/builds", {
512
+ method: "POST",
513
+ headers: { "content-type": "application/json" },
514
+ body: JSON.stringify({
515
+ environmentId,
516
+ templateDefinition: serializeSandboxTemplate(template),
517
+ ...templateBuildEnvs !== void 0 && { templateBuildEnvs }
518
+ })
519
+ })).json();
520
+ }
521
+ function validateResourceValue(value, name) {
522
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive safe integer`);
523
+ return value;
524
+ }
525
+ function validateString(value, name, allowEmpty = false) {
526
+ if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
527
+ if (!allowEmpty && value.length === 0) throw new TypeError(`${name} must not be empty`);
528
+ if (value.length > MAX_STRING_LENGTH) throw new RangeError(`${name} cannot exceed ${MAX_STRING_LENGTH} characters`);
529
+ return value;
530
+ }
531
+ function validateStringOrStrings(value, name) {
532
+ if (typeof value === "string") return validateString(value, name);
533
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be a string or an array of strings`);
534
+ assertCollectionSize(value.length, name);
535
+ if (value.length === 0) throw new TypeError(`${name} must not be empty`);
536
+ return Array.from(value, (item, index) => validateString(item, `${name}[${index}]`));
537
+ }
538
+ function validateStringRecord(value, name, entryName) {
539
+ assertPlainObject(value, name);
540
+ const entries = Object.entries(value);
541
+ assertCollectionSize(entries.length, name);
542
+ return Object.fromEntries(entries.map(([key, item]) => [validateString(key, `${entryName} name`), validateString(item, `${entryName} ${key}`, true)]));
543
+ }
544
+ function validateBooleanOptions(value, keys) {
545
+ assertPlainObject(value, "options");
546
+ const options = value;
547
+ const unknownKey = Object.keys(options).find((key) => !keys.includes(key));
548
+ if (unknownKey) throw new TypeError(`Unsupported option: ${unknownKey}`);
549
+ const copy = {};
550
+ for (const key of keys) {
551
+ const option = options[key];
552
+ if (option === void 0) continue;
553
+ if (typeof option !== "boolean") throw new TypeError(`${key} must be a boolean`);
554
+ copy[key] = option;
555
+ }
556
+ return copy;
557
+ }
558
+ function assertPlainObject(value, name) {
559
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${name} must be a plain object`);
560
+ const prototype = Object.getPrototypeOf(value);
561
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`${name} must be a plain object`);
562
+ }
563
+ function assertCollectionSize(size, name) {
564
+ if (size > MAX_COLLECTION_ITEMS) throw new RangeError(`${name} cannot contain more than ${MAX_COLLECTION_ITEMS} items`);
565
+ }
566
+ function cloneJson(value) {
567
+ if (Array.isArray(value)) return value.map((item) => cloneJson(item));
568
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneJson(item)]));
569
+ if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError("Sandbox template values must contain only finite numbers");
570
+ return value;
571
+ }
572
+ //#endregion
389
573
  //#region src/direct-exec.ts
390
574
  /**
391
575
  * Direct exec client — opens Railway's tcp-proxy exec WebSocket directly using
@@ -926,10 +1110,26 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
926
1110
  name = "PlatformSandbox";
927
1111
  provider = "platform";
928
1112
  status = "pending";
1113
+ /**
1114
+ * Populated from the platform's create/reattach response when the sandbox
1115
+ * booted from a prior member of the same template family or the provider
1116
+ * base template while the requested exact template continues to build in
1117
+ * the background.
1118
+ * `undefined` when the sandbox booted on the exact template (or when no
1119
+ * template was requested). Observability-only; consumers reconcile freshness
1120
+ * in their own runtime setup and reprovision to pick up the ready template
1121
+ * on a later start.
1122
+ */
1123
+ templatePending;
929
1124
  _client;
1125
+ _usesProviderRoutes;
930
1126
  _environmentId;
931
1127
  _sandboxId;
932
1128
  _seedCheckpointName;
1129
+ _templateDefinition;
1130
+ _templateBuildEnvs;
1131
+ _template;
1132
+ _templateResolutionInFlight;
933
1133
  _idleTimeoutMinutes;
934
1134
  _networkIsolation;
935
1135
  _env;
@@ -1017,6 +1217,8 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1017
1217
  if (!this._environmentId && !options.sandboxId) throw new Error("environmentId is required");
1018
1218
  this._sandboxId = options.sandboxId;
1019
1219
  this._seedCheckpointName = options.seedCheckpointName;
1220
+ this._usesProviderRoutes = options.template !== void 0;
1221
+ this._template = options.template;
1020
1222
  this._idleTimeoutMinutes = options.idleTimeoutMinutes;
1021
1223
  this._networkIsolation = options.networkIsolation;
1022
1224
  this._env = options.env ?? {};
@@ -1030,6 +1232,9 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1030
1232
  generateId() {
1031
1233
  return `platform-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1032
1234
  }
1235
+ _request(path, options = {}) {
1236
+ return this._usesProviderRoutes ? this._client.requestProvider(path, options) : this._client.request(path, options);
1237
+ }
1033
1238
  /**
1034
1239
  * Construct a sibling {@link PlatformSandbox} that inherits this sandbox's
1035
1240
  * credentials and defaults (access token, project, environment, network
@@ -1045,10 +1250,11 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1045
1250
  clone(options = {}) {
1046
1251
  const id = options.id ?? options.checkpointName;
1047
1252
  const seedCheckpointName = options.seedCheckpointName ?? (this._client.sandboxProvider === "e2b" ? options.checkpointName : void 0) ?? this._seedCheckpointName;
1048
- return new PlatformSandbox({
1253
+ const clone = new PlatformSandbox({
1049
1254
  ...id !== void 0 && { id },
1050
1255
  accessToken: this._client.accessToken,
1051
1256
  projectId: this._client.projectId,
1257
+ ...this._usesProviderRoutes || this._client.sandboxProvider !== "railway" ? { sandboxProvider: this._client.sandboxProvider } : {},
1052
1258
  actingUserId: options.actingUserId ?? this._client.actingUserId,
1053
1259
  ...this._client.sessionId !== void 0 && { sessionId: this._client.sessionId },
1054
1260
  ...this._client.threadId !== void 0 && { threadId: this._client.threadId },
@@ -1056,6 +1262,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1056
1262
  environmentId: this._environmentId,
1057
1263
  ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
1058
1264
  ...seedCheckpointName !== void 0 && { seedCheckpointName },
1265
+ ...this._template !== void 0 && { template: this._template },
1059
1266
  idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
1060
1267
  ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
1061
1268
  env: options.env ?? this._env,
@@ -1066,6 +1273,9 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1066
1273
  ...this._privateNetFetch !== void 0 && { privateNetFetch: this._privateNetFetch },
1067
1274
  ...this._addressRegistry !== void 0 && { addressRegistry: this._addressRegistry }
1068
1275
  });
1276
+ clone._templateDefinition = this._templateDefinition ? structuredClone(this._templateDefinition) : void 0;
1277
+ clone._templateBuildEnvs = this._templateBuildEnvs ? { ...this._templateBuildEnvs } : void 0;
1278
+ return clone;
1069
1279
  }
1070
1280
  /**
1071
1281
  * Start the sandbox: reattach to a known provider `sandboxId` when one is
@@ -1084,7 +1294,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1084
1294
  const startedAt = Date.now();
1085
1295
  if (this._sandboxId) try {
1086
1296
  const requestStartedAt = Date.now();
1087
- const response = await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
1297
+ const response = await this._request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
1088
1298
  const requestMs = Date.now() - requestStartedAt;
1089
1299
  const json = await response.json();
1090
1300
  if (!json.destroyedAt) {
@@ -1099,9 +1309,12 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1099
1309
  this._sandboxId = void 0;
1100
1310
  }
1101
1311
  if (!this._environmentId) throw new Error("environmentId is required");
1102
- const body = JSON.stringify({
1312
+ await this._prepareLazyTemplate();
1313
+ const createBody = () => JSON.stringify({
1103
1314
  id: this.id,
1104
1315
  seedCheckpointName: this._seedCheckpointName,
1316
+ templateDefinition: this._templateDefinition,
1317
+ templateBuildEnvs: this._templateBuildEnvs,
1105
1318
  environmentId: this._environmentId,
1106
1319
  idleTimeoutMinutes: this._idleTimeoutMinutes,
1107
1320
  networkIsolation: this._networkIsolation,
@@ -1110,10 +1323,10 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1110
1323
  let response;
1111
1324
  const requestStartedAt = Date.now();
1112
1325
  for (let attempt = 1;; attempt++) try {
1113
- response = await this._client.request("/sandbox", {
1326
+ response = await this._request("/sandbox", {
1114
1327
  method: "POST",
1115
1328
  headers: { "content-type": "application/json" },
1116
- body
1329
+ body: createBody()
1117
1330
  });
1118
1331
  break;
1119
1332
  } catch (error) {
@@ -1124,10 +1337,31 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1124
1337
  const json = await response.json();
1125
1338
  this._sandboxId = json.id;
1126
1339
  this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
1340
+ this.templatePending = json.templatePending;
1127
1341
  this._populateAddressFromResponse(json);
1128
1342
  this._logStartComplete(json.id, startedAt, requestMs, "provision");
1129
1343
  return { outcome: "created" };
1130
1344
  }
1345
+ async _prepareLazyTemplate() {
1346
+ if (this._templateDefinition !== void 0 || this._template === void 0) return;
1347
+ if (!this._templateResolutionInFlight) {
1348
+ const attempt = this._resolveTemplate();
1349
+ this._templateResolutionInFlight = attempt.finally(() => {
1350
+ this._templateResolutionInFlight = void 0;
1351
+ });
1352
+ }
1353
+ await this._templateResolutionInFlight;
1354
+ }
1355
+ async _resolveTemplate() {
1356
+ try {
1357
+ const resolved = typeof this._template === "function" ? await this._template() : this._template;
1358
+ if (!resolved) return;
1359
+ this._templateDefinition = serializeSandboxTemplate(resolved);
1360
+ this._templateBuildEnvs = getSandboxTemplateBuildEnvs(resolved);
1361
+ } catch (error) {
1362
+ this.logger.warn(`Platform sandbox template resolution failed; using provider default template: ${String(error)}`);
1363
+ }
1364
+ }
1131
1365
  /**
1132
1366
  * One timing summary per completed `start()` — the whole
1133
1367
  * `PlatformSandbox`-visible boot in a single greppable line.
@@ -1292,7 +1526,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1292
1526
  const destroyedSandboxId = this._sandboxId;
1293
1527
  this._captureInFlight = null;
1294
1528
  if (this._hasRecoveryKey || this._client.sandboxProvider === "e2b") try {
1295
- await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}/checkpoint`, {
1529
+ await this._request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}/checkpoint`, {
1296
1530
  method: "DELETE",
1297
1531
  headers: { "content-type": "application/json" },
1298
1532
  body: JSON.stringify({ id: this.id })
@@ -1319,7 +1553,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1319
1553
  this._probeGeneration++;
1320
1554
  this._probeTarget = null;
1321
1555
  this._transportReadyPromise = null;
1322
- await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
1556
+ await this._request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
1323
1557
  this._sandboxId = void 0;
1324
1558
  this._createdAt = null;
1325
1559
  this._lease = null;
@@ -1409,7 +1643,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1409
1643
  async _doCaptureCheckpoint(sandboxId) {
1410
1644
  let response;
1411
1645
  try {
1412
- response = await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/checkpoint`, {
1646
+ response = await this._request(`/sandbox/${encodeURIComponent(sandboxId)}/checkpoint`, {
1413
1647
  method: "POST",
1414
1648
  headers: { "content-type": "application/json" },
1415
1649
  body: JSON.stringify({ id: this.id })
@@ -1540,8 +1774,20 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1540
1774
  * Returns a result with a real `exitCode` OR `timedOut: true`. Never
1541
1775
  * returns `{ exitCode: null, timedOut: false }` — that case throws.
1542
1776
  */
1777
+ /**
1778
+ * Drop undefined values so the result matches the Record<string, string>
1779
+ * shape the exec transports expect (`ExecuteCommandOptions.env` is
1780
+ * NodeJS.ProcessEnv). The sandbox's own env is already merged in by
1781
+ * `executeCommand` before a transport sees these options. Returns undefined
1782
+ * when there is nothing to send.
1783
+ */
1784
+ _execEnv(options) {
1785
+ if (!options?.env) return void 0;
1786
+ const filtered = Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0));
1787
+ return Object.keys(filtered).length > 0 ? filtered : void 0;
1788
+ }
1543
1789
  async _runDirectExec(fullCommand, effectiveTimeout, options) {
1544
- const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
1790
+ const filteredEnv = this._execEnv(options);
1545
1791
  let lastResult;
1546
1792
  let lastLease;
1547
1793
  let attemptsMade = 0;
@@ -1604,7 +1850,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1604
1850
  * sidecar bug). Only this specific exec falls back.
1605
1851
  */
1606
1852
  async _tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options) {
1607
- const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
1853
+ const filteredEnv = this._execEnv(options);
1608
1854
  const execOptions = {
1609
1855
  command: fullCommand,
1610
1856
  ...options?.cwd !== void 0 && { cwd: options.cwd },
@@ -1653,7 +1899,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1653
1899
  if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
1654
1900
  const sandboxId = this._sandboxId;
1655
1901
  const inFlight = (async () => {
1656
- const json = await (await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/exec-lease`, { method: "POST" })).json();
1902
+ const json = await (await this._request(`/sandbox/${encodeURIComponent(sandboxId)}/exec-lease`, { method: "POST" })).json();
1657
1903
  const expiresAtMs = json.expiresAt ? Date.parse(json.expiresAt) : null;
1658
1904
  const lease = {
1659
1905
  provider: json.provider,
@@ -1691,7 +1937,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1691
1937
  createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
1692
1938
  metadata: { sandboxId: this._sandboxId }
1693
1939
  };
1694
- const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
1940
+ const json = await (await this._request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
1695
1941
  return {
1696
1942
  id: json.id,
1697
1943
  name: this.name,
@@ -1716,6 +1962,122 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1716
1962
  }
1717
1963
  };
1718
1964
  //#endregion
1965
+ //#region src/repo-template.ts
1966
+ const execFileAsync = (0, util.promisify)(child_process.execFile);
1967
+ const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
1968
+ const BUILD_TOKEN_ENV = "MASTRA_REPOSITORY_ACCESS_TOKEN";
1969
+ /**
1970
+ * Clone URLs interpolate into the template's build commands, so constrain
1971
+ * them to https plus plain host/path characters. Every regex here is a
1972
+ * single anchored character class, so matching stays linear on adversarial
1973
+ * input; the structural checks go through WHATWG URL parsing instead of one
1974
+ * big backtracking pattern.
1975
+ */
1976
+ const CLONE_URL_ALLOWED_CHARS = /^[a-z0-9:/._-]+$/i;
1977
+ const CLONE_URL_HOST_PATTERN = /^[a-z0-9.-]+$/i;
1978
+ const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
1979
+ /**
1980
+ * Create a lazy repository template definition for PlatformSandbox, mirroring
1981
+ * `@mastra/e2b`'s `createRepoTemplate`: pass the sandbox context through and a
1982
+ * repo-less session boots the provider default.
1983
+ *
1984
+ * The resolver performs no work until a fresh sandbox starts. It clones the
1985
+ * URL `getRepositoryAccess` resolves — the only source of the clone URL, so
1986
+ * what gets cloned and what the template is identified by can't drift — and
1987
+ * pins repositories to their current default-branch commit. Private repository
1988
+ * credentials are used for head resolution and sent to the provider as
1989
+ * transient build envs; they never enter the serialized definition. If the
1990
+ * head cannot be resolved, the resolver returns undefined so PlatformSandbox
1991
+ * boots from the provider default and the caller's runtime setup materializes
1992
+ * the checkout instead.
1993
+ */
1994
+ function createRepoTemplate(options) {
1995
+ const getRepositoryAccess = options.getRepositoryAccess;
1996
+ if (!getRepositoryAccess) return void 0;
1997
+ const resolveHead = options.resolveHead ?? resolveDefaultBranchHead;
1998
+ return async () => {
1999
+ const access = await getRepositoryAccess().catch(() => void 0);
2000
+ if (!access?.cloneUrl) return void 0;
2001
+ const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2002
+ if (!isValidCloneUrl(cloneUrl)) return void 0;
2003
+ const token = access.authorization?.token;
2004
+ const sha = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch(() => void 0);
2005
+ if (!sha || !SHA_PATTERN.test(sha)) return void 0;
2006
+ const workdir = defaultWorkdir(cloneUrl);
2007
+ const auth = token ? `${gitAuthFlag()} ` : "";
2008
+ const steps = [
2009
+ `git ${auth}clone ${cloneUrl} "${workdir}"`,
2010
+ `git -C "${workdir}" ${auth}fetch origin ${sha}`,
2011
+ `git -C "${workdir}" checkout ${sha}`,
2012
+ ...options.setupCommand ? [`cd "${workdir}" && ${options.setupCommand}`] : []
2013
+ ];
2014
+ const family = `repo:${cloneUrl}:${workdir}`;
2015
+ let template = Template();
2016
+ if (token) template = template.setEnvs({ [BUILD_TOKEN_ENV]: token }, { ephemeral: true });
2017
+ if (options.cpuCount !== void 0) template = template.cpuCount(options.cpuCount);
2018
+ if (options.memoryMB !== void 0) template = template.memoryMB(options.memoryMB);
2019
+ return template.runCmd(steps).withFamily(family);
2020
+ };
2021
+ }
2022
+ function isValidCloneUrl(cloneUrl) {
2023
+ if (cloneUrl.length > 2048 || !CLONE_URL_ALLOWED_CHARS.test(cloneUrl)) return false;
2024
+ let url;
2025
+ try {
2026
+ url = new URL(cloneUrl);
2027
+ } catch {
2028
+ return false;
2029
+ }
2030
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) return false;
2031
+ if (!CLONE_URL_HOST_PATTERN.test(url.hostname)) return false;
2032
+ const segments = url.pathname.split("/").slice(1);
2033
+ return segments.length > 0 && segments.every((segment) => CLONE_URL_SEGMENT_PATTERN.test(segment));
2034
+ }
2035
+ /**
2036
+ * Canonical form used for identity, the family key, and the build's clone:
2037
+ * lowercase host, no trailing `.git` or slash. Two spellings of one
2038
+ * repository must not produce two templates.
2039
+ */
2040
+ function normalizeCloneUrl(cloneUrl) {
2041
+ let end = cloneUrl.length;
2042
+ while (end > 0 && cloneUrl[end - 1] === "/") end--;
2043
+ return cloneUrl.slice(0, end).replace(/\.git$/i, "").replace(/^(https:\/\/)([^/]+)/i, (_match, scheme, host) => {
2044
+ return `${scheme.toLowerCase()}${host.toLowerCase()}`;
2045
+ });
2046
+ }
2047
+ function defaultWorkdir(cloneUrl) {
2048
+ return `$HOME/${(normalizeCloneUrl(cloneUrl).split("/").at(-1) ?? "").replace(/[^\w.-]/g, "-").replace(/^\.+/, "") || "repo"}`;
2049
+ }
2050
+ function gitAuthFlag() {
2051
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
2052
+ }
2053
+ async function resolveDefaultBranchHead(cloneUrl, token, execute = execFileAsync) {
2054
+ try {
2055
+ const env = {
2056
+ ...process.env,
2057
+ GIT_TERMINAL_PROMPT: "0"
2058
+ };
2059
+ if (token) {
2060
+ env.GIT_CONFIG_COUNT = "1";
2061
+ env.GIT_CONFIG_KEY_0 = "http.extraheader";
2062
+ env.GIT_CONFIG_VALUE_0 = `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
2063
+ }
2064
+ const { stdout } = await execute("git", [
2065
+ "ls-remote",
2066
+ "--",
2067
+ cloneUrl,
2068
+ "HEAD"
2069
+ ], {
2070
+ timeout: 1e4,
2071
+ maxBuffer: 1024 * 1024,
2072
+ env
2073
+ });
2074
+ const sha = stdout.trim().split(/\s+/, 1)[0];
2075
+ return sha && SHA_PATTERN.test(sha) ? sha : void 0;
2076
+ } catch {
2077
+ return;
2078
+ }
2079
+ }
2080
+ //#endregion
1719
2081
  //#region src/provider.ts
1720
2082
  const platformSandboxProvider = {
1721
2083
  id: "platform",
@@ -1736,6 +2098,12 @@ const platformSandboxProvider = {
1736
2098
  type: "string",
1737
2099
  description: "Opaque user subject attributed to sandbox requests"
1738
2100
  },
2101
+ sandboxProvider: {
2102
+ type: "string",
2103
+ description: "Sandbox provider (falls back to SANDBOX_PROVIDER, then e2b)",
2104
+ enum: ["railway", "e2b"],
2105
+ default: "e2b"
2106
+ },
1739
2107
  environmentId: {
1740
2108
  type: "string",
1741
2109
  description: "Platform environment ID (falls back to MASTRA_ENVIRONMENT_ID)"
@@ -1837,6 +2205,8 @@ exports.PlatformSandbox = PlatformSandbox;
1837
2205
  exports.PrivateNetExecHttpError = PrivateNetExecHttpError;
1838
2206
  exports.SandboxDestroyedError = SandboxDestroyedError;
1839
2207
  exports.SandboxExecTransportError = SandboxExecTransportError;
2208
+ exports.Template = Template;
2209
+ exports.createRepoTemplate = createRepoTemplate;
1840
2210
  exports.execViaPrivateNetwork = execViaPrivateNetwork;
1841
2211
  exports.platformFilesystemProvider = platformFilesystemProvider;
1842
2212
  exports.platformSandboxProvider = platformSandboxProvider;