@mastra/blaxel 0.0.2 → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/LICENSE.md +15 -0
- package/README.md +1 -1
- package/dist/index.cjs +305 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +306 -33
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +14 -2
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/mounts/gcs.d.ts.map +1 -1
- package/dist/sandbox/mounts/s3.d.ts.map +1 -1
- package/dist/sandbox/mounts/types.d.ts +9 -0
- package/dist/sandbox/mounts/types.d.ts.map +1 -1
- package/dist/sandbox/process-manager.d.ts +27 -0
- package/dist/sandbox/process-manager.d.ts.map +1 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @mastra/blaxel
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Abort signal support in sandbox commands ([#13597](https://github.com/mastra-ai/mastra/pull/13597))
|
|
8
|
+
- Sandbox commands can now be cancelled via `abortSignal` in command options
|
|
9
|
+
- Partial stdout/stderr output is now preserved when a command is aborted or times out
|
|
10
|
+
|
|
11
|
+
- Added background process management support for Blaxel sandboxes. Agents can now spawn, monitor, and kill long-running processes using the standard `ProcessHandle` interface. ([`79177b1`](https://github.com/mastra-ai/mastra/commit/79177b1fa8c9221196290d38f6ed5e4c592dc4e2))
|
|
12
|
+
|
|
13
|
+
**Example usage:**
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
const sandbox = new BlaxelSandbox({ timeout: '5m' });
|
|
17
|
+
const workspace = new Workspace({ sandbox });
|
|
18
|
+
|
|
19
|
+
// Process manager is available via sandbox.processes
|
|
20
|
+
const handle = await sandbox.processes.spawn('python server.py');
|
|
21
|
+
|
|
22
|
+
// Monitor output
|
|
23
|
+
handle.onStdout(data => console.log(data));
|
|
24
|
+
|
|
25
|
+
// Check status
|
|
26
|
+
const info = await sandbox.processes.list();
|
|
27
|
+
|
|
28
|
+
// Kill when done
|
|
29
|
+
await handle.kill();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Note:** Process stdin is not supported in Blaxel sandboxes.
|
|
33
|
+
|
|
34
|
+
**Additional improvements:**
|
|
35
|
+
- Fixed detection of expired sandboxes, ensuring operations automatically retry when a sandbox has timed out
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- Fixed command timeouts in Blaxel sandboxes so long-running commands now respect configured limits. ([#13520](https://github.com/mastra-ai/mastra/pull/13520))
|
|
40
|
+
|
|
41
|
+
Changed the default Blaxel image to `blaxel/ts-app:latest` (Debian-based), which supports both S3 and GCS mounts out of the box.
|
|
42
|
+
|
|
43
|
+
Added distro detection for mount scripts so S3 mounts work on Alpine-based images (e.g. `blaxel/node:latest`) via `apk`, and GCS mounts give a clear error on Alpine since gcsfuse is unavailable.
|
|
44
|
+
|
|
45
|
+
Removed working directory from sandbox instructions to avoid breaking prompt caching.
|
|
46
|
+
|
|
47
|
+
- Remove internal `processes` field from sandbox provider options ([#13597](https://github.com/mastra-ai/mastra/pull/13597))
|
|
48
|
+
|
|
49
|
+
The `processes` field is no longer exposed in constructor options for E2B, Daytona, and Blaxel sandbox providers. This field is managed internally and was not intended to be user-configurable.
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [[`504fc8b`](https://github.com/mastra-ai/mastra/commit/504fc8b9d0ddab717577ad3bf9c95ea4bd5377bd), [`f9c150b`](https://github.com/mastra-ai/mastra/commit/f9c150b7595ad05ad9cc9a11098e2944361e8c22), [`88de7e8`](https://github.com/mastra-ai/mastra/commit/88de7e8dfe4b7e1951a9e441bb33136e705ce24e), [`edee4b3`](https://github.com/mastra-ai/mastra/commit/edee4b37dff0af515fc7cc0e8d71ee39e6a762f0), [`3790c75`](https://github.com/mastra-ai/mastra/commit/3790c7578cc6a47d854eb12d89e6b1912867fe29), [`e7a235b`](https://github.com/mastra-ai/mastra/commit/e7a235be6472e0c870ed6c791ddb17c492dc188b), [`d51d298`](https://github.com/mastra-ai/mastra/commit/d51d298953967aab1f58ec965b644d109214f085), [`6dbeeb9`](https://github.com/mastra-ai/mastra/commit/6dbeeb94a8b1eebb727300d1a98961f882180794), [`d5f0d8d`](https://github.com/mastra-ai/mastra/commit/d5f0d8d6a03e515ddaa9b5da19b7e44b8357b07b), [`09c3b18`](https://github.com/mastra-ai/mastra/commit/09c3b1802ff14e243a8a8baea327440bc8cc2e32), [`b896379`](https://github.com/mastra-ai/mastra/commit/b8963791c6afa79484645fcec596a201f936b9a2), [`85c84eb`](https://github.com/mastra-ai/mastra/commit/85c84ebb78aebfcba9d209c8e152b16d7a00cb71), [`a89272a`](https://github.com/mastra-ai/mastra/commit/a89272a5d71939b9fcd284e6a6dc1dd091a6bdcf), [`ee9c8df`](https://github.com/mastra-ai/mastra/commit/ee9c8df644f19d055af5f496bf4942705f5a47b7), [`77b4a25`](https://github.com/mastra-ai/mastra/commit/77b4a254e51907f8ff3a3ba95596a18e93ae4b35), [`276246e`](https://github.com/mastra-ai/mastra/commit/276246e0b9066a1ea48bbc70df84dbe528daaf99), [`08ecfdb`](https://github.com/mastra-ai/mastra/commit/08ecfdbdad6fb8285deef86a034bdf4a6047cfca), [`d5f628c`](https://github.com/mastra-ai/mastra/commit/d5f628ca86c6f6f3ff1035d52f635df32dd81cab), [`524c0f3`](https://github.com/mastra-ai/mastra/commit/524c0f3c434c3d9d18f66338dcef383d6161b59c), [`c18a0e9`](https://github.com/mastra-ai/mastra/commit/c18a0e9cef1e4ca004b2963d35e4cfc031971eac), [`4bd21ea`](https://github.com/mastra-ai/mastra/commit/4bd21ea43d44d0a0427414fc047577f9f0aa3bec), [`115a7a4`](https://github.com/mastra-ai/mastra/commit/115a7a47db5e9896fec12ae6507501adb9ec89bf), [`22a48ae`](https://github.com/mastra-ai/mastra/commit/22a48ae2513eb54d8d79dad361fddbca97a155e8), [`3c6ef79`](https://github.com/mastra-ai/mastra/commit/3c6ef798481e00d6d22563be2de98818fd4dd5e0), [`9311c17`](https://github.com/mastra-ai/mastra/commit/9311c17d7a0640d9c4da2e71b814dc67c57c6369), [`7edf78f`](https://github.com/mastra-ai/mastra/commit/7edf78f80422c43e84585f08ba11df0d4d0b73c5), [`1c4221c`](https://github.com/mastra-ai/mastra/commit/1c4221cf6032ec98d0e094d4ee11da3e48490d96), [`d25b9ea`](https://github.com/mastra-ai/mastra/commit/d25b9eabd400167255a97b690ffbc4ee4097ded5), [`fe1ce5c`](https://github.com/mastra-ai/mastra/commit/fe1ce5c9211c03d561606fda95cbfe7df1d9a9b5), [`b03c0e0`](https://github.com/mastra-ai/mastra/commit/b03c0e0389a799523929a458b0509c9e4244d562), [`0a8366b`](https://github.com/mastra-ai/mastra/commit/0a8366b0a692fcdde56c4d526e4cf03c502ae4ac), [`85664e9`](https://github.com/mastra-ai/mastra/commit/85664e9fd857320fbc245e301f764f45f66f32a3), [`bc79650`](https://github.com/mastra-ai/mastra/commit/bc796500c6e0334faa158a96077e3fb332274869), [`9257d01`](https://github.com/mastra-ai/mastra/commit/9257d01d1366d81f84c582fe02b5e200cf9621f4), [`3a3a59e`](https://github.com/mastra-ai/mastra/commit/3a3a59e8ffaa6a985fe3d9a126a3f5ade11a6724), [`3108d4e`](https://github.com/mastra-ai/mastra/commit/3108d4e649c9fddbf03253a6feeb388a5fa9fa5a), [`0c33b2c`](https://github.com/mastra-ai/mastra/commit/0c33b2c9db537f815e1c59e2c898ffce2e395a79), [`191e5bd`](https://github.com/mastra-ai/mastra/commit/191e5bd29b82f5bda35243945790da7bc7b695c2), [`f77cd94`](https://github.com/mastra-ai/mastra/commit/f77cd94c44eabed490384e7d19232a865e13214c), [`e8135c7`](https://github.com/mastra-ai/mastra/commit/e8135c7e300dac5040670eec7eab896ac6092e30), [`daca48f`](https://github.com/mastra-ai/mastra/commit/daca48f0fb17b7ae0b62a2ac40cf0e491b2fd0b7), [`257d14f`](https://github.com/mastra-ai/mastra/commit/257d14faca5931f2e4186fc165b6f0b1f915deee), [`352f25d`](https://github.com/mastra-ai/mastra/commit/352f25da316b24cdd5b410fd8dddf6a8b763da2a), [`93477d0`](https://github.com/mastra-ai/mastra/commit/93477d0769b8a13ea5ed73d508d967fb23eaeed9), [`31c78b3`](https://github.com/mastra-ai/mastra/commit/31c78b3eb28f58a8017f1dcc795c33214d87feac), [`0bc0720`](https://github.com/mastra-ai/mastra/commit/0bc07201095791858087cc56f353fcd65e87ab54), [`36516ac`](https://github.com/mastra-ai/mastra/commit/36516aca1021cbeb42e74751b46a2614101f37c8), [`e947652`](https://github.com/mastra-ai/mastra/commit/e9476527fdecb4449e54570e80dfaf8466901254), [`3c6ef79`](https://github.com/mastra-ai/mastra/commit/3c6ef798481e00d6d22563be2de98818fd4dd5e0), [`9257d01`](https://github.com/mastra-ai/mastra/commit/9257d01d1366d81f84c582fe02b5e200cf9621f4), [`ec248f6`](https://github.com/mastra-ai/mastra/commit/ec248f6b56e8a037c066c49b2178e2507471d988)]:
|
|
52
|
+
- @mastra/core@1.9.0-alpha.0
|
|
53
|
+
|
|
3
54
|
## 0.0.2
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
package/LICENSE.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Portions of this software are licensed as follows:
|
|
2
|
+
|
|
3
|
+
- All content that resides under any directory named "ee/" within this
|
|
4
|
+
repository, including but not limited to:
|
|
5
|
+
- `packages/core/src/auth/ee/`
|
|
6
|
+
- `packages/server/src/server/auth/ee/`
|
|
7
|
+
is licensed under the license defined in `ee/LICENSE`.
|
|
8
|
+
|
|
9
|
+
- All third-party components incorporated into the Mastra Software are
|
|
10
|
+
licensed under the original license provided by the owner of the
|
|
11
|
+
applicable component.
|
|
12
|
+
|
|
13
|
+
- Content outside of the above-mentioned directories or restrictions is
|
|
14
|
+
available under the "Apache License 2.0" as defined below.
|
|
15
|
+
|
|
1
16
|
# Apache License 2.0
|
|
2
17
|
|
|
3
18
|
Copyright (c) 2025 Kepler Software, Inc.
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ const agent = new Agent({
|
|
|
34
34
|
| Option | Type | Default | Description |
|
|
35
35
|
| ---------- | ------------------------------------- | ---------------------------- | ------------------------------------------------------ |
|
|
36
36
|
| `id` | `string` | auto-generated | Unique identifier for the sandbox instance |
|
|
37
|
-
| `image` | `string` | `'blaxel/
|
|
37
|
+
| `image` | `string` | `'blaxel/ts-app:latest'` | Docker image to use |
|
|
38
38
|
| `memory` | `number` | `4096` | Memory allocation in MB |
|
|
39
39
|
| `timeout` | `string` | `'5m'` | Sandbox TTL as a duration string (e.g. `'5m'`, `'1h'`) |
|
|
40
40
|
| `env` | `Record<string, string>` | — | Environment variables to set in the sandbox |
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,16 @@ function validateEndpoint(endpoint) {
|
|
|
37
37
|
throw new Error(`Invalid endpoint URL scheme: "${parsed.protocol}". Only http: and https: are allowed.`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
async function detectPackageManager(sandbox) {
|
|
41
|
+
const result = await runCommand(
|
|
42
|
+
sandbox,
|
|
43
|
+
'which apt-get >/dev/null 2>&1 && echo "apt" || (which apk >/dev/null 2>&1 && echo "apk" || echo "unknown")'
|
|
44
|
+
);
|
|
45
|
+
const pm = result.stdout.trim();
|
|
46
|
+
if (pm === "apt") return "apt";
|
|
47
|
+
if (pm === "apk") return "apk";
|
|
48
|
+
return "unknown";
|
|
49
|
+
}
|
|
40
50
|
async function runCommand(sandbox, command, options) {
|
|
41
51
|
const result = await sandbox.process.exec({
|
|
42
52
|
command,
|
|
@@ -59,25 +69,45 @@ async function mountS3(mountPath, config, ctx) {
|
|
|
59
69
|
if (checkResult.stdout.includes("not found")) {
|
|
60
70
|
logger.warn(`${LOG_PREFIX} s3fs not found, attempting runtime installation...`);
|
|
61
71
|
logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install s3fs in your sandbox image`);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
const pm = await detectPackageManager(sandbox);
|
|
73
|
+
logger.debug(`${LOG_PREFIX} Detected package manager: ${pm}`);
|
|
74
|
+
if (pm === "apt") {
|
|
75
|
+
const updateResult = await runCommand(sandbox, "apt-get update 2>&1", { timeout: 6e4 });
|
|
76
|
+
if (updateResult.exitCode !== 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Failed to update package lists for s3fs installation.
|
|
66
79
|
Error details: ${updateResult.stderr || updateResult.stdout}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const installResult = await runCommand(
|
|
83
|
+
sandbox,
|
|
84
|
+
"apt-get install -y s3fs fuse 2>&1 || apt-get install -y s3fs-fuse fuse 2>&1",
|
|
85
|
+
{ timeout: 12e4 }
|
|
67
86
|
);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"apt-get install -y s3fs fuse 2>&1 || apt-get install -y s3fs-fuse fuse 2>&1",
|
|
72
|
-
{ timeout: 12e4 }
|
|
73
|
-
);
|
|
74
|
-
if (installResult.exitCode !== 0) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
`Failed to install s3fs. For S3 mounting, your sandbox image needs s3fs and fuse packages.
|
|
87
|
+
if (installResult.exitCode !== 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Failed to install s3fs. For S3 mounting, your sandbox image needs s3fs and fuse packages.
|
|
77
90
|
|
|
78
91
|
Pre-install in your image: apt-get install -y s3fs fuse
|
|
79
92
|
|
|
80
93
|
Error details: ${installResult.stderr || installResult.stdout}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} else if (pm === "apk") {
|
|
97
|
+
const installResult = await runCommand(sandbox, "apk add --no-cache s3fs-fuse fuse 2>&1", { timeout: 12e4 });
|
|
98
|
+
if (installResult.exitCode !== 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Failed to install s3fs on Alpine Linux. Ensure the Alpine community repository is enabled.
|
|
101
|
+
|
|
102
|
+
Pre-install in your image: apk add --no-cache s3fs-fuse fuse
|
|
103
|
+
|
|
104
|
+
Error details: ${installResult.stderr || installResult.stdout}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Cannot install s3fs: no supported package manager found (need apt-get or apk).
|
|
110
|
+
Use a Debian-based image (e.g. blaxel/ts-app:latest) or Alpine-based image (e.g. blaxel/node:latest), or pre-install s3fs in your custom image.`
|
|
81
111
|
);
|
|
82
112
|
}
|
|
83
113
|
}
|
|
@@ -139,6 +169,26 @@ async function mountGCS(mountPath, config, ctx) {
|
|
|
139
169
|
const checkResult = await runCommand(sandbox, 'which gcsfuse || echo "not found"');
|
|
140
170
|
if (checkResult.stdout.includes("not found")) {
|
|
141
171
|
logger.warn(`${LOG_PREFIX} gcsfuse not found, attempting runtime installation...`);
|
|
172
|
+
const pm = await detectPackageManager(sandbox);
|
|
173
|
+
logger.debug(`${LOG_PREFIX} Detected package manager: ${pm}`);
|
|
174
|
+
if (pm === "apk") {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`gcsfuse is not available on Alpine Linux. Google only provides gcsfuse packages for Debian/Ubuntu.
|
|
177
|
+
|
|
178
|
+
Use a Debian-based Blaxel image for GCS mounts:
|
|
179
|
+
new BlaxelSandbox({ image: 'blaxel/ts-app:latest' })
|
|
180
|
+
new BlaxelSandbox({ image: 'blaxel/py-app:latest' })`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (pm !== "apt") {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Cannot install gcsfuse: no supported package manager found (need apt-get).
|
|
186
|
+
gcsfuse is only available on Debian/Ubuntu-based images.
|
|
187
|
+
|
|
188
|
+
Use a Debian-based Blaxel image:
|
|
189
|
+
new BlaxelSandbox({ image: 'blaxel/ts-app:latest' })`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
142
192
|
logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install gcsfuse in your sandbox image`);
|
|
143
193
|
const codenameResult = await runCommand(
|
|
144
194
|
sandbox,
|
|
@@ -209,6 +259,138 @@ ${installResult.stderr}`
|
|
|
209
259
|
throw new Error(`Failed to mount GCS bucket: ${result.stderr || result.stdout}`);
|
|
210
260
|
}
|
|
211
261
|
}
|
|
262
|
+
var BlaxelProcessHandle = class extends workspace.ProcessHandle {
|
|
263
|
+
pid;
|
|
264
|
+
_identifier;
|
|
265
|
+
_sandbox;
|
|
266
|
+
_startTime;
|
|
267
|
+
_exitCode;
|
|
268
|
+
_waitPromise = null;
|
|
269
|
+
_streamingDone = null;
|
|
270
|
+
_closeStream = null;
|
|
271
|
+
_killed = false;
|
|
272
|
+
constructor(pid, identifier, sandbox, startTime, options) {
|
|
273
|
+
super(options);
|
|
274
|
+
this.pid = pid;
|
|
275
|
+
this._identifier = identifier;
|
|
276
|
+
this._sandbox = sandbox;
|
|
277
|
+
this._startTime = startTime;
|
|
278
|
+
}
|
|
279
|
+
get exitCode() {
|
|
280
|
+
return this._exitCode;
|
|
281
|
+
}
|
|
282
|
+
/** @internal Set by the process manager after streaming starts. */
|
|
283
|
+
set streamControl(control) {
|
|
284
|
+
this._closeStream = control.close;
|
|
285
|
+
this._streamingDone = control.wait();
|
|
286
|
+
this._streamingDone.then(() => this._resolveExitCode()).catch(() => this._resolveExitCode());
|
|
287
|
+
}
|
|
288
|
+
/** Fetch exit code from Blaxel and set _exitCode. No-op if already set. */
|
|
289
|
+
async _resolveExitCode() {
|
|
290
|
+
if (this._exitCode !== void 0) return;
|
|
291
|
+
try {
|
|
292
|
+
const proc = await this._sandbox.process.get(this._identifier);
|
|
293
|
+
this._exitCode = proc.status === "completed" ? proc.exitCode ?? 0 : proc.exitCode ?? 1;
|
|
294
|
+
} catch {
|
|
295
|
+
if (this._exitCode === void 0) {
|
|
296
|
+
this._exitCode = 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async wait() {
|
|
301
|
+
if (!this._waitPromise) {
|
|
302
|
+
this._waitPromise = this._doWait();
|
|
303
|
+
}
|
|
304
|
+
return this._waitPromise;
|
|
305
|
+
}
|
|
306
|
+
async _doWait() {
|
|
307
|
+
if (this._streamingDone) {
|
|
308
|
+
await this._streamingDone.catch(() => {
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (this._killed) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
exitCode: this._exitCode ?? 137,
|
|
315
|
+
stdout: this.stdout,
|
|
316
|
+
stderr: this.stderr,
|
|
317
|
+
executionTimeMs: Date.now() - this._startTime
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
await this._resolveExitCode();
|
|
321
|
+
return {
|
|
322
|
+
success: this._exitCode === 0,
|
|
323
|
+
exitCode: this._exitCode ?? 1,
|
|
324
|
+
stdout: this.stdout,
|
|
325
|
+
stderr: this.stderr,
|
|
326
|
+
executionTimeMs: Date.now() - this._startTime
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async kill() {
|
|
330
|
+
if (this._exitCode !== void 0) return false;
|
|
331
|
+
this._killed = true;
|
|
332
|
+
this._exitCode = 137;
|
|
333
|
+
this._closeStream?.();
|
|
334
|
+
try {
|
|
335
|
+
await this._sandbox.process.kill(this._identifier);
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
async sendStdin(_data) {
|
|
341
|
+
throw new Error("Blaxel sandboxes do not support stdin");
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
var BlaxelProcessManager = class extends workspace.SandboxProcessManager {
|
|
345
|
+
constructor(opts = {}) {
|
|
346
|
+
super({ env: opts.env });
|
|
347
|
+
}
|
|
348
|
+
async spawn(command, options = {}) {
|
|
349
|
+
return this.sandbox.retryOnDead(async () => {
|
|
350
|
+
const blaxel = this.sandbox.instance;
|
|
351
|
+
const mergedEnv = { ...this.env, ...options.env };
|
|
352
|
+
const envs = Object.fromEntries(
|
|
353
|
+
Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
|
|
354
|
+
);
|
|
355
|
+
const result = await blaxel.process.exec({
|
|
356
|
+
command,
|
|
357
|
+
waitForCompletion: false,
|
|
358
|
+
workingDir: options.cwd,
|
|
359
|
+
...Object.keys(envs).length > 0 && { env: envs },
|
|
360
|
+
...options.timeout && { timeout: Math.ceil(options.timeout / 1e3) }
|
|
361
|
+
});
|
|
362
|
+
const identifier = result.pid;
|
|
363
|
+
const pid = parseInt(identifier, 10);
|
|
364
|
+
const handle = new BlaxelProcessHandle(pid, identifier, blaxel, Date.now(), options);
|
|
365
|
+
const streamControl = blaxel.process.streamLogs(identifier, {
|
|
366
|
+
onStdout: (data) => handle.emitStdout(data),
|
|
367
|
+
onStderr: (data) => handle.emitStderr(data),
|
|
368
|
+
onError: (err) => {
|
|
369
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
370
|
+
handle.emitStderr(msg);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
handle.streamControl = streamControl;
|
|
374
|
+
this._tracked.set(pid, handle);
|
|
375
|
+
return handle;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async list() {
|
|
379
|
+
const result = [];
|
|
380
|
+
for (const [pid, handle] of this._tracked) {
|
|
381
|
+
result.push({
|
|
382
|
+
pid,
|
|
383
|
+
command: handle.command,
|
|
384
|
+
running: handle.exitCode === void 0,
|
|
385
|
+
exitCode: handle.exitCode
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
async get(pid) {
|
|
391
|
+
return this._tracked.get(pid);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
212
394
|
|
|
213
395
|
// src/sandbox/index.ts
|
|
214
396
|
var SAFE_MOUNT_PATH = /^\/[a-zA-Z0-9_.\-/]+$/;
|
|
@@ -250,9 +432,13 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
250
432
|
ports;
|
|
251
433
|
// Non-optional (initialized by BaseSandbox)
|
|
252
434
|
constructor(options = {}) {
|
|
253
|
-
super({
|
|
435
|
+
super({
|
|
436
|
+
...options,
|
|
437
|
+
name: "BlaxelSandbox",
|
|
438
|
+
processes: new BlaxelProcessManager({ env: options.env })
|
|
439
|
+
});
|
|
254
440
|
this.id = options.id ?? this.generateId();
|
|
255
|
-
this.image = options.image ?? "blaxel/
|
|
441
|
+
this.image = options.image ?? "blaxel/ts-app:latest";
|
|
256
442
|
this.memory = options.memory ?? 4096;
|
|
257
443
|
this.timeout = options.timeout;
|
|
258
444
|
this.env = options.env ?? {};
|
|
@@ -595,7 +781,6 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
595
781
|
image: this.image,
|
|
596
782
|
memory: this.memory,
|
|
597
783
|
...this.timeout && { ttl: this.timeout },
|
|
598
|
-
envs: Object.entries(this.env).map(([name, value]) => ({ name, value })),
|
|
599
784
|
labels: {
|
|
600
785
|
...this.labels,
|
|
601
786
|
"mastra-sandbox-id": this.id
|
|
@@ -653,6 +838,13 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
653
838
|
* Status management is handled by the base class.
|
|
654
839
|
*/
|
|
655
840
|
async stop() {
|
|
841
|
+
if (this.processes) {
|
|
842
|
+
try {
|
|
843
|
+
const procs = await this.processes.list();
|
|
844
|
+
await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
|
|
845
|
+
} catch {
|
|
846
|
+
}
|
|
847
|
+
}
|
|
656
848
|
for (const mountPath of [...this.mounts.entries.keys()]) {
|
|
657
849
|
try {
|
|
658
850
|
await this.unmount(mountPath);
|
|
@@ -667,6 +859,13 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
667
859
|
* Status management is handled by the base class.
|
|
668
860
|
*/
|
|
669
861
|
async destroy() {
|
|
862
|
+
if (this.processes) {
|
|
863
|
+
try {
|
|
864
|
+
const procs = await this.processes.list();
|
|
865
|
+
await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
|
|
866
|
+
} catch {
|
|
867
|
+
}
|
|
868
|
+
}
|
|
670
869
|
for (const mountPath of [...this.mounts.entries.keys()]) {
|
|
671
870
|
try {
|
|
672
871
|
await this.unmount(mountPath);
|
|
@@ -717,7 +916,7 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
717
916
|
getInstructions() {
|
|
718
917
|
const mountCount = this.mounts.entries.size;
|
|
719
918
|
const mountInfo = mountCount > 0 ? ` ${mountCount} filesystem(s) mounted via FUSE.` : "";
|
|
720
|
-
return `Cloud sandbox
|
|
919
|
+
return `Cloud sandbox.${mountInfo}`;
|
|
721
920
|
}
|
|
722
921
|
// ---------------------------------------------------------------------------
|
|
723
922
|
// Internal Helpers
|
|
@@ -737,8 +936,8 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
737
936
|
*/
|
|
738
937
|
isSandboxDeadError(error) {
|
|
739
938
|
if (!error) return false;
|
|
740
|
-
const errorStr = errorToString(error);
|
|
741
|
-
return errorStr.includes("
|
|
939
|
+
const errorStr = errorToString(error).toLowerCase();
|
|
940
|
+
return errorStr.includes("terminated") || errorStr.includes("sandbox was not found") || errorStr.includes("sandbox not found") || errorStr.includes('"not found"');
|
|
742
941
|
}
|
|
743
942
|
/**
|
|
744
943
|
* Handle sandbox timeout by clearing the instance and resetting state.
|
|
@@ -756,6 +955,31 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
756
955
|
}
|
|
757
956
|
this.status = "stopped";
|
|
758
957
|
}
|
|
958
|
+
/**
|
|
959
|
+
* Execute an operation with automatic retry if the sandbox is found to be dead.
|
|
960
|
+
*
|
|
961
|
+
* When the Blaxel sandbox times out or crashes mid-operation, this method
|
|
962
|
+
* resets sandbox state, restarts it, and retries the operation once.
|
|
963
|
+
*
|
|
964
|
+
* @internal Used by BlaxelProcessManager to handle dead sandboxes during spawn.
|
|
965
|
+
*/
|
|
966
|
+
async retryOnDead(fn) {
|
|
967
|
+
try {
|
|
968
|
+
return await fn();
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (this.isSandboxDeadError(error) && !this._isRetrying) {
|
|
971
|
+
this.handleSandboxTimeout();
|
|
972
|
+
this._isRetrying = true;
|
|
973
|
+
try {
|
|
974
|
+
await this.ensureRunning();
|
|
975
|
+
return await fn();
|
|
976
|
+
} finally {
|
|
977
|
+
this._isRetrying = false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
759
983
|
// ---------------------------------------------------------------------------
|
|
760
984
|
// Command Execution
|
|
761
985
|
// ---------------------------------------------------------------------------
|
|
@@ -770,26 +994,77 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
|
|
|
770
994
|
const startTime = Date.now();
|
|
771
995
|
const fullCommand = args.length > 0 ? `${command} ${args.map(shellQuote).join(" ")}` : command;
|
|
772
996
|
this.logger.debug(`${LOG_PREFIX} Executing: ${fullCommand}`);
|
|
997
|
+
let capturedStdout = "";
|
|
998
|
+
let capturedStderr = "";
|
|
773
999
|
try {
|
|
774
1000
|
const mergedEnv = { ...this.env, ...options.env };
|
|
775
1001
|
const envRecord = Object.fromEntries(
|
|
776
1002
|
Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
|
|
777
1003
|
);
|
|
778
|
-
const
|
|
1004
|
+
const apiTimeout = options.timeout ? Math.ceil(options.timeout / 1e3) : void 0;
|
|
1005
|
+
const execPromise = sandbox.process.exec({
|
|
779
1006
|
command: fullCommand,
|
|
780
1007
|
workingDir: options.cwd,
|
|
781
1008
|
env: envRecord,
|
|
782
1009
|
waitForCompletion: true,
|
|
783
|
-
...
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
1010
|
+
...apiTimeout && { timeout: apiTimeout },
|
|
1011
|
+
onStdout: (data) => {
|
|
1012
|
+
capturedStdout += data;
|
|
1013
|
+
options.onStdout?.(data);
|
|
1014
|
+
},
|
|
1015
|
+
onStderr: (data) => {
|
|
1016
|
+
capturedStderr += data;
|
|
1017
|
+
options.onStderr?.(data);
|
|
1018
|
+
}
|
|
788
1019
|
});
|
|
1020
|
+
const racePromises = [];
|
|
1021
|
+
let timer;
|
|
1022
|
+
let abortHandler;
|
|
1023
|
+
if (options.timeout) {
|
|
1024
|
+
racePromises.push(
|
|
1025
|
+
new Promise((_, reject) => {
|
|
1026
|
+
timer = setTimeout(() => {
|
|
1027
|
+
runCommand(sandbox, `pkill -f ${shellQuote(fullCommand)}`, { timeout: 5e3 }).catch(() => {
|
|
1028
|
+
});
|
|
1029
|
+
reject(new Error(`Command timed out after ${options.timeout}ms`));
|
|
1030
|
+
}, options.timeout);
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
if (options.abortSignal) {
|
|
1035
|
+
if (options.abortSignal.aborted) {
|
|
1036
|
+
runCommand(sandbox, `pkill -f ${shellQuote(fullCommand)}`, { timeout: 5e3 }).catch(() => {
|
|
1037
|
+
});
|
|
1038
|
+
throw new Error("Process aborted");
|
|
1039
|
+
}
|
|
1040
|
+
racePromises.push(
|
|
1041
|
+
new Promise((_, reject) => {
|
|
1042
|
+
abortHandler = () => {
|
|
1043
|
+
runCommand(sandbox, `pkill -f ${shellQuote(fullCommand)}`, { timeout: 5e3 }).catch(() => {
|
|
1044
|
+
});
|
|
1045
|
+
reject(new Error("Process aborted"));
|
|
1046
|
+
};
|
|
1047
|
+
options.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
1048
|
+
})
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
let result;
|
|
1052
|
+
try {
|
|
1053
|
+
if (racePromises.length > 0) {
|
|
1054
|
+
result = await Promise.race([execPromise, ...racePromises]);
|
|
1055
|
+
} else {
|
|
1056
|
+
result = await execPromise;
|
|
1057
|
+
}
|
|
1058
|
+
} finally {
|
|
1059
|
+
if (timer) clearTimeout(timer);
|
|
1060
|
+
if (abortHandler && options.abortSignal) {
|
|
1061
|
+
options.abortSignal.removeEventListener("abort", abortHandler);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
789
1064
|
const executionTimeMs = Date.now() - startTime;
|
|
790
1065
|
const exitCode = result.exitCode ?? 0;
|
|
791
|
-
const stdout = result.stdout
|
|
792
|
-
const stderr = result.stderr
|
|
1066
|
+
const stdout = capturedStdout || result.stdout || "";
|
|
1067
|
+
const stderr = capturedStderr || result.stderr || "";
|
|
793
1068
|
this.logger.debug(`${LOG_PREFIX} Exit code: ${exitCode} (${executionTimeMs}ms)`);
|
|
794
1069
|
if (stdout) this.logger.debug(`${LOG_PREFIX} stdout:
|
|
795
1070
|
${stdout}`);
|
|
@@ -815,13 +1090,11 @@ ${stderr}`);
|
|
|
815
1090
|
}
|
|
816
1091
|
}
|
|
817
1092
|
const executionTimeMs = Date.now() - startTime;
|
|
818
|
-
const stderr = errorToString(error);
|
|
819
|
-
this.logger.debug(`${LOG_PREFIX} Execution error (${executionTimeMs}ms): ${stderr}`);
|
|
820
1093
|
return {
|
|
821
1094
|
success: false,
|
|
822
1095
|
exitCode: 1,
|
|
823
|
-
stdout:
|
|
824
|
-
stderr,
|
|
1096
|
+
stdout: capturedStdout,
|
|
1097
|
+
stderr: capturedStderr || errorToString(error),
|
|
825
1098
|
executionTimeMs,
|
|
826
1099
|
command,
|
|
827
1100
|
args
|
|
@@ -830,6 +1103,7 @@ ${stderr}`);
|
|
|
830
1103
|
}
|
|
831
1104
|
};
|
|
832
1105
|
|
|
1106
|
+
exports.BlaxelProcessManager = BlaxelProcessManager;
|
|
833
1107
|
exports.BlaxelSandbox = BlaxelSandbox;
|
|
834
1108
|
//# sourceMappingURL=index.cjs.map
|
|
835
1109
|
//# sourceMappingURL=index.cjs.map
|