@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/CHANGELOG.md +11 -0
- package/dist/code-mode/transport.d.ts.map +1 -1
- package/dist/index.cjs +1469 -1419
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1466 -1415
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
package/dist/index.cjs
CHANGED
|
@@ -1,94 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
try {
|
|
66
|
+
new URL(endpoint);
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error(`Invalid endpoint URL: "${endpoint}"`);
|
|
69
|
+
}
|
|
39
70
|
}
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
104
|
+
if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
|
|
105
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
68
106
|
}
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
269
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
255
270
|
}
|
|
256
271
|
function parseOsRelease(output) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
1530
|
+
|
|
1481
1531
|
//# sourceMappingURL=index.cjs.map
|