@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/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Buffer as Buffer$1 } from "buffer";
2
- import nodePath from "path";
3
- import { FactorySandbox, FileExistsError, FileNotFoundError, MastraFilesystem, MastraSandbox, ProcessHandle, SandboxError, SandboxNotReadyError, SandboxProcessManager, UnsupportedStdinCloseError, WorkspaceReadOnlyError } from "@mastra/core/workspace";
2
+ import nodePath, { posix } from "path";
3
+ import { DirectoryNotEmptyError, DirectoryNotFoundError, FactorySandbox, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, MastraSandbox, NotDirectoryError, ProcessHandle, SandboxError, SandboxNotReadyError, SandboxProcessManager, UnsupportedStdinCloseError, WorkspaceReadOnlyError } from "@mastra/core/workspace";
4
4
  import { createHash } from "crypto";
5
- import { CommandExitError, Sandbox, TimeoutError } from "e2b";
5
+ import { AuthenticationError, CommandExitError, InvalidArgumentError, NotFoundError, Sandbox, SandboxError as SandboxError$1, SandboxNotFoundError, TimeoutError } from "e2b";
6
6
  import { execFile } from "child_process";
7
7
  import { promisify } from "util";
8
8
  //#region src/client.ts
@@ -310,7 +310,7 @@ var PlatformFilesystem = class extends MastraFilesystem {
310
310
  name: nameFromPath(object.key),
311
311
  type: "file",
312
312
  size: object.size
313
- }))].filter((entry) => !options?.extension || entry.type === "directory" || matchesExtension(entry.name, options.extension));
313
+ }))].filter((entry) => !options?.extension || entry.type === "directory" || matchesExtension$1(entry.name, options.extension));
314
314
  }
315
315
  async listPrefix(prefix, recursive) {
316
316
  const normalized = prefix.replace(/\/$/, "");
@@ -393,7 +393,7 @@ var PlatformFilesystem = class extends MastraFilesystem {
393
393
  };
394
394
  }
395
395
  };
396
- function matchesExtension(name, extension) {
396
+ function matchesExtension$1(name, extension) {
397
397
  return (Array.isArray(extension) ? extension : [extension]).some((ext) => name.endsWith(ext));
398
398
  }
399
399
  //#endregion
@@ -401,8 +401,8 @@ function matchesExtension(name, extension) {
401
401
  const SERIALIZE_TEMPLATE = Symbol("serializeTemplate");
402
402
  const GET_TEMPLATE_BUILD_ENVS = Symbol("getTemplateBuildEnvs");
403
403
  const MAX_OPERATIONS = 256;
404
- const MAX_SERIALIZED_BYTES = 256 * 1024;
405
- const MAX_STRING_LENGTH = 32 * 1024;
404
+ const MAX_SERIALIZED_BYTES = 262144;
405
+ const MAX_STRING_LENGTH = 32768;
406
406
  const MAX_COLLECTION_ITEMS = 512;
407
407
  const MAX_FAMILY_LENGTH = 200;
408
408
  var SerializableSandboxTemplateBuilder = class SerializableSandboxTemplateBuilder {
@@ -785,6 +785,14 @@ const execViaE2BLease = async (lease, options) => {
785
785
  };
786
786
  }
787
787
  };
788
+ /** Opens E2B's files API directly against the VM, authenticated by the exec lease. */
789
+ const e2bFilesForLease = (lease) => new Sandbox({
790
+ sandboxId: lease.sandboxId,
791
+ envdVersion: E2B_ENVD_VERSION,
792
+ envdAccessToken: lease.jwt,
793
+ sandboxUrl: lease.wsEndpoint,
794
+ validateApiKey: false
795
+ }).files;
788
796
  //#endregion
789
797
  //#region src/private-net-exec.ts
790
798
  /**
@@ -955,6 +963,499 @@ async function execViaPrivateNetwork(instanceUrl, options) {
955
963
  };
956
964
  }
957
965
  //#endregion
966
+ //#region ../../packages/_internals/workspace/dist/index.js
967
+ /** Directory, relative to the template's build cwd, that holds every marker below. */
968
+ const SETUP_MARKER_DIR = ".mastra-sandbox";
969
+ /**
970
+ * Setup completion marker shared by repo templates and their consumers.
971
+ *
972
+ * A repo template writes this file beside the checkout as its last build
973
+ * step, so it exists only in images where every setup command succeeded. Its
974
+ * content is a digest of the setup commands the image ran, letting a sandbox
975
+ * booted from the image tell whether the setup it is about to run already
976
+ * happened. Relative to the template's build cwd, which is also the runtime
977
+ * working directory the repo was cloned into.
978
+ *
979
+ * This is the single-repository marker. Templates built from a repository
980
+ * list write one marker per repository instead ({@link repoSetupMarkerPath}).
981
+ */
982
+ const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
983
+ /**
984
+ * Marker for the workspace-level setup of a multi-repository template: the
985
+ * commands that ran at the working directory after every repository was set
986
+ * up. Written even when there were no such commands, so a consumer can tell
987
+ * "ran nothing" from "never built".
988
+ */
989
+ const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
990
+ /**
991
+ * One line per repository directory whose setup failed while the build was
992
+ * told to continue. Absent when nothing failed. Consumers check this before
993
+ * trusting a per-repository marker: that marker means every step ran, not
994
+ * that every step succeeded.
995
+ */
996
+ const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
997
+ /** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
998
+ function repoSetupMarkerPath(repoDir) {
999
+ return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
1000
+ }
1001
+ /** Blank entries never become build steps, so they never count toward the digest either. */
1002
+ function normalizeSetupCommands(setupCommand) {
1003
+ return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
1004
+ }
1005
+ /** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */
1006
+ function setupMarkerContent(setupCommand) {
1007
+ return `sha256:${createHash("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
1008
+ }
1009
+ /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
1010
+ function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
1011
+ return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
1012
+ }
1013
+ /** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
1014
+ const REPO_DIR_PATTERN = /^[\w.-]+$/;
1015
+ /**
1016
+ * Shell step that runs one setup command inside a repository clone.
1017
+ *
1018
+ * Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
1019
+ * the `cd` sits inside the parenthesized group so that the failure branch
1020
+ * runs back at the build cwd and the failure list lands at the workspace
1021
+ * level, never inside the repository.
1022
+ */
1023
+ function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
1024
+ if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
1025
+ if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
1026
+ 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}"; }`;
1027
+ }
1028
+ function shellQuote$2(value) {
1029
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1030
+ }
1031
+ function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
1032
+ return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : ""}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;
1033
+ }
1034
+ /** Per-invocation auth header; `-c` config never reaches `.git/config`. */
1035
+ function gitAuthFlag$1(tokenEnv) {
1036
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
1037
+ }
1038
+ function shellQuote$1(value) {
1039
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1040
+ }
1041
+ /**
1042
+ * Shell scripts for the file operations a sandbox can only do atomically (or
1043
+ * without moving file bytes over the network) by running a command inside the
1044
+ * VM. Each builder takes absolute paths and returns one POSIX `sh` script; the
1045
+ * caller runs it with `sh -c` and maps the exit codes below to typed errors.
1046
+ */
1047
+ /** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */
1048
+ const FILE_COMMAND_EXIT = {
1049
+ notFound: 20,
1050
+ isDirectory: 21,
1051
+ exists: 22,
1052
+ notDirectory: 23,
1053
+ destIsDirectory: 24
1054
+ };
1055
+ /** Single-quote a string for safe POSIX shell interpolation. */
1056
+ function shellQuote$3(value) {
1057
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1058
+ }
1059
+ const { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;
1060
+ /** `[ -e ]` is false for a broken symlink, which still occupies the path. */
1061
+ const present = (variable) => `{ [ -e "$${variable}" ] || [ -L "$${variable}" ]; }`;
1062
+ /**
1063
+ * A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on
1064
+ * one filesystem. Hidden, and unique per call.
1065
+ */
1066
+ function siblingTempPath(path) {
1067
+ const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
1068
+ return posix.join(posix.dirname(path), `.${posix.basename(path)}.mastra-tmp-${suffix}`);
1069
+ }
1070
+ /**
1071
+ * Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when
1072
+ * something is at `$dest`. `ln` onto a directory (or a link to one) would
1073
+ * create the link inside it and succeed, so that case is undone and reported
1074
+ * as taken too.
1075
+ */
1076
+ function linkTempIntoPlace() {
1077
+ return [
1078
+ `ln "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; ${present("dest")} && exit ${exists}; exit 1; }`,
1079
+ `if [ -d "$dest" ]; then rm -f "$dest/$(basename "$tmp")" "$tmp"; exit ${exists}; fi`,
1080
+ `rm -f "$tmp"`
1081
+ ];
1082
+ }
1083
+ /**
1084
+ * Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.
1085
+ *
1086
+ * A file source copies its content (a symlinked source is followed). A
1087
+ * directory source with `recursive` copies its contents into `dest`, keeping
1088
+ * symlinks inside the tree as links. With `overwrite: false` the destination
1089
+ * is claimed atomically: a directory with an exclusive `mkdir`, a file by
1090
+ * copying to a temp name and hard-linking it into place (`link(2)` fails when
1091
+ * the destination exists).
1092
+ *
1093
+ * Exits: `notFound` (no source), `isDirectory` (directory source without
1094
+ * `recursive`), `destIsDirectory` (a file onto an existing directory or a link
1095
+ * to one), `exists`.
1096
+ */
1097
+ function copyCommand(src, dest, options = {}) {
1098
+ const noClobber = options.overwrite === false;
1099
+ return [
1100
+ `src=${shellQuote$3(src)}`,
1101
+ `dest=${shellQuote$3(dest)}`,
1102
+ `[ -e "$src" ] || exit ${notFound}`,
1103
+ `mkdir -p "$(dirname "$dest")" || exit 1`,
1104
+ `if [ -d "$src" ]; then`,
1105
+ options.recursive ? ` :` : ` exit ${isDirectory}`,
1106
+ noClobber ? ` mkdir "$dest" 2>/dev/null || exit ${exists}` : ` mkdir -p "$dest" || exit 1`,
1107
+ ` cp -R -P "$src"/. "$dest"/ || exit 1`,
1108
+ ` exit 0`,
1109
+ `fi`,
1110
+ noClobber ? `${present("dest")} && exit ${exists}` : `if [ -d "$dest" ]; then exit ${destIsDirectory}; fi`,
1111
+ ...noClobber ? [
1112
+ `tmp=${shellQuote$3(siblingTempPath(dest))}`,
1113
+ `cp "$src" "$tmp" || { rm -f "$tmp"; exit 1; }`,
1114
+ ...linkTempIntoPlace()
1115
+ ] : [`cp "$src" "$dest" || exit 1`]
1116
+ ].join("\n");
1117
+ }
1118
+ /**
1119
+ * Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s
1120
+ * parent. A skipped `mv -n` is detected by the source surviving, whatever
1121
+ * its exit code. If a directory appears at `dest` between the check and the
1122
+ * move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported
1123
+ * as success; the no-clobber rename itself is atomic.
1124
+ *
1125
+ * Exits: `notFound` (no source), `exists`.
1126
+ */
1127
+ function moveNoClobberCommand(src, dest) {
1128
+ return [
1129
+ `src=${shellQuote$3(src)}`,
1130
+ `dest=${shellQuote$3(dest)}`,
1131
+ `${present("src")} || exit ${notFound}`,
1132
+ `${present("dest")} && exit ${exists}`,
1133
+ `mkdir -p "$(dirname "$dest")" || exit 1`,
1134
+ `mv -n "$src" "$dest" 2>/dev/null || { ${present("src")} && ${present("dest")} && exit ${exists}; exit 1; }`,
1135
+ `${present("src")} && exit ${exists}`,
1136
+ `exit 0`
1137
+ ].join("\n");
1138
+ }
1139
+ /**
1140
+ * Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.
1141
+ * The content travels as a file rather than a command argument, so it has no
1142
+ * size limit. Creates `dest` (and its parent) when missing.
1143
+ *
1144
+ * Exits: `isDirectory` (dest is a directory).
1145
+ */
1146
+ function appendFromCommand(tmp, dest) {
1147
+ return [
1148
+ `tmp=${shellQuote$3(tmp)}`,
1149
+ `dest=${shellQuote$3(dest)}`,
1150
+ `if [ -d "$dest" ]; then rm -f "$tmp"; exit ${isDirectory}; fi`,
1151
+ `mkdir -p "$(dirname "$dest")" && cat "$tmp" >> "$dest"`,
1152
+ `status=$?`,
1153
+ `rm -f "$tmp"`,
1154
+ `exit $status`
1155
+ ].join("\n");
1156
+ }
1157
+ /**
1158
+ * Publish the file already written to `tmp` at `dest` only if nothing is
1159
+ * there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;
1160
+ * `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.
1161
+ *
1162
+ * Exits: `exists`, `notDirectory` (dest's parent is not a directory).
1163
+ */
1164
+ function linkNoClobberCommand(tmp, dest) {
1165
+ return [
1166
+ `tmp=${shellQuote$3(tmp)}`,
1167
+ `dest=${shellQuote$3(dest)}`,
1168
+ `if [ ! -d "$(dirname "$dest")" ]; then rm -f "$tmp"; exit ${notDirectory}; fi`,
1169
+ `if ${present("dest")}; then rm -f "$tmp"; exit ${exists}; fi`,
1170
+ ...linkTempIntoPlace()
1171
+ ].join("\n");
1172
+ }
1173
+ /** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */
1174
+ const DEFAULT_MAX_DEPTH = 100;
1175
+ const toAbsolute = (path) => posix.resolve("/", path);
1176
+ /** The entry's own type: a symlink is a link even when it points at a directory. */
1177
+ const isSymlink = (info) => info.symlinkTarget !== void 0;
1178
+ function toWriteData(content) {
1179
+ if (typeof content === "string") return content;
1180
+ return content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
1181
+ }
1182
+ function matchesExtension(name, extension) {
1183
+ if (!extension) return true;
1184
+ const ext = posix.extname(name);
1185
+ return (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));
1186
+ }
1187
+ /**
1188
+ * A sandbox's disk, read and written through E2B's envd files API, with the
1189
+ * operations the files API can't do in one call (copy, append, no-clobber
1190
+ * writes and moves) run as one shell command inside the VM so they are atomic
1191
+ * and file bytes never round-trip through the host. Paths are absolute inside
1192
+ * the sandbox; this class does not confine paths or symlinks to any root.
1193
+ *
1194
+ * Providers supply how to reach the files API and the shell, and how to read
1195
+ * their SDK's errors.
1196
+ *
1197
+ * If a provider's `runCommand` re-runs a command whose first run already
1198
+ * finished (e.g. a retry after a dropped connection), the second run sees the
1199
+ * first's result: a no-clobber copy, write or move reports `FileExistsError`
1200
+ * or `FileNotFoundError`, and an append fails, even though the work was done.
1201
+ */
1202
+ var FilesApiFilesystem = class extends MastraFilesystem {
1203
+ async readFile(path, options) {
1204
+ const abs = toAbsolute(path);
1205
+ const bytes = await this.runFiles("readFile", async (files) => {
1206
+ try {
1207
+ return await files.read(abs, { format: "bytes" });
1208
+ } catch (error) {
1209
+ const kind = this.errorKind(error);
1210
+ if (kind === "not-found") throw new FileNotFoundError(path);
1211
+ if (kind === "invalid-argument") throw new IsDirectoryError(path);
1212
+ throw error;
1213
+ }
1214
+ });
1215
+ const buffer = Buffer$1.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1216
+ return options?.encoding ? buffer.toString(options.encoding) : buffer;
1217
+ }
1218
+ async writeFile(path, content, options) {
1219
+ const abs = toAbsolute(path);
1220
+ if (options?.overwrite !== false) {
1221
+ await this.runFiles("writeFile", async (files) => {
1222
+ if (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);
1223
+ await this.#write(files, abs, path, toWriteData(content));
1224
+ });
1225
+ return;
1226
+ }
1227
+ const tmp = siblingTempPath(abs);
1228
+ const upload = async (files) => {
1229
+ if (options.recursive === false) await this.#assertParentDirectory(files, abs, path);
1230
+ await this.#write(files, tmp, path, toWriteData(content));
1231
+ };
1232
+ await this.#publishTemp(tmp, "writeFile", upload, linkNoClobberCommand(tmp, abs), path, {
1233
+ exists: () => new FileExistsError(path),
1234
+ notDirectory: () => new NotDirectoryError(posix.dirname(path))
1235
+ });
1236
+ }
1237
+ async appendFile(path, content) {
1238
+ const abs = toAbsolute(path);
1239
+ const tmp = siblingTempPath(abs);
1240
+ const upload = (files) => this.#write(files, tmp, path, toWriteData(content));
1241
+ await this.#publishTemp(tmp, "appendFile", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new IsDirectoryError(path) });
1242
+ }
1243
+ async deleteFile(path, options) {
1244
+ const abs = toAbsolute(path);
1245
+ await this.runFiles("deleteFile", async (files) => {
1246
+ const info = await this.#info(files, abs);
1247
+ if (!info) {
1248
+ if (options?.force) return;
1249
+ throw new FileNotFoundError(path);
1250
+ }
1251
+ if (!isSymlink(info) && info.type === "dir") throw new IsDirectoryError(path);
1252
+ await files.remove(abs);
1253
+ });
1254
+ }
1255
+ /**
1256
+ * One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as
1257
+ * links; `overwrite: false` claims the destination atomically.
1258
+ */
1259
+ async copyFile(src, dest, options) {
1260
+ const command = copyCommand(toAbsolute(src), toAbsolute(dest), {
1261
+ recursive: options?.recursive,
1262
+ overwrite: options?.overwrite
1263
+ });
1264
+ await this.#command("copyFile", command, `${src} -> ${dest}`, {
1265
+ notFound: () => new FileNotFoundError(src),
1266
+ isDirectory: () => new IsDirectoryError(src),
1267
+ destIsDirectory: () => new IsDirectoryError(dest),
1268
+ exists: () => new FileExistsError(dest)
1269
+ });
1270
+ }
1271
+ async moveFile(src, dest, options) {
1272
+ const srcAbs = toAbsolute(src);
1273
+ const destAbs = toAbsolute(dest);
1274
+ if (options?.overwrite === false) {
1275
+ await this.#command("moveFile", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {
1276
+ notFound: () => new FileNotFoundError(src),
1277
+ exists: () => new FileExistsError(dest)
1278
+ });
1279
+ return;
1280
+ }
1281
+ await this.runFiles("moveFile", async (files) => {
1282
+ try {
1283
+ await files.rename(srcAbs, destAbs);
1284
+ } catch (error) {
1285
+ if (this.errorKind(error) === "not-found") throw new FileNotFoundError(src);
1286
+ throw error;
1287
+ }
1288
+ });
1289
+ }
1290
+ async mkdir(path, options) {
1291
+ const abs = toAbsolute(path);
1292
+ await this.runFiles("mkdir", async (files) => {
1293
+ if (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);
1294
+ try {
1295
+ await files.makeDir(abs);
1296
+ } catch (error) {
1297
+ if (this.errorKind(error) === "invalid-argument") throw new FileExistsError(path);
1298
+ throw error;
1299
+ }
1300
+ });
1301
+ }
1302
+ async rmdir(path, options) {
1303
+ const abs = toAbsolute(path);
1304
+ await this.runFiles("rmdir", async (files) => {
1305
+ const info = await this.#info(files, abs);
1306
+ if (!info) {
1307
+ if (options?.force) return;
1308
+ throw new DirectoryNotFoundError(path);
1309
+ }
1310
+ if (!options?.recursive) {
1311
+ if (isSymlink(info) || info.type !== "dir") throw new NotDirectoryError(path);
1312
+ if ((await files.list(abs)).length > 0) throw new DirectoryNotEmptyError(path);
1313
+ } else if (!isSymlink(info) && info.type !== "dir") throw new NotDirectoryError(path);
1314
+ await files.remove(abs);
1315
+ });
1316
+ }
1317
+ async readdir(path, options) {
1318
+ const abs = toAbsolute(path);
1319
+ const depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;
1320
+ const entries = await this.runFiles("readdir", async (files) => {
1321
+ try {
1322
+ return await files.list(abs, { depth });
1323
+ } catch (error) {
1324
+ const kind = this.errorKind(error);
1325
+ if (kind === "not-found") throw new DirectoryNotFoundError(path);
1326
+ if (kind === "invalid-argument") throw new NotDirectoryError(path);
1327
+ throw error;
1328
+ }
1329
+ });
1330
+ const result = [];
1331
+ for (const entry of entries) {
1332
+ if (!entry.type) continue;
1333
+ const name = posix.relative(abs, entry.path);
1334
+ const type = entry.type === "dir" ? "directory" : "file";
1335
+ if (type === "file" && !matchesExtension(name, options?.extension)) continue;
1336
+ const link = isSymlink(entry);
1337
+ result.push({
1338
+ name,
1339
+ type,
1340
+ ...type === "file" && !link && { size: entry.size },
1341
+ ...link && {
1342
+ isSymlink: true,
1343
+ symlinkTarget: entry.symlinkTarget
1344
+ }
1345
+ });
1346
+ }
1347
+ return result;
1348
+ }
1349
+ async exists(path) {
1350
+ const abs = toAbsolute(path);
1351
+ return this.runFiles("exists", async (files) => Boolean((await this.#info(files, abs))?.type));
1352
+ }
1353
+ async stat(path) {
1354
+ const abs = toAbsolute(path);
1355
+ return this.runFiles("stat", async (files) => {
1356
+ let info = await this.#info(files, abs);
1357
+ if (info?.type && isSymlink(info)) info = await this.#info(files, posix.resolve(posix.dirname(abs), info.symlinkTarget));
1358
+ if (!info?.type) throw new FileNotFoundError(path);
1359
+ const modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);
1360
+ return {
1361
+ name: posix.basename(abs),
1362
+ path: abs,
1363
+ type: info.type === "dir" ? "directory" : "file",
1364
+ size: info.type === "dir" ? 0 : info.size,
1365
+ createdAt: modifiedAt,
1366
+ modifiedAt
1367
+ };
1368
+ });
1369
+ }
1370
+ /** `getInfo` without following the entry itself, or `undefined` when nothing is there. */
1371
+ async #info(files, abs) {
1372
+ try {
1373
+ return await files.getInfo(abs);
1374
+ } catch (error) {
1375
+ if (this.errorKind(error) === "not-found") return void 0;
1376
+ throw error;
1377
+ }
1378
+ }
1379
+ async #assertParentDirectory(files, abs, path) {
1380
+ const parent = await this.#info(files, posix.dirname(abs));
1381
+ if (!parent?.type) throw new DirectoryNotFoundError(posix.dirname(path));
1382
+ if (parent.type !== "dir") throw new NotDirectoryError(posix.dirname(path));
1383
+ }
1384
+ async #write(files, abs, path, data) {
1385
+ try {
1386
+ await files.write(abs, data);
1387
+ } catch (error) {
1388
+ if (this.errorKind(error) === "invalid-argument") throw new IsDirectoryError(path);
1389
+ throw error;
1390
+ }
1391
+ }
1392
+ /**
1393
+ * Upload the temp file `tmp`, then run the command that consumes it. The
1394
+ * script deletes it on every path it controls; if the upload or the command
1395
+ * fails part-way, delete it here so it doesn't linger in the user's tree.
1396
+ */
1397
+ async #publishTemp(tmp, operation, upload, script, subject, errors) {
1398
+ try {
1399
+ await this.runFiles(operation, upload);
1400
+ await this.#command(operation, script, subject, errors);
1401
+ } catch (error) {
1402
+ await this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});
1403
+ throw error;
1404
+ }
1405
+ }
1406
+ /** Run a file command and turn its known exit codes into typed errors. */
1407
+ async #command(operation, script, subject, errors) {
1408
+ const result = await this.runCommand(operation, script);
1409
+ if (result.exitCode === 0) return;
1410
+ for (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {
1411
+ const toError = errors[outcome];
1412
+ if (code === result.exitCode && toError) throw toError();
1413
+ }
1414
+ throw new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);
1415
+ }
1416
+ };
1417
+ //#endregion
1418
+ //#region src/sandbox-filesystem.ts
1419
+ /**
1420
+ * envd's edge answers a paused or missing sandbox with a 404 or 502 that the
1421
+ * SDK reports as `FileNotFoundError` or `TimeoutError`. Same message pattern as
1422
+ * `E2BSandbox`'s dead-sandbox check.
1423
+ */
1424
+ const DEAD_SANDBOX = /\b(?:paused\s+)?sandbox(?:\s+\S+)?\s+(?:was\s+)?not found\b/i;
1425
+ const isDeadSandbox = (error) => error instanceof SandboxNotFoundError || error instanceof Error && DEAD_SANDBOX.test(error.message);
1426
+ /** 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. */
1427
+ const isBrokenSandboxError = (error) => isDeadSandbox(error) || error instanceof TimeoutError || !(error instanceof SandboxError$1);
1428
+ /**
1429
+ * The disk of a {@link PlatformSandbox}, read and written through E2B's files
1430
+ * API directly on the VM, authenticated by the same lease `executeCommand`
1431
+ * uses. Copies, appends and no-clobber writes and moves run as one shell
1432
+ * command in the VM, so they are atomic and file bytes don't round-trip
1433
+ * through the host. Paths are absolute inside the sandbox; this class does not
1434
+ * confine paths or symlinks to any root.
1435
+ */
1436
+ var PlatformSandboxFilesystem = class extends FilesApiFilesystem {
1437
+ id;
1438
+ name = "PlatformSandboxFilesystem";
1439
+ provider = "platform";
1440
+ status = "ready";
1441
+ #runner;
1442
+ constructor(runner) {
1443
+ super({ name: "PlatformSandboxFilesystem" });
1444
+ this.id = `${runner.id}-filesystem`;
1445
+ this.#runner = runner;
1446
+ }
1447
+ runFiles(operation, fn) {
1448
+ return this.#runner.withFiles(operation, fn);
1449
+ }
1450
+ runCommand(_operation, script) {
1451
+ return this.#runner.runCommand(script);
1452
+ }
1453
+ errorKind(error) {
1454
+ if (error instanceof NotFoundError && !isDeadSandbox(error)) return "not-found";
1455
+ if (error instanceof InvalidArgumentError) return "invalid-argument";
1456
+ }
1457
+ };
1458
+ //#endregion
958
1459
  //#region src/sandbox.ts
959
1460
  /**
960
1461
  * How long before a lease's stated `expiresAt` we should treat it as
@@ -1037,6 +1538,8 @@ var SandboxDestroyedError = class extends Error {
1037
1538
  this.attempts = diagnostics.attempts;
1038
1539
  }
1039
1540
  };
1541
+ /** Cap on one file command (a recursive copy can be large) when the sandbox sets no timeout. */
1542
+ const FILE_COMMAND_TIMEOUT_MS = 6e5;
1040
1543
  /**
1041
1544
  * Compose a shell command line from a `command` string and optional `args`.
1042
1545
  *
@@ -1052,9 +1555,9 @@ var SandboxDestroyedError = class extends Error {
1052
1555
  * sandbox.
1053
1556
  */
1054
1557
  function buildCommand(command, args) {
1055
- return args?.length ? `${command} ${args.map(shellQuote$2).join(" ")}` : command;
1558
+ return args?.length ? `${command} ${args.map(shellQuote).join(" ")}` : command;
1056
1559
  }
1057
- function shellQuote$2(arg) {
1560
+ function shellQuote(arg) {
1058
1561
  if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
1059
1562
  return `'${arg.replace(/'/g, `'\\''`)}'`;
1060
1563
  }
@@ -1155,6 +1658,10 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1155
1658
  _createdAt = null;
1156
1659
  _webSocketFactory;
1157
1660
  _e2bExecRunner;
1661
+ _e2bFilesFactory;
1662
+ /** Files handle for the current lease, rebuilt whenever the lease changes. */
1663
+ _e2bFiles = null;
1664
+ _filesystem;
1158
1665
  _privateNetFetch;
1159
1666
  /**
1160
1667
  * Registry that maps `sandboxId → instanceUrl` for the private-network
@@ -1244,6 +1751,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1244
1751
  this._instructionsOverride = options.instructions;
1245
1752
  this._webSocketFactory = options.webSocketFactory;
1246
1753
  this._e2bExecRunner = options.e2bExecRunner ?? execViaE2BLease;
1754
+ this._e2bFilesFactory = options.e2bFilesFactory ?? e2bFilesForLease;
1247
1755
  this._privateNetFetch = options.privateNetFetch;
1248
1756
  this._addressRegistry = options.addressRegistry;
1249
1757
  }
@@ -1750,6 +2258,15 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1750
2258
  * execution on the remote sandbox.
1751
2259
  */
1752
2260
  async executeCommand(command, args, options) {
2261
+ return this._executeCommand(command, args, options, { once: false });
2262
+ }
2263
+ /**
2264
+ * `executeCommand`, optionally sent at most once. With `once`, a transport
2265
+ * failure after the command may have started is thrown instead of retried
2266
+ * (on the lease or over the private network), so a script that changes files
2267
+ * never runs twice.
2268
+ */
2269
+ async _executeCommand(command, args, options, { once }) {
1753
2270
  await this.ensureRunning();
1754
2271
  if (!this._sandboxId) throw new SandboxNotReadyError(this.id);
1755
2272
  const started = Date.now();
@@ -1764,7 +2281,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1764
2281
  }
1765
2282
  } : options;
1766
2283
  await this._awaitTransportReady();
1767
- const instanceUrl = this._addressRegistry?.get(this._sandboxId);
2284
+ const instanceUrl = once ? void 0 : this._addressRegistry?.get(this._sandboxId);
1768
2285
  if (instanceUrl) {
1769
2286
  const privateNet = await this._tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, execCallOptions);
1770
2287
  if (privateNet) {
@@ -1780,7 +2297,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1780
2297
  };
1781
2298
  }
1782
2299
  }
1783
- const result = await this._runDirectExec(fullCommand, effectiveTimeout, execCallOptions);
2300
+ const result = await this._runDirectExec(fullCommand, effectiveTimeout, execCallOptions, once ? 1 : 2);
1784
2301
  const exitCode = result.exitCode ?? 124;
1785
2302
  return {
1786
2303
  success: exitCode === 0,
@@ -1825,12 +2342,12 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1825
2342
  const filtered = Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0));
1826
2343
  return Object.keys(filtered).length > 0 ? filtered : void 0;
1827
2344
  }
1828
- async _runDirectExec(fullCommand, effectiveTimeout, options) {
2345
+ async _runDirectExec(fullCommand, effectiveTimeout, options, attempts = 2) {
1829
2346
  const filteredEnv = this._execEnv(options);
1830
2347
  let lastResult;
1831
2348
  let lastLease;
1832
2349
  let attemptsMade = 0;
1833
- for (let attempt = 0; attempt < 2; attempt++) {
2350
+ for (let attempt = 0; attempt < attempts; attempt++) {
1834
2351
  if (attempt > 0 && lastLease && this._lease === lastLease) this._lease = null;
1835
2352
  let lease;
1836
2353
  try {
@@ -1962,6 +2479,91 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1962
2479
  if (this._leaseInFlight === inFlight) this._leaseInFlight = null;
1963
2480
  }
1964
2481
  }
2482
+ /**
2483
+ * The sandbox's disk through E2B's files API, called directly on the VM with
2484
+ * the same lease `executeCommand` uses. Paths are absolute inside the
2485
+ * sandbox and are not confined to any root. Absent on non-E2B sandboxes, so
2486
+ * callers fall back to `executeCommand`.
2487
+ */
2488
+ get filesystem() {
2489
+ if (this._client.sandboxProvider !== "e2b") return void 0;
2490
+ this._filesystem ??= new PlatformSandboxFilesystem({
2491
+ id: this.id,
2492
+ withFiles: (operation, fn) => this._withE2BFiles(operation, fn),
2493
+ runCommand: (script) => this._executeCommand("sh", ["-c", script], this._timeout === void 0 ? { timeout: FILE_COMMAND_TIMEOUT_MS } : {}, { once: true })
2494
+ });
2495
+ return this._filesystem;
2496
+ }
2497
+ /**
2498
+ * Run a filesystem operation against the VM through the files API. Each
2499
+ * files call that envd rejects for its token is retried once with a fresh
2500
+ * lease: the rejection proves that call did nothing, and the calls before it
2501
+ * already succeeded, so nothing is replayed. Any other failure is thrown
2502
+ * unchanged. A dead sandbox or a broken transport also drops the cached
2503
+ * lease, so the next operation re-mints it and learns about a destroyed
2504
+ * sandbox (410) the same way `executeCommand` does.
2505
+ */
2506
+ async _withE2BFiles(operation, fn) {
2507
+ await this.ensureRunning();
2508
+ if (!this._sandboxId) throw new SandboxNotReadyError(this.id);
2509
+ let current = await this._filesHandle(operation, 1);
2510
+ const call = async (invoke) => {
2511
+ try {
2512
+ return await invoke(current.files);
2513
+ } catch (error) {
2514
+ if (error instanceof AuthenticationError) {
2515
+ if (this._lease === current.lease) this._lease = null;
2516
+ current = await this._filesHandle(operation, 2);
2517
+ try {
2518
+ return await invoke(current.files);
2519
+ } catch (retryError) {
2520
+ if (isBrokenSandboxError(retryError) && this._lease === current.lease) this._lease = null;
2521
+ throw retryError;
2522
+ }
2523
+ }
2524
+ if (isBrokenSandboxError(error) && this._lease === current.lease) this._lease = null;
2525
+ throw error;
2526
+ }
2527
+ };
2528
+ return fn({
2529
+ read: (path, opts) => call((files) => files.read(path, opts)),
2530
+ write: (path, data) => call((files) => files.write(path, data)),
2531
+ list: (path, opts) => call((files) => files.list(path, opts)),
2532
+ makeDir: (path) => call((files) => files.makeDir(path)),
2533
+ rename: (oldPath, newPath) => call((files) => files.rename(oldPath, newPath)),
2534
+ remove: (path) => call((files) => files.remove(path)),
2535
+ getInfo: (path) => call((files) => files.getInfo(path))
2536
+ });
2537
+ }
2538
+ /** The files handle for the current lease, rebuilt only when the lease changes. */
2539
+ async _filesHandle(operation, attempts) {
2540
+ const lease = await this._fileLease(operation, attempts);
2541
+ if (lease.provider !== "e2b") throw new Error(`PlatformSandbox.filesystem requires an E2B-backed sandbox; this sandbox's provider is "${lease.provider}"`);
2542
+ if (this._e2bFiles?.lease !== lease) this._e2bFiles = {
2543
+ lease,
2544
+ files: this._e2bFilesFactory(lease)
2545
+ };
2546
+ return this._e2bFiles;
2547
+ }
2548
+ /** `_ensureLease`, with a 410 handled the same way `_runDirectExec` handles it. */
2549
+ async _fileLease(operation, attempts) {
2550
+ try {
2551
+ return await this._ensureLease();
2552
+ } catch (error) {
2553
+ if (error instanceof PlatformApiError && error.status === 410) {
2554
+ this._lease = null;
2555
+ const priorSandboxId = this._sandboxId;
2556
+ this._sandboxId = void 0;
2557
+ this.status = "stopped";
2558
+ throw new SandboxDestroyedError(`Sandbox ${priorSandboxId ?? "(unknown)"} was destroyed; /exec-lease returned 410`, {
2559
+ ...priorSandboxId && { sandboxId: priorSandboxId },
2560
+ command: `filesystem.${operation}`,
2561
+ attempts
2562
+ });
2563
+ }
2564
+ throw error;
2565
+ }
2566
+ }
1965
2567
  async getInfo() {
1966
2568
  if (!this._sandboxId || this.status !== "running") return {
1967
2569
  id: this.id,
@@ -2014,82 +2616,6 @@ function templateHash(definition) {
2014
2616
  return createHash("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
2015
2617
  }
2016
2618
  //#endregion
2017
- //#region ../../packages/_internals/workspace/dist/index.js
2018
- /** Directory, relative to the template's build cwd, that holds every marker below. */
2019
- const SETUP_MARKER_DIR = ".mastra-sandbox";
2020
- /**
2021
- * Setup completion marker shared by repo templates and their consumers.
2022
- *
2023
- * A repo template writes this file beside the checkout as its last build
2024
- * step, so it exists only in images where every setup command succeeded. Its
2025
- * content is a digest of the setup commands the image ran, letting a sandbox
2026
- * booted from the image tell whether the setup it is about to run already
2027
- * happened. Relative to the template's build cwd, which is also the runtime
2028
- * working directory the repo was cloned into.
2029
- *
2030
- * This is the single-repository marker. Templates built from a repository
2031
- * list write one marker per repository instead ({@link repoSetupMarkerPath}).
2032
- */
2033
- const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
2034
- /**
2035
- * Marker for the workspace-level setup of a multi-repository template: the
2036
- * commands that ran at the working directory after every repository was set
2037
- * up. Written even when there were no such commands, so a consumer can tell
2038
- * "ran nothing" from "never built".
2039
- */
2040
- const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
2041
- /**
2042
- * One line per repository directory whose setup failed while the build was
2043
- * told to continue. Absent when nothing failed. Consumers check this before
2044
- * trusting a per-repository marker: that marker means every step ran, not
2045
- * that every step succeeded.
2046
- */
2047
- const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
2048
- /** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
2049
- function repoSetupMarkerPath(repoDir) {
2050
- return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
2051
- }
2052
- /** Blank entries never become build steps, so they never count toward the digest either. */
2053
- function normalizeSetupCommands(setupCommand) {
2054
- return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
2055
- }
2056
- /** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */
2057
- function setupMarkerContent(setupCommand) {
2058
- return `sha256:${createHash("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
2059
- }
2060
- /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
2061
- function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
2062
- return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
2063
- }
2064
- /** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
2065
- const REPO_DIR_PATTERN = /^[\w.-]+$/;
2066
- /**
2067
- * Shell step that runs one setup command inside a repository clone.
2068
- *
2069
- * Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
2070
- * the `cd` sits inside the parenthesized group so that the failure branch
2071
- * runs back at the build cwd and the failure list lands at the workspace
2072
- * level, never inside the repository.
2073
- */
2074
- function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
2075
- if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
2076
- if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
2077
- 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}"; }`;
2078
- }
2079
- function shellQuote$1(value) {
2080
- return `'${value.replace(/'/g, `'\\''`)}'`;
2081
- }
2082
- function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
2083
- return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
2084
- }
2085
- /** Per-invocation auth header; `-c` config never reaches `.git/config`. */
2086
- function gitAuthFlag$1(tokenEnv) {
2087
- return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
2088
- }
2089
- function shellQuote(value) {
2090
- return `'${value.replace(/'/g, `'\\''`)}'`;
2091
- }
2092
- //#endregion
2093
2619
  //#region src/repo-template.ts
2094
2620
  const execFileAsync = promisify(execFile);
2095
2621
  const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
@@ -2368,7 +2894,7 @@ async function resolveDefaultBranchHead(cloneUrl, token, execute = execFileAsync
2368
2894
  "HEAD"
2369
2895
  ], {
2370
2896
  timeout: 1e4,
2371
- maxBuffer: 1024 * 1024,
2897
+ maxBuffer: 1048576,
2372
2898
  env
2373
2899
  });
2374
2900
  const sha = stdout.trim().split(/\s+/, 1)[0];
@@ -2670,6 +3196,6 @@ var InProcessSandboxAddressRegistry = class {
2670
3196
  }
2671
3197
  };
2672
3198
  //#endregion
2673
- export { InProcessSandboxAddressRegistry, PlatformApiError, PlatformClient, PlatformFactorySandbox, PlatformFilesystem, PlatformSandbox, PrivateNetExecHttpError, SandboxDestroyedError, SandboxExecTransportError, Template, createRepoTemplate, execViaPrivateNetwork, platformFilesystemProvider, platformSandboxProvider };
3199
+ export { InProcessSandboxAddressRegistry, PlatformApiError, PlatformClient, PlatformFactorySandbox, PlatformFilesystem, PlatformSandbox, PlatformSandboxFilesystem, PrivateNetExecHttpError, SandboxDestroyedError, SandboxExecTransportError, Template, createRepoTemplate, execViaPrivateNetwork, platformFilesystemProvider, platformSandboxProvider };
2674
3200
 
2675
3201
  //# sourceMappingURL=index.js.map