@mastra/platform-workspace 0.2.2-alpha.0 → 0.2.2
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 +18 -0
- package/dist/direct-exec.d.ts +89 -0
- package/dist/direct-exec.d.ts.map +1 -0
- package/dist/index.cjs +259 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +259 -1
- package/dist/index.js.map +1 -1
- package/dist/sandbox.d.ts +42 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @mastra/platform
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- **Added:** Direct exec data plane for `PlatformSandbox`. ([#20326](https://github.com/mastra-ai/mastra/pull/20326))
|
|
8
|
+
|
|
9
|
+
Commands now execute against Railway's WebSocket endpoint directly using a short-lived JWT lease minted by the workspace proxy, instead of proxying every exec through the HTTP proxy. This removes the workspace proxy from the exec hot path — cutting latency for large-payload commands (e.g. `pnpm install`) and eliminating duplicated observability spans.
|
|
10
|
+
|
|
11
|
+
The change is transparent: `executeCommand` still returns the same `CommandResult` shape. If the proxy's `/exec-lease` endpoint is unavailable (older deployments), the client automatically falls back to the legacy `POST /sandbox/:id/exec` route for the lifetime of the sandbox.
|
|
12
|
+
|
|
13
|
+
- Fixed platform sandbox reattach and made provisioning resilient to transient proxy failures: ([#20294](https://github.com/mastra-ai/mastra/pull/20294))
|
|
14
|
+
|
|
15
|
+
- The workspace proxy assigns its own sandbox id on create (the advisory id in the request body is not honored), but `getInfo()` never exposed it, so callers persisting a reattach id (e.g. the Factory sandbox fleet, which reads `metadata.sandboxId`) stored the locally generated construction id instead. Every reattach then 404'd and each session open provisioned a brand-new sandbox and re-cloned the repository. `getInfo()` now reports the platform-assigned id in `metadata.sandboxId`, and `start()` treats a sandbox record with `destroyedAt` set as gone (falls through to a fresh provision) instead of pointing exec at a dead resource.
|
|
16
|
+
- Sandbox creation retries transient workspace-proxy 5xx responses with a short backoff. Provisioning intermittently fails with proxy 500s while the provider is under load; a retry keeps a single flaky window from failing the caller's whole workflow (e.g. Factory kickoff runs). Non-transient errors (4xx) still fail immediately.
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [[`ce93a3c`](https://github.com/mastra-ai/mastra/commit/ce93a3c114ea1cbfbd576f3db41d7c26c9844f5b), [`5718a22`](https://github.com/mastra-ai/mastra/commit/5718a229281dcfd36bcd1f42a242e3717e510a33), [`a211d09`](https://github.com/mastra-ai/mastra/commit/a211d09185dc65a746534914cf38b67f21ee9bac), [`0dca9d0`](https://github.com/mastra-ai/mastra/commit/0dca9d0b1356024a53b72ea6f040db528b126caa), [`6218217`](https://github.com/mastra-ai/mastra/commit/62182171b6cfca0b099f1c6a77a2e65e7639ab86), [`5807d3a`](https://github.com/mastra-ai/mastra/commit/5807d3ae1d259b8b7d6df7e5bf2b485c694af9c8), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`05db566`](https://github.com/mastra-ai/mastra/commit/05db566fcbdcbf33d0bffca0c72ec30129e2e3ca), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`5718a22`](https://github.com/mastra-ai/mastra/commit/5718a229281dcfd36bcd1f42a242e3717e510a33), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`d1b7e3a`](https://github.com/mastra-ai/mastra/commit/d1b7e3a978a309a5653eeaa490d2d6c7c53bd093), [`29c584a`](https://github.com/mastra-ai/mastra/commit/29c584a13a88831e5ed1fdeb0ff8e82eae180433), [`c093146`](https://github.com/mastra-ai/mastra/commit/c0931466404d3c521308ea119cb165bb7e695155), [`8124754`](https://github.com/mastra-ai/mastra/commit/8124754ae89fbc69f8136d1df4a91904d0f84c4e), [`d12b2e4`](https://github.com/mastra-ai/mastra/commit/d12b2e4023fd9e3d3e93a9169f5088bcee2a849c)]:
|
|
19
|
+
- @mastra/core@1.54.0
|
|
20
|
+
|
|
3
21
|
## 0.2.2-alpha.0
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct exec client — opens Railway's tcp-proxy exec WebSocket directly using
|
|
3
|
+
* a short-lived JWT minted by the workspace proxy's exec-lease endpoint. This
|
|
4
|
+
* removes the platform data plane from the exec stdout/stderr path entirely
|
|
5
|
+
* (see `docs/factory/direct-sandbox-connection.md` in the Platform repo),
|
|
6
|
+
* cutting payload-scaled Cloud Run egress and RTT for commands like
|
|
7
|
+
* `pnpm install` that stream tens of MB of output.
|
|
8
|
+
*
|
|
9
|
+
* The frame protocol below mirrors `connectExecWs()` in `railway@3.5.5`
|
|
10
|
+
* (`workspaces/railway/node_modules/railway/dist/index.js`). The `railway`
|
|
11
|
+
* SDK's version is pinned on both sides (platform + here); a version bump
|
|
12
|
+
* signals the protocol may have drifted and this module must be revisited.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Minimal WebSocket surface this module depends on. Matches both the browser
|
|
16
|
+
* `WebSocket` global and Node 22+'s built-in `WebSocket`. Extracted so tests
|
|
17
|
+
* can inject a fake without pulling in `ws` or jsdom.
|
|
18
|
+
*/
|
|
19
|
+
export interface DirectExecWebSocket {
|
|
20
|
+
binaryType: 'blob' | 'arraybuffer';
|
|
21
|
+
onopen: ((event: unknown) => void) | null;
|
|
22
|
+
onmessage: ((event: {
|
|
23
|
+
data: unknown;
|
|
24
|
+
}) => void) | null;
|
|
25
|
+
onclose: ((event: {
|
|
26
|
+
code: number;
|
|
27
|
+
reason: string;
|
|
28
|
+
}) => void) | null;
|
|
29
|
+
onerror: ((event: unknown) => void) | null;
|
|
30
|
+
send(data: string): void;
|
|
31
|
+
close(code?: number, reason?: string): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Factory that opens a WebSocket to `endpoint` with the given subprotocols.
|
|
35
|
+
* Defaults to the global `WebSocket` when omitted, which works on Node 22+
|
|
36
|
+
* (the package's minimum) and in the browser. Tests inject a fake here.
|
|
37
|
+
*/
|
|
38
|
+
export type DirectExecWebSocketFactory = (endpoint: string, subprotocols: string[]) => DirectExecWebSocket;
|
|
39
|
+
/** Lease payload returned by `POST /v1/projects/:projectId/sandbox/:sandboxId/exec-lease`. */
|
|
40
|
+
export interface ExecLease {
|
|
41
|
+
jwt: string;
|
|
42
|
+
wsEndpoint: string;
|
|
43
|
+
subprotocol: string;
|
|
44
|
+
/** ISO-8601 UTC. Null when the provider issues a JWT without an `exp` claim. */
|
|
45
|
+
expiresAt: string | null;
|
|
46
|
+
}
|
|
47
|
+
/** Inputs to a direct exec invocation. Mirrors the shape of the `/exec` route body. */
|
|
48
|
+
export interface DirectExecOptions {
|
|
49
|
+
command: string;
|
|
50
|
+
cwd?: string;
|
|
51
|
+
env?: Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Wall-clock cap for the exec. When elapsed, we close the socket and
|
|
54
|
+
* return `{timedOut: true, exitCode: 124}` matching the semantics of the
|
|
55
|
+
* proxy's `/exec` route.
|
|
56
|
+
*/
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
onStdout?: (chunk: string) => void;
|
|
59
|
+
onStderr?: (chunk: string) => void;
|
|
60
|
+
/** Injected for tests. Defaults to `globalThis.WebSocket`. */
|
|
61
|
+
webSocketFactory?: DirectExecWebSocketFactory;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Result of a direct exec. Shape matches the workspace-proxy `/exec` response
|
|
65
|
+
* so the caller (`PlatformSandbox.executeCommand`) can hand it back with no
|
|
66
|
+
* translation.
|
|
67
|
+
*
|
|
68
|
+
* `exitCode` is `null` when the socket closed without an `exit` frame AND the
|
|
69
|
+
* exec did not time out (rare — usually a mid-stream network drop). Callers
|
|
70
|
+
* currently coerce `null` to `1` upstream; kept nullable here to preserve the
|
|
71
|
+
* distinction for future observability.
|
|
72
|
+
*/
|
|
73
|
+
export interface DirectExecResult {
|
|
74
|
+
exitCode: number | null;
|
|
75
|
+
stdout: string;
|
|
76
|
+
stderr: string;
|
|
77
|
+
truncated: boolean;
|
|
78
|
+
timedOut: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Open the provider exec WebSocket using `lease`, run `command`, and resolve
|
|
82
|
+
* with the accumulated stdout/stderr + exit code. See the module docstring
|
|
83
|
+
* for the wire protocol reference.
|
|
84
|
+
*
|
|
85
|
+
* The client sends `stdin_close` immediately after `init_exec`, matching the
|
|
86
|
+
* SDK's own one-shot exec behavior — we never stream stdin from the caller.
|
|
87
|
+
*/
|
|
88
|
+
export declare function execViaLease(lease: ExecLease, options: DirectExecOptions): Promise<DirectExecResult>;
|
|
89
|
+
//# sourceMappingURL=direct-exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"direct-exec.d.ts","sourceRoot":"","sources":["../src/direct-exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACvD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,mBAAmB,CAAC;AAE3G,8FAA8F;AAC9F,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,uFAAuF;AACvF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;CAC/C;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAcD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6IpG"}
|
package/dist/index.cjs
CHANGED
|
@@ -358,7 +358,160 @@ function matchesExtension(name, extension) {
|
|
|
358
358
|
return (Array.isArray(extension) ? extension : [extension]).some((ext) => name.endsWith(ext));
|
|
359
359
|
}
|
|
360
360
|
//#endregion
|
|
361
|
+
//#region src/direct-exec.ts
|
|
362
|
+
/**
|
|
363
|
+
* Direct exec client — opens Railway's tcp-proxy exec WebSocket directly using
|
|
364
|
+
* a short-lived JWT minted by the workspace proxy's exec-lease endpoint. This
|
|
365
|
+
* removes the platform data plane from the exec stdout/stderr path entirely
|
|
366
|
+
* (see `docs/factory/direct-sandbox-connection.md` in the Platform repo),
|
|
367
|
+
* cutting payload-scaled Cloud Run egress and RTT for commands like
|
|
368
|
+
* `pnpm install` that stream tens of MB of output.
|
|
369
|
+
*
|
|
370
|
+
* The frame protocol below mirrors `connectExecWs()` in `railway@3.5.5`
|
|
371
|
+
* (`workspaces/railway/node_modules/railway/dist/index.js`). The `railway`
|
|
372
|
+
* SDK's version is pinned on both sides (platform + here); a version bump
|
|
373
|
+
* signals the protocol may have drifted and this module must be revisited.
|
|
374
|
+
*/
|
|
375
|
+
/** Byte-0 tag on binary WS frames for stdout output. */
|
|
376
|
+
const STDOUT_FRAME = 1;
|
|
377
|
+
/** Byte-0 tag on binary WS frames for stderr output. */
|
|
378
|
+
const STDERR_FRAME = 3;
|
|
379
|
+
/**
|
|
380
|
+
* Upper bound on how long we'll wait for the WebSocket to open when the
|
|
381
|
+
* caller didn't supply a `timeoutMs`. Guards against a stalled TLS/WS
|
|
382
|
+
* handshake leaving the promise unresolved forever. Not applied once the
|
|
383
|
+
* socket has opened — a caller with no timeout has opted in to unbounded
|
|
384
|
+
* command runtime, just not to unbounded connection setup.
|
|
385
|
+
*/
|
|
386
|
+
const HANDSHAKE_DEADLINE_MS = 3e4;
|
|
387
|
+
const DEFAULT_WS_FACTORY = (endpoint, subprotocols) => {
|
|
388
|
+
const WS = globalThis.WebSocket;
|
|
389
|
+
if (!WS) throw new Error("Direct exec requires a WebSocket implementation. Node 22+ provides one globally; on older runtimes, pass webSocketFactory explicitly.");
|
|
390
|
+
return new WS(endpoint, subprotocols);
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Open the provider exec WebSocket using `lease`, run `command`, and resolve
|
|
394
|
+
* with the accumulated stdout/stderr + exit code. See the module docstring
|
|
395
|
+
* for the wire protocol reference.
|
|
396
|
+
*
|
|
397
|
+
* The client sends `stdin_close` immediately after `init_exec`, matching the
|
|
398
|
+
* SDK's own one-shot exec behavior — we never stream stdin from the caller.
|
|
399
|
+
*/
|
|
400
|
+
function execViaLease(lease, options) {
|
|
401
|
+
const factory = options.webSocketFactory ?? DEFAULT_WS_FACTORY;
|
|
402
|
+
const stdoutDecoder = new TextDecoder();
|
|
403
|
+
const stderrDecoder = new TextDecoder();
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
let stdout = "";
|
|
406
|
+
let stderr = "";
|
|
407
|
+
let exitCode = null;
|
|
408
|
+
let timedOut = false;
|
|
409
|
+
let settled = false;
|
|
410
|
+
let opened = false;
|
|
411
|
+
let timer;
|
|
412
|
+
let handshakeTimer;
|
|
413
|
+
const settle = () => {
|
|
414
|
+
if (settled) return;
|
|
415
|
+
settled = true;
|
|
416
|
+
if (timer) clearTimeout(timer);
|
|
417
|
+
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
418
|
+
const stdoutTail = stdoutDecoder.decode();
|
|
419
|
+
if (stdoutTail) {
|
|
420
|
+
stdout += stdoutTail;
|
|
421
|
+
options.onStdout?.(stdoutTail);
|
|
422
|
+
}
|
|
423
|
+
const stderrTail = stderrDecoder.decode();
|
|
424
|
+
if (stderrTail) {
|
|
425
|
+
stderr += stderrTail;
|
|
426
|
+
options.onStderr?.(stderrTail);
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
socket.close(1e3, "");
|
|
430
|
+
} catch {}
|
|
431
|
+
resolve({
|
|
432
|
+
exitCode,
|
|
433
|
+
stdout,
|
|
434
|
+
stderr,
|
|
435
|
+
truncated: false,
|
|
436
|
+
timedOut
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) timer = setTimeout(() => {
|
|
440
|
+
timedOut = true;
|
|
441
|
+
if (exitCode === null) exitCode = 124;
|
|
442
|
+
settle();
|
|
443
|
+
}, options.timeoutMs);
|
|
444
|
+
else handshakeTimer = setTimeout(() => {
|
|
445
|
+
if (!opened) settle();
|
|
446
|
+
}, HANDSHAKE_DEADLINE_MS);
|
|
447
|
+
const socket = factory(lease.wsEndpoint, [lease.subprotocol, lease.jwt]);
|
|
448
|
+
socket.binaryType = "arraybuffer";
|
|
449
|
+
socket.onopen = () => {
|
|
450
|
+
opened = true;
|
|
451
|
+
if (handshakeTimer) {
|
|
452
|
+
clearTimeout(handshakeTimer);
|
|
453
|
+
handshakeTimer = void 0;
|
|
454
|
+
}
|
|
455
|
+
const data = { command: options.command };
|
|
456
|
+
if (options.cwd) data.cwd = options.cwd;
|
|
457
|
+
if (options.env && Object.keys(options.env).length > 0) data.env = options.env;
|
|
458
|
+
socket.send(JSON.stringify({
|
|
459
|
+
type: "init_exec",
|
|
460
|
+
data
|
|
461
|
+
}));
|
|
462
|
+
socket.send(JSON.stringify({ type: "stdin_close" }));
|
|
463
|
+
};
|
|
464
|
+
socket.onmessage = (event) => {
|
|
465
|
+
const { data } = event;
|
|
466
|
+
if (data instanceof ArrayBuffer) handleBinaryFrame(data);
|
|
467
|
+
else if (typeof data === "string") handleTextFrame(data);
|
|
468
|
+
};
|
|
469
|
+
socket.onclose = (event) => {
|
|
470
|
+
if (!opened) {
|
|
471
|
+
settle();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
settle();
|
|
475
|
+
};
|
|
476
|
+
socket.onerror = () => {
|
|
477
|
+
if (settled) return;
|
|
478
|
+
if (!opened) settle();
|
|
479
|
+
};
|
|
480
|
+
function handleBinaryFrame(buffer) {
|
|
481
|
+
const view = new Uint8Array(buffer);
|
|
482
|
+
if (view.length <= 1) return;
|
|
483
|
+
if (view[0] === STDOUT_FRAME) {
|
|
484
|
+
const chunk = stdoutDecoder.decode(view.subarray(1), { stream: true });
|
|
485
|
+
stdout += chunk;
|
|
486
|
+
options.onStdout?.(chunk);
|
|
487
|
+
} else if (view[0] === STDERR_FRAME) {
|
|
488
|
+
const chunk = stderrDecoder.decode(view.subarray(1), { stream: true });
|
|
489
|
+
stderr += chunk;
|
|
490
|
+
options.onStderr?.(chunk);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function handleTextFrame(text) {
|
|
494
|
+
let frame;
|
|
495
|
+
try {
|
|
496
|
+
frame = JSON.parse(text);
|
|
497
|
+
} catch {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (frame.type === "exit") {
|
|
501
|
+
exitCode = frame.data?.exit_code ?? 0;
|
|
502
|
+
settle();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
361
508
|
//#region src/sandbox.ts
|
|
509
|
+
/**
|
|
510
|
+
* How long before a lease's stated `expiresAt` we should treat it as
|
|
511
|
+
* expired. Avoids a race where the JWT is valid at cache-hit time but the
|
|
512
|
+
* server rejects it by the time the WebSocket handshake completes.
|
|
513
|
+
*/
|
|
514
|
+
const LEASE_REFRESH_MARGIN_MS = 6e4;
|
|
362
515
|
/** Max attempts for `POST /sandbox` when the proxy returns transient 5xx errors. */
|
|
363
516
|
const CREATE_MAX_ATTEMPTS = 3;
|
|
364
517
|
/** Base delay between create retries; multiplied by the attempt number. */
|
|
@@ -448,6 +601,30 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
448
601
|
_timeout;
|
|
449
602
|
_instructionsOverride;
|
|
450
603
|
_createdAt = null;
|
|
604
|
+
_webSocketFactory;
|
|
605
|
+
/**
|
|
606
|
+
* Cached exec lease for this sandbox. `null` before the first exec and
|
|
607
|
+
* after {@link destroy}. Refreshed when `expiresAt - LEASE_REFRESH_MARGIN_MS < now`
|
|
608
|
+
* (see {@link _ensureLease}); a lease without a disclosed `expiresAt`
|
|
609
|
+
* is refreshed on every call.
|
|
610
|
+
*/
|
|
611
|
+
_lease = null;
|
|
612
|
+
/**
|
|
613
|
+
* In-flight mint request; concurrent `_ensureLease` callers on a cold or
|
|
614
|
+
* near-expiry cache all await this single promise so we don't burn N
|
|
615
|
+
* `POST /exec-lease` round-trips when the sandbox is doing N parallel execs.
|
|
616
|
+
* Cleared (regardless of success or failure) when the request settles.
|
|
617
|
+
*/
|
|
618
|
+
_leaseInFlight = null;
|
|
619
|
+
/**
|
|
620
|
+
* Tri-state feature detection for the platform's exec-lease endpoint:
|
|
621
|
+
* undefined — not yet tried (default; try direct on first exec)
|
|
622
|
+
* true — endpoint present, use direct exec
|
|
623
|
+
* false — endpoint returned 404 or 501, fall back permanently to /exec
|
|
624
|
+
* Sticky per instance so we make the fallback decision once per sandbox
|
|
625
|
+
* lifetime instead of paying an extra round-trip on every exec.
|
|
626
|
+
*/
|
|
627
|
+
_directExecAvailable = void 0;
|
|
451
628
|
constructor(options = {}) {
|
|
452
629
|
super({
|
|
453
630
|
...options,
|
|
@@ -464,6 +641,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
464
641
|
this._env = options.env ?? {};
|
|
465
642
|
this._timeout = options.timeout;
|
|
466
643
|
this._instructionsOverride = options.instructions;
|
|
644
|
+
this._webSocketFactory = options.webSocketFactory;
|
|
467
645
|
}
|
|
468
646
|
generateId() {
|
|
469
647
|
return `platform-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -492,7 +670,8 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
492
670
|
...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
|
|
493
671
|
env: options.env ?? this._env,
|
|
494
672
|
...this._timeout !== void 0 && { timeout: this._timeout },
|
|
495
|
-
...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride }
|
|
673
|
+
...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride },
|
|
674
|
+
...this._webSocketFactory !== void 0 && { webSocketFactory: this._webSocketFactory }
|
|
496
675
|
});
|
|
497
676
|
}
|
|
498
677
|
async start() {
|
|
@@ -539,6 +718,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
539
718
|
await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`, { method: "DELETE" });
|
|
540
719
|
this._sandboxId = void 0;
|
|
541
720
|
this._createdAt = null;
|
|
721
|
+
this._lease = null;
|
|
542
722
|
}
|
|
543
723
|
/**
|
|
544
724
|
* Execute a command on the remote sandbox.
|
|
@@ -563,6 +743,51 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
563
743
|
const started = Date.now();
|
|
564
744
|
const fullCommand = buildCommand(command, args);
|
|
565
745
|
const effectiveTimeout = options?.timeout ?? this._timeout;
|
|
746
|
+
if (this._directExecAvailable !== false) {
|
|
747
|
+
const leaseResult = await this._tryDirectExec(fullCommand, effectiveTimeout, options);
|
|
748
|
+
if (leaseResult) return {
|
|
749
|
+
...leaseResult,
|
|
750
|
+
executionTimeMs: Date.now() - started
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return this._execViaProxy(fullCommand, effectiveTimeout, options, started);
|
|
754
|
+
}
|
|
755
|
+
async _tryDirectExec(fullCommand, effectiveTimeout, options) {
|
|
756
|
+
let lease;
|
|
757
|
+
try {
|
|
758
|
+
lease = await this._ensureLease();
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (error instanceof PlatformApiError && (error.status === 404 || error.status === 501)) {
|
|
761
|
+
this._directExecAvailable = false;
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
this._directExecAvailable = true;
|
|
767
|
+
const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
|
|
768
|
+
const result = await execViaLease(lease, {
|
|
769
|
+
command: fullCommand,
|
|
770
|
+
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
771
|
+
...filteredEnv !== void 0 && { env: filteredEnv },
|
|
772
|
+
...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
|
|
773
|
+
...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
|
|
774
|
+
});
|
|
775
|
+
if (result.exitCode === null && !result.timedOut) {
|
|
776
|
+
this._lease = null;
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
const exitCode = result.exitCode ?? 124;
|
|
780
|
+
return {
|
|
781
|
+
success: exitCode === 0,
|
|
782
|
+
exitCode,
|
|
783
|
+
stdout: result.stdout,
|
|
784
|
+
stderr: result.stderr,
|
|
785
|
+
timedOut: result.timedOut,
|
|
786
|
+
command: fullCommand
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async _execViaProxy(fullCommand, effectiveTimeout, options, started) {
|
|
790
|
+
if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
566
791
|
const timeoutSec = effectiveTimeout != null ? Math.ceil(effectiveTimeout / 1e3) : void 0;
|
|
567
792
|
const clientSignal = effectiveTimeout != null && effectiveTimeout > 0 ? AbortSignal.timeout(effectiveTimeout + 3e4) : void 0;
|
|
568
793
|
const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}/exec`, {
|
|
@@ -587,6 +812,39 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
587
812
|
command: fullCommand
|
|
588
813
|
};
|
|
589
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Return a cached exec lease, minting a fresh one when the cache is empty
|
|
817
|
+
* or the JWT is within {@link LEASE_REFRESH_MARGIN_MS} of `expiresAt`.
|
|
818
|
+
*
|
|
819
|
+
* Callers are expected to be on the "sandbox is running" path; we don't
|
|
820
|
+
* re-check `_sandboxId` here because `executeCommand` already gated on it.
|
|
821
|
+
*/
|
|
822
|
+
async _ensureLease() {
|
|
823
|
+
const now = Date.now();
|
|
824
|
+
if (this._lease && this._lease.expiresAtMs !== null && this._lease.expiresAtMs - LEASE_REFRESH_MARGIN_MS > now) return this._lease;
|
|
825
|
+
if (this._leaseInFlight) return this._leaseInFlight;
|
|
826
|
+
if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
|
|
827
|
+
const sandboxId = this._sandboxId;
|
|
828
|
+
const inFlight = (async () => {
|
|
829
|
+
const json = await (await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/exec-lease`, { method: "POST" })).json();
|
|
830
|
+
const expiresAtMs = json.expiresAt ? Date.parse(json.expiresAt) : null;
|
|
831
|
+
const lease = {
|
|
832
|
+
jwt: json.jwt,
|
|
833
|
+
wsEndpoint: json.wsEndpoint,
|
|
834
|
+
subprotocol: json.subprotocol,
|
|
835
|
+
expiresAt: json.expiresAt,
|
|
836
|
+
expiresAtMs: expiresAtMs !== null && !Number.isNaN(expiresAtMs) ? expiresAtMs : null
|
|
837
|
+
};
|
|
838
|
+
this._lease = lease;
|
|
839
|
+
return lease;
|
|
840
|
+
})();
|
|
841
|
+
this._leaseInFlight = inFlight;
|
|
842
|
+
try {
|
|
843
|
+
return await inFlight;
|
|
844
|
+
} finally {
|
|
845
|
+
if (this._leaseInFlight === inFlight) this._leaseInFlight = null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
590
848
|
async getInfo() {
|
|
591
849
|
if (!this._sandboxId) return {
|
|
592
850
|
id: this.id,
|