@mastra/platform-workspace 1.8.2-alpha.0 → 1.9.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/_types/@internal_workspace/dist/file-commands.d.ts +69 -0
- package/dist/_types/@internal_workspace/dist/files-api-filesystem.d.ts +85 -0
- package/dist/_types/@internal_workspace/dist/index.d.ts +5 -0
- package/dist/_types/@internal_workspace/dist/repo-clone.d.ts +22 -0
- package/dist/_types/@internal_workspace/dist/setup-marker.d.ts +56 -0
- package/dist/_types/@internal_workspace/package.json +1 -0
- package/dist/e2b-exec.d.ts +21 -0
- package/dist/e2b-exec.d.ts.map +1 -1
- package/dist/index.cjs +650 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +617 -91
- package/dist/index.js.map +1 -1
- package/dist/repo-template.d.ts +1 -1
- package/dist/repo-template.d.ts.map +1 -1
- package/dist/sandbox-filesystem.d.ts +33 -0
- package/dist/sandbox-filesystem.d.ts.map +1 -0
- package/dist/sandbox.d.ts +36 -1
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/template.d.ts +1 -1
- package/dist/template.d.ts.map +1 -1
- package/package.json +4 -5
package/dist/index.cjs
CHANGED
|
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
20
|
value: mod,
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
@@ -168,8 +168,8 @@ function normalizePath(input) {
|
|
|
168
168
|
normalized = path.default.posix.normalize(normalized);
|
|
169
169
|
return normalized === "." ? "/" : normalized;
|
|
170
170
|
}
|
|
171
|
-
function keyFromPath(path
|
|
172
|
-
const normalized = normalizePath(path
|
|
171
|
+
function keyFromPath(path) {
|
|
172
|
+
const normalized = normalizePath(path);
|
|
173
173
|
return normalized === "/" ? "" : normalized.slice(1);
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
@@ -181,8 +181,8 @@ function keyFromPath(path$1) {
|
|
|
181
181
|
function encodeKeyPath(key) {
|
|
182
182
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
183
183
|
}
|
|
184
|
-
function nameFromPath(path
|
|
185
|
-
const normalized = normalizePath(path
|
|
184
|
+
function nameFromPath(path) {
|
|
185
|
+
const normalized = normalizePath(path);
|
|
186
186
|
if (normalized === "/") return "";
|
|
187
187
|
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
188
188
|
}
|
|
@@ -231,32 +231,32 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
231
231
|
generateId() {
|
|
232
232
|
return `platform-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
233
233
|
}
|
|
234
|
-
async readFile(path
|
|
234
|
+
async readFile(path, options) {
|
|
235
235
|
await this.ensureReady();
|
|
236
236
|
let response;
|
|
237
237
|
try {
|
|
238
|
-
response = await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path
|
|
238
|
+
response = await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`);
|
|
239
239
|
} catch (error) {
|
|
240
|
-
if (isNotFound(error)) throw new _mastra_core_workspace.FileNotFoundError(path
|
|
240
|
+
if (isNotFound(error)) throw new _mastra_core_workspace.FileNotFoundError(path);
|
|
241
241
|
throw error;
|
|
242
242
|
}
|
|
243
|
-
const buffer$
|
|
244
|
-
return options?.encoding ? buffer$
|
|
243
|
+
const buffer$2 = buffer.Buffer.from(await response.arrayBuffer());
|
|
244
|
+
return options?.encoding ? buffer$2.toString(options.encoding) : buffer$2;
|
|
245
245
|
}
|
|
246
|
-
async writeFile(path
|
|
246
|
+
async writeFile(path, content, options) {
|
|
247
247
|
await this.ensureReady();
|
|
248
248
|
if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("writeFile");
|
|
249
249
|
const headers = {};
|
|
250
250
|
if (options?.mimeType) headers["content-type"] = options.mimeType;
|
|
251
251
|
if (options?.overwrite === false) headers["if-none-match"] = "*";
|
|
252
252
|
try {
|
|
253
|
-
await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path
|
|
253
|
+
await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`, {
|
|
254
254
|
method: "PUT",
|
|
255
255
|
headers,
|
|
256
256
|
body: contentToBody(content)
|
|
257
257
|
});
|
|
258
258
|
} catch (error) {
|
|
259
|
-
if (typeof error === "object" && error !== null && "status" in error && error.status === 412) throw new _mastra_core_workspace.FileExistsError(path
|
|
259
|
+
if (typeof error === "object" && error !== null && "status" in error && error.status === 412) throw new _mastra_core_workspace.FileExistsError(path);
|
|
260
260
|
throw error;
|
|
261
261
|
}
|
|
262
262
|
}
|
|
@@ -270,21 +270,21 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
270
270
|
* the same path can overwrite each other's writes ("last write wins").
|
|
271
271
|
* Use `writeFile` with distinct keys for concurrent writers.
|
|
272
272
|
*/
|
|
273
|
-
async appendFile(path
|
|
274
|
-
const existing = await this.exists(path
|
|
275
|
-
await this.writeFile(path
|
|
273
|
+
async appendFile(path, content) {
|
|
274
|
+
const existing = await this.exists(path) ? await this.readFile(path) : buffer.Buffer.alloc(0);
|
|
275
|
+
await this.writeFile(path, buffer.Buffer.concat([buffer.Buffer.isBuffer(existing) ? existing : buffer.Buffer.from(existing), buffer.Buffer.from(content)]));
|
|
276
276
|
}
|
|
277
|
-
async deleteFile(path
|
|
277
|
+
async deleteFile(path, options) {
|
|
278
278
|
await this.ensureReady();
|
|
279
279
|
if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("deleteFile");
|
|
280
280
|
try {
|
|
281
|
-
await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path
|
|
281
|
+
await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`, {
|
|
282
282
|
method: "DELETE",
|
|
283
283
|
query: { recursive: options?.recursive }
|
|
284
284
|
});
|
|
285
285
|
} catch (error) {
|
|
286
286
|
if (isNotFound(error) && options?.force) return;
|
|
287
|
-
if (isNotFound(error)) throw new _mastra_core_workspace.FileNotFoundError(path
|
|
287
|
+
if (isNotFound(error)) throw new _mastra_core_workspace.FileNotFoundError(path);
|
|
288
288
|
throw error;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
@@ -310,23 +310,23 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
310
310
|
body: JSON.stringify({ destination: keyFromPath(dest) })
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
-
async mkdir(path
|
|
313
|
+
async mkdir(path, _options) {
|
|
314
314
|
await this.ensureReady();
|
|
315
315
|
if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("mkdir");
|
|
316
|
-
await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path
|
|
316
|
+
await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`, {
|
|
317
317
|
method: "POST",
|
|
318
318
|
query: { op: "mkdir" }
|
|
319
319
|
});
|
|
320
320
|
}
|
|
321
|
-
async rmdir(path
|
|
322
|
-
await this.deleteFile(path
|
|
321
|
+
async rmdir(path, options) {
|
|
322
|
+
await this.deleteFile(path.endsWith("/") ? path : `${path}/`, {
|
|
323
323
|
recursive: true,
|
|
324
324
|
force: options?.force
|
|
325
325
|
});
|
|
326
326
|
}
|
|
327
|
-
async readdir(path
|
|
327
|
+
async readdir(path, options) {
|
|
328
328
|
await this.ensureReady();
|
|
329
|
-
const json = await this.listPrefix(keyFromPath(path
|
|
329
|
+
const json = await this.listPrefix(keyFromPath(path), options?.recursive);
|
|
330
330
|
return [...(json.commonPrefixes ?? []).map((prefix) => ({
|
|
331
331
|
name: nameFromPath(prefix.replace(/\/$/, "")),
|
|
332
332
|
type: "directory"
|
|
@@ -334,7 +334,7 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
334
334
|
name: nameFromPath(object.key),
|
|
335
335
|
type: "file",
|
|
336
336
|
size: object.size
|
|
337
|
-
}))].filter((entry) => !options?.extension || entry.type === "directory" || matchesExtension(entry.name, options.extension));
|
|
337
|
+
}))].filter((entry) => !options?.extension || entry.type === "directory" || matchesExtension$1(entry.name, options.extension));
|
|
338
338
|
}
|
|
339
339
|
async listPrefix(prefix, recursive) {
|
|
340
340
|
const normalized = prefix.replace(/\/$/, "");
|
|
@@ -343,18 +343,18 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
343
343
|
prefix: normalized ? `${normalized}/` : void 0
|
|
344
344
|
} })).json();
|
|
345
345
|
}
|
|
346
|
-
async exists(path
|
|
346
|
+
async exists(path) {
|
|
347
347
|
try {
|
|
348
|
-
await this.stat(path
|
|
348
|
+
await this.stat(path);
|
|
349
349
|
return true;
|
|
350
350
|
} catch (error) {
|
|
351
351
|
if (isNotFound(error) || error instanceof _mastra_core_workspace.FileNotFoundError) return false;
|
|
352
352
|
throw error;
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
async stat(path
|
|
355
|
+
async stat(path) {
|
|
356
356
|
await this.ensureReady();
|
|
357
|
-
const normalized = normalizePath(path
|
|
357
|
+
const normalized = normalizePath(path);
|
|
358
358
|
if (normalized === "/") return {
|
|
359
359
|
name: "",
|
|
360
360
|
path: "/",
|
|
@@ -365,13 +365,13 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
365
365
|
};
|
|
366
366
|
let response;
|
|
367
367
|
try {
|
|
368
|
-
response = await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path
|
|
368
|
+
response = await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`, { method: "HEAD" });
|
|
369
369
|
} catch (error) {
|
|
370
370
|
if (!isNotFound(error)) throw error;
|
|
371
|
-
const listing = await this.listPrefix(keyFromPath(path
|
|
372
|
-
if (!listing.contents?.length && !listing.commonPrefixes?.length) throw new _mastra_core_workspace.FileNotFoundError(path
|
|
371
|
+
const listing = await this.listPrefix(keyFromPath(path));
|
|
372
|
+
if (!listing.contents?.length && !listing.commonPrefixes?.length) throw new _mastra_core_workspace.FileNotFoundError(path);
|
|
373
373
|
return {
|
|
374
|
-
name: nameFromPath(path
|
|
374
|
+
name: nameFromPath(path),
|
|
375
375
|
path: normalized,
|
|
376
376
|
type: "directory",
|
|
377
377
|
size: 0,
|
|
@@ -380,7 +380,7 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
380
380
|
};
|
|
381
381
|
}
|
|
382
382
|
return {
|
|
383
|
-
name: nameFromPath(path
|
|
383
|
+
name: nameFromPath(path),
|
|
384
384
|
path: normalized,
|
|
385
385
|
type: normalized.endsWith("/") ? "directory" : "file",
|
|
386
386
|
size: headerSize(response.headers),
|
|
@@ -389,8 +389,8 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
389
389
|
mimeType: response.headers.get("content-type") ?? void 0
|
|
390
390
|
};
|
|
391
391
|
}
|
|
392
|
-
realpath(path
|
|
393
|
-
return Promise.resolve(normalizePath(path
|
|
392
|
+
realpath(path) {
|
|
393
|
+
return Promise.resolve(normalizePath(path));
|
|
394
394
|
}
|
|
395
395
|
getInstructions(opts) {
|
|
396
396
|
const defaultInstructions = `Platform filesystem backed by Mastra Platform bucket ${this._bucketName}. Use absolute workspace paths.`;
|
|
@@ -417,7 +417,7 @@ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
419
|
};
|
|
420
|
-
function matchesExtension(name, extension) {
|
|
420
|
+
function matchesExtension$1(name, extension) {
|
|
421
421
|
return (Array.isArray(extension) ? extension : [extension]).some((ext) => name.endsWith(ext));
|
|
422
422
|
}
|
|
423
423
|
//#endregion
|
|
@@ -425,8 +425,8 @@ function matchesExtension(name, extension) {
|
|
|
425
425
|
const SERIALIZE_TEMPLATE = Symbol("serializeTemplate");
|
|
426
426
|
const GET_TEMPLATE_BUILD_ENVS = Symbol("getTemplateBuildEnvs");
|
|
427
427
|
const MAX_OPERATIONS = 256;
|
|
428
|
-
const MAX_SERIALIZED_BYTES =
|
|
429
|
-
const MAX_STRING_LENGTH =
|
|
428
|
+
const MAX_SERIALIZED_BYTES = 262144;
|
|
429
|
+
const MAX_STRING_LENGTH = 32768;
|
|
430
430
|
const MAX_COLLECTION_ITEMS = 512;
|
|
431
431
|
const MAX_FAMILY_LENGTH = 200;
|
|
432
432
|
var SerializableSandboxTemplateBuilder = class SerializableSandboxTemplateBuilder {
|
|
@@ -809,6 +809,14 @@ const execViaE2BLease = async (lease, options) => {
|
|
|
809
809
|
};
|
|
810
810
|
}
|
|
811
811
|
};
|
|
812
|
+
/** Opens E2B's files API directly against the VM, authenticated by the exec lease. */
|
|
813
|
+
const e2bFilesForLease = (lease) => new e2b.Sandbox({
|
|
814
|
+
sandboxId: lease.sandboxId,
|
|
815
|
+
envdVersion: E2B_ENVD_VERSION,
|
|
816
|
+
envdAccessToken: lease.jwt,
|
|
817
|
+
sandboxUrl: lease.wsEndpoint,
|
|
818
|
+
validateApiKey: false
|
|
819
|
+
}).files;
|
|
812
820
|
//#endregion
|
|
813
821
|
//#region src/private-net-exec.ts
|
|
814
822
|
/**
|
|
@@ -979,6 +987,499 @@ async function execViaPrivateNetwork(instanceUrl, options) {
|
|
|
979
987
|
};
|
|
980
988
|
}
|
|
981
989
|
//#endregion
|
|
990
|
+
//#region ../../packages/_internals/workspace/dist/index.js
|
|
991
|
+
/** Directory, relative to the template's build cwd, that holds every marker below. */
|
|
992
|
+
const SETUP_MARKER_DIR = ".mastra-sandbox";
|
|
993
|
+
/**
|
|
994
|
+
* Setup completion marker shared by repo templates and their consumers.
|
|
995
|
+
*
|
|
996
|
+
* A repo template writes this file beside the checkout as its last build
|
|
997
|
+
* step, so it exists only in images where every setup command succeeded. Its
|
|
998
|
+
* content is a digest of the setup commands the image ran, letting a sandbox
|
|
999
|
+
* booted from the image tell whether the setup it is about to run already
|
|
1000
|
+
* happened. Relative to the template's build cwd, which is also the runtime
|
|
1001
|
+
* working directory the repo was cloned into.
|
|
1002
|
+
*
|
|
1003
|
+
* This is the single-repository marker. Templates built from a repository
|
|
1004
|
+
* list write one marker per repository instead ({@link repoSetupMarkerPath}).
|
|
1005
|
+
*/
|
|
1006
|
+
const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
|
|
1007
|
+
/**
|
|
1008
|
+
* Marker for the workspace-level setup of a multi-repository template: the
|
|
1009
|
+
* commands that ran at the working directory after every repository was set
|
|
1010
|
+
* up. Written even when there were no such commands, so a consumer can tell
|
|
1011
|
+
* "ran nothing" from "never built".
|
|
1012
|
+
*/
|
|
1013
|
+
const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
|
|
1014
|
+
/**
|
|
1015
|
+
* One line per repository directory whose setup failed while the build was
|
|
1016
|
+
* told to continue. Absent when nothing failed. Consumers check this before
|
|
1017
|
+
* trusting a per-repository marker: that marker means every step ran, not
|
|
1018
|
+
* that every step succeeded.
|
|
1019
|
+
*/
|
|
1020
|
+
const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
|
|
1021
|
+
/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
|
|
1022
|
+
function repoSetupMarkerPath(repoDir) {
|
|
1023
|
+
return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
|
|
1024
|
+
}
|
|
1025
|
+
/** Blank entries never become build steps, so they never count toward the digest either. */
|
|
1026
|
+
function normalizeSetupCommands(setupCommand) {
|
|
1027
|
+
return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
|
|
1028
|
+
}
|
|
1029
|
+
/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */
|
|
1030
|
+
function setupMarkerContent(setupCommand) {
|
|
1031
|
+
return `sha256:${(0, crypto.createHash)("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
|
|
1032
|
+
}
|
|
1033
|
+
/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
|
|
1034
|
+
function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
|
|
1035
|
+
return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
|
|
1036
|
+
}
|
|
1037
|
+
/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
|
|
1038
|
+
const REPO_DIR_PATTERN = /^[\w.-]+$/;
|
|
1039
|
+
/**
|
|
1040
|
+
* Shell step that runs one setup command inside a repository clone.
|
|
1041
|
+
*
|
|
1042
|
+
* Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
|
|
1043
|
+
* the `cd` sits inside the parenthesized group so that the failure branch
|
|
1044
|
+
* runs back at the build cwd and the failure list lands at the workspace
|
|
1045
|
+
* level, never inside the repository.
|
|
1046
|
+
*/
|
|
1047
|
+
function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
|
|
1048
|
+
if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
|
|
1049
|
+
if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
|
|
1050
|
+
return `( cd "${repoDir}" && sh -c ${shellQuote$2(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
|
|
1051
|
+
}
|
|
1052
|
+
function shellQuote$2(value) {
|
|
1053
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1054
|
+
}
|
|
1055
|
+
function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
|
|
1056
|
+
return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : ""}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;
|
|
1057
|
+
}
|
|
1058
|
+
/** Per-invocation auth header; `-c` config never reaches `.git/config`. */
|
|
1059
|
+
function gitAuthFlag$1(tokenEnv) {
|
|
1060
|
+
return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
|
|
1061
|
+
}
|
|
1062
|
+
function shellQuote$1(value) {
|
|
1063
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Shell scripts for the file operations a sandbox can only do atomically (or
|
|
1067
|
+
* without moving file bytes over the network) by running a command inside the
|
|
1068
|
+
* VM. Each builder takes absolute paths and returns one POSIX `sh` script; the
|
|
1069
|
+
* caller runs it with `sh -c` and maps the exit codes below to typed errors.
|
|
1070
|
+
*/
|
|
1071
|
+
/** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */
|
|
1072
|
+
const FILE_COMMAND_EXIT = {
|
|
1073
|
+
notFound: 20,
|
|
1074
|
+
isDirectory: 21,
|
|
1075
|
+
exists: 22,
|
|
1076
|
+
notDirectory: 23,
|
|
1077
|
+
destIsDirectory: 24
|
|
1078
|
+
};
|
|
1079
|
+
/** Single-quote a string for safe POSIX shell interpolation. */
|
|
1080
|
+
function shellQuote$3(value) {
|
|
1081
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1082
|
+
}
|
|
1083
|
+
const { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;
|
|
1084
|
+
/** `[ -e ]` is false for a broken symlink, which still occupies the path. */
|
|
1085
|
+
const present = (variable) => `{ [ -e "$${variable}" ] || [ -L "$${variable}" ]; }`;
|
|
1086
|
+
/**
|
|
1087
|
+
* A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on
|
|
1088
|
+
* one filesystem. Hidden, and unique per call.
|
|
1089
|
+
*/
|
|
1090
|
+
function siblingTempPath(path$1) {
|
|
1091
|
+
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
1092
|
+
return path.posix.join(path.posix.dirname(path$1), `.${path.posix.basename(path$1)}.mastra-tmp-${suffix}`);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when
|
|
1096
|
+
* something is at `$dest`. `ln` onto a directory (or a link to one) would
|
|
1097
|
+
* create the link inside it and succeed, so that case is undone and reported
|
|
1098
|
+
* as taken too.
|
|
1099
|
+
*/
|
|
1100
|
+
function linkTempIntoPlace() {
|
|
1101
|
+
return [
|
|
1102
|
+
`ln "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; ${present("dest")} && exit ${exists}; exit 1; }`,
|
|
1103
|
+
`if [ -d "$dest" ]; then rm -f "$dest/$(basename "$tmp")" "$tmp"; exit ${exists}; fi`,
|
|
1104
|
+
`rm -f "$tmp"`
|
|
1105
|
+
];
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.
|
|
1109
|
+
*
|
|
1110
|
+
* A file source copies its content (a symlinked source is followed). A
|
|
1111
|
+
* directory source with `recursive` copies its contents into `dest`, keeping
|
|
1112
|
+
* symlinks inside the tree as links. With `overwrite: false` the destination
|
|
1113
|
+
* is claimed atomically: a directory with an exclusive `mkdir`, a file by
|
|
1114
|
+
* copying to a temp name and hard-linking it into place (`link(2)` fails when
|
|
1115
|
+
* the destination exists).
|
|
1116
|
+
*
|
|
1117
|
+
* Exits: `notFound` (no source), `isDirectory` (directory source without
|
|
1118
|
+
* `recursive`), `destIsDirectory` (a file onto an existing directory or a link
|
|
1119
|
+
* to one), `exists`.
|
|
1120
|
+
*/
|
|
1121
|
+
function copyCommand(src, dest, options = {}) {
|
|
1122
|
+
const noClobber = options.overwrite === false;
|
|
1123
|
+
return [
|
|
1124
|
+
`src=${shellQuote$3(src)}`,
|
|
1125
|
+
`dest=${shellQuote$3(dest)}`,
|
|
1126
|
+
`[ -e "$src" ] || exit ${notFound}`,
|
|
1127
|
+
`mkdir -p "$(dirname "$dest")" || exit 1`,
|
|
1128
|
+
`if [ -d "$src" ]; then`,
|
|
1129
|
+
options.recursive ? ` :` : ` exit ${isDirectory}`,
|
|
1130
|
+
noClobber ? ` mkdir "$dest" 2>/dev/null || exit ${exists}` : ` mkdir -p "$dest" || exit 1`,
|
|
1131
|
+
` cp -R -P "$src"/. "$dest"/ || exit 1`,
|
|
1132
|
+
` exit 0`,
|
|
1133
|
+
`fi`,
|
|
1134
|
+
noClobber ? `${present("dest")} && exit ${exists}` : `if [ -d "$dest" ]; then exit ${destIsDirectory}; fi`,
|
|
1135
|
+
...noClobber ? [
|
|
1136
|
+
`tmp=${shellQuote$3(siblingTempPath(dest))}`,
|
|
1137
|
+
`cp "$src" "$tmp" || { rm -f "$tmp"; exit 1; }`,
|
|
1138
|
+
...linkTempIntoPlace()
|
|
1139
|
+
] : [`cp "$src" "$dest" || exit 1`]
|
|
1140
|
+
].join("\n");
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s
|
|
1144
|
+
* parent. A skipped `mv -n` is detected by the source surviving, whatever
|
|
1145
|
+
* its exit code. If a directory appears at `dest` between the check and the
|
|
1146
|
+
* move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported
|
|
1147
|
+
* as success; the no-clobber rename itself is atomic.
|
|
1148
|
+
*
|
|
1149
|
+
* Exits: `notFound` (no source), `exists`.
|
|
1150
|
+
*/
|
|
1151
|
+
function moveNoClobberCommand(src, dest) {
|
|
1152
|
+
return [
|
|
1153
|
+
`src=${shellQuote$3(src)}`,
|
|
1154
|
+
`dest=${shellQuote$3(dest)}`,
|
|
1155
|
+
`${present("src")} || exit ${notFound}`,
|
|
1156
|
+
`${present("dest")} && exit ${exists}`,
|
|
1157
|
+
`mkdir -p "$(dirname "$dest")" || exit 1`,
|
|
1158
|
+
`mv -n "$src" "$dest" 2>/dev/null || { ${present("src")} && ${present("dest")} && exit ${exists}; exit 1; }`,
|
|
1159
|
+
`${present("src")} && exit ${exists}`,
|
|
1160
|
+
`exit 0`
|
|
1161
|
+
].join("\n");
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.
|
|
1165
|
+
* The content travels as a file rather than a command argument, so it has no
|
|
1166
|
+
* size limit. Creates `dest` (and its parent) when missing.
|
|
1167
|
+
*
|
|
1168
|
+
* Exits: `isDirectory` (dest is a directory).
|
|
1169
|
+
*/
|
|
1170
|
+
function appendFromCommand(tmp, dest) {
|
|
1171
|
+
return [
|
|
1172
|
+
`tmp=${shellQuote$3(tmp)}`,
|
|
1173
|
+
`dest=${shellQuote$3(dest)}`,
|
|
1174
|
+
`if [ -d "$dest" ]; then rm -f "$tmp"; exit ${isDirectory}; fi`,
|
|
1175
|
+
`mkdir -p "$(dirname "$dest")" && cat "$tmp" >> "$dest"`,
|
|
1176
|
+
`status=$?`,
|
|
1177
|
+
`rm -f "$tmp"`,
|
|
1178
|
+
`exit $status`
|
|
1179
|
+
].join("\n");
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Publish the file already written to `tmp` at `dest` only if nothing is
|
|
1183
|
+
* there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;
|
|
1184
|
+
* `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.
|
|
1185
|
+
*
|
|
1186
|
+
* Exits: `exists`, `notDirectory` (dest's parent is not a directory).
|
|
1187
|
+
*/
|
|
1188
|
+
function linkNoClobberCommand(tmp, dest) {
|
|
1189
|
+
return [
|
|
1190
|
+
`tmp=${shellQuote$3(tmp)}`,
|
|
1191
|
+
`dest=${shellQuote$3(dest)}`,
|
|
1192
|
+
`if [ ! -d "$(dirname "$dest")" ]; then rm -f "$tmp"; exit ${notDirectory}; fi`,
|
|
1193
|
+
`if ${present("dest")}; then rm -f "$tmp"; exit ${exists}; fi`,
|
|
1194
|
+
...linkTempIntoPlace()
|
|
1195
|
+
].join("\n");
|
|
1196
|
+
}
|
|
1197
|
+
/** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */
|
|
1198
|
+
const DEFAULT_MAX_DEPTH = 100;
|
|
1199
|
+
const toAbsolute = (path$2) => path.posix.resolve("/", path$2);
|
|
1200
|
+
/** The entry's own type: a symlink is a link even when it points at a directory. */
|
|
1201
|
+
const isSymlink = (info) => info.symlinkTarget !== void 0;
|
|
1202
|
+
function toWriteData(content) {
|
|
1203
|
+
if (typeof content === "string") return content;
|
|
1204
|
+
return content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
1205
|
+
}
|
|
1206
|
+
function matchesExtension(name, extension) {
|
|
1207
|
+
if (!extension) return true;
|
|
1208
|
+
const ext = path.posix.extname(name);
|
|
1209
|
+
return (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* A sandbox's disk, read and written through E2B's envd files API, with the
|
|
1213
|
+
* operations the files API can't do in one call (copy, append, no-clobber
|
|
1214
|
+
* writes and moves) run as one shell command inside the VM so they are atomic
|
|
1215
|
+
* and file bytes never round-trip through the host. Paths are absolute inside
|
|
1216
|
+
* the sandbox; this class does not confine paths or symlinks to any root.
|
|
1217
|
+
*
|
|
1218
|
+
* Providers supply how to reach the files API and the shell, and how to read
|
|
1219
|
+
* their SDK's errors.
|
|
1220
|
+
*
|
|
1221
|
+
* If a provider's `runCommand` re-runs a command whose first run already
|
|
1222
|
+
* finished (e.g. a retry after a dropped connection), the second run sees the
|
|
1223
|
+
* first's result: a no-clobber copy, write or move reports `FileExistsError`
|
|
1224
|
+
* or `FileNotFoundError`, and an append fails, even though the work was done.
|
|
1225
|
+
*/
|
|
1226
|
+
var FilesApiFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
|
|
1227
|
+
async readFile(path, options) {
|
|
1228
|
+
const abs = toAbsolute(path);
|
|
1229
|
+
const bytes = await this.runFiles("readFile", async (files) => {
|
|
1230
|
+
try {
|
|
1231
|
+
return await files.read(abs, { format: "bytes" });
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
const kind = this.errorKind(error);
|
|
1234
|
+
if (kind === "not-found") throw new _mastra_core_workspace.FileNotFoundError(path);
|
|
1235
|
+
if (kind === "invalid-argument") throw new _mastra_core_workspace.IsDirectoryError(path);
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
const buffer$1 = buffer.Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1240
|
+
return options?.encoding ? buffer$1.toString(options.encoding) : buffer$1;
|
|
1241
|
+
}
|
|
1242
|
+
async writeFile(path$3, content, options) {
|
|
1243
|
+
const abs = toAbsolute(path$3);
|
|
1244
|
+
if (options?.overwrite !== false) {
|
|
1245
|
+
await this.runFiles("writeFile", async (files) => {
|
|
1246
|
+
if (options?.recursive === false) await this.#assertParentDirectory(files, abs, path$3);
|
|
1247
|
+
await this.#write(files, abs, path$3, toWriteData(content));
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const tmp = siblingTempPath(abs);
|
|
1252
|
+
const upload = async (files) => {
|
|
1253
|
+
if (options.recursive === false) await this.#assertParentDirectory(files, abs, path$3);
|
|
1254
|
+
await this.#write(files, tmp, path$3, toWriteData(content));
|
|
1255
|
+
};
|
|
1256
|
+
await this.#publishTemp(tmp, "writeFile", upload, linkNoClobberCommand(tmp, abs), path$3, {
|
|
1257
|
+
exists: () => new _mastra_core_workspace.FileExistsError(path$3),
|
|
1258
|
+
notDirectory: () => new _mastra_core_workspace.NotDirectoryError(path.posix.dirname(path$3))
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
async appendFile(path, content) {
|
|
1262
|
+
const abs = toAbsolute(path);
|
|
1263
|
+
const tmp = siblingTempPath(abs);
|
|
1264
|
+
const upload = (files) => this.#write(files, tmp, path, toWriteData(content));
|
|
1265
|
+
await this.#publishTemp(tmp, "appendFile", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new _mastra_core_workspace.IsDirectoryError(path) });
|
|
1266
|
+
}
|
|
1267
|
+
async deleteFile(path, options) {
|
|
1268
|
+
const abs = toAbsolute(path);
|
|
1269
|
+
await this.runFiles("deleteFile", async (files) => {
|
|
1270
|
+
const info = await this.#info(files, abs);
|
|
1271
|
+
if (!info) {
|
|
1272
|
+
if (options?.force) return;
|
|
1273
|
+
throw new _mastra_core_workspace.FileNotFoundError(path);
|
|
1274
|
+
}
|
|
1275
|
+
if (!isSymlink(info) && info.type === "dir") throw new _mastra_core_workspace.IsDirectoryError(path);
|
|
1276
|
+
await files.remove(abs);
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as
|
|
1281
|
+
* links; `overwrite: false` claims the destination atomically.
|
|
1282
|
+
*/
|
|
1283
|
+
async copyFile(src, dest, options) {
|
|
1284
|
+
const command = copyCommand(toAbsolute(src), toAbsolute(dest), {
|
|
1285
|
+
recursive: options?.recursive,
|
|
1286
|
+
overwrite: options?.overwrite
|
|
1287
|
+
});
|
|
1288
|
+
await this.#command("copyFile", command, `${src} -> ${dest}`, {
|
|
1289
|
+
notFound: () => new _mastra_core_workspace.FileNotFoundError(src),
|
|
1290
|
+
isDirectory: () => new _mastra_core_workspace.IsDirectoryError(src),
|
|
1291
|
+
destIsDirectory: () => new _mastra_core_workspace.IsDirectoryError(dest),
|
|
1292
|
+
exists: () => new _mastra_core_workspace.FileExistsError(dest)
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
async moveFile(src, dest, options) {
|
|
1296
|
+
const srcAbs = toAbsolute(src);
|
|
1297
|
+
const destAbs = toAbsolute(dest);
|
|
1298
|
+
if (options?.overwrite === false) {
|
|
1299
|
+
await this.#command("moveFile", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {
|
|
1300
|
+
notFound: () => new _mastra_core_workspace.FileNotFoundError(src),
|
|
1301
|
+
exists: () => new _mastra_core_workspace.FileExistsError(dest)
|
|
1302
|
+
});
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
await this.runFiles("moveFile", async (files) => {
|
|
1306
|
+
try {
|
|
1307
|
+
await files.rename(srcAbs, destAbs);
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
if (this.errorKind(error) === "not-found") throw new _mastra_core_workspace.FileNotFoundError(src);
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
async mkdir(path, options) {
|
|
1315
|
+
const abs = toAbsolute(path);
|
|
1316
|
+
await this.runFiles("mkdir", async (files) => {
|
|
1317
|
+
if (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);
|
|
1318
|
+
try {
|
|
1319
|
+
await files.makeDir(abs);
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
if (this.errorKind(error) === "invalid-argument") throw new _mastra_core_workspace.FileExistsError(path);
|
|
1322
|
+
throw error;
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
async rmdir(path, options) {
|
|
1327
|
+
const abs = toAbsolute(path);
|
|
1328
|
+
await this.runFiles("rmdir", async (files) => {
|
|
1329
|
+
const info = await this.#info(files, abs);
|
|
1330
|
+
if (!info) {
|
|
1331
|
+
if (options?.force) return;
|
|
1332
|
+
throw new _mastra_core_workspace.DirectoryNotFoundError(path);
|
|
1333
|
+
}
|
|
1334
|
+
if (!options?.recursive) {
|
|
1335
|
+
if (isSymlink(info) || info.type !== "dir") throw new _mastra_core_workspace.NotDirectoryError(path);
|
|
1336
|
+
if ((await files.list(abs)).length > 0) throw new _mastra_core_workspace.DirectoryNotEmptyError(path);
|
|
1337
|
+
} else if (!isSymlink(info) && info.type !== "dir") throw new _mastra_core_workspace.NotDirectoryError(path);
|
|
1338
|
+
await files.remove(abs);
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
async readdir(path$4, options) {
|
|
1342
|
+
const abs = toAbsolute(path$4);
|
|
1343
|
+
const depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;
|
|
1344
|
+
const entries = await this.runFiles("readdir", async (files) => {
|
|
1345
|
+
try {
|
|
1346
|
+
return await files.list(abs, { depth });
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
const kind = this.errorKind(error);
|
|
1349
|
+
if (kind === "not-found") throw new _mastra_core_workspace.DirectoryNotFoundError(path$4);
|
|
1350
|
+
if (kind === "invalid-argument") throw new _mastra_core_workspace.NotDirectoryError(path$4);
|
|
1351
|
+
throw error;
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
const result = [];
|
|
1355
|
+
for (const entry of entries) {
|
|
1356
|
+
if (!entry.type) continue;
|
|
1357
|
+
const name = path.posix.relative(abs, entry.path);
|
|
1358
|
+
const type = entry.type === "dir" ? "directory" : "file";
|
|
1359
|
+
if (type === "file" && !matchesExtension(name, options?.extension)) continue;
|
|
1360
|
+
const link = isSymlink(entry);
|
|
1361
|
+
result.push({
|
|
1362
|
+
name,
|
|
1363
|
+
type,
|
|
1364
|
+
...type === "file" && !link && { size: entry.size },
|
|
1365
|
+
...link && {
|
|
1366
|
+
isSymlink: true,
|
|
1367
|
+
symlinkTarget: entry.symlinkTarget
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
return result;
|
|
1372
|
+
}
|
|
1373
|
+
async exists(path) {
|
|
1374
|
+
const abs = toAbsolute(path);
|
|
1375
|
+
return this.runFiles("exists", async (files) => Boolean((await this.#info(files, abs))?.type));
|
|
1376
|
+
}
|
|
1377
|
+
async stat(path$5) {
|
|
1378
|
+
const abs = toAbsolute(path$5);
|
|
1379
|
+
return this.runFiles("stat", async (files) => {
|
|
1380
|
+
let info = await this.#info(files, abs);
|
|
1381
|
+
if (info?.type && isSymlink(info)) info = await this.#info(files, path.posix.resolve(path.posix.dirname(abs), info.symlinkTarget));
|
|
1382
|
+
if (!info?.type) throw new _mastra_core_workspace.FileNotFoundError(path$5);
|
|
1383
|
+
const modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);
|
|
1384
|
+
return {
|
|
1385
|
+
name: path.posix.basename(abs),
|
|
1386
|
+
path: abs,
|
|
1387
|
+
type: info.type === "dir" ? "directory" : "file",
|
|
1388
|
+
size: info.type === "dir" ? 0 : info.size,
|
|
1389
|
+
createdAt: modifiedAt,
|
|
1390
|
+
modifiedAt
|
|
1391
|
+
};
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
/** `getInfo` without following the entry itself, or `undefined` when nothing is there. */
|
|
1395
|
+
async #info(files, abs) {
|
|
1396
|
+
try {
|
|
1397
|
+
return await files.getInfo(abs);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
if (this.errorKind(error) === "not-found") return void 0;
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
async #assertParentDirectory(files, abs, path$6) {
|
|
1404
|
+
const parent = await this.#info(files, path.posix.dirname(abs));
|
|
1405
|
+
if (!parent?.type) throw new _mastra_core_workspace.DirectoryNotFoundError(path.posix.dirname(path$6));
|
|
1406
|
+
if (parent.type !== "dir") throw new _mastra_core_workspace.NotDirectoryError(path.posix.dirname(path$6));
|
|
1407
|
+
}
|
|
1408
|
+
async #write(files, abs, path, data) {
|
|
1409
|
+
try {
|
|
1410
|
+
await files.write(abs, data);
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
if (this.errorKind(error) === "invalid-argument") throw new _mastra_core_workspace.IsDirectoryError(path);
|
|
1413
|
+
throw error;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Upload the temp file `tmp`, then run the command that consumes it. The
|
|
1418
|
+
* script deletes it on every path it controls; if the upload or the command
|
|
1419
|
+
* fails part-way, delete it here so it doesn't linger in the user's tree.
|
|
1420
|
+
*/
|
|
1421
|
+
async #publishTemp(tmp, operation, upload, script, subject, errors) {
|
|
1422
|
+
try {
|
|
1423
|
+
await this.runFiles(operation, upload);
|
|
1424
|
+
await this.#command(operation, script, subject, errors);
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
await this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});
|
|
1427
|
+
throw error;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
/** Run a file command and turn its known exit codes into typed errors. */
|
|
1431
|
+
async #command(operation, script, subject, errors) {
|
|
1432
|
+
const result = await this.runCommand(operation, script);
|
|
1433
|
+
if (result.exitCode === 0) return;
|
|
1434
|
+
for (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {
|
|
1435
|
+
const toError = errors[outcome];
|
|
1436
|
+
if (code === result.exitCode && toError) throw toError();
|
|
1437
|
+
}
|
|
1438
|
+
throw new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/sandbox-filesystem.ts
|
|
1443
|
+
/**
|
|
1444
|
+
* envd's edge answers a paused or missing sandbox with a 404 or 502 that the
|
|
1445
|
+
* SDK reports as `FileNotFoundError` or `TimeoutError`. Same message pattern as
|
|
1446
|
+
* `E2BSandbox`'s dead-sandbox check.
|
|
1447
|
+
*/
|
|
1448
|
+
const DEAD_SANDBOX = /\b(?:paused\s+)?sandbox(?:\s+\S+)?\s+(?:was\s+)?not found\b/i;
|
|
1449
|
+
const isDeadSandbox = (error) => error instanceof e2b.SandboxNotFoundError || error instanceof Error && DEAD_SANDBOX.test(error.message);
|
|
1450
|
+
/** A failure that isn't a file-level answer from envd: the sandbox is gone, the call timed out, or it failed before envd replied. */
|
|
1451
|
+
const isBrokenSandboxError = (error) => isDeadSandbox(error) || error instanceof e2b.TimeoutError || !(error instanceof e2b.SandboxError);
|
|
1452
|
+
/**
|
|
1453
|
+
* The disk of a {@link PlatformSandbox}, read and written through E2B's files
|
|
1454
|
+
* API directly on the VM, authenticated by the same lease `executeCommand`
|
|
1455
|
+
* uses. Copies, appends and no-clobber writes and moves run as one shell
|
|
1456
|
+
* command in the VM, so they are atomic and file bytes don't round-trip
|
|
1457
|
+
* through the host. Paths are absolute inside the sandbox; this class does not
|
|
1458
|
+
* confine paths or symlinks to any root.
|
|
1459
|
+
*/
|
|
1460
|
+
var PlatformSandboxFilesystem = class extends FilesApiFilesystem {
|
|
1461
|
+
id;
|
|
1462
|
+
name = "PlatformSandboxFilesystem";
|
|
1463
|
+
provider = "platform";
|
|
1464
|
+
status = "ready";
|
|
1465
|
+
#runner;
|
|
1466
|
+
constructor(runner) {
|
|
1467
|
+
super({ name: "PlatformSandboxFilesystem" });
|
|
1468
|
+
this.id = `${runner.id}-filesystem`;
|
|
1469
|
+
this.#runner = runner;
|
|
1470
|
+
}
|
|
1471
|
+
runFiles(operation, fn) {
|
|
1472
|
+
return this.#runner.withFiles(operation, fn);
|
|
1473
|
+
}
|
|
1474
|
+
runCommand(_operation, script) {
|
|
1475
|
+
return this.#runner.runCommand(script);
|
|
1476
|
+
}
|
|
1477
|
+
errorKind(error) {
|
|
1478
|
+
if (error instanceof e2b.NotFoundError && !isDeadSandbox(error)) return "not-found";
|
|
1479
|
+
if (error instanceof e2b.InvalidArgumentError) return "invalid-argument";
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
//#endregion
|
|
982
1483
|
//#region src/sandbox.ts
|
|
983
1484
|
/**
|
|
984
1485
|
* How long before a lease's stated `expiresAt` we should treat it as
|
|
@@ -1061,6 +1562,8 @@ var SandboxDestroyedError = class extends Error {
|
|
|
1061
1562
|
this.attempts = diagnostics.attempts;
|
|
1062
1563
|
}
|
|
1063
1564
|
};
|
|
1565
|
+
/** Cap on one file command (a recursive copy can be large) when the sandbox sets no timeout. */
|
|
1566
|
+
const FILE_COMMAND_TIMEOUT_MS = 6e5;
|
|
1064
1567
|
/**
|
|
1065
1568
|
* Compose a shell command line from a `command` string and optional `args`.
|
|
1066
1569
|
*
|
|
@@ -1076,9 +1579,9 @@ var SandboxDestroyedError = class extends Error {
|
|
|
1076
1579
|
* sandbox.
|
|
1077
1580
|
*/
|
|
1078
1581
|
function buildCommand(command, args) {
|
|
1079
|
-
return args?.length ? `${command} ${args.map(shellQuote
|
|
1582
|
+
return args?.length ? `${command} ${args.map(shellQuote).join(" ")}` : command;
|
|
1080
1583
|
}
|
|
1081
|
-
function shellQuote
|
|
1584
|
+
function shellQuote(arg) {
|
|
1082
1585
|
if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
|
|
1083
1586
|
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
1084
1587
|
}
|
|
@@ -1179,6 +1682,10 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1179
1682
|
_createdAt = null;
|
|
1180
1683
|
_webSocketFactory;
|
|
1181
1684
|
_e2bExecRunner;
|
|
1685
|
+
_e2bFilesFactory;
|
|
1686
|
+
/** Files handle for the current lease, rebuilt whenever the lease changes. */
|
|
1687
|
+
_e2bFiles = null;
|
|
1688
|
+
_filesystem;
|
|
1182
1689
|
_privateNetFetch;
|
|
1183
1690
|
/**
|
|
1184
1691
|
* Registry that maps `sandboxId → instanceUrl` for the private-network
|
|
@@ -1268,6 +1775,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1268
1775
|
this._instructionsOverride = options.instructions;
|
|
1269
1776
|
this._webSocketFactory = options.webSocketFactory;
|
|
1270
1777
|
this._e2bExecRunner = options.e2bExecRunner ?? execViaE2BLease;
|
|
1778
|
+
this._e2bFilesFactory = options.e2bFilesFactory ?? e2bFilesForLease;
|
|
1271
1779
|
this._privateNetFetch = options.privateNetFetch;
|
|
1272
1780
|
this._addressRegistry = options.addressRegistry;
|
|
1273
1781
|
}
|
|
@@ -1774,6 +2282,15 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1774
2282
|
* execution on the remote sandbox.
|
|
1775
2283
|
*/
|
|
1776
2284
|
async executeCommand(command, args, options) {
|
|
2285
|
+
return this._executeCommand(command, args, options, { once: false });
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* `executeCommand`, optionally sent at most once. With `once`, a transport
|
|
2289
|
+
* failure after the command may have started is thrown instead of retried
|
|
2290
|
+
* (on the lease or over the private network), so a script that changes files
|
|
2291
|
+
* never runs twice.
|
|
2292
|
+
*/
|
|
2293
|
+
async _executeCommand(command, args, options, { once }) {
|
|
1777
2294
|
await this.ensureRunning();
|
|
1778
2295
|
if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
1779
2296
|
const started = Date.now();
|
|
@@ -1788,7 +2305,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1788
2305
|
}
|
|
1789
2306
|
} : options;
|
|
1790
2307
|
await this._awaitTransportReady();
|
|
1791
|
-
const instanceUrl = this._addressRegistry?.get(this._sandboxId);
|
|
2308
|
+
const instanceUrl = once ? void 0 : this._addressRegistry?.get(this._sandboxId);
|
|
1792
2309
|
if (instanceUrl) {
|
|
1793
2310
|
const privateNet = await this._tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, execCallOptions);
|
|
1794
2311
|
if (privateNet) {
|
|
@@ -1804,7 +2321,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1804
2321
|
};
|
|
1805
2322
|
}
|
|
1806
2323
|
}
|
|
1807
|
-
const result = await this._runDirectExec(fullCommand, effectiveTimeout, execCallOptions);
|
|
2324
|
+
const result = await this._runDirectExec(fullCommand, effectiveTimeout, execCallOptions, once ? 1 : 2);
|
|
1808
2325
|
const exitCode = result.exitCode ?? 124;
|
|
1809
2326
|
return {
|
|
1810
2327
|
success: exitCode === 0,
|
|
@@ -1849,12 +2366,12 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1849
2366
|
const filtered = Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0));
|
|
1850
2367
|
return Object.keys(filtered).length > 0 ? filtered : void 0;
|
|
1851
2368
|
}
|
|
1852
|
-
async _runDirectExec(fullCommand, effectiveTimeout, options) {
|
|
2369
|
+
async _runDirectExec(fullCommand, effectiveTimeout, options, attempts = 2) {
|
|
1853
2370
|
const filteredEnv = this._execEnv(options);
|
|
1854
2371
|
let lastResult;
|
|
1855
2372
|
let lastLease;
|
|
1856
2373
|
let attemptsMade = 0;
|
|
1857
|
-
for (let attempt = 0; attempt <
|
|
2374
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1858
2375
|
if (attempt > 0 && lastLease && this._lease === lastLease) this._lease = null;
|
|
1859
2376
|
let lease;
|
|
1860
2377
|
try {
|
|
@@ -1986,6 +2503,91 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1986
2503
|
if (this._leaseInFlight === inFlight) this._leaseInFlight = null;
|
|
1987
2504
|
}
|
|
1988
2505
|
}
|
|
2506
|
+
/**
|
|
2507
|
+
* The sandbox's disk through E2B's files API, called directly on the VM with
|
|
2508
|
+
* the same lease `executeCommand` uses. Paths are absolute inside the
|
|
2509
|
+
* sandbox and are not confined to any root. Absent on non-E2B sandboxes, so
|
|
2510
|
+
* callers fall back to `executeCommand`.
|
|
2511
|
+
*/
|
|
2512
|
+
get filesystem() {
|
|
2513
|
+
if (this._client.sandboxProvider !== "e2b") return void 0;
|
|
2514
|
+
this._filesystem ??= new PlatformSandboxFilesystem({
|
|
2515
|
+
id: this.id,
|
|
2516
|
+
withFiles: (operation, fn) => this._withE2BFiles(operation, fn),
|
|
2517
|
+
runCommand: (script) => this._executeCommand("sh", ["-c", script], this._timeout === void 0 ? { timeout: FILE_COMMAND_TIMEOUT_MS } : {}, { once: true })
|
|
2518
|
+
});
|
|
2519
|
+
return this._filesystem;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Run a filesystem operation against the VM through the files API. Each
|
|
2523
|
+
* files call that envd rejects for its token is retried once with a fresh
|
|
2524
|
+
* lease: the rejection proves that call did nothing, and the calls before it
|
|
2525
|
+
* already succeeded, so nothing is replayed. Any other failure is thrown
|
|
2526
|
+
* unchanged. A dead sandbox or a broken transport also drops the cached
|
|
2527
|
+
* lease, so the next operation re-mints it and learns about a destroyed
|
|
2528
|
+
* sandbox (410) the same way `executeCommand` does.
|
|
2529
|
+
*/
|
|
2530
|
+
async _withE2BFiles(operation, fn) {
|
|
2531
|
+
await this.ensureRunning();
|
|
2532
|
+
if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
2533
|
+
let current = await this._filesHandle(operation, 1);
|
|
2534
|
+
const call = async (invoke) => {
|
|
2535
|
+
try {
|
|
2536
|
+
return await invoke(current.files);
|
|
2537
|
+
} catch (error) {
|
|
2538
|
+
if (error instanceof e2b.AuthenticationError) {
|
|
2539
|
+
if (this._lease === current.lease) this._lease = null;
|
|
2540
|
+
current = await this._filesHandle(operation, 2);
|
|
2541
|
+
try {
|
|
2542
|
+
return await invoke(current.files);
|
|
2543
|
+
} catch (retryError) {
|
|
2544
|
+
if (isBrokenSandboxError(retryError) && this._lease === current.lease) this._lease = null;
|
|
2545
|
+
throw retryError;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
if (isBrokenSandboxError(error) && this._lease === current.lease) this._lease = null;
|
|
2549
|
+
throw error;
|
|
2550
|
+
}
|
|
2551
|
+
};
|
|
2552
|
+
return fn({
|
|
2553
|
+
read: (path, opts) => call((files) => files.read(path, opts)),
|
|
2554
|
+
write: (path, data) => call((files) => files.write(path, data)),
|
|
2555
|
+
list: (path, opts) => call((files) => files.list(path, opts)),
|
|
2556
|
+
makeDir: (path) => call((files) => files.makeDir(path)),
|
|
2557
|
+
rename: (oldPath, newPath) => call((files) => files.rename(oldPath, newPath)),
|
|
2558
|
+
remove: (path) => call((files) => files.remove(path)),
|
|
2559
|
+
getInfo: (path) => call((files) => files.getInfo(path))
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
/** The files handle for the current lease, rebuilt only when the lease changes. */
|
|
2563
|
+
async _filesHandle(operation, attempts) {
|
|
2564
|
+
const lease = await this._fileLease(operation, attempts);
|
|
2565
|
+
if (lease.provider !== "e2b") throw new Error(`PlatformSandbox.filesystem requires an E2B-backed sandbox; this sandbox's provider is "${lease.provider}"`);
|
|
2566
|
+
if (this._e2bFiles?.lease !== lease) this._e2bFiles = {
|
|
2567
|
+
lease,
|
|
2568
|
+
files: this._e2bFilesFactory(lease)
|
|
2569
|
+
};
|
|
2570
|
+
return this._e2bFiles;
|
|
2571
|
+
}
|
|
2572
|
+
/** `_ensureLease`, with a 410 handled the same way `_runDirectExec` handles it. */
|
|
2573
|
+
async _fileLease(operation, attempts) {
|
|
2574
|
+
try {
|
|
2575
|
+
return await this._ensureLease();
|
|
2576
|
+
} catch (error) {
|
|
2577
|
+
if (error instanceof PlatformApiError && error.status === 410) {
|
|
2578
|
+
this._lease = null;
|
|
2579
|
+
const priorSandboxId = this._sandboxId;
|
|
2580
|
+
this._sandboxId = void 0;
|
|
2581
|
+
this.status = "stopped";
|
|
2582
|
+
throw new SandboxDestroyedError(`Sandbox ${priorSandboxId ?? "(unknown)"} was destroyed; /exec-lease returned 410`, {
|
|
2583
|
+
...priorSandboxId && { sandboxId: priorSandboxId },
|
|
2584
|
+
command: `filesystem.${operation}`,
|
|
2585
|
+
attempts
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
throw error;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
1989
2591
|
async getInfo() {
|
|
1990
2592
|
if (!this._sandboxId || this.status !== "running") return {
|
|
1991
2593
|
id: this.id,
|
|
@@ -2038,82 +2640,6 @@ function templateHash(definition) {
|
|
|
2038
2640
|
return (0, crypto.createHash)("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
|
|
2039
2641
|
}
|
|
2040
2642
|
//#endregion
|
|
2041
|
-
//#region ../../packages/_internals/workspace/dist/index.js
|
|
2042
|
-
/** Directory, relative to the template's build cwd, that holds every marker below. */
|
|
2043
|
-
const SETUP_MARKER_DIR = ".mastra-sandbox";
|
|
2044
|
-
/**
|
|
2045
|
-
* Setup completion marker shared by repo templates and their consumers.
|
|
2046
|
-
*
|
|
2047
|
-
* A repo template writes this file beside the checkout as its last build
|
|
2048
|
-
* step, so it exists only in images where every setup command succeeded. Its
|
|
2049
|
-
* content is a digest of the setup commands the image ran, letting a sandbox
|
|
2050
|
-
* booted from the image tell whether the setup it is about to run already
|
|
2051
|
-
* happened. Relative to the template's build cwd, which is also the runtime
|
|
2052
|
-
* working directory the repo was cloned into.
|
|
2053
|
-
*
|
|
2054
|
-
* This is the single-repository marker. Templates built from a repository
|
|
2055
|
-
* list write one marker per repository instead ({@link repoSetupMarkerPath}).
|
|
2056
|
-
*/
|
|
2057
|
-
const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
|
|
2058
|
-
/**
|
|
2059
|
-
* Marker for the workspace-level setup of a multi-repository template: the
|
|
2060
|
-
* commands that ran at the working directory after every repository was set
|
|
2061
|
-
* up. Written even when there were no such commands, so a consumer can tell
|
|
2062
|
-
* "ran nothing" from "never built".
|
|
2063
|
-
*/
|
|
2064
|
-
const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
|
|
2065
|
-
/**
|
|
2066
|
-
* One line per repository directory whose setup failed while the build was
|
|
2067
|
-
* told to continue. Absent when nothing failed. Consumers check this before
|
|
2068
|
-
* trusting a per-repository marker: that marker means every step ran, not
|
|
2069
|
-
* that every step succeeded.
|
|
2070
|
-
*/
|
|
2071
|
-
const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
|
|
2072
|
-
/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
|
|
2073
|
-
function repoSetupMarkerPath(repoDir) {
|
|
2074
|
-
return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
|
|
2075
|
-
}
|
|
2076
|
-
/** Blank entries never become build steps, so they never count toward the digest either. */
|
|
2077
|
-
function normalizeSetupCommands(setupCommand) {
|
|
2078
|
-
return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
|
|
2079
|
-
}
|
|
2080
|
-
/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */
|
|
2081
|
-
function setupMarkerContent(setupCommand) {
|
|
2082
|
-
return `sha256:${(0, crypto.createHash)("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
|
|
2083
|
-
}
|
|
2084
|
-
/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
|
|
2085
|
-
function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
|
|
2086
|
-
return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
|
|
2087
|
-
}
|
|
2088
|
-
/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
|
|
2089
|
-
const REPO_DIR_PATTERN = /^[\w.-]+$/;
|
|
2090
|
-
/**
|
|
2091
|
-
* Shell step that runs one setup command inside a repository clone.
|
|
2092
|
-
*
|
|
2093
|
-
* Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
|
|
2094
|
-
* the `cd` sits inside the parenthesized group so that the failure branch
|
|
2095
|
-
* runs back at the build cwd and the failure list lands at the workspace
|
|
2096
|
-
* level, never inside the repository.
|
|
2097
|
-
*/
|
|
2098
|
-
function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
|
|
2099
|
-
if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
|
|
2100
|
-
if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
|
|
2101
|
-
return `( cd "${repoDir}" && sh -c ${shellQuote$1(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
|
|
2102
|
-
}
|
|
2103
|
-
function shellQuote$1(value) {
|
|
2104
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2105
|
-
}
|
|
2106
|
-
function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
|
|
2107
|
-
return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
|
|
2108
|
-
}
|
|
2109
|
-
/** Per-invocation auth header; `-c` config never reaches `.git/config`. */
|
|
2110
|
-
function gitAuthFlag$1(tokenEnv) {
|
|
2111
|
-
return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
|
|
2112
|
-
}
|
|
2113
|
-
function shellQuote(value) {
|
|
2114
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2115
|
-
}
|
|
2116
|
-
//#endregion
|
|
2117
2643
|
//#region src/repo-template.ts
|
|
2118
2644
|
const execFileAsync = (0, util.promisify)(child_process.execFile);
|
|
2119
2645
|
const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
@@ -2392,7 +2918,7 @@ async function resolveDefaultBranchHead(cloneUrl, token, execute = execFileAsync
|
|
|
2392
2918
|
"HEAD"
|
|
2393
2919
|
], {
|
|
2394
2920
|
timeout: 1e4,
|
|
2395
|
-
maxBuffer:
|
|
2921
|
+
maxBuffer: 1048576,
|
|
2396
2922
|
env
|
|
2397
2923
|
});
|
|
2398
2924
|
const sha = stdout.trim().split(/\s+/, 1)[0];
|
|
@@ -2700,6 +3226,7 @@ exports.PlatformClient = PlatformClient;
|
|
|
2700
3226
|
exports.PlatformFactorySandbox = PlatformFactorySandbox;
|
|
2701
3227
|
exports.PlatformFilesystem = PlatformFilesystem;
|
|
2702
3228
|
exports.PlatformSandbox = PlatformSandbox;
|
|
3229
|
+
exports.PlatformSandboxFilesystem = PlatformSandboxFilesystem;
|
|
2703
3230
|
exports.PrivateNetExecHttpError = PrivateNetExecHttpError;
|
|
2704
3231
|
exports.SandboxDestroyedError = SandboxDestroyedError;
|
|
2705
3232
|
exports.SandboxExecTransportError = SandboxExecTransportError;
|