@mastra/platform-workspace 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,643 +1,964 @@
1
- 'use strict';
2
-
3
- var buffer = require('buffer');
4
- var nodePath = require('path');
5
- var workspace = require('@mastra/core/workspace');
6
-
7
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
-
9
- var nodePath__default = /*#__PURE__*/_interopDefault(nodePath);
10
-
11
- // src/client.ts
12
- var DEFAULT_PROXY_URL = "https://workspaces.mastra.ai";
13
- var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
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;
14
36
  function requireOption(value, name) {
15
- if (!value) throw new Error(`${name} is required`);
16
- return value;
37
+ if (!value) throw new Error(`${name} is required`);
38
+ return value;
17
39
  }
18
40
  function resolvePlatformOptions(options) {
19
- return {
20
- accessToken: requireOption(
21
- options.accessToken ?? process.env.MASTRA_PLATFORM_SECRET_KEY ?? // Deprecated alias — prefer MASTRA_PLATFORM_SECRET_KEY.
22
- process.env.MASTRA_PLATFORM_ACCESS_TOKEN,
23
- "accessToken"
24
- ),
25
- projectId: requireOption(options.projectId ?? process.env.MASTRA_PROJECT_ID, "projectId"),
26
- proxyUrl: (process.env.MASTRA_WORKSPACE_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/$/, ""),
27
- fetch: options.fetch ?? fetch
28
- };
41
+ return {
42
+ accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_SECRET_KEY ?? 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
+ };
29
47
  }
30
48
  function parseProxyError(body) {
31
- if (!body) return void 0;
32
- let parsed;
33
- try {
34
- parsed = JSON.parse(body);
35
- } catch {
36
- return void 0;
37
- }
38
- if (typeof parsed !== "object" || parsed === null) return void 0;
39
- const err = parsed.error;
40
- if (typeof err !== "object" || err === null) return void 0;
41
- const { message, type } = err;
42
- if (typeof message !== "string" || typeof type !== "string") return void 0;
43
- return { message, type };
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
+ };
44
65
  }
45
66
  var PlatformApiError = class extends Error {
46
- status;
47
- body;
48
- /** Machine-readable proxy error kind (e.g. `not_found`), when the response body matches `{ error: { message, type } }`. */
49
- code;
50
- /** Human-readable proxy error message, when the response body matches `{ error: { message, type } }`. */
51
- proxyMessage;
52
- constructor(status, body) {
53
- const parsed = parseProxyError(body);
54
- const summary = parsed ? `${parsed.type}: ${parsed.message}` : body;
55
- super(`Platform proxy request failed with ${status}${summary ? `: ${summary}` : ""}`);
56
- this.name = "PlatformApiError";
57
- this.status = status;
58
- this.body = body;
59
- this.code = parsed?.type;
60
- this.proxyMessage = parsed?.message;
61
- }
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
+ }
62
83
  };
63
84
  var PlatformClient = class {
64
- accessToken;
65
- projectId;
66
- proxyUrl;
67
- fetch;
68
- constructor(options) {
69
- const resolved = resolvePlatformOptions(options);
70
- this.accessToken = resolved.accessToken;
71
- this.projectId = resolved.projectId;
72
- this.proxyUrl = resolved.proxyUrl;
73
- this.fetch = resolved.fetch;
74
- }
75
- async request(path, options = {}) {
76
- const url = new URL(`${this.proxyUrl}/v1/projects/${encodeURIComponent(this.projectId)}${path}`);
77
- for (const [key, value] of Object.entries(options.query ?? {})) {
78
- if (value !== void 0) url.searchParams.set(key, String(value));
79
- }
80
- const headers = new Headers(options.headers);
81
- headers.set("authorization", `Bearer ${this.accessToken}`);
82
- const { query: _query, ...fetchOptions } = options;
83
- const signal = fetchOptions.signal ?? AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
84
- const response = await this.fetch(url, { ...fetchOptions, headers, signal });
85
- if (!response.ok) {
86
- throw new PlatformApiError(response.status, await response.text());
87
- }
88
- return response;
89
- }
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
+ }
90
111
  };
112
+ //#endregion
113
+ //#region src/filesystem.ts
91
114
  function normalizePath(input) {
92
- if (!input || input === ".") return "/";
93
- let normalized = input.startsWith("/") ? input : `/${input}`;
94
- normalized = nodePath__default.default.posix.normalize(normalized);
95
- return normalized === "." ? "/" : normalized;
115
+ if (!input || input === ".") return "/";
116
+ let normalized = input.startsWith("/") ? input : `/${input}`;
117
+ normalized = path.default.posix.normalize(normalized);
118
+ return normalized === "." ? "/" : normalized;
96
119
  }
97
- function keyFromPath(path) {
98
- const normalized = normalizePath(path);
99
- return normalized === "/" ? "" : normalized.slice(1);
120
+ function keyFromPath(path$1) {
121
+ const normalized = normalizePath(path$1);
122
+ return normalized === "/" ? "" : normalized.slice(1);
100
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
+ */
101
130
  function encodeKeyPath(key) {
102
- return key.split("/").map(encodeURIComponent).join("/");
131
+ return key.split("/").map(encodeURIComponent).join("/");
103
132
  }
104
- function nameFromPath(path) {
105
- const normalized = normalizePath(path);
106
- if (normalized === "/") return "";
107
- return normalized.slice(normalized.lastIndexOf("/") + 1);
133
+ function nameFromPath(path$2) {
134
+ const normalized = normalizePath(path$2);
135
+ if (normalized === "/") return "";
136
+ return normalized.slice(normalized.lastIndexOf("/") + 1);
108
137
  }
109
138
  function contentToBody(content) {
110
- if (typeof content === "string") return content;
111
- return buffer.Buffer.from(content);
139
+ if (typeof content === "string") return content;
140
+ return buffer.Buffer.from(content);
112
141
  }
113
142
  function headerDate(headers, name) {
114
- const value = headers.get(name);
115
- return value ? new Date(value) : /* @__PURE__ */ new Date(0);
143
+ const value = headers.get(name);
144
+ return value ? new Date(value) : /* @__PURE__ */ new Date(0);
116
145
  }
117
146
  function headerSize(headers) {
118
- const value = headers.get("content-length");
119
- return value ? Number(value) : 0;
147
+ const value = headers.get("content-length");
148
+ return value ? Number(value) : 0;
120
149
  }
121
150
  function isNotFound(error) {
122
- return typeof error === "object" && error !== null && "status" in error && error.status === 404;
151
+ return typeof error === "object" && error !== null && "status" in error && error.status === 404;
123
152
  }
124
- var PlatformFilesystem = class extends workspace.MastraFilesystem {
125
- id;
126
- name = "PlatformFilesystem";
127
- provider = "platform";
128
- readOnly;
129
- displayName;
130
- icon;
131
- description;
132
- status = "pending";
133
- _client;
134
- _bucketName;
135
- _instructionsOverride;
136
- constructor(options = {}) {
137
- super({ ...options, name: "PlatformFilesystem" });
138
- this.id = options.id ?? this.generateId();
139
- this._bucketName = options.bucketName ?? process.env.MASTRA_PLATFORM_BUCKET_NAME ?? "";
140
- if (!this._bucketName) throw new Error("bucketName is required");
141
- this.readOnly = options.readOnly;
142
- this.displayName = options.displayName;
143
- this.icon = options.icon ?? "cloud";
144
- this.description = options.description;
145
- this._instructionsOverride = options.instructions;
146
- this._client = new PlatformClient(options);
147
- }
148
- generateId() {
149
- return `platform-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
150
- }
151
- async readFile(path, options) {
152
- await this.ensureReady();
153
- let response;
154
- try {
155
- response = await this._client.request(
156
- `/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`
157
- );
158
- } catch (error) {
159
- if (isNotFound(error)) throw new workspace.FileNotFoundError(path);
160
- throw error;
161
- }
162
- const buffer$1 = buffer.Buffer.from(await response.arrayBuffer());
163
- return options?.encoding ? buffer$1.toString(options.encoding) : buffer$1;
164
- }
165
- async writeFile(path, content, options) {
166
- await this.ensureReady();
167
- if (this.readOnly) throw new workspace.WorkspaceReadOnlyError("writeFile");
168
- const headers = {};
169
- if (options?.mimeType) headers["content-type"] = options.mimeType;
170
- if (options?.overwrite === false) headers["if-none-match"] = "*";
171
- try {
172
- await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`, {
173
- method: "PUT",
174
- headers,
175
- body: contentToBody(content)
176
- });
177
- } catch (error) {
178
- if (typeof error === "object" && error !== null && "status" in error && error.status === 412) {
179
- throw new workspace.FileExistsError(path);
180
- }
181
- throw error;
182
- }
183
- }
184
- /**
185
- * Append bytes to a file.
186
- *
187
- * **Not atomic.** Object storage behind the workspace proxy has no native
188
- * append or compare-and-swap primitive, so this implementation is a
189
- * read-modify-write: it reads the current contents, concatenates the new
190
- * bytes, and PUTs the whole object back. Concurrent `appendFile` calls to
191
- * the same path can overwrite each other's writes ("last write wins").
192
- * Use `writeFile` with distinct keys for concurrent writers.
193
- */
194
- async appendFile(path, content) {
195
- const existing = await this.exists(path) ? await this.readFile(path) : buffer.Buffer.alloc(0);
196
- await this.writeFile(
197
- path,
198
- buffer.Buffer.concat([buffer.Buffer.isBuffer(existing) ? existing : buffer.Buffer.from(existing), buffer.Buffer.from(content)])
199
- );
200
- }
201
- async deleteFile(path, options) {
202
- await this.ensureReady();
203
- if (this.readOnly) throw new workspace.WorkspaceReadOnlyError("deleteFile");
204
- try {
205
- await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`, {
206
- method: "DELETE",
207
- query: { recursive: options?.recursive }
208
- });
209
- } catch (error) {
210
- if (isNotFound(error) && options?.force) return;
211
- if (isNotFound(error)) throw new workspace.FileNotFoundError(path);
212
- throw error;
213
- }
214
- }
215
- async copyFile(src, dest, options) {
216
- await this.ensureReady();
217
- if (this.readOnly) throw new workspace.WorkspaceReadOnlyError("copyFile");
218
- if (options?.overwrite === false) {
219
- throw new Error("PlatformFilesystem.copyFile does not support overwrite: false \u2014 the proxy always overwrites.");
220
- }
221
- await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(src))}`, {
222
- method: "POST",
223
- query: { op: "copy" },
224
- headers: { "content-type": "application/json" },
225
- body: JSON.stringify({ destination: keyFromPath(dest) })
226
- });
227
- }
228
- async moveFile(src, dest, options) {
229
- await this.ensureReady();
230
- if (this.readOnly) throw new workspace.WorkspaceReadOnlyError("moveFile");
231
- if (options?.overwrite === false) {
232
- throw new Error("PlatformFilesystem.moveFile does not support overwrite: false \u2014 the proxy always overwrites.");
233
- }
234
- await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(src))}`, {
235
- method: "POST",
236
- query: { op: "rename" },
237
- headers: { "content-type": "application/json" },
238
- body: JSON.stringify({ destination: keyFromPath(dest) })
239
- });
240
- }
241
- async mkdir(path, _options) {
242
- await this.ensureReady();
243
- if (this.readOnly) throw new workspace.WorkspaceReadOnlyError("mkdir");
244
- await this._client.request(`/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`, {
245
- method: "POST",
246
- query: { op: "mkdir" }
247
- });
248
- }
249
- async rmdir(path, options) {
250
- await this.deleteFile(path.endsWith("/") ? path : `${path}/`, { recursive: true, force: options?.force });
251
- }
252
- async readdir(path, options) {
253
- await this.ensureReady();
254
- const prefix = keyFromPath(path);
255
- const response = await this._client.request(
256
- `/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(prefix)}`,
257
- {
258
- query: {
259
- delimiter: options?.recursive ? void 0 : "/",
260
- prefix: prefix ? `${prefix.replace(/\/$/, "")}/` : void 0
261
- }
262
- }
263
- );
264
- const json = await response.json();
265
- return [
266
- ...(json.commonPrefixes ?? []).map((prefix2) => ({
267
- name: nameFromPath(prefix2.replace(/\/$/, "")),
268
- type: "directory"
269
- })),
270
- ...(json.contents ?? []).filter((object) => object.key && !object.key.endsWith("/")).map((object) => ({
271
- name: nameFromPath(object.key),
272
- type: "file",
273
- size: object.size
274
- }))
275
- ].filter(
276
- (entry) => !options?.extension || entry.type === "directory" || matchesExtension(entry.name, options.extension)
277
- );
278
- }
279
- async exists(path) {
280
- try {
281
- await this.stat(path);
282
- return true;
283
- } catch (error) {
284
- if (isNotFound(error) || error instanceof workspace.FileNotFoundError) return false;
285
- throw error;
286
- }
287
- }
288
- async stat(path) {
289
- await this.ensureReady();
290
- const normalized = normalizePath(path);
291
- if (normalized === "/") {
292
- return { name: "", path: "/", type: "directory", size: 0, createdAt: /* @__PURE__ */ new Date(0), modifiedAt: /* @__PURE__ */ new Date(0) };
293
- }
294
- let response;
295
- try {
296
- response = await this._client.request(
297
- `/fs/${encodeURIComponent(this._bucketName)}/${encodeKeyPath(keyFromPath(path))}`,
298
- {
299
- method: "HEAD"
300
- }
301
- );
302
- } catch (error) {
303
- if (isNotFound(error)) throw new workspace.FileNotFoundError(path);
304
- throw error;
305
- }
306
- return {
307
- name: nameFromPath(path),
308
- path: normalized,
309
- type: normalized.endsWith("/") ? "directory" : "file",
310
- size: headerSize(response.headers),
311
- createdAt: headerDate(response.headers, "last-modified"),
312
- modifiedAt: headerDate(response.headers, "last-modified"),
313
- mimeType: response.headers.get("content-type") ?? void 0
314
- };
315
- }
316
- realpath(path) {
317
- return Promise.resolve(normalizePath(path));
318
- }
319
- getInstructions(opts) {
320
- const defaultInstructions = `Platform filesystem backed by Mastra Platform bucket ${this._bucketName}. Use absolute workspace paths.`;
321
- if (typeof this._instructionsOverride === "function") {
322
- return this._instructionsOverride({ defaultInstructions, requestContext: opts?.requestContext });
323
- }
324
- if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
325
- return defaultInstructions;
326
- }
327
- getInfo() {
328
- return {
329
- id: this.id,
330
- name: this.name,
331
- provider: this.provider,
332
- status: this.status,
333
- readOnly: this.readOnly,
334
- icon: this.icon,
335
- metadata: {
336
- bucketName: this._bucketName,
337
- ...this.displayName && { displayName: this.displayName },
338
- ...this.description && { description: this.description }
339
- }
340
- };
341
- }
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
+ }
342
356
  };
343
357
  function matchesExtension(name, extension) {
344
- const extensions = Array.isArray(extension) ? extension : [extension];
345
- return extensions.some((ext) => name.endsWith(ext));
358
+ return (Array.isArray(extension) ? extension : [extension]).some((ext) => name.endsWith(ext));
346
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 timer;
412
+ let handshakeTimer;
413
+ const settle = () => {
414
+ if (settled) return;
415
+ settled = true;
416
+ if (timer) clearTimeout(timer);
417
+ if (handshakeTimer) clearTimeout(handshakeTimer);
418
+ const stdoutTail = stdoutDecoder.decode();
419
+ if (stdoutTail) {
420
+ stdout += stdoutTail;
421
+ options.onStdout?.(stdoutTail);
422
+ }
423
+ const stderrTail = stderrDecoder.decode();
424
+ if (stderrTail) {
425
+ stderr += stderrTail;
426
+ options.onStderr?.(stderrTail);
427
+ }
428
+ try {
429
+ socket.close(1e3, "");
430
+ } catch {}
431
+ resolve({
432
+ exitCode,
433
+ stdout,
434
+ stderr,
435
+ truncated: false,
436
+ timedOut
437
+ });
438
+ };
439
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) timer = setTimeout(() => {
440
+ timedOut = true;
441
+ if (exitCode === null) exitCode = 124;
442
+ settle();
443
+ }, options.timeoutMs);
444
+ else handshakeTimer = setTimeout(() => {
445
+ if (!opened) settle();
446
+ }, HANDSHAKE_DEADLINE_MS);
447
+ const socket = factory(lease.wsEndpoint, [lease.subprotocol, lease.jwt]);
448
+ socket.binaryType = "arraybuffer";
449
+ socket.onopen = () => {
450
+ opened = true;
451
+ if (handshakeTimer) {
452
+ clearTimeout(handshakeTimer);
453
+ handshakeTimer = void 0;
454
+ }
455
+ const data = { command: options.command };
456
+ if (options.cwd) data.cwd = options.cwd;
457
+ if (options.env && Object.keys(options.env).length > 0) data.env = options.env;
458
+ socket.send(JSON.stringify({
459
+ type: "init_exec",
460
+ data
461
+ }));
462
+ socket.send(JSON.stringify({ type: "stdin_close" }));
463
+ };
464
+ socket.onmessage = (event) => {
465
+ const { data } = event;
466
+ if (data instanceof ArrayBuffer) handleBinaryFrame(data);
467
+ else if (typeof data === "string") handleTextFrame(data);
468
+ };
469
+ socket.onclose = (event) => {
470
+ if (!opened) {
471
+ settle();
472
+ return;
473
+ }
474
+ settle();
475
+ };
476
+ socket.onerror = () => {
477
+ if (settled) return;
478
+ if (!opened) settle();
479
+ };
480
+ function handleBinaryFrame(buffer) {
481
+ const view = new Uint8Array(buffer);
482
+ if (view.length <= 1) return;
483
+ if (view[0] === STDOUT_FRAME) {
484
+ const chunk = stdoutDecoder.decode(view.subarray(1), { stream: true });
485
+ stdout += chunk;
486
+ options.onStdout?.(chunk);
487
+ } else if (view[0] === STDERR_FRAME) {
488
+ const chunk = stderrDecoder.decode(view.subarray(1), { stream: true });
489
+ stderr += chunk;
490
+ options.onStderr?.(chunk);
491
+ }
492
+ }
493
+ function handleTextFrame(text) {
494
+ let frame;
495
+ try {
496
+ frame = JSON.parse(text);
497
+ } catch {
498
+ return;
499
+ }
500
+ if (frame.type === "exit") {
501
+ exitCode = frame.data?.exit_code ?? 0;
502
+ settle();
503
+ }
504
+ }
505
+ });
506
+ }
507
+ //#endregion
508
+ //#region src/sandbox.ts
509
+ /**
510
+ * How long before a lease's stated `expiresAt` we should treat it as
511
+ * expired. Avoids a race where the JWT is valid at cache-hit time but the
512
+ * server rejects it by the time the WebSocket handshake completes.
513
+ */
514
+ const LEASE_REFRESH_MARGIN_MS = 6e4;
515
+ /** Max attempts for `POST /sandbox` when the proxy returns transient 5xx errors. */
516
+ const CREATE_MAX_ATTEMPTS = 3;
517
+ /** Base delay between create retries; multiplied by the attempt number. */
518
+ const CREATE_RETRY_BASE_DELAY_MS = 2e3;
519
+ /**
520
+ * Compose a shell command line from a `command` string and optional `args`.
521
+ *
522
+ * IMPORTANT: `command` is treated as a **shell string** and passed to the
523
+ * remote shell verbatim so callers can use pipes, redirects, and chaining
524
+ * (`ls -la | grep foo`). This matches the contract of {@link MastraSandbox}
525
+ * and the local sandbox implementation. `args` are always shell-quoted so
526
+ * they cannot inject syntax.
527
+ *
528
+ * Callers MUST NOT pass untrusted input as `command`. Untrusted values must
529
+ * be passed via `args`, where they are safely quoted. Passing untrusted
530
+ * input as `command` allows arbitrary shell syntax execution on the remote
531
+ * sandbox.
532
+ */
347
533
  function buildCommand(command, args) {
348
- return args?.length ? `${command} ${args.map(shellQuote).join(" ")}` : command;
534
+ return args?.length ? `${command} ${args.map(shellQuote).join(" ")}` : command;
349
535
  }
350
536
  function shellQuote(arg) {
351
- if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
352
- return `'${arg.replace(/'/g, `'\\''`)}'`;
537
+ if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
538
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
353
539
  }
354
- var PlatformProcessHandle = class extends workspace.ProcessHandle {
355
- pid;
356
- resultPromise;
357
- exitCodeValue;
358
- constructor(pid, resultPromise, options) {
359
- super(options);
360
- this.pid = pid;
361
- this.resultPromise = resultPromise.then((result) => {
362
- this.exitCodeValue = result.exitCode;
363
- if (result.stdout) this.emitStdout(result.stdout);
364
- if (result.stderr) this.emitStderr(result.stderr);
365
- return result;
366
- });
367
- }
368
- get exitCode() {
369
- return this.exitCodeValue;
370
- }
371
- async wait() {
372
- return this.resultPromise;
373
- }
374
- async kill() {
375
- throw new Error("Platform sandbox command execution does not support killing individual processes");
376
- }
377
- async sendStdin() {
378
- throw new Error("Platform sandbox command execution does not support stdin");
379
- }
540
+ var PlatformProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
541
+ pid;
542
+ resultPromise;
543
+ exitCodeValue;
544
+ constructor(pid, resultPromise, options) {
545
+ super(options);
546
+ this.pid = pid;
547
+ this.resultPromise = resultPromise.then((result) => {
548
+ this.exitCodeValue = result.exitCode;
549
+ if (result.stdout) this.emitStdout(result.stdout);
550
+ if (result.stderr) this.emitStderr(result.stderr);
551
+ return result;
552
+ });
553
+ }
554
+ get exitCode() {
555
+ return this.exitCodeValue;
556
+ }
557
+ async wait() {
558
+ return this.resultPromise;
559
+ }
560
+ async kill() {
561
+ throw new Error("Platform sandbox command execution does not support killing individual processes");
562
+ }
563
+ async sendStdin() {
564
+ throw new Error("Platform sandbox command execution does not support stdin");
565
+ }
380
566
  };
381
- var PlatformProcessManager = class extends workspace.SandboxProcessManager {
382
- spawnCounter = 0;
383
- /**
384
- * Spawn a process on the remote sandbox.
385
- *
386
- * `command` is interpreted as a shell string by the remote shell, matching
387
- * the {@link MastraSandbox} contract. See {@link PlatformSandbox.executeCommand}
388
- * for the untrusted-input caveat: never pass untrusted values as `command`.
389
- */
390
- async spawn(command, options = {}) {
391
- const pid = `platform-proc-${Date.now().toString(36)}-${(this.spawnCounter++).toString(36)}`;
392
- const resultPromise = this.sandbox.executeCommand(command, void 0, options);
393
- const handle = new PlatformProcessHandle(pid, resultPromise, options);
394
- this._tracked.set(handle.pid, handle);
395
- return handle;
396
- }
397
- async list() {
398
- return Array.from(this._tracked.values()).map((handle) => ({
399
- pid: handle.pid,
400
- command: handle.command,
401
- running: handle.exitCode === void 0,
402
- ...handle.exitCode !== void 0 && { exitCode: handle.exitCode }
403
- }));
404
- }
567
+ var PlatformProcessManager = class extends _mastra_core_workspace.SandboxProcessManager {
568
+ spawnCounter = 0;
569
+ /**
570
+ * Spawn a process on the remote sandbox.
571
+ *
572
+ * `command` is interpreted as a shell string by the remote shell, matching
573
+ * the {@link MastraSandbox} contract. See {@link PlatformSandbox.executeCommand}
574
+ * for the untrusted-input caveat: never pass untrusted values as `command`.
575
+ */
576
+ async spawn(command, options = {}) {
577
+ const handle = new PlatformProcessHandle(`platform-proc-${Date.now().toString(36)}-${(this.spawnCounter++).toString(36)}`, this.sandbox.executeCommand(command, void 0, options), options);
578
+ this._tracked.set(handle.pid, handle);
579
+ return handle;
580
+ }
581
+ async list() {
582
+ return Array.from(this._tracked.values()).map((handle) => ({
583
+ pid: handle.pid,
584
+ command: handle.command,
585
+ running: handle.exitCode === void 0,
586
+ ...handle.exitCode !== void 0 && { exitCode: handle.exitCode }
587
+ }));
588
+ }
405
589
  };
406
- var PlatformSandbox = class _PlatformSandbox extends workspace.MastraSandbox {
407
- id;
408
- name = "PlatformSandbox";
409
- provider = "platform";
410
- status = "pending";
411
- _client;
412
- _environmentId;
413
- _sandboxId;
414
- _idleTimeoutMinutes;
415
- _networkIsolation;
416
- _env;
417
- _timeout;
418
- _instructionsOverride;
419
- _createdAt = null;
420
- constructor(options = {}) {
421
- super({ ...options, name: "PlatformSandbox", processes: new PlatformProcessManager() });
422
- this.id = options.id ?? this.generateId();
423
- this._client = new PlatformClient(options);
424
- this._environmentId = options.environmentId ?? process.env.MASTRA_ENVIRONMENT_ID ?? "";
425
- if (!this._environmentId && !options.sandboxId) throw new Error("environmentId is required");
426
- this._sandboxId = options.sandboxId;
427
- this._idleTimeoutMinutes = options.idleTimeoutMinutes;
428
- this._networkIsolation = options.networkIsolation;
429
- this._env = options.env ?? {};
430
- this._timeout = options.timeout;
431
- this._instructionsOverride = options.instructions;
432
- }
433
- generateId() {
434
- return `platform-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
435
- }
436
- /**
437
- * Construct a sibling {@link PlatformSandbox} that inherits this sandbox's
438
- * credentials and defaults (access token, project, environment, network
439
- * isolation, timeout, instructions, env, idle timeout) with per-instance
440
- * overrides from `options`.
441
- *
442
- * Performs no I/O and does not require this sandbox to be started — the
443
- * returned sandbox is not started and provisions (or reattaches, when
444
- * `sandboxId` is set) on its own `start()`. Use it when one configured
445
- * sandbox acts as the template for a fleet of independent sandboxes
446
- * (e.g. one per project).
447
- */
448
- clone(options = {}) {
449
- return new _PlatformSandbox({
450
- ...options.id !== void 0 && { id: options.id },
451
- accessToken: this._client.accessToken,
452
- projectId: this._client.projectId,
453
- fetch: this._client.fetch,
454
- environmentId: this._environmentId,
455
- ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
456
- idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
457
- ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
458
- env: options.env ?? this._env,
459
- ...this._timeout !== void 0 && { timeout: this._timeout },
460
- ...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride }
461
- });
462
- }
463
- async start() {
464
- if (this._sandboxId) {
465
- try {
466
- const response2 = await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
467
- const json2 = await response2.json();
468
- this._createdAt = json2.createdAt ? new Date(json2.createdAt) : /* @__PURE__ */ new Date();
469
- return;
470
- } catch (error) {
471
- if (!(error instanceof PlatformApiError) || error.status !== 404) throw error;
472
- this._sandboxId = void 0;
473
- }
474
- }
475
- if (!this._environmentId) throw new Error("environmentId is required");
476
- const response = await this._client.request("/sandbox", {
477
- method: "POST",
478
- headers: { "content-type": "application/json" },
479
- body: JSON.stringify({
480
- // Sent so the platform can associate the provisioned resource with a
481
- // caller-stable identifier (used for opt-in checkpoint recovery). The
482
- // platform treats it as an advisory key: unknown values fall through
483
- // to a fresh sandbox, matching pre-existing behavior.
484
- id: this.id,
485
- environmentId: this._environmentId,
486
- idleTimeoutMinutes: this._idleTimeoutMinutes,
487
- networkIsolation: this._networkIsolation,
488
- env: this._env
489
- })
490
- });
491
- const json = await response.json();
492
- this._sandboxId = json.id;
493
- this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
494
- }
495
- async stop() {
496
- await this.destroy();
497
- }
498
- async destroy() {
499
- if (!this._sandboxId) return;
500
- await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`, { method: "DELETE" });
501
- this._sandboxId = void 0;
502
- this._createdAt = null;
503
- }
504
- /**
505
- * Execute a command on the remote sandbox.
506
- *
507
- * `command` is a **shell string**: it is concatenated verbatim into the
508
- * command line sent to the remote shell, which lets callers use pipes,
509
- * redirects, and chaining (`ls -la | grep foo`). This matches the contract
510
- * of {@link MastraSandbox} and the local sandbox implementation.
511
- *
512
- * `args`, when provided, are always shell-quoted so they cannot inject
513
- * additional shell syntax.
514
- *
515
- * Security: callers MUST NOT pass untrusted input as `command`. If any part
516
- * of the invocation is derived from an untrusted source, pass it through
517
- * `args` (which is safely quoted) or shell-quote it yourself before
518
- * inclusion. Untrusted `command` values allow arbitrary shell syntax
519
- * execution on the remote sandbox.
520
- */
521
- async executeCommand(command, args, options) {
522
- await this.ensureRunning();
523
- if (!this._sandboxId) throw new workspace.SandboxNotReadyError(this.id);
524
- const started = Date.now();
525
- const fullCommand = buildCommand(command, args);
526
- const effectiveTimeout = options?.timeout ?? this._timeout;
527
- const timeoutSec = effectiveTimeout != null ? Math.ceil(effectiveTimeout / 1e3) : void 0;
528
- const clientSignal = effectiveTimeout != null && effectiveTimeout > 0 ? AbortSignal.timeout(effectiveTimeout + 3e4) : void 0;
529
- const response = await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}/exec`, {
530
- method: "POST",
531
- headers: { "content-type": "application/json" },
532
- body: JSON.stringify({
533
- command: fullCommand,
534
- timeoutSec,
535
- cwd: options?.cwd,
536
- env: options?.env
537
- }),
538
- signal: clientSignal
539
- });
540
- const json = await response.json();
541
- const exitCode = json.exitCode ?? (json.timedOut ? 124 : 1);
542
- return {
543
- success: exitCode === 0,
544
- exitCode,
545
- stdout: json.stdout,
546
- stderr: json.stderr,
547
- executionTimeMs: Date.now() - started,
548
- timedOut: json.timedOut,
549
- command: fullCommand
550
- };
551
- }
552
- async getInfo() {
553
- if (!this._sandboxId) {
554
- return {
555
- id: this.id,
556
- name: this.name,
557
- provider: this.provider,
558
- status: this.status,
559
- createdAt: this._createdAt ?? /* @__PURE__ */ new Date()
560
- };
561
- }
562
- const response = await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
563
- const json = await response.json();
564
- return {
565
- id: json.id,
566
- name: this.name,
567
- provider: this.provider,
568
- status: this.status,
569
- createdAt: json.createdAt ? new Date(json.createdAt) : this._createdAt ?? /* @__PURE__ */ new Date(),
570
- metadata: {
571
- providerResourceId: json.providerResourceId ?? void 0,
572
- platformStatus: json.status
573
- }
574
- };
575
- }
576
- getInstructions(opts) {
577
- const defaultInstructions = `Platform sandbox${this._sandboxId ? ` ${this._sandboxId}` : ""}. Execute commands with the sandbox command APIs.`;
578
- if (typeof this._instructionsOverride === "function") {
579
- return this._instructionsOverride({ defaultInstructions, requestContext: opts?.requestContext });
580
- }
581
- if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
582
- return defaultInstructions;
583
- }
590
+ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.MastraSandbox {
591
+ id;
592
+ name = "PlatformSandbox";
593
+ provider = "platform";
594
+ status = "pending";
595
+ _client;
596
+ _environmentId;
597
+ _sandboxId;
598
+ _idleTimeoutMinutes;
599
+ _networkIsolation;
600
+ _env;
601
+ _timeout;
602
+ _instructionsOverride;
603
+ _createdAt = null;
604
+ _webSocketFactory;
605
+ /**
606
+ * Cached exec lease for this sandbox. `null` before the first exec and
607
+ * after {@link destroy}. Refreshed when `expiresAt - LEASE_REFRESH_MARGIN_MS < now`
608
+ * (see {@link _ensureLease}); a lease without a disclosed `expiresAt`
609
+ * is refreshed on every call.
610
+ */
611
+ _lease = null;
612
+ /**
613
+ * In-flight mint request; concurrent `_ensureLease` callers on a cold or
614
+ * near-expiry cache all await this single promise so we don't burn N
615
+ * `POST /exec-lease` round-trips when the sandbox is doing N parallel execs.
616
+ * Cleared (regardless of success or failure) when the request settles.
617
+ */
618
+ _leaseInFlight = null;
619
+ /**
620
+ * Tri-state feature detection for the platform's exec-lease endpoint:
621
+ * undefined not yet tried (default; try direct on first exec)
622
+ * true — endpoint present, use direct exec
623
+ * false — endpoint returned 404 or 501, fall back permanently to /exec
624
+ * Sticky per instance so we make the fallback decision once per sandbox
625
+ * lifetime instead of paying an extra round-trip on every exec.
626
+ */
627
+ _directExecAvailable = void 0;
628
+ constructor(options = {}) {
629
+ super({
630
+ ...options,
631
+ name: "PlatformSandbox",
632
+ processes: new PlatformProcessManager()
633
+ });
634
+ this.id = options.id ?? this.generateId();
635
+ this._client = new PlatformClient(options);
636
+ this._environmentId = options.environmentId ?? process.env.MASTRA_ENVIRONMENT_ID ?? "";
637
+ if (!this._environmentId && !options.sandboxId) throw new Error("environmentId is required");
638
+ this._sandboxId = options.sandboxId;
639
+ this._idleTimeoutMinutes = options.idleTimeoutMinutes;
640
+ this._networkIsolation = options.networkIsolation;
641
+ this._env = options.env ?? {};
642
+ this._timeout = options.timeout;
643
+ this._instructionsOverride = options.instructions;
644
+ this._webSocketFactory = options.webSocketFactory;
645
+ }
646
+ generateId() {
647
+ return `platform-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
648
+ }
649
+ /**
650
+ * Construct a sibling {@link PlatformSandbox} that inherits this sandbox's
651
+ * credentials and defaults (access token, project, environment, network
652
+ * isolation, timeout, instructions, env, idle timeout) with per-instance
653
+ * overrides from `options`.
654
+ *
655
+ * Performs no I/O and does not require this sandbox to be started — the
656
+ * returned sandbox is not started and provisions (or reattaches, when
657
+ * `sandboxId` is set) on its own `start()`. Use it when one configured
658
+ * sandbox acts as the template for a fleet of independent sandboxes
659
+ * (e.g. one per project).
660
+ */
661
+ clone(options = {}) {
662
+ return new PlatformSandbox({
663
+ ...options.id !== void 0 && { id: options.id },
664
+ accessToken: this._client.accessToken,
665
+ projectId: this._client.projectId,
666
+ fetch: this._client.fetch,
667
+ environmentId: this._environmentId,
668
+ ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
669
+ idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
670
+ ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
671
+ env: options.env ?? this._env,
672
+ ...this._timeout !== void 0 && { timeout: this._timeout },
673
+ ...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride },
674
+ ...this._webSocketFactory !== void 0 && { webSocketFactory: this._webSocketFactory }
675
+ });
676
+ }
677
+ async start() {
678
+ if (this._sandboxId) try {
679
+ const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
680
+ if (!json.destroyedAt) {
681
+ this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
682
+ return;
683
+ }
684
+ this._sandboxId = void 0;
685
+ } catch (error) {
686
+ if (!(error instanceof PlatformApiError) || error.status !== 404) throw error;
687
+ this._sandboxId = void 0;
688
+ }
689
+ if (!this._environmentId) throw new Error("environmentId is required");
690
+ const body = JSON.stringify({
691
+ id: this.id,
692
+ environmentId: this._environmentId,
693
+ idleTimeoutMinutes: this._idleTimeoutMinutes,
694
+ networkIsolation: this._networkIsolation,
695
+ env: this._env
696
+ });
697
+ let response;
698
+ for (let attempt = 1;; attempt++) try {
699
+ response = await this._client.request("/sandbox", {
700
+ method: "POST",
701
+ headers: { "content-type": "application/json" },
702
+ body
703
+ });
704
+ break;
705
+ } catch (error) {
706
+ if (!(error instanceof PlatformApiError && error.status >= 500) || attempt >= CREATE_MAX_ATTEMPTS) throw error;
707
+ await new Promise((resolve) => setTimeout(resolve, CREATE_RETRY_BASE_DELAY_MS * attempt));
708
+ }
709
+ const json = await response.json();
710
+ this._sandboxId = json.id;
711
+ this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
712
+ }
713
+ async stop() {
714
+ await this.destroy();
715
+ }
716
+ async destroy() {
717
+ if (!this._sandboxId) return;
718
+ await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`, { method: "DELETE" });
719
+ this._sandboxId = void 0;
720
+ this._createdAt = null;
721
+ this._lease = null;
722
+ }
723
+ /**
724
+ * Execute a command on the remote sandbox.
725
+ *
726
+ * `command` is a **shell string**: it is concatenated verbatim into the
727
+ * command line sent to the remote shell, which lets callers use pipes,
728
+ * redirects, and chaining (`ls -la | grep foo`). This matches the contract
729
+ * of {@link MastraSandbox} and the local sandbox implementation.
730
+ *
731
+ * `args`, when provided, are always shell-quoted so they cannot inject
732
+ * additional shell syntax.
733
+ *
734
+ * Security: callers MUST NOT pass untrusted input as `command`. If any part
735
+ * of the invocation is derived from an untrusted source, pass it through
736
+ * `args` (which is safely quoted) or shell-quote it yourself before
737
+ * inclusion. Untrusted `command` values allow arbitrary shell syntax
738
+ * execution on the remote sandbox.
739
+ */
740
+ async executeCommand(command, args, options) {
741
+ await this.ensureRunning();
742
+ if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
743
+ const started = Date.now();
744
+ const fullCommand = buildCommand(command, args);
745
+ const effectiveTimeout = options?.timeout ?? this._timeout;
746
+ if (this._directExecAvailable !== false) {
747
+ const leaseResult = await this._tryDirectExec(fullCommand, effectiveTimeout, options);
748
+ if (leaseResult) return {
749
+ ...leaseResult,
750
+ executionTimeMs: Date.now() - started
751
+ };
752
+ }
753
+ return this._execViaProxy(fullCommand, effectiveTimeout, options, started);
754
+ }
755
+ async _tryDirectExec(fullCommand, effectiveTimeout, options) {
756
+ let lease;
757
+ try {
758
+ lease = await this._ensureLease();
759
+ } catch (error) {
760
+ if (error instanceof PlatformApiError && (error.status === 404 || error.status === 501)) {
761
+ this._directExecAvailable = false;
762
+ return null;
763
+ }
764
+ throw error;
765
+ }
766
+ this._directExecAvailable = true;
767
+ const filteredEnv = options?.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
768
+ const result = await execViaLease(lease, {
769
+ command: fullCommand,
770
+ ...options?.cwd !== void 0 && { cwd: options.cwd },
771
+ ...filteredEnv !== void 0 && { env: filteredEnv },
772
+ ...effectiveTimeout != null && effectiveTimeout > 0 && { timeoutMs: effectiveTimeout },
773
+ ...this._webSocketFactory && { webSocketFactory: this._webSocketFactory }
774
+ });
775
+ if (result.exitCode === null && !result.timedOut) {
776
+ this._lease = null;
777
+ return null;
778
+ }
779
+ const exitCode = result.exitCode ?? 124;
780
+ return {
781
+ success: exitCode === 0,
782
+ exitCode,
783
+ stdout: result.stdout,
784
+ stderr: result.stderr,
785
+ timedOut: result.timedOut,
786
+ command: fullCommand
787
+ };
788
+ }
789
+ async _execViaProxy(fullCommand, effectiveTimeout, options, started) {
790
+ if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
791
+ const timeoutSec = effectiveTimeout != null ? Math.ceil(effectiveTimeout / 1e3) : void 0;
792
+ const clientSignal = effectiveTimeout != null && effectiveTimeout > 0 ? AbortSignal.timeout(effectiveTimeout + 3e4) : void 0;
793
+ const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}/exec`, {
794
+ method: "POST",
795
+ headers: { "content-type": "application/json" },
796
+ body: JSON.stringify({
797
+ command: fullCommand,
798
+ timeoutSec,
799
+ cwd: options?.cwd,
800
+ env: options?.env
801
+ }),
802
+ signal: clientSignal
803
+ })).json();
804
+ const exitCode = json.exitCode ?? (json.timedOut ? 124 : 1);
805
+ return {
806
+ success: exitCode === 0,
807
+ exitCode,
808
+ stdout: json.stdout,
809
+ stderr: json.stderr,
810
+ executionTimeMs: Date.now() - started,
811
+ timedOut: json.timedOut,
812
+ command: fullCommand
813
+ };
814
+ }
815
+ /**
816
+ * Return a cached exec lease, minting a fresh one when the cache is empty
817
+ * or the JWT is within {@link LEASE_REFRESH_MARGIN_MS} of `expiresAt`.
818
+ *
819
+ * Callers are expected to be on the "sandbox is running" path; we don't
820
+ * re-check `_sandboxId` here because `executeCommand` already gated on it.
821
+ */
822
+ async _ensureLease() {
823
+ const now = Date.now();
824
+ if (this._lease && this._lease.expiresAtMs !== null && this._lease.expiresAtMs - LEASE_REFRESH_MARGIN_MS > now) return this._lease;
825
+ if (this._leaseInFlight) return this._leaseInFlight;
826
+ if (!this._sandboxId) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
827
+ const sandboxId = this._sandboxId;
828
+ const inFlight = (async () => {
829
+ const json = await (await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/exec-lease`, { method: "POST" })).json();
830
+ const expiresAtMs = json.expiresAt ? Date.parse(json.expiresAt) : null;
831
+ const lease = {
832
+ jwt: json.jwt,
833
+ wsEndpoint: json.wsEndpoint,
834
+ subprotocol: json.subprotocol,
835
+ expiresAt: json.expiresAt,
836
+ expiresAtMs: expiresAtMs !== null && !Number.isNaN(expiresAtMs) ? expiresAtMs : null
837
+ };
838
+ this._lease = lease;
839
+ return lease;
840
+ })();
841
+ this._leaseInFlight = inFlight;
842
+ try {
843
+ return await inFlight;
844
+ } finally {
845
+ if (this._leaseInFlight === inFlight) this._leaseInFlight = null;
846
+ }
847
+ }
848
+ async getInfo() {
849
+ if (!this._sandboxId) return {
850
+ id: this.id,
851
+ name: this.name,
852
+ provider: this.provider,
853
+ status: this.status,
854
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date()
855
+ };
856
+ const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
857
+ return {
858
+ id: json.id,
859
+ name: this.name,
860
+ provider: this.provider,
861
+ status: this.status,
862
+ createdAt: json.createdAt ? new Date(json.createdAt) : this._createdAt ?? /* @__PURE__ */ new Date(),
863
+ metadata: {
864
+ sandboxId: json.id,
865
+ providerResourceId: json.providerResourceId ?? void 0,
866
+ platformStatus: json.status
867
+ }
868
+ };
869
+ }
870
+ getInstructions(opts) {
871
+ const defaultInstructions = `Platform sandbox${this._sandboxId ? ` ${this._sandboxId}` : ""}. Execute commands with the sandbox command APIs.`;
872
+ if (typeof this._instructionsOverride === "function") return this._instructionsOverride({
873
+ defaultInstructions,
874
+ requestContext: opts?.requestContext
875
+ });
876
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
877
+ return defaultInstructions;
878
+ }
584
879
  };
585
-
586
- // src/provider.ts
587
- var platformSandboxProvider = {
588
- id: "platform",
589
- name: "Mastra Platform Sandbox",
590
- description: "Environment-scoped sandbox execution through Mastra Platform workspace proxy",
591
- configSchema: {
592
- type: "object",
593
- properties: {
594
- accessToken: {
595
- type: "string",
596
- description: "Mastra Platform secret key (falls back to MASTRA_PLATFORM_SECRET_KEY)"
597
- },
598
- projectId: { type: "string", description: "Platform project ID (falls back to MASTRA_PROJECT_ID)" },
599
- environmentId: { type: "string", description: "Platform environment ID (falls back to MASTRA_ENVIRONMENT_ID)" },
600
- sandboxId: { type: "string", description: "Reattach to an existing Platform sandbox by ID" },
601
- idleTimeoutMinutes: { type: "number", description: "Minutes before the sandbox can be destroyed while idle" },
602
- networkIsolation: {
603
- type: "string",
604
- description: "Network isolation mode",
605
- enum: ["ISOLATED", "PRIVATE"],
606
- default: "ISOLATED"
607
- },
608
- env: { type: "object", description: "Environment variables", additionalProperties: { type: "string" } },
609
- timeout: { type: "number", description: "Default command timeout in ms" }
610
- }
611
- },
612
- createSandbox: (config) => new PlatformSandbox(config)
880
+ //#endregion
881
+ //#region src/provider.ts
882
+ const platformSandboxProvider = {
883
+ id: "platform",
884
+ name: "Mastra Platform Sandbox",
885
+ description: "Environment-scoped sandbox execution through Mastra Platform workspace proxy",
886
+ configSchema: {
887
+ type: "object",
888
+ properties: {
889
+ accessToken: {
890
+ type: "string",
891
+ description: "Mastra Platform secret key (falls back to MASTRA_PLATFORM_SECRET_KEY)"
892
+ },
893
+ projectId: {
894
+ type: "string",
895
+ description: "Platform project ID (falls back to MASTRA_PROJECT_ID)"
896
+ },
897
+ environmentId: {
898
+ type: "string",
899
+ description: "Platform environment ID (falls back to MASTRA_ENVIRONMENT_ID)"
900
+ },
901
+ sandboxId: {
902
+ type: "string",
903
+ description: "Reattach to an existing Platform sandbox by ID"
904
+ },
905
+ idleTimeoutMinutes: {
906
+ type: "number",
907
+ description: "Minutes before the sandbox can be destroyed while idle"
908
+ },
909
+ networkIsolation: {
910
+ type: "string",
911
+ description: "Network isolation mode",
912
+ enum: ["ISOLATED", "PRIVATE"],
913
+ default: "ISOLATED"
914
+ },
915
+ env: {
916
+ type: "object",
917
+ description: "Environment variables",
918
+ additionalProperties: { type: "string" }
919
+ },
920
+ timeout: {
921
+ type: "number",
922
+ description: "Default command timeout in ms"
923
+ }
924
+ }
925
+ },
926
+ createSandbox: (config) => new PlatformSandbox(config)
613
927
  };
614
- var platformFilesystemProvider = {
615
- id: "platform",
616
- name: "Mastra Platform Filesystem",
617
- description: "Bucket-backed filesystem access through Mastra Platform workspace proxy",
618
- configSchema: {
619
- type: "object",
620
- properties: {
621
- accessToken: {
622
- type: "string",
623
- description: "Mastra Platform secret key (falls back to MASTRA_PLATFORM_SECRET_KEY)"
624
- },
625
- projectId: { type: "string", description: "Platform project ID (falls back to MASTRA_PROJECT_ID)" },
626
- bucketName: {
627
- type: "string",
628
- description: "Platform workspace bucket name (falls back to MASTRA_PLATFORM_BUCKET_NAME)"
629
- },
630
- readOnly: { type: "boolean", description: "Mount as read-only", default: false }
631
- }
632
- },
633
- createFilesystem: (config) => new PlatformFilesystem(config)
928
+ const platformFilesystemProvider = {
929
+ id: "platform",
930
+ name: "Mastra Platform Filesystem",
931
+ description: "Bucket-backed filesystem access through Mastra Platform workspace proxy",
932
+ configSchema: {
933
+ type: "object",
934
+ properties: {
935
+ accessToken: {
936
+ type: "string",
937
+ description: "Mastra Platform secret key (falls back to MASTRA_PLATFORM_SECRET_KEY)"
938
+ },
939
+ projectId: {
940
+ type: "string",
941
+ description: "Platform project ID (falls back to MASTRA_PROJECT_ID)"
942
+ },
943
+ bucketName: {
944
+ type: "string",
945
+ description: "Platform workspace bucket name (falls back to MASTRA_PLATFORM_BUCKET_NAME)"
946
+ },
947
+ readOnly: {
948
+ type: "boolean",
949
+ description: "Mount as read-only",
950
+ default: false
951
+ }
952
+ }
953
+ },
954
+ createFilesystem: (config) => new PlatformFilesystem(config)
634
955
  };
635
-
956
+ //#endregion
636
957
  exports.PlatformApiError = PlatformApiError;
637
958
  exports.PlatformClient = PlatformClient;
638
959
  exports.PlatformFilesystem = PlatformFilesystem;
639
960
  exports.PlatformSandbox = PlatformSandbox;
640
961
  exports.platformFilesystemProvider = platformFilesystemProvider;
641
962
  exports.platformSandboxProvider = platformSandboxProvider;
642
- //# sourceMappingURL=index.cjs.map
963
+
643
964
  //# sourceMappingURL=index.cjs.map