@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 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,