@mastra/platform-workspace 0.2.4-alpha.0 → 1.0.0-alpha.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/dist/index.js CHANGED
@@ -15,7 +15,7 @@ function requireOption(value, name) {
15
15
  }
16
16
  function resolvePlatformOptions(options) {
17
17
  return {
18
- accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_SECRET_KEY ?? process.env.MASTRA_PLATFORM_ACCESS_TOKEN, "accessToken"),
18
+ accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_ACCESS_TOKEN, "accessToken"),
19
19
  projectId: requireOption(options.projectId ?? process.env.MASTRA_PROJECT_ID, "projectId"),
20
20
  proxyUrl: (process.env.MASTRA_WORKSPACE_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/$/, ""),
21
21
  fetch: options.fetch ?? fetch
@@ -500,6 +500,59 @@ const CREATE_MAX_ATTEMPTS = 3;
500
500
  /** Base delay between create retries; multiplied by the attempt number. */
501
501
  const CREATE_RETRY_BASE_DELAY_MS = 2e3;
502
502
  /**
503
+ * Diagnostic error thrown when the direct-exec WebSocket transport fails
504
+ * twice in a row (opening handshake refused or socket closed mid-stream
505
+ * without an `exit` frame). Distinguishes "the sandbox transport is broken"
506
+ * from "your command failed" so callers can decide whether to retry at a
507
+ * higher level (e.g. reprovision the sandbox) or surface the error.
508
+ *
509
+ * `opened` is `true` when the WebSocket completed its handshake at least
510
+ * once before closing; `false` when Railway refused the upgrade outright.
511
+ */
512
+ var SandboxExecTransportError = class extends Error {
513
+ sandboxId;
514
+ command;
515
+ attempts;
516
+ opened;
517
+ closeCode;
518
+ closeReason;
519
+ wsEndpoint;
520
+ constructor(message, diagnostics) {
521
+ super(message);
522
+ this.name = "SandboxExecTransportError";
523
+ this.sandboxId = diagnostics.sandboxId;
524
+ this.command = diagnostics.command;
525
+ this.attempts = diagnostics.attempts;
526
+ this.opened = diagnostics.opened;
527
+ this.closeCode = diagnostics.closeCode;
528
+ this.closeReason = diagnostics.closeReason;
529
+ this.wsEndpoint = diagnostics.wsEndpoint;
530
+ }
531
+ };
532
+ /**
533
+ * Thrown when `/exec-lease` returns 410 Gone — the sandbox has been destroyed
534
+ * (Railway destroy, quota reclamation, etc.). The client cannot recover from
535
+ * this on its own because it does not own the binding store; only the fleet
536
+ * layer can clear the stale sandbox id and provision a fresh one. Callers
537
+ * (typically `SandboxFleet`) must catch this and reprovision-and-replay.
538
+ *
539
+ * When this is thrown the cached `_lease` and `_sandboxId` on the sandbox
540
+ * instance are cleared, so the next `ensureRunning()` on a reused instance
541
+ * will re-provision cleanly.
542
+ */
543
+ var SandboxDestroyedError = class extends Error {
544
+ sandboxId;
545
+ command;
546
+ attempts;
547
+ constructor(message, diagnostics) {
548
+ super(message);
549
+ this.name = "SandboxDestroyedError";
550
+ this.sandboxId = diagnostics.sandboxId;
551
+ this.command = diagnostics.command;
552
+ this.attempts = diagnostics.attempts;
553
+ }
554
+ };
555
+ /**
503
556
  * Compose a shell command line from a `command` string and optional `args`.
504
557
  *
505
558
  * IMPORTANT: `command` is treated as a **shell string** and passed to the
@@ -599,16 +652,6 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
599
652
  * Cleared (regardless of success or failure) when the request settles.
600
653
  */
601
654
  _leaseInFlight = null;
602
- /**
603
- * Tri-state feature detection for the platform's exec-lease endpoint:
604
- * undefined — not yet tried (default; try direct on first exec)
605
- * true — endpoint present, use direct exec
606
- * false — endpoint absent (404/501) OR the WebSocket transport failed
607
- * once; fall back permanently to /exec for this sandbox
608
- * Sticky per instance so we make the fallback decision once per sandbox
609
- * lifetime instead of paying an extra round-trip on every exec.
610
- */
611
- _directExecAvailable = void 0;
612
655
  constructor(options = {}) {
613
656
  super({
614
657
  ...options,
@@ -728,47 +771,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
728
771
  const started = Date.now();
729
772
  const fullCommand = buildCommand(command, args);
730
773
  const effectiveTimeout = options?.timeout ?? this._timeout;
731
- if (this._directExecAvailable !== false) {
732
- const leaseResult = await this._tryDirectExec(fullCommand, effectiveTimeout, options);
733
- if (leaseResult) return {
734
- ...leaseResult,
735
- executionTimeMs: Date.now() - started
736
- };
737
- }
738
- return this._execViaProxy(fullCommand, effectiveTimeout, options, started);
739
- }
740
- async _tryDirectExec(fullCommand, effectiveTimeout, options) {
741
- let lease;
742
- try {
743
- lease = await this._ensureLease();
744
- } catch (error) {
745
- if (error instanceof PlatformApiError && (error.status === 404 || error.status === 501)) {
746
- this._directExecAvailable = false;
747
- return null;
748
- }
749
- throw error;
750
- }
751
- this._directExecAvailable = true;
752
- const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
753
- const result = await execViaLease(lease, {
754
- command: fullCommand,
755
- ...options?.cwd !== void 0 && { cwd: options.cwd },
756
- ...filteredEnv !== void 0 && { env: filteredEnv },
757
- ...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
758
- ...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
759
- });
760
- if (result.exitCode === null && !result.timedOut) {
761
- console.warn("[platform-workspace] direct-exec transport failed; falling back to /exec permanently for this sandbox", {
762
- sandboxId: this._sandboxId,
763
- opened: result.opened,
764
- closeCode: result.closeCode,
765
- closeReason: result.closeReason,
766
- wsEndpoint: lease.wsEndpoint
767
- });
768
- this._lease = null;
769
- this._directExecAvailable = false;
770
- return null;
771
- }
774
+ const result = await this._runDirectExec(fullCommand, effectiveTimeout, options);
772
775
  const exitCode = result.exitCode ?? 124;
773
776
  return {
774
777
  success: exitCode === 0,
@@ -776,34 +779,78 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
776
779
  stdout: result.stdout,
777
780
  stderr: result.stderr,
778
781
  timedOut: result.timedOut,
779
- command: fullCommand
782
+ command: fullCommand,
783
+ executionTimeMs: Date.now() - started
780
784
  };
781
785
  }
782
- async _execViaProxy(fullCommand, effectiveTimeout, options, started) {
783
- if (!this._sandboxId) throw new SandboxNotReadyError(this.id);
784
- const timeoutSec = effectiveTimeout != null ? Math.ceil(effectiveTimeout / 1e3) : void 0;
785
- const clientSignal = effectiveTimeout != null && effectiveTimeout > 0 ? AbortSignal.timeout(effectiveTimeout + 3e4) : void 0;
786
- const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}/exec`, {
787
- method: "POST",
788
- headers: { "content-type": "application/json" },
789
- body: JSON.stringify({
786
+ /**
787
+ * Run a single exec against the direct-exec transport, with one in-flight
788
+ * retry on WebSocket transport failure (socket closed without an `exit`
789
+ * frame and the exec did not time out). The retry mints a fresh lease
790
+ * the failure could be a stale JWT — and reopens a new WebSocket.
791
+ *
792
+ * Error taxonomy:
793
+ * - **410 on `/exec-lease`** (either attempt) → the sandbox is gone.
794
+ * Nulls the cached `_lease` and `_sandboxId` and throws
795
+ * {@link SandboxDestroyedError}. Callers (typically `SandboxFleet`) must
796
+ * catch this, clear the stale binding, and reprovision + replay.
797
+ * - **Persistent transport failure** (both WS attempts close without an
798
+ * `exit` frame against a live sandbox) → {@link SandboxExecTransportError}
799
+ * with WebSocket close diagnostics.
800
+ * - **Other `PlatformApiError`s** (404/500/501) propagate directly.
801
+ * - **Real command result** (exit code from Railway's exit frame, or
802
+ * `timedOut: true`) returns normally.
803
+ *
804
+ * Returns a result with a real `exitCode` OR `timedOut: true`. Never
805
+ * returns `{ exitCode: null, timedOut: false }` — that case throws.
806
+ */
807
+ async _runDirectExec(fullCommand, effectiveTimeout, options) {
808
+ const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
809
+ let lastResult;
810
+ let lastLease;
811
+ let attemptsMade = 0;
812
+ for (let attempt = 0; attempt < 2; attempt++) {
813
+ if (attempt > 0 && lastLease && this._lease === lastLease) this._lease = null;
814
+ let lease;
815
+ try {
816
+ lease = await this._ensureLease();
817
+ } catch (error) {
818
+ if (error instanceof PlatformApiError && error.status === 410) {
819
+ this._lease = null;
820
+ const priorSandboxId = this._sandboxId;
821
+ this._sandboxId = void 0;
822
+ throw new SandboxDestroyedError(`Sandbox ${priorSandboxId ?? "(unknown)"} was destroyed; /exec-lease returned 410`, {
823
+ ...priorSandboxId && { sandboxId: priorSandboxId },
824
+ command: fullCommand,
825
+ attempts: attempt + 1
826
+ });
827
+ }
828
+ throw error;
829
+ }
830
+ lastLease = lease;
831
+ attemptsMade = attempt + 1;
832
+ const result = await execViaLease(lease, {
790
833
  command: fullCommand,
791
- timeoutSec,
792
- cwd: options?.cwd,
793
- env: options?.env
794
- }),
795
- signal: clientSignal
796
- })).json();
797
- const exitCode = json.exitCode ?? (json.timedOut ? 124 : 1);
798
- return {
799
- success: exitCode === 0,
800
- exitCode,
801
- stdout: json.stdout,
802
- stderr: json.stderr,
803
- executionTimeMs: Date.now() - started,
804
- timedOut: json.timedOut,
805
- command: fullCommand
806
- };
834
+ ...options?.cwd !== void 0 && { cwd: options.cwd },
835
+ ...filteredEnv !== void 0 && { env: filteredEnv },
836
+ ...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
837
+ ...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
838
+ });
839
+ lastResult = result;
840
+ if (result.exitCode !== null || result.timedOut) return result;
841
+ }
842
+ const result = lastResult;
843
+ const lease = lastLease;
844
+ if (this._lease === lease) this._lease = null;
845
+ throw new SandboxExecTransportError(`Direct-exec transport failed for sandbox ${this._sandboxId ?? "(unknown)"} after ${attemptsMade} attempt(s)` + (result.closeCode !== void 0 ? ` (close ${result.closeCode}${result.closeReason ? ` ${result.closeReason}` : ""})` : ""), {
846
+ ...this._sandboxId && { sandboxId: this._sandboxId },
847
+ command: fullCommand,
848
+ attempts: attemptsMade,
849
+ opened: result.opened ?? false,
850
+ ...result.closeCode !== void 0 && { closeCode: result.closeCode },
851
+ ...result.closeReason !== void 0 && { closeReason: result.closeReason },
852
+ wsEndpoint: lease.wsEndpoint
853
+ });
807
854
  }
808
855
  /**
809
856
  * Return a cached exec lease, minting a fresh one when the cache is empty
@@ -881,7 +928,7 @@ const platformSandboxProvider = {
881
928
  properties: {
882
929
  accessToken: {
883
930
  type: "string",
884
- description: "Mastra Platform secret key (falls back to MASTRA_PLATFORM_SECRET_KEY)"
931
+ description: "Mastra Platform access token (falls back to MASTRA_PLATFORM_ACCESS_TOKEN)"
885
932
  },
886
933
  projectId: {
887
934
  type: "string",
@@ -927,7 +974,7 @@ const platformFilesystemProvider = {
927
974
  properties: {
928
975
  accessToken: {
929
976
  type: "string",
930
- description: "Mastra Platform secret key (falls back to MASTRA_PLATFORM_SECRET_KEY)"
977
+ description: "Mastra Platform access token (falls back to MASTRA_PLATFORM_ACCESS_TOKEN)"
931
978
  },
932
979
  projectId: {
933
980
  type: "string",
@@ -947,6 +994,6 @@ const platformFilesystemProvider = {
947
994
  createFilesystem: (config) => new PlatformFilesystem(config)
948
995
  };
949
996
  //#endregion
950
- export { PlatformApiError, PlatformClient, PlatformFilesystem, PlatformSandbox, platformFilesystemProvider, platformSandboxProvider };
997
+ export { PlatformApiError, PlatformClient, PlatformFilesystem, PlatformSandbox, SandboxDestroyedError, SandboxExecTransportError, platformFilesystemProvider, platformSandboxProvider };
951
998
 
952
999
  //# sourceMappingURL=index.js.map