@mastra/e2b 0.7.0 → 0.8.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,94 +1,125 @@
1
- 'use strict';
2
-
3
- var workspace = require('@mastra/core/workspace');
4
- var e2b = require('e2b');
5
- var crypto = require('crypto');
6
- var tools = require('@mastra/core/tools');
7
- var esbuild = require('esbuild');
8
-
9
- // src/sandbox/index.ts
10
- var MOUNTABLE_TEMPLATE_VERSION = "v1";
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _mastra_core_workspace = require("@mastra/core/workspace");
3
+ let e2b = require("e2b");
4
+ let crypto = require("crypto");
5
+ let _mastra_core_tools = require("@mastra/core/tools");
6
+ let esbuild = require("esbuild");
7
+ /**
8
+ * Create a base template with FUSE mounting dependencies pre-installed.
9
+ *
10
+ * This template includes s3fs and fuse packages required for mounting
11
+ * cloud filesystems (S3, GCS, R2) into the sandbox.
12
+ *
13
+ * The returned `id` is deterministic, allowing E2BSandbox to check if
14
+ * the template already exists before building it.
15
+ *
16
+ * @example Basic usage
17
+ * ```typescript
18
+ * const { template, id } = createMountableTemplate();
19
+ * // First time: builds and caches the template
20
+ * // Subsequent times: reuses existing template
21
+ * const sandbox = new E2BSandbox({ template });
22
+ * ```
23
+ *
24
+ * @example With customization
25
+ * ```typescript
26
+ * const { template } = createMountableTemplate();
27
+ * const customTemplate = template
28
+ * .aptInstall(['nodejs', 'npm'])
29
+ * .runCmd('npm install -g typescript');
30
+ *
31
+ * // Note: customized templates get a unique ID, not the cached one
32
+ * const sandbox = new E2BSandbox({ template: customTemplate });
33
+ * ```
34
+ *
35
+ * @returns Object with template builder and deterministic ID
36
+ */
11
37
  function createDefaultMountableTemplate() {
12
- const aptPackages = ["s3fs", "fuse"];
13
- const config = { version: MOUNTABLE_TEMPLATE_VERSION, aptPackages };
14
- const hash = crypto.createHash("sha256").update(JSON.stringify(config, Object.keys(config).sort())).digest("hex").slice(0, 16);
15
- const template = e2b.Template().fromTemplate("base").aptInstall(aptPackages);
16
- return {
17
- template,
18
- id: `mastra-${hash}`,
19
- aptPackages
20
- };
38
+ const aptPackages = ["s3fs", "fuse"];
39
+ const config = {
40
+ version: "v1",
41
+ aptPackages
42
+ };
43
+ const hash = (0, crypto.createHash)("sha256").update(JSON.stringify(config, Object.keys(config).sort())).digest("hex").slice(0, 16);
44
+ return {
45
+ template: (0, e2b.Template)().fromTemplate("base").aptInstall(aptPackages),
46
+ id: `mastra-${hash}`,
47
+ aptPackages
48
+ };
21
49
  }
22
-
23
- // src/sandbox/mounts/types.ts
24
- var LOG_PREFIX = "[@mastra/e2b]";
25
- var SAFE_BUCKET_NAME = /^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$/;
50
+ //#endregion
51
+ //#region src/sandbox/mounts/types.ts
52
+ const LOG_PREFIX = "[@mastra/e2b]";
53
+ /**
54
+ * Validate a bucket name before interpolating into shell commands.
55
+ * Covers S3, GCS, and S3-compatible (R2, MinIO) naming rules.
56
+ */
57
+ const SAFE_BUCKET_NAME = /^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$/;
26
58
  function validateBucketName(bucket) {
27
- if (!SAFE_BUCKET_NAME.test(bucket)) {
28
- throw new Error(
29
- `Invalid bucket name: "${bucket}". Bucket names must be 3-63 characters, lowercase alphanumeric, hyphens, or dots.`
30
- );
31
- }
59
+ if (!SAFE_BUCKET_NAME.test(bucket)) throw new Error(`Invalid bucket name: "${bucket}". Bucket names must be 3-63 characters, lowercase alphanumeric, hyphens, or dots.`);
32
60
  }
61
+ /**
62
+ * Validate an endpoint URL before interpolating into shell commands.
63
+ */
33
64
  function validateEndpoint(endpoint) {
34
- try {
35
- new URL(endpoint);
36
- } catch {
37
- throw new Error(`Invalid endpoint URL: "${endpoint}"`);
38
- }
65
+ try {
66
+ new URL(endpoint);
67
+ } catch {
68
+ throw new Error(`Invalid endpoint URL: "${endpoint}"`);
69
+ }
39
70
  }
40
- var SAFE_REGION = /^[a-z0-9-]{2,32}$/;
71
+ /**
72
+ * Validate an AWS region (or R2's "auto") before interpolating into shell commands.
73
+ * Accepts standard AWS region codes like "us-east-1", "ap-northeast-1", and "auto".
74
+ */
75
+ const SAFE_REGION = /^[a-z0-9-]{2,32}$/;
41
76
  function validateRegion(region) {
42
- if (typeof region !== "string" || !SAFE_REGION.test(region)) {
43
- throw new Error(
44
- `Invalid region: ${JSON.stringify(region)}. Region must be a string of lowercase alphanumeric or hyphens (e.g., "us-east-1", "ap-northeast-1", "auto").`
45
- );
46
- }
77
+ if (typeof region !== "string" || !SAFE_REGION.test(region)) throw new Error(`Invalid region: ${JSON.stringify(region)}. Region must be a string of lowercase alphanumeric or hyphens (e.g., "us-east-1", "ap-northeast-1", "auto").`);
47
78
  }
79
+ /**
80
+ * Validate and normalize a mount prefix before interpolating into shell commands.
81
+ * Returns the normalized prefix (no leading/trailing slashes).
82
+ *
83
+ * Shell safety is handled by shellQuote() at the call site, so this function
84
+ * only enforces path-level rules (no traversal, no empty result, no control chars).
85
+ */
48
86
  function validatePrefix(prefix) {
49
- let normalized = prefix;
50
- while (normalized.startsWith("/")) normalized = normalized.slice(1);
51
- while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
52
- if (!normalized) {
53
- throw new Error("Mount prefix cannot be empty after normalization.");
54
- }
55
- if (normalized.includes("//") || normalized.split("/").some((s) => s === "." || s === "..")) {
56
- throw new Error(`Invalid mount prefix: "${prefix}". Path traversal is not allowed.`);
57
- }
58
- if (/[\x00-\x1f\x7f]/.test(normalized)) {
59
- throw new Error(`Invalid mount prefix: "${prefix}". Control characters are not allowed.`);
60
- }
61
- return normalized;
87
+ let normalized = prefix;
88
+ while (normalized.startsWith("/")) normalized = normalized.slice(1);
89
+ while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
90
+ if (!normalized) throw new Error("Mount prefix cannot be empty after normalization.");
91
+ if (normalized.includes("//") || normalized.split("/").some((s) => s === "." || s === "..")) throw new Error(`Invalid mount prefix: "${prefix}". Path traversal is not allowed.`);
92
+ if (/[\x00-\x1f\x7f]/.test(normalized)) throw new Error(`Invalid mount prefix: "${prefix}". Control characters are not allowed.`);
93
+ return normalized;
62
94
  }
63
-
64
- // src/utils/shell-quote.ts
95
+ //#endregion
96
+ //#region src/utils/shell-quote.ts
97
+ /**
98
+ * Shell-quote a single argument for safe use in a command string.
99
+ *
100
+ * Arguments containing only safe characters are returned as-is.
101
+ * All others are wrapped in single quotes with embedded single quotes escaped.
102
+ */
65
103
  function shellQuote(arg) {
66
- if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
67
- return "'" + arg.replace(/'/g, "'\\''") + "'";
104
+ if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
105
+ return "'" + arg.replace(/'/g, "'\\''") + "'";
68
106
  }
69
-
70
- // src/sandbox/mounts/s3.ts
107
+ //#endregion
108
+ //#region src/sandbox/mounts/s3.ts
109
+ /**
110
+ * Mount an S3 bucket using s3fs-fuse.
111
+ */
71
112
  async function mountS3(mountPath, config, ctx) {
72
- const { sandbox, logger } = ctx;
73
- validateBucketName(config.bucket);
74
- validateRegion(config.region);
75
- if (config.endpoint) {
76
- validateEndpoint(config.endpoint);
77
- }
78
- const checkResult = await sandbox.commands.run('which s3fs || echo "not found"');
79
- if (checkResult.stdout.includes("not found")) {
80
- logger.warn(`${LOG_PREFIX} s3fs not found, attempting runtime installation...`);
81
- logger.info(
82
- `${LOG_PREFIX} Tip: For faster startup, use createMountableTemplate() to pre-install s3fs in your sandbox template`
83
- );
84
- await sandbox.commands.run("sudo apt-get update 2>&1", { timeoutMs: 6e4 });
85
- const installResult = await sandbox.commands.run(
86
- "sudo apt-get install -y s3fs fuse 2>&1 || sudo apt-get install -y s3fs-fuse fuse 2>&1",
87
- { timeoutMs: 12e4 }
88
- );
89
- if (installResult.exitCode !== 0) {
90
- throw new Error(
91
- `Failed to install s3fs. For S3 mounting, your template needs s3fs and fuse packages.
113
+ const { sandbox, logger } = ctx;
114
+ validateBucketName(config.bucket);
115
+ validateRegion(config.region);
116
+ if (config.endpoint) validateEndpoint(config.endpoint);
117
+ if ((await sandbox.commands.run("which s3fs || echo \"not found\"")).stdout.includes("not found")) {
118
+ logger.warn(`${LOG_PREFIX} s3fs not found, attempting runtime installation...`);
119
+ logger.info(`${LOG_PREFIX} Tip: For faster startup, use createMountableTemplate() to pre-install s3fs in your sandbox template`);
120
+ await sandbox.commands.run("sudo apt-get update 2>&1", { timeoutMs: 6e4 });
121
+ const installResult = await sandbox.commands.run("sudo apt-get install -y s3fs fuse 2>&1 || sudo apt-get install -y s3fs-fuse fuse 2>&1", { timeoutMs: 12e4 });
122
+ if (installResult.exitCode !== 0) throw new Error(`Failed to install s3fs. For S3 mounting, your template needs s3fs and fuse packages.
92
123
 
93
124
  Option 1: Use createMountableTemplate() helper:
94
125
  import { E2BSandbox, createMountableTemplate } from '@mastra/e2b';
@@ -97,314 +128,275 @@ Option 1: Use createMountableTemplate() helper:
97
128
  Option 2: Customize the base template:
98
129
  new E2BSandbox({ template: base => base.aptInstall(['your-packages']) })
99
130
 
100
- Error details: ${installResult.stderr || installResult.stdout}`
101
- );
102
- }
103
- }
104
- const idResult = await sandbox.commands.run("id -u && id -g");
105
- const [uid, gid] = idResult.stdout.trim().split("\n");
106
- const hasAccessKey = !!config.accessKeyId;
107
- const hasSecretKey = !!config.secretAccessKey;
108
- if (hasAccessKey !== hasSecretKey) {
109
- throw new Error("Both accessKeyId and secretAccessKey must be provided together.");
110
- }
111
- const hasCredentials = hasAccessKey && hasSecretKey;
112
- const mountHash = crypto.createHash("md5").update(mountPath).digest("hex").slice(0, 8);
113
- const credentialsPath = `/tmp/.passwd-s3fs-${mountHash}`;
114
- if (!hasCredentials && config.endpoint) {
115
- throw new Error(
116
- `S3-compatible storage requires credentials. Detected endpoint: ${config.endpoint}. The public_bucket option only works for AWS S3 public buckets, not R2, MinIO, etc.`
117
- );
118
- }
119
- if (hasCredentials) {
120
- const credentialsContent = `${config.accessKeyId}:${config.secretAccessKey}`;
121
- await sandbox.commands.run(`sudo rm -f ${credentialsPath}`);
122
- await sandbox.files.write(credentialsPath, credentialsContent);
123
- await sandbox.commands.run(`chmod 600 ${credentialsPath}`);
124
- }
125
- const mountOptions = [];
126
- if (hasCredentials) {
127
- mountOptions.push(`passwd_file=${credentialsPath}`);
128
- } else {
129
- mountOptions.push("public_bucket=1");
130
- logger.debug(`${LOG_PREFIX} No credentials provided, mounting as public bucket (read-only)`);
131
- }
132
- mountOptions.push("allow_other");
133
- if (uid && gid) {
134
- mountOptions.push(`uid=${uid}`, `gid=${gid}`);
135
- }
136
- if (config.endpoint) {
137
- const endpoint = config.endpoint.replace(/\/$/, "");
138
- mountOptions.push(`url=${endpoint}`, "use_path_request_style", "sigv4", "nomultipart");
139
- }
140
- mountOptions.push(`endpoint=${config.region}`);
141
- if (config.readOnly) {
142
- mountOptions.push("ro");
143
- logger.debug(`${LOG_PREFIX} Mounting as read-only`);
144
- }
145
- let bucketArg = config.bucket;
146
- if (config.prefix) {
147
- const normalizedPrefix = validatePrefix(config.prefix);
148
- bucketArg = `${config.bucket}:/${normalizedPrefix}`;
149
- }
150
- const mountCmd = `sudo s3fs ${shellQuote(bucketArg)} ${shellQuote(mountPath)} -o ${mountOptions.join(" -o ")}`;
151
- logger.debug(`${LOG_PREFIX} Mounting S3:`, hasCredentials ? mountCmd.replace(credentialsPath, "***") : mountCmd);
152
- try {
153
- const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
154
- logger.debug(`${LOG_PREFIX} s3fs result:`, {
155
- exitCode: result.exitCode,
156
- stdout: result.stdout,
157
- stderr: result.stderr
158
- });
159
- if (result.exitCode !== 0) {
160
- throw new Error(`Failed to mount S3 bucket: ${result.stderr || result.stdout}`);
161
- }
162
- } catch (error) {
163
- const errorObj = error;
164
- const stderr = errorObj.result?.stderr || "";
165
- const stdout = errorObj.result?.stdout || "";
166
- logger.error(`${LOG_PREFIX} s3fs error:`, { stderr, stdout, error: String(error) });
167
- throw new Error(`Failed to mount S3 bucket: ${stderr || stdout || error}`);
168
- }
169
- const verify = await sandbox.commands.run(`mountpoint -q ${shellQuote(mountPath)}`);
170
- if (verify.exitCode !== 0) {
171
- throw new Error(
172
- `s3fs returned exit 0 but ${mountPath} is not a mountpoint. The s3fs daemon likely failed during FUSE init (common causes: region mismatch, invalid credentials, or an S3-compatible endpoint that rejects the signature). Re-run inside the sandbox with '-f -o dbglevel=info' to see the underlying error.`
173
- );
174
- }
131
+ Error details: ${installResult.stderr || installResult.stdout}`);
132
+ }
133
+ const [uid, gid] = (await sandbox.commands.run("id -u && id -g")).stdout.trim().split("\n");
134
+ const hasAccessKey = !!config.accessKeyId;
135
+ const hasSecretKey = !!config.secretAccessKey;
136
+ if (hasAccessKey !== hasSecretKey) throw new Error("Both accessKeyId and secretAccessKey must be provided together.");
137
+ const hasCredentials = hasAccessKey && hasSecretKey;
138
+ const credentialsPath = `/tmp/.passwd-s3fs-${(0, crypto.createHash)("md5").update(mountPath).digest("hex").slice(0, 8)}`;
139
+ if (!hasCredentials && config.endpoint) throw new Error(`S3-compatible storage requires credentials. Detected endpoint: ${config.endpoint}. The public_bucket option only works for AWS S3 public buckets, not R2, MinIO, etc.`);
140
+ if (hasCredentials) {
141
+ const credentialsContent = `${config.accessKeyId}:${config.secretAccessKey}`;
142
+ await sandbox.commands.run(`sudo rm -f ${credentialsPath}`);
143
+ await sandbox.files.write(credentialsPath, credentialsContent);
144
+ await sandbox.commands.run(`chmod 600 ${credentialsPath}`);
145
+ }
146
+ const mountOptions = [];
147
+ if (hasCredentials) mountOptions.push(`passwd_file=${credentialsPath}`);
148
+ else {
149
+ mountOptions.push("public_bucket=1");
150
+ logger.debug(`${LOG_PREFIX} No credentials provided, mounting as public bucket (read-only)`);
151
+ }
152
+ mountOptions.push("allow_other");
153
+ if (uid && gid) mountOptions.push(`uid=${uid}`, `gid=${gid}`);
154
+ if (config.endpoint) {
155
+ const endpoint = config.endpoint.replace(/\/$/, "");
156
+ mountOptions.push(`url=${endpoint}`, "use_path_request_style", "sigv4", "nomultipart");
157
+ }
158
+ mountOptions.push(`endpoint=${config.region}`);
159
+ if (config.readOnly) {
160
+ mountOptions.push("ro");
161
+ logger.debug(`${LOG_PREFIX} Mounting as read-only`);
162
+ }
163
+ let bucketArg = config.bucket;
164
+ if (config.prefix) {
165
+ const normalizedPrefix = validatePrefix(config.prefix);
166
+ bucketArg = `${config.bucket}:/${normalizedPrefix}`;
167
+ }
168
+ const mountCmd = `sudo s3fs ${shellQuote(bucketArg)} ${shellQuote(mountPath)} -o ${mountOptions.join(" -o ")}`;
169
+ logger.debug(`${LOG_PREFIX} Mounting S3:`, hasCredentials ? mountCmd.replace(credentialsPath, "***") : mountCmd);
170
+ try {
171
+ const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
172
+ logger.debug(`${LOG_PREFIX} s3fs result:`, {
173
+ exitCode: result.exitCode,
174
+ stdout: result.stdout,
175
+ stderr: result.stderr
176
+ });
177
+ if (result.exitCode !== 0) throw new Error(`Failed to mount S3 bucket: ${result.stderr || result.stdout}`);
178
+ } catch (error) {
179
+ const errorObj = error;
180
+ const stderr = errorObj.result?.stderr || "";
181
+ const stdout = errorObj.result?.stdout || "";
182
+ logger.error(`${LOG_PREFIX} s3fs error:`, {
183
+ stderr,
184
+ stdout,
185
+ error: String(error)
186
+ });
187
+ throw new Error(`Failed to mount S3 bucket: ${stderr || stdout || error}`);
188
+ }
189
+ if ((await sandbox.commands.run(`mountpoint -q ${shellQuote(mountPath)}`)).exitCode !== 0) throw new Error(`s3fs returned exit 0 but ${mountPath} is not a mountpoint. The s3fs daemon likely failed during FUSE init (common causes: region mismatch, invalid credentials, or an S3-compatible endpoint that rejects the signature). Re-run inside the sandbox with '-f -o dbglevel=info' to see the underlying error.`);
175
190
  }
191
+ //#endregion
192
+ //#region src/sandbox/mounts/gcs.ts
193
+ /**
194
+ * Mount a GCS bucket using gcsfuse.
195
+ *
196
+ * When `config.prefix` is set, gcsfuse uses `--only-dir` to mount only that
197
+ * subdirectory, aligning sandbox paths with the prefixed GCS keys (mirrors the
198
+ * S3 `bucket:/prefix` and Azure `--subdirectory` mounts).
199
+ */
176
200
  async function mountGCS(mountPath, config, ctx) {
177
- const { sandbox, logger } = ctx;
178
- validateBucketName(config.bucket);
179
- const checkResult = await sandbox.commands.run('which gcsfuse || echo "not found"');
180
- if (checkResult.stdout.includes("not found")) {
181
- const codenameResult = await sandbox.commands.run("lsb_release -cs 2>/dev/null || echo jammy");
182
- const codename = codenameResult.stdout.trim() || "jammy";
183
- await sandbox.commands.run(
184
- `curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg && echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] https://packages.cloud.google.com/apt gcsfuse-${codename} main" | sudo tee /etc/apt/sources.list.d/gcsfuse.list && sudo apt-get update && sudo apt-get install -y gcsfuse`,
185
- { timeoutMs: 12e4 }
186
- );
187
- }
188
- const idResult = await sandbox.commands.run("id -u && id -g");
189
- const [uid, gid] = idResult.stdout.trim().split("\n");
190
- const uidGidFlags = uid && gid ? `--uid=${uid} --gid=${gid}` : "";
191
- const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote(validatePrefix(config.prefix))}` : "";
192
- const hasCredentials = !!config.serviceAccountKey;
193
- let mountCmd;
194
- if (hasCredentials) {
195
- const mountHash = crypto.createHash("md5").update(mountPath).digest("hex").slice(0, 8);
196
- const keyPath = `/tmp/gcs-key-${mountHash}.json`;
197
- await sandbox.commands.run(`sudo rm -f ${keyPath}`);
198
- await sandbox.files.write(keyPath, config.serviceAccountKey);
199
- await sandbox.commands.run(`sudo chown root:root ${keyPath} && sudo chmod 600 ${keyPath}`);
200
- mountCmd = `sudo gcsfuse --key-file=${keyPath} -o allow_other ${uidGidFlags}${onlyDirFlag} ${config.bucket} ${mountPath}`;
201
- } else {
202
- logger.debug(`${LOG_PREFIX} No credentials provided, mounting GCS as public bucket (read-only)`);
203
- mountCmd = `sudo gcsfuse --anonymous-access -o allow_other ${uidGidFlags}${onlyDirFlag} ${config.bucket} ${mountPath}`;
204
- }
205
- logger.debug(`${LOG_PREFIX} Mounting GCS:`, mountCmd);
206
- try {
207
- const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
208
- logger.debug(`${LOG_PREFIX} gcsfuse result:`, {
209
- exitCode: result.exitCode,
210
- stdout: result.stdout,
211
- stderr: result.stderr
212
- });
213
- if (result.exitCode !== 0) {
214
- throw new Error(`Failed to mount GCS bucket: ${result.stderr || result.stdout}`);
215
- }
216
- } catch (error) {
217
- const errorObj = error;
218
- const stderr = errorObj.result?.stderr || "";
219
- const stdout = errorObj.result?.stdout || "";
220
- logger.error(`${LOG_PREFIX} gcsfuse error:`, { stderr, stdout, error: String(error) });
221
- throw new Error(`Failed to mount GCS bucket: ${stderr || stdout || error}`);
222
- }
201
+ const { sandbox, logger } = ctx;
202
+ validateBucketName(config.bucket);
203
+ if ((await sandbox.commands.run("which gcsfuse || echo \"not found\"")).stdout.includes("not found")) {
204
+ const codename = (await sandbox.commands.run("lsb_release -cs 2>/dev/null || echo jammy")).stdout.trim() || "jammy";
205
+ await sandbox.commands.run(`curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg && echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] https://packages.cloud.google.com/apt gcsfuse-${codename} main" | sudo tee /etc/apt/sources.list.d/gcsfuse.list && sudo apt-get update && sudo apt-get install -y gcsfuse`, { timeoutMs: 12e4 });
206
+ }
207
+ const [uid, gid] = (await sandbox.commands.run("id -u && id -g")).stdout.trim().split("\n");
208
+ const uidGidFlags = uid && gid ? `--uid=${uid} --gid=${gid}` : "";
209
+ const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote(validatePrefix(config.prefix))}` : "";
210
+ const hasCredentials = !!config.serviceAccountKey;
211
+ let mountCmd;
212
+ if (hasCredentials) {
213
+ const keyPath = `/tmp/gcs-key-${(0, crypto.createHash)("md5").update(mountPath).digest("hex").slice(0, 8)}.json`;
214
+ await sandbox.commands.run(`sudo rm -f ${keyPath}`);
215
+ await sandbox.files.write(keyPath, config.serviceAccountKey);
216
+ await sandbox.commands.run(`sudo chown root:root ${keyPath} && sudo chmod 600 ${keyPath}`);
217
+ mountCmd = `sudo gcsfuse --key-file=${keyPath} -o allow_other ${uidGidFlags}${onlyDirFlag} ${config.bucket} ${mountPath}`;
218
+ } else {
219
+ logger.debug(`${LOG_PREFIX} No credentials provided, mounting GCS as public bucket (read-only)`);
220
+ mountCmd = `sudo gcsfuse --anonymous-access -o allow_other ${uidGidFlags}${onlyDirFlag} ${config.bucket} ${mountPath}`;
221
+ }
222
+ logger.debug(`${LOG_PREFIX} Mounting GCS:`, mountCmd);
223
+ try {
224
+ const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
225
+ logger.debug(`${LOG_PREFIX} gcsfuse result:`, {
226
+ exitCode: result.exitCode,
227
+ stdout: result.stdout,
228
+ stderr: result.stderr
229
+ });
230
+ if (result.exitCode !== 0) throw new Error(`Failed to mount GCS bucket: ${result.stderr || result.stdout}`);
231
+ } catch (error) {
232
+ const errorObj = error;
233
+ const stderr = errorObj.result?.stderr || "";
234
+ const stdout = errorObj.result?.stdout || "";
235
+ logger.error(`${LOG_PREFIX} gcsfuse error:`, {
236
+ stderr,
237
+ stdout,
238
+ error: String(error)
239
+ });
240
+ throw new Error(`Failed to mount GCS bucket: ${stderr || stdout || error}`);
241
+ }
223
242
  }
224
- var SAFE_CONTAINER_NAME = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
225
- var BLOBFUSE2_GITHUB_DEB = "https://github.com/Azure/azure-storage-fuse/releases/download/blobfuse2-2.5.1/blobfuse2-2.5.1-Ubuntu-22.04.x86_64.deb";
243
+ //#endregion
244
+ //#region src/sandbox/mounts/azure.ts
245
+ const SAFE_CONTAINER_NAME = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
246
+ const BLOBFUSE2_GITHUB_DEB = "https://github.com/Azure/azure-storage-fuse/releases/download/blobfuse2-2.5.1/blobfuse2-2.5.1-Ubuntu-22.04.x86_64.deb";
226
247
  function validateContainerName(name) {
227
- if (!SAFE_CONTAINER_NAME.test(name) || name.includes("--")) {
228
- throw new Error(
229
- `Invalid Azure container name: "${name}". Container names must be 3-63 lowercase alphanumeric characters or hyphens, with no consecutive hyphens.`
230
- );
231
- }
248
+ if (!SAFE_CONTAINER_NAME.test(name) || name.includes("--")) throw new Error(`Invalid Azure container name: "${name}". Container names must be 3-63 lowercase alphanumeric characters or hyphens, with no consecutive hyphens.`);
232
249
  }
233
250
  function parseConnectionString(cs) {
234
- const out = {};
235
- for (const part of cs.split(";")) {
236
- const eq = part.indexOf("=");
237
- if (eq === -1) continue;
238
- const key = part.slice(0, eq).trim();
239
- const value = part.slice(eq + 1).trim();
240
- if (!value) continue;
241
- if (key === "AccountName") out.accountName = value;
242
- else if (key === "AccountKey") out.accountKey = value;
243
- else if (key === "SharedAccessSignature") out.sasToken = value;
244
- else if (key === "BlobEndpoint") out.endpoint = value;
245
- else if (key === "EndpointSuffix") out.endpointSuffix = value;
246
- else if (key === "DefaultEndpointsProtocol") out.protocol = value;
247
- }
248
- if (!out.endpoint && out.accountName) {
249
- out.endpoint = `${out.protocol || "https"}://${out.accountName}.blob.${out.endpointSuffix || "core.windows.net"}`;
250
- }
251
- return out;
251
+ const out = {};
252
+ for (const part of cs.split(";")) {
253
+ const eq = part.indexOf("=");
254
+ if (eq === -1) continue;
255
+ const key = part.slice(0, eq).trim();
256
+ const value = part.slice(eq + 1).trim();
257
+ if (!value) continue;
258
+ if (key === "AccountName") out.accountName = value;
259
+ else if (key === "AccountKey") out.accountKey = value;
260
+ else if (key === "SharedAccessSignature") out.sasToken = value;
261
+ else if (key === "BlobEndpoint") out.endpoint = value;
262
+ else if (key === "EndpointSuffix") out.endpointSuffix = value;
263
+ else if (key === "DefaultEndpointsProtocol") out.protocol = value;
264
+ }
265
+ if (!out.endpoint && out.accountName) out.endpoint = `${out.protocol || "https"}://${out.accountName}.blob.${out.endpointSuffix || "core.windows.net"}`;
266
+ return out;
252
267
  }
253
268
  function yamlString(value) {
254
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
269
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
255
270
  }
256
271
  function parseOsRelease(output) {
257
- const values = {};
258
- for (const line of output.split("\n")) {
259
- const eq = line.indexOf("=");
260
- if (eq === -1) continue;
261
- const key = line.slice(0, eq);
262
- const value = line.slice(eq + 1).trim().replace(/^"|"$/g, "");
263
- values[key] = value;
264
- }
265
- return values;
272
+ const values = {};
273
+ for (const line of output.split("\n")) {
274
+ const eq = line.indexOf("=");
275
+ if (eq === -1) continue;
276
+ const key = line.slice(0, eq);
277
+ values[key] = line.slice(eq + 1).trim().replace(/^"|"$/g, "");
278
+ }
279
+ return values;
266
280
  }
267
281
  function resolveMicrosoftAptRepos(osReleaseOutput) {
268
- const osRelease = parseOsRelease(osReleaseOutput);
269
- const distroId = osRelease.ID || "ubuntu";
270
- const codename = osRelease.VERSION_CODENAME || (distroId === "debian" ? "bookworm" : "jammy");
271
- const versionId = osRelease.VERSION_ID || (distroId === "debian" ? "12" : "22.04");
272
- if (!/^[a-z0-9][a-z0-9-]*$/.test(codename)) {
273
- throw new Error(`Invalid distro codename for blobfuse2 repo: "${codename}"`);
274
- }
275
- if (!/^\d+(?:\.\d+)?$/.test(versionId)) {
276
- throw new Error(`Invalid distro version for blobfuse2 repo: "${versionId}"`);
277
- }
278
- if (distroId === "debian") {
279
- const repos = [
280
- { repoUrl: `https://packages.microsoft.com/debian/${versionId.split(".")[0]}/prod`, suite: codename }
281
- ];
282
- if (versionId.split(".")[0] !== "12" || codename !== "bookworm") {
283
- repos.push({ repoUrl: "https://packages.microsoft.com/debian/12/prod", suite: "bookworm" });
284
- }
285
- return repos;
286
- }
287
- if (distroId === "ubuntu") {
288
- const repos = [{ repoUrl: `https://packages.microsoft.com/ubuntu/${versionId}/prod`, suite: codename }];
289
- if (versionId !== "24.04" || codename !== "noble") {
290
- repos.push({ repoUrl: "https://packages.microsoft.com/ubuntu/24.04/prod", suite: "noble" });
291
- }
292
- if (versionId !== "22.04" || codename !== "jammy") {
293
- repos.push({ repoUrl: "https://packages.microsoft.com/ubuntu/22.04/prod", suite: "jammy" });
294
- }
295
- return repos;
296
- }
297
- throw new Error(`Unsupported distro for blobfuse2 runtime installation: "${distroId}"`);
282
+ const osRelease = parseOsRelease(osReleaseOutput);
283
+ const distroId = osRelease.ID || "ubuntu";
284
+ const codename = osRelease.VERSION_CODENAME || (distroId === "debian" ? "bookworm" : "jammy");
285
+ const versionId = osRelease.VERSION_ID || (distroId === "debian" ? "12" : "22.04");
286
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(codename)) throw new Error(`Invalid distro codename for blobfuse2 repo: "${codename}"`);
287
+ if (!/^\d+(?:\.\d+)?$/.test(versionId)) throw new Error(`Invalid distro version for blobfuse2 repo: "${versionId}"`);
288
+ if (distroId === "debian") {
289
+ const repos = [{
290
+ repoUrl: `https://packages.microsoft.com/debian/${versionId.split(".")[0]}/prod`,
291
+ suite: codename
292
+ }];
293
+ if (versionId.split(".")[0] !== "12" || codename !== "bookworm") repos.push({
294
+ repoUrl: "https://packages.microsoft.com/debian/12/prod",
295
+ suite: "bookworm"
296
+ });
297
+ return repos;
298
+ }
299
+ if (distroId === "ubuntu") {
300
+ const repos = [{
301
+ repoUrl: `https://packages.microsoft.com/ubuntu/${versionId}/prod`,
302
+ suite: codename
303
+ }];
304
+ if (versionId !== "24.04" || codename !== "noble") repos.push({
305
+ repoUrl: "https://packages.microsoft.com/ubuntu/24.04/prod",
306
+ suite: "noble"
307
+ });
308
+ if (versionId !== "22.04" || codename !== "jammy") repos.push({
309
+ repoUrl: "https://packages.microsoft.com/ubuntu/22.04/prod",
310
+ suite: "jammy"
311
+ });
312
+ return repos;
313
+ }
314
+ throw new Error(`Unsupported distro for blobfuse2 runtime installation: "${distroId}"`);
298
315
  }
299
316
  function resolveAuth(config) {
300
- let accountName = config.accountName;
301
- let accountKey = config.accountKey;
302
- let sasToken = config.sasToken;
303
- let endpoint = config.endpoint;
304
- if (config.connectionString) {
305
- const parsed = parseConnectionString(config.connectionString);
306
- accountName = accountName ?? parsed.accountName;
307
- accountKey = accountKey ?? parsed.accountKey;
308
- sasToken = sasToken ?? parsed.sasToken;
309
- endpoint = endpoint ?? parsed.endpoint;
310
- }
311
- let mode;
312
- if (config.useDefaultCredential) {
313
- mode = "msi";
314
- } else if (sasToken) {
315
- mode = "sas";
316
- } else if (accountKey) {
317
- mode = "key";
318
- } else {
319
- throw new Error(
320
- "Azure Blob mount requires credentials: provide connectionString, accountKey + accountName, sasToken + accountName, or useDefaultCredential."
321
- );
322
- }
323
- if (!accountName) {
324
- throw new Error("Azure Blob mount requires an accountName (either explicitly or via connectionString).");
325
- }
326
- if (endpoint) {
327
- validateEndpoint(endpoint);
328
- }
329
- return { mode, accountName, accountKey, sasToken, endpoint };
317
+ let accountName = config.accountName;
318
+ let accountKey = config.accountKey;
319
+ let sasToken = config.sasToken;
320
+ let endpoint = config.endpoint;
321
+ if (config.connectionString) {
322
+ const parsed = parseConnectionString(config.connectionString);
323
+ accountName = accountName ?? parsed.accountName;
324
+ accountKey = accountKey ?? parsed.accountKey;
325
+ sasToken = sasToken ?? parsed.sasToken;
326
+ endpoint = endpoint ?? parsed.endpoint;
327
+ }
328
+ let mode;
329
+ if (config.useDefaultCredential) mode = "msi";
330
+ else if (sasToken) mode = "sas";
331
+ else if (accountKey) mode = "key";
332
+ else throw new Error("Azure Blob mount requires credentials: provide connectionString, accountKey + accountName, sasToken + accountName, or useDefaultCredential.");
333
+ if (!accountName) throw new Error("Azure Blob mount requires an accountName (either explicitly or via connectionString).");
334
+ if (endpoint) validateEndpoint(endpoint);
335
+ return {
336
+ mode,
337
+ accountName,
338
+ accountKey,
339
+ sasToken,
340
+ endpoint
341
+ };
330
342
  }
331
343
  function buildBlobfuseConfig(container, auth, cachePath, readOnly) {
332
- const lines = [
333
- "allow-other: true",
334
- "foreground: false",
335
- `read-only: ${readOnly ? "true" : "false"}`,
336
- "logging:",
337
- " type: silent",
338
- "components:",
339
- " - libfuse",
340
- " - file_cache",
341
- " - attr_cache",
342
- " - azstorage",
343
- "libfuse:",
344
- " attribute-expiration-sec: 240",
345
- " entry-expiration-sec: 240",
346
- " negative-entry-expiration-sec: 120",
347
- "file_cache:",
348
- ` path: ${yamlString(cachePath)}`,
349
- " timeout-sec: 120",
350
- "attr_cache:",
351
- " timeout-sec: 7200",
352
- "azstorage:",
353
- ` mode: ${auth.mode}`,
354
- ` account-name: ${yamlString(auth.accountName)}`,
355
- ` container: ${yamlString(container)}`
356
- ];
357
- if (auth.mode === "key" && auth.accountKey) {
358
- lines.push(` account-key: ${yamlString(auth.accountKey)}`);
359
- } else if (auth.mode === "sas" && auth.sasToken) {
360
- lines.push(` sas: ${yamlString(auth.sasToken)}`);
361
- }
362
- if (auth.endpoint) {
363
- lines.push(` endpoint: ${yamlString(auth.endpoint.replace(/\/$/, ""))}`);
364
- }
365
- return lines.join("\n") + "\n";
344
+ const lines = [
345
+ "allow-other: true",
346
+ "foreground: false",
347
+ `read-only: ${readOnly ? "true" : "false"}`,
348
+ "logging:",
349
+ " type: silent",
350
+ "components:",
351
+ " - libfuse",
352
+ " - file_cache",
353
+ " - attr_cache",
354
+ " - azstorage",
355
+ "libfuse:",
356
+ " attribute-expiration-sec: 240",
357
+ " entry-expiration-sec: 240",
358
+ " negative-entry-expiration-sec: 120",
359
+ "file_cache:",
360
+ ` path: ${yamlString(cachePath)}`,
361
+ " timeout-sec: 120",
362
+ "attr_cache:",
363
+ " timeout-sec: 7200",
364
+ "azstorage:",
365
+ ` mode: ${auth.mode}`,
366
+ ` account-name: ${yamlString(auth.accountName)}`,
367
+ ` container: ${yamlString(container)}`
368
+ ];
369
+ if (auth.mode === "key" && auth.accountKey) lines.push(` account-key: ${yamlString(auth.accountKey)}`);
370
+ else if (auth.mode === "sas" && auth.sasToken) lines.push(` sas: ${yamlString(auth.sasToken)}`);
371
+ if (auth.endpoint) lines.push(` endpoint: ${yamlString(auth.endpoint.replace(/\/$/, ""))}`);
372
+ return lines.join("\n") + "\n";
366
373
  }
374
+ /**
375
+ * Mount an Azure Blob container using blobfuse2.
376
+ */
367
377
  async function mountAzure(mountPath, config, ctx) {
368
- const { sandbox, logger } = ctx;
369
- validateContainerName(config.container);
370
- const auth = resolveAuth(config);
371
- const prefix = config.prefix ? validatePrefix(config.prefix) : void 0;
372
- const checkResult = await sandbox.commands.run('which blobfuse2 || echo "not found"');
373
- if (checkResult.stdout.includes("not found")) {
374
- logger.warn(`${LOG_PREFIX} blobfuse2 not found, attempting runtime installation...`);
375
- logger.info(
376
- `${LOG_PREFIX} Tip: For faster startup, pre-install blobfuse2 in your sandbox template via createMountableTemplate()`
377
- );
378
- const osReleaseResult = await sandbox.commands.run("cat /etc/os-release 2>/dev/null || true");
379
- const repos = resolveMicrosoftAptRepos(osReleaseResult.stdout);
380
- const repoSetupResult = await sandbox.commands.run(
381
- "sudo mkdir -p /etc/apt/keyrings && curl --retry 3 --retry-all-errors --retry-delay 2 -fsSL https://packages.microsoft.com/keys/microsoft.asc -o /tmp/ms-key.asc && sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/microsoft.gpg /tmp/ms-key.asc",
382
- { timeoutMs: 6e4 }
383
- );
384
- let installResult;
385
- if (repoSetupResult.exitCode === 0) {
386
- for (const { repoUrl, suite } of repos) {
387
- installResult = await sandbox.commands.run(
388
- `echo "deb [signed-by=/etc/apt/keyrings/microsoft.gpg] ${repoUrl} ${suite} main" | sudo tee /etc/apt/sources.list.d/microsoft-prod.list && sudo apt-get update 2>&1 && sudo apt-get install -y blobfuse2 fuse3 2>&1`,
389
- { timeoutMs: 18e4 }
390
- );
391
- if (installResult.exitCode === 0) break;
392
- logger.warn(`${LOG_PREFIX} blobfuse2 install failed for ${repoUrl} ${suite}, trying fallback if available`);
393
- }
394
- } else {
395
- logger.warn(`${LOG_PREFIX} Failed to set up Microsoft apt repository, trying GitHub release fallback`);
396
- }
397
- let verifyResult = await sandbox.commands.run("which blobfuse2 && blobfuse2 --version", { timeoutMs: 3e4 });
398
- if (verifyResult.exitCode !== 0) {
399
- installResult = await sandbox.commands.run(
400
- `sudo apt-get update -qq 2>&1 || true && sudo apt-get install -y fuse3 ca-certificates curl 2>&1 && curl -L --retry 3 --retry-all-errors --retry-delay 2 -fSLo /tmp/blobfuse2.deb ${BLOBFUSE2_GITHUB_DEB} && sudo dpkg -i /tmp/blobfuse2.deb 2>&1 && sudo bash -c 'lib=$(find /usr/lib -name "libfuse3.so.3.*" | head -1); [ -z "$lib" ] || ln -sf "$lib" /usr/lib/x86_64-linux-gnu/libfuse3.so.3'`,
401
- { timeoutMs: 18e4 }
402
- );
403
- verifyResult = await sandbox.commands.run("which blobfuse2 && blobfuse2 --version", { timeoutMs: 3e4 });
404
- }
405
- if (!installResult || verifyResult.exitCode !== 0) {
406
- throw new Error(
407
- `Failed to install blobfuse2. For Azure Blob mounting, your template needs blobfuse2 and fuse3.
378
+ const { sandbox, logger } = ctx;
379
+ validateContainerName(config.container);
380
+ const auth = resolveAuth(config);
381
+ const prefix = config.prefix ? validatePrefix(config.prefix) : void 0;
382
+ if ((await sandbox.commands.run("which blobfuse2 || echo \"not found\"")).stdout.includes("not found")) {
383
+ logger.warn(`${LOG_PREFIX} blobfuse2 not found, attempting runtime installation...`);
384
+ logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install blobfuse2 in your sandbox template via createMountableTemplate()`);
385
+ const repos = resolveMicrosoftAptRepos((await sandbox.commands.run("cat /etc/os-release 2>/dev/null || true")).stdout);
386
+ const repoSetupResult = await sandbox.commands.run("sudo mkdir -p /etc/apt/keyrings && curl --retry 3 --retry-all-errors --retry-delay 2 -fsSL https://packages.microsoft.com/keys/microsoft.asc -o /tmp/ms-key.asc && sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/microsoft.gpg /tmp/ms-key.asc", { timeoutMs: 6e4 });
387
+ let installResult;
388
+ if (repoSetupResult.exitCode === 0) for (const { repoUrl, suite } of repos) {
389
+ installResult = await sandbox.commands.run(`echo "deb [signed-by=/etc/apt/keyrings/microsoft.gpg] ${repoUrl} ${suite} main" | sudo tee /etc/apt/sources.list.d/microsoft-prod.list && sudo apt-get update 2>&1 && sudo apt-get install -y blobfuse2 fuse3 2>&1`, { timeoutMs: 18e4 });
390
+ if (installResult.exitCode === 0) break;
391
+ logger.warn(`${LOG_PREFIX} blobfuse2 install failed for ${repoUrl} ${suite}, trying fallback if available`);
392
+ }
393
+ else logger.warn(`${LOG_PREFIX} Failed to set up Microsoft apt repository, trying GitHub release fallback`);
394
+ let verifyResult = await sandbox.commands.run("which blobfuse2 && blobfuse2 --version", { timeoutMs: 3e4 });
395
+ if (verifyResult.exitCode !== 0) {
396
+ installResult = await sandbox.commands.run(`sudo apt-get update -qq 2>&1 || true && sudo apt-get install -y fuse3 ca-certificates curl 2>&1 && curl -L --retry 3 --retry-all-errors --retry-delay 2 -fSLo /tmp/blobfuse2.deb ${BLOBFUSE2_GITHUB_DEB} && sudo dpkg -i /tmp/blobfuse2.deb 2>&1 && sudo bash -c 'lib=\$(find /usr/lib -name "libfuse3.so.3.*" | head -1); [ -z "\$lib" ] || ln -sf "\$lib" /usr/lib/x86_64-linux-gnu/libfuse3.so.3'`, { timeoutMs: 18e4 });
397
+ verifyResult = await sandbox.commands.run("which blobfuse2 && blobfuse2 --version", { timeoutMs: 3e4 });
398
+ }
399
+ if (!installResult || verifyResult.exitCode !== 0) throw new Error(`Failed to install blobfuse2. For Azure Blob mounting, your template needs blobfuse2 and fuse3.
408
400
 
409
401
  Option 1: Use createMountableTemplate() helper:
410
402
  import { E2BSandbox, createMountableTemplate } from '@mastra/e2b';
@@ -413,1069 +405,1127 @@ Option 1: Use createMountableTemplate() helper:
413
405
  Option 2: Customize the base template:
414
406
  new E2BSandbox({ template: base => base.aptInstall(['your-packages']) })
415
407
 
416
- Error details: ${verifyResult.stderr || verifyResult.stdout || installResult?.stderr || installResult?.stdout || "unknown error"}`
417
- );
418
- }
419
- }
420
- const mountHash = crypto.createHash("md5").update(mountPath).digest("hex").slice(0, 8);
421
- const configPath = `/tmp/.blobfuse2-config-${mountHash}.yaml`;
422
- const cachePath = `/tmp/blobfuse2-cache-${mountHash}`;
423
- const yaml = buildBlobfuseConfig(config.container, auth, cachePath, !!config.readOnly);
424
- await sandbox.commands.run(`sudo rm -f ${configPath}`);
425
- await sandbox.files.write(configPath, yaml);
426
- await sandbox.commands.run(`sudo chown root:root ${configPath} && sudo chmod 600 ${configPath}`);
427
- await sandbox.commands.run(`sudo rm -rf ${shellQuote(cachePath)} && sudo mkdir -p ${shellQuote(cachePath)}`);
428
- const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote(prefix)}` : "";
429
- const mountCmd = `sudo blobfuse2 mount ${shellQuote(mountPath)} --config-file=${shellQuote(configPath)}${prefixFlags}`;
430
- logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
431
- try {
432
- const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
433
- logger.debug(`${LOG_PREFIX} blobfuse2 result:`, {
434
- exitCode: result.exitCode,
435
- stdout: result.stdout,
436
- stderr: result.stderr
437
- });
438
- if (result.exitCode !== 0) {
439
- throw new Error(`Failed to mount Azure Blob container: ${result.stderr || result.stdout}`);
440
- }
441
- } catch (error) {
442
- const errorObj = error;
443
- const stderr = errorObj.result?.stderr || "";
444
- const stdout = errorObj.result?.stdout || "";
445
- logger.error(`${LOG_PREFIX} blobfuse2 error:`, { stderr, stdout, error: String(error) });
446
- throw new Error(`Failed to mount Azure Blob container: ${stderr || stdout || error}`);
447
- }
408
+ Error details: ${verifyResult.stderr || verifyResult.stdout || installResult?.stderr || installResult?.stdout || "unknown error"}`);
409
+ }
410
+ const mountHash = (0, crypto.createHash)("md5").update(mountPath).digest("hex").slice(0, 8);
411
+ const configPath = `/tmp/.blobfuse2-config-${mountHash}.yaml`;
412
+ const cachePath = `/tmp/blobfuse2-cache-${mountHash}`;
413
+ const yaml = buildBlobfuseConfig(config.container, auth, cachePath, !!config.readOnly);
414
+ await sandbox.commands.run(`sudo rm -f ${configPath}`);
415
+ await sandbox.files.write(configPath, yaml);
416
+ await sandbox.commands.run(`sudo chown root:root ${configPath} && sudo chmod 600 ${configPath}`);
417
+ await sandbox.commands.run(`sudo rm -rf ${shellQuote(cachePath)} && sudo mkdir -p ${shellQuote(cachePath)}`);
418
+ const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote(prefix)}` : "";
419
+ const mountCmd = `sudo blobfuse2 mount ${shellQuote(mountPath)} --config-file=${shellQuote(configPath)}${prefixFlags}`;
420
+ logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
421
+ try {
422
+ const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
423
+ logger.debug(`${LOG_PREFIX} blobfuse2 result:`, {
424
+ exitCode: result.exitCode,
425
+ stdout: result.stdout,
426
+ stderr: result.stderr
427
+ });
428
+ if (result.exitCode !== 0) throw new Error(`Failed to mount Azure Blob container: ${result.stderr || result.stdout}`);
429
+ } catch (error) {
430
+ const errorObj = error;
431
+ const stderr = errorObj.result?.stderr || "";
432
+ const stdout = errorObj.result?.stdout || "";
433
+ logger.error(`${LOG_PREFIX} blobfuse2 error:`, {
434
+ stderr,
435
+ stdout,
436
+ error: String(error)
437
+ });
438
+ throw new Error(`Failed to mount Azure Blob container: ${stderr || stdout || error}`);
439
+ }
448
440
  }
449
- var E2BProcessHandle = class extends workspace.ProcessHandle {
450
- pid;
451
- _e2bHandle;
452
- _sandbox;
453
- _startTime;
454
- constructor(e2bHandle, sandbox, startTime, options) {
455
- super(options);
456
- this.pid = String(e2bHandle.pid);
457
- this._e2bHandle = e2bHandle;
458
- this._sandbox = sandbox;
459
- this._startTime = startTime;
460
- }
461
- /** Delegates to E2B's handle so exitCode reflects server-side state without needing wait(). */
462
- get exitCode() {
463
- return this._e2bHandle.exitCode;
464
- }
465
- async wait() {
466
- try {
467
- const result = await this._e2bHandle.wait();
468
- return {
469
- success: result.exitCode === 0,
470
- exitCode: result.exitCode,
471
- stdout: this.stdout,
472
- stderr: this.stderr,
473
- executionTimeMs: Date.now() - this._startTime
474
- };
475
- } catch (error) {
476
- const errorObj = error;
477
- const exitCode = errorObj.result?.exitCode ?? errorObj.exitCode ?? this.exitCode ?? 1;
478
- if (errorObj.result?.stdout) this.emitStdout(errorObj.result.stdout);
479
- if (errorObj.result?.stderr) this.emitStderr(errorObj.result.stderr);
480
- return {
481
- success: false,
482
- exitCode,
483
- stdout: this.stdout,
484
- stderr: this.stderr || (error instanceof Error ? error.message : String(error)),
485
- executionTimeMs: Date.now() - this._startTime
486
- };
487
- }
488
- }
489
- async kill() {
490
- if (this.exitCode !== void 0) return false;
491
- return this._e2bHandle.kill();
492
- }
493
- async sendStdin(data) {
494
- if (this.exitCode !== void 0) {
495
- throw new Error(`Process ${this.pid} has already exited with code ${this.exitCode}`);
496
- }
497
- await this._sandbox.commands.sendStdin(this._e2bHandle.pid, data);
498
- }
441
+ //#endregion
442
+ //#region src/sandbox/process-manager.ts
443
+ /**
444
+ * E2B Process Manager
445
+ *
446
+ * Implements SandboxProcessManager for E2B cloud sandboxes.
447
+ * Wraps the E2B SDK's commands API (background mode, sendStdin, kill, list).
448
+ */
449
+ /**
450
+ * Wraps an E2B CommandHandle to conform to Mastra's ProcessHandle.
451
+ * Not exported — internal to this module.
452
+ *
453
+ * Listener dispatch is handled by the base class. The manager's spawn()/get()
454
+ * methods wire E2B's constructor-time callbacks to handle.emitStdout/emitStderr.
455
+ */
456
+ var E2BProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
457
+ pid;
458
+ _e2bHandle;
459
+ _sandbox;
460
+ _startTime;
461
+ constructor(e2bHandle, sandbox, startTime, options) {
462
+ super(options);
463
+ this.pid = String(e2bHandle.pid);
464
+ this._e2bHandle = e2bHandle;
465
+ this._sandbox = sandbox;
466
+ this._startTime = startTime;
467
+ }
468
+ /** Delegates to E2B's handle so exitCode reflects server-side state without needing wait(). */
469
+ get exitCode() {
470
+ return this._e2bHandle.exitCode;
471
+ }
472
+ async wait() {
473
+ try {
474
+ const result = await this._e2bHandle.wait();
475
+ return {
476
+ success: result.exitCode === 0,
477
+ exitCode: result.exitCode,
478
+ stdout: this.stdout,
479
+ stderr: this.stderr,
480
+ executionTimeMs: Date.now() - this._startTime
481
+ };
482
+ } catch (error) {
483
+ const errorObj = error;
484
+ const exitCode = errorObj.result?.exitCode ?? errorObj.exitCode ?? this.exitCode ?? 1;
485
+ if (errorObj.result?.stdout) this.emitStdout(errorObj.result.stdout);
486
+ if (errorObj.result?.stderr) this.emitStderr(errorObj.result.stderr);
487
+ return {
488
+ success: false,
489
+ exitCode,
490
+ stdout: this.stdout,
491
+ stderr: this.stderr || (error instanceof Error ? error.message : String(error)),
492
+ executionTimeMs: Date.now() - this._startTime
493
+ };
494
+ }
495
+ }
496
+ async kill() {
497
+ if (this.exitCode !== void 0) return false;
498
+ return this._e2bHandle.kill();
499
+ }
500
+ async sendStdin(data) {
501
+ if (this.exitCode !== void 0) throw new Error(`Process ${this.pid} has already exited with code ${this.exitCode}`);
502
+ await this._sandbox.commands.sendStdin(this._e2bHandle.pid, data);
503
+ }
499
504
  };
500
- var E2BProcessManager = class extends workspace.SandboxProcessManager {
501
- async spawn(command, options = {}) {
502
- return this.sandbox.retryOnDead(async () => {
503
- const e2b = this.sandbox.e2b;
504
- const mergedEnv = { ...this.env, ...options.env };
505
- const envs = Object.fromEntries(
506
- Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
507
- );
508
- let handle;
509
- const e2bHandle = await e2b.commands.run(command, {
510
- background: true,
511
- stdin: true,
512
- cwd: options.cwd,
513
- envs,
514
- timeoutMs: options.timeout,
515
- onStdout: (data) => handle.emitStdout(data),
516
- onStderr: (data) => handle.emitStderr(data)
517
- });
518
- handle = new E2BProcessHandle(e2bHandle, e2b, Date.now(), options);
519
- this._tracked.set(handle.pid, handle);
520
- return handle;
521
- });
522
- }
523
- /**
524
- * List processes by querying E2B's commands API.
525
- * E2B manages all state server-side — no local tracking needed.
526
- */
527
- async list() {
528
- const e2b = this.sandbox.e2b;
529
- const procs = await e2b.commands.list();
530
- return procs.map((proc) => ({
531
- pid: String(proc.pid),
532
- command: [proc.cmd, ...proc.args].join(" "),
533
- running: true
534
- // E2B only lists running processes
535
- }));
536
- }
537
- /**
538
- * Get a handle to a process by PID.
539
- * Checks base class tracking first, then falls back to commands.connect()
540
- * for processes spawned externally or before reconnection.
541
- */
542
- async get(pid) {
543
- const tracked = this._tracked.get(pid);
544
- if (tracked) return tracked;
545
- const numericPid = /^\d+$/.test(pid) ? Number(pid) : void 0;
546
- if (numericPid === void 0) return void 0;
547
- const e2b = this.sandbox.e2b;
548
- let handle;
549
- try {
550
- const e2bHandle = await e2b.commands.connect(numericPid, {
551
- onStdout: (data) => handle.emitStdout(data),
552
- onStderr: (data) => handle.emitStderr(data)
553
- });
554
- handle = new E2BProcessHandle(e2bHandle, e2b, Date.now());
555
- this._tracked.set(handle.pid, handle);
556
- return handle;
557
- } catch {
558
- return void 0;
559
- }
560
- }
505
+ /**
506
+ * E2B implementation of SandboxProcessManager.
507
+ * Uses the E2B SDK's commands.run() with background: true.
508
+ */
509
+ var E2BProcessManager = class extends _mastra_core_workspace.SandboxProcessManager {
510
+ async spawn(command, options = {}) {
511
+ return this.sandbox.retryOnDead(async () => {
512
+ const e2b = this.sandbox.e2b;
513
+ const mergedEnv = {
514
+ ...this.env,
515
+ ...options.env
516
+ };
517
+ const envs = Object.fromEntries(Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0));
518
+ let handle;
519
+ handle = new E2BProcessHandle(await e2b.commands.run(command, {
520
+ background: true,
521
+ stdin: true,
522
+ cwd: options.cwd,
523
+ envs,
524
+ timeoutMs: options.timeout,
525
+ onStdout: (data) => handle.emitStdout(data),
526
+ onStderr: (data) => handle.emitStderr(data)
527
+ }), e2b, Date.now(), options);
528
+ this._tracked.set(handle.pid, handle);
529
+ return handle;
530
+ });
531
+ }
532
+ /**
533
+ * List processes by querying E2B's commands API.
534
+ * E2B manages all state server-side — no local tracking needed.
535
+ */
536
+ async list() {
537
+ return (await this.sandbox.e2b.commands.list()).map((proc) => ({
538
+ pid: String(proc.pid),
539
+ command: [proc.cmd, ...proc.args].join(" "),
540
+ running: true
541
+ }));
542
+ }
543
+ /**
544
+ * Get a handle to a process by PID.
545
+ * Checks base class tracking first, then falls back to commands.connect()
546
+ * for processes spawned externally or before reconnection.
547
+ */
548
+ async get(pid) {
549
+ const tracked = this._tracked.get(pid);
550
+ if (tracked) return tracked;
551
+ const numericPid = /^\d+$/.test(pid) ? Number(pid) : void 0;
552
+ if (numericPid === void 0) return void 0;
553
+ const e2b = this.sandbox.e2b;
554
+ let handle;
555
+ try {
556
+ handle = new E2BProcessHandle(await e2b.commands.connect(numericPid, {
557
+ onStdout: (data) => handle.emitStdout(data),
558
+ onStderr: (data) => handle.emitStderr(data)
559
+ }), e2b, Date.now());
560
+ this._tracked.set(handle.pid, handle);
561
+ return handle;
562
+ } catch {
563
+ return;
564
+ }
565
+ }
561
566
  };
562
-
563
- // src/sandbox/index.ts
564
- var SAFE_MOUNT_PATH = /^\/[a-zA-Z0-9_.\-/]+$/;
567
+ //#endregion
568
+ //#region src/sandbox/index.ts
569
+ /** Allowlist pattern for mount paths — absolute path with safe characters only. */
570
+ const SAFE_MOUNT_PATH = /^\/[a-zA-Z0-9_.\-/]+$/;
565
571
  function validateMountPath(mountPath) {
566
- if (!SAFE_MOUNT_PATH.test(mountPath)) {
567
- throw new Error(
568
- `Invalid mount path: ${mountPath}. Must be an absolute path with alphanumeric, dash, dot, underscore, or slash characters only.`
569
- );
570
- }
572
+ if (!SAFE_MOUNT_PATH.test(mountPath)) throw new Error(`Invalid mount path: ${mountPath}. Must be an absolute path with alphanumeric, dash, dot, underscore, or slash characters only.`);
571
573
  }
572
- var SAFE_MARKER_NAME = /^mount-[a-z0-9]+$/;
573
- var E2BSandbox = class _E2BSandbox extends workspace.MastraSandbox {
574
- id;
575
- name = "E2BSandbox";
576
- provider = "e2b";
577
- status = "pending";
578
- /**
579
- * Networking capability: public HTTPS URLs for sandbox ports.
580
- * E2B exposes every port via `getHost(port)` — no upfront declaration needed.
581
- *
582
- * When not attached in this process, the URL is resolved by looking up the
583
- * existing sandbox by identity (without resuming it) and deriving the host
584
- * (`{port}-{sandboxId}.{domain}`), so other processes can resolve
585
- * deployments without waking a paused sandbox.
586
- */
587
- networking = {
588
- getPortUrl: async (port) => {
589
- try {
590
- if (this._sandbox) {
591
- return `https://${this._sandbox.getHost(port)}`;
592
- }
593
- const info = await this.lookupExistingSandboxInfo();
594
- if (!info) return null;
595
- return `https://${port}-${info.sandboxId}.${this.sandboxDomain}`;
596
- } catch {
597
- return null;
598
- }
599
- }
600
- };
601
- _sandbox = null;
602
- _createdAt = null;
603
- _isRetrying = false;
604
- timeout;
605
- templateSpec;
606
- env;
607
- metadata;
608
- network;
609
- connectionOpts;
610
- _instructionsOverride;
611
- _constructorOptions;
612
- /** Resolved template ID after building (if needed) */
613
- _resolvedTemplateId;
614
- /** Promise for template preparation (started in constructor) */
615
- _templatePreparePromise;
616
- constructor(options = {}) {
617
- super({
618
- ...options,
619
- name: "E2BSandbox",
620
- processes: new E2BProcessManager({ env: options.env ?? {} })
621
- });
622
- this.id = options.id ?? this.generateId();
623
- this.timeout = options.timeout ?? 3e5;
624
- this.templateSpec = options.template;
625
- this.env = options.env ?? {};
626
- this.metadata = options.metadata ?? {};
627
- this.network = options.network;
628
- this.connectionOpts = {
629
- ...options.domain && { domain: options.domain },
630
- ...options.apiUrl && { apiUrl: options.apiUrl },
631
- ...options.apiKey && { apiKey: options.apiKey },
632
- ...options.accessToken && { accessToken: options.accessToken }
633
- };
634
- this._instructionsOverride = options.instructions;
635
- this._constructorOptions = { ...options };
636
- this._templatePreparePromise = this.resolveTemplate().catch((err) => {
637
- this.logger.debug(`${LOG_PREFIX} Template preparation error (will retry on start):`, err);
638
- return "";
639
- });
640
- }
641
- /**
642
- * Construct a sibling `E2BSandbox` that inherits this sandbox's
643
- * configuration (credentials, template, network, metadata, instructions)
644
- * with per-instance overrides.
645
- *
646
- * Performs no I/O — the sandbox clone provisions (or reconnects to an
647
- * existing E2B sandbox with the same logical `id`) on its own `start()`.
648
- * Use it when one configured sandbox acts as the template for a fleet of
649
- * independent sandboxes (e.g. one per project).
650
- *
651
- * `options.idleTimeoutMinutes` maps to the E2B sandbox `timeout` (ms);
652
- * `options.sandboxId` is ignored because E2B reconnects by logical `id`.
653
- */
654
- clone(options = {}) {
655
- const { id: _id, ...base } = this._constructorOptions;
656
- return new _E2BSandbox({
657
- ...base,
658
- ...options.id !== void 0 && { id: options.id },
659
- ...options.env !== void 0 && { env: options.env },
660
- ...options.idleTimeoutMinutes !== void 0 && { timeout: options.idleTimeoutMinutes * 6e4 }
661
- });
662
- }
663
- /**
664
- * Get the underlying E2B Sandbox instance for direct access to E2B APIs.
665
- *
666
- * Use this when you need to access E2B features not exposed through the
667
- * WorkspaceSandbox interface (e.g., files API, ports, etc.).
668
- *
669
- * @throws {SandboxNotReadyError} If the sandbox has not been started
670
- *
671
- * @example Direct file operations
672
- * ```typescript
673
- * const e2b = sandbox.e2b;
674
- * await e2b.files.write('/tmp/test.txt', 'Hello');
675
- * const content = await e2b.files.read('/tmp/test.txt');
676
- * const files = await e2b.files.list('/tmp');
677
- * ```
678
- *
679
- * @example Access ports
680
- * ```typescript
681
- * const e2b = sandbox.e2b;
682
- * const url = e2b.getHost(3000);
683
- * ```
684
- */
685
- get e2b() {
686
- if (!this._sandbox) {
687
- throw new workspace.SandboxNotReadyError(this.id);
688
- }
689
- return this._sandbox;
690
- }
691
- // ---------------------------------------------------------------------------
692
- // Lifecycle
693
- // ---------------------------------------------------------------------------
694
- /**
695
- * Start the E2B sandbox.
696
- * Handles template preparation, existing sandbox reconnection, and new sandbox creation.
697
- *
698
- * Status management and mount processing are handled by the base class.
699
- */
700
- async start() {
701
- if (this._sandbox) {
702
- return;
703
- }
704
- const [existingSandbox, templateId] = await Promise.all([
705
- this.findExistingSandbox(),
706
- this._templatePreparePromise || this.resolveTemplate()
707
- ]);
708
- if (existingSandbox) {
709
- this._sandbox = existingSandbox;
710
- this._createdAt = /* @__PURE__ */ new Date();
711
- this.logger.debug(`${LOG_PREFIX} Reconnected to existing sandbox for: ${this.id}`);
712
- const expectedPaths = Array.from(this.mounts.entries.keys());
713
- this.logger.debug(`${LOG_PREFIX} Running mount reconciliation...`);
714
- await this.reconcileMounts(expectedPaths);
715
- this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
716
- return;
717
- }
718
- let resolvedTemplateId = templateId;
719
- if (!resolvedTemplateId) {
720
- this.logger.debug(`${LOG_PREFIX} Template preparation failed earlier, retrying...`);
721
- resolvedTemplateId = await this.resolveTemplate();
722
- }
723
- this.logger.debug(`${LOG_PREFIX} Creating new sandbox for: ${this.id} with template: ${resolvedTemplateId}`);
724
- try {
725
- this._sandbox = await e2b.Sandbox.create(resolvedTemplateId, {
726
- ...this.connectionOpts,
727
- lifecycle: { onTimeout: "pause" },
728
- metadata: {
729
- ...this.metadata,
730
- "mastra-sandbox-id": this.id
731
- },
732
- ...this.network && { network: this.network },
733
- timeoutMs: this.timeout
734
- });
735
- } catch (createError) {
736
- const errorStr = String(createError);
737
- if (errorStr.includes("404") && errorStr.includes("not found") && !this.templateSpec) {
738
- this.logger.debug(`${LOG_PREFIX} Template not found, rebuilding: ${templateId}`);
739
- this._resolvedTemplateId = void 0;
740
- const rebuiltTemplateId = await this.buildDefaultTemplate();
741
- this.logger.debug(`${LOG_PREFIX} Retrying sandbox creation with rebuilt template: ${rebuiltTemplateId}`);
742
- this._sandbox = await e2b.Sandbox.create(rebuiltTemplateId, {
743
- ...this.connectionOpts,
744
- lifecycle: { onTimeout: "pause" },
745
- metadata: {
746
- ...this.metadata,
747
- "mastra-sandbox-id": this.id
748
- },
749
- ...this.network && { network: this.network },
750
- timeoutMs: this.timeout
751
- });
752
- } else {
753
- throw createError;
754
- }
755
- }
756
- this.logger.debug(`${LOG_PREFIX} Created sandbox ${this._sandbox.sandboxId} for logical ID: ${this.id}`);
757
- this._createdAt = /* @__PURE__ */ new Date();
758
- }
759
- /**
760
- * Stop the E2B sandbox by pausing it (snapshot-stop).
761
- *
762
- * Pausing freezes the whole VM — filesystem, memory, and running processes —
763
- * and stops billing immediately. The next `start()` reconnects and resumes it,
764
- * with background processes still running. Filesystem mounts are unmounted
765
- * first (FUSE mounts don't survive pause) and reconciled again on start.
766
- *
767
- * Status management is handled by the base class.
768
- */
769
- async stop() {
770
- for (const mountPath of [...this.mounts.entries.keys()]) {
771
- try {
772
- await this.unmount(mountPath);
773
- } catch {
774
- }
775
- }
776
- if (this._sandbox) {
777
- await this._sandbox.pause();
778
- this.logger.debug(`${LOG_PREFIX} Paused sandbox ${this._sandbox.sandboxId} for: ${this.id}`);
779
- } else {
780
- const info = await this.lookupExistingSandboxInfo();
781
- if (info?.state === "running") {
782
- await e2b.Sandbox.pause(info.sandboxId, this.connectionOpts);
783
- this.logger.debug(`${LOG_PREFIX} Paused detached sandbox ${info.sandboxId} for: ${this.id}`);
784
- }
785
- }
786
- this._sandbox = null;
787
- }
788
- /**
789
- * Destroy the E2B sandbox and clean up all resources.
790
- * Unmounts filesystems, kills the sandbox, and clears mount state.
791
- * Status management is handled by the base class.
792
- */
793
- async destroy() {
794
- if (this._sandbox) {
795
- try {
796
- const procs = await this.processes.list();
797
- await Promise.all(procs.map((p) => this.processes.kill(p.pid)));
798
- } catch {
799
- }
800
- for (const mountPath of [...this.mounts.entries.keys()]) {
801
- try {
802
- await this.unmount(mountPath);
803
- } catch {
804
- }
805
- }
806
- await this._sandbox.kill();
807
- this._sandbox = null;
808
- } else {
809
- const info = await this.lookupExistingSandboxInfo();
810
- if (info) {
811
- await e2b.Sandbox.kill(info.sandboxId, this.connectionOpts);
812
- this.logger.debug(`${LOG_PREFIX} Killed detached sandbox ${info.sandboxId} for: ${this.id}`);
813
- }
814
- }
815
- this.mounts.clear();
816
- }
817
- async getInfo() {
818
- return {
819
- id: this.id,
820
- name: this.name,
821
- provider: this.provider,
822
- status: this.status,
823
- createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
824
- mounts: Array.from(this.mounts.entries).map(([path, entry]) => ({
825
- path,
826
- filesystem: entry.filesystem?.provider ?? entry.config?.type ?? "unknown"
827
- })),
828
- metadata: {
829
- ...this.metadata
830
- }
831
- };
832
- }
833
- // ---------------------------------------------------------------------------
834
- // File Upload
835
- // ---------------------------------------------------------------------------
836
- /**
837
- * Bulk-write files into the sandbox filesystem via the SDK's native upload.
838
- */
839
- async writeFiles(files) {
840
- await this.ensureRunning();
841
- await this.e2b.files.write(
842
- files.map((f) => ({
843
- path: f.path,
844
- data: typeof f.content === "string" ? f.content : new Blob([new Uint8Array(f.content)])
845
- }))
846
- );
847
- }
848
- /**
849
- * Get instructions describing this E2B sandbox.
850
- * Used by agents to understand the execution environment.
851
- */
852
- getInstructions(opts) {
853
- if (this._instructionsOverride === void 0) return this._getDefaultInstructions();
854
- if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
855
- const defaultInstructions = this._getDefaultInstructions();
856
- return this._instructionsOverride({ defaultInstructions, requestContext: opts?.requestContext });
857
- }
858
- _getDefaultInstructions() {
859
- const mountCount = this.mounts.entries.size;
860
- const mountInfo = mountCount > 0 ? ` ${mountCount} filesystem(s) mounted via FUSE.` : "";
861
- return `Cloud sandbox.${mountInfo}`;
862
- }
863
- // ---------------------------------------------------------------------------
864
- // Mounting
865
- // ---------------------------------------------------------------------------
866
- /**
867
- * Mount a filesystem at a path in the sandbox.
868
- * Uses FUSE tools (s3fs, gcsfuse) to mount cloud storage.
869
- */
870
- async mount(filesystem, mountPath) {
871
- validateMountPath(mountPath);
872
- if (!this._sandbox) {
873
- throw new workspace.SandboxNotReadyError(this.id);
874
- }
875
- this.logger.debug(`${LOG_PREFIX} Mounting "${mountPath}"...`);
876
- const config = filesystem.getMountConfig?.();
877
- if (!config) {
878
- const error = `Filesystem "${filesystem.id}" does not provide a mount config`;
879
- this.logger.error(`${LOG_PREFIX} ${error}`);
880
- this.mounts.set(mountPath, { filesystem, state: "error", error });
881
- return { success: false, mountPath, error };
882
- }
883
- const existingMount = await this.checkExistingMount(mountPath, config);
884
- if (existingMount === "matching") {
885
- this.logger.debug(
886
- `${LOG_PREFIX} Detected existing mount for ${filesystem.provider} ("${filesystem.id}") at "${mountPath}" with correct config, skipping`
887
- );
888
- this.mounts.set(mountPath, { state: "mounted", config });
889
- return { success: true, mountPath };
890
- } else if (existingMount === "mismatched") {
891
- this.logger.debug(`${LOG_PREFIX} Config mismatch, unmounting to re-mount with new config...`);
892
- await this.unmount(mountPath);
893
- }
894
- this.logger.debug(`${LOG_PREFIX} Config type: ${config.type}`);
895
- this.mounts.set(mountPath, { filesystem, state: "mounting", config });
896
- try {
897
- const checkResult = await this._sandbox.commands.run(
898
- `[ -d "${mountPath}" ] && [ "$(ls -A "${mountPath}" 2>/dev/null)" ] && echo "non-empty" || echo "ok"`
899
- );
900
- if (checkResult.stdout.trim() === "non-empty") {
901
- const error = `Cannot mount at ${mountPath}: directory exists and is not empty. Mounting would hide existing files. Use a different path or empty the directory first.`;
902
- this.logger.error(`${LOG_PREFIX} ${error}`);
903
- this.mounts.set(mountPath, { filesystem, state: "error", config, error });
904
- return { success: false, mountPath, error };
905
- }
906
- } catch {
907
- }
908
- try {
909
- this.logger.debug(`${LOG_PREFIX} Creating mount directory for ${mountPath}...`);
910
- const mkdirCommand = `sudo mkdir -p "${mountPath}" && sudo chown $(id -u):$(id -g) "${mountPath}"`;
911
- this.logger.debug(`${LOG_PREFIX} Running command: ${mkdirCommand}`);
912
- const mkdirResult = await this._sandbox.commands.run(mkdirCommand);
913
- this.logger.debug(`${LOG_PREFIX} Created mount directory for mount path "${mountPath}":`, mkdirResult);
914
- } catch (mkdirError) {
915
- this.logger.debug(`${LOG_PREFIX} mkdir error for "${mountPath}":`, mkdirError);
916
- this.mounts.set(mountPath, { filesystem, state: "error", config, error: String(mkdirError) });
917
- return { success: false, mountPath, error: String(mkdirError) };
918
- }
919
- const mountCtx = {
920
- sandbox: this._sandbox,
921
- logger: this.logger
922
- };
923
- try {
924
- switch (config.type) {
925
- case "s3":
926
- this.logger.debug(`${LOG_PREFIX} Mounting S3 bucket at ${mountPath}...`);
927
- await mountS3(mountPath, config, mountCtx);
928
- this.logger.debug(`${LOG_PREFIX} Mounted S3 bucket at ${mountPath}`);
929
- break;
930
- case "gcs":
931
- this.logger.debug(`${LOG_PREFIX} Mounting GCS bucket at ${mountPath}...`);
932
- await mountGCS(mountPath, config, mountCtx);
933
- this.logger.debug(`${LOG_PREFIX} Mounted GCS bucket at ${mountPath}`);
934
- break;
935
- case "azure-blob":
936
- this.logger.debug(`${LOG_PREFIX} Mounting Azure Blob container at ${mountPath}...`);
937
- await mountAzure(mountPath, config, mountCtx);
938
- this.logger.debug(`${LOG_PREFIX} Mounted Azure Blob container at ${mountPath}`);
939
- break;
940
- default:
941
- this.mounts.set(mountPath, {
942
- filesystem,
943
- state: "unsupported",
944
- config,
945
- error: `Unsupported mount type: ${config.type}`
946
- });
947
- return {
948
- success: false,
949
- mountPath,
950
- error: `Unsupported mount type: ${config.type}`
951
- };
952
- }
953
- } catch (error) {
954
- this.logger.error(
955
- `${LOG_PREFIX} Error mounting "${filesystem.provider}" (${filesystem.id}) at "${mountPath}":`,
956
- error
957
- );
958
- this.mounts.set(mountPath, { filesystem, state: "error", config, error: String(error) });
959
- try {
960
- await this._sandbox.commands.run(`sudo rmdir "${mountPath}" 2>/dev/null || true`);
961
- this.logger.debug(`${LOG_PREFIX} Cleaned up directory after failed mount: ${mountPath}`);
962
- } catch {
963
- }
964
- return { success: false, mountPath, error: String(error) };
965
- }
966
- this.mounts.set(mountPath, { state: "mounted", config });
967
- await this.writeMarkerFile(mountPath);
968
- this.logger.debug(`${LOG_PREFIX} Mounted ${mountPath}`);
969
- return { success: true, mountPath };
970
- }
971
- /**
972
- * Unmount a filesystem from a path in the sandbox.
973
- */
974
- async unmount(mountPath) {
975
- validateMountPath(mountPath);
976
- if (!this._sandbox) {
977
- throw new workspace.SandboxNotReadyError(this.id);
978
- }
979
- this.logger.debug(`${LOG_PREFIX} Unmounting ${mountPath}...`);
980
- try {
981
- const result = await this._sandbox.commands.run(
982
- `sudo fusermount -u "${mountPath}" 2>/dev/null || sudo umount "${mountPath}"`
983
- );
984
- if (result.exitCode !== 0) {
985
- this.logger.debug(`${LOG_PREFIX} Unmount warning: ${result.stderr || result.stdout}`);
986
- }
987
- } catch (error) {
988
- this.logger.debug(`${LOG_PREFIX} Unmount error:`, error);
989
- await this._sandbox.commands.run(`sudo umount -l "${mountPath}" 2>/dev/null || true`);
990
- }
991
- this.mounts.delete(mountPath);
992
- const filename = this.mounts.markerFilename(mountPath);
993
- const markerPath = `/tmp/.mastra-mounts/${filename}`;
994
- await this._sandbox.commands.run(`rm -f "${markerPath}" 2>/dev/null || true`);
995
- const rmdirResult = await this._sandbox.commands.run(`sudo rmdir "${mountPath}" 2>&1`);
996
- if (rmdirResult.exitCode === 0) {
997
- this.logger.debug(`${LOG_PREFIX} Unmounted and removed ${mountPath}`);
998
- } else {
999
- this.logger.debug(
1000
- `${LOG_PREFIX} Unmounted ${mountPath} (directory not removed: ${rmdirResult.stderr?.trim() || "not empty"})`
1001
- );
1002
- }
1003
- }
1004
- /**
1005
- * Unmount all stale mounts that are not in the expected mounts list.
1006
- * Also cleans up orphaned directories and marker files from failed mount attempts.
1007
- * Call this after reconnecting to an existing sandbox to clean up old mounts.
1008
- */
1009
- async reconcileMounts(expectedMountPaths) {
1010
- if (!this._sandbox) {
1011
- throw new workspace.SandboxNotReadyError(this.id);
1012
- }
1013
- this.logger.debug(`${LOG_PREFIX} Reconciling mounts. Expected paths:`, expectedMountPaths);
1014
- const mountsResult = await this._sandbox.commands.run(
1015
- `grep -E 'fuse\\.(s3fs|gcsfuse|blobfuse2)' /proc/mounts | awk '{print $2}'`
1016
- );
1017
- const currentMounts = mountsResult.stdout.trim().split("\n").filter((p) => p.length > 0);
1018
- this.logger.debug(`${LOG_PREFIX} Current FUSE mounts in sandbox:`, currentMounts);
1019
- const markersResult = await this._sandbox.commands.run(`ls /tmp/.mastra-mounts/ 2>/dev/null || echo ""`);
1020
- const markerFiles = markersResult.stdout.trim().split("\n").filter((f) => f.length > 0 && SAFE_MARKER_NAME.test(f));
1021
- const managedMountPaths = /* @__PURE__ */ new Map();
1022
- for (const markerFile of markerFiles) {
1023
- const markerResult = await this._sandbox.commands.run(
1024
- `cat "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || echo ""`
1025
- );
1026
- const parsed = this.mounts.parseMarkerContent(markerResult.stdout.trim());
1027
- if (parsed && SAFE_MOUNT_PATH.test(parsed.path)) {
1028
- managedMountPaths.set(parsed.path, markerFile);
1029
- }
1030
- }
1031
- const staleMounts = currentMounts.filter((path) => !expectedMountPaths.includes(path));
1032
- for (const stalePath of staleMounts) {
1033
- if (managedMountPaths.has(stalePath)) {
1034
- this.logger.debug(`${LOG_PREFIX} Found stale managed FUSE mount at ${stalePath}, unmounting...`);
1035
- await this.unmount(stalePath);
1036
- } else {
1037
- this.logger.debug(`${LOG_PREFIX} Found external FUSE mount at ${stalePath}, leaving untouched`);
1038
- }
1039
- }
1040
- try {
1041
- const expectedMarkerFiles = new Set(expectedMountPaths.map((p) => this.mounts.markerFilename(p)));
1042
- const markerToPath = /* @__PURE__ */ new Map();
1043
- for (const [path, file] of managedMountPaths) {
1044
- markerToPath.set(file, path);
1045
- }
1046
- for (const markerFile of markerFiles) {
1047
- if (!expectedMarkerFiles.has(markerFile)) {
1048
- const mountPath = markerToPath.get(markerFile);
1049
- if (mountPath) {
1050
- if (!currentMounts.includes(mountPath)) {
1051
- this.logger.debug(`${LOG_PREFIX} Cleaning up orphaned marker and directory for ${mountPath}`);
1052
- await this._sandbox.commands.run(`rm -f "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || true`);
1053
- await this._sandbox.commands.run(`sudo rmdir "${mountPath}" 2>/dev/null || true`);
1054
- }
1055
- } else {
1056
- this.logger.debug(`${LOG_PREFIX} Removing malformed marker file: ${markerFile}`);
1057
- await this._sandbox.commands.run(`rm -f "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || true`);
1058
- }
1059
- }
1060
- }
1061
- } catch {
1062
- this.logger.debug(`${LOG_PREFIX} Error during orphan cleanup (non-fatal)`);
1063
- }
1064
- }
1065
- // ---------------------------------------------------------------------------
1066
- // Deprecated
1067
- // ---------------------------------------------------------------------------
1068
- /** @deprecated Use `e2b` instead. */
1069
- get instance() {
1070
- return this.e2b;
1071
- }
1072
- /** @deprecated Use `status === 'running'` instead. */
1073
- async isReady() {
1074
- return this.status === "running" && this._sandbox !== null;
1075
- }
1076
- // ---------------------------------------------------------------------------
1077
- // Internal Helpers
1078
- // ---------------------------------------------------------------------------
1079
- generateId() {
1080
- return `e2b-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1081
- }
1082
- /** Domain used to derive public sandbox hosts (self-hosted E2B or e2b.app). */
1083
- get sandboxDomain() {
1084
- return this.connectionOpts.domain ?? process.env.E2B_DOMAIN ?? "e2b.app";
1085
- }
1086
- /**
1087
- * Look up an existing sandbox with matching mastra-sandbox-id metadata
1088
- * WITHOUT connecting or resuming it. Returns its list info or null.
1089
- */
1090
- async lookupExistingSandboxInfo() {
1091
- try {
1092
- const paginator = e2b.Sandbox.list({
1093
- ...this.connectionOpts,
1094
- query: {
1095
- metadata: { "mastra-sandbox-id": this.id },
1096
- state: ["running", "paused"]
1097
- }
1098
- });
1099
- const sandboxes = await paginator.nextItems();
1100
- this.logger.debug(`${LOG_PREFIX} sandboxes:`, sandboxes);
1101
- if (sandboxes.length > 0) {
1102
- const existingSandbox = sandboxes[0];
1103
- this.logger.debug(
1104
- `${LOG_PREFIX} Found existing sandbox for ${this.id}: ${existingSandbox.sandboxId} (state: ${existingSandbox.state})`
1105
- );
1106
- return existingSandbox;
1107
- }
1108
- } catch (e) {
1109
- this.logger.debug(`${LOG_PREFIX} Error querying for existing sandbox:`, e);
1110
- }
1111
- return null;
1112
- }
1113
- /**
1114
- * Find an existing sandbox with matching mastra-sandbox-id metadata.
1115
- * Returns the connected sandbox if found, null otherwise.
1116
- * Connecting to a paused sandbox resumes it.
1117
- */
1118
- async findExistingSandbox() {
1119
- const info = await this.lookupExistingSandboxInfo();
1120
- if (!info) return null;
1121
- try {
1122
- return await e2b.Sandbox.connect(info.sandboxId, this.connectionOpts);
1123
- } catch (e) {
1124
- this.logger.debug(`${LOG_PREFIX} Error connecting to existing sandbox:`, e);
1125
- return null;
1126
- }
1127
- }
1128
- /**
1129
- * Resolve the template specification to a template ID.
1130
- *
1131
- * - String: Use as-is (template ID)
1132
- * - TemplateBuilder: Build and return the template ID
1133
- * - Function: Apply to base mountable template, then build
1134
- * - undefined: Use default mountable template (cached)
1135
- */
1136
- async resolveTemplate() {
1137
- if (this._resolvedTemplateId) {
1138
- return this._resolvedTemplateId;
1139
- }
1140
- if (!this.templateSpec) {
1141
- const { template: template2, id } = createDefaultMountableTemplate();
1142
- const exists = await e2b.Template.exists(id, this.connectionOpts);
1143
- if (exists) {
1144
- this.logger.debug(`${LOG_PREFIX} Using cached mountable template: ${id}`);
1145
- this._resolvedTemplateId = id;
1146
- return id;
1147
- }
1148
- this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
1149
- const buildResult2 = await e2b.Template.build(template2, id, this.connectionOpts);
1150
- this._resolvedTemplateId = buildResult2.templateId;
1151
- this.logger.debug(`${LOG_PREFIX} Template built and cached: ${buildResult2.templateId}`);
1152
- return buildResult2.templateId;
1153
- }
1154
- if (typeof this.templateSpec === "string") {
1155
- this._resolvedTemplateId = this.templateSpec;
1156
- return this.templateSpec;
1157
- }
1158
- let template;
1159
- let templateName;
1160
- if (typeof this.templateSpec === "function") {
1161
- const { template: baseTemplate } = createDefaultMountableTemplate();
1162
- template = this.templateSpec(baseTemplate);
1163
- templateName = `mastra-custom-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
1164
- } else {
1165
- template = this.templateSpec;
1166
- templateName = `mastra-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
1167
- }
1168
- this.logger.debug(`${LOG_PREFIX} Building custom template: ${templateName}...`);
1169
- const buildResult = await e2b.Template.build(template, templateName, this.connectionOpts);
1170
- this._resolvedTemplateId = buildResult.templateId;
1171
- this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
1172
- return buildResult.templateId;
1173
- }
1174
- /**
1175
- * Build the default mountable template (bypasses exists check).
1176
- */
1177
- async buildDefaultTemplate() {
1178
- const { template, id } = createDefaultMountableTemplate();
1179
- this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
1180
- const buildResult = await e2b.Template.build(template, id, this.connectionOpts);
1181
- this._resolvedTemplateId = buildResult.templateId;
1182
- this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
1183
- return buildResult.templateId;
1184
- }
1185
- /**
1186
- * Write marker file for detecting config changes on reconnect.
1187
- * Stores both the mount path and config hash in the file.
1188
- */
1189
- async writeMarkerFile(mountPath) {
1190
- if (!this._sandbox) return;
1191
- const markerContent = this.mounts.getMarkerContent(mountPath);
1192
- if (!markerContent) return;
1193
- const filename = this.mounts.markerFilename(mountPath);
1194
- const markerPath = `/tmp/.mastra-mounts/${filename}`;
1195
- try {
1196
- await this._sandbox.commands.run("mkdir -p /tmp/.mastra-mounts");
1197
- await this._sandbox.files.write(markerPath, markerContent);
1198
- } catch {
1199
- this.logger.debug(`${LOG_PREFIX} Warning: Could not write marker file at ${markerPath}`);
1200
- }
1201
- }
1202
- /**
1203
- * Check if a path is already mounted and if the config matches.
1204
- */
1205
- async checkExistingMount(mountPath, newConfig) {
1206
- if (!this._sandbox) throw new workspace.SandboxNotReadyError(this.id);
1207
- const mountCheck = await this._sandbox.commands.run(
1208
- `mountpoint -q "${mountPath}" && echo "mounted" || echo "not mounted"`
1209
- );
1210
- if (mountCheck.stdout.trim() !== "mounted") {
1211
- return "not_mounted";
1212
- }
1213
- const filename = this.mounts.markerFilename(mountPath);
1214
- const markerPath = `/tmp/.mastra-mounts/${filename}`;
1215
- try {
1216
- const markerResult = await this._sandbox.commands.run(`cat "${markerPath}" 2>/dev/null || echo ""`);
1217
- const parsed = this.mounts.parseMarkerContent(markerResult.stdout.trim());
1218
- if (!parsed) {
1219
- return "mismatched";
1220
- }
1221
- const newConfigHash = this.mounts.computeConfigHash(newConfig);
1222
- this.logger.debug(
1223
- `${LOG_PREFIX} Marker check - stored hash: "${parsed.configHash}", new config hash: "${newConfigHash}"`
1224
- );
1225
- if (parsed.path === mountPath && parsed.configHash === newConfigHash) {
1226
- return "matching";
1227
- }
1228
- } catch {
1229
- }
1230
- return "mismatched";
1231
- }
1232
- /**
1233
- * Check if an error indicates the sandbox itself is dead/gone.
1234
- * Does NOT include code execution timeouts (those are the user's code taking too long).
1235
- * Does NOT include "port is not open" - that needs sandbox kill, not reconnect.
1236
- */
1237
- isSandboxDeadError(error) {
1238
- if (!error) return false;
1239
- const errorStr = String(error);
1240
- return errorStr.includes("sandbox was not found") || errorStr.includes("Sandbox is probably not running") || errorStr.includes("Sandbox not found") || errorStr.includes("sandbox has been killed");
1241
- }
1242
- /**
1243
- * Handle sandbox timeout by clearing the instance and resetting state.
1244
- *
1245
- * Bypasses the normal stop() lifecycle because the sandbox is already dead —
1246
- * we can't unmount filesystems or run cleanup commands. Instead we reset
1247
- * mount states to 'pending' so they get re-mounted when start() runs again.
1248
- */
1249
- handleSandboxTimeout() {
1250
- this._sandbox = null;
1251
- for (const [path, entry] of this.mounts.entries) {
1252
- if (entry.state === "mounted" || entry.state === "mounting") {
1253
- this.mounts.set(path, { state: "pending" });
1254
- }
1255
- }
1256
- this.status = "stopped";
1257
- }
1258
- /**
1259
- * Execute an operation with automatic retry if the sandbox is found to be dead.
1260
- *
1261
- * When the E2B sandbox times out or crashes mid-operation, this method
1262
- * resets sandbox state, restarts it, and retries the operation once.
1263
- *
1264
- * @internal Used by E2BProcessManager to handle dead sandboxes during spawn.
1265
- */
1266
- async retryOnDead(fn) {
1267
- try {
1268
- return await fn();
1269
- } catch (error) {
1270
- if (this.isSandboxDeadError(error) && !this._isRetrying) {
1271
- this.handleSandboxTimeout();
1272
- this._isRetrying = true;
1273
- try {
1274
- await this.ensureRunning();
1275
- return await fn();
1276
- } finally {
1277
- this._isRetrying = false;
1278
- }
1279
- }
1280
- throw error;
1281
- }
1282
- }
574
+ /** Allowlist for marker filenames from ls output — e.g. "mount-abc123" */
575
+ const SAFE_MARKER_NAME = /^mount-[a-z0-9]+$/;
576
+ /**
577
+ * Simplified E2B sandbox implementation.
578
+ *
579
+ * Features:
580
+ * - Single sandbox instance lifecycle
581
+ * - Supports mounting cloud filesystems (S3, GCS, R2) via FUSE
582
+ * - Automatic sandbox timeout handling with retry
583
+ *
584
+ * @example Basic usage
585
+ * ```typescript
586
+ * import { Workspace } from '@mastra/core/workspace';
587
+ * import { E2BSandbox } from '@mastra/e2b';
588
+ *
589
+ * const sandbox = new E2BSandbox({
590
+ * timeout: 60000,
591
+ * });
592
+ *
593
+ * const workspace = new Workspace({ sandbox });
594
+ * const result = await workspace.executeCode('console.log("Hello!")');
595
+ * ```
596
+ *
597
+ * @example With S3 filesystem mounting
598
+ * ```typescript
599
+ * import { Workspace } from '@mastra/core/workspace';
600
+ * import { E2BSandbox } from '@mastra/e2b';
601
+ * import { S3Filesystem } from '@mastra/s3';
602
+ *
603
+ * const workspace = new Workspace({
604
+ * mounts: {
605
+ * '/bucket': new S3Filesystem({
606
+ * bucket: 'my-bucket',
607
+ * region: 'us-east-1',
608
+ * }),
609
+ * },
610
+ * sandbox: new E2BSandbox({ timeout: 60000 }),
611
+ * });
612
+ *
613
+ * ```
614
+ */
615
+ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
616
+ id;
617
+ name = "E2BSandbox";
618
+ provider = "e2b";
619
+ status = "pending";
620
+ /**
621
+ * Networking capability: public HTTPS URLs for sandbox ports.
622
+ * E2B exposes every port via `getHost(port)` — no upfront declaration needed.
623
+ *
624
+ * When not attached in this process, the URL is resolved by looking up the
625
+ * existing sandbox by identity (without resuming it) and deriving the host
626
+ * (`{port}-{sandboxId}.{domain}`), so other processes can resolve
627
+ * deployments without waking a paused sandbox.
628
+ */
629
+ networking = { getPortUrl: async (port) => {
630
+ try {
631
+ if (this._sandbox) return `https://${this._sandbox.getHost(port)}`;
632
+ const info = await this.lookupExistingSandboxInfo();
633
+ if (!info) return null;
634
+ return `https://${port}-${info.sandboxId}.${this.sandboxDomain}`;
635
+ } catch {
636
+ return null;
637
+ }
638
+ } };
639
+ _sandbox = null;
640
+ _createdAt = null;
641
+ _isRetrying = false;
642
+ timeout;
643
+ templateSpec;
644
+ env;
645
+ metadata;
646
+ network;
647
+ connectionOpts;
648
+ _instructionsOverride;
649
+ _constructorOptions;
650
+ /** Resolved template ID after building (if needed) */
651
+ _resolvedTemplateId;
652
+ /** Promise for template preparation (started in constructor) */
653
+ _templatePreparePromise;
654
+ constructor(options = {}) {
655
+ super({
656
+ ...options,
657
+ name: "E2BSandbox",
658
+ processes: new E2BProcessManager({ env: options.env ?? {} })
659
+ });
660
+ this.id = options.id ?? this.generateId();
661
+ this.timeout = options.timeout ?? 3e5;
662
+ this.templateSpec = options.template;
663
+ this.env = options.env ?? {};
664
+ this.metadata = options.metadata ?? {};
665
+ this.network = options.network;
666
+ this.connectionOpts = {
667
+ ...options.domain && { domain: options.domain },
668
+ ...options.apiUrl && { apiUrl: options.apiUrl },
669
+ ...options.apiKey && { apiKey: options.apiKey },
670
+ ...options.accessToken && { accessToken: options.accessToken }
671
+ };
672
+ this._instructionsOverride = options.instructions;
673
+ this._constructorOptions = { ...options };
674
+ this._templatePreparePromise = this.resolveTemplate().catch((err) => {
675
+ this.logger.debug(`${LOG_PREFIX} Template preparation error (will retry on start):`, err);
676
+ return "";
677
+ });
678
+ }
679
+ /**
680
+ * Construct a sibling `E2BSandbox` that inherits this sandbox's
681
+ * configuration (credentials, template, network, metadata, instructions)
682
+ * with per-instance overrides.
683
+ *
684
+ * Performs no I/O — the sandbox clone provisions (or reconnects to an
685
+ * existing E2B sandbox with the same logical `id`) on its own `start()`.
686
+ * Use it when one configured sandbox acts as the template for a fleet of
687
+ * independent sandboxes (e.g. one per project).
688
+ *
689
+ * `options.idleTimeoutMinutes` maps to the E2B sandbox `timeout` (ms);
690
+ * `options.sandboxId` is ignored because E2B reconnects by logical `id`.
691
+ */
692
+ clone(options = {}) {
693
+ const { id: _id, ...base } = this._constructorOptions;
694
+ return new E2BSandbox({
695
+ ...base,
696
+ ...options.id !== void 0 && { id: options.id },
697
+ ...options.env !== void 0 && { env: options.env },
698
+ ...options.idleTimeoutMinutes !== void 0 && { timeout: options.idleTimeoutMinutes * 6e4 }
699
+ });
700
+ }
701
+ /**
702
+ * Get the underlying E2B Sandbox instance for direct access to E2B APIs.
703
+ *
704
+ * Use this when you need to access E2B features not exposed through the
705
+ * WorkspaceSandbox interface (e.g., files API, ports, etc.).
706
+ *
707
+ * @throws {SandboxNotReadyError} If the sandbox has not been started
708
+ *
709
+ * @example Direct file operations
710
+ * ```typescript
711
+ * const e2b = sandbox.e2b;
712
+ * await e2b.files.write('/tmp/test.txt', 'Hello');
713
+ * const content = await e2b.files.read('/tmp/test.txt');
714
+ * const files = await e2b.files.list('/tmp');
715
+ * ```
716
+ *
717
+ * @example Access ports
718
+ * ```typescript
719
+ * const e2b = sandbox.e2b;
720
+ * const url = e2b.getHost(3000);
721
+ * ```
722
+ */
723
+ get e2b() {
724
+ if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
725
+ return this._sandbox;
726
+ }
727
+ /**
728
+ * Start the E2B sandbox.
729
+ * Handles template preparation, existing sandbox reconnection, and new sandbox creation.
730
+ *
731
+ * Status management and mount processing are handled by the base class.
732
+ */
733
+ async start() {
734
+ if (this._sandbox) return;
735
+ const [existingSandbox, templateId] = await Promise.all([this.findExistingSandbox(), this._templatePreparePromise || this.resolveTemplate()]);
736
+ if (existingSandbox) {
737
+ this._sandbox = existingSandbox;
738
+ this._createdAt = /* @__PURE__ */ new Date();
739
+ this.logger.debug(`${LOG_PREFIX} Reconnected to existing sandbox for: ${this.id}`);
740
+ const expectedPaths = Array.from(this.mounts.entries.keys());
741
+ this.logger.debug(`${LOG_PREFIX} Running mount reconciliation...`);
742
+ await this.reconcileMounts(expectedPaths);
743
+ this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
744
+ return;
745
+ }
746
+ let resolvedTemplateId = templateId;
747
+ if (!resolvedTemplateId) {
748
+ this.logger.debug(`${LOG_PREFIX} Template preparation failed earlier, retrying...`);
749
+ resolvedTemplateId = await this.resolveTemplate();
750
+ }
751
+ this.logger.debug(`${LOG_PREFIX} Creating new sandbox for: ${this.id} with template: ${resolvedTemplateId}`);
752
+ try {
753
+ this._sandbox = await e2b.Sandbox.create(resolvedTemplateId, {
754
+ ...this.connectionOpts,
755
+ lifecycle: { onTimeout: "pause" },
756
+ metadata: {
757
+ ...this.metadata,
758
+ "mastra-sandbox-id": this.id
759
+ },
760
+ ...this.network && { network: this.network },
761
+ timeoutMs: this.timeout
762
+ });
763
+ } catch (createError) {
764
+ const errorStr = String(createError);
765
+ if (errorStr.includes("404") && errorStr.includes("not found") && !this.templateSpec) {
766
+ this.logger.debug(`${LOG_PREFIX} Template not found, rebuilding: ${templateId}`);
767
+ this._resolvedTemplateId = void 0;
768
+ const rebuiltTemplateId = await this.buildDefaultTemplate();
769
+ this.logger.debug(`${LOG_PREFIX} Retrying sandbox creation with rebuilt template: ${rebuiltTemplateId}`);
770
+ this._sandbox = await e2b.Sandbox.create(rebuiltTemplateId, {
771
+ ...this.connectionOpts,
772
+ lifecycle: { onTimeout: "pause" },
773
+ metadata: {
774
+ ...this.metadata,
775
+ "mastra-sandbox-id": this.id
776
+ },
777
+ ...this.network && { network: this.network },
778
+ timeoutMs: this.timeout
779
+ });
780
+ } else throw createError;
781
+ }
782
+ this.logger.debug(`${LOG_PREFIX} Created sandbox ${this._sandbox.sandboxId} for logical ID: ${this.id}`);
783
+ this._createdAt = /* @__PURE__ */ new Date();
784
+ }
785
+ /**
786
+ * Stop the E2B sandbox by pausing it (snapshot-stop).
787
+ *
788
+ * Pausing freezes the whole VM — filesystem, memory, and running processes —
789
+ * and stops billing immediately. The next `start()` reconnects and resumes it,
790
+ * with background processes still running. Filesystem mounts are unmounted
791
+ * first (FUSE mounts don't survive pause) and reconciled again on start.
792
+ *
793
+ * Status management is handled by the base class.
794
+ */
795
+ async stop() {
796
+ for (const mountPath of [...this.mounts.entries.keys()]) try {
797
+ await this.unmount(mountPath);
798
+ } catch {}
799
+ if (this._sandbox) {
800
+ await this._sandbox.pause();
801
+ this.logger.debug(`${LOG_PREFIX} Paused sandbox ${this._sandbox.sandboxId} for: ${this.id}`);
802
+ } else {
803
+ const info = await this.lookupExistingSandboxInfo();
804
+ if (info?.state === "running") {
805
+ await e2b.Sandbox.pause(info.sandboxId, this.connectionOpts);
806
+ this.logger.debug(`${LOG_PREFIX} Paused detached sandbox ${info.sandboxId} for: ${this.id}`);
807
+ }
808
+ }
809
+ this._sandbox = null;
810
+ }
811
+ /**
812
+ * Destroy the E2B sandbox and clean up all resources.
813
+ * Unmounts filesystems, kills the sandbox, and clears mount state.
814
+ * Status management is handled by the base class.
815
+ */
816
+ async destroy() {
817
+ if (this._sandbox) {
818
+ try {
819
+ const procs = await this.processes.list();
820
+ await Promise.all(procs.map((p) => this.processes.kill(p.pid)));
821
+ } catch {}
822
+ for (const mountPath of [...this.mounts.entries.keys()]) try {
823
+ await this.unmount(mountPath);
824
+ } catch {}
825
+ await this._sandbox.kill();
826
+ this._sandbox = null;
827
+ } else {
828
+ const info = await this.lookupExistingSandboxInfo();
829
+ if (info) {
830
+ await e2b.Sandbox.kill(info.sandboxId, this.connectionOpts);
831
+ this.logger.debug(`${LOG_PREFIX} Killed detached sandbox ${info.sandboxId} for: ${this.id}`);
832
+ }
833
+ }
834
+ this.mounts.clear();
835
+ }
836
+ async getInfo() {
837
+ return {
838
+ id: this.id,
839
+ name: this.name,
840
+ provider: this.provider,
841
+ status: this.status,
842
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
843
+ mounts: Array.from(this.mounts.entries).map(([path, entry]) => ({
844
+ path,
845
+ filesystem: entry.filesystem?.provider ?? entry.config?.type ?? "unknown"
846
+ })),
847
+ metadata: { ...this.metadata }
848
+ };
849
+ }
850
+ /**
851
+ * Bulk-write files into the sandbox filesystem via the SDK's native upload.
852
+ */
853
+ async writeFiles(files) {
854
+ await this.ensureRunning();
855
+ await this.e2b.files.write(files.map((f) => ({
856
+ path: f.path,
857
+ data: typeof f.content === "string" ? f.content : new Blob([new Uint8Array(f.content)])
858
+ })));
859
+ }
860
+ /**
861
+ * Get instructions describing this E2B sandbox.
862
+ * Used by agents to understand the execution environment.
863
+ */
864
+ getInstructions(opts) {
865
+ if (this._instructionsOverride === void 0) return this._getDefaultInstructions();
866
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
867
+ const defaultInstructions = this._getDefaultInstructions();
868
+ return this._instructionsOverride({
869
+ defaultInstructions,
870
+ requestContext: opts?.requestContext
871
+ });
872
+ }
873
+ _getDefaultInstructions() {
874
+ const mountCount = this.mounts.entries.size;
875
+ return `Cloud sandbox.${mountCount > 0 ? ` ${mountCount} filesystem(s) mounted via FUSE.` : ""}`;
876
+ }
877
+ /**
878
+ * Mount a filesystem at a path in the sandbox.
879
+ * Uses FUSE tools (s3fs, gcsfuse) to mount cloud storage.
880
+ */
881
+ async mount(filesystem, mountPath) {
882
+ validateMountPath(mountPath);
883
+ if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
884
+ this.logger.debug(`${LOG_PREFIX} Mounting "${mountPath}"...`);
885
+ const config = filesystem.getMountConfig?.();
886
+ if (!config) {
887
+ const error = `Filesystem "${filesystem.id}" does not provide a mount config`;
888
+ this.logger.error(`${LOG_PREFIX} ${error}`);
889
+ this.mounts.set(mountPath, {
890
+ filesystem,
891
+ state: "error",
892
+ error
893
+ });
894
+ return {
895
+ success: false,
896
+ mountPath,
897
+ error
898
+ };
899
+ }
900
+ const existingMount = await this.checkExistingMount(mountPath, config);
901
+ if (existingMount === "matching") {
902
+ this.logger.debug(`${LOG_PREFIX} Detected existing mount for ${filesystem.provider} ("${filesystem.id}") at "${mountPath}" with correct config, skipping`);
903
+ this.mounts.set(mountPath, {
904
+ state: "mounted",
905
+ config
906
+ });
907
+ return {
908
+ success: true,
909
+ mountPath
910
+ };
911
+ } else if (existingMount === "mismatched") {
912
+ this.logger.debug(`${LOG_PREFIX} Config mismatch, unmounting to re-mount with new config...`);
913
+ await this.unmount(mountPath);
914
+ }
915
+ this.logger.debug(`${LOG_PREFIX} Config type: ${config.type}`);
916
+ this.mounts.set(mountPath, {
917
+ filesystem,
918
+ state: "mounting",
919
+ config
920
+ });
921
+ try {
922
+ if ((await this._sandbox.commands.run(`[ -d "${mountPath}" ] && [ "$(ls -A "${mountPath}" 2>/dev/null)" ] && echo "non-empty" || echo "ok"`)).stdout.trim() === "non-empty") {
923
+ const error = `Cannot mount at ${mountPath}: directory exists and is not empty. Mounting would hide existing files. Use a different path or empty the directory first.`;
924
+ this.logger.error(`${LOG_PREFIX} ${error}`);
925
+ this.mounts.set(mountPath, {
926
+ filesystem,
927
+ state: "error",
928
+ config,
929
+ error
930
+ });
931
+ return {
932
+ success: false,
933
+ mountPath,
934
+ error
935
+ };
936
+ }
937
+ } catch {}
938
+ try {
939
+ this.logger.debug(`${LOG_PREFIX} Creating mount directory for ${mountPath}...`);
940
+ const mkdirCommand = `sudo mkdir -p "${mountPath}" && sudo chown $(id -u):$(id -g) "${mountPath}"`;
941
+ this.logger.debug(`${LOG_PREFIX} Running command: ${mkdirCommand}`);
942
+ const mkdirResult = await this._sandbox.commands.run(mkdirCommand);
943
+ this.logger.debug(`${LOG_PREFIX} Created mount directory for mount path "${mountPath}":`, mkdirResult);
944
+ } catch (mkdirError) {
945
+ this.logger.debug(`${LOG_PREFIX} mkdir error for "${mountPath}":`, mkdirError);
946
+ this.mounts.set(mountPath, {
947
+ filesystem,
948
+ state: "error",
949
+ config,
950
+ error: String(mkdirError)
951
+ });
952
+ return {
953
+ success: false,
954
+ mountPath,
955
+ error: String(mkdirError)
956
+ };
957
+ }
958
+ const mountCtx = {
959
+ sandbox: this._sandbox,
960
+ logger: this.logger
961
+ };
962
+ try {
963
+ switch (config.type) {
964
+ case "s3":
965
+ this.logger.debug(`${LOG_PREFIX} Mounting S3 bucket at ${mountPath}...`);
966
+ await mountS3(mountPath, config, mountCtx);
967
+ this.logger.debug(`${LOG_PREFIX} Mounted S3 bucket at ${mountPath}`);
968
+ break;
969
+ case "gcs":
970
+ this.logger.debug(`${LOG_PREFIX} Mounting GCS bucket at ${mountPath}...`);
971
+ await mountGCS(mountPath, config, mountCtx);
972
+ this.logger.debug(`${LOG_PREFIX} Mounted GCS bucket at ${mountPath}`);
973
+ break;
974
+ case "azure-blob":
975
+ this.logger.debug(`${LOG_PREFIX} Mounting Azure Blob container at ${mountPath}...`);
976
+ await mountAzure(mountPath, config, mountCtx);
977
+ this.logger.debug(`${LOG_PREFIX} Mounted Azure Blob container at ${mountPath}`);
978
+ break;
979
+ default:
980
+ this.mounts.set(mountPath, {
981
+ filesystem,
982
+ state: "unsupported",
983
+ config,
984
+ error: `Unsupported mount type: ${config.type}`
985
+ });
986
+ return {
987
+ success: false,
988
+ mountPath,
989
+ error: `Unsupported mount type: ${config.type}`
990
+ };
991
+ }
992
+ } catch (error) {
993
+ this.logger.error(`${LOG_PREFIX} Error mounting "${filesystem.provider}" (${filesystem.id}) at "${mountPath}":`, error);
994
+ this.mounts.set(mountPath, {
995
+ filesystem,
996
+ state: "error",
997
+ config,
998
+ error: String(error)
999
+ });
1000
+ try {
1001
+ await this._sandbox.commands.run(`sudo rmdir "${mountPath}" 2>/dev/null || true`);
1002
+ this.logger.debug(`${LOG_PREFIX} Cleaned up directory after failed mount: ${mountPath}`);
1003
+ } catch {}
1004
+ return {
1005
+ success: false,
1006
+ mountPath,
1007
+ error: String(error)
1008
+ };
1009
+ }
1010
+ this.mounts.set(mountPath, {
1011
+ state: "mounted",
1012
+ config
1013
+ });
1014
+ await this.writeMarkerFile(mountPath);
1015
+ this.logger.debug(`${LOG_PREFIX} Mounted ${mountPath}`);
1016
+ return {
1017
+ success: true,
1018
+ mountPath
1019
+ };
1020
+ }
1021
+ /**
1022
+ * Unmount a filesystem from a path in the sandbox.
1023
+ */
1024
+ async unmount(mountPath) {
1025
+ validateMountPath(mountPath);
1026
+ if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
1027
+ this.logger.debug(`${LOG_PREFIX} Unmounting ${mountPath}...`);
1028
+ try {
1029
+ const result = await this._sandbox.commands.run(`sudo fusermount -u "${mountPath}" 2>/dev/null || sudo umount "${mountPath}"`);
1030
+ if (result.exitCode !== 0) this.logger.debug(`${LOG_PREFIX} Unmount warning: ${result.stderr || result.stdout}`);
1031
+ } catch (error) {
1032
+ this.logger.debug(`${LOG_PREFIX} Unmount error:`, error);
1033
+ await this._sandbox.commands.run(`sudo umount -l "${mountPath}" 2>/dev/null || true`);
1034
+ }
1035
+ this.mounts.delete(mountPath);
1036
+ const markerPath = `/tmp/.mastra-mounts/${this.mounts.markerFilename(mountPath)}`;
1037
+ await this._sandbox.commands.run(`rm -f "${markerPath}" 2>/dev/null || true`);
1038
+ const rmdirResult = await this._sandbox.commands.run(`sudo rmdir "${mountPath}" 2>&1`);
1039
+ if (rmdirResult.exitCode === 0) this.logger.debug(`${LOG_PREFIX} Unmounted and removed ${mountPath}`);
1040
+ else this.logger.debug(`${LOG_PREFIX} Unmounted ${mountPath} (directory not removed: ${rmdirResult.stderr?.trim() || "not empty"})`);
1041
+ }
1042
+ /**
1043
+ * Unmount all stale mounts that are not in the expected mounts list.
1044
+ * Also cleans up orphaned directories and marker files from failed mount attempts.
1045
+ * Call this after reconnecting to an existing sandbox to clean up old mounts.
1046
+ */
1047
+ async reconcileMounts(expectedMountPaths) {
1048
+ if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
1049
+ this.logger.debug(`${LOG_PREFIX} Reconciling mounts. Expected paths:`, expectedMountPaths);
1050
+ const currentMounts = (await this._sandbox.commands.run(`grep -E 'fuse\\.(s3fs|gcsfuse|blobfuse2)' /proc/mounts | awk '{print $2}'`)).stdout.trim().split("\n").filter((p) => p.length > 0);
1051
+ this.logger.debug(`${LOG_PREFIX} Current FUSE mounts in sandbox:`, currentMounts);
1052
+ const markerFiles = (await this._sandbox.commands.run(`ls /tmp/.mastra-mounts/ 2>/dev/null || echo ""`)).stdout.trim().split("\n").filter((f) => f.length > 0 && SAFE_MARKER_NAME.test(f));
1053
+ const managedMountPaths = /* @__PURE__ */ new Map();
1054
+ for (const markerFile of markerFiles) {
1055
+ const markerResult = await this._sandbox.commands.run(`cat "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || echo ""`);
1056
+ const parsed = this.mounts.parseMarkerContent(markerResult.stdout.trim());
1057
+ if (parsed && SAFE_MOUNT_PATH.test(parsed.path)) managedMountPaths.set(parsed.path, markerFile);
1058
+ }
1059
+ const staleMounts = currentMounts.filter((path) => !expectedMountPaths.includes(path));
1060
+ for (const stalePath of staleMounts) if (managedMountPaths.has(stalePath)) {
1061
+ this.logger.debug(`${LOG_PREFIX} Found stale managed FUSE mount at ${stalePath}, unmounting...`);
1062
+ await this.unmount(stalePath);
1063
+ } else this.logger.debug(`${LOG_PREFIX} Found external FUSE mount at ${stalePath}, leaving untouched`);
1064
+ try {
1065
+ const expectedMarkerFiles = new Set(expectedMountPaths.map((p) => this.mounts.markerFilename(p)));
1066
+ const markerToPath = /* @__PURE__ */ new Map();
1067
+ for (const [path, file] of managedMountPaths) markerToPath.set(file, path);
1068
+ for (const markerFile of markerFiles) if (!expectedMarkerFiles.has(markerFile)) {
1069
+ const mountPath = markerToPath.get(markerFile);
1070
+ if (mountPath) {
1071
+ if (!currentMounts.includes(mountPath)) {
1072
+ this.logger.debug(`${LOG_PREFIX} Cleaning up orphaned marker and directory for ${mountPath}`);
1073
+ await this._sandbox.commands.run(`rm -f "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || true`);
1074
+ await this._sandbox.commands.run(`sudo rmdir "${mountPath}" 2>/dev/null || true`);
1075
+ }
1076
+ } else {
1077
+ this.logger.debug(`${LOG_PREFIX} Removing malformed marker file: ${markerFile}`);
1078
+ await this._sandbox.commands.run(`rm -f "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || true`);
1079
+ }
1080
+ }
1081
+ } catch {
1082
+ this.logger.debug(`${LOG_PREFIX} Error during orphan cleanup (non-fatal)`);
1083
+ }
1084
+ }
1085
+ /** @deprecated Use `e2b` instead. */
1086
+ get instance() {
1087
+ return this.e2b;
1088
+ }
1089
+ /** @deprecated Use `status === 'running'` instead. */
1090
+ async isReady() {
1091
+ return this.status === "running" && this._sandbox !== null;
1092
+ }
1093
+ generateId() {
1094
+ return `e2b-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1095
+ }
1096
+ /** Domain used to derive public sandbox hosts (self-hosted E2B or e2b.app). */
1097
+ get sandboxDomain() {
1098
+ return this.connectionOpts.domain ?? process.env.E2B_DOMAIN ?? "e2b.app";
1099
+ }
1100
+ /**
1101
+ * Look up an existing sandbox with matching mastra-sandbox-id metadata
1102
+ * WITHOUT connecting or resuming it. Returns its list info or null.
1103
+ */
1104
+ async lookupExistingSandboxInfo() {
1105
+ try {
1106
+ const sandboxes = await e2b.Sandbox.list({
1107
+ ...this.connectionOpts,
1108
+ query: {
1109
+ metadata: { "mastra-sandbox-id": this.id },
1110
+ state: ["running", "paused"]
1111
+ }
1112
+ }).nextItems();
1113
+ this.logger.debug(`${LOG_PREFIX} sandboxes:`, sandboxes);
1114
+ if (sandboxes.length > 0) {
1115
+ const existingSandbox = sandboxes[0];
1116
+ this.logger.debug(`${LOG_PREFIX} Found existing sandbox for ${this.id}: ${existingSandbox.sandboxId} (state: ${existingSandbox.state})`);
1117
+ return existingSandbox;
1118
+ }
1119
+ } catch (e) {
1120
+ this.logger.debug(`${LOG_PREFIX} Error querying for existing sandbox:`, e);
1121
+ }
1122
+ return null;
1123
+ }
1124
+ /**
1125
+ * Find an existing sandbox with matching mastra-sandbox-id metadata.
1126
+ * Returns the connected sandbox if found, null otherwise.
1127
+ * Connecting to a paused sandbox resumes it.
1128
+ */
1129
+ async findExistingSandbox() {
1130
+ const info = await this.lookupExistingSandboxInfo();
1131
+ if (!info) return null;
1132
+ try {
1133
+ return await e2b.Sandbox.connect(info.sandboxId, this.connectionOpts);
1134
+ } catch (e) {
1135
+ this.logger.debug(`${LOG_PREFIX} Error connecting to existing sandbox:`, e);
1136
+ return null;
1137
+ }
1138
+ }
1139
+ /**
1140
+ * Resolve the template specification to a template ID.
1141
+ *
1142
+ * - String: Use as-is (template ID)
1143
+ * - TemplateBuilder: Build and return the template ID
1144
+ * - Function: Apply to base mountable template, then build
1145
+ * - undefined: Use default mountable template (cached)
1146
+ */
1147
+ async resolveTemplate() {
1148
+ if (this._resolvedTemplateId) return this._resolvedTemplateId;
1149
+ if (!this.templateSpec) {
1150
+ const { template, id } = createDefaultMountableTemplate();
1151
+ if (await e2b.Template.exists(id, this.connectionOpts)) {
1152
+ this.logger.debug(`${LOG_PREFIX} Using cached mountable template: ${id}`);
1153
+ this._resolvedTemplateId = id;
1154
+ return id;
1155
+ }
1156
+ this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
1157
+ const buildResult = await e2b.Template.build(template, id, this.connectionOpts);
1158
+ this._resolvedTemplateId = buildResult.templateId;
1159
+ this.logger.debug(`${LOG_PREFIX} Template built and cached: ${buildResult.templateId}`);
1160
+ return buildResult.templateId;
1161
+ }
1162
+ if (typeof this.templateSpec === "string") {
1163
+ this._resolvedTemplateId = this.templateSpec;
1164
+ return this.templateSpec;
1165
+ }
1166
+ let template;
1167
+ let templateName;
1168
+ if (typeof this.templateSpec === "function") {
1169
+ const { template: baseTemplate } = createDefaultMountableTemplate();
1170
+ template = this.templateSpec(baseTemplate);
1171
+ templateName = `mastra-custom-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
1172
+ } else {
1173
+ template = this.templateSpec;
1174
+ templateName = `mastra-${this.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
1175
+ }
1176
+ this.logger.debug(`${LOG_PREFIX} Building custom template: ${templateName}...`);
1177
+ const buildResult = await e2b.Template.build(template, templateName, this.connectionOpts);
1178
+ this._resolvedTemplateId = buildResult.templateId;
1179
+ this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
1180
+ return buildResult.templateId;
1181
+ }
1182
+ /**
1183
+ * Build the default mountable template (bypasses exists check).
1184
+ */
1185
+ async buildDefaultTemplate() {
1186
+ const { template, id } = createDefaultMountableTemplate();
1187
+ this.logger.debug(`${LOG_PREFIX} Building default mountable template: ${id}...`);
1188
+ const buildResult = await e2b.Template.build(template, id, this.connectionOpts);
1189
+ this._resolvedTemplateId = buildResult.templateId;
1190
+ this.logger.debug(`${LOG_PREFIX} Template built: ${buildResult.templateId}`);
1191
+ return buildResult.templateId;
1192
+ }
1193
+ /**
1194
+ * Write marker file for detecting config changes on reconnect.
1195
+ * Stores both the mount path and config hash in the file.
1196
+ */
1197
+ async writeMarkerFile(mountPath) {
1198
+ if (!this._sandbox) return;
1199
+ const markerContent = this.mounts.getMarkerContent(mountPath);
1200
+ if (!markerContent) return;
1201
+ const markerPath = `/tmp/.mastra-mounts/${this.mounts.markerFilename(mountPath)}`;
1202
+ try {
1203
+ await this._sandbox.commands.run("mkdir -p /tmp/.mastra-mounts");
1204
+ await this._sandbox.files.write(markerPath, markerContent);
1205
+ } catch {
1206
+ this.logger.debug(`${LOG_PREFIX} Warning: Could not write marker file at ${markerPath}`);
1207
+ }
1208
+ }
1209
+ /**
1210
+ * Check if a path is already mounted and if the config matches.
1211
+ */
1212
+ async checkExistingMount(mountPath, newConfig) {
1213
+ if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
1214
+ if ((await this._sandbox.commands.run(`mountpoint -q "${mountPath}" && echo "mounted" || echo "not mounted"`)).stdout.trim() !== "mounted") return "not_mounted";
1215
+ const markerPath = `/tmp/.mastra-mounts/${this.mounts.markerFilename(mountPath)}`;
1216
+ try {
1217
+ const markerResult = await this._sandbox.commands.run(`cat "${markerPath}" 2>/dev/null || echo ""`);
1218
+ const parsed = this.mounts.parseMarkerContent(markerResult.stdout.trim());
1219
+ if (!parsed) return "mismatched";
1220
+ const newConfigHash = this.mounts.computeConfigHash(newConfig);
1221
+ this.logger.debug(`${LOG_PREFIX} Marker check - stored hash: "${parsed.configHash}", new config hash: "${newConfigHash}"`);
1222
+ if (parsed.path === mountPath && parsed.configHash === newConfigHash) return "matching";
1223
+ } catch {}
1224
+ return "mismatched";
1225
+ }
1226
+ /**
1227
+ * Check if an error indicates the sandbox itself is dead/gone.
1228
+ * Does NOT include code execution timeouts (those are the user's code taking too long).
1229
+ * Does NOT include "port is not open" - that needs sandbox kill, not reconnect.
1230
+ */
1231
+ isSandboxDeadError(error) {
1232
+ if (!error) return false;
1233
+ const errorStr = String(error);
1234
+ return errorStr.includes("sandbox was not found") || errorStr.includes("Sandbox is probably not running") || errorStr.includes("Sandbox not found") || errorStr.includes("sandbox has been killed");
1235
+ }
1236
+ /**
1237
+ * Handle sandbox timeout by clearing the instance and resetting state.
1238
+ *
1239
+ * Bypasses the normal stop() lifecycle because the sandbox is already dead —
1240
+ * we can't unmount filesystems or run cleanup commands. Instead we reset
1241
+ * mount states to 'pending' so they get re-mounted when start() runs again.
1242
+ */
1243
+ handleSandboxTimeout() {
1244
+ this._sandbox = null;
1245
+ for (const [path, entry] of this.mounts.entries) if (entry.state === "mounted" || entry.state === "mounting") this.mounts.set(path, { state: "pending" });
1246
+ this.status = "stopped";
1247
+ }
1248
+ /**
1249
+ * Execute an operation with automatic retry if the sandbox is found to be dead.
1250
+ *
1251
+ * When the E2B sandbox times out or crashes mid-operation, this method
1252
+ * resets sandbox state, restarts it, and retries the operation once.
1253
+ *
1254
+ * @internal Used by E2BProcessManager to handle dead sandboxes during spawn.
1255
+ */
1256
+ async retryOnDead(fn) {
1257
+ try {
1258
+ return await fn();
1259
+ } catch (error) {
1260
+ if (this.isSandboxDeadError(error) && !this._isRetrying) {
1261
+ this.handleSandboxTimeout();
1262
+ this._isRetrying = true;
1263
+ try {
1264
+ await this.ensureRunning();
1265
+ return await fn();
1266
+ } finally {
1267
+ this._isRetrying = false;
1268
+ }
1269
+ }
1270
+ throw error;
1271
+ }
1272
+ }
1283
1273
  };
1284
-
1285
- // src/provider.ts
1286
- var e2bSandboxProvider = {
1287
- id: "e2b",
1288
- name: "E2B Sandbox",
1289
- description: "Cloud sandbox powered by E2B",
1290
- configSchema: {
1291
- type: "object",
1292
- properties: {
1293
- template: { type: "string", description: "Sandbox template ID" },
1294
- timeout: { type: "number", description: "Execution timeout in milliseconds", default: 3e5 },
1295
- env: {
1296
- type: "object",
1297
- description: "Environment variables",
1298
- additionalProperties: { type: "string" }
1299
- },
1300
- metadata: {
1301
- type: "object",
1302
- description: "Custom metadata",
1303
- additionalProperties: true
1304
- },
1305
- domain: { type: "string", description: "Domain for self-hosted E2B" },
1306
- apiUrl: { type: "string", description: "API URL for self-hosted E2B" },
1307
- apiKey: { type: "string", description: "E2B API key" },
1308
- accessToken: { type: "string", description: "E2B access token" }
1309
- }
1310
- },
1311
- createSandbox: (config) => new E2BSandbox(config)
1274
+ //#endregion
1275
+ //#region src/provider.ts
1276
+ const e2bSandboxProvider = {
1277
+ id: "e2b",
1278
+ name: "E2B Sandbox",
1279
+ description: "Cloud sandbox powered by E2B",
1280
+ configSchema: {
1281
+ type: "object",
1282
+ properties: {
1283
+ template: {
1284
+ type: "string",
1285
+ description: "Sandbox template ID"
1286
+ },
1287
+ timeout: {
1288
+ type: "number",
1289
+ description: "Execution timeout in milliseconds",
1290
+ default: 3e5
1291
+ },
1292
+ env: {
1293
+ type: "object",
1294
+ description: "Environment variables",
1295
+ additionalProperties: { type: "string" }
1296
+ },
1297
+ metadata: {
1298
+ type: "object",
1299
+ description: "Custom metadata",
1300
+ additionalProperties: true
1301
+ },
1302
+ domain: {
1303
+ type: "string",
1304
+ description: "Domain for self-hosted E2B"
1305
+ },
1306
+ apiUrl: {
1307
+ type: "string",
1308
+ description: "API URL for self-hosted E2B"
1309
+ },
1310
+ apiKey: {
1311
+ type: "string",
1312
+ description: "E2B API key"
1313
+ },
1314
+ accessToken: {
1315
+ type: "string",
1316
+ description: "E2B access token"
1317
+ }
1318
+ }
1319
+ },
1320
+ createSandbox: (config) => new E2BSandbox(config)
1312
1321
  };
1313
- var SANDBOX_TMP = "/home/user/mastra-code-mode";
1314
- function sanitize(id) {
1315
- const cleaned = id.replace(/[^A-Za-z0-9_$]/g, "_");
1316
- return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
1317
- }
1322
+ //#endregion
1323
+ //#region src/code-mode/transport.ts
1324
+ /**
1325
+ * Code Mode — E2B transport
1326
+ *
1327
+ * The default {@link StdioCodeModeTransport} in `@mastra/core` writes the
1328
+ * runner/program files to the *host* tmpdir and spawns `node <hostPath>`. That
1329
+ * only works when the sandbox shares the host filesystem (e.g. `LocalSandbox`).
1330
+ * E2B runs the program in a remote micro-VM with its own filesystem, so the
1331
+ * host paths don't exist there and `node` exits immediately.
1332
+ *
1333
+ * `E2BCodeModeTransport` writes the runner/program *into* the sandbox via the
1334
+ * E2B files API and runs plain `node <runnerPath>` inside the VM. TypeScript is
1335
+ * stripped on the host with esbuild before upload, so it doesn't depend on the
1336
+ * sandbox's Node version (the core transport relies on
1337
+ * `node --experimental-strip-types`, which needs Node >= 22.6).
1338
+ *
1339
+ * The RPC frame protocol (host <-> runner) is unchanged: it reuses
1340
+ * `buildProgramModule`, `buildRunner`, and `FRAME_PREFIX` from
1341
+ * `@mastra/core/tools`.
1342
+ */
1343
+ /** Base directory inside the E2B sandbox where Code Mode programs are written. */
1344
+ const SANDBOX_TMP = "/home/user/mastra-code-mode";
1345
+ /**
1346
+ * Code Mode transport for {@link E2BSandbox}.
1347
+ *
1348
+ * Writes the generated program and runner into the sandbox filesystem, runs
1349
+ * `node` there, and bridges `external_*` RPC calls back to the host over the
1350
+ * process's stdout/stdin — the same frame protocol as the core stdio transport.
1351
+ *
1352
+ * @example
1353
+ * ```typescript
1354
+ * import { createCodeMode } from '@mastra/core/tools';
1355
+ * import { E2BSandbox, E2BCodeModeTransport } from '@mastra/e2b';
1356
+ *
1357
+ * const { tool, instructions } = createCodeMode(
1358
+ * { tools: { getWeather, getForecast }, sandbox: new E2BSandbox() },
1359
+ * new E2BCodeModeTransport(),
1360
+ * );
1361
+ * ```
1362
+ */
1318
1363
  var E2BCodeModeTransport = class {
1319
- async run(opts) {
1320
- const { sandbox, program, toolIds, dispatch, timeout, abortSignal, onExternalCall, onExternalResult } = opts;
1321
- if (!(sandbox instanceof E2BSandbox)) {
1322
- throw new Error("E2BCodeModeTransport requires an E2BSandbox");
1323
- }
1324
- if (!sandbox.processes) {
1325
- throw new Error("Sandbox has no process manager");
1326
- }
1327
- if (sandbox.status !== "running") {
1328
- await sandbox.start();
1329
- }
1330
- const e2b = sandbox.e2b;
1331
- const externals = toolIds.map((toolId) => ({ toolId, externalName: sanitize(toolId) }));
1332
- const allowList = new Set(toolIds);
1333
- const suffix = crypto.randomBytes(4).toString("hex");
1334
- const dir = `${SANDBOX_TMP}/${suffix}`;
1335
- const programPath = `${dir}/program-${suffix}.mjs`;
1336
- const runnerPath = `${dir}/runner-${suffix}.mjs`;
1337
- const programSource = esbuild.transformSync(tools.buildProgramModule(program), { loader: "ts", target: "es2022" }).code;
1338
- const runnerSource = tools.buildRunner({ programModule: `file://${programPath}`, externals });
1339
- const logs = [];
1340
- let stderr = "";
1341
- let done;
1342
- let stdoutBuffer = "";
1343
- let resolveDone;
1344
- const donePromise = new Promise((resolve) => {
1345
- resolveDone = resolve;
1346
- });
1347
- const notifyCall = (tool, args) => {
1348
- try {
1349
- onExternalCall?.(tool, args);
1350
- } catch {
1351
- }
1352
- };
1353
- const notifyResult = (tool, durationMs, error) => {
1354
- try {
1355
- onExternalResult?.(tool, durationMs, error);
1356
- } catch {
1357
- }
1358
- };
1359
- try {
1360
- await e2b.files.makeDir(dir);
1361
- await e2b.files.write(programPath, programSource);
1362
- await e2b.files.write(runnerPath, runnerSource);
1363
- let handle;
1364
- const respond = async (id, ok, result, error) => {
1365
- await handle.sendStdin(JSON.stringify({ type: "rpc-result", id, ok, result, error }) + "\n");
1366
- };
1367
- const serveRpc = async (id, tool, args) => {
1368
- const started = Date.now();
1369
- notifyCall(tool, args);
1370
- if (!allowList.has(tool)) {
1371
- notifyResult(tool, Date.now() - started, new Error("not allowed"));
1372
- await respond(id, false, void 0, {
1373
- message: `Tool "${tool}" is not available in Code Mode`,
1374
- name: "NotAllowedError"
1375
- });
1376
- return;
1377
- }
1378
- try {
1379
- const result = await dispatch(tool, args);
1380
- notifyResult(tool, Date.now() - started);
1381
- await respond(id, true, result);
1382
- } catch (error) {
1383
- const err = error;
1384
- notifyResult(tool, Date.now() - started, error instanceof Error ? error : new Error(String(error)));
1385
- await respond(id, false, void 0, {
1386
- message: err?.message ?? String(error),
1387
- name: err?.name
1388
- });
1389
- }
1390
- };
1391
- const handleFrame = (frame) => {
1392
- switch (frame.type) {
1393
- case "log":
1394
- logs.push(frame.message);
1395
- return;
1396
- case "done":
1397
- done = frame.ok ? { success: true, result: frame.result, logs } : { success: false, error: frame.error, logs };
1398
- resolveDone();
1399
- return;
1400
- case "rpc":
1401
- void serveRpc(frame.id, frame.tool, frame.args).catch(() => {
1402
- });
1403
- return;
1404
- }
1405
- };
1406
- handle = await sandbox.processes.spawn(`node ${runnerPath}`, {
1407
- cwd: dir,
1408
- abortSignal,
1409
- // E2B failures are otherwise silent, which makes them painful to debug.
1410
- // Capture stderr and surface it in Timeout/NoResult errors below.
1411
- onStderr: (chunk) => {
1412
- stderr += chunk;
1413
- },
1414
- onStdout: (chunk) => {
1415
- stdoutBuffer += chunk;
1416
- let idx;
1417
- while ((idx = stdoutBuffer.indexOf("\n")) >= 0) {
1418
- const line = stdoutBuffer.slice(0, idx);
1419
- stdoutBuffer = stdoutBuffer.slice(idx + 1);
1420
- if (!line.startsWith(tools.FRAME_PREFIX)) continue;
1421
- let frame;
1422
- try {
1423
- frame = JSON.parse(line.slice(tools.FRAME_PREFIX.length));
1424
- } catch {
1425
- continue;
1426
- }
1427
- handleFrame(frame);
1428
- }
1429
- }
1430
- });
1431
- let timer;
1432
- const timeoutPromise = new Promise((resolve) => {
1433
- timer = setTimeout(() => resolve("timeout"), timeout);
1434
- });
1435
- const exitPromise = handle.wait().then(() => "exited");
1436
- const outcome = await Promise.race([
1437
- donePromise.then(() => "done"),
1438
- exitPromise.catch(() => "exited"),
1439
- timeoutPromise
1440
- ]);
1441
- if (timer) clearTimeout(timer);
1442
- if (outcome === "timeout") {
1443
- await handle.kill().catch(() => {
1444
- });
1445
- return {
1446
- success: false,
1447
- logs,
1448
- error: {
1449
- message: `Code Mode execution timed out after ${timeout}ms${stderr ? `
1450
- stderr: ${stderr}` : ""}`,
1451
- name: "TimeoutError"
1452
- }
1453
- };
1454
- }
1455
- if (!done) {
1456
- await exitPromise.catch(() => {
1457
- });
1458
- }
1459
- return done ?? {
1460
- success: false,
1461
- logs,
1462
- error: {
1463
- message: `Program exited without returning a result${stderr ? `
1464
- stderr: ${stderr}` : ""}`,
1465
- name: "NoResultError"
1466
- }
1467
- };
1468
- } finally {
1469
- await e2b.files.remove(dir).catch(() => {
1470
- });
1471
- }
1472
- }
1364
+ async run(opts) {
1365
+ const { sandbox, program, toolIds, dispatch, timeout, abortSignal, onExternalCall, onExternalResult } = opts;
1366
+ if (!(sandbox instanceof E2BSandbox)) throw new Error("E2BCodeModeTransport requires an E2BSandbox");
1367
+ if (!sandbox.processes) throw new Error("Sandbox has no process manager");
1368
+ if (sandbox.status !== "running") await sandbox.start();
1369
+ const e2b = sandbox.e2b;
1370
+ const externals = toolIds.map((toolId) => ({
1371
+ toolId,
1372
+ externalName: (0, _mastra_core_tools.sanitizeToolId)(toolId)
1373
+ }));
1374
+ const allowList = new Set(toolIds);
1375
+ const suffix = (0, crypto.randomBytes)(4).toString("hex");
1376
+ const dir = `${SANDBOX_TMP}/${suffix}`;
1377
+ const programPath = `${dir}/program-${suffix}.mjs`;
1378
+ const runnerPath = `${dir}/runner-${suffix}.mjs`;
1379
+ const programSource = (0, esbuild.transformSync)((0, _mastra_core_tools.buildProgramModule)(program), {
1380
+ loader: "ts",
1381
+ target: "es2022"
1382
+ }).code;
1383
+ const runnerSource = (0, _mastra_core_tools.buildRunner)({
1384
+ programModule: `file://${programPath}`,
1385
+ externals
1386
+ });
1387
+ const logs = [];
1388
+ let stderr = "";
1389
+ let done;
1390
+ let stdoutBuffer = "";
1391
+ let resolveDone;
1392
+ const donePromise = new Promise((resolve) => {
1393
+ resolveDone = resolve;
1394
+ });
1395
+ const notifyCall = (tool, args) => {
1396
+ try {
1397
+ onExternalCall?.(tool, args);
1398
+ } catch {}
1399
+ };
1400
+ const notifyResult = (tool, durationMs, error) => {
1401
+ try {
1402
+ onExternalResult?.(tool, durationMs, error);
1403
+ } catch {}
1404
+ };
1405
+ try {
1406
+ await e2b.files.makeDir(dir);
1407
+ await e2b.files.write(programPath, programSource);
1408
+ await e2b.files.write(runnerPath, runnerSource);
1409
+ let handle;
1410
+ const respond = async (id, ok, result, error) => {
1411
+ await handle.sendStdin(JSON.stringify({
1412
+ type: "rpc-result",
1413
+ id,
1414
+ ok,
1415
+ result,
1416
+ error
1417
+ }) + "\n");
1418
+ };
1419
+ const serveRpc = async (id, tool, args) => {
1420
+ const started = Date.now();
1421
+ notifyCall(tool, args);
1422
+ if (!allowList.has(tool)) {
1423
+ notifyResult(tool, Date.now() - started, /* @__PURE__ */ new Error("not allowed"));
1424
+ await respond(id, false, void 0, {
1425
+ message: `Tool "${tool}" is not available in Code Mode`,
1426
+ name: "NotAllowedError"
1427
+ });
1428
+ return;
1429
+ }
1430
+ try {
1431
+ const result = await dispatch(tool, args);
1432
+ notifyResult(tool, Date.now() - started);
1433
+ await respond(id, true, result);
1434
+ } catch (error) {
1435
+ const err = error;
1436
+ notifyResult(tool, Date.now() - started, error instanceof Error ? error : new Error(String(error)));
1437
+ await respond(id, false, void 0, {
1438
+ message: err?.message ?? String(error),
1439
+ name: err?.name
1440
+ });
1441
+ }
1442
+ };
1443
+ const handleFrame = (frame) => {
1444
+ switch (frame.type) {
1445
+ case "log":
1446
+ logs.push(frame.message);
1447
+ return;
1448
+ case "done":
1449
+ done = frame.ok ? {
1450
+ success: true,
1451
+ result: frame.result,
1452
+ logs
1453
+ } : {
1454
+ success: false,
1455
+ error: frame.error,
1456
+ logs
1457
+ };
1458
+ resolveDone();
1459
+ return;
1460
+ case "rpc":
1461
+ serveRpc(frame.id, frame.tool, frame.args).catch(() => {});
1462
+ return;
1463
+ }
1464
+ };
1465
+ handle = await sandbox.processes.spawn(`node ${runnerPath}`, {
1466
+ cwd: dir,
1467
+ abortSignal,
1468
+ onStderr: (chunk) => {
1469
+ stderr += chunk;
1470
+ },
1471
+ onStdout: (chunk) => {
1472
+ stdoutBuffer += chunk;
1473
+ let idx;
1474
+ while ((idx = stdoutBuffer.indexOf("\n")) >= 0) {
1475
+ const line = stdoutBuffer.slice(0, idx);
1476
+ stdoutBuffer = stdoutBuffer.slice(idx + 1);
1477
+ if (!line.startsWith(_mastra_core_tools.FRAME_PREFIX)) continue;
1478
+ let frame;
1479
+ try {
1480
+ frame = JSON.parse(line.slice(_mastra_core_tools.FRAME_PREFIX.length));
1481
+ } catch {
1482
+ continue;
1483
+ }
1484
+ handleFrame(frame);
1485
+ }
1486
+ }
1487
+ });
1488
+ let timer;
1489
+ const timeoutPromise = new Promise((resolve) => {
1490
+ timer = setTimeout(() => resolve("timeout"), timeout);
1491
+ });
1492
+ const exitPromise = handle.wait().then(() => "exited");
1493
+ const outcome = await Promise.race([
1494
+ donePromise.then(() => "done"),
1495
+ exitPromise.catch(() => "exited"),
1496
+ timeoutPromise
1497
+ ]);
1498
+ if (timer) clearTimeout(timer);
1499
+ if (outcome === "timeout") {
1500
+ await handle.kill().catch(() => {});
1501
+ return {
1502
+ success: false,
1503
+ logs,
1504
+ error: {
1505
+ message: `Code Mode execution timed out after ${timeout}ms${stderr ? `\nstderr: ${stderr}` : ""}`,
1506
+ name: "TimeoutError"
1507
+ }
1508
+ };
1509
+ }
1510
+ if (!done) await exitPromise.catch(() => {});
1511
+ return done ?? {
1512
+ success: false,
1513
+ logs,
1514
+ error: {
1515
+ message: `Program exited without returning a result${stderr ? `\nstderr: ${stderr}` : ""}`,
1516
+ name: "NoResultError"
1517
+ }
1518
+ };
1519
+ } finally {
1520
+ await e2b.files.remove(dir).catch(() => {});
1521
+ }
1522
+ }
1473
1523
  };
1474
-
1524
+ //#endregion
1475
1525
  exports.E2BCodeModeTransport = E2BCodeModeTransport;
1476
1526
  exports.E2BProcessManager = E2BProcessManager;
1477
1527
  exports.E2BSandbox = E2BSandbox;
1478
1528
  exports.createDefaultMountableTemplate = createDefaultMountableTemplate;
1479
1529
  exports.e2bSandboxProvider = e2bSandboxProvider;
1480
- //# sourceMappingURL=index.cjs.map
1530
+
1481
1531
  //# sourceMappingURL=index.cjs.map