@mastra/platform-workspace 0.2.3 → 0.3.0-alpha.1
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 +47 -0
- package/README.md +9 -0
- package/dist/index.cjs +127 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +126 -78
- package/dist/index.js.map +1 -1
- package/dist/sandbox.d.ts +71 -12
- package/dist/sandbox.d.ts.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @mastra/platform
|
|
2
2
|
|
|
3
|
+
## 0.3.0-alpha.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `PlatformSandbox.executeCommand` now retries a dropped connection once and continues using direct execution for later commands. Previously a single connection hiccup permanently downgraded the sandbox to a slower fallback route for the rest of its lifetime. ([#20482](https://github.com/mastra-ai/mastra/pull/20482))
|
|
8
|
+
|
|
9
|
+
Execution failures now surface directly:
|
|
10
|
+
|
|
11
|
+
- A destroyed sandbox throws the new `SandboxDestroyedError`. The cached sandbox is cleared, so the next call provisions a fresh one.
|
|
12
|
+
- Two connection failures in a row against a live sandbox throw the new `SandboxExecTransportError`, which carries `sandboxId`, `command`, `attempts`, `opened`, `closeCode`, `closeReason`, and `wsEndpoint` for diagnostics.
|
|
13
|
+
- Other platform errors previously masked by the fallback now bubble out as `PlatformApiError`.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { SandboxDestroyedError, SandboxExecTransportError } from '@mastra/platform-workspace';
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await sandbox.executeCommand('pytest');
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err instanceof SandboxDestroyedError) {
|
|
22
|
+
// Reprovision and retry.
|
|
23
|
+
} else if (err instanceof SandboxExecTransportError) {
|
|
24
|
+
// Connection failed twice; sandbox is still alive.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [[`594f7b2`](https://github.com/mastra-ai/mastra/commit/594f7b28f5263fb9982fd50d95c471fb971ea984), [`311f943`](https://github.com/mastra-ai/mastra/commit/311f943bee60e8fdf5c84499ea50e884276c936c), [`0c89896`](https://github.com/mastra-ai/mastra/commit/0c8989673fb7d106837098398131e570c6023b68), [`23b4238`](https://github.com/mastra-ai/mastra/commit/23b423844ad0bcf2a502a68dd62866d6160f9f6d), [`e320a76`](https://github.com/mastra-ai/mastra/commit/e320a763feaf65c6be3cebecf746defcbde161b3), [`03b4918`](https://github.com/mastra-ai/mastra/commit/03b4918c80d188ce375334c393e131c6e94bd7eb), [`14ef73a`](https://github.com/mastra-ai/mastra/commit/14ef73a4bbd73e7808414816eb0628ce1d80b5d7), [`1d677d5`](https://github.com/mastra-ai/mastra/commit/1d677d5f99d7db403f7828585e8c25f299f72628), [`93e28ec`](https://github.com/mastra-ai/mastra/commit/93e28ecce9031c02397e0ae8406593e5c7a95883), [`729dab4`](https://github.com/mastra-ai/mastra/commit/729dab408faccfaef0cbb048e5a4338f9172847e), [`484003d`](https://github.com/mastra-ai/mastra/commit/484003d33ff59330c86b19863e4a38732d7e4155), [`933d291`](https://github.com/mastra-ai/mastra/commit/933d291146b789c19442ad206f94da3e4be90c64)]:
|
|
32
|
+
- @mastra/core@1.56.0-alpha.3
|
|
33
|
+
|
|
34
|
+
## 0.2.4-alpha.0
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Fixed `PlatformSandbox.clone()` silently ignoring `checkpointName`. Clones created with `clone({ checkpointName })` now reuse a matching captured checkpoint on `start()` instead of always provisioning a fresh sandbox, so repeated boots of the same session start much faster. ([#20477](https://github.com/mastra-ai/mastra/pull/20477))
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const child = template.clone({ checkpointName: 'mastra-recovery-session-42' });
|
|
42
|
+
await child.start(); // Reuses the captured checkpoint when one is available.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
An explicit `id` still takes precedence over `checkpointName` when both are passed.
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [[`322daa6`](https://github.com/mastra-ai/mastra/commit/322daa6d90552909204044790d850958f6745fed), [`cadaa13`](https://github.com/mastra-ai/mastra/commit/cadaa1372e1077c8e85eb64c5499ba8803caa323), [`06000d7`](https://github.com/mastra-ai/mastra/commit/06000d73712911572e913b8a83339270296d0a22), [`3de0188`](https://github.com/mastra-ai/mastra/commit/3de0188bfaf9a9c09c95fe322b53838cf52c70b6)]:
|
|
48
|
+
- @mastra/core@1.56.0-alpha.2
|
|
49
|
+
|
|
3
50
|
## 0.2.3
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -106,3 +106,12 @@ try {
|
|
|
106
106
|
`code` / `proxyMessage` are `undefined` when the proxy returns a non-JSON body (e.g. an HTML 502 from a load balancer).
|
|
107
107
|
|
|
108
108
|
Filesystem-specific errors (`FileNotFoundError`, `FileExistsError`, `WorkspaceReadOnlyError`) are re-exported from `@mastra/core`.
|
|
109
|
+
|
|
110
|
+
### Sandbox exec errors
|
|
111
|
+
|
|
112
|
+
`PlatformSandbox.executeCommand` runs over the direct-exec data plane (a WebSocket straight to the Railway tcp-proxy) and can throw two typed errors on unrecoverable failure:
|
|
113
|
+
|
|
114
|
+
- `SandboxDestroyedError` — the platform returned 410 for `/exec-lease`, meaning the sandbox has been destroyed. The cached sandbox id and lease are cleared, so a reused `PlatformSandbox` instance will re-provision on the next call. Fleet-level code that owns a binding store should catch this, clear the stale sandbox id, and reprovision + replay.
|
|
115
|
+
- `SandboxExecTransportError` — both the initial WebSocket attempt and the built-in retry closed without an `exit` frame against a live sandbox. Carries `{ opened, closeCode, closeReason, wsEndpoint }` diagnostics plus `sandboxId`, `command`, and `attempts` so upstream logs / alerts can distinguish "the Railway data plane is broken" from "your command failed".
|
|
116
|
+
|
|
117
|
+
`PlatformApiError` (with status 404 / 500 / 501 on `/exec-lease`) can also bubble up from `executeCommand` — those are configuration or platform errors, not "reprovision me" signals, and are propagated as-is.
|
package/dist/index.cjs
CHANGED
|
@@ -524,6 +524,59 @@ const CREATE_MAX_ATTEMPTS = 3;
|
|
|
524
524
|
/** Base delay between create retries; multiplied by the attempt number. */
|
|
525
525
|
const CREATE_RETRY_BASE_DELAY_MS = 2e3;
|
|
526
526
|
/**
|
|
527
|
+
* Diagnostic error thrown when the direct-exec WebSocket transport fails
|
|
528
|
+
* twice in a row (opening handshake refused or socket closed mid-stream
|
|
529
|
+
* without an `exit` frame). Distinguishes "the sandbox transport is broken"
|
|
530
|
+
* from "your command failed" so callers can decide whether to retry at a
|
|
531
|
+
* higher level (e.g. reprovision the sandbox) or surface the error.
|
|
532
|
+
*
|
|
533
|
+
* `opened` is `true` when the WebSocket completed its handshake at least
|
|
534
|
+
* once before closing; `false` when Railway refused the upgrade outright.
|
|
535
|
+
*/
|
|
536
|
+
var SandboxExecTransportError = class extends Error {
|
|
537
|
+
sandboxId;
|
|
538
|
+
command;
|
|
539
|
+
attempts;
|
|
540
|
+
opened;
|
|
541
|
+
closeCode;
|
|
542
|
+
closeReason;
|
|
543
|
+
wsEndpoint;
|
|
544
|
+
constructor(message, diagnostics) {
|
|
545
|
+
super(message);
|
|
546
|
+
this.name = "SandboxExecTransportError";
|
|
547
|
+
this.sandboxId = diagnostics.sandboxId;
|
|
548
|
+
this.command = diagnostics.command;
|
|
549
|
+
this.attempts = diagnostics.attempts;
|
|
550
|
+
this.opened = diagnostics.opened;
|
|
551
|
+
this.closeCode = diagnostics.closeCode;
|
|
552
|
+
this.closeReason = diagnostics.closeReason;
|
|
553
|
+
this.wsEndpoint = diagnostics.wsEndpoint;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Thrown when `/exec-lease` returns 410 Gone — the sandbox has been destroyed
|
|
558
|
+
* (Railway destroy, quota reclamation, etc.). The client cannot recover from
|
|
559
|
+
* this on its own because it does not own the binding store; only the fleet
|
|
560
|
+
* layer can clear the stale sandbox id and provision a fresh one. Callers
|
|
561
|
+
* (typically `SandboxFleet`) must catch this and reprovision-and-replay.
|
|
562
|
+
*
|
|
563
|
+
* When this is thrown the cached `_lease` and `_sandboxId` on the sandbox
|
|
564
|
+
* instance are cleared, so the next `ensureRunning()` on a reused instance
|
|
565
|
+
* will re-provision cleanly.
|
|
566
|
+
*/
|
|
567
|
+
var SandboxDestroyedError = class extends Error {
|
|
568
|
+
sandboxId;
|
|
569
|
+
command;
|
|
570
|
+
attempts;
|
|
571
|
+
constructor(message, diagnostics) {
|
|
572
|
+
super(message);
|
|
573
|
+
this.name = "SandboxDestroyedError";
|
|
574
|
+
this.sandboxId = diagnostics.sandboxId;
|
|
575
|
+
this.command = diagnostics.command;
|
|
576
|
+
this.attempts = diagnostics.attempts;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
527
580
|
* Compose a shell command line from a `command` string and optional `args`.
|
|
528
581
|
*
|
|
529
582
|
* IMPORTANT: `command` is treated as a **shell string** and passed to the
|
|
@@ -623,16 +676,6 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
623
676
|
* Cleared (regardless of success or failure) when the request settles.
|
|
624
677
|
*/
|
|
625
678
|
_leaseInFlight = null;
|
|
626
|
-
/**
|
|
627
|
-
* Tri-state feature detection for the platform's exec-lease endpoint:
|
|
628
|
-
* undefined — not yet tried (default; try direct on first exec)
|
|
629
|
-
* true — endpoint present, use direct exec
|
|
630
|
-
* false — endpoint absent (404/501) OR the WebSocket transport failed
|
|
631
|
-
* once; fall back permanently to /exec for this sandbox
|
|
632
|
-
* Sticky per instance so we make the fallback decision once per sandbox
|
|
633
|
-
* lifetime instead of paying an extra round-trip on every exec.
|
|
634
|
-
*/
|
|
635
|
-
_directExecAvailable = void 0;
|
|
636
679
|
constructor(options = {}) {
|
|
637
680
|
super({
|
|
638
681
|
...options,
|
|
@@ -667,8 +710,9 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
667
710
|
* (e.g. one per project).
|
|
668
711
|
*/
|
|
669
712
|
clone(options = {}) {
|
|
713
|
+
const id = options.id ?? options.checkpointName;
|
|
670
714
|
return new PlatformSandbox({
|
|
671
|
-
...
|
|
715
|
+
...id !== void 0 && { id },
|
|
672
716
|
accessToken: this._client.accessToken,
|
|
673
717
|
projectId: this._client.projectId,
|
|
674
718
|
fetch: this._client.fetch,
|
|
@@ -751,47 +795,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
751
795
|
const started = Date.now();
|
|
752
796
|
const fullCommand = buildCommand(command, args);
|
|
753
797
|
const effectiveTimeout = options?.timeout ?? this._timeout;
|
|
754
|
-
|
|
755
|
-
const leaseResult = await this._tryDirectExec(fullCommand, effectiveTimeout, options);
|
|
756
|
-
if (leaseResult) return {
|
|
757
|
-
...leaseResult,
|
|
758
|
-
executionTimeMs: Date.now() - started
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
return this._execViaProxy(fullCommand, effectiveTimeout, options, started);
|
|
762
|
-
}
|
|
763
|
-
async _tryDirectExec(fullCommand, effectiveTimeout, options) {
|
|
764
|
-
let lease;
|
|
765
|
-
try {
|
|
766
|
-
lease = await this._ensureLease();
|
|
767
|
-
} catch (error) {
|
|
768
|
-
if (error instanceof PlatformApiError && (error.status === 404 || error.status === 501)) {
|
|
769
|
-
this._directExecAvailable = false;
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
throw error;
|
|
773
|
-
}
|
|
774
|
-
this._directExecAvailable = true;
|
|
775
|
-
const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
|
|
776
|
-
const result = await execViaLease(lease, {
|
|
777
|
-
command: fullCommand,
|
|
778
|
-
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
779
|
-
...filteredEnv !== void 0 && { env: filteredEnv },
|
|
780
|
-
...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
|
|
781
|
-
...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
|
|
782
|
-
});
|
|
783
|
-
if (result.exitCode === null && !result.timedOut) {
|
|
784
|
-
console.warn("[platform-workspace] direct-exec transport failed; falling back to /exec permanently for this sandbox", {
|
|
785
|
-
sandboxId: this._sandboxId,
|
|
786
|
-
opened: result.opened,
|
|
787
|
-
closeCode: result.closeCode,
|
|
788
|
-
closeReason: result.closeReason,
|
|
789
|
-
wsEndpoint: lease.wsEndpoint
|
|
790
|
-
});
|
|
791
|
-
this._lease = null;
|
|
792
|
-
this._directExecAvailable = false;
|
|
793
|
-
return null;
|
|
794
|
-
}
|
|
798
|
+
const result = await this._runDirectExec(fullCommand, effectiveTimeout, options);
|
|
795
799
|
const exitCode = result.exitCode ?? 124;
|
|
796
800
|
return {
|
|
797
801
|
success: exitCode === 0,
|
|
@@ -799,34 +803,78 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
799
803
|
stdout: result.stdout,
|
|
800
804
|
stderr: result.stderr,
|
|
801
805
|
timedOut: result.timedOut,
|
|
802
|
-
command: fullCommand
|
|
806
|
+
command: fullCommand,
|
|
807
|
+
executionTimeMs: Date.now() - started
|
|
803
808
|
};
|
|
804
809
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
810
|
+
/**
|
|
811
|
+
* Run a single exec against the direct-exec transport, with one in-flight
|
|
812
|
+
* retry on WebSocket transport failure (socket closed without an `exit`
|
|
813
|
+
* frame and the exec did not time out). The retry mints a fresh lease
|
|
814
|
+
* — the failure could be a stale JWT — and reopens a new WebSocket.
|
|
815
|
+
*
|
|
816
|
+
* Error taxonomy:
|
|
817
|
+
* - **410 on `/exec-lease`** (either attempt) → the sandbox is gone.
|
|
818
|
+
* Nulls the cached `_lease` and `_sandboxId` and throws
|
|
819
|
+
* {@link SandboxDestroyedError}. Callers (typically `SandboxFleet`) must
|
|
820
|
+
* catch this, clear the stale binding, and reprovision + replay.
|
|
821
|
+
* - **Persistent transport failure** (both WS attempts close without an
|
|
822
|
+
* `exit` frame against a live sandbox) → {@link SandboxExecTransportError}
|
|
823
|
+
* with WebSocket close diagnostics.
|
|
824
|
+
* - **Other `PlatformApiError`s** (404/500/501) propagate directly.
|
|
825
|
+
* - **Real command result** (exit code from Railway's exit frame, or
|
|
826
|
+
* `timedOut: true`) returns normally.
|
|
827
|
+
*
|
|
828
|
+
* Returns a result with a real `exitCode` OR `timedOut: true`. Never
|
|
829
|
+
* returns `{ exitCode: null, timedOut: false }` — that case throws.
|
|
830
|
+
*/
|
|
831
|
+
async _runDirectExec(fullCommand, effectiveTimeout, options) {
|
|
832
|
+
const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
|
|
833
|
+
let lastResult;
|
|
834
|
+
let lastLease;
|
|
835
|
+
let attemptsMade = 0;
|
|
836
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
837
|
+
if (attempt > 0 && lastLease && this._lease === lastLease) this._lease = null;
|
|
838
|
+
let lease;
|
|
839
|
+
try {
|
|
840
|
+
lease = await this._ensureLease();
|
|
841
|
+
} catch (error) {
|
|
842
|
+
if (error instanceof PlatformApiError && error.status === 410) {
|
|
843
|
+
this._lease = null;
|
|
844
|
+
const priorSandboxId = this._sandboxId;
|
|
845
|
+
this._sandboxId = void 0;
|
|
846
|
+
throw new SandboxDestroyedError(`Sandbox ${priorSandboxId ?? "(unknown)"} was destroyed; /exec-lease returned 410`, {
|
|
847
|
+
...priorSandboxId && { sandboxId: priorSandboxId },
|
|
848
|
+
command: fullCommand,
|
|
849
|
+
attempts: attempt + 1
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
lastLease = lease;
|
|
855
|
+
attemptsMade = attempt + 1;
|
|
856
|
+
const result = await execViaLease(lease, {
|
|
813
857
|
command: fullCommand,
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
858
|
+
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
859
|
+
...filteredEnv !== void 0 && { env: filteredEnv },
|
|
860
|
+
...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
|
|
861
|
+
...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
|
|
862
|
+
});
|
|
863
|
+
lastResult = result;
|
|
864
|
+
if (result.exitCode !== null || result.timedOut) return result;
|
|
865
|
+
}
|
|
866
|
+
const result = lastResult;
|
|
867
|
+
const lease = lastLease;
|
|
868
|
+
if (this._lease === lease) this._lease = null;
|
|
869
|
+
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}` : ""})` : ""), {
|
|
870
|
+
...this._sandboxId && { sandboxId: this._sandboxId },
|
|
871
|
+
command: fullCommand,
|
|
872
|
+
attempts: attemptsMade,
|
|
873
|
+
opened: result.opened ?? false,
|
|
874
|
+
...result.closeCode !== void 0 && { closeCode: result.closeCode },
|
|
875
|
+
...result.closeReason !== void 0 && { closeReason: result.closeReason },
|
|
876
|
+
wsEndpoint: lease.wsEndpoint
|
|
877
|
+
});
|
|
830
878
|
}
|
|
831
879
|
/**
|
|
832
880
|
* Return a cached exec lease, minting a fresh one when the cache is empty
|
|
@@ -974,6 +1022,8 @@ exports.PlatformApiError = PlatformApiError;
|
|
|
974
1022
|
exports.PlatformClient = PlatformClient;
|
|
975
1023
|
exports.PlatformFilesystem = PlatformFilesystem;
|
|
976
1024
|
exports.PlatformSandbox = PlatformSandbox;
|
|
1025
|
+
exports.SandboxDestroyedError = SandboxDestroyedError;
|
|
1026
|
+
exports.SandboxExecTransportError = SandboxExecTransportError;
|
|
977
1027
|
exports.platformFilesystemProvider = platformFilesystemProvider;
|
|
978
1028
|
exports.platformSandboxProvider = platformSandboxProvider;
|
|
979
1029
|
|