@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/CHANGELOG.md +37 -0
- package/LICENSE.md +6 -4
- package/README.md +40 -9
- package/dist/address-registry.d.ts.map +1 -1
- package/dist/client.d.ts +3 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/index.cjs +388 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +392 -24
- package/dist/index.js.map +1 -1
- package/dist/private-net-exec.d.ts.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/repo-template.d.ts +69 -0
- package/dist/repo-template.d.ts.map +1 -0
- package/dist/sandbox.d.ts +60 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/template.d.ts +95 -0
- package/dist/template.d.ts.map +1 -0
- package/package.json +7 -6
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() || "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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;
|