@mastra/platform-workspace 0.2.4-alpha.0 → 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 +31 -0
- package/README.md +9 -0
- package/dist/index.cjs +125 -76
- 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 +124 -77
- 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 +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
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
|
+
|
|
3
34
|
## 0.2.4-alpha.0
|
|
4
35
|
|
|
5
36
|
### 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,
|
|
@@ -752,47 +795,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
752
795
|
const started = Date.now();
|
|
753
796
|
const fullCommand = buildCommand(command, args);
|
|
754
797
|
const effectiveTimeout = options?.timeout ?? this._timeout;
|
|
755
|
-
|
|
756
|
-
const leaseResult = await this._tryDirectExec(fullCommand, effectiveTimeout, options);
|
|
757
|
-
if (leaseResult) return {
|
|
758
|
-
...leaseResult,
|
|
759
|
-
executionTimeMs: Date.now() - started
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
return this._execViaProxy(fullCommand, effectiveTimeout, options, started);
|
|
763
|
-
}
|
|
764
|
-
async _tryDirectExec(fullCommand, effectiveTimeout, options) {
|
|
765
|
-
let lease;
|
|
766
|
-
try {
|
|
767
|
-
lease = await this._ensureLease();
|
|
768
|
-
} catch (error) {
|
|
769
|
-
if (error instanceof PlatformApiError && (error.status === 404 || error.status === 501)) {
|
|
770
|
-
this._directExecAvailable = false;
|
|
771
|
-
return null;
|
|
772
|
-
}
|
|
773
|
-
throw error;
|
|
774
|
-
}
|
|
775
|
-
this._directExecAvailable = true;
|
|
776
|
-
const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
|
|
777
|
-
const result = await execViaLease(lease, {
|
|
778
|
-
command: fullCommand,
|
|
779
|
-
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
780
|
-
...filteredEnv !== void 0 && { env: filteredEnv },
|
|
781
|
-
...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
|
|
782
|
-
...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
|
|
783
|
-
});
|
|
784
|
-
if (result.exitCode === null && !result.timedOut) {
|
|
785
|
-
console.warn("[platform-workspace] direct-exec transport failed; falling back to /exec permanently for this sandbox", {
|
|
786
|
-
sandboxId: this._sandboxId,
|
|
787
|
-
opened: result.opened,
|
|
788
|
-
closeCode: result.closeCode,
|
|
789
|
-
closeReason: result.closeReason,
|
|
790
|
-
wsEndpoint: lease.wsEndpoint
|
|
791
|
-
});
|
|
792
|
-
this._lease = null;
|
|
793
|
-
this._directExecAvailable = false;
|
|
794
|
-
return null;
|
|
795
|
-
}
|
|
798
|
+
const result = await this._runDirectExec(fullCommand, effectiveTimeout, options);
|
|
796
799
|
const exitCode = result.exitCode ?? 124;
|
|
797
800
|
return {
|
|
798
801
|
success: exitCode === 0,
|
|
@@ -800,34 +803,78 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
800
803
|
stdout: result.stdout,
|
|
801
804
|
stderr: result.stderr,
|
|
802
805
|
timedOut: result.timedOut,
|
|
803
|
-
command: fullCommand
|
|
806
|
+
command: fullCommand,
|
|
807
|
+
executionTimeMs: Date.now() - started
|
|
804
808
|
};
|
|
805
809
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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, {
|
|
814
857
|
command: fullCommand,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
+
});
|
|
831
878
|
}
|
|
832
879
|
/**
|
|
833
880
|
* Return a cached exec lease, minting a fresh one when the cache is empty
|
|
@@ -975,6 +1022,8 @@ exports.PlatformApiError = PlatformApiError;
|
|
|
975
1022
|
exports.PlatformClient = PlatformClient;
|
|
976
1023
|
exports.PlatformFilesystem = PlatformFilesystem;
|
|
977
1024
|
exports.PlatformSandbox = PlatformSandbox;
|
|
1025
|
+
exports.SandboxDestroyedError = SandboxDestroyedError;
|
|
1026
|
+
exports.SandboxExecTransportError = SandboxExecTransportError;
|
|
978
1027
|
exports.platformFilesystemProvider = platformFilesystemProvider;
|
|
979
1028
|
exports.platformSandboxProvider = platformSandboxProvider;
|
|
980
1029
|
|