@mastra/platform-workspace 1.4.1 → 1.5.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,8 @@
1
- export { PlatformClient, PlatformApiError, type PlatformClientOptions, type PlatformProxyError } from './client.js';
1
+ export { PlatformClient, PlatformApiError, type PlatformClientOptions, type PlatformProxyError, type SandboxProvider, } from './client.js';
2
2
  export { PlatformFilesystem, type PlatformFilesystemOptions } from './filesystem.js';
3
- export { PlatformSandbox, SandboxExecTransportError, SandboxDestroyedError, type PlatformSandboxOptions, type PlatformSandboxNetworkIsolation, type SandboxAddressRegistry, } from './sandbox.js';
3
+ export { Template, type AptInstallOptions, type JsonPrimitive, type JsonValue, type NpmInstallOptions, type PipInstallOptions, type SandboxTemplateBuildOptions, type SandboxTemplateBuildResult, type SandboxTemplateBuilder, type SandboxTemplateMethod, type SetEnvsOptions, type SandboxTemplateOperation, type SerializedSandboxTemplate, } from './template.js';
4
+ export { PlatformSandbox, SandboxExecTransportError, SandboxDestroyedError, type PlatformSandboxOptions, type PlatformSandboxNetworkIsolation, type PlatformSandboxTemplate, type SandboxAddressRegistry, type SandboxTemplatePending, } from './sandbox.js';
5
+ export { createRepoTemplate, type PlatformRepoTemplateOptions, type PlatformRepoTemplateResolver, } from './repo-template.js';
4
6
  export { platformFilesystemProvider, platformSandboxProvider } from './provider.js';
5
7
  export { execViaPrivateNetwork, PrivateNetExecHttpError, type PrivateNetExecOptions, type PrivateNetExecResult, type PrivateNetFetch, } from './private-net-exec.js';
6
8
  export { InProcessSandboxAddressRegistry } from './address-registry.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACpH,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EACL,QAAQ,EACR,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,GAClC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- import { Buffer } from "buffer";
1
+ import { Buffer as Buffer$1 } from "buffer";
2
2
  import nodePath from "path";
3
3
  import { FileExistsError, FileNotFoundError, MastraFilesystem, MastraSandbox, ProcessHandle, SandboxNotReadyError, SandboxProcessManager, UnsupportedStdinCloseError, WorkspaceReadOnlyError } from "@mastra/core/workspace";
4
4
  import { CommandExitError, Sandbox, TimeoutError } from "e2b";
5
+ import { execFile } from "child_process";
6
+ import { promisify } from "util";
5
7
  //#region src/client.ts
6
8
  const DEFAULT_PROXY_URL = "https://workspaces.mastra.ai";
7
9
  /**
@@ -15,19 +17,19 @@ function requireOption(value, name) {
15
17
  return value;
16
18
  }
17
19
  function resolveSandboxProvider(value) {
18
- const provider = value?.trim() || "railway";
20
+ const provider = value?.trim() || "e2b";
19
21
  if (provider !== "railway" && provider !== "e2b") throw new Error("SANDBOX_PROVIDER must be either \"railway\" or \"e2b\"");
20
22
  return provider;
21
23
  }
22
24
  function resolvePlatformOptions(options) {
23
- const configuredSandboxProvider = process.env.SANDBOX_PROVIDER?.trim();
25
+ const environmentSandboxProvider = process.env.SANDBOX_PROVIDER?.trim();
26
+ const configuredSandboxProvider = options.sandboxProvider ?? environmentSandboxProvider;
24
27
  return {
25
28
  accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_ACCESS_TOKEN, "accessToken"),
26
29
  projectId: requireOption(options.projectId ?? process.env.MASTRA_PROJECT_ID, "projectId"),
27
30
  actingUserId: options.actingUserId?.trim() || void 0,
28
31
  proxyUrl: (process.env.MASTRA_WORKSPACE_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/$/, ""),
29
32
  sandboxProvider: resolveSandboxProvider(configuredSandboxProvider),
30
- useLegacyRoutes: !configuredSandboxProvider,
31
33
  sessionId: options.sessionId,
32
34
  threadId: options.threadId,
33
35
  fetch: options.fetch ?? fetch
@@ -75,7 +77,6 @@ var PlatformClient = class {
75
77
  actingUserId;
76
78
  proxyUrl;
77
79
  sandboxProvider;
78
- useLegacyRoutes;
79
80
  /** Advisory session correlation id — see {@link PlatformClientOptions.sessionId}. */
80
81
  sessionId;
81
82
  /** Advisory thread correlation id — see {@link PlatformClientOptions.threadId}. */
@@ -88,13 +89,17 @@ var PlatformClient = class {
88
89
  this.actingUserId = resolved.actingUserId;
89
90
  this.proxyUrl = resolved.proxyUrl;
90
91
  this.sandboxProvider = resolved.sandboxProvider;
91
- this.useLegacyRoutes = resolved.useLegacyRoutes;
92
92
  this.sessionId = resolved.sessionId;
93
93
  this.threadId = resolved.threadId;
94
94
  this.fetch = resolved.fetch;
95
95
  }
96
96
  async request(path, options = {}) {
97
- const providerPath = this.useLegacyRoutes ? "" : `/${this.sandboxProvider}`;
97
+ return this.requestAtPath(`/${this.sandboxProvider}`, path, options);
98
+ }
99
+ async requestProvider(path, options = {}) {
100
+ return this.requestAtPath(`/${this.sandboxProvider}`, path, options);
101
+ }
102
+ async requestAtPath(providerPath, path, options) {
98
103
  const url = new URL(`${this.proxyUrl}/v1${providerPath}/projects/${encodeURIComponent(this.projectId)}${path}`);
99
104
  for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, String(value));
100
105
  const headers = new Headers(options.headers);
@@ -141,7 +146,7 @@ function nameFromPath(path) {
141
146
  }
142
147
  function contentToBody(content) {
143
148
  if (typeof content === "string") return content;
144
- return Buffer.from(content);
149
+ return Buffer$1.from(content);
145
150
  }
146
151
  function headerDate(headers, name) {
147
152
  const value = headers.get(name);
@@ -193,7 +198,7 @@ var PlatformFilesystem = class extends MastraFilesystem {
193
198
  if (isNotFound(error)) throw new FileNotFoundError(path);
194
199
  throw error;
195
200
  }
196
- const buffer = Buffer.from(await response.arrayBuffer());
201
+ const buffer = Buffer$1.from(await response.arrayBuffer());
197
202
  return options?.encoding ? buffer.toString(options.encoding) : buffer;
198
203
  }
199
204
  async writeFile(path, content, options) {
@@ -224,8 +229,8 @@ var PlatformFilesystem = class extends MastraFilesystem {
224
229
  * Use `writeFile` with distinct keys for concurrent writers.
225
230
  */
226
231
  async appendFile(path, content) {
227
- const existing = await this.exists(path) ? await this.readFile(path) : Buffer.alloc(0);
228
- await this.writeFile(path, Buffer.concat([Buffer.isBuffer(existing) ? existing : Buffer.from(existing), Buffer.from(content)]));
232
+ const existing = await this.exists(path) ? await this.readFile(path) : Buffer$1.alloc(0);
233
+ await this.writeFile(path, Buffer$1.concat([Buffer$1.isBuffer(existing) ? existing : Buffer$1.from(existing), Buffer$1.from(content)]));
229
234
  }
230
235
  async deleteFile(path, options) {
231
236
  await this.ensureReady();
@@ -362,6 +367,185 @@ function matchesExtension(name, extension) {
362
367
  return (Array.isArray(extension) ? extension : [extension]).some((ext) => name.endsWith(ext));
363
368
  }
364
369
  //#endregion
370
+ //#region src/template.ts
371
+ const SERIALIZE_TEMPLATE = Symbol("serializeTemplate");
372
+ const GET_TEMPLATE_BUILD_ENVS = Symbol("getTemplateBuildEnvs");
373
+ const MAX_OPERATIONS = 256;
374
+ const MAX_SERIALIZED_BYTES = 256 * 1024;
375
+ const MAX_STRING_LENGTH = 32 * 1024;
376
+ const MAX_COLLECTION_ITEMS = 512;
377
+ const MAX_FAMILY_LENGTH = 200;
378
+ var SerializableSandboxTemplateBuilder = class SerializableSandboxTemplateBuilder {
379
+ #operations;
380
+ #family;
381
+ #buildEnvs;
382
+ constructor(operations = [], family, buildEnvs = {}) {
383
+ this.#operations = operations;
384
+ this.#family = family;
385
+ this.#buildEnvs = buildEnvs;
386
+ }
387
+ cpuCount(count) {
388
+ return this.#append("cpuCount", [validateResourceValue(count, "count")]);
389
+ }
390
+ memoryMB(memoryMB) {
391
+ return this.#append("memoryMB", [validateResourceValue(memoryMB, "memoryMB")]);
392
+ }
393
+ async build(options = {}) {
394
+ return buildSandboxTemplate(this, options);
395
+ }
396
+ runCmd(command) {
397
+ return this.#append("runCmd", [validateStringOrStrings(command, "command")]);
398
+ }
399
+ setWorkdir(path) {
400
+ return this.#append("setWorkdir", [validateString(path, "path")]);
401
+ }
402
+ setEnvs(envs, options) {
403
+ const copy = validateStringRecord(envs, "envs", "environment variable");
404
+ if ((options === void 0 ? void 0 : validateBooleanOptions(options, ["ephemeral"]))?.ephemeral === true) return new SerializableSandboxTemplateBuilder(this.#operations, this.#family, {
405
+ ...this.#buildEnvs,
406
+ ...copy
407
+ });
408
+ return this.#append("setEnvs", [copy]);
409
+ }
410
+ aptInstall(packages, options) {
411
+ const args = [validateStringOrStrings(packages, "packages")];
412
+ if (options !== void 0) args.push(validateBooleanOptions(options, ["noInstallRecommends", "fixMissing"]));
413
+ return this.#append("aptInstall", args);
414
+ }
415
+ pipInstall(packages, options) {
416
+ return this.#appendOptionalInstall("pipInstall", packages, options, ["g"]);
417
+ }
418
+ npmInstall(packages, options) {
419
+ return this.#appendOptionalInstall("npmInstall", packages, options, ["g", "dev"]);
420
+ }
421
+ withFamily(family) {
422
+ if (typeof family !== "string" || family.length === 0) throw new TypeError("family must be a non-empty string");
423
+ if (family.length > MAX_FAMILY_LENGTH) throw new RangeError(`family cannot exceed ${MAX_FAMILY_LENGTH} characters`);
424
+ assertSerializedSize(this.#operations, family);
425
+ return new SerializableSandboxTemplateBuilder(this.#operations, family, this.#buildEnvs);
426
+ }
427
+ [GET_TEMPLATE_BUILD_ENVS]() {
428
+ return Object.keys(this.#buildEnvs).length > 0 ? { ...this.#buildEnvs } : void 0;
429
+ }
430
+ [SERIALIZE_TEMPLATE]() {
431
+ return {
432
+ schemaVersion: 1,
433
+ operations: this.#operations.map((operation) => ({
434
+ method: operation.method,
435
+ args: cloneJson(operation.args)
436
+ })),
437
+ ...this.#family !== void 0 && { family: this.#family }
438
+ };
439
+ }
440
+ #appendOptionalInstall(method, packages, options, optionKeys) {
441
+ const args = [];
442
+ if (packages !== void 0) args.push(validateStringOrStrings(packages, "packages"));
443
+ if (options !== void 0) {
444
+ if (packages === void 0) args.push(null);
445
+ args.push(validateBooleanOptions(options, optionKeys));
446
+ }
447
+ return this.#append(method, args);
448
+ }
449
+ #append(method, args) {
450
+ if (this.#operations.length >= MAX_OPERATIONS) throw new RangeError(`Sandbox template cannot contain more than ${MAX_OPERATIONS} operations`);
451
+ const operation = {
452
+ method,
453
+ args: cloneJson(args)
454
+ };
455
+ const operations = [...this.#operations, operation];
456
+ assertSerializedSize(operations, this.#family);
457
+ return new SerializableSandboxTemplateBuilder(operations, this.#family, this.#buildEnvs);
458
+ }
459
+ };
460
+ function assertSerializedSize(operations, family) {
461
+ const serialized = JSON.stringify({
462
+ schemaVersion: 1,
463
+ operations,
464
+ ...family !== void 0 && { family }
465
+ });
466
+ if (new TextEncoder().encode(serialized).byteLength > MAX_SERIALIZED_BYTES) throw new RangeError(`Serialized sandbox template cannot exceed ${MAX_SERIALIZED_BYTES} bytes`);
467
+ }
468
+ function Template() {
469
+ return new SerializableSandboxTemplateBuilder();
470
+ }
471
+ function isSandboxTemplateBuilder(value) {
472
+ return value instanceof SerializableSandboxTemplateBuilder;
473
+ }
474
+ function serializeSandboxTemplate(template) {
475
+ if (!isSandboxTemplateBuilder(template)) throw new TypeError("template must be created with Template()");
476
+ return template[SERIALIZE_TEMPLATE]();
477
+ }
478
+ function getSandboxTemplateBuildEnvs(template) {
479
+ if (!isSandboxTemplateBuilder(template)) throw new TypeError("template must be created with Template()");
480
+ return template[GET_TEMPLATE_BUILD_ENVS]();
481
+ }
482
+ async function buildSandboxTemplate(template, options) {
483
+ const environmentId = options.environmentId ?? process.env.MASTRA_ENVIRONMENT_ID;
484
+ if (!environmentId) throw new Error("environmentId is required. Pass it or set MASTRA_ENVIRONMENT_ID.");
485
+ const client = new PlatformClient(options);
486
+ const templateBuildEnvs = getSandboxTemplateBuildEnvs(template);
487
+ return await (await client.requestProvider("/sandbox/templates/builds", {
488
+ method: "POST",
489
+ headers: { "content-type": "application/json" },
490
+ body: JSON.stringify({
491
+ environmentId,
492
+ templateDefinition: serializeSandboxTemplate(template),
493
+ ...templateBuildEnvs !== void 0 && { templateBuildEnvs }
494
+ })
495
+ })).json();
496
+ }
497
+ function validateResourceValue(value, name) {
498
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive safe integer`);
499
+ return value;
500
+ }
501
+ function validateString(value, name, allowEmpty = false) {
502
+ if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
503
+ if (!allowEmpty && value.length === 0) throw new TypeError(`${name} must not be empty`);
504
+ if (value.length > MAX_STRING_LENGTH) throw new RangeError(`${name} cannot exceed ${MAX_STRING_LENGTH} characters`);
505
+ return value;
506
+ }
507
+ function validateStringOrStrings(value, name) {
508
+ if (typeof value === "string") return validateString(value, name);
509
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be a string or an array of strings`);
510
+ assertCollectionSize(value.length, name);
511
+ if (value.length === 0) throw new TypeError(`${name} must not be empty`);
512
+ return Array.from(value, (item, index) => validateString(item, `${name}[${index}]`));
513
+ }
514
+ function validateStringRecord(value, name, entryName) {
515
+ assertPlainObject(value, name);
516
+ const entries = Object.entries(value);
517
+ assertCollectionSize(entries.length, name);
518
+ return Object.fromEntries(entries.map(([key, item]) => [validateString(key, `${entryName} name`), validateString(item, `${entryName} ${key}`, true)]));
519
+ }
520
+ function validateBooleanOptions(value, keys) {
521
+ assertPlainObject(value, "options");
522
+ const options = value;
523
+ const unknownKey = Object.keys(options).find((key) => !keys.includes(key));
524
+ if (unknownKey) throw new TypeError(`Unsupported option: ${unknownKey}`);
525
+ const copy = {};
526
+ for (const key of keys) {
527
+ const option = options[key];
528
+ if (option === void 0) continue;
529
+ if (typeof option !== "boolean") throw new TypeError(`${key} must be a boolean`);
530
+ copy[key] = option;
531
+ }
532
+ return copy;
533
+ }
534
+ function assertPlainObject(value, name) {
535
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${name} must be a plain object`);
536
+ const prototype = Object.getPrototypeOf(value);
537
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`${name} must be a plain object`);
538
+ }
539
+ function assertCollectionSize(size, name) {
540
+ if (size > MAX_COLLECTION_ITEMS) throw new RangeError(`${name} cannot contain more than ${MAX_COLLECTION_ITEMS} items`);
541
+ }
542
+ function cloneJson(value) {
543
+ if (Array.isArray(value)) return value.map((item) => cloneJson(item));
544
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneJson(item)]));
545
+ if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError("Sandbox template values must contain only finite numbers");
546
+ return value;
547
+ }
548
+ //#endregion
365
549
  //#region src/direct-exec.ts
366
550
  /**
367
551
  * Direct exec client — opens Railway's tcp-proxy exec WebSocket directly using
@@ -902,10 +1086,26 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
902
1086
  name = "PlatformSandbox";
903
1087
  provider = "platform";
904
1088
  status = "pending";
1089
+ /**
1090
+ * Populated from the platform's create/reattach response when the sandbox
1091
+ * booted from a prior member of the same template family or the provider
1092
+ * base template while the requested exact template continues to build in
1093
+ * the background.
1094
+ * `undefined` when the sandbox booted on the exact template (or when no
1095
+ * template was requested). Observability-only; consumers reconcile freshness
1096
+ * in their own runtime setup and reprovision to pick up the ready template
1097
+ * on a later start.
1098
+ */
1099
+ templatePending;
905
1100
  _client;
1101
+ _usesProviderRoutes;
906
1102
  _environmentId;
907
1103
  _sandboxId;
908
1104
  _seedCheckpointName;
1105
+ _templateDefinition;
1106
+ _templateBuildEnvs;
1107
+ _template;
1108
+ _templateResolutionInFlight;
909
1109
  _idleTimeoutMinutes;
910
1110
  _networkIsolation;
911
1111
  _env;
@@ -993,6 +1193,8 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
993
1193
  if (!this._environmentId && !options.sandboxId) throw new Error("environmentId is required");
994
1194
  this._sandboxId = options.sandboxId;
995
1195
  this._seedCheckpointName = options.seedCheckpointName;
1196
+ this._usesProviderRoutes = options.template !== void 0;
1197
+ this._template = options.template;
996
1198
  this._idleTimeoutMinutes = options.idleTimeoutMinutes;
997
1199
  this._networkIsolation = options.networkIsolation;
998
1200
  this._env = options.env ?? {};
@@ -1006,6 +1208,9 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1006
1208
  generateId() {
1007
1209
  return `platform-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1008
1210
  }
1211
+ _request(path, options = {}) {
1212
+ return this._usesProviderRoutes ? this._client.requestProvider(path, options) : this._client.request(path, options);
1213
+ }
1009
1214
  /**
1010
1215
  * Construct a sibling {@link PlatformSandbox} that inherits this sandbox's
1011
1216
  * credentials and defaults (access token, project, environment, network
@@ -1021,10 +1226,11 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1021
1226
  clone(options = {}) {
1022
1227
  const id = options.id ?? options.checkpointName;
1023
1228
  const seedCheckpointName = options.seedCheckpointName ?? (this._client.sandboxProvider === "e2b" ? options.checkpointName : void 0) ?? this._seedCheckpointName;
1024
- return new PlatformSandbox({
1229
+ const clone = new PlatformSandbox({
1025
1230
  ...id !== void 0 && { id },
1026
1231
  accessToken: this._client.accessToken,
1027
1232
  projectId: this._client.projectId,
1233
+ ...this._usesProviderRoutes || this._client.sandboxProvider !== "railway" ? { sandboxProvider: this._client.sandboxProvider } : {},
1028
1234
  actingUserId: options.actingUserId ?? this._client.actingUserId,
1029
1235
  ...this._client.sessionId !== void 0 && { sessionId: this._client.sessionId },
1030
1236
  ...this._client.threadId !== void 0 && { threadId: this._client.threadId },
@@ -1032,6 +1238,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1032
1238
  environmentId: this._environmentId,
1033
1239
  ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
1034
1240
  ...seedCheckpointName !== void 0 && { seedCheckpointName },
1241
+ ...this._template !== void 0 && { template: this._template },
1035
1242
  idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
1036
1243
  ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
1037
1244
  env: options.env ?? this._env,
@@ -1042,6 +1249,9 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1042
1249
  ...this._privateNetFetch !== void 0 && { privateNetFetch: this._privateNetFetch },
1043
1250
  ...this._addressRegistry !== void 0 && { addressRegistry: this._addressRegistry }
1044
1251
  });
1252
+ clone._templateDefinition = this._templateDefinition ? structuredClone(this._templateDefinition) : void 0;
1253
+ clone._templateBuildEnvs = this._templateBuildEnvs ? { ...this._templateBuildEnvs } : void 0;
1254
+ return clone;
1045
1255
  }
1046
1256
  /**
1047
1257
  * Start the sandbox: reattach to a known provider `sandboxId` when one is
@@ -1060,7 +1270,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1060
1270
  const startedAt = Date.now();
1061
1271
  if (this._sandboxId) try {
1062
1272
  const requestStartedAt = Date.now();
1063
- const response = await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
1273
+ const response = await this._request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
1064
1274
  const requestMs = Date.now() - requestStartedAt;
1065
1275
  const json = await response.json();
1066
1276
  if (!json.destroyedAt) {
@@ -1075,9 +1285,12 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1075
1285
  this._sandboxId = void 0;
1076
1286
  }
1077
1287
  if (!this._environmentId) throw new Error("environmentId is required");
1078
- const body = JSON.stringify({
1288
+ await this._prepareLazyTemplate();
1289
+ const createBody = () => JSON.stringify({
1079
1290
  id: this.id,
1080
1291
  seedCheckpointName: this._seedCheckpointName,
1292
+ templateDefinition: this._templateDefinition,
1293
+ templateBuildEnvs: this._templateBuildEnvs,
1081
1294
  environmentId: this._environmentId,
1082
1295
  idleTimeoutMinutes: this._idleTimeoutMinutes,
1083
1296
  networkIsolation: this._networkIsolation,
@@ -1086,10 +1299,10 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1086
1299
  let response;
1087
1300
  const requestStartedAt = Date.now();
1088
1301
  for (let attempt = 1;; attempt++) try {
1089
- response = await this._client.request("/sandbox", {
1302
+ response = await this._request("/sandbox", {
1090
1303
  method: "POST",
1091
1304
  headers: { "content-type": "application/json" },
1092
- body
1305
+ body: createBody()
1093
1306
  });
1094
1307
  break;
1095
1308
  } catch (error) {
@@ -1100,10 +1313,31 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1100
1313
  const json = await response.json();
1101
1314
  this._sandboxId = json.id;
1102
1315
  this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
1316
+ this.templatePending = json.templatePending;
1103
1317
  this._populateAddressFromResponse(json);
1104
1318
  this._logStartComplete(json.id, startedAt, requestMs, "provision");
1105
1319
  return { outcome: "created" };
1106
1320
  }
1321
+ async _prepareLazyTemplate() {
1322
+ if (this._templateDefinition !== void 0 || this._template === void 0) return;
1323
+ if (!this._templateResolutionInFlight) {
1324
+ const attempt = this._resolveTemplate();
1325
+ this._templateResolutionInFlight = attempt.finally(() => {
1326
+ this._templateResolutionInFlight = void 0;
1327
+ });
1328
+ }
1329
+ await this._templateResolutionInFlight;
1330
+ }
1331
+ async _resolveTemplate() {
1332
+ try {
1333
+ const resolved = typeof this._template === "function" ? await this._template() : this._template;
1334
+ if (!resolved) return;
1335
+ this._templateDefinition = serializeSandboxTemplate(resolved);
1336
+ this._templateBuildEnvs = getSandboxTemplateBuildEnvs(resolved);
1337
+ } catch (error) {
1338
+ this.logger.warn(`Platform sandbox template resolution failed; using provider default template: ${String(error)}`);
1339
+ }
1340
+ }
1107
1341
  /**
1108
1342
  * One timing summary per completed `start()` — the whole
1109
1343
  * `PlatformSandbox`-visible boot in a single greppable line.
@@ -1268,7 +1502,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1268
1502
  const destroyedSandboxId = this._sandboxId;
1269
1503
  this._captureInFlight = null;
1270
1504
  if (this._hasRecoveryKey || this._client.sandboxProvider === "e2b") try {
1271
- await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}/checkpoint`, {
1505
+ await this._request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}/checkpoint`, {
1272
1506
  method: "DELETE",
1273
1507
  headers: { "content-type": "application/json" },
1274
1508
  body: JSON.stringify({ id: this.id })
@@ -1295,7 +1529,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1295
1529
  this._probeGeneration++;
1296
1530
  this._probeTarget = null;
1297
1531
  this._transportReadyPromise = null;
1298
- await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
1532
+ await this._request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
1299
1533
  this._sandboxId = void 0;
1300
1534
  this._createdAt = null;
1301
1535
  this._lease = null;
@@ -1385,7 +1619,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1385
1619
  async _doCaptureCheckpoint(sandboxId) {
1386
1620
  let response;
1387
1621
  try {
1388
- response = await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/checkpoint`, {
1622
+ response = await this._request(`/sandbox/${encodeURIComponent(sandboxId)}/checkpoint`, {
1389
1623
  method: "POST",
1390
1624
  headers: { "content-type": "application/json" },
1391
1625
  body: JSON.stringify({ id: this.id })
@@ -1516,8 +1750,20 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1516
1750
  * Returns a result with a real `exitCode` OR `timedOut: true`. Never
1517
1751
  * returns `{ exitCode: null, timedOut: false }` — that case throws.
1518
1752
  */
1753
+ /**
1754
+ * Drop undefined values so the result matches the Record<string, string>
1755
+ * shape the exec transports expect (`ExecuteCommandOptions.env` is
1756
+ * NodeJS.ProcessEnv). The sandbox's own env is already merged in by
1757
+ * `executeCommand` before a transport sees these options. Returns undefined
1758
+ * when there is nothing to send.
1759
+ */
1760
+ _execEnv(options) {
1761
+ if (!options?.env) return void 0;
1762
+ const filtered = Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0));
1763
+ return Object.keys(filtered).length > 0 ? filtered : void 0;
1764
+ }
1519
1765
  async _runDirectExec(fullCommand, effectiveTimeout, options) {
1520
- const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
1766
+ const filteredEnv = this._execEnv(options);
1521
1767
  let lastResult;
1522
1768
  let lastLease;
1523
1769
  let attemptsMade = 0;
@@ -1542,9 +1788,10 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1542
1788
  }
1543
1789
  lastLease = lease;
1544
1790
  attemptsMade = attempt + 1;
1791
+ const leaseCwd = options?.cwd ?? this.workingDirectory;
1545
1792
  const execOptions = {
1546
1793
  command: fullCommand,
1547
- ...options?.cwd !== void 0 && { cwd: options.cwd },
1794
+ ...leaseCwd !== void 0 && { cwd: leaseCwd },
1548
1795
  ...filteredEnv !== void 0 && { env: filteredEnv },
1549
1796
  ...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
1550
1797
  ...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
@@ -1580,10 +1827,11 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1580
1827
  * sidecar bug). Only this specific exec falls back.
1581
1828
  */
1582
1829
  async _tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options) {
1583
- const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
1830
+ const filteredEnv = this._execEnv(options);
1831
+ const privateNetCwd = options?.cwd ?? this.workingDirectory;
1584
1832
  const execOptions = {
1585
1833
  command: fullCommand,
1586
- ...options?.cwd !== void 0 && { cwd: options.cwd },
1834
+ ...privateNetCwd !== void 0 && { cwd: privateNetCwd },
1587
1835
  ...filteredEnv !== void 0 && { env: filteredEnv },
1588
1836
  ...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
1589
1837
  ...this._privateNetFetch && { fetch: this._privateNetFetch }
@@ -1629,7 +1877,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1629
1877
  if (!this._sandboxId) throw new SandboxNotReadyError(this.id);
1630
1878
  const sandboxId = this._sandboxId;
1631
1879
  const inFlight = (async () => {
1632
- const json = await (await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/exec-lease`, { method: "POST" })).json();
1880
+ const json = await (await this._request(`/sandbox/${encodeURIComponent(sandboxId)}/exec-lease`, { method: "POST" })).json();
1633
1881
  const expiresAtMs = json.expiresAt ? Date.parse(json.expiresAt) : null;
1634
1882
  const lease = {
1635
1883
  provider: json.provider,
@@ -1667,7 +1915,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1667
1915
  createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
1668
1916
  metadata: { sandboxId: this._sandboxId }
1669
1917
  };
1670
- const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
1918
+ const json = await (await this._request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
1671
1919
  return {
1672
1920
  id: json.id,
1673
1921
  name: this.name,
@@ -1692,6 +1940,122 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1692
1940
  }
1693
1941
  };
1694
1942
  //#endregion
1943
+ //#region src/repo-template.ts
1944
+ const execFileAsync = promisify(execFile);
1945
+ const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
1946
+ const BUILD_TOKEN_ENV = "MASTRA_REPOSITORY_ACCESS_TOKEN";
1947
+ /**
1948
+ * Clone URLs interpolate into the template's build commands, so constrain
1949
+ * them to https plus plain host/path characters. Every regex here is a
1950
+ * single anchored character class, so matching stays linear on adversarial
1951
+ * input; the structural checks go through WHATWG URL parsing instead of one
1952
+ * big backtracking pattern.
1953
+ */
1954
+ const CLONE_URL_ALLOWED_CHARS = /^[a-z0-9:/._-]+$/i;
1955
+ const CLONE_URL_HOST_PATTERN = /^[a-z0-9.-]+$/i;
1956
+ const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
1957
+ /**
1958
+ * Create a lazy repository template definition for PlatformSandbox, mirroring
1959
+ * `@mastra/e2b`'s `createRepoTemplate`: pass the sandbox context through and a
1960
+ * repo-less session boots the provider default.
1961
+ *
1962
+ * The resolver performs no work until a fresh sandbox starts. It clones the
1963
+ * URL `getRepositoryAccess` resolves — the only source of the clone URL, so
1964
+ * what gets cloned and what the template is identified by can't drift — and
1965
+ * pins repositories to their current default-branch commit. Private repository
1966
+ * credentials are used for head resolution and sent to the provider as
1967
+ * transient build envs; they never enter the serialized definition. If the
1968
+ * head cannot be resolved, the resolver returns undefined so PlatformSandbox
1969
+ * boots from the provider default and the caller's runtime setup materializes
1970
+ * the checkout instead.
1971
+ */
1972
+ function createRepoTemplate(options) {
1973
+ const getRepositoryAccess = options.getRepositoryAccess;
1974
+ if (!getRepositoryAccess) return void 0;
1975
+ const resolveHead = options.resolveHead ?? resolveDefaultBranchHead;
1976
+ return async () => {
1977
+ const access = await getRepositoryAccess().catch(() => void 0);
1978
+ if (!access?.cloneUrl) return void 0;
1979
+ const cloneUrl = normalizeCloneUrl(access.cloneUrl);
1980
+ if (!isValidCloneUrl(cloneUrl)) return void 0;
1981
+ const token = access.authorization?.token;
1982
+ const sha = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch(() => void 0);
1983
+ if (!sha || !SHA_PATTERN.test(sha)) return void 0;
1984
+ const workdir = defaultWorkdir(cloneUrl);
1985
+ const auth = token ? `${gitAuthFlag()} ` : "";
1986
+ const steps = [
1987
+ `git ${auth}clone ${cloneUrl} "${workdir}"`,
1988
+ `git -C "${workdir}" ${auth}fetch origin ${sha}`,
1989
+ `git -C "${workdir}" checkout ${sha}`,
1990
+ ...options.setupCommand ? [`cd "${workdir}" && ${options.setupCommand}`] : []
1991
+ ];
1992
+ const family = `repo:${cloneUrl}:${workdir}`;
1993
+ let template = Template();
1994
+ if (token) template = template.setEnvs({ [BUILD_TOKEN_ENV]: token }, { ephemeral: true });
1995
+ if (options.cpuCount !== void 0) template = template.cpuCount(options.cpuCount);
1996
+ if (options.memoryMB !== void 0) template = template.memoryMB(options.memoryMB);
1997
+ return template.runCmd(steps).withFamily(family);
1998
+ };
1999
+ }
2000
+ function isValidCloneUrl(cloneUrl) {
2001
+ if (cloneUrl.length > 2048 || !CLONE_URL_ALLOWED_CHARS.test(cloneUrl)) return false;
2002
+ let url;
2003
+ try {
2004
+ url = new URL(cloneUrl);
2005
+ } catch {
2006
+ return false;
2007
+ }
2008
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) return false;
2009
+ if (!CLONE_URL_HOST_PATTERN.test(url.hostname)) return false;
2010
+ const segments = url.pathname.split("/").slice(1);
2011
+ return segments.length > 0 && segments.every((segment) => CLONE_URL_SEGMENT_PATTERN.test(segment));
2012
+ }
2013
+ /**
2014
+ * Canonical form used for identity, the family key, and the build's clone:
2015
+ * lowercase host, no trailing `.git` or slash. Two spellings of one
2016
+ * repository must not produce two templates.
2017
+ */
2018
+ function normalizeCloneUrl(cloneUrl) {
2019
+ let end = cloneUrl.length;
2020
+ while (end > 0 && cloneUrl[end - 1] === "/") end--;
2021
+ return cloneUrl.slice(0, end).replace(/\.git$/i, "").replace(/^(https:\/\/)([^/]+)/i, (_match, scheme, host) => {
2022
+ return `${scheme.toLowerCase()}${host.toLowerCase()}`;
2023
+ });
2024
+ }
2025
+ function defaultWorkdir(cloneUrl) {
2026
+ return `$HOME/${(normalizeCloneUrl(cloneUrl).split("/").at(-1) ?? "").replace(/[^\w.-]/g, "-").replace(/^\.+/, "") || "repo"}`;
2027
+ }
2028
+ function gitAuthFlag() {
2029
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
2030
+ }
2031
+ async function resolveDefaultBranchHead(cloneUrl, token, execute = execFileAsync) {
2032
+ try {
2033
+ const env = {
2034
+ ...process.env,
2035
+ GIT_TERMINAL_PROMPT: "0"
2036
+ };
2037
+ if (token) {
2038
+ env.GIT_CONFIG_COUNT = "1";
2039
+ env.GIT_CONFIG_KEY_0 = "http.extraheader";
2040
+ env.GIT_CONFIG_VALUE_0 = `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
2041
+ }
2042
+ const { stdout } = await execute("git", [
2043
+ "ls-remote",
2044
+ "--",
2045
+ cloneUrl,
2046
+ "HEAD"
2047
+ ], {
2048
+ timeout: 1e4,
2049
+ maxBuffer: 1024 * 1024,
2050
+ env
2051
+ });
2052
+ const sha = stdout.trim().split(/\s+/, 1)[0];
2053
+ return sha && SHA_PATTERN.test(sha) ? sha : void 0;
2054
+ } catch {
2055
+ return;
2056
+ }
2057
+ }
2058
+ //#endregion
1695
2059
  //#region src/provider.ts
1696
2060
  const platformSandboxProvider = {
1697
2061
  id: "platform",
@@ -1712,6 +2076,12 @@ const platformSandboxProvider = {
1712
2076
  type: "string",
1713
2077
  description: "Opaque user subject attributed to sandbox requests"
1714
2078
  },
2079
+ sandboxProvider: {
2080
+ type: "string",
2081
+ description: "Sandbox provider (falls back to SANDBOX_PROVIDER, then e2b)",
2082
+ enum: ["railway", "e2b"],
2083
+ default: "e2b"
2084
+ },
1715
2085
  environmentId: {
1716
2086
  type: "string",
1717
2087
  description: "Platform environment ID (falls back to MASTRA_ENVIRONMENT_ID)"
@@ -1805,6 +2175,6 @@ var InProcessSandboxAddressRegistry = class {
1805
2175
  }
1806
2176
  };
1807
2177
  //#endregion
1808
- export { InProcessSandboxAddressRegistry, PlatformApiError, PlatformClient, PlatformFilesystem, PlatformSandbox, PrivateNetExecHttpError, SandboxDestroyedError, SandboxExecTransportError, execViaPrivateNetwork, platformFilesystemProvider, platformSandboxProvider };
2178
+ export { InProcessSandboxAddressRegistry, PlatformApiError, PlatformClient, PlatformFilesystem, PlatformSandbox, PrivateNetExecHttpError, SandboxDestroyedError, SandboxExecTransportError, Template, createRepoTemplate, execViaPrivateNetwork, platformFilesystemProvider, platformSandboxProvider };
1809
2179
 
1810
2180
  //# sourceMappingURL=index.js.map