@mastra/platform-workspace 1.8.2-alpha.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_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 +7 -8
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 =
|
|
405
|
-
const MAX_STRING_LENGTH =
|
|
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
|
|
1558
|
+
return args?.length ? `${command} ${args.map(shellQuote).join(" ")}` : command;
|
|
1056
1559
|
}
|
|
1057
|
-
function shellQuote
|
|
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 <
|
|
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:
|
|
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
|