@mastra/daytona 0.6.0 → 0.7.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 +35 -0
- package/dist/index.cjs +1388 -1523
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1385 -1519
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +9 -0
- package/dist/sandbox/index.d.ts.map +1 -1
- package/package.json +12 -11
package/dist/index.cjs
CHANGED
|
@@ -1,1568 +1,1433 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// src/utils/compact.ts
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _daytonaio_sdk = require("@daytonaio/sdk");
|
|
3
|
+
let _mastra_core_workspace = require("@mastra/core/workspace");
|
|
4
|
+
let crypto = require("crypto");
|
|
5
|
+
//#region src/utils/compact.ts
|
|
6
|
+
/**
|
|
7
|
+
* Returns a shallow copy of the object with all undefined values removed.
|
|
8
|
+
*/
|
|
10
9
|
function compact(obj) {
|
|
11
|
-
|
|
10
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/utils/shell-quote.ts
|
|
14
|
+
/**
|
|
15
|
+
* Shell-quote a single argument for safe use in a command string.
|
|
16
|
+
*
|
|
17
|
+
* Arguments containing only safe characters are returned as-is.
|
|
18
|
+
* All others are wrapped in single quotes with embedded single quotes escaped.
|
|
19
|
+
*/
|
|
15
20
|
function shellQuote(arg) {
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
|
|
22
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
18
23
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/sandbox/mounts/types.ts
|
|
26
|
+
const LOG_PREFIX = "[@mastra/daytona]";
|
|
27
|
+
/**
|
|
28
|
+
* Validate a bucket name before interpolating into shell commands.
|
|
29
|
+
* Covers S3, GCS, and S3-compatible (R2, MinIO) naming rules.
|
|
30
|
+
*/
|
|
31
|
+
const SAFE_BUCKET_NAME = /^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$/;
|
|
23
32
|
function validateBucketName(bucket) {
|
|
24
|
-
|
|
25
|
-
throw new Error(
|
|
26
|
-
`Invalid bucket name: "${bucket}". Bucket names must be 3-63 characters, lowercase alphanumeric, hyphens, or dots.`
|
|
27
|
-
);
|
|
28
|
-
}
|
|
33
|
+
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.`);
|
|
29
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate an endpoint URL before interpolating into shell commands.
|
|
37
|
+
* Only http and https schemes are allowed.
|
|
38
|
+
*/
|
|
30
39
|
function validateEndpoint(endpoint) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw new Error(`Invalid endpoint URL scheme: "${parsed.protocol}". Only http: and https: are allowed.`);
|
|
39
|
-
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = new URL(endpoint);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(`Invalid endpoint URL: "${endpoint}"`);
|
|
45
|
+
}
|
|
46
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Invalid endpoint URL scheme: "${parsed.protocol}". Only http: and https: are allowed.`);
|
|
40
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate and normalize a mount prefix before interpolating into shell commands.
|
|
50
|
+
* Returns the normalized prefix (no leading/trailing slashes).
|
|
51
|
+
*
|
|
52
|
+
* Shell safety is handled by shellQuote() at the call site, so this function
|
|
53
|
+
* only enforces path-level rules (no traversal, no empty result, no control chars).
|
|
54
|
+
*/
|
|
41
55
|
function validatePrefix(prefix) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
throw new Error(`Invalid mount prefix: "${prefix}". Path traversal is not allowed.`);
|
|
50
|
-
}
|
|
51
|
-
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
52
|
-
throw new Error(`Invalid mount prefix: "${prefix}". Control characters are not allowed.`);
|
|
53
|
-
}
|
|
54
|
-
return normalized;
|
|
56
|
+
let normalized = prefix;
|
|
57
|
+
while (normalized.startsWith("/")) normalized = normalized.slice(1);
|
|
58
|
+
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
59
|
+
if (!normalized) throw new Error("Mount prefix cannot be empty after normalization.");
|
|
60
|
+
if (normalized.includes("//") || normalized.split("/").some((s) => s === "." || s === "..")) throw new Error(`Invalid mount prefix: "${prefix}". Path traversal is not allowed.`);
|
|
61
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) throw new Error(`Invalid mount prefix: "${prefix}". Control characters are not allowed.`);
|
|
62
|
+
return normalized;
|
|
55
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Run a command in the Daytona sandbox.
|
|
66
|
+
*
|
|
67
|
+
* Thin wrapper around `sandbox.process.executeCommand` that converts timeout
|
|
68
|
+
* from milliseconds to seconds and null-coalesces the output string.
|
|
69
|
+
*
|
|
70
|
+
* Does NOT throw on non-zero exit codes — callers should check `exitCode`.
|
|
71
|
+
*/
|
|
56
72
|
async function runCommand(sandbox, command, options) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// env
|
|
63
|
-
options?.timeout !== void 0 ? Math.ceil(options.timeout / 1e3) : void 0
|
|
64
|
-
);
|
|
65
|
-
return {
|
|
66
|
-
exitCode: result.exitCode,
|
|
67
|
-
output: result.result ?? ""
|
|
68
|
-
};
|
|
73
|
+
const result = await sandbox.process.executeCommand(command, void 0, void 0, options?.timeout !== void 0 ? Math.ceil(options.timeout / 1e3) : void 0);
|
|
74
|
+
return {
|
|
75
|
+
exitCode: result.exitCode,
|
|
76
|
+
output: result.result ?? ""
|
|
77
|
+
};
|
|
69
78
|
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/sandbox/mounts/s3.ts
|
|
81
|
+
/**
|
|
82
|
+
* Mount an S3 bucket using s3fs-fuse.
|
|
83
|
+
*/
|
|
70
84
|
async function mountS3(mountPath, config, ctx) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
logger.debug(`${LOG_PREFIX} No credentials provided, mounting as public bucket (read-only)`);
|
|
136
|
-
}
|
|
137
|
-
mountOptions.push("allow_other");
|
|
138
|
-
if (validUidGid) {
|
|
139
|
-
mountOptions.push(`uid=${uid}`, `gid=${gid}`);
|
|
140
|
-
}
|
|
141
|
-
if (config.endpoint) {
|
|
142
|
-
const endpoint = config.endpoint.replace(/\/$/, "");
|
|
143
|
-
mountOptions.push(`url=${shellQuote(endpoint)}`, "use_path_request_style", "sigv4", "nomultipart");
|
|
144
|
-
}
|
|
145
|
-
if (config.readOnly) {
|
|
146
|
-
mountOptions.push("ro");
|
|
147
|
-
logger.debug(`${LOG_PREFIX} Mounting as read-only`);
|
|
148
|
-
}
|
|
149
|
-
let bucketArg = config.bucket;
|
|
150
|
-
if (config.prefix) {
|
|
151
|
-
const normalizedPrefix = validatePrefix(config.prefix);
|
|
152
|
-
bucketArg = `${config.bucket}:/${normalizedPrefix}`;
|
|
153
|
-
}
|
|
154
|
-
const mountCmd = `s3fs ${shellQuote(bucketArg)} ${quotedMountPath} -o ${mountOptions.join(" -o ")}`;
|
|
155
|
-
logger.debug(`${LOG_PREFIX} Mounting S3:`, hasCredentials ? mountCmd.replace(credentialsPath, "***") : mountCmd);
|
|
156
|
-
const result = await run(mountCmd, 6e4);
|
|
157
|
-
logger.debug(`${LOG_PREFIX} s3fs result:`, {
|
|
158
|
-
exitCode: result.exitCode,
|
|
159
|
-
stdout: result.stdout,
|
|
160
|
-
stderr: result.stderr
|
|
161
|
-
});
|
|
162
|
-
if (result.exitCode !== 0) {
|
|
163
|
-
throw new Error(`Failed to mount S3 bucket: ${result.stderr || result.stdout}`);
|
|
164
|
-
}
|
|
85
|
+
const { run, writeFile, logger } = ctx;
|
|
86
|
+
validateBucketName(config.bucket);
|
|
87
|
+
if (config.endpoint) validateEndpoint(config.endpoint);
|
|
88
|
+
const quotedMountPath = shellQuote(mountPath);
|
|
89
|
+
const hasAccessKey = !!config.accessKeyId;
|
|
90
|
+
const hasSecretKey = !!config.secretAccessKey;
|
|
91
|
+
if (hasAccessKey !== hasSecretKey) throw new Error("Both accessKeyId and secretAccessKey must be provided together.");
|
|
92
|
+
const hasCredentials = hasAccessKey && hasSecretKey;
|
|
93
|
+
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.`);
|
|
94
|
+
if (config.endpoint) {
|
|
95
|
+
const endpoint = config.endpoint.replace(/\/$/, "");
|
|
96
|
+
const connectivityCheck = await run(`curl -sS --max-time 5 ${shellQuote(endpoint)} 2>&1`, 1e4);
|
|
97
|
+
const checkOutput = connectivityCheck.stdout.trim();
|
|
98
|
+
if (connectivityCheck.exitCode !== 0 || checkOutput.toLowerCase().includes("restricted") || checkOutput.toLowerCase().includes("blocked")) throw new Error(`Cannot reach ${endpoint} from this sandbox. S3-compatible storage mounting requires network access to the configured endpoint, which may be blocked on Daytona's restricted tiers. Upgrade to a tier with unrestricted internet access, or contact Daytona support to remove the network restriction.` + (checkOutput ? `\n\nSandbox network response: ${checkOutput}` : ""));
|
|
99
|
+
}
|
|
100
|
+
if ((await run("which s3fs 2>/dev/null || echo \"not found\"", 3e4)).stdout.includes("not found")) {
|
|
101
|
+
logger.warn(`${LOG_PREFIX} s3fs not found, attempting runtime installation...`);
|
|
102
|
+
logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install s3fs in your sandbox image`);
|
|
103
|
+
await run("sudo apt-get update -qq 2>&1", 6e4);
|
|
104
|
+
await run("sudo apt-get install -y s3fs fuse 2>&1 || sudo apt-get install -y s3fs-fuse fuse 2>&1 || true", 12e4);
|
|
105
|
+
if ((await run("which s3fs 2>/dev/null || echo \"not found\"", 3e4)).stdout.includes("not found")) throw new Error("Failed to install s3fs: binary not found after install attempt");
|
|
106
|
+
}
|
|
107
|
+
await run("sudo chmod u+s /usr/bin/fusermount3 /usr/bin/fusermount 2>/dev/null || true", 3e4);
|
|
108
|
+
const idResult = await run("id -u && id -g", 3e4);
|
|
109
|
+
const [uid, gid] = idResult.stdout.trim().split("\n");
|
|
110
|
+
const validUidGid = uid && gid && /^\d+$/.test(uid) && /^\d+$/.test(gid);
|
|
111
|
+
if (!validUidGid) logger.warn(`${LOG_PREFIX} Unexpected uid/gid format: "${idResult.stdout.trim()}" — mounted files will be owned by root`);
|
|
112
|
+
const credentialsPath = `/tmp/.passwd-s3fs-${(0, crypto.createHash)("md5").update(mountPath).digest("hex").slice(0, 8)}`;
|
|
113
|
+
await run("sudo chmod a+rw /dev/fuse 2>/dev/null || true; sudo bash -c 'grep -q \"^user_allow_other\" /etc/fuse.conf 2>/dev/null || echo \"user_allow_other\" >> /etc/fuse.conf' 2>/dev/null || true");
|
|
114
|
+
if (hasCredentials) {
|
|
115
|
+
await run(`sudo rm -f ${shellQuote(credentialsPath)}`, 3e4);
|
|
116
|
+
await writeFile(credentialsPath, `${config.accessKeyId}:${config.secretAccessKey}`);
|
|
117
|
+
await run(`chmod 600 ${shellQuote(credentialsPath)}`, 3e4);
|
|
118
|
+
}
|
|
119
|
+
const mountOptions = [];
|
|
120
|
+
if (hasCredentials) mountOptions.push(`passwd_file=${credentialsPath}`);
|
|
121
|
+
else {
|
|
122
|
+
mountOptions.push("public_bucket=1");
|
|
123
|
+
logger.debug(`${LOG_PREFIX} No credentials provided, mounting as public bucket (read-only)`);
|
|
124
|
+
}
|
|
125
|
+
mountOptions.push("allow_other");
|
|
126
|
+
if (validUidGid) mountOptions.push(`uid=${uid}`, `gid=${gid}`);
|
|
127
|
+
if (config.endpoint) {
|
|
128
|
+
const endpoint = config.endpoint.replace(/\/$/, "");
|
|
129
|
+
mountOptions.push(`url=${shellQuote(endpoint)}`, "use_path_request_style", "sigv4", "nomultipart");
|
|
130
|
+
}
|
|
131
|
+
if (config.readOnly) {
|
|
132
|
+
mountOptions.push("ro");
|
|
133
|
+
logger.debug(`${LOG_PREFIX} Mounting as read-only`);
|
|
134
|
+
}
|
|
135
|
+
let bucketArg = config.bucket;
|
|
136
|
+
if (config.prefix) {
|
|
137
|
+
const normalizedPrefix = validatePrefix(config.prefix);
|
|
138
|
+
bucketArg = `${config.bucket}:/${normalizedPrefix}`;
|
|
139
|
+
}
|
|
140
|
+
const mountCmd = `s3fs ${shellQuote(bucketArg)} ${quotedMountPath} -o ${mountOptions.join(" -o ")}`;
|
|
141
|
+
logger.debug(`${LOG_PREFIX} Mounting S3:`, hasCredentials ? mountCmd.replace(credentialsPath, "***") : mountCmd);
|
|
142
|
+
const result = await run(mountCmd, 6e4);
|
|
143
|
+
logger.debug(`${LOG_PREFIX} s3fs result:`, {
|
|
144
|
+
exitCode: result.exitCode,
|
|
145
|
+
stdout: result.stdout,
|
|
146
|
+
stderr: result.stderr
|
|
147
|
+
});
|
|
148
|
+
if (result.exitCode !== 0) throw new Error(`Failed to mount S3 bucket: ${result.stderr || result.stdout}`);
|
|
165
149
|
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/sandbox/mounts/gcs.ts
|
|
152
|
+
/**
|
|
153
|
+
* Mount a GCS bucket using gcsfuse.
|
|
154
|
+
*/
|
|
166
155
|
async function mountGCS(mountPath, config, ctx) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (verifyResult.stdout.includes("not found")) {
|
|
227
|
-
throw new Error(`Failed to install gcsfuse: ${installResult.stderr || installResult.stdout}`);
|
|
228
|
-
}
|
|
229
|
-
if (installResult.exitCode !== 0) {
|
|
230
|
-
logger.warn(
|
|
231
|
-
`${LOG_PREFIX} gcsfuse install reported dpkg errors (likely fuse post-install in container) but binary is present \u2014 proceeding`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const idResult = await run("id -u && id -g", 3e4);
|
|
236
|
-
const [uid, gid] = idResult.stdout.trim().split("\n");
|
|
237
|
-
const validUidGid = uid && gid && /^\d+$/.test(uid) && /^\d+$/.test(gid);
|
|
238
|
-
if (!validUidGid) {
|
|
239
|
-
logger.warn(
|
|
240
|
-
`${LOG_PREFIX} Unexpected uid/gid format: "${idResult.stdout.trim()}" \u2014 mounted files will be owned by root`
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
const uidGidFlags = validUidGid ? `--uid=${uid} --gid=${gid}` : "";
|
|
244
|
-
await run(
|
|
245
|
-
`sudo chmod a+rw /dev/fuse 2>/dev/null || true; sudo bash -c 'grep -q "^user_allow_other" /etc/fuse.conf 2>/dev/null || echo "user_allow_other" >> /etc/fuse.conf' 2>/dev/null || true`
|
|
246
|
-
);
|
|
247
|
-
let onlyDirFlag = "";
|
|
248
|
-
if (config.prefix) {
|
|
249
|
-
const normalizedPrefix = validatePrefix(config.prefix);
|
|
250
|
-
onlyDirFlag = `--only-dir=${shellQuote(normalizedPrefix)} `;
|
|
251
|
-
}
|
|
252
|
-
const hasCredentials = !!config.serviceAccountKey;
|
|
253
|
-
const implicitDirsFlag = "--implicit-dirs";
|
|
254
|
-
let mountCmd;
|
|
255
|
-
if (hasCredentials) {
|
|
256
|
-
const mountHash = crypto.createHash("md5").update(mountPath).digest("hex").slice(0, 8);
|
|
257
|
-
const keyPath = `/tmp/gcs-key-${mountHash}.json`;
|
|
258
|
-
await run(`sudo rm -f ${shellQuote(keyPath)}`, 3e4);
|
|
259
|
-
await writeFile(keyPath, config.serviceAccountKey);
|
|
260
|
-
await run(`chmod 600 ${shellQuote(keyPath)}`, 3e4);
|
|
261
|
-
mountCmd = `gcsfuse --key-file=${shellQuote(keyPath)} ${implicitDirsFlag} ${onlyDirFlag}-o allow_other ${uidGidFlags} ${shellQuote(config.bucket)} ${quotedMountPath}`;
|
|
262
|
-
} else {
|
|
263
|
-
logger.debug(`${LOG_PREFIX} No credentials provided, mounting GCS as public bucket (read-only)`);
|
|
264
|
-
mountCmd = `gcsfuse --anonymous-access ${implicitDirsFlag} ${onlyDirFlag}-o allow_other ${uidGidFlags} ${shellQuote(config.bucket)} ${quotedMountPath}`;
|
|
265
|
-
}
|
|
266
|
-
logger.debug(`${LOG_PREFIX} Mounting GCS:`, mountCmd);
|
|
267
|
-
const result = await run(mountCmd, 6e4);
|
|
268
|
-
logger.debug(`${LOG_PREFIX} gcsfuse result:`, {
|
|
269
|
-
exitCode: result.exitCode,
|
|
270
|
-
stdout: result.stdout,
|
|
271
|
-
stderr: result.stderr
|
|
272
|
-
});
|
|
273
|
-
if (result.exitCode !== 0) {
|
|
274
|
-
throw new Error(`Failed to mount GCS bucket: ${result.stderr || result.stdout}`);
|
|
275
|
-
}
|
|
156
|
+
const { run, writeFile, logger } = ctx;
|
|
157
|
+
validateBucketName(config.bucket);
|
|
158
|
+
const quotedMountPath = shellQuote(mountPath);
|
|
159
|
+
const connectivityCheck = await run("curl -sS --max-time 5 http://storage.googleapis.com 2>&1", 1e4);
|
|
160
|
+
const checkOutput = connectivityCheck.stdout.trim();
|
|
161
|
+
if (connectivityCheck.exitCode !== 0 || checkOutput.toLowerCase().includes("restricted") || checkOutput.toLowerCase().includes("blocked")) throw new Error("Cannot reach Google Cloud Storage from this sandbox. GCS mounting requires network access to storage.googleapis.com, which may be blocked on Daytona's restricted tiers. Upgrade to a tier with unrestricted internet access, or contact Daytona support to remove the network restriction." + (checkOutput ? `\n\nSandbox network response: ${checkOutput}` : ""));
|
|
162
|
+
if ((await run("which gcsfuse 2>/dev/null || echo \"not found\"", 3e4)).stdout.includes("not found")) {
|
|
163
|
+
logger.warn(`${LOG_PREFIX} gcsfuse not found, attempting runtime installation...`);
|
|
164
|
+
logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install gcsfuse in your sandbox image`);
|
|
165
|
+
await run("sudo apt-get update -qq 2>&1", 6e4);
|
|
166
|
+
const prepResult = await run("sudo apt-get install -y curl gnupg 2>&1", 12e4);
|
|
167
|
+
if (prepResult.exitCode !== 0) throw new Error(`Failed to install gcsfuse prerequisites (curl, gnupg): ${prepResult.stderr || prepResult.stdout}`);
|
|
168
|
+
const distroId = (await run("cat /etc/os-release 2>/dev/null | grep \"^ID=\" | cut -d= -f2 || echo debian", 3e4)).stdout.trim().replace(/"/g, "") || "debian";
|
|
169
|
+
const fallbackCodename = distroId === "ubuntu" ? "jammy" : "bookworm";
|
|
170
|
+
const detectedCodename = (await run(`cat /etc/os-release 2>/dev/null | grep "^VERSION_CODENAME=" | cut -d= -f2 || echo ${fallbackCodename}`, 3e4)).stdout.trim() || fallbackCodename;
|
|
171
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(detectedCodename)) throw new Error(`Invalid distro codename for gcsfuse repo: "${detectedCodename}"`);
|
|
172
|
+
logger.debug(`${LOG_PREFIX} Detected distro: ${distroId}/${detectedCodename}, fallback: ${fallbackCodename}`);
|
|
173
|
+
const repoSetup = await run(`sudo mkdir -p /etc/apt/keyrings && curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg -o /tmp/gcsfuse-key.gpg && sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/gcsfuse.gpg /tmp/gcsfuse-key.gpg && echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] https://packages.cloud.google.com/apt gcsfuse-${detectedCodename} main" | sudo tee /etc/apt/sources.list.d/gcsfuse.list`, 3e4);
|
|
174
|
+
if (repoSetup.exitCode !== 0) throw new Error(`Failed to set up gcsfuse apt repository: ${repoSetup.stderr || repoSetup.stdout}`);
|
|
175
|
+
await run("sudo apt-get update -qq 2>&1 || true", 6e4);
|
|
176
|
+
let installResult = await run("sudo apt-get install -y gcsfuse 2>&1", 12e4);
|
|
177
|
+
if (installResult.exitCode !== 0 && detectedCodename !== fallbackCodename) {
|
|
178
|
+
logger.warn(`${LOG_PREFIX} gcsfuse install failed for "${detectedCodename}", retrying with "${fallbackCodename}" fallback`);
|
|
179
|
+
await run(`sudo rm -f /etc/apt/sources.list.d/gcsfuse.list && echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] https://packages.cloud.google.com/apt gcsfuse-${fallbackCodename} main" | sudo tee /etc/apt/sources.list.d/gcsfuse.list`, 1e4);
|
|
180
|
+
await run("sudo apt-get update -qq 2>&1 || true", 6e4);
|
|
181
|
+
installResult = await run("sudo apt-get install -y gcsfuse 2>&1", 12e4);
|
|
182
|
+
}
|
|
183
|
+
if ((await run("which gcsfuse 2>/dev/null || echo \"not found\"", 3e4)).stdout.includes("not found")) throw new Error(`Failed to install gcsfuse: ${installResult.stderr || installResult.stdout}`);
|
|
184
|
+
if (installResult.exitCode !== 0) logger.warn(`${LOG_PREFIX} gcsfuse install reported dpkg errors (likely fuse post-install in container) but binary is present — proceeding`);
|
|
185
|
+
}
|
|
186
|
+
const idResult = await run("id -u && id -g", 3e4);
|
|
187
|
+
const [uid, gid] = idResult.stdout.trim().split("\n");
|
|
188
|
+
const validUidGid = uid && gid && /^\d+$/.test(uid) && /^\d+$/.test(gid);
|
|
189
|
+
if (!validUidGid) logger.warn(`${LOG_PREFIX} Unexpected uid/gid format: "${idResult.stdout.trim()}" — mounted files will be owned by root`);
|
|
190
|
+
const uidGidFlags = validUidGid ? `--uid=${uid} --gid=${gid}` : "";
|
|
191
|
+
await run("sudo chmod a+rw /dev/fuse 2>/dev/null || true; sudo bash -c 'grep -q \"^user_allow_other\" /etc/fuse.conf 2>/dev/null || echo \"user_allow_other\" >> /etc/fuse.conf' 2>/dev/null || true");
|
|
192
|
+
let onlyDirFlag = "";
|
|
193
|
+
if (config.prefix) onlyDirFlag = `--only-dir=${shellQuote(validatePrefix(config.prefix))} `;
|
|
194
|
+
const hasCredentials = !!config.serviceAccountKey;
|
|
195
|
+
const implicitDirsFlag = "--implicit-dirs";
|
|
196
|
+
let mountCmd;
|
|
197
|
+
if (hasCredentials) {
|
|
198
|
+
const keyPath = `/tmp/gcs-key-${(0, crypto.createHash)("md5").update(mountPath).digest("hex").slice(0, 8)}.json`;
|
|
199
|
+
await run(`sudo rm -f ${shellQuote(keyPath)}`, 3e4);
|
|
200
|
+
await writeFile(keyPath, config.serviceAccountKey);
|
|
201
|
+
await run(`chmod 600 ${shellQuote(keyPath)}`, 3e4);
|
|
202
|
+
mountCmd = `gcsfuse --key-file=${shellQuote(keyPath)} ${implicitDirsFlag} ${onlyDirFlag}-o allow_other ${uidGidFlags} ${shellQuote(config.bucket)} ${quotedMountPath}`;
|
|
203
|
+
} else {
|
|
204
|
+
logger.debug(`${LOG_PREFIX} No credentials provided, mounting GCS as public bucket (read-only)`);
|
|
205
|
+
mountCmd = `gcsfuse --anonymous-access ${implicitDirsFlag} ${onlyDirFlag}-o allow_other ${uidGidFlags} ${shellQuote(config.bucket)} ${quotedMountPath}`;
|
|
206
|
+
}
|
|
207
|
+
logger.debug(`${LOG_PREFIX} Mounting GCS:`, mountCmd);
|
|
208
|
+
const result = await run(mountCmd, 6e4);
|
|
209
|
+
logger.debug(`${LOG_PREFIX} gcsfuse result:`, {
|
|
210
|
+
exitCode: result.exitCode,
|
|
211
|
+
stdout: result.stdout,
|
|
212
|
+
stderr: result.stderr
|
|
213
|
+
});
|
|
214
|
+
if (result.exitCode !== 0) throw new Error(`Failed to mount GCS bucket: ${result.stderr || result.stdout}`);
|
|
276
215
|
}
|
|
277
|
-
|
|
278
|
-
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/sandbox/mounts/azure.ts
|
|
218
|
+
const SAFE_CONTAINER_NAME = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
|
|
219
|
+
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";
|
|
279
220
|
function validateContainerName(name) {
|
|
280
|
-
|
|
281
|
-
throw new Error(
|
|
282
|
-
`Invalid Azure container name: "${name}". Container names must be 3-63 lowercase alphanumeric characters or hyphens, with no consecutive hyphens.`
|
|
283
|
-
);
|
|
284
|
-
}
|
|
221
|
+
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.`);
|
|
285
222
|
}
|
|
286
223
|
function parseConnectionString(cs) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
return out;
|
|
224
|
+
const out = {};
|
|
225
|
+
for (const part of cs.split(";")) {
|
|
226
|
+
const eq = part.indexOf("=");
|
|
227
|
+
if (eq === -1) continue;
|
|
228
|
+
const key = part.slice(0, eq).trim();
|
|
229
|
+
const value = part.slice(eq + 1).trim();
|
|
230
|
+
if (!value) continue;
|
|
231
|
+
if (key === "AccountName") out.accountName = value;
|
|
232
|
+
else if (key === "AccountKey") out.accountKey = value;
|
|
233
|
+
else if (key === "SharedAccessSignature") out.sasToken = value;
|
|
234
|
+
else if (key === "BlobEndpoint") out.endpoint = value;
|
|
235
|
+
else if (key === "EndpointSuffix") out.endpointSuffix = value;
|
|
236
|
+
else if (key === "DefaultEndpointsProtocol") out.protocol = value;
|
|
237
|
+
}
|
|
238
|
+
if (!out.endpoint && out.accountName) out.endpoint = `${out.protocol || "https"}://${out.accountName}.blob.${out.endpointSuffix || "core.windows.net"}`;
|
|
239
|
+
return out;
|
|
305
240
|
}
|
|
306
241
|
function yamlString(value) {
|
|
307
|
-
|
|
242
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
308
243
|
}
|
|
309
244
|
function parseOsRelease(output) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return values;
|
|
245
|
+
const values = {};
|
|
246
|
+
for (const line of output.split("\n")) {
|
|
247
|
+
const eq = line.indexOf("=");
|
|
248
|
+
if (eq === -1) continue;
|
|
249
|
+
const key = line.slice(0, eq);
|
|
250
|
+
values[key] = line.slice(eq + 1).trim().replace(/^"|"$/g, "");
|
|
251
|
+
}
|
|
252
|
+
return values;
|
|
319
253
|
}
|
|
320
254
|
function resolveMicrosoftAptRepos(osReleaseOutput) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
255
|
+
const osRelease = parseOsRelease(osReleaseOutput);
|
|
256
|
+
const distroId = osRelease.ID || "ubuntu";
|
|
257
|
+
const codename = osRelease.VERSION_CODENAME || (distroId === "debian" ? "bookworm" : "jammy");
|
|
258
|
+
const versionId = osRelease.VERSION_ID || (distroId === "debian" ? "12" : "22.04");
|
|
259
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(codename)) throw new Error(`Invalid distro codename for blobfuse2 repo: "${codename}"`);
|
|
260
|
+
if (!/^\d+(?:\.\d+)?$/.test(versionId)) throw new Error(`Invalid distro version for blobfuse2 repo: "${versionId}"`);
|
|
261
|
+
if (distroId === "debian") {
|
|
262
|
+
const repos = [{
|
|
263
|
+
repoUrl: `https://packages.microsoft.com/debian/${versionId.split(".")[0]}/prod`,
|
|
264
|
+
suite: codename
|
|
265
|
+
}];
|
|
266
|
+
if (versionId.split(".")[0] !== "12" || codename !== "bookworm") repos.push({
|
|
267
|
+
repoUrl: "https://packages.microsoft.com/debian/12/prod",
|
|
268
|
+
suite: "bookworm"
|
|
269
|
+
});
|
|
270
|
+
return repos;
|
|
271
|
+
}
|
|
272
|
+
if (distroId === "ubuntu") {
|
|
273
|
+
const repos = [{
|
|
274
|
+
repoUrl: `https://packages.microsoft.com/ubuntu/${versionId}/prod`,
|
|
275
|
+
suite: codename
|
|
276
|
+
}];
|
|
277
|
+
if (versionId !== "24.04" || codename !== "noble") repos.push({
|
|
278
|
+
repoUrl: "https://packages.microsoft.com/ubuntu/24.04/prod",
|
|
279
|
+
suite: "noble"
|
|
280
|
+
});
|
|
281
|
+
if (versionId !== "22.04" || codename !== "jammy") repos.push({
|
|
282
|
+
repoUrl: "https://packages.microsoft.com/ubuntu/22.04/prod",
|
|
283
|
+
suite: "jammy"
|
|
284
|
+
});
|
|
285
|
+
return repos;
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Unsupported distro for blobfuse2 runtime installation: "${distroId}"`);
|
|
351
288
|
}
|
|
352
289
|
function resolveAuth(config) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
if (endpoint) {
|
|
380
|
-
validateEndpoint(endpoint);
|
|
381
|
-
}
|
|
382
|
-
return { mode, accountName, accountKey, sasToken, endpoint };
|
|
290
|
+
let accountName = config.accountName;
|
|
291
|
+
let accountKey = config.accountKey;
|
|
292
|
+
let sasToken = config.sasToken;
|
|
293
|
+
let endpoint = config.endpoint;
|
|
294
|
+
if (config.connectionString) {
|
|
295
|
+
const parsed = parseConnectionString(config.connectionString);
|
|
296
|
+
accountName = accountName ?? parsed.accountName;
|
|
297
|
+
accountKey = accountKey ?? parsed.accountKey;
|
|
298
|
+
sasToken = sasToken ?? parsed.sasToken;
|
|
299
|
+
endpoint = endpoint ?? parsed.endpoint;
|
|
300
|
+
}
|
|
301
|
+
let mode;
|
|
302
|
+
if (config.useDefaultCredential) mode = "msi";
|
|
303
|
+
else if (sasToken) mode = "sas";
|
|
304
|
+
else if (accountKey) mode = "key";
|
|
305
|
+
else throw new Error("Azure Blob mount requires credentials: provide connectionString, accountKey + accountName, sasToken + accountName, or useDefaultCredential.");
|
|
306
|
+
if (!accountName) throw new Error("Azure Blob mount requires an accountName (either explicitly or via connectionString).");
|
|
307
|
+
if (endpoint) validateEndpoint(endpoint);
|
|
308
|
+
return {
|
|
309
|
+
mode,
|
|
310
|
+
accountName,
|
|
311
|
+
accountKey,
|
|
312
|
+
sasToken,
|
|
313
|
+
endpoint
|
|
314
|
+
};
|
|
383
315
|
}
|
|
384
316
|
function buildBlobfuseConfig(container, auth, cachePath, readOnly) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
if (auth.endpoint) {
|
|
416
|
-
lines.push(` endpoint: ${yamlString(auth.endpoint.replace(/\/$/, ""))}`);
|
|
417
|
-
}
|
|
418
|
-
return lines.join("\n") + "\n";
|
|
317
|
+
const lines = [
|
|
318
|
+
"allow-other: true",
|
|
319
|
+
"foreground: false",
|
|
320
|
+
`read-only: ${readOnly ? "true" : "false"}`,
|
|
321
|
+
"logging:",
|
|
322
|
+
" type: silent",
|
|
323
|
+
"components:",
|
|
324
|
+
" - libfuse",
|
|
325
|
+
" - file_cache",
|
|
326
|
+
" - attr_cache",
|
|
327
|
+
" - azstorage",
|
|
328
|
+
"libfuse:",
|
|
329
|
+
" attribute-expiration-sec: 240",
|
|
330
|
+
" entry-expiration-sec: 240",
|
|
331
|
+
" negative-entry-expiration-sec: 120",
|
|
332
|
+
"file_cache:",
|
|
333
|
+
` path: ${yamlString(cachePath)}`,
|
|
334
|
+
" timeout-sec: 120",
|
|
335
|
+
"attr_cache:",
|
|
336
|
+
" timeout-sec: 7200",
|
|
337
|
+
"azstorage:",
|
|
338
|
+
` mode: ${auth.mode}`,
|
|
339
|
+
` account-name: ${yamlString(auth.accountName)}`,
|
|
340
|
+
` container: ${yamlString(container)}`
|
|
341
|
+
];
|
|
342
|
+
if (auth.mode === "key" && auth.accountKey) lines.push(` account-key: ${yamlString(auth.accountKey)}`);
|
|
343
|
+
else if (auth.mode === "sas" && auth.sasToken) lines.push(` sas: ${yamlString(auth.sasToken)}`);
|
|
344
|
+
if (auth.endpoint) lines.push(` endpoint: ${yamlString(auth.endpoint.replace(/\/$/, ""))}`);
|
|
345
|
+
return lines.join("\n") + "\n";
|
|
419
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Mount an Azure Blob container using blobfuse2.
|
|
349
|
+
*/
|
|
420
350
|
async function mountAzure(mountPath, config, ctx) {
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
`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'`,
|
|
481
|
-
18e4
|
|
482
|
-
);
|
|
483
|
-
verifyResult = await run("which blobfuse2 && blobfuse2 --version", 3e4);
|
|
484
|
-
}
|
|
485
|
-
if (!installResult || verifyResult.exitCode !== 0) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
`Failed to install blobfuse2: ${verifyResult.stderr || verifyResult.stdout || installResult?.stderr || installResult?.stdout || "unknown error"}`
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
const idResult = await run("id -u && id -g", 3e4);
|
|
492
|
-
const [uid, gid] = idResult.stdout.trim().split("\n");
|
|
493
|
-
const validUidGid = uid && gid && /^\d+$/.test(uid) && /^\d+$/.test(gid);
|
|
494
|
-
await run(
|
|
495
|
-
`sudo chmod a+rw /dev/fuse 2>/dev/null || true; sudo bash -c 'grep -q "^user_allow_other" /etc/fuse.conf 2>/dev/null || echo "user_allow_other" >> /etc/fuse.conf' 2>/dev/null || true`
|
|
496
|
-
);
|
|
497
|
-
const mountHash = crypto.createHash("md5").update(mountPath).digest("hex").slice(0, 8);
|
|
498
|
-
const configPath = `/tmp/.blobfuse2-config-${mountHash}.yaml`;
|
|
499
|
-
const cachePath = `/tmp/blobfuse2-cache-${mountHash}`;
|
|
500
|
-
const yaml = buildBlobfuseConfig(config.container, auth, cachePath, !!config.readOnly);
|
|
501
|
-
await run(`sudo rm -f ${shellQuote(configPath)}`, 3e4);
|
|
502
|
-
await writeFile(configPath, yaml);
|
|
503
|
-
await run(`chmod 600 ${shellQuote(configPath)}`, 3e4);
|
|
504
|
-
await run(`sudo rm -rf ${shellQuote(cachePath)} && mkdir -p ${shellQuote(cachePath)}`, 3e4);
|
|
505
|
-
if (validUidGid) {
|
|
506
|
-
await run(`sudo chown ${uid}:${gid} ${shellQuote(cachePath)} 2>/dev/null || true`, 3e4);
|
|
507
|
-
}
|
|
508
|
-
const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote(prefix)}` : "";
|
|
509
|
-
const mountCmd = `blobfuse2 mount ${quotedMountPath} --config-file=${shellQuote(configPath)}${prefixFlags}`;
|
|
510
|
-
logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
|
|
511
|
-
const result = await run(mountCmd, 6e4);
|
|
512
|
-
logger.debug(`${LOG_PREFIX} blobfuse2 result:`, {
|
|
513
|
-
exitCode: result.exitCode,
|
|
514
|
-
stdout: result.stdout,
|
|
515
|
-
stderr: result.stderr
|
|
516
|
-
});
|
|
517
|
-
if (result.exitCode !== 0) {
|
|
518
|
-
throw new Error(`Failed to mount Azure Blob container: ${result.stderr || result.stdout}`);
|
|
519
|
-
}
|
|
351
|
+
const { run, writeFile, logger } = ctx;
|
|
352
|
+
validateContainerName(config.container);
|
|
353
|
+
const auth = resolveAuth(config);
|
|
354
|
+
const prefix = config.prefix ? validatePrefix(config.prefix) : void 0;
|
|
355
|
+
const quotedMountPath = shellQuote(mountPath);
|
|
356
|
+
if ((await run("which curl 2>/dev/null || echo \"not found\"", 3e4)).stdout.includes("not found")) {
|
|
357
|
+
const curlInstall = await run("sudo apt-get update -qq 2>&1 && sudo apt-get install -y curl 2>&1", 12e4);
|
|
358
|
+
if (curlInstall.exitCode !== 0) throw new Error(`Failed to install curl for Azure Blob reachability check: ${curlInstall.stderr || curlInstall.stdout}`);
|
|
359
|
+
}
|
|
360
|
+
const probeUrl = auth.endpoint ? auth.endpoint.replace(/\/$/, "") : `https://${auth.accountName}.blob.core.windows.net`;
|
|
361
|
+
const connectivityCheck = await run(`curl -sS --max-time 5 ${shellQuote(probeUrl)} 2>&1`, 1e4);
|
|
362
|
+
const checkOutput = connectivityCheck.stdout.trim();
|
|
363
|
+
if (connectivityCheck.exitCode !== 0 || checkOutput.toLowerCase().includes("restricted") || checkOutput.toLowerCase().includes("blocked")) throw new Error(`Cannot reach ${probeUrl} from this sandbox. Azure Blob mounting requires network access to the storage endpoint, which may be blocked on Daytona's restricted tiers. Upgrade to a tier with unrestricted internet access, or contact Daytona support to remove the network restriction.` + (checkOutput ? `\n\nSandbox network response: ${checkOutput}` : ""));
|
|
364
|
+
if ((await run("which blobfuse2 2>/dev/null || echo \"not found\"", 3e4)).stdout.includes("not found")) {
|
|
365
|
+
logger.warn(`${LOG_PREFIX} blobfuse2 not found, attempting runtime installation...`);
|
|
366
|
+
logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install blobfuse2 in your sandbox image`);
|
|
367
|
+
await run("sudo apt-get update -qq 2>&1", 6e4);
|
|
368
|
+
const prepResult = await run("sudo apt-get install -y curl gnupg 2>&1", 12e4);
|
|
369
|
+
if (prepResult.exitCode !== 0) throw new Error(`Failed to install blobfuse2 prerequisites (curl, gnupg): ${prepResult.stderr || prepResult.stdout}`);
|
|
370
|
+
const repos = resolveMicrosoftAptRepos((await run("cat /etc/os-release 2>/dev/null || true", 3e4)).stdout);
|
|
371
|
+
const repoSetup = await 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", 3e4);
|
|
372
|
+
let installResult;
|
|
373
|
+
if (repoSetup.exitCode === 0) for (const { repoUrl, suite } of repos) {
|
|
374
|
+
await run(`echo "deb [signed-by=/etc/apt/keyrings/microsoft.gpg] ${repoUrl} ${suite} main" | sudo tee /etc/apt/sources.list.d/microsoft-prod.list`, 3e4);
|
|
375
|
+
await run("sudo apt-get update -qq 2>&1 || true", 6e4);
|
|
376
|
+
installResult = await run("sudo apt-get install -y blobfuse2 fuse3 2>&1", 12e4);
|
|
377
|
+
if (installResult.exitCode === 0) break;
|
|
378
|
+
logger.warn(`${LOG_PREFIX} blobfuse2 install failed for ${repoUrl} ${suite}, trying fallback if available`);
|
|
379
|
+
}
|
|
380
|
+
else logger.warn(`${LOG_PREFIX} Failed to set up Microsoft apt repository, trying GitHub release fallback`);
|
|
381
|
+
let verifyResult = await run("which blobfuse2 && blobfuse2 --version", 3e4);
|
|
382
|
+
if (verifyResult.exitCode !== 0) {
|
|
383
|
+
installResult = await 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'`, 18e4);
|
|
384
|
+
verifyResult = await run("which blobfuse2 && blobfuse2 --version", 3e4);
|
|
385
|
+
}
|
|
386
|
+
if (!installResult || verifyResult.exitCode !== 0) throw new Error(`Failed to install blobfuse2: ${verifyResult.stderr || verifyResult.stdout || installResult?.stderr || installResult?.stdout || "unknown error"}`);
|
|
387
|
+
}
|
|
388
|
+
const [uid, gid] = (await run("id -u && id -g", 3e4)).stdout.trim().split("\n");
|
|
389
|
+
const validUidGid = uid && gid && /^\d+$/.test(uid) && /^\d+$/.test(gid);
|
|
390
|
+
await run("sudo chmod a+rw /dev/fuse 2>/dev/null || true; sudo bash -c 'grep -q \"^user_allow_other\" /etc/fuse.conf 2>/dev/null || echo \"user_allow_other\" >> /etc/fuse.conf' 2>/dev/null || true");
|
|
391
|
+
const mountHash = (0, crypto.createHash)("md5").update(mountPath).digest("hex").slice(0, 8);
|
|
392
|
+
const configPath = `/tmp/.blobfuse2-config-${mountHash}.yaml`;
|
|
393
|
+
const cachePath = `/tmp/blobfuse2-cache-${mountHash}`;
|
|
394
|
+
const yaml = buildBlobfuseConfig(config.container, auth, cachePath, !!config.readOnly);
|
|
395
|
+
await run(`sudo rm -f ${shellQuote(configPath)}`, 3e4);
|
|
396
|
+
await writeFile(configPath, yaml);
|
|
397
|
+
await run(`chmod 600 ${shellQuote(configPath)}`, 3e4);
|
|
398
|
+
await run(`sudo rm -rf ${shellQuote(cachePath)} && mkdir -p ${shellQuote(cachePath)}`, 3e4);
|
|
399
|
+
if (validUidGid) await run(`sudo chown ${uid}:${gid} ${shellQuote(cachePath)} 2>/dev/null || true`, 3e4);
|
|
400
|
+
const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote(prefix)}` : "";
|
|
401
|
+
const mountCmd = `blobfuse2 mount ${quotedMountPath} --config-file=${shellQuote(configPath)}${prefixFlags}`;
|
|
402
|
+
logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
|
|
403
|
+
const result = await run(mountCmd, 6e4);
|
|
404
|
+
logger.debug(`${LOG_PREFIX} blobfuse2 result:`, {
|
|
405
|
+
exitCode: result.exitCode,
|
|
406
|
+
stdout: result.stdout,
|
|
407
|
+
stderr: result.stderr
|
|
408
|
+
});
|
|
409
|
+
if (result.exitCode !== 0) throw new Error(`Failed to mount Azure Blob container: ${result.stderr || result.stdout}`);
|
|
520
410
|
}
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
async sendStdin(data) {
|
|
627
|
-
if (this._exitCode !== void 0) {
|
|
628
|
-
throw new Error(`Process ${this.pid} has already exited with code ${this._exitCode}`);
|
|
629
|
-
}
|
|
630
|
-
await this._sandbox.process.sendSessionCommandInput(this.pid, this._cmdId, data);
|
|
631
|
-
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/sandbox/process-manager.ts
|
|
413
|
+
/**
|
|
414
|
+
* Wraps a Daytona session + command pair to conform to Mastra's ProcessHandle.
|
|
415
|
+
* Not exported — internal to this module.
|
|
416
|
+
*/
|
|
417
|
+
var DaytonaProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
|
|
418
|
+
pid;
|
|
419
|
+
_cmdId;
|
|
420
|
+
_sandbox;
|
|
421
|
+
_startTime;
|
|
422
|
+
_timeout;
|
|
423
|
+
_exitCode;
|
|
424
|
+
_waitPromise = null;
|
|
425
|
+
_streamingPromise = null;
|
|
426
|
+
_killed = false;
|
|
427
|
+
constructor(sessionId, cmdId, sandbox, startTime, options) {
|
|
428
|
+
super(options);
|
|
429
|
+
this.pid = sessionId;
|
|
430
|
+
this._cmdId = cmdId;
|
|
431
|
+
this._sandbox = sandbox;
|
|
432
|
+
this._startTime = startTime;
|
|
433
|
+
this._timeout = options?.timeout;
|
|
434
|
+
}
|
|
435
|
+
get exitCode() {
|
|
436
|
+
return this._exitCode;
|
|
437
|
+
}
|
|
438
|
+
/** @internal Set by the process manager after streaming starts. */
|
|
439
|
+
set streamingPromise(p) {
|
|
440
|
+
this._streamingPromise = p;
|
|
441
|
+
p.then(() => this._resolveExitCode()).catch(() => this._resolveExitCode());
|
|
442
|
+
}
|
|
443
|
+
/** Fetch the exit code from Daytona and set _exitCode. No-op if already set. */
|
|
444
|
+
async _resolveExitCode() {
|
|
445
|
+
if (this._exitCode !== void 0) return;
|
|
446
|
+
try {
|
|
447
|
+
const cmd = await this._sandbox.process.getSessionCommand(this.pid, this._cmdId);
|
|
448
|
+
this._exitCode = cmd.exitCode ?? 0;
|
|
449
|
+
} catch {
|
|
450
|
+
if (this._exitCode === void 0) this._exitCode = 1;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async wait() {
|
|
454
|
+
if (!this._waitPromise) this._waitPromise = this._doWait();
|
|
455
|
+
return this._waitPromise;
|
|
456
|
+
}
|
|
457
|
+
async _doWait() {
|
|
458
|
+
const streamDone = this._streamingPromise ?? Promise.resolve();
|
|
459
|
+
if (this._timeout) {
|
|
460
|
+
let timeoutId;
|
|
461
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
462
|
+
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Command timed out after ${this._timeout}ms`)), this._timeout);
|
|
463
|
+
});
|
|
464
|
+
try {
|
|
465
|
+
await Promise.race([streamDone, timeoutPromise]);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof Error && error.message.includes("timed out")) {
|
|
468
|
+
await this.kill();
|
|
469
|
+
this._exitCode = 124;
|
|
470
|
+
return {
|
|
471
|
+
success: false,
|
|
472
|
+
exitCode: 124,
|
|
473
|
+
stdout: this.stdout,
|
|
474
|
+
stderr: this.stderr || error.message,
|
|
475
|
+
executionTimeMs: Date.now() - this._startTime,
|
|
476
|
+
killed: true,
|
|
477
|
+
timedOut: true
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
throw error;
|
|
481
|
+
} finally {
|
|
482
|
+
clearTimeout(timeoutId);
|
|
483
|
+
}
|
|
484
|
+
} else await streamDone.catch(() => {});
|
|
485
|
+
if (this._killed) return {
|
|
486
|
+
success: false,
|
|
487
|
+
exitCode: this._exitCode ?? 137,
|
|
488
|
+
stdout: this.stdout,
|
|
489
|
+
stderr: this.stderr,
|
|
490
|
+
executionTimeMs: Date.now() - this._startTime,
|
|
491
|
+
killed: true,
|
|
492
|
+
timedOut: false
|
|
493
|
+
};
|
|
494
|
+
await this._resolveExitCode();
|
|
495
|
+
return {
|
|
496
|
+
success: this._exitCode === 0,
|
|
497
|
+
exitCode: this._exitCode ?? 1,
|
|
498
|
+
stdout: this.stdout,
|
|
499
|
+
stderr: this.stderr,
|
|
500
|
+
executionTimeMs: Date.now() - this._startTime
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async kill() {
|
|
504
|
+
if (this._exitCode !== void 0) return false;
|
|
505
|
+
this._killed = true;
|
|
506
|
+
this._exitCode = 137;
|
|
507
|
+
try {
|
|
508
|
+
await this._sandbox.process.deleteSession(this.pid);
|
|
509
|
+
} catch {}
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
async sendStdin(data) {
|
|
513
|
+
if (this._exitCode !== void 0) throw new Error(`Process ${this.pid} has already exited with code ${this._exitCode}`);
|
|
514
|
+
await this._sandbox.process.sendSessionCommandInput(this.pid, this._cmdId, data);
|
|
515
|
+
}
|
|
632
516
|
};
|
|
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
|
-
return result;
|
|
683
|
-
}
|
|
517
|
+
/**
|
|
518
|
+
* Daytona implementation of SandboxProcessManager.
|
|
519
|
+
* Uses the Daytona SDK's session API with one session per spawned process.
|
|
520
|
+
*/
|
|
521
|
+
var DaytonaProcessManager = class extends _mastra_core_workspace.SandboxProcessManager {
|
|
522
|
+
_spawnCounter = 0;
|
|
523
|
+
_defaultTimeout;
|
|
524
|
+
constructor(opts = {}) {
|
|
525
|
+
super({ env: opts.env });
|
|
526
|
+
this._defaultTimeout = opts.defaultTimeout;
|
|
527
|
+
}
|
|
528
|
+
async spawn(command, options = {}) {
|
|
529
|
+
const effectiveOptions = {
|
|
530
|
+
...options,
|
|
531
|
+
timeout: options.timeout ?? this._defaultTimeout,
|
|
532
|
+
cwd: options.cwd ?? this.sandbox.mounts?.entries?.keys().next().value
|
|
533
|
+
};
|
|
534
|
+
const mergedEnv = {
|
|
535
|
+
...this.env,
|
|
536
|
+
...effectiveOptions.env
|
|
537
|
+
};
|
|
538
|
+
const envs = Object.fromEntries(Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0));
|
|
539
|
+
const sessionCommand = buildSpawnCommand(command, effectiveOptions.cwd, envs);
|
|
540
|
+
return this.sandbox.retryOnDead(async () => {
|
|
541
|
+
const sandbox = this.sandbox.daytona;
|
|
542
|
+
const sessionId = `mastra-proc-${Date.now().toString(36)}-${++this._spawnCounter}`;
|
|
543
|
+
await sandbox.process.createSession(sessionId);
|
|
544
|
+
const { cmdId } = await sandbox.process.executeSessionCommand(sessionId, {
|
|
545
|
+
command: sessionCommand,
|
|
546
|
+
runAsync: true
|
|
547
|
+
});
|
|
548
|
+
const handle = new DaytonaProcessHandle(sessionId, cmdId, sandbox, Date.now(), effectiveOptions);
|
|
549
|
+
handle.streamingPromise = sandbox.process.getSessionCommandLogs(sessionId, cmdId, (chunk) => handle.emitStdout(chunk), (chunk) => handle.emitStderr(chunk)).catch(() => {});
|
|
550
|
+
this._tracked.set(handle.pid, handle);
|
|
551
|
+
return handle;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
async list() {
|
|
555
|
+
const result = [];
|
|
556
|
+
for (const [pid, handle] of this._tracked) result.push({
|
|
557
|
+
pid,
|
|
558
|
+
command: handle.command,
|
|
559
|
+
running: handle.exitCode === void 0,
|
|
560
|
+
exitCode: handle.exitCode
|
|
561
|
+
});
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
684
564
|
};
|
|
685
|
-
|
|
565
|
+
const SHELL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
566
|
+
/**
|
|
567
|
+
* Build a shell command string that bakes in cwd and env vars.
|
|
568
|
+
* Wraps the user command in a subshell `(command)` so that:
|
|
569
|
+
* - `exit N` exits the subshell, not the session shell
|
|
570
|
+
* - Heredocs work correctly within the subshell
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* buildSpawnCommand('npm test', '/app', { NODE_ENV: 'test' })
|
|
574
|
+
* // → "export NODE_ENV='test' && cd '/app' && (npm test)"
|
|
575
|
+
*/
|
|
686
576
|
function buildSpawnCommand(command, cwd, envs) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
parts.push(`cd ${shellQuote(cwd)}`);
|
|
696
|
-
}
|
|
697
|
-
parts.push(`(${command})`);
|
|
698
|
-
return parts.join(" && ");
|
|
577
|
+
const parts = [];
|
|
578
|
+
for (const [k, v] of Object.entries(envs)) {
|
|
579
|
+
if (!SHELL_IDENTIFIER_PATTERN.test(k)) throw new Error(`Invalid environment variable name: ${JSON.stringify(k)}`);
|
|
580
|
+
parts.push(`export ${k}=${shellQuote(v)}`);
|
|
581
|
+
}
|
|
582
|
+
if (cwd) parts.push(`cd ${shellQuote(cwd)}`);
|
|
583
|
+
parts.push(`(${command})`);
|
|
584
|
+
return parts.join(" && ");
|
|
699
585
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/sandbox/index.ts
|
|
588
|
+
/**
|
|
589
|
+
* Daytona Sandbox Provider
|
|
590
|
+
*
|
|
591
|
+
* A Daytona sandbox implementation for Mastra workspaces.
|
|
592
|
+
* Supports command execution, environment variables, resource configuration,
|
|
593
|
+
* snapshots, Daytona volumes, and FUSE-based cloud filesystem mounting (S3, GCS).
|
|
594
|
+
*
|
|
595
|
+
* @see https://www.daytona.io/docs
|
|
596
|
+
*/
|
|
597
|
+
/** Allowlist pattern for mount paths — absolute path with safe characters only. */
|
|
598
|
+
const SAFE_MOUNT_PATH = /^\/[a-zA-Z0-9_.\-/]+$/;
|
|
599
|
+
/** Default timeout for mount lifecycle shell commands (mkdir, unmount, proc reads, etc.) */
|
|
600
|
+
const MOUNT_COMMAND_TIMEOUT_MS = 3e4;
|
|
601
|
+
/** Convert an unknown error to a readable string. */
|
|
704
602
|
function errorToString(error) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
return String(error);
|
|
717
|
-
}
|
|
603
|
+
if (error instanceof Error) return error.message;
|
|
604
|
+
if (typeof error === "string") return error;
|
|
605
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
606
|
+
const maybeError = error;
|
|
607
|
+
if (typeof maybeError.message === "string") return maybeError.message;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
return JSON.stringify(error);
|
|
611
|
+
} catch {
|
|
612
|
+
return String(error);
|
|
613
|
+
}
|
|
718
614
|
}
|
|
719
615
|
function validateMountPath(mountPath) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
const segments = mountPath.split("/");
|
|
726
|
-
if (mountPath.includes("//") || segments.some((segment) => segment === "." || segment === "..")) {
|
|
727
|
-
throw new Error(`Invalid mount path: ${mountPath}. Path traversal segments are not allowed.`);
|
|
728
|
-
}
|
|
616
|
+
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.`);
|
|
617
|
+
const segments = mountPath.split("/");
|
|
618
|
+
if (mountPath.includes("//") || segments.some((segment) => segment === "." || segment === "..")) throw new Error(`Invalid mount path: ${mountPath}. Path traversal segments are not allowed.`);
|
|
729
619
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
620
|
+
/** Allowlist for marker filenames from ls output — e.g. "mount-abc123" */
|
|
621
|
+
const SAFE_MARKER_NAME = /^mount-[a-z0-9]+$/;
|
|
622
|
+
/** Patterns indicating the sandbox is dead/gone (@daytonaio/sdk@0.143.0). */
|
|
623
|
+
const SANDBOX_DEAD_PATTERNS = [
|
|
624
|
+
/sandbox is not running/i,
|
|
625
|
+
/sandbox already destroyed/i,
|
|
626
|
+
/sandbox.*not found/i,
|
|
627
|
+
/failed to resolve container IP/i,
|
|
628
|
+
/is the sandbox started/i
|
|
737
629
|
];
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
}
|
|
1537
|
-
this.status = "stopped";
|
|
1538
|
-
}
|
|
1539
|
-
// ---------------------------------------------------------------------------
|
|
1540
|
-
// Retry on Dead
|
|
1541
|
-
// ---------------------------------------------------------------------------
|
|
1542
|
-
/**
|
|
1543
|
-
* Execute a function, retrying once if the sandbox is found to be dead.
|
|
1544
|
-
* Used by DaytonaProcessManager to handle stale sandboxes transparently.
|
|
1545
|
-
*/
|
|
1546
|
-
async retryOnDead(fn) {
|
|
1547
|
-
try {
|
|
1548
|
-
return await fn();
|
|
1549
|
-
} catch (error) {
|
|
1550
|
-
if (this.isSandboxDeadError(error) && !this._isRetrying) {
|
|
1551
|
-
this.handleSandboxTimeout();
|
|
1552
|
-
this._isRetrying = true;
|
|
1553
|
-
try {
|
|
1554
|
-
await this.ensureRunning();
|
|
1555
|
-
return await fn();
|
|
1556
|
-
} finally {
|
|
1557
|
-
this._isRetrying = false;
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
throw error;
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
630
|
+
/**
|
|
631
|
+
* Daytona sandbox provider for Mastra workspaces.
|
|
632
|
+
*
|
|
633
|
+
* Features:
|
|
634
|
+
* - Isolated cloud sandbox via Daytona SDK
|
|
635
|
+
* - Multi-runtime support (TypeScript, JavaScript, Python)
|
|
636
|
+
* - Resource configuration (CPU, memory, disk)
|
|
637
|
+
* - Volume attachment at creation time
|
|
638
|
+
* - FUSE-based cloud filesystem mounting (S3, GCS)
|
|
639
|
+
* - Automatic sandbox timeout handling with retry
|
|
640
|
+
*
|
|
641
|
+
* @example Basic usage
|
|
642
|
+
* ```typescript
|
|
643
|
+
* import { Workspace } from '@mastra/core/workspace';
|
|
644
|
+
* import { DaytonaSandbox } from '@mastra/daytona';
|
|
645
|
+
*
|
|
646
|
+
* const sandbox = new DaytonaSandbox({
|
|
647
|
+
* timeout: 60000,
|
|
648
|
+
* language: 'typescript',
|
|
649
|
+
* });
|
|
650
|
+
*
|
|
651
|
+
* const workspace = new Workspace({ sandbox });
|
|
652
|
+
* const result = await workspace.executeCode('console.log("Hello!")');
|
|
653
|
+
* ```
|
|
654
|
+
*
|
|
655
|
+
* @example With resources and volumes
|
|
656
|
+
* ```typescript
|
|
657
|
+
* const sandbox = new DaytonaSandbox({
|
|
658
|
+
* resources: { cpu: 2, memory: 4, disk: 6 },
|
|
659
|
+
* volumes: [{ volumeId: 'vol-123', mountPath: '/data' }],
|
|
660
|
+
* env: { NODE_ENV: 'production' },
|
|
661
|
+
* });
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
var DaytonaSandbox = class DaytonaSandbox extends _mastra_core_workspace.MastraSandbox {
|
|
665
|
+
id;
|
|
666
|
+
name = "DaytonaSandbox";
|
|
667
|
+
provider = "daytona";
|
|
668
|
+
/**
|
|
669
|
+
* Networking capability: public HTTPS URLs for sandbox ports.
|
|
670
|
+
* Daytona exposes ports through preview links (`getPreviewLink(port)`) —
|
|
671
|
+
* if the port is closed it is opened automatically. Private sandboxes
|
|
672
|
+
* require the preview token; pass `public: true` for tokenless URLs
|
|
673
|
+
* (required for sandbox deploys).
|
|
674
|
+
*
|
|
675
|
+
* When not attached in this process, the sandbox is looked up by identity
|
|
676
|
+
* (`daytona.get()` does not start it), so other processes can resolve
|
|
677
|
+
* deployments without waking a stopped sandbox.
|
|
678
|
+
*/
|
|
679
|
+
networking = { getPortUrl: async (port) => {
|
|
680
|
+
try {
|
|
681
|
+
const sandbox = this._sandbox ?? await this.lookupDetachedSandbox();
|
|
682
|
+
if (!sandbox) return null;
|
|
683
|
+
return (await sandbox.getPreviewLink(port))?.url ?? null;
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
} };
|
|
688
|
+
status = "pending";
|
|
689
|
+
_daytona = null;
|
|
690
|
+
_sandbox = null;
|
|
691
|
+
_createdAt = null;
|
|
692
|
+
_workingDir = null;
|
|
693
|
+
_isRetrying = false;
|
|
694
|
+
timeout;
|
|
695
|
+
language;
|
|
696
|
+
resources;
|
|
697
|
+
env;
|
|
698
|
+
labels;
|
|
699
|
+
snapshotId;
|
|
700
|
+
image;
|
|
701
|
+
ephemeral;
|
|
702
|
+
autoStopInterval;
|
|
703
|
+
autoArchiveInterval;
|
|
704
|
+
autoDeleteInterval;
|
|
705
|
+
volumeConfigs;
|
|
706
|
+
sandboxName;
|
|
707
|
+
_daytonaSandboxId;
|
|
708
|
+
sandboxUser;
|
|
709
|
+
sandboxPublic;
|
|
710
|
+
networkBlockAll;
|
|
711
|
+
networkAllowList;
|
|
712
|
+
domainAllowList;
|
|
713
|
+
connectionOpts;
|
|
714
|
+
_constructorOptions;
|
|
715
|
+
constructor(options = {}) {
|
|
716
|
+
super({
|
|
717
|
+
...options,
|
|
718
|
+
name: "DaytonaSandbox",
|
|
719
|
+
processes: new DaytonaProcessManager({
|
|
720
|
+
env: options.env,
|
|
721
|
+
defaultTimeout: options.timeout ?? 3e5
|
|
722
|
+
})
|
|
723
|
+
});
|
|
724
|
+
this.id = options.id ?? this.generateId();
|
|
725
|
+
this.timeout = options.timeout ?? 3e5;
|
|
726
|
+
this.language = options.language ?? "typescript";
|
|
727
|
+
this.resources = options.resources;
|
|
728
|
+
this.env = options.env ?? {};
|
|
729
|
+
this.labels = options.labels ?? {};
|
|
730
|
+
this.snapshotId = options.snapshot;
|
|
731
|
+
this.image = options.image;
|
|
732
|
+
this.ephemeral = options.ephemeral ?? false;
|
|
733
|
+
this.autoStopInterval = options.autoStopInterval ?? 15;
|
|
734
|
+
this.autoArchiveInterval = options.autoArchiveInterval;
|
|
735
|
+
this.autoDeleteInterval = options.autoDeleteInterval;
|
|
736
|
+
this.volumeConfigs = options.volumes ?? [];
|
|
737
|
+
this.sandboxName = options.name ?? this.id;
|
|
738
|
+
this.sandboxUser = options.user;
|
|
739
|
+
this.sandboxPublic = options.public;
|
|
740
|
+
this.networkBlockAll = options.networkBlockAll;
|
|
741
|
+
this.networkAllowList = options.networkAllowList;
|
|
742
|
+
this.domainAllowList = options.domainAllowList;
|
|
743
|
+
this.connectionOpts = {
|
|
744
|
+
...options.apiKey !== void 0 && { apiKey: options.apiKey },
|
|
745
|
+
...options.apiUrl !== void 0 && { apiUrl: options.apiUrl },
|
|
746
|
+
...options.target !== void 0 && { target: options.target }
|
|
747
|
+
};
|
|
748
|
+
this._constructorOptions = { ...options };
|
|
749
|
+
}
|
|
750
|
+
generateId() {
|
|
751
|
+
return `daytona-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Construct a sibling `DaytonaSandbox` that inherits this sandbox's
|
|
755
|
+
* configuration (credentials, snapshot/image, resources, network policy)
|
|
756
|
+
* with per-instance overrides.
|
|
757
|
+
*
|
|
758
|
+
* Performs no I/O — the sandbox clone provisions (or reconnects to an
|
|
759
|
+
* existing Daytona sandbox with the same logical `id`) on its own `start()`.
|
|
760
|
+
* Use it when one configured sandbox acts as the template for a fleet of
|
|
761
|
+
* independent sandboxes (e.g. one per project).
|
|
762
|
+
*
|
|
763
|
+
* `options.idleTimeoutMinutes` maps to Daytona's `autoStopInterval`
|
|
764
|
+
* (minutes); `options.sandboxId` is ignored because Daytona reconnects by
|
|
765
|
+
* logical `id`.
|
|
766
|
+
*/
|
|
767
|
+
clone(options = {}) {
|
|
768
|
+
const { id: _id, name: _name, ...base } = this._constructorOptions;
|
|
769
|
+
return new DaytonaSandbox({
|
|
770
|
+
...base,
|
|
771
|
+
...options.id !== void 0 && { id: options.id },
|
|
772
|
+
...options.env !== void 0 && { env: options.env },
|
|
773
|
+
...options.idleTimeoutMinutes !== void 0 && { autoStopInterval: options.idleTimeoutMinutes }
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get the underlying Daytona Sandbox instance for direct access to Daytona APIs.
|
|
778
|
+
*
|
|
779
|
+
* Use this when you need to access Daytona features not exposed through the
|
|
780
|
+
* WorkspaceSandbox interface (e.g., filesystem API, git operations, LSP).
|
|
781
|
+
*
|
|
782
|
+
* @throws {SandboxNotReadyError} If the sandbox has not been started
|
|
783
|
+
*
|
|
784
|
+
* @example Direct file operations
|
|
785
|
+
* ```typescript
|
|
786
|
+
* await sandbox.start();
|
|
787
|
+
* const daytonaSandbox = sandbox.daytona;
|
|
788
|
+
* await daytonaSandbox.fs.uploadFile(Buffer.from('Hello'), '/tmp/test.txt');
|
|
789
|
+
* ```
|
|
790
|
+
*/
|
|
791
|
+
get daytona() {
|
|
792
|
+
if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
793
|
+
return this._sandbox;
|
|
794
|
+
}
|
|
795
|
+
/** @deprecated Use `daytona` instead. */
|
|
796
|
+
get instance() {
|
|
797
|
+
return this.daytona;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Start the Daytona sandbox.
|
|
801
|
+
* Reconnects to an existing sandbox with the same logical ID if one exists,
|
|
802
|
+
* otherwise creates a new sandbox instance.
|
|
803
|
+
*/
|
|
804
|
+
async start() {
|
|
805
|
+
if (this._sandbox) return;
|
|
806
|
+
if (!this._daytona) this._daytona = new _daytonaio_sdk.Daytona(this.connectionOpts);
|
|
807
|
+
const existing = await this.findExistingSandbox();
|
|
808
|
+
if (existing) {
|
|
809
|
+
this._sandbox = existing;
|
|
810
|
+
this._daytonaSandboxId = existing.id;
|
|
811
|
+
this._createdAt = existing.createdAt ? new Date(existing.createdAt) : /* @__PURE__ */ new Date();
|
|
812
|
+
this.logger.debug(`${LOG_PREFIX} Reconnected to existing sandbox ${existing.id} for: ${this.id}`);
|
|
813
|
+
const expectedPaths = Array.from(this.mounts.entries.keys());
|
|
814
|
+
this.logger.debug(`${LOG_PREFIX} Running mount reconciliation...`);
|
|
815
|
+
await this.reconcileMounts(expectedPaths);
|
|
816
|
+
this.logger.debug(`${LOG_PREFIX} Mount reconciliation complete`);
|
|
817
|
+
await this.detectWorkingDir();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
this.logger.debug(`${LOG_PREFIX} Creating sandbox for: ${this.id}`);
|
|
821
|
+
const baseParams = compact({
|
|
822
|
+
language: this.language,
|
|
823
|
+
labels: {
|
|
824
|
+
...this.labels,
|
|
825
|
+
"mastra-sandbox-id": this.id
|
|
826
|
+
},
|
|
827
|
+
ephemeral: this.ephemeral,
|
|
828
|
+
autoStopInterval: this.autoStopInterval,
|
|
829
|
+
autoArchiveInterval: this.autoArchiveInterval,
|
|
830
|
+
autoDeleteInterval: this.autoDeleteInterval,
|
|
831
|
+
volumes: this.volumeConfigs.length > 0 ? this.volumeConfigs : void 0,
|
|
832
|
+
name: this.sandboxName,
|
|
833
|
+
user: this.sandboxUser,
|
|
834
|
+
public: this.sandboxPublic,
|
|
835
|
+
networkBlockAll: this.networkBlockAll,
|
|
836
|
+
networkAllowList: this.networkAllowList,
|
|
837
|
+
domainAllowList: this.domainAllowList
|
|
838
|
+
});
|
|
839
|
+
if (this.resources && !this.image) this.logger.warn(`${LOG_PREFIX} 'resources' option requires 'image' to take effect — falling back to snapshot-based creation without custom resources`);
|
|
840
|
+
const createParams = this.image && !this.snapshotId ? compact({
|
|
841
|
+
...baseParams,
|
|
842
|
+
image: this.image,
|
|
843
|
+
resources: this.resources
|
|
844
|
+
}) : compact({
|
|
845
|
+
...baseParams,
|
|
846
|
+
snapshot: this.snapshotId
|
|
847
|
+
});
|
|
848
|
+
this._sandbox = await this._daytona.create(createParams);
|
|
849
|
+
this._daytonaSandboxId = this._sandbox.id;
|
|
850
|
+
this.logger.debug(`${LOG_PREFIX} Created sandbox ${this._sandbox.id} for logical ID: ${this.id}`);
|
|
851
|
+
this._createdAt = /* @__PURE__ */ new Date();
|
|
852
|
+
await this.detectWorkingDir();
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Stop the Daytona sandbox.
|
|
856
|
+
* Unmounts all filesystems, then stops the sandbox.
|
|
857
|
+
*/
|
|
858
|
+
async stop() {
|
|
859
|
+
if (!this._sandbox) {
|
|
860
|
+
try {
|
|
861
|
+
const existing = await this.lookupDetachedSandbox();
|
|
862
|
+
if (existing && existing.state === _daytonaio_sdk.SandboxState.STARTED) await this._daytona.stop(existing);
|
|
863
|
+
} catch {}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
for (const mountPath of [...this.mounts.entries.keys()]) try {
|
|
867
|
+
await this.unmount(mountPath);
|
|
868
|
+
} catch {}
|
|
869
|
+
if (this._daytona) try {
|
|
870
|
+
await this._daytona.stop(this._sandbox);
|
|
871
|
+
} catch {}
|
|
872
|
+
this._sandbox = null;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Destroy the Daytona sandbox and clean up all resources.
|
|
876
|
+
* Deletes the sandbox and clears all state.
|
|
877
|
+
*/
|
|
878
|
+
async destroy() {
|
|
879
|
+
if (this._sandbox && this._daytona) try {
|
|
880
|
+
await this._daytona.delete(this._sandbox);
|
|
881
|
+
} catch {}
|
|
882
|
+
else if (!this._sandbox) try {
|
|
883
|
+
const orphan = await this.lookupDetachedSandbox();
|
|
884
|
+
if (orphan) await this._daytona.delete(orphan);
|
|
885
|
+
} catch {}
|
|
886
|
+
this._sandbox = null;
|
|
887
|
+
this._daytonaSandboxId = void 0;
|
|
888
|
+
this._daytona = null;
|
|
889
|
+
this.mounts?.clear();
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Check if the sandbox is ready for operations.
|
|
893
|
+
*/
|
|
894
|
+
async isReady() {
|
|
895
|
+
return this.status === "running" && this._sandbox !== null;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Get information about the current state of the sandbox.
|
|
899
|
+
*/
|
|
900
|
+
async getInfo() {
|
|
901
|
+
return {
|
|
902
|
+
id: this.id,
|
|
903
|
+
name: this.name,
|
|
904
|
+
provider: this.provider,
|
|
905
|
+
status: this.status,
|
|
906
|
+
createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
|
|
907
|
+
mounts: this.mounts ? Array.from(this.mounts.entries).map(([path, entry]) => ({
|
|
908
|
+
path,
|
|
909
|
+
filesystem: entry.filesystem?.provider ?? entry.config?.type ?? "unknown"
|
|
910
|
+
})) : [],
|
|
911
|
+
...this._sandbox && { resources: {
|
|
912
|
+
cpuCores: this._sandbox.cpu,
|
|
913
|
+
memoryMB: this._sandbox.memory * 1024,
|
|
914
|
+
diskMB: this._sandbox.disk * 1024
|
|
915
|
+
} },
|
|
916
|
+
metadata: {
|
|
917
|
+
language: this.language,
|
|
918
|
+
ephemeral: this.ephemeral,
|
|
919
|
+
...this.snapshotId && { snapshot: this.snapshotId },
|
|
920
|
+
...this.image && { image: this.image },
|
|
921
|
+
...this._sandbox && { target: this._sandbox.target }
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Get instructions describing this Daytona sandbox.
|
|
927
|
+
* Used by agents to understand the execution environment.
|
|
928
|
+
*/
|
|
929
|
+
getInstructions() {
|
|
930
|
+
const parts = [];
|
|
931
|
+
const mountCount = this.mounts.entries.size;
|
|
932
|
+
const mountInfo = mountCount > 0 ? ` ${mountCount} filesystem(s) mounted via FUSE.` : "";
|
|
933
|
+
parts.push(`Cloud sandbox with isolated execution (${this.language} runtime).${mountInfo}`);
|
|
934
|
+
if (this._workingDir) parts.push(`Default working directory: ${this._workingDir}.`);
|
|
935
|
+
parts.push(`Command timeout: ${Math.ceil(this.timeout / 1e3)}s.`);
|
|
936
|
+
parts.push(`Running as user: ${this.sandboxUser ?? "daytona"}.`);
|
|
937
|
+
if (this.volumeConfigs.length > 0) parts.push(`${this.volumeConfigs.length} volume(s) attached.`);
|
|
938
|
+
if (this.networkBlockAll) parts.push(`Network access is blocked.`);
|
|
939
|
+
return parts.join(" ");
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Execute a command in the sandbox and return the result.
|
|
943
|
+
*/
|
|
944
|
+
async executeCommand(command, args = [], options = {}) {
|
|
945
|
+
await this.ensureRunning();
|
|
946
|
+
const fullCommand = args.length > 0 ? `${command} ${args.map(shellQuote).join(" ")}` : command;
|
|
947
|
+
return {
|
|
948
|
+
...await (await this.processes.spawn(fullCommand, options)).wait(),
|
|
949
|
+
command,
|
|
950
|
+
args
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Bulk-write files into the sandbox filesystem via the SDK's native upload.
|
|
955
|
+
*/
|
|
956
|
+
async writeFiles(files) {
|
|
957
|
+
await this.ensureRunning();
|
|
958
|
+
await this.daytona.fs.uploadFiles(files.map((file) => ({
|
|
959
|
+
source: Buffer.isBuffer(file.content) ? file.content : Buffer.from(file.content),
|
|
960
|
+
destination: file.path
|
|
961
|
+
})));
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Mount a filesystem at a path in the sandbox.
|
|
965
|
+
* Uses FUSE tools (s3fs, gcsfuse) to mount cloud storage.
|
|
966
|
+
*/
|
|
967
|
+
async mount(filesystem, mountPath) {
|
|
968
|
+
validateMountPath(mountPath);
|
|
969
|
+
if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
970
|
+
const sandbox = this._sandbox;
|
|
971
|
+
this.logger.debug(`${LOG_PREFIX} Mounting "${mountPath}"...`);
|
|
972
|
+
const config = filesystem.getMountConfig?.();
|
|
973
|
+
if (!config) {
|
|
974
|
+
const error = `Filesystem "${filesystem.id}" does not provide a mount config`;
|
|
975
|
+
this.logger.error(`${LOG_PREFIX} ${error}`);
|
|
976
|
+
this.mounts.set(mountPath, {
|
|
977
|
+
filesystem,
|
|
978
|
+
state: "error",
|
|
979
|
+
error
|
|
980
|
+
});
|
|
981
|
+
return {
|
|
982
|
+
success: false,
|
|
983
|
+
mountPath,
|
|
984
|
+
error
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
const existingMount = await this.checkExistingMount(mountPath, config);
|
|
988
|
+
if (existingMount === "matching") {
|
|
989
|
+
this.logger.debug(`${LOG_PREFIX} Detected existing mount for ${filesystem.provider} ("${filesystem.id}") at "${mountPath}" with correct config, skipping`);
|
|
990
|
+
this.mounts.set(mountPath, {
|
|
991
|
+
state: "mounted",
|
|
992
|
+
config
|
|
993
|
+
});
|
|
994
|
+
return {
|
|
995
|
+
success: true,
|
|
996
|
+
mountPath
|
|
997
|
+
};
|
|
998
|
+
} else if (existingMount === "mismatched") {
|
|
999
|
+
this.logger.debug(`${LOG_PREFIX} Config mismatch at "${mountPath}", unmounting to re-mount with new config...`);
|
|
1000
|
+
await this.unmount(mountPath);
|
|
1001
|
+
} else if (existingMount === "unmanaged") {
|
|
1002
|
+
const error = `Mount path "${mountPath}" is already mounted by an unmanaged source`;
|
|
1003
|
+
this.logger.error(`${LOG_PREFIX} ${error}`);
|
|
1004
|
+
this.mounts.set(mountPath, {
|
|
1005
|
+
filesystem,
|
|
1006
|
+
state: "error",
|
|
1007
|
+
config,
|
|
1008
|
+
error
|
|
1009
|
+
});
|
|
1010
|
+
return {
|
|
1011
|
+
success: false,
|
|
1012
|
+
mountPath,
|
|
1013
|
+
error
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
this.mounts.set(mountPath, {
|
|
1017
|
+
filesystem,
|
|
1018
|
+
state: "mounting",
|
|
1019
|
+
config
|
|
1020
|
+
});
|
|
1021
|
+
this.logger.debug(`${LOG_PREFIX} Config type: ${config.type}`);
|
|
1022
|
+
try {
|
|
1023
|
+
const quotedPath = shellQuote(mountPath);
|
|
1024
|
+
if ((await runCommand(sandbox, `[ -d ${quotedPath} ] && ! mountpoint -q ${quotedPath} 2>/dev/null && [ "$(ls -A ${quotedPath} 2>/dev/null)" ] && echo "non-empty" || echo "ok"`, { timeout: MOUNT_COMMAND_TIMEOUT_MS })).output.trim() === "non-empty") {
|
|
1025
|
+
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.`;
|
|
1026
|
+
this.logger.error(`${LOG_PREFIX} ${error}`);
|
|
1027
|
+
this.mounts.set(mountPath, {
|
|
1028
|
+
filesystem,
|
|
1029
|
+
state: "error",
|
|
1030
|
+
config,
|
|
1031
|
+
error
|
|
1032
|
+
});
|
|
1033
|
+
return {
|
|
1034
|
+
success: false,
|
|
1035
|
+
mountPath,
|
|
1036
|
+
error
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
} catch {}
|
|
1040
|
+
this.logger.debug(`${LOG_PREFIX} Creating mount directory for "${mountPath}"...`);
|
|
1041
|
+
try {
|
|
1042
|
+
const quotedPath = shellQuote(mountPath);
|
|
1043
|
+
const mkdirResult = await runCommand(sandbox, `mountpoint -q ${quotedPath} 2>/dev/null && sudo mount -t tmpfs tmpfs ${quotedPath} 2>/dev/null; sudo mkdir -p ${quotedPath} 2>/dev/null; sudo chown $(id -u):$(id -g) ${quotedPath}`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1044
|
+
if (mkdirResult.exitCode !== 0) {
|
|
1045
|
+
const error = mkdirResult.output || "Failed to create mount directory";
|
|
1046
|
+
this.logger.debug(`${LOG_PREFIX} mkdir error for "${mountPath}":`, error);
|
|
1047
|
+
this.mounts.set(mountPath, {
|
|
1048
|
+
filesystem,
|
|
1049
|
+
state: "error",
|
|
1050
|
+
config,
|
|
1051
|
+
error
|
|
1052
|
+
});
|
|
1053
|
+
return {
|
|
1054
|
+
success: false,
|
|
1055
|
+
mountPath,
|
|
1056
|
+
error
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
const error = `Failed to create mount directory: ${err}`;
|
|
1061
|
+
this.mounts.set(mountPath, {
|
|
1062
|
+
filesystem,
|
|
1063
|
+
state: "error",
|
|
1064
|
+
config,
|
|
1065
|
+
error
|
|
1066
|
+
});
|
|
1067
|
+
return {
|
|
1068
|
+
success: false,
|
|
1069
|
+
mountPath,
|
|
1070
|
+
error
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const mountCtx = {
|
|
1074
|
+
run: async (cmd, timeoutMs) => {
|
|
1075
|
+
const result = await runCommand(sandbox, cmd, timeoutMs !== void 0 ? { timeout: timeoutMs } : void 0);
|
|
1076
|
+
return {
|
|
1077
|
+
exitCode: result.exitCode,
|
|
1078
|
+
stdout: result.output,
|
|
1079
|
+
stderr: result.exitCode !== 0 ? result.output : ""
|
|
1080
|
+
};
|
|
1081
|
+
},
|
|
1082
|
+
writeFile: async (path, content) => {
|
|
1083
|
+
await sandbox.fs.uploadFile(Buffer.from(content), path);
|
|
1084
|
+
},
|
|
1085
|
+
logger: this.logger
|
|
1086
|
+
};
|
|
1087
|
+
try {
|
|
1088
|
+
switch (config.type) {
|
|
1089
|
+
case "s3":
|
|
1090
|
+
this.logger.debug(`${LOG_PREFIX} Mounting S3 at "${mountPath}"...`);
|
|
1091
|
+
await mountS3(mountPath, config, mountCtx);
|
|
1092
|
+
this.logger.debug(`${LOG_PREFIX} Mounted S3 bucket at ${mountPath}`);
|
|
1093
|
+
break;
|
|
1094
|
+
case "gcs":
|
|
1095
|
+
this.logger.debug(`${LOG_PREFIX} Mounting GCS at "${mountPath}"...`);
|
|
1096
|
+
await mountGCS(mountPath, config, mountCtx);
|
|
1097
|
+
this.logger.debug(`${LOG_PREFIX} Mounted GCS bucket at ${mountPath}`);
|
|
1098
|
+
break;
|
|
1099
|
+
case "azure-blob":
|
|
1100
|
+
this.logger.debug(`${LOG_PREFIX} Mounting Azure Blob at "${mountPath}"...`);
|
|
1101
|
+
await mountAzure(mountPath, config, mountCtx);
|
|
1102
|
+
this.logger.debug(`${LOG_PREFIX} Mounted Azure Blob container at ${mountPath}`);
|
|
1103
|
+
break;
|
|
1104
|
+
default: {
|
|
1105
|
+
const error = `Unsupported mount type: ${config.type}`;
|
|
1106
|
+
this.mounts.set(mountPath, {
|
|
1107
|
+
filesystem,
|
|
1108
|
+
state: "unsupported",
|
|
1109
|
+
config,
|
|
1110
|
+
error
|
|
1111
|
+
});
|
|
1112
|
+
return {
|
|
1113
|
+
success: false,
|
|
1114
|
+
mountPath,
|
|
1115
|
+
error
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
this.logger.error(`${LOG_PREFIX} Error mounting "${filesystem.provider}" (${filesystem.id}) at "${mountPath}":`, error);
|
|
1121
|
+
this.mounts.set(mountPath, {
|
|
1122
|
+
filesystem,
|
|
1123
|
+
state: "error",
|
|
1124
|
+
config,
|
|
1125
|
+
error: errorToString(error)
|
|
1126
|
+
});
|
|
1127
|
+
await runCommand(sandbox, `sudo rmdir ${shellQuote(mountPath)} 2>/dev/null || true`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1128
|
+
this.logger.debug(`${LOG_PREFIX} Cleaned up directory after failed mount: ${mountPath}`);
|
|
1129
|
+
return {
|
|
1130
|
+
success: false,
|
|
1131
|
+
mountPath,
|
|
1132
|
+
error: errorToString(error)
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
this.mounts.set(mountPath, {
|
|
1136
|
+
state: "mounted",
|
|
1137
|
+
config
|
|
1138
|
+
});
|
|
1139
|
+
await this.writeMarkerFile(mountPath);
|
|
1140
|
+
this.logger.debug(`${LOG_PREFIX} Mounted "${mountPath}"`);
|
|
1141
|
+
return {
|
|
1142
|
+
success: true,
|
|
1143
|
+
mountPath
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Unmount a filesystem from a path in the sandbox.
|
|
1148
|
+
*/
|
|
1149
|
+
async unmount(mountPath) {
|
|
1150
|
+
validateMountPath(mountPath);
|
|
1151
|
+
if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
1152
|
+
const sandbox = this._sandbox;
|
|
1153
|
+
this.logger.debug(`${LOG_PREFIX} Unmounting "${mountPath}"...`);
|
|
1154
|
+
const quotedPath = shellQuote(mountPath);
|
|
1155
|
+
await runCommand(sandbox, `sudo fusermount -u ${quotedPath} 2>/dev/null; sudo umount -l ${quotedPath} 2>/dev/null; mountpoint -q ${quotedPath} 2>/dev/null && { _p="/tmp/.mastra-defunct-$$"; sudo mkdir -p "$_p" && sudo mount --move ${quotedPath} "$_p" 2>/dev/null; sudo umount -l "$_p" 2>/dev/null; sudo rmdir "$_p" 2>/dev/null; }`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1156
|
+
this.mounts.delete(mountPath);
|
|
1157
|
+
const rmdirResult = await runCommand(sandbox, `rm -f ${shellQuote(`/tmp/.mastra-mounts/${this.mounts.markerFilename(mountPath)}`)} 2>/dev/null; sudo rmdir ${quotedPath} 2>&1`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1158
|
+
if (rmdirResult.exitCode === 0) this.logger.debug(`${LOG_PREFIX} Unmounted and removed ${mountPath}`);
|
|
1159
|
+
else this.logger.debug(`${LOG_PREFIX} Unmounted ${mountPath} (directory not removed: ${rmdirResult.output.trim() || "not empty"})`);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Unmount all stale mounts that are not in the expected mounts list.
|
|
1163
|
+
* Also cleans up orphaned directories and marker files from failed mount attempts.
|
|
1164
|
+
* Call this after reconnecting to an existing sandbox to clean up old mounts.
|
|
1165
|
+
*/
|
|
1166
|
+
async reconcileMounts(expectedMountPaths) {
|
|
1167
|
+
if (!this._sandbox) return;
|
|
1168
|
+
const sandbox = this._sandbox;
|
|
1169
|
+
this.logger.debug(`${LOG_PREFIX} Reconciling mounts. Expected paths:`, expectedMountPaths);
|
|
1170
|
+
let currentMounts = [];
|
|
1171
|
+
try {
|
|
1172
|
+
currentMounts = (await runCommand(sandbox, `grep -E 'fuse\\.(s3fs|gcsfuse|blobfuse2)' /proc/mounts | awk '{print $2}'`, { timeout: MOUNT_COMMAND_TIMEOUT_MS })).output.trim().split("\n").filter((p) => p.length > 0);
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
this.logger.debug(`${LOG_PREFIX} Could not read /proc/mounts: ${err}`);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
this.logger.debug(`${LOG_PREFIX} Current FUSE mounts in sandbox:`, currentMounts);
|
|
1178
|
+
let markerFiles = [];
|
|
1179
|
+
try {
|
|
1180
|
+
markerFiles = (await runCommand(sandbox, "ls /tmp/.mastra-mounts/ 2>/dev/null || echo \"\"", { timeout: MOUNT_COMMAND_TIMEOUT_MS })).output.trim().split("\n").filter((f) => f.length > 0 && SAFE_MARKER_NAME.test(f));
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
this.logger.debug(`${LOG_PREFIX} Could not read marker files: ${err}`);
|
|
1183
|
+
}
|
|
1184
|
+
const managedMountPaths = /* @__PURE__ */ new Map();
|
|
1185
|
+
for (const markerFile of markerFiles) {
|
|
1186
|
+
const markerResult = await runCommand(sandbox, `cat "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || echo ""`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1187
|
+
const parsed = this.mounts.parseMarkerContent(markerResult.output.trim());
|
|
1188
|
+
if (parsed && SAFE_MOUNT_PATH.test(parsed.path)) managedMountPaths.set(parsed.path, markerFile);
|
|
1189
|
+
}
|
|
1190
|
+
const staleMounts = currentMounts.filter((path) => !expectedMountPaths.includes(path));
|
|
1191
|
+
for (const stalePath of staleMounts) if (managedMountPaths.has(stalePath)) {
|
|
1192
|
+
this.logger.debug(`${LOG_PREFIX} Found stale managed FUSE mount at "${stalePath}", unmounting...`);
|
|
1193
|
+
try {
|
|
1194
|
+
await this.unmount(stalePath);
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
this.logger.debug(`${LOG_PREFIX} Failed to unmount stale mount at "${stalePath}": ${err}`);
|
|
1197
|
+
}
|
|
1198
|
+
} else this.logger.debug(`${LOG_PREFIX} Found external FUSE mount at "${stalePath}", leaving untouched`);
|
|
1199
|
+
try {
|
|
1200
|
+
const expectedMarkerFiles = new Set(expectedMountPaths.map((p) => this.mounts.markerFilename(p)));
|
|
1201
|
+
const markerToPath = /* @__PURE__ */ new Map();
|
|
1202
|
+
for (const [path, file] of managedMountPaths) markerToPath.set(file, path);
|
|
1203
|
+
for (const markerFile of markerFiles) if (!expectedMarkerFiles.has(markerFile)) {
|
|
1204
|
+
const mountPath = markerToPath.get(markerFile);
|
|
1205
|
+
if (mountPath) {
|
|
1206
|
+
if (!currentMounts.includes(mountPath)) {
|
|
1207
|
+
this.logger.debug(`${LOG_PREFIX} Cleaning up orphaned marker and directory for ${mountPath}`);
|
|
1208
|
+
await runCommand(sandbox, `rm -f "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null; sudo rmdir ${shellQuote(mountPath)} 2>/dev/null`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1209
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
this.logger.debug(`${LOG_PREFIX} Removing malformed marker file: ${markerFile}`);
|
|
1212
|
+
await runCommand(sandbox, `rm -f "/tmp/.mastra-mounts/${markerFile}" 2>/dev/null || true`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
} catch {
|
|
1216
|
+
this.logger.debug(`${LOG_PREFIX} Error during orphan cleanup (non-fatal)`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Write marker file for detecting config changes on reconnect.
|
|
1221
|
+
* Stores both the mount path and config hash in the file.
|
|
1222
|
+
*/
|
|
1223
|
+
async writeMarkerFile(mountPath) {
|
|
1224
|
+
if (!this._sandbox) return;
|
|
1225
|
+
const markerContent = this.mounts.getMarkerContent(mountPath);
|
|
1226
|
+
if (!markerContent) return;
|
|
1227
|
+
const markerPath = `/tmp/.mastra-mounts/${this.mounts.markerFilename(mountPath)}`;
|
|
1228
|
+
try {
|
|
1229
|
+
await runCommand(this._sandbox, "mkdir -p /tmp/.mastra-mounts", { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1230
|
+
await this._sandbox.fs.uploadFile(Buffer.from(markerContent, "utf-8"), markerPath);
|
|
1231
|
+
} catch {
|
|
1232
|
+
this.logger.debug(`${LOG_PREFIX} Warning: Could not write marker file at ${markerPath}`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Check if a path is already mounted and whether the config matches.
|
|
1237
|
+
*
|
|
1238
|
+
* @param mountPath - The mount path to check
|
|
1239
|
+
* @param newConfig - The new config to compare against the stored config
|
|
1240
|
+
* @returns 'not_mounted' | 'matching' | 'mismatched' | 'unmanaged'
|
|
1241
|
+
*/
|
|
1242
|
+
async checkExistingMount(mountPath, newConfig) {
|
|
1243
|
+
if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
1244
|
+
const sandbox = this._sandbox;
|
|
1245
|
+
try {
|
|
1246
|
+
if ((await runCommand(sandbox, `mountpoint -q ${shellQuote(mountPath)} && echo "mounted" || echo "not mounted"`, { timeout: MOUNT_COMMAND_TIMEOUT_MS })).output.trim() !== "mounted") return "not_mounted";
|
|
1247
|
+
} catch {
|
|
1248
|
+
return "not_mounted";
|
|
1249
|
+
}
|
|
1250
|
+
const markerPath = `/tmp/.mastra-mounts/${this.mounts.markerFilename(mountPath)}`;
|
|
1251
|
+
let parsed;
|
|
1252
|
+
try {
|
|
1253
|
+
const markerResult = await runCommand(sandbox, `cat ${shellQuote(markerPath)} 2>/dev/null || echo ""`, { timeout: MOUNT_COMMAND_TIMEOUT_MS });
|
|
1254
|
+
parsed = this.mounts.parseMarkerContent(markerResult.output.trim());
|
|
1255
|
+
} catch {
|
|
1256
|
+
return "unmanaged";
|
|
1257
|
+
}
|
|
1258
|
+
if (!parsed) return "unmanaged";
|
|
1259
|
+
const newConfigHash = this.mounts.computeConfigHash(newConfig);
|
|
1260
|
+
this.logger.debug(`${LOG_PREFIX} Marker check - stored hash: "${parsed.configHash}", new config hash: "${newConfigHash}"`);
|
|
1261
|
+
if (parsed.path === mountPath && parsed.configHash === newConfigHash) return "matching";
|
|
1262
|
+
return "mismatched";
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Try to find and reconnect to an existing Daytona sandbox by ID.
|
|
1266
|
+
* Returns the sandbox if found and usable, or null if a fresh one should
|
|
1267
|
+
* be created.
|
|
1268
|
+
*/
|
|
1269
|
+
async detectWorkingDir() {
|
|
1270
|
+
if (!this._sandbox) return;
|
|
1271
|
+
try {
|
|
1272
|
+
const dir = (await runCommand(this._sandbox, "pwd", { timeout: MOUNT_COMMAND_TIMEOUT_MS })).output?.trim();
|
|
1273
|
+
if (dir) {
|
|
1274
|
+
this._workingDir = dir;
|
|
1275
|
+
this.logger.debug(`${LOG_PREFIX} Detected working directory: ${dir}`);
|
|
1276
|
+
}
|
|
1277
|
+
} catch {
|
|
1278
|
+
this.logger.debug(`${LOG_PREFIX} Could not detect working directory, will omit from instructions`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Look up the existing Daytona sandbox by identity WITHOUT starting it.
|
|
1283
|
+
* Used for detached (cross-process) networking/stop/destroy so those
|
|
1284
|
+
* operations never wake a stopped sandbox. Returns null when not found.
|
|
1285
|
+
*/
|
|
1286
|
+
async lookupDetachedSandbox() {
|
|
1287
|
+
const lookupKey = this._daytonaSandboxId ?? this.sandboxName;
|
|
1288
|
+
if (!lookupKey) return null;
|
|
1289
|
+
if (!this._daytona) this._daytona = new _daytonaio_sdk.Daytona(this.connectionOpts);
|
|
1290
|
+
try {
|
|
1291
|
+
return await this._daytona.get(lookupKey);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (error instanceof _daytonaio_sdk.DaytonaNotFoundError) return null;
|
|
1294
|
+
throw error;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async findExistingSandbox() {
|
|
1298
|
+
const DEAD_STATES = [
|
|
1299
|
+
_daytonaio_sdk.SandboxState.DESTROYED,
|
|
1300
|
+
_daytonaio_sdk.SandboxState.DESTROYING,
|
|
1301
|
+
_daytonaio_sdk.SandboxState.ERROR,
|
|
1302
|
+
_daytonaio_sdk.SandboxState.BUILD_FAILED
|
|
1303
|
+
];
|
|
1304
|
+
const lookupKey = this._daytonaSandboxId ?? this.sandboxName;
|
|
1305
|
+
if (!lookupKey) return null;
|
|
1306
|
+
let sandbox;
|
|
1307
|
+
try {
|
|
1308
|
+
sandbox = await this._daytona.get(lookupKey);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (error instanceof _daytonaio_sdk.DaytonaNotFoundError) {
|
|
1311
|
+
this._daytonaSandboxId = void 0;
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
throw error;
|
|
1315
|
+
}
|
|
1316
|
+
const state = sandbox.state;
|
|
1317
|
+
if (state && DEAD_STATES.includes(state)) {
|
|
1318
|
+
this.logger.debug(`${LOG_PREFIX} Existing sandbox ${sandbox.id} is dead (${state}), deleting and creating fresh`);
|
|
1319
|
+
try {
|
|
1320
|
+
await this._daytona.delete(sandbox);
|
|
1321
|
+
} catch {}
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
if (state !== _daytonaio_sdk.SandboxState.STARTED) {
|
|
1325
|
+
this.logger.debug(`${LOG_PREFIX} Restarting sandbox ${sandbox.id} (state: ${state})`);
|
|
1326
|
+
await this.waitForStableStateAndStart(sandbox);
|
|
1327
|
+
}
|
|
1328
|
+
return sandbox;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Transitional states where the Daytona API will reject start() with
|
|
1332
|
+
* "State change in progress". We poll until the sandbox reaches a stable
|
|
1333
|
+
* state before attempting start().
|
|
1334
|
+
*/
|
|
1335
|
+
static TRANSITIONAL_STATES = [
|
|
1336
|
+
_daytonaio_sdk.SandboxState.STARTING,
|
|
1337
|
+
_daytonaio_sdk.SandboxState.STOPPING,
|
|
1338
|
+
_daytonaio_sdk.SandboxState.CREATING,
|
|
1339
|
+
_daytonaio_sdk.SandboxState.RESTORING,
|
|
1340
|
+
_daytonaio_sdk.SandboxState.ARCHIVING,
|
|
1341
|
+
_daytonaio_sdk.SandboxState.RESIZING,
|
|
1342
|
+
_daytonaio_sdk.SandboxState.PULLING_SNAPSHOT,
|
|
1343
|
+
_daytonaio_sdk.SandboxState.BUILDING_SNAPSHOT
|
|
1344
|
+
];
|
|
1345
|
+
/**
|
|
1346
|
+
* Wait for the sandbox to leave a transitional state, then start it if needed.
|
|
1347
|
+
* Polls every 2s for up to 120s. If the sandbox reaches STARTED on its own
|
|
1348
|
+
* (e.g. it was STARTING), we skip the start() call. If start() still fails
|
|
1349
|
+
* with "State change in progress", we retry with backoff.
|
|
1350
|
+
*/
|
|
1351
|
+
async waitForStableStateAndStart(sandbox) {
|
|
1352
|
+
const MAX_WAIT_MS = 12e4;
|
|
1353
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
1354
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
1355
|
+
let current = sandbox;
|
|
1356
|
+
while (current.state && DaytonaSandbox.TRANSITIONAL_STATES.includes(current.state) && Date.now() < deadline) {
|
|
1357
|
+
this.logger.debug(`${LOG_PREFIX} Sandbox ${current.id} is in transitional state (${current.state}), waiting...`);
|
|
1358
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1359
|
+
current = await this._daytona.get(current.id);
|
|
1360
|
+
}
|
|
1361
|
+
if (current.state === _daytonaio_sdk.SandboxState.STARTED) {
|
|
1362
|
+
Object.assign(sandbox, current);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
while (Date.now() < deadline) try {
|
|
1366
|
+
await this._daytona.start(current);
|
|
1367
|
+
return;
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
if ((error instanceof Error ? error.message : String(error)).includes("State change in progress") && Date.now() < deadline) {
|
|
1370
|
+
this.logger.debug(`${LOG_PREFIX} start() returned "State change in progress", retrying...`);
|
|
1371
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1372
|
+
current = await this._daytona.get(current.id);
|
|
1373
|
+
if (current.state === _daytonaio_sdk.SandboxState.STARTED) {
|
|
1374
|
+
Object.assign(sandbox, current);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1381
|
+
await this._daytona.start(current);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Check if an error indicates the sandbox is dead/gone.
|
|
1385
|
+
* Uses DaytonaNotFoundError from the SDK when available,
|
|
1386
|
+
* with string fallback for edge cases.
|
|
1387
|
+
*
|
|
1388
|
+
* String patterns observed in @daytonaio/sdk@0.143.0 error messages.
|
|
1389
|
+
* Update if SDK error messages change in future versions.
|
|
1390
|
+
*/
|
|
1391
|
+
isSandboxDeadError(error) {
|
|
1392
|
+
if (!error) return false;
|
|
1393
|
+
if (error instanceof _daytonaio_sdk.DaytonaNotFoundError) return true;
|
|
1394
|
+
const errorStr = String(error);
|
|
1395
|
+
return SANDBOX_DEAD_PATTERNS.some((pattern) => pattern.test(errorStr));
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Handle sandbox timeout by clearing the instance and resetting state.
|
|
1399
|
+
*/
|
|
1400
|
+
handleSandboxTimeout() {
|
|
1401
|
+
this._sandbox = null;
|
|
1402
|
+
if (this.mounts) {
|
|
1403
|
+
for (const [path, entry] of this.mounts.entries) if (entry.state === "mounted" || entry.state === "mounting") this.mounts.set(path, { state: "pending" });
|
|
1404
|
+
}
|
|
1405
|
+
this.status = "stopped";
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Execute a function, retrying once if the sandbox is found to be dead.
|
|
1409
|
+
* Used by DaytonaProcessManager to handle stale sandboxes transparently.
|
|
1410
|
+
*/
|
|
1411
|
+
async retryOnDead(fn) {
|
|
1412
|
+
try {
|
|
1413
|
+
return await fn();
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
if (this.isSandboxDeadError(error) && !this._isRetrying) {
|
|
1416
|
+
this.handleSandboxTimeout();
|
|
1417
|
+
this._isRetrying = true;
|
|
1418
|
+
try {
|
|
1419
|
+
await this.ensureRunning();
|
|
1420
|
+
return await fn();
|
|
1421
|
+
} finally {
|
|
1422
|
+
this._isRetrying = false;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
throw error;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1563
1428
|
};
|
|
1564
|
-
|
|
1429
|
+
//#endregion
|
|
1565
1430
|
exports.DaytonaProcessManager = DaytonaProcessManager;
|
|
1566
1431
|
exports.DaytonaSandbox = DaytonaSandbox;
|
|
1567
|
-
|
|
1432
|
+
|
|
1568
1433
|
//# sourceMappingURL=index.cjs.map
|