@mastra/platform-workspace 0.0.0-esbuild-bundle-worker-20260807173433

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.cjs ADDED
@@ -0,0 +1,1493 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let buffer = require("buffer");
25
+ let path = require("path");
26
+ path = __toESM(path, 1);
27
+ let _mastra_core_workspace = require("@mastra/core/workspace");
28
+ //#region src/client.ts
29
+ const DEFAULT_PROXY_URL = "https://workspaces.mastra.ai";
30
+ /**
31
+ * Default per-request timeout for calls to the workspace proxy. Applied only
32
+ * when the caller doesn't already pass an `AbortSignal`. Long-running routes
33
+ * (e.g. `POST /sandbox/:id/exec`) pass their own longer signal.
34
+ */
35
+ const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
36
+ function requireOption(value, name) {
37
+ if (!value) throw new Error(`${name} is required`);
38
+ return value;
39
+ }
40
+ function resolvePlatformOptions(options) {
41
+ return {
42
+ accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_ACCESS_TOKEN, "accessToken"),
43
+ projectId: requireOption(options.projectId ?? process.env.MASTRA_PROJECT_ID, "projectId"),
44
+ proxyUrl: (process.env.MASTRA_WORKSPACE_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/$/, ""),
45
+ fetch: options.fetch ?? fetch
46
+ };
47
+ }
48
+ function parseProxyError(body) {
49
+ if (!body) return void 0;
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(body);
53
+ } catch {
54
+ return;
55
+ }
56
+ if (typeof parsed !== "object" || parsed === null) return void 0;
57
+ const err = parsed.error;
58
+ if (typeof err !== "object" || err === null) return void 0;
59
+ const { message, type } = err;
60
+ if (typeof message !== "string" || typeof type !== "string") return void 0;
61
+ return {
62
+ message,
63
+ type
64
+ };
65
+ }
66
+ var PlatformApiError = class extends Error {
67
+ status;
68
+ body;
69
+ /** Machine-readable proxy error kind (e.g. `not_found`), when the response body matches `{ error: { message, type } }`. */
70
+ code;
71
+ /** Human-readable proxy error message, when the response body matches `{ error: { message, type } }`. */
72
+ proxyMessage;
73
+ constructor(status, body) {
74
+ const parsed = parseProxyError(body);
75
+ const summary = parsed ? `${parsed.type}: ${parsed.message}` : body;
76
+ super(`Platform proxy request failed with ${status}${summary ? `: ${summary}` : ""}`);
77
+ this.name = "PlatformApiError";
78
+ this.status = status;
79
+ this.body = body;
80
+ this.code = parsed?.type;
81
+ this.proxyMessage = parsed?.message;
82
+ }
83
+ };
84
+ var PlatformClient = class {
85
+ accessToken;
86
+ projectId;
87
+ proxyUrl;
88
+ fetch;
89
+ constructor(options) {
90
+ const resolved = resolvePlatformOptions(options);
91
+ this.accessToken = resolved.accessToken;
92
+ this.projectId = resolved.projectId;
93
+ this.proxyUrl = resolved.proxyUrl;
94
+ this.fetch = resolved.fetch;
95
+ }
96
+ async request(path, options = {}) {
97
+ const url = new URL(`${this.proxyUrl}/v1/projects/${encodeURIComponent(this.projectId)}${path}`);
98
+ for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, String(value));
99
+ const headers = new Headers(options.headers);
100
+ headers.set("authorization", `Bearer ${this.accessToken}`);
101
+ const { query: _query, ...fetchOptions } = options;
102
+ const signal = fetchOptions.signal ?? AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
103
+ const response = await this.fetch(url, {
104
+ ...fetchOptions,
105
+ headers,
106
+ signal
107
+ });
108
+ if (!response.ok) throw new PlatformApiError(response.status, await response.text());
109
+ return response;
110
+ }
111
+ };
112
+ //#endregion
113
+ //#region src/filesystem.ts
114
+ function normalizePath(input) {
115
+ if (!input || input === ".") return "/";
116
+ let normalized = input.startsWith("/") ? input : `/${input}`;
117
+ normalized = path.default.posix.normalize(normalized);
118
+ return normalized === "." ? "/" : normalized;
119
+ }
120
+ function keyFromPath(path$1) {
121
+ const normalized = normalizePath(path$1);
122
+ return normalized === "/" ? "" : normalized.slice(1);
123
+ }
124
+ /**
125
+ * Encode each `/`-delimited segment of an object key with `encodeURIComponent`
126
+ * so reserved URL characters (`?`, `#`, `%`, `&`, `+`, spaces, etc.) are
127
+ * treated as part of the key instead of URL syntax. Kept segment-aware so
128
+ * `/` continues to act as a path separator on the wire.
129
+ */
130
+ function encodeKeyPath(key) {
131
+ return key.split("/").map(encodeURIComponent).join("/");
132
+ }
133
+ function nameFromPath(path$2) {
134
+ const normalized = normalizePath(path$2);
135
+ if (normalized === "/") return "";
136
+ return normalized.slice(normalized.lastIndexOf("/") + 1);
137
+ }
138
+ function contentToBody(content) {
139
+ if (typeof content === "string") return content;
140
+ return buffer.Buffer.from(content);
141
+ }
142
+ function headerDate(headers, name) {
143
+ const value = headers.get(name);
144
+ return value ? new Date(value) : /* @__PURE__ */ new Date(0);
145
+ }
146
+ function headerSize(headers) {
147
+ const value = headers.get("content-length");
148
+ return value ? Number(value) : 0;
149
+ }
150
+ function isNotFound(error) {
151
+ return typeof error === "object" && error !== null && "status" in error && error.status === 404;
152
+ }
153
+ var PlatformFilesystem = class extends _mastra_core_workspace.MastraFilesystem {
154
+ id;
155
+ name = "PlatformFilesystem";
156
+ provider = "platform";
157
+ readOnly;
158
+ displayName;
159
+ icon;
160
+ description;
161
+ status = "pending";
162
+ _client;
163
+ _bucketName;
164
+ _instructionsOverride;
165
+ constructor(options = {}) {
166
+ super({
167
+ ...options,
168
+ name: "PlatformFilesystem"
169
+ });
170
+ this.id = options.id ?? this.generateId();
171
+ this._bucketName = options.bucketName ?? process.env.MASTRA_PLATFORM_BUCKET_NAME ?? "";
172
+ if (!this._bucketName) throw new Error("bucketName is required");
173
+ this.readOnly = options.readOnly;
174
+ this.displayName = options.displayName;
175
+ this.icon = options.icon ?? "cloud";
176
+ this.description = options.description;
177
+ this._instructionsOverride = options.instructions;
178
+ this._client = new PlatformClient(options);
179
+ }
180
+ generateId() {
181
+ return `platform-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
182
+ }
183
+ async readFile(path$3, options) {
184
+ await this.ensureReady();
185
+ let response;
186
+ try {
187
+ response = await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path$3))}`);
188
+ } catch (error) {
189
+ if (isNotFound(error)) throw new _mastra_core_workspace.FileNotFoundError(path$3);
190
+ throw error;
191
+ }
192
+ const buffer$1 = buffer.Buffer.from(await response.arrayBuffer());
193
+ return options?.encoding ? buffer$1.toString(options.encoding) : buffer$1;
194
+ }
195
+ async writeFile(path$4, content, options) {
196
+ await this.ensureReady();
197
+ if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("writeFile");
198
+ const headers = {};
199
+ if (options?.mimeType) headers["content-type"] = options.mimeType;
200
+ if (options?.overwrite === false) headers["if-none-match"] = "*";
201
+ try {
202
+ await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path$4))}`, {
203
+ method: "PUT",
204
+ headers,
205
+ body: contentToBody(content)
206
+ });
207
+ } catch (error) {
208
+ if (typeof error === "object" && error !== null && "status" in error && error.status === 412) throw new _mastra_core_workspace.FileExistsError(path$4);
209
+ throw error;
210
+ }
211
+ }
212
+ /**
213
+ * Append bytes to a file.
214
+ *
215
+ * **Not atomic.** Object storage behind the workspace proxy has no native
216
+ * append or compare-and-swap primitive, so this implementation is a
217
+ * read-modify-write: it reads the current contents, concatenates the new
218
+ * bytes, and PUTs the whole object back. Concurrent `appendFile` calls to
219
+ * the same path can overwrite each other's writes ("last write wins").
220
+ * Use `writeFile` with distinct keys for concurrent writers.
221
+ */
222
+ async appendFile(path$5, content) {
223
+ const existing = await this.exists(path$5) ? await this.readFile(path$5) : buffer.Buffer.alloc(0);
224
+ await this.writeFile(path$5, buffer.Buffer.concat([buffer.Buffer.isBuffer(existing) ? existing : buffer.Buffer.from(existing), buffer.Buffer.from(content)]));
225
+ }
226
+ async deleteFile(path$6, options) {
227
+ await this.ensureReady();
228
+ if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("deleteFile");
229
+ try {
230
+ await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path$6))}`, {
231
+ method: "DELETE",
232
+ query: { recursive: options?.recursive }
233
+ });
234
+ } catch (error) {
235
+ if (isNotFound(error) && options?.force) return;
236
+ if (isNotFound(error)) throw new _mastra_core_workspace.FileNotFoundError(path$6);
237
+ throw error;
238
+ }
239
+ }
240
+ async copyFile(src, dest, options) {
241
+ await this.ensureReady();
242
+ if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("copyFile");
243
+ if (options?.overwrite === false) throw new Error("PlatformFilesystem.copyFile does not support overwrite: false — the proxy always overwrites.");
244
+ await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(src))}`, {
245
+ method: "POST",
246
+ query: { op: "copy" },
247
+ headers: { "content-type": "application/json" },
248
+ body: JSON.stringify({ destination: keyFromPath(dest) })
249
+ });
250
+ }
251
+ async moveFile(src, dest, options) {
252
+ await this.ensureReady();
253
+ if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("moveFile");
254
+ if (options?.overwrite === false) throw new Error("PlatformFilesystem.moveFile does not support overwrite: false — the proxy always overwrites.");
255
+ await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(src))}`, {
256
+ method: "POST",
257
+ query: { op: "rename" },
258
+ headers: { "content-type": "application/json" },
259
+ body: JSON.stringify({ destination: keyFromPath(dest) })
260
+ });
261
+ }
262
+ async mkdir(path$7, _options) {
263
+ await this.ensureReady();
264
+ if (this.readOnly) throw new _mastra_core_workspace.WorkspaceReadOnlyError("mkdir");
265
+ await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path$7))}`, {
266
+ method: "POST",
267
+ query: { op: "mkdir" }
268
+ });
269
+ }
270
+ async rmdir(path$8, options) {
271
+ await this.deleteFile(path$8.endsWith("/") ? path$8 : `${path$8}/`, {
272
+ recursive: true,
273
+ force: options?.force
274
+ });
275
+ }
276
+ async readdir(path$9, options) {
277
+ await this.ensureReady();
278
+ const prefix = keyFromPath(path$9);
279
+ const json = await (await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(prefix)}`, { query: {
280
+ delimiter: options?.recursive ? void 0 : "/",
281
+ prefix: prefix ? `${prefix.replace(/\/$/, "")}/` : void 0
282
+ } })).json();
283
+ return [...(json.commonPrefixes ?? []).map((prefix) => ({
284
+ name: nameFromPath(prefix.replace(/\/$/, "")),
285
+ type: "directory"
286
+ })), ...(json.contents ?? []).filter((object) => object.key && !object.key.endsWith("/")).map((object) => ({
287
+ name: nameFromPath(object.key),
288
+ type: "file",
289
+ size: object.size
290
+ }))].filter((entry) => !options?.extension || entry.type === "directory" || matchesExtension(entry.name, options.extension));
291
+ }
292
+ async exists(path$10) {
293
+ try {
294
+ await this.stat(path$10);
295
+ return true;
296
+ } catch (error) {
297
+ if (isNotFound(error) || error instanceof _mastra_core_workspace.FileNotFoundError) return false;
298
+ throw error;
299
+ }
300
+ }
301
+ async stat(path$11) {
302
+ await this.ensureReady();
303
+ const normalized = normalizePath(path$11);
304
+ if (normalized === "/") return {
305
+ name: "",
306
+ path: "/",
307
+ type: "directory",
308
+ size: 0,
309
+ createdAt: /* @__PURE__ */ new Date(0),
310
+ modifiedAt: /* @__PURE__ */ new Date(0)
311
+ };
312
+ let response;
313
+ try {
314
+ response = await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path$11))}`, { method: "HEAD" });
315
+ } catch (error) {
316
+ if (isNotFound(error)) throw new _mastra_core_workspace.FileNotFoundError(path$11);
317
+ throw error;
318
+ }
319
+ return {
320
+ name: nameFromPath(path$11),
321
+ path: normalized,
322
+ type: normalized.endsWith("/") ? "directory" : "file",
323
+ size: headerSize(response.headers),
324
+ createdAt: headerDate(response.headers, "last-modified"),
325
+ modifiedAt: headerDate(response.headers, "last-modified"),
326
+ mimeType: response.headers.get("content-type") ?? void 0
327
+ };
328
+ }
329
+ realpath(path$12) {
330
+ return Promise.resolve(normalizePath(path$12));
331
+ }
332
+ getInstructions(opts) {
333
+ const defaultInstructions = `Platform filesystem backed by Mastra Platform bucket ${this._bucketName}. Use absolute workspace paths.`;
334
+ if (typeof this._instructionsOverride === "function") return this._instructionsOverride({
335
+ defaultInstructions,
336
+ requestContext: opts?.requestContext
337
+ });
338
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
339
+ return defaultInstructions;
340
+ }
341
+ getInfo() {
342
+ return {
343
+ id: this.id,
344
+ name: this.name,
345
+ provider: this.provider,
346
+ status: this.status,
347
+ readOnly: this.readOnly,
348
+ icon: this.icon,
349
+ metadata: {
350
+ bucketName: this._bucketName,
351
+ ...this.displayName && { displayName: this.displayName },
352
+ ...this.description && { description: this.description }
353
+ }
354
+ };
355
+ }
356
+ };
357
+ function matchesExtension(name, extension) {
358
+ return (Array.isArray(extension) ? extension : [extension]).some((ext) => name.endsWith(ext));
359
+ }
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 closeCode;
412
+ let closeReason;
413
+ let timer;
414
+ let handshakeTimer;
415
+ const settle = () => {
416
+ if (settled) return;
417
+ settled = true;
418
+ if (timer) clearTimeout(timer);
419
+ if (handshakeTimer) clearTimeout(handshakeTimer);
420
+ const stdoutTail = stdoutDecoder.decode();
421
+ if (stdoutTail) {
422
+ stdout += stdoutTail;
423
+ options.onStdout?.(stdoutTail);
424
+ }
425
+ const stderrTail = stderrDecoder.decode();
426
+ if (stderrTail) {
427
+ stderr += stderrTail;
428
+ options.onStderr?.(stderrTail);
429
+ }
430
+ try {
431
+ socket.close(1e3, "");
432
+ } catch {}
433
+ resolve({
434
+ exitCode,
435
+ stdout,
436
+ stderr,
437
+ truncated: false,
438
+ timedOut,
439
+ ...closeCode !== void 0 && { closeCode },
440
+ ...closeReason !== void 0 && { closeReason },
441
+ opened
442
+ });
443
+ };
444
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) timer = setTimeout(() => {
445
+ timedOut = true;
446
+ if (exitCode === null) exitCode = 124;
447
+ settle();
448
+ }, options.timeoutMs);
449
+ else handshakeTimer = setTimeout(() => {
450
+ if (!opened) settle();
451
+ }, HANDSHAKE_DEADLINE_MS);
452
+ const socket = factory(lease.wsEndpoint, [lease.subprotocol, lease.jwt]);
453
+ socket.binaryType = "arraybuffer";
454
+ socket.onopen = () => {
455
+ opened = true;
456
+ if (handshakeTimer) {
457
+ clearTimeout(handshakeTimer);
458
+ handshakeTimer = void 0;
459
+ }
460
+ const data = { command: options.command };
461
+ if (options.cwd) data.cwd = options.cwd;
462
+ if (options.env && Object.keys(options.env).length > 0) data.env = options.env;
463
+ socket.send(JSON.stringify({
464
+ type: "init_exec",
465
+ data
466
+ }));
467
+ socket.send(JSON.stringify({ type: "stdin_close" }));
468
+ };
469
+ socket.onmessage = (event) => {
470
+ const { data } = event;
471
+ if (data instanceof ArrayBuffer) handleBinaryFrame(data);
472
+ else if (typeof data === "string") handleTextFrame(data);
473
+ };
474
+ socket.onclose = (event) => {
475
+ closeCode = event.code;
476
+ closeReason = event.reason;
477
+ if (!opened) {
478
+ settle();
479
+ return;
480
+ }
481
+ settle();
482
+ };
483
+ socket.onerror = () => {
484
+ if (settled) return;
485
+ if (!opened) settle();
486
+ };
487
+ function handleBinaryFrame(buffer) {
488
+ const view = new Uint8Array(buffer);
489
+ if (view.length <= 1) return;
490
+ if (view[0] === STDOUT_FRAME) {
491
+ const chunk = stdoutDecoder.decode(view.subarray(1), { stream: true });
492
+ stdout += chunk;
493
+ options.onStdout?.(chunk);
494
+ } else if (view[0] === STDERR_FRAME) {
495
+ const chunk = stderrDecoder.decode(view.subarray(1), { stream: true });
496
+ stderr += chunk;
497
+ options.onStderr?.(chunk);
498
+ }
499
+ }
500
+ function handleTextFrame(text) {
501
+ let frame;
502
+ try {
503
+ frame = JSON.parse(text);
504
+ } catch {
505
+ return;
506
+ }
507
+ if (frame.type === "exit") {
508
+ exitCode = frame.data?.exit_code ?? 0;
509
+ settle();
510
+ }
511
+ }
512
+ });
513
+ }
514
+ //#endregion
515
+ //#region src/private-net-exec.ts
516
+ /**
517
+ * Thrown by {@link execViaPrivateNetwork} when the sidecar returns a non-2xx
518
+ * HTTP response. This is an *application* error, not a transport error — the
519
+ * sidecar is reachable and answered, it just refused the exec. Callers should
520
+ * fall back to the lease path for this one call but MUST NOT invalidate the
521
+ * cached `instanceUrl` (the address is still good).
522
+ */
523
+ var PrivateNetExecHttpError = class extends Error {
524
+ status;
525
+ body;
526
+ constructor(status, body) {
527
+ super(`Sidecar /exec returned ${status}${body ? `: ${body.slice(0, 200)}` : ""}`);
528
+ this.name = "PrivateNetExecHttpError";
529
+ this.status = status;
530
+ this.body = body;
531
+ }
532
+ };
533
+ const DEFAULT_FETCH = (input, init) => {
534
+ const f = globalThis.fetch;
535
+ if (!f) throw new Error("Private-network exec requires a fetch implementation. Node 22+ provides one globally; on older runtimes, pass fetch explicitly.");
536
+ return f(input, init);
537
+ };
538
+ /**
539
+ * Dial `${instanceUrl}/exec` and stream the response, resolving with the
540
+ * accumulated stdout/stderr + exit code.
541
+ *
542
+ * Errors:
543
+ * - Connection failure (DNS, refused, reset) → resolves with
544
+ * `{opened:false, exitCode:null, transportErrorMessage}`. Never throws for
545
+ * transport failures — the shape matches the lease-path result so the
546
+ * caller can treat both transports uniformly.
547
+ * - Non-2xx HTTP response from the sidecar → throws {@link PrivateNetExecHttpError}.
548
+ * Application-level; caller decides whether to fall back.
549
+ * - Stream ends without an `exit` frame → resolves with
550
+ * `{opened:true, exitCode:null}`, matching the lease-path semantics for a
551
+ * mid-stream drop.
552
+ * - `timeoutMs` elapsed → aborts the request, resolves with
553
+ * `{timedOut:true, exitCode:124}`.
554
+ */
555
+ async function execViaPrivateNetwork(instanceUrl, options) {
556
+ const fetchImpl = options.fetch ?? DEFAULT_FETCH;
557
+ const url = `${instanceUrl.replace(/\/$/, "")}/exec`;
558
+ const controller = new AbortController();
559
+ let timedOut = false;
560
+ let timeoutTimer;
561
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) timeoutTimer = setTimeout(() => {
562
+ timedOut = true;
563
+ controller.abort();
564
+ }, options.timeoutMs);
565
+ const body = { command: options.command };
566
+ if (options.cwd) body.cwd = options.cwd;
567
+ if (options.env && Object.keys(options.env).length > 0) body.env = options.env;
568
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) body.timeoutMs = options.timeoutMs;
569
+ const headers = { "content-type": "application/json" };
570
+ if (options.bearerToken) headers.authorization = `Bearer ${options.bearerToken}`;
571
+ let response;
572
+ try {
573
+ response = await fetchImpl(url, {
574
+ method: "POST",
575
+ headers,
576
+ body: JSON.stringify(body),
577
+ signal: controller.signal
578
+ });
579
+ } catch (error) {
580
+ if (timeoutTimer) clearTimeout(timeoutTimer);
581
+ if (timedOut) return {
582
+ exitCode: 124,
583
+ stdout: "",
584
+ stderr: "",
585
+ timedOut: true,
586
+ opened: false
587
+ };
588
+ return {
589
+ exitCode: null,
590
+ stdout: "",
591
+ stderr: "",
592
+ timedOut: false,
593
+ opened: false,
594
+ transportErrorMessage: error instanceof Error ? error.message : String(error)
595
+ };
596
+ }
597
+ if (!response.ok) {
598
+ if (timeoutTimer) clearTimeout(timeoutTimer);
599
+ const text = await response.text().catch(() => "");
600
+ throw new PrivateNetExecHttpError(response.status, text);
601
+ }
602
+ if (!response.body) {
603
+ if (timeoutTimer) clearTimeout(timeoutTimer);
604
+ return {
605
+ exitCode: null,
606
+ stdout: "",
607
+ stderr: "",
608
+ timedOut: false,
609
+ opened: true,
610
+ status: response.status
611
+ };
612
+ }
613
+ let stdout = "";
614
+ let stderr = "";
615
+ let exitCode = null;
616
+ const decoder = new TextDecoder();
617
+ let buffer = "";
618
+ const handleLine = (line) => {
619
+ if (!line) return;
620
+ let frame;
621
+ try {
622
+ frame = JSON.parse(line);
623
+ } catch {
624
+ return;
625
+ }
626
+ if (!frame || typeof frame !== "object") return;
627
+ if (frame.type === "stdout" && typeof frame.data === "string") {
628
+ stdout += frame.data;
629
+ options.onStdout?.(frame.data);
630
+ } else if (frame.type === "stderr" && typeof frame.data === "string") {
631
+ stderr += frame.data;
632
+ options.onStderr?.(frame.data);
633
+ } else if (frame.type === "exit" && typeof frame.code === "number") exitCode = frame.code;
634
+ };
635
+ try {
636
+ const reader = response.body.getReader();
637
+ while (true) {
638
+ const { value, done } = await reader.read();
639
+ if (done) break;
640
+ buffer += decoder.decode(value, { stream: true });
641
+ let newlineIdx = buffer.indexOf("\n");
642
+ while (newlineIdx !== -1) {
643
+ const line = buffer.slice(0, newlineIdx).trim();
644
+ buffer = buffer.slice(newlineIdx + 1);
645
+ handleLine(line);
646
+ newlineIdx = buffer.indexOf("\n");
647
+ }
648
+ }
649
+ buffer += decoder.decode();
650
+ const trailing = buffer.trim();
651
+ if (trailing) handleLine(trailing);
652
+ } catch (error) {
653
+ if (timeoutTimer) clearTimeout(timeoutTimer);
654
+ if (timedOut) return {
655
+ exitCode: 124,
656
+ stdout,
657
+ stderr,
658
+ timedOut: true,
659
+ opened: true,
660
+ status: response.status
661
+ };
662
+ return {
663
+ exitCode,
664
+ stdout,
665
+ stderr,
666
+ timedOut: false,
667
+ opened: true,
668
+ status: response.status,
669
+ transportErrorMessage: error instanceof Error ? error.message : String(error)
670
+ };
671
+ } finally {
672
+ if (timeoutTimer) clearTimeout(timeoutTimer);
673
+ }
674
+ return {
675
+ exitCode,
676
+ stdout,
677
+ stderr,
678
+ timedOut: false,
679
+ opened: true,
680
+ status: response.status
681
+ };
682
+ }
683
+ //#endregion
684
+ //#region src/sandbox.ts
685
+ /**
686
+ * How long before a lease's stated `expiresAt` we should treat it as
687
+ * expired. Avoids a race where the JWT is valid at cache-hit time but the
688
+ * server rejects it by the time the WebSocket handshake completes.
689
+ */
690
+ const LEASE_REFRESH_MARGIN_MS = 6e4;
691
+ /** Max attempts for `POST /sandbox` when the proxy returns transient 5xx errors. */
692
+ const CREATE_MAX_ATTEMPTS = 3;
693
+ /** Base delay between create retries; multiplied by the attempt number. */
694
+ const CREATE_RETRY_BASE_DELAY_MS = 2e3;
695
+ /**
696
+ * Diagnostic error thrown when the direct-exec WebSocket transport fails
697
+ * twice in a row (opening handshake refused or socket closed mid-stream
698
+ * without an `exit` frame). Distinguishes "the sandbox transport is broken"
699
+ * from "your command failed" so callers can decide whether to retry at a
700
+ * higher level (e.g. reprovision the sandbox) or surface the error.
701
+ *
702
+ * `opened` is `true` when the WebSocket completed its handshake at least
703
+ * once before closing; `false` when Railway refused the upgrade outright.
704
+ */
705
+ var SandboxExecTransportError = class extends Error {
706
+ sandboxId;
707
+ command;
708
+ attempts;
709
+ opened;
710
+ closeCode;
711
+ closeReason;
712
+ wsEndpoint;
713
+ constructor(message, diagnostics) {
714
+ super(message);
715
+ this.name = "SandboxExecTransportError";
716
+ this.sandboxId = diagnostics.sandboxId;
717
+ this.command = diagnostics.command;
718
+ this.attempts = diagnostics.attempts;
719
+ this.opened = diagnostics.opened;
720
+ this.closeCode = diagnostics.closeCode;
721
+ this.closeReason = diagnostics.closeReason;
722
+ this.wsEndpoint = diagnostics.wsEndpoint;
723
+ }
724
+ };
725
+ /**
726
+ * Thrown when `/exec-lease` returns 410 Gone — the sandbox has been destroyed
727
+ * (Railway destroy, quota reclamation, etc.). The client cannot recover from
728
+ * this on its own because it does not own the binding store; only the fleet
729
+ * layer can clear the stale sandbox id and provision a fresh one. Callers
730
+ * (typically `SandboxFleet`) must catch this and reprovision-and-replay.
731
+ *
732
+ * When this is thrown the cached `_lease` and `_sandboxId` on the sandbox
733
+ * instance are cleared, so the next `ensureRunning()` on a reused instance
734
+ * will re-provision cleanly.
735
+ */
736
+ var SandboxDestroyedError = class extends Error {
737
+ sandboxId;
738
+ command;
739
+ attempts;
740
+ constructor(message, diagnostics) {
741
+ super(message);
742
+ this.name = "SandboxDestroyedError";
743
+ this.sandboxId = diagnostics.sandboxId;
744
+ this.command = diagnostics.command;
745
+ this.attempts = diagnostics.attempts;
746
+ }
747
+ };
748
+ /**
749
+ * Compose a shell command line from a `command` string and optional `args`.
750
+ *
751
+ * IMPORTANT: `command` is treated as a **shell string** and passed to the
752
+ * remote shell verbatim so callers can use pipes, redirects, and chaining
753
+ * (`ls -la | grep foo`). This matches the contract of {@link MastraSandbox}
754
+ * and the local sandbox implementation. `args` are always shell-quoted so
755
+ * they cannot inject syntax.
756
+ *
757
+ * Callers MUST NOT pass untrusted input as `command`. Untrusted values must
758
+ * be passed via `args`, where they are safely quoted. Passing untrusted
759
+ * input as `command` allows arbitrary shell syntax execution on the remote
760
+ * sandbox.
761
+ */
762
+ function buildCommand(command, args) {
763
+ return args?.length ? `${command} ${args.map(shellQuote).join(" ")}` : command;
764
+ }
765
+ function shellQuote(arg) {
766
+ if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
767
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
768
+ }
769
+ var PlatformProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
770
+ pid;
771
+ resultPromise;
772
+ exitCodeValue;
773
+ constructor(pid, resultPromise, options) {
774
+ super(options);
775
+ this.pid = pid;
776
+ this.resultPromise = resultPromise.then((result) => {
777
+ this.exitCodeValue = result.exitCode;
778
+ if (result.stdout) this.emitStdout(result.stdout);
779
+ if (result.stderr) this.emitStderr(result.stderr);
780
+ return result;
781
+ });
782
+ }
783
+ get exitCode() {
784
+ return this.exitCodeValue;
785
+ }
786
+ async wait() {
787
+ return this.resultPromise;
788
+ }
789
+ async kill() {
790
+ throw new Error("Platform sandbox command execution does not support killing individual processes");
791
+ }
792
+ async sendStdin() {
793
+ throw new Error("Platform sandbox command execution does not support stdin");
794
+ }
795
+ };
796
+ var PlatformProcessManager = class extends _mastra_core_workspace.SandboxProcessManager {
797
+ spawnCounter = 0;
798
+ /**
799
+ * Spawn a process on the remote sandbox.
800
+ *
801
+ * `command` is interpreted as a shell string by the remote shell, matching
802
+ * the {@link MastraSandbox} contract. See {@link PlatformSandbox.executeCommand}
803
+ * for the untrusted-input caveat: never pass untrusted values as `command`.
804
+ */
805
+ async spawn(command, options = {}) {
806
+ const handle = new PlatformProcessHandle(`platform-proc-${Date.now().toString(36)}-${(this.spawnCounter++).toString(36)}`, this.sandbox.executeCommand(command, void 0, options), options);
807
+ this._tracked.set(handle.pid, handle);
808
+ return handle;
809
+ }
810
+ async list() {
811
+ return Array.from(this._tracked.values()).map((handle) => ({
812
+ pid: handle.pid,
813
+ command: handle.command,
814
+ running: handle.exitCode === void 0,
815
+ ...handle.exitCode !== void 0 && { exitCode: handle.exitCode }
816
+ }));
817
+ }
818
+ };
819
+ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.MastraSandbox {
820
+ id;
821
+ name = "PlatformSandbox";
822
+ provider = "platform";
823
+ status = "pending";
824
+ _client;
825
+ _environmentId;
826
+ _sandboxId;
827
+ _idleTimeoutMinutes;
828
+ _networkIsolation;
829
+ _env;
830
+ _timeout;
831
+ _instructionsOverride;
832
+ _createdAt = null;
833
+ _webSocketFactory;
834
+ _privateNetFetch;
835
+ /**
836
+ * Registry that maps `sandboxId → instanceUrl` for the private-network
837
+ * exec path. Injected by the composition site via
838
+ * {@link PlatformSandboxOptions.addressRegistry} and populated by this
839
+ * class itself in `start()` when the workspace-proxy's create/reattach
840
+ * response includes an `instanceUrl` field. The registry IS the cache —
841
+ * there is no per-instance mirror on `PlatformSandbox`, so every exec is
842
+ * a `Map.get()` (in the default in-process impl) against the live view.
843
+ * When absent, executes go straight to the lease path with no extra
844
+ * round-trip.
845
+ */
846
+ _addressRegistry;
847
+ /**
848
+ * Cached exec lease for this sandbox. `null` before the first exec and
849
+ * after {@link destroy}. Refreshed when `expiresAt - LEASE_REFRESH_MARGIN_MS < now`
850
+ * (see {@link _ensureLease}); a lease without a disclosed `expiresAt`
851
+ * is refreshed on every call.
852
+ */
853
+ _lease = null;
854
+ /**
855
+ * In-flight mint request; concurrent `_ensureLease` callers on a cold or
856
+ * near-expiry cache all await this single promise so we don't burn N
857
+ * `POST /exec-lease` round-trips when the sandbox is doing N parallel execs.
858
+ * Cleared (regardless of success or failure) when the request settles.
859
+ */
860
+ _leaseInFlight = null;
861
+ /**
862
+ * True when this sandbox was constructed with a caller-supplied `id` (the
863
+ * recovery key the proxy hashes into an on-provider checkpoint name).
864
+ * `captureCheckpoint()` needs this to distinguish "no checkpoint intent"
865
+ * (auto-generated random id — capture would land under a name no future
866
+ * boot would look for) from "capture on demand". Cloned sandboxes route
867
+ * `checkpointName` through `id`, so both entry points set this the same
868
+ * way.
869
+ */
870
+ _hasRecoveryKey;
871
+ /**
872
+ * In-flight `captureCheckpoint()` request. Concurrent callers on the same
873
+ * instance coalesce onto this single promise so we don't burn N `POST
874
+ * /checkpoint` round-trips when the fleet fires several turn-end captures
875
+ * before the first one resolves. Cleared when the request settles.
876
+ */
877
+ _captureInFlight = null;
878
+ constructor(options = {}) {
879
+ super({
880
+ ...options,
881
+ name: "PlatformSandbox",
882
+ processes: new PlatformProcessManager()
883
+ });
884
+ this._hasRecoveryKey = options.id !== void 0;
885
+ this.id = options.id ?? this.generateId();
886
+ this._client = new PlatformClient(options);
887
+ this._environmentId = options.environmentId ?? process.env.MASTRA_ENVIRONMENT_ID ?? "";
888
+ if (!this._environmentId && !options.sandboxId) throw new Error("environmentId is required");
889
+ this._sandboxId = options.sandboxId;
890
+ this._idleTimeoutMinutes = options.idleTimeoutMinutes;
891
+ this._networkIsolation = options.networkIsolation;
892
+ this._env = options.env ?? {};
893
+ this._timeout = options.timeout;
894
+ this._instructionsOverride = options.instructions;
895
+ this._webSocketFactory = options.webSocketFactory;
896
+ this._privateNetFetch = options.privateNetFetch;
897
+ this._addressRegistry = options.addressRegistry;
898
+ }
899
+ generateId() {
900
+ return `platform-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
901
+ }
902
+ /**
903
+ * Construct a sibling {@link PlatformSandbox} that inherits this sandbox's
904
+ * credentials and defaults (access token, project, environment, network
905
+ * isolation, timeout, instructions, env, idle timeout) with per-instance
906
+ * overrides from `options`.
907
+ *
908
+ * Performs no I/O and does not require this sandbox to be started — the
909
+ * returned sandbox is not started and provisions (or reattaches, when
910
+ * `sandboxId` is set) on its own `start()`. Use it when one configured
911
+ * sandbox acts as the template for a fleet of independent sandboxes
912
+ * (e.g. one per project).
913
+ */
914
+ clone(options = {}) {
915
+ const id = options.id ?? options.checkpointName;
916
+ return new PlatformSandbox({
917
+ ...id !== void 0 && { id },
918
+ accessToken: this._client.accessToken,
919
+ projectId: this._client.projectId,
920
+ fetch: this._client.fetch,
921
+ environmentId: this._environmentId,
922
+ ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
923
+ idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
924
+ ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
925
+ env: options.env ?? this._env,
926
+ ...this._timeout !== void 0 && { timeout: this._timeout },
927
+ ...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride },
928
+ ...this._webSocketFactory !== void 0 && { webSocketFactory: this._webSocketFactory },
929
+ ...this._privateNetFetch !== void 0 && { privateNetFetch: this._privateNetFetch },
930
+ ...this._addressRegistry !== void 0 && { addressRegistry: this._addressRegistry }
931
+ });
932
+ }
933
+ async start() {
934
+ if (this._sandboxId) try {
935
+ const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
936
+ if (!json.destroyedAt) {
937
+ this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
938
+ this._populateAddressFromResponse(json);
939
+ return;
940
+ }
941
+ this._sandboxId = void 0;
942
+ } catch (error) {
943
+ if (!(error instanceof PlatformApiError) || error.status !== 404) throw error;
944
+ this._sandboxId = void 0;
945
+ }
946
+ if (!this._environmentId) throw new Error("environmentId is required");
947
+ const body = JSON.stringify({
948
+ id: this.id,
949
+ environmentId: this._environmentId,
950
+ idleTimeoutMinutes: this._idleTimeoutMinutes,
951
+ networkIsolation: this._networkIsolation,
952
+ env: this._env
953
+ });
954
+ let response;
955
+ for (let attempt = 1;; attempt++) try {
956
+ response = await this._client.request("/sandbox", {
957
+ method: "POST",
958
+ headers: { "content-type": "application/json" },
959
+ body
960
+ });
961
+ break;
962
+ } catch (error) {
963
+ if (!(error instanceof PlatformApiError && error.status >= 500) || attempt >= CREATE_MAX_ATTEMPTS) throw error;
964
+ await new Promise((resolve) => setTimeout(resolve, CREATE_RETRY_BASE_DELAY_MS * attempt));
965
+ }
966
+ const json = await response.json();
967
+ this._sandboxId = json.id;
968
+ this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
969
+ this._populateAddressFromResponse(json);
970
+ }
971
+ /**
972
+ * Copy `response.instanceUrl` into the injected {@link SandboxAddressRegistry}
973
+ * when both are present. Called from both {@link start} branches (fresh
974
+ * provision + reattach) with the workspace-proxy response for this sandbox.
975
+ *
976
+ * The proxy discovers the IPv6 during `Sandbox.create()` and stores it in
977
+ * `environment_sandboxes.instance_url`; both the create response and
978
+ * `GET /sandbox/:id` echo the same field. The runtime does not do any
979
+ * discovery of its own — it only mirrors the field into an in-process map
980
+ * so {@link executeCommand} can `Map.get()` before every exec without an
981
+ * HTTP round-trip.
982
+ *
983
+ * `null`/absent `instanceUrl` (proxy discovery failed, or an older proxy
984
+ * that predates the field) leaves the registry untouched — executes fall
985
+ * through to the lease path with no branch here.
986
+ */
987
+ _populateAddressFromResponse(json) {
988
+ if (!this._addressRegistry) return;
989
+ if (!json.instanceUrl) return;
990
+ this._addressRegistry.set(json.id, json.instanceUrl);
991
+ }
992
+ async stop() {
993
+ await this.destroy();
994
+ }
995
+ async destroy() {
996
+ if (!this._sandboxId) return;
997
+ const destroyedSandboxId = this._sandboxId;
998
+ await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
999
+ this._sandboxId = void 0;
1000
+ this._createdAt = null;
1001
+ this._lease = null;
1002
+ this._addressRegistry?.delete(destroyedSandboxId);
1003
+ }
1004
+ /**
1005
+ * Capture the sandbox's checkpoint on demand, outside any refresh timer the
1006
+ * workspace-proxy owns internally.
1007
+ *
1008
+ * Intended for callers (e.g. a factory-side scheduler) that want to refresh
1009
+ * the recovery checkpoint at semantic moments — turn end, session-idle,
1010
+ * pre-teardown — rather than only just before the upstream's idle destroy.
1011
+ *
1012
+ * Mirrors the OSS `@mastra/railway` `RailwaySandbox.captureCheckpoint()`
1013
+ * shape so factory can call `sandbox.captureCheckpoint()` uniformly and
1014
+ * branch on `status`/`reason` without knowing which provider is underneath.
1015
+ * Both `captured` and `coalesced` carry the checkpoint name inline so the
1016
+ * caller can persist a session→checkpoint binding atomically with the
1017
+ * awaited capture.
1018
+ *
1019
+ * Skip semantics:
1020
+ * - No caller-supplied `id`: returns `{ status: 'skipped', reason:
1021
+ * 'no-checkpoint-name-configured' }`. An auto-generated random id is
1022
+ * never a meaningful recovery key (no future boot would look for a
1023
+ * checkpoint under it), so capturing would silently produce dead data.
1024
+ * - Not started (no `_sandboxId`): returns `{ status: 'skipped', reason:
1025
+ * 'sandbox-not-running' }` without a round-trip.
1026
+ * - Upstream 410 (workspace-proxy or Railway reports the sandbox is
1027
+ * already destroyed): returns the same `sandbox-not-running` skip so
1028
+ * the discriminant matches the pre-flight case. Local state
1029
+ * (`_sandboxId`, `_lease`, sidecar address) is cleared as a side
1030
+ * effect so the next `start()` provisions fresh instead of reattaching
1031
+ * to a dead id. The diagnostic distinction (pre-flight vs post-hoc)
1032
+ * is preserved in log level: debug for the expected pre-flight skip,
1033
+ * warn for the surprise upstream destroy.
1034
+ *
1035
+ * Concurrent callers on the same instance coalesce onto a single in-flight
1036
+ * `POST /checkpoint` so N simultaneous turn-end fires (e.g. several tabs)
1037
+ * do not each round-trip the proxy. Both the originator and joiners
1038
+ * receive `{ status: 'coalesced', ... }` for the joined result — the
1039
+ * outer contract does not distinguish who started the request, only that
1040
+ * one upstream capture was made.
1041
+ *
1042
+ * Never throws for expected outcomes. Transport failures (5xx, 4xx other
1043
+ * than 410) propagate as {@link PlatformApiError}; a 410 is normalized
1044
+ * to a skip as described above.
1045
+ */
1046
+ async captureCheckpoint() {
1047
+ if (!this._hasRecoveryKey) {
1048
+ this.logger.debug(`captureCheckpoint skipped: no recovery key configured for sandbox ${this._sandboxId ?? "(unstarted)"}`);
1049
+ return {
1050
+ status: "skipped",
1051
+ reason: "no-checkpoint-name-configured"
1052
+ };
1053
+ }
1054
+ if (!this._sandboxId) {
1055
+ this.logger.debug(`captureCheckpoint skipped: sandbox not running (local pre-flight, id=${this.id})`);
1056
+ return {
1057
+ status: "skipped",
1058
+ reason: "sandbox-not-running"
1059
+ };
1060
+ }
1061
+ if (this._captureInFlight) return this._captureInFlight;
1062
+ const sandboxId = this._sandboxId;
1063
+ const capture = this._doCaptureCheckpoint(sandboxId).finally(() => {
1064
+ if (this._captureInFlight === capture) this._captureInFlight = null;
1065
+ });
1066
+ this._captureInFlight = capture;
1067
+ return capture;
1068
+ }
1069
+ /**
1070
+ * The single `POST /checkpoint` attempt behind {@link captureCheckpoint}.
1071
+ *
1072
+ * Split out so the coalescing wrapper can install a shared in-flight
1073
+ * promise without inlining the transport + response-mapping logic.
1074
+ * Joined callers observe `{ status: 'coalesced', ... }` — the initiator
1075
+ * sees the underlying `captured` / `coalesced` / `skipped` result the
1076
+ * proxy returned. Both are legitimate: the OSS mirror uses the same
1077
+ * "initiator sees the truth, joiners see coalesced" split.
1078
+ */
1079
+ async _doCaptureCheckpoint(sandboxId) {
1080
+ let response;
1081
+ try {
1082
+ response = await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/checkpoint`, {
1083
+ method: "POST",
1084
+ headers: { "content-type": "application/json" },
1085
+ body: JSON.stringify({ id: this.id })
1086
+ });
1087
+ } catch (error) {
1088
+ if (error instanceof PlatformApiError && error.status === 410) {
1089
+ this.logger.warn(`captureCheckpoint skipped: sandbox destroyed upstream (proxy 410, sandboxId=${sandboxId})`);
1090
+ this._clearDestroyedState(sandboxId);
1091
+ return {
1092
+ status: "skipped",
1093
+ reason: "sandbox-not-running"
1094
+ };
1095
+ }
1096
+ throw error;
1097
+ }
1098
+ const json = await response.json();
1099
+ if (json.status === "skipped") {
1100
+ this.logger.warn(`captureCheckpoint skipped: sandbox destroyed upstream (proxy reported skipped, sandboxId=${sandboxId})`);
1101
+ this._clearDestroyedState(sandboxId);
1102
+ return {
1103
+ status: "skipped",
1104
+ reason: "sandbox-not-running"
1105
+ };
1106
+ }
1107
+ return {
1108
+ status: json.status,
1109
+ checkpointName: json.checkpointName
1110
+ };
1111
+ }
1112
+ /**
1113
+ * Clear local state that would otherwise let the caller keep exec'ing
1114
+ * against a sandbox the upstream has already destroyed. Mirrors what
1115
+ * `destroy()` does minus the outbound DELETE — the sandbox is already
1116
+ * gone, so all that remains is to stop pointing at it.
1117
+ *
1118
+ * Also resets `status` to `'pending'` so a subsequent `_start()` on this
1119
+ * reused instance re-runs provisioning instead of short-circuiting on
1120
+ * the cached `'running'` state (see `MastraSandbox._start`).
1121
+ */
1122
+ _clearDestroyedState(destroyedSandboxId) {
1123
+ this._sandboxId = void 0;
1124
+ this._createdAt = null;
1125
+ this._lease = null;
1126
+ this._addressRegistry?.delete(destroyedSandboxId);
1127
+ this.status = "pending";
1128
+ }
1129
+ /**
1130
+ * Execute a command on the remote sandbox.
1131
+ *
1132
+ * `command` is a **shell string**: it is concatenated verbatim into the
1133
+ * command line sent to the remote shell, which lets callers use pipes,
1134
+ * redirects, and chaining (`ls -la | grep foo`). This matches the contract
1135
+ * of {@link MastraSandbox} and the local sandbox implementation.
1136
+ *
1137
+ * `args`, when provided, are always shell-quoted so they cannot inject
1138
+ * additional shell syntax.
1139
+ *
1140
+ * Security: callers MUST NOT pass untrusted input as `command`. If any part
1141
+ * of the invocation is derived from an untrusted source, pass it through
1142
+ * `args` (which is safely quoted) or shell-quote it yourself before
1143
+ * inclusion. Untrusted `command` values allow arbitrary shell syntax
1144
+ * execution on the remote sandbox.
1145
+ */
1146
+ async executeCommand(command, args, options) {
1147
+ await this.ensureRunning();
1148
+ if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
1149
+ const started = Date.now();
1150
+ const fullCommand = buildCommand(command, args);
1151
+ const effectiveTimeout = options?.timeout ?? this._timeout;
1152
+ const instanceUrl = this._addressRegistry?.get(this._sandboxId);
1153
+ if (instanceUrl) {
1154
+ const privateNet = await this._tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options);
1155
+ if (privateNet) {
1156
+ const privateExit = privateNet.exitCode ?? 124;
1157
+ return {
1158
+ success: privateExit === 0,
1159
+ exitCode: privateExit,
1160
+ stdout: privateNet.stdout,
1161
+ stderr: privateNet.stderr,
1162
+ timedOut: privateNet.timedOut,
1163
+ command: fullCommand,
1164
+ executionTimeMs: Date.now() - started
1165
+ };
1166
+ }
1167
+ }
1168
+ const result = await this._runDirectExec(fullCommand, effectiveTimeout, options);
1169
+ const exitCode = result.exitCode ?? 124;
1170
+ return {
1171
+ success: exitCode === 0,
1172
+ exitCode,
1173
+ stdout: result.stdout,
1174
+ stderr: result.stderr,
1175
+ timedOut: result.timedOut,
1176
+ command: fullCommand,
1177
+ executionTimeMs: Date.now() - started
1178
+ };
1179
+ }
1180
+ /**
1181
+ * Run a single exec against the direct-exec transport, with one in-flight
1182
+ * retry on WebSocket transport failure (socket closed without an `exit`
1183
+ * frame and the exec did not time out). The retry mints a fresh lease
1184
+ * — the failure could be a stale JWT — and reopens a new WebSocket.
1185
+ *
1186
+ * Error taxonomy:
1187
+ * - **410 on `/exec-lease`** (either attempt) → the sandbox is gone.
1188
+ * Nulls the cached `_lease` and `_sandboxId` and throws
1189
+ * {@link SandboxDestroyedError}. Callers (typically `SandboxFleet`) must
1190
+ * catch this, clear the stale binding, and reprovision + replay.
1191
+ * - **Persistent transport failure** (both WS attempts close without an
1192
+ * `exit` frame against a live sandbox) → {@link SandboxExecTransportError}
1193
+ * with WebSocket close diagnostics.
1194
+ * - **Other `PlatformApiError`s** (404/500/501) propagate directly.
1195
+ * - **Real command result** (exit code from Railway's exit frame, or
1196
+ * `timedOut: true`) returns normally.
1197
+ *
1198
+ * Returns a result with a real `exitCode` OR `timedOut: true`. Never
1199
+ * returns `{ exitCode: null, timedOut: false }` — that case throws.
1200
+ */
1201
+ async _runDirectExec(fullCommand, effectiveTimeout, options) {
1202
+ const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
1203
+ let lastResult;
1204
+ let lastLease;
1205
+ let attemptsMade = 0;
1206
+ for (let attempt = 0; attempt < 2; attempt++) {
1207
+ if (attempt > 0 && lastLease && this._lease === lastLease) this._lease = null;
1208
+ let lease;
1209
+ try {
1210
+ lease = await this._ensureLease();
1211
+ } catch (error) {
1212
+ if (error instanceof PlatformApiError && error.status === 410) {
1213
+ this._lease = null;
1214
+ const priorSandboxId = this._sandboxId;
1215
+ this._sandboxId = void 0;
1216
+ throw new SandboxDestroyedError(`Sandbox ${priorSandboxId ?? "(unknown)"} was destroyed; /exec-lease returned 410`, {
1217
+ ...priorSandboxId && { sandboxId: priorSandboxId },
1218
+ command: fullCommand,
1219
+ attempts: attempt + 1
1220
+ });
1221
+ }
1222
+ throw error;
1223
+ }
1224
+ lastLease = lease;
1225
+ attemptsMade = attempt + 1;
1226
+ const result = await execViaLease(lease, {
1227
+ command: fullCommand,
1228
+ ...options?.cwd !== void 0 && { cwd: options.cwd },
1229
+ ...filteredEnv !== void 0 && { env: filteredEnv },
1230
+ ...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
1231
+ ...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
1232
+ });
1233
+ lastResult = result;
1234
+ if (result.exitCode !== null || result.timedOut) return result;
1235
+ }
1236
+ const result = lastResult;
1237
+ const lease = lastLease;
1238
+ if (this._lease === lease) this._lease = null;
1239
+ 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}` : ""})` : ""), {
1240
+ ...this._sandboxId && { sandboxId: this._sandboxId },
1241
+ command: fullCommand,
1242
+ attempts: attemptsMade,
1243
+ opened: result.opened ?? false,
1244
+ ...result.closeCode !== void 0 && { closeCode: result.closeCode },
1245
+ ...result.closeReason !== void 0 && { closeReason: result.closeReason },
1246
+ wsEndpoint: lease.wsEndpoint
1247
+ });
1248
+ }
1249
+ /**
1250
+ * Try to run the exec against the in-sandbox sidecar over Railway's private
1251
+ * network. Returns the result on success (including non-zero exit codes and
1252
+ * timeouts — those are real command results, not failures). Returns
1253
+ * `undefined` when the caller should fall back to the lease path:
1254
+ *
1255
+ * - Transport failure (connection refused, mid-stream drop, no `exit`
1256
+ * frame). The registry entry is evicted so subsequent execs skip the
1257
+ * private-net dial until the sidecar re-registers.
1258
+ * - Sidecar answered with a non-2xx HTTP status. Registry is left intact —
1259
+ * the address is still valid; something else is wrong (bad request,
1260
+ * sidecar bug). Only this specific exec falls back.
1261
+ */
1262
+ async _tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options) {
1263
+ const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
1264
+ const execOptions = {
1265
+ command: fullCommand,
1266
+ ...options?.cwd !== void 0 && { cwd: options.cwd },
1267
+ ...filteredEnv !== void 0 && { env: filteredEnv },
1268
+ ...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
1269
+ ...this._privateNetFetch && { fetch: this._privateNetFetch }
1270
+ };
1271
+ let result;
1272
+ try {
1273
+ result = await execViaPrivateNetwork(instanceUrl, execOptions);
1274
+ } catch (error) {
1275
+ if (error instanceof PrivateNetExecHttpError) return;
1276
+ this._invalidateAddress();
1277
+ return;
1278
+ }
1279
+ if (result.timedOut) {
1280
+ if (!result.opened) this._invalidateAddress();
1281
+ return result;
1282
+ }
1283
+ if (!result.opened || result.exitCode === null) {
1284
+ this._invalidateAddress();
1285
+ return;
1286
+ }
1287
+ return result;
1288
+ }
1289
+ /**
1290
+ * Evict this sandbox's entry from the address registry after an observed
1291
+ * transport failure. The entry stays gone until the next start() re-reads
1292
+ * `instanceUrl` from a workspace-proxy response — until then, execs skip
1293
+ * the private-net dial and go straight to the lease path.
1294
+ */
1295
+ _invalidateAddress() {
1296
+ if (this._sandboxId) this._addressRegistry?.delete(this._sandboxId);
1297
+ }
1298
+ /**
1299
+ * Return a cached exec lease, minting a fresh one when the cache is empty
1300
+ * or the JWT is within {@link LEASE_REFRESH_MARGIN_MS} of `expiresAt`.
1301
+ *
1302
+ * Callers are expected to be on the "sandbox is running" path; we don't
1303
+ * re-check `_sandboxId` here because `executeCommand` already gated on it.
1304
+ */
1305
+ async _ensureLease() {
1306
+ const now = Date.now();
1307
+ if (this._lease && this._lease.expiresAtMs !== null && this._lease.expiresAtMs - LEASE_REFRESH_MARGIN_MS > now) return this._lease;
1308
+ if (this._leaseInFlight) return this._leaseInFlight;
1309
+ if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
1310
+ const sandboxId = this._sandboxId;
1311
+ const inFlight = (async () => {
1312
+ const json = await (await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/exec-lease`, { method: "POST" })).json();
1313
+ const expiresAtMs = json.expiresAt ? Date.parse(json.expiresAt) : null;
1314
+ const lease = {
1315
+ jwt: json.jwt,
1316
+ wsEndpoint: json.wsEndpoint,
1317
+ subprotocol: json.subprotocol,
1318
+ expiresAt: json.expiresAt,
1319
+ expiresAtMs: expiresAtMs !== null && !Number.isNaN(expiresAtMs) ? expiresAtMs : null
1320
+ };
1321
+ this._lease = lease;
1322
+ return lease;
1323
+ })();
1324
+ this._leaseInFlight = inFlight;
1325
+ try {
1326
+ return await inFlight;
1327
+ } finally {
1328
+ if (this._leaseInFlight === inFlight) this._leaseInFlight = null;
1329
+ }
1330
+ }
1331
+ async getInfo() {
1332
+ if (!this._sandboxId) return {
1333
+ id: this.id,
1334
+ name: this.name,
1335
+ provider: this.provider,
1336
+ status: this.status,
1337
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date()
1338
+ };
1339
+ if (this._addressRegistry?.get(this._sandboxId)) return {
1340
+ id: this._sandboxId,
1341
+ name: this.name,
1342
+ provider: this.provider,
1343
+ status: this.status,
1344
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
1345
+ metadata: { sandboxId: this._sandboxId }
1346
+ };
1347
+ const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
1348
+ return {
1349
+ id: json.id,
1350
+ name: this.name,
1351
+ provider: this.provider,
1352
+ status: this.status,
1353
+ createdAt: json.createdAt ? new Date(json.createdAt) : this._createdAt ?? /* @__PURE__ */ new Date(),
1354
+ metadata: {
1355
+ sandboxId: json.id,
1356
+ providerResourceId: json.providerResourceId ?? void 0,
1357
+ platformStatus: json.status
1358
+ }
1359
+ };
1360
+ }
1361
+ getInstructions(opts) {
1362
+ const defaultInstructions = `Platform sandbox${this._sandboxId ? ` ${this._sandboxId}` : ""}. Execute commands with the sandbox command APIs.`;
1363
+ if (typeof this._instructionsOverride === "function") return this._instructionsOverride({
1364
+ defaultInstructions,
1365
+ requestContext: opts?.requestContext
1366
+ });
1367
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
1368
+ return defaultInstructions;
1369
+ }
1370
+ };
1371
+ //#endregion
1372
+ //#region src/provider.ts
1373
+ const platformSandboxProvider = {
1374
+ id: "platform",
1375
+ name: "Mastra Platform Sandbox",
1376
+ description: "Environment-scoped sandbox execution through Mastra Platform workspace proxy",
1377
+ configSchema: {
1378
+ type: "object",
1379
+ properties: {
1380
+ accessToken: {
1381
+ type: "string",
1382
+ description: "Mastra Platform access token (falls back to MASTRA_PLATFORM_ACCESS_TOKEN)"
1383
+ },
1384
+ projectId: {
1385
+ type: "string",
1386
+ description: "Platform project ID (falls back to MASTRA_PROJECT_ID)"
1387
+ },
1388
+ environmentId: {
1389
+ type: "string",
1390
+ description: "Platform environment ID (falls back to MASTRA_ENVIRONMENT_ID)"
1391
+ },
1392
+ sandboxId: {
1393
+ type: "string",
1394
+ description: "Reattach to an existing Platform sandbox by ID"
1395
+ },
1396
+ idleTimeoutMinutes: {
1397
+ type: "number",
1398
+ description: "Minutes before the sandbox can be destroyed while idle"
1399
+ },
1400
+ networkIsolation: {
1401
+ type: "string",
1402
+ description: "Network isolation mode",
1403
+ enum: ["ISOLATED", "PRIVATE"],
1404
+ default: "ISOLATED"
1405
+ },
1406
+ env: {
1407
+ type: "object",
1408
+ description: "Environment variables",
1409
+ additionalProperties: { type: "string" }
1410
+ },
1411
+ timeout: {
1412
+ type: "number",
1413
+ description: "Default command timeout in ms"
1414
+ }
1415
+ }
1416
+ },
1417
+ createSandbox: (config) => new PlatformSandbox(config)
1418
+ };
1419
+ const platformFilesystemProvider = {
1420
+ id: "platform",
1421
+ name: "Mastra Platform Filesystem",
1422
+ description: "Bucket-backed filesystem access through Mastra Platform workspace proxy",
1423
+ configSchema: {
1424
+ type: "object",
1425
+ properties: {
1426
+ accessToken: {
1427
+ type: "string",
1428
+ description: "Mastra Platform access token (falls back to MASTRA_PLATFORM_ACCESS_TOKEN)"
1429
+ },
1430
+ projectId: {
1431
+ type: "string",
1432
+ description: "Platform project ID (falls back to MASTRA_PROJECT_ID)"
1433
+ },
1434
+ bucketName: {
1435
+ type: "string",
1436
+ description: "Platform workspace bucket name (falls back to MASTRA_PLATFORM_BUCKET_NAME)"
1437
+ },
1438
+ readOnly: {
1439
+ type: "boolean",
1440
+ description: "Mount as read-only",
1441
+ default: false
1442
+ }
1443
+ }
1444
+ },
1445
+ createFilesystem: (config) => new PlatformFilesystem(config)
1446
+ };
1447
+ //#endregion
1448
+ //#region src/address-registry.ts
1449
+ /**
1450
+ * Concrete in-process {@link SandboxAddressRegistry}. Backed by a `Map`; no
1451
+ * eviction policy, no TTL — entries live until an observed transport failure
1452
+ * calls `delete`, until the sandbox is explicitly destroyed, or until the
1453
+ * process exits.
1454
+ */
1455
+ var InProcessSandboxAddressRegistry = class {
1456
+ #map = /* @__PURE__ */ new Map();
1457
+ get(sandboxId) {
1458
+ return this.#map.get(sandboxId);
1459
+ }
1460
+ /**
1461
+ * Populate or overwrite the address for a sandbox. Called by
1462
+ * {@link PlatformSandbox.start} on every fresh provision and every reattach;
1463
+ * overwriting is intentional so a re-provision with a fresh IPv6 heals the
1464
+ * map without a branch.
1465
+ */
1466
+ set(sandboxId, instanceUrl) {
1467
+ this.#map.set(sandboxId, instanceUrl);
1468
+ }
1469
+ delete(sandboxId) {
1470
+ this.#map.delete(sandboxId);
1471
+ }
1472
+ /**
1473
+ * Test-only introspection. Not part of {@link SandboxAddressRegistry} —
1474
+ * production callers must not read the registry as a whole.
1475
+ */
1476
+ get size() {
1477
+ return this.#map.size;
1478
+ }
1479
+ };
1480
+ //#endregion
1481
+ exports.InProcessSandboxAddressRegistry = InProcessSandboxAddressRegistry;
1482
+ exports.PlatformApiError = PlatformApiError;
1483
+ exports.PlatformClient = PlatformClient;
1484
+ exports.PlatformFilesystem = PlatformFilesystem;
1485
+ exports.PlatformSandbox = PlatformSandbox;
1486
+ exports.PrivateNetExecHttpError = PrivateNetExecHttpError;
1487
+ exports.SandboxDestroyedError = SandboxDestroyedError;
1488
+ exports.SandboxExecTransportError = SandboxExecTransportError;
1489
+ exports.execViaPrivateNetwork = execViaPrivateNetwork;
1490
+ exports.platformFilesystemProvider = platformFilesystemProvider;
1491
+ exports.platformSandboxProvider = platformSandboxProvider;
1492
+
1493
+ //# sourceMappingURL=index.cjs.map