@mastra/modal 0.4.0 → 0.5.0

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,419 +1,411 @@
1
- 'use strict';
2
-
3
- var workspace = require('@mastra/core/workspace');
4
- var modal = require('modal');
5
-
6
- // src/sandbox/index.ts
7
- var ModalProcessHandle = class extends workspace.ProcessHandle {
8
- pid;
9
- _proc;
10
- _startTime;
11
- _timeout;
12
- _exitCode;
13
- _waitPromise = null;
14
- _streamingPromise = null;
15
- _killed = false;
16
- _stdoutReader = null;
17
- _stderrReader = null;
18
- constructor(pid, proc, startTime, options) {
19
- super(options);
20
- this.pid = pid;
21
- this._proc = proc;
22
- this._startTime = startTime;
23
- this._timeout = options?.timeout;
24
- }
25
- get exitCode() {
26
- return this._exitCode;
27
- }
28
- /** @internal Set by the process manager after streaming starts. */
29
- set streamingPromise(p) {
30
- this._streamingPromise = p;
31
- p.then(() => this._resolveExitCode()).catch(() => this._resolveExitCode());
32
- }
33
- /** @internal Set by the process manager so kill() can cancel the readers. */
34
- setReaders(stdoutReader, stderrReader) {
35
- this._stdoutReader = stdoutReader;
36
- this._stderrReader = stderrReader;
37
- }
38
- /** Fetch the exit code from the Modal process. No-op if already set. */
39
- async _resolveExitCode() {
40
- if (this._exitCode !== void 0) return;
41
- try {
42
- this._exitCode = await this._proc.wait();
43
- } catch {
44
- if (this._exitCode === void 0) {
45
- this._exitCode = 1;
46
- }
47
- }
48
- }
49
- async wait() {
50
- if (!this._waitPromise) {
51
- this._waitPromise = this._doWait();
52
- }
53
- return this._waitPromise;
54
- }
55
- async _doWait() {
56
- const streamDone = this._streamingPromise ?? Promise.resolve();
57
- if (this._timeout) {
58
- let timeoutId;
59
- const timeoutPromise = new Promise((_, reject) => {
60
- timeoutId = setTimeout(() => reject(new Error(`Command timed out after ${this._timeout}ms`)), this._timeout);
61
- });
62
- try {
63
- await Promise.race([streamDone, timeoutPromise]);
64
- } catch (error) {
65
- if (error instanceof Error && error.message.includes("timed out")) {
66
- await this.kill();
67
- this._exitCode = 124;
68
- return {
69
- success: false,
70
- exitCode: 124,
71
- stdout: this.stdout,
72
- stderr: this.stderr || error.message,
73
- executionTimeMs: Date.now() - this._startTime,
74
- killed: true,
75
- timedOut: true
76
- };
77
- }
78
- throw error;
79
- } finally {
80
- clearTimeout(timeoutId);
81
- }
82
- } else {
83
- await streamDone.catch(() => {
84
- });
85
- }
86
- if (this._killed) {
87
- return {
88
- success: false,
89
- exitCode: this._exitCode ?? 137,
90
- stdout: this.stdout,
91
- stderr: this.stderr,
92
- executionTimeMs: Date.now() - this._startTime,
93
- killed: true,
94
- timedOut: false
95
- };
96
- }
97
- await this._resolveExitCode();
98
- return {
99
- success: this._exitCode === 0,
100
- exitCode: this._exitCode ?? 1,
101
- stdout: this.stdout,
102
- stderr: this.stderr,
103
- executionTimeMs: Date.now() - this._startTime
104
- };
105
- }
106
- async kill() {
107
- if (this._exitCode !== void 0) return false;
108
- this._killed = true;
109
- this._exitCode = 137;
110
- try {
111
- await this._stdoutReader?.cancel();
112
- } catch {
113
- }
114
- try {
115
- await this._stderrReader?.cancel();
116
- } catch {
117
- }
118
- return true;
119
- }
120
- async sendStdin(_data) {
121
- throw new Error("Modal JS SDK does not expose stdin on exec() \u2014 sendStdin() is not supported");
122
- }
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _mastra_core_workspace = require("@mastra/core/workspace");
3
+ let modal = require("modal");
4
+ //#region src/sandbox/process-manager.ts
5
+ /**
6
+ * Modal process manager. Each spawn() creates a ContainerProcess via exec().
7
+ * kill() has no SDK equivalent — it cancels stream readers locally; the remote
8
+ * process runs until the sandbox timeout.
9
+ */
10
+ /**
11
+ * Wraps a Modal ContainerProcess to conform to Mastra's ProcessHandle.
12
+ */
13
+ var ModalProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
14
+ pid;
15
+ _proc;
16
+ _startTime;
17
+ _timeout;
18
+ _exitCode;
19
+ _waitPromise = null;
20
+ _streamingPromise = null;
21
+ _killed = false;
22
+ _stdoutReader = null;
23
+ _stderrReader = null;
24
+ constructor(pid, proc, startTime, options) {
25
+ super(options);
26
+ this.pid = pid;
27
+ this._proc = proc;
28
+ this._startTime = startTime;
29
+ this._timeout = options?.timeout;
30
+ }
31
+ get exitCode() {
32
+ return this._exitCode;
33
+ }
34
+ /** @internal Set by the process manager after streaming starts. */
35
+ set streamingPromise(p) {
36
+ this._streamingPromise = p;
37
+ p.then(() => this._resolveExitCode()).catch(() => this._resolveExitCode());
38
+ }
39
+ /** @internal Set by the process manager so kill() can cancel the readers. */
40
+ setReaders(stdoutReader, stderrReader) {
41
+ this._stdoutReader = stdoutReader;
42
+ this._stderrReader = stderrReader;
43
+ }
44
+ /** Fetch the exit code from the Modal process. No-op if already set. */
45
+ async _resolveExitCode() {
46
+ if (this._exitCode !== void 0) return;
47
+ try {
48
+ this._exitCode = await this._proc.wait();
49
+ } catch {
50
+ if (this._exitCode === void 0) this._exitCode = 1;
51
+ }
52
+ }
53
+ async wait() {
54
+ if (!this._waitPromise) this._waitPromise = this._doWait();
55
+ return this._waitPromise;
56
+ }
57
+ async _doWait() {
58
+ const streamDone = this._streamingPromise ?? Promise.resolve();
59
+ if (this._timeout) {
60
+ let timeoutId;
61
+ const timeoutPromise = new Promise((_, reject) => {
62
+ timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Command timed out after ${this._timeout}ms`)), this._timeout);
63
+ });
64
+ try {
65
+ await Promise.race([streamDone, timeoutPromise]);
66
+ } catch (error) {
67
+ if (error instanceof Error && error.message.includes("timed out")) {
68
+ await this.kill();
69
+ this._exitCode = 124;
70
+ return {
71
+ success: false,
72
+ exitCode: 124,
73
+ stdout: this.stdout,
74
+ stderr: this.stderr || error.message,
75
+ executionTimeMs: Date.now() - this._startTime,
76
+ killed: true,
77
+ timedOut: true
78
+ };
79
+ }
80
+ throw error;
81
+ } finally {
82
+ clearTimeout(timeoutId);
83
+ }
84
+ } else await streamDone.catch(() => {});
85
+ if (this._killed) return {
86
+ success: false,
87
+ exitCode: this._exitCode ?? 137,
88
+ stdout: this.stdout,
89
+ stderr: this.stderr,
90
+ executionTimeMs: Date.now() - this._startTime,
91
+ killed: true,
92
+ timedOut: false
93
+ };
94
+ await this._resolveExitCode();
95
+ return {
96
+ success: this._exitCode === 0,
97
+ exitCode: this._exitCode ?? 1,
98
+ stdout: this.stdout,
99
+ stderr: this.stderr,
100
+ executionTimeMs: Date.now() - this._startTime
101
+ };
102
+ }
103
+ async kill() {
104
+ if (this._exitCode !== void 0) return false;
105
+ this._killed = true;
106
+ this._exitCode = 137;
107
+ try {
108
+ await this._stdoutReader?.cancel();
109
+ } catch {}
110
+ try {
111
+ await this._stderrReader?.cancel();
112
+ } catch {}
113
+ return true;
114
+ }
115
+ async sendStdin(_data) {
116
+ throw new Error("Modal JS SDK does not expose stdin on exec() — sendStdin() is not supported");
117
+ }
118
+ async closeStdin() {
119
+ throw new _mastra_core_workspace.UnsupportedStdinCloseError("Modal JS SDK does not expose stdin on exec() — closeStdin() is not supported");
120
+ }
123
121
  };
124
- var ModalProcessManager = class extends workspace.SandboxProcessManager {
125
- _spawnCounter = 0;
126
- constructor(opts = {}) {
127
- super({ env: opts.env });
128
- }
129
- async spawn(command, options = {}) {
130
- return this.sandbox.retryOnDead(async () => {
131
- const sb = this.sandbox.modal;
132
- const mergedEnv = { ...this.env, ...options.env };
133
- const env = Object.fromEntries(
134
- Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
135
- );
136
- const argv = ["sh", "-c", command];
137
- const proc = await sb.exec(argv, {
138
- env: Object.keys(env).length > 0 ? env : void 0,
139
- workdir: options.cwd,
140
- timeoutMs: options.timeout
141
- });
142
- const pid = `modal-proc-${Date.now().toString(36)}-${++this._spawnCounter}`;
143
- const handle = new ModalProcessHandle(pid, proc, Date.now(), options);
144
- const stdoutReader = proc.stdout.getReader();
145
- const stderrReader = proc.stderr.getReader();
146
- handle.setReaders(stdoutReader, stderrReader);
147
- const streamingPromise = Promise.all([
148
- drainReader(stdoutReader, (chunk) => handle.emitStdout(chunk)),
149
- drainReader(stderrReader, (chunk) => handle.emitStderr(chunk))
150
- ]).then(() => {
151
- });
152
- handle.streamingPromise = streamingPromise;
153
- this._tracked.set(pid, handle);
154
- return handle;
155
- });
156
- }
157
- async list() {
158
- const result = [];
159
- for (const [pid, handle] of this._tracked) {
160
- result.push({
161
- pid,
162
- command: handle.command,
163
- running: handle.exitCode === void 0,
164
- exitCode: handle.exitCode
165
- });
166
- }
167
- return result;
168
- }
122
+ /**
123
+ * Modal implementation of SandboxProcessManager.
124
+ * Uses the Modal SDK's exec() API with one ContainerProcess per spawn.
125
+ */
126
+ var ModalProcessManager = class extends _mastra_core_workspace.SandboxProcessManager {
127
+ _spawnCounter = 0;
128
+ constructor(opts = {}) {
129
+ super({ env: opts.env });
130
+ }
131
+ async spawn(command, options = {}) {
132
+ return this.sandbox.retryOnDead(async () => {
133
+ const sb = this.sandbox.modal;
134
+ const mergedEnv = {
135
+ ...this.env,
136
+ ...options.env
137
+ };
138
+ const env = Object.fromEntries(Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0));
139
+ const argv = [
140
+ "sh",
141
+ "-c",
142
+ command
143
+ ];
144
+ const proc = await sb.exec(argv, {
145
+ env: Object.keys(env).length > 0 ? env : void 0,
146
+ workdir: options.cwd,
147
+ timeoutMs: options.timeout
148
+ });
149
+ const pid = `modal-proc-${Date.now().toString(36)}-${++this._spawnCounter}`;
150
+ const handle = new ModalProcessHandle(pid, proc, Date.now(), options);
151
+ const stdoutReader = proc.stdout.getReader();
152
+ const stderrReader = proc.stderr.getReader();
153
+ handle.setReaders(stdoutReader, stderrReader);
154
+ handle.streamingPromise = Promise.all([drainReader(stdoutReader, (chunk) => handle.emitStdout(chunk)), drainReader(stderrReader, (chunk) => handle.emitStderr(chunk))]).then(() => {});
155
+ this._tracked.set(pid, handle);
156
+ return handle;
157
+ });
158
+ }
159
+ async list() {
160
+ const result = [];
161
+ for (const [pid, handle] of this._tracked) result.push({
162
+ pid,
163
+ command: handle.command,
164
+ running: handle.exitCode === void 0,
165
+ exitCode: handle.exitCode
166
+ });
167
+ return result;
168
+ }
169
169
  };
170
+ /** Reads chunks from a stream until done or cancelled. */
170
171
  async function drainReader(reader, emit) {
171
- try {
172
- while (true) {
173
- const { done, value } = await reader.read();
174
- if (done) break;
175
- emit(value);
176
- }
177
- } catch {
178
- }
172
+ try {
173
+ while (true) {
174
+ const { done, value } = await reader.read();
175
+ if (done) break;
176
+ emit(value);
177
+ }
178
+ } catch {}
179
179
  }
180
-
181
- // src/sandbox/index.ts
182
- var LOG_PREFIX = "[ModalSandbox]";
183
- var ModalSandbox = class _ModalSandbox extends workspace.MastraSandbox {
184
- id;
185
- name = "ModalSandbox";
186
- provider = "modal";
187
- status = "pending";
188
- _sb = null;
189
- _imageSnapshot = null;
190
- // for stop-and-resume
191
- _client = null;
192
- _createdAt = null;
193
- _isRetrying = false;
194
- appName;
195
- baseImage;
196
- timeoutMs;
197
- env;
198
- workdir;
199
- tokenId;
200
- tokenSecret;
201
- _instructionsOverride;
202
- _constructorOptions;
203
- constructor(options = {}) {
204
- super({
205
- ...options,
206
- name: "ModalSandbox",
207
- processes: new ModalProcessManager({ env: options.env ?? {} })
208
- });
209
- this.id = options.id ?? this._generateId();
210
- this.appName = options.appName ?? "mastra";
211
- this.baseImage = options.baseImage ?? "ubuntu:22.04";
212
- this.timeoutMs = options.timeoutMs ?? 3e5;
213
- this.env = options.env ?? {};
214
- this.workdir = options.workdir;
215
- this.tokenId = options.tokenId;
216
- this.tokenSecret = options.tokenSecret;
217
- this._instructionsOverride = options.instructions;
218
- this._constructorOptions = { ...options };
219
- }
220
- /**
221
- * Construct a sibling `ModalSandbox` that inherits this sandbox's
222
- * configuration (credentials, app, base image, workdir, instructions) with
223
- * per-instance overrides.
224
- *
225
- * Performs no I/O — the sandbox clone provisions (or reconnects to a
226
- * running Modal sandbox with the same logical `id`) on its own `start()`.
227
- * Use it when one configured sandbox acts as the template for a fleet of
228
- * independent sandboxes (e.g. one per project).
229
- *
230
- * `options.idleTimeoutMinutes` maps to Modal's `timeoutMs` (wall-clock
231
- * lifetime); `options.sandboxId` is ignored because Modal reconnects by
232
- * logical `id`.
233
- */
234
- clone(options = {}) {
235
- const { id: _id, ...base } = this._constructorOptions;
236
- return new _ModalSandbox({
237
- ...base,
238
- ...options.id !== void 0 && { id: options.id },
239
- ...options.env !== void 0 && { env: options.env },
240
- ...options.idleTimeoutMinutes !== void 0 && { timeoutMs: options.idleTimeoutMinutes * 6e4 }
241
- });
242
- }
243
- /**
244
- * Get the underlying Modal Sandbox instance for direct SDK access.
245
- *
246
- * @throws {SandboxNotReadyError} If the sandbox has not been started.
247
- */
248
- get modal() {
249
- if (!this._sb) {
250
- throw new workspace.SandboxNotReadyError(this.id);
251
- }
252
- return this._sb;
253
- }
254
- // ---------------------------------------------------------------------------
255
- // Lifecycle
256
- // ---------------------------------------------------------------------------
257
- /** Reconnects to a running sandbox with this id if one exists, otherwise creates a new one. */
258
- async start() {
259
- if (this._sb) {
260
- return;
261
- }
262
- const client = this._getClient();
263
- try {
264
- this._sb = await client.sandboxes.fromName(this.appName, this.id);
265
- this._createdAt = /* @__PURE__ */ new Date();
266
- this.logger.debug(`${LOG_PREFIX} Reconnected to running sandbox: ${this.id}`);
267
- return;
268
- } catch (error) {
269
- if (!(error instanceof modal.NotFoundError)) {
270
- throw error;
271
- }
272
- }
273
- const app = await client.apps.fromName(this.appName, { createIfMissing: true });
274
- if (this._imageSnapshot) {
275
- this.logger.debug(`${LOG_PREFIX} Rebooting from snapshot: ${this.id}`);
276
- this._sb = await client.sandboxes.create(app, this._imageSnapshot, {
277
- name: this.id,
278
- timeoutMs: this.timeoutMs,
279
- env: Object.keys(this.env).length > 0 ? this.env : void 0,
280
- workdir: this.workdir
281
- });
282
- this._createdAt = /* @__PURE__ */ new Date();
283
- this.logger.debug(`${LOG_PREFIX} Created new sandbox from snapshot: ${this._sb?.sandboxId}`);
284
- return;
285
- }
286
- const image = client.images.fromRegistry(this.baseImage);
287
- this.logger.debug(`${LOG_PREFIX} Creating sandbox: ${this.id} (baseImage: ${this.baseImage})`);
288
- this._sb = await client.sandboxes.create(app, image, {
289
- name: this.id,
290
- timeoutMs: this.timeoutMs,
291
- env: Object.keys(this.env).length > 0 ? this.env : void 0,
292
- workdir: this.workdir
293
- });
294
- this._createdAt = /* @__PURE__ */ new Date();
295
- this.logger.debug(`${LOG_PREFIX} Created sandbox: ${this._sb.sandboxId}`);
296
- }
297
- /**
298
- * Snapshot the sandbox filesystem before terminating it.
299
- * Future starts will create net-new sandboxes from the snapshot.
300
- */
301
- async stop() {
302
- if (!this._sb) return;
303
- try {
304
- const procs = await this.processes.list();
305
- await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
306
- } catch {
307
- }
308
- try {
309
- this._imageSnapshot = await this._sb.snapshotFilesystem();
310
- this.logger.debug(`${LOG_PREFIX} Snapshot created: ${this._imageSnapshot.imageId}`);
311
- } catch (error) {
312
- this.logger.debug(`${LOG_PREFIX} Snapshot failed, terminating without snapshot:`, error);
313
- }
314
- try {
315
- await this._sb.terminate({ wait: true });
316
- this.logger.debug(`${LOG_PREFIX} Sandbox terminated: ${this._sb.sandboxId}`);
317
- this._sb = null;
318
- } catch (error) {
319
- if (this.isSandboxDeadError(error)) {
320
- this._sb = null;
321
- } else {
322
- throw error;
323
- }
324
- }
325
- }
326
- /** Terminates the sandbox, ending its lifetime. Unlike stop(), no snapshot is preserved. */
327
- async destroy() {
328
- if (this._sb) {
329
- try {
330
- const procs = await this.processes.list();
331
- await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
332
- } catch {
333
- }
334
- try {
335
- await this._sb.terminate();
336
- this.logger.debug(`${LOG_PREFIX} Sandbox terminated: ${this._sb.sandboxId}`);
337
- } catch {
338
- }
339
- this._sb = null;
340
- }
341
- this._imageSnapshot = null;
342
- }
343
- async getInfo() {
344
- return {
345
- id: this.id,
346
- name: this.name,
347
- provider: this.provider,
348
- status: this.status,
349
- createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
350
- metadata: {
351
- appName: this.appName,
352
- image: this._imageSnapshot?.imageId ?? this.baseImage,
353
- timeoutMs: this.timeoutMs
354
- }
355
- };
356
- }
357
- getInstructions() {
358
- const defaultInstructions = this._getDefaultInstructions();
359
- if (this._instructionsOverride === void 0) return defaultInstructions;
360
- if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
361
- return this._instructionsOverride({ defaultInstructions });
362
- }
363
- _getDefaultInstructions() {
364
- return `Modal cloud sandbox running ${this.baseImage}. Use executeCommand() to run shell commands.`;
365
- }
366
- // ---------------------------------------------------------------------------
367
- // Dead-sandbox Retry
368
- // ---------------------------------------------------------------------------
369
- isSandboxDeadError(error) {
370
- if (!error) return false;
371
- if (error instanceof modal.ClientClosedError) return true;
372
- if (error instanceof modal.NotFoundError) return true;
373
- const errorStr = String(error);
374
- return errorStr.includes("sandbox not found") || errorStr.includes("has been terminated") || errorStr.includes("already completed") || errorStr.includes("was cancelled") || // gRPC NOT_FOUND (code 5)
375
- /status[:\s]+5\b/.test(errorStr) || errorStr.includes("NOT_FOUND");
376
- }
377
- handleSandboxDead() {
378
- this._sb = null;
379
- this.status = "stopped";
380
- }
381
- /** @internal Retries fn() once after restarting if the sandbox is dead. */
382
- async retryOnDead(fn) {
383
- try {
384
- return await fn();
385
- } catch (error) {
386
- if (this.isSandboxDeadError(error) && !this._isRetrying) {
387
- this.handleSandboxDead();
388
- this._isRetrying = true;
389
- try {
390
- await this.ensureRunning();
391
- return await fn();
392
- } finally {
393
- this._isRetrying = false;
394
- }
395
- }
396
- throw error;
397
- }
398
- }
399
- // ---------------------------------------------------------------------------
400
- // Internal Helpers
401
- // ---------------------------------------------------------------------------
402
- _generateId() {
403
- return `modal-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
404
- }
405
- _getClient() {
406
- if (!this._client) {
407
- this._client = new modal.ModalClient({
408
- ...this.tokenId && { tokenId: this.tokenId },
409
- ...this.tokenSecret && { tokenSecret: this.tokenSecret }
410
- });
411
- }
412
- return this._client;
413
- }
180
+ //#endregion
181
+ //#region src/sandbox/index.ts
182
+ const LOG_PREFIX = "[ModalSandbox]";
183
+ /**
184
+ * Modal cloud sandbox provider for Mastra workspaces.
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * import { Workspace } from '@mastra/core/workspace';
189
+ * import { ModalSandbox } from '@mastra/modal';
190
+ *
191
+ * const sandbox = new ModalSandbox({
192
+ * baseImage: 'ubuntu:22.04',
193
+ * timeoutMs: 60_000,
194
+ * });
195
+ *
196
+ * const workspace = new Workspace({ sandbox });
197
+ * const result = await workspace.executeCommand('echo hello');
198
+ * ```
199
+ */
200
+ var ModalSandbox = class ModalSandbox extends _mastra_core_workspace.MastraSandbox {
201
+ id;
202
+ name = "ModalSandbox";
203
+ provider = "modal";
204
+ status = "pending";
205
+ _sb = null;
206
+ _imageSnapshot = null;
207
+ _client = null;
208
+ _createdAt = null;
209
+ _isRetrying = false;
210
+ appName;
211
+ baseImage;
212
+ timeoutMs;
213
+ env;
214
+ workdir;
215
+ tokenId;
216
+ tokenSecret;
217
+ _instructionsOverride;
218
+ _constructorOptions;
219
+ constructor(options = {}) {
220
+ super({
221
+ ...options,
222
+ name: "ModalSandbox",
223
+ processes: new ModalProcessManager({ env: options.env ?? {} })
224
+ });
225
+ this.id = options.id ?? this._generateId();
226
+ this.appName = options.appName ?? "mastra";
227
+ this.baseImage = options.baseImage ?? "ubuntu:22.04";
228
+ this.timeoutMs = options.timeoutMs ?? 3e5;
229
+ this.env = options.env ?? {};
230
+ this.workdir = options.workdir;
231
+ this.tokenId = options.tokenId;
232
+ this.tokenSecret = options.tokenSecret;
233
+ this._instructionsOverride = options.instructions;
234
+ this._constructorOptions = { ...options };
235
+ }
236
+ /**
237
+ * Construct a sibling `ModalSandbox` that inherits this sandbox's
238
+ * configuration (credentials, app, base image, workdir, instructions) with
239
+ * per-instance overrides.
240
+ *
241
+ * Performs no I/O — the sandbox clone provisions (or reconnects to a
242
+ * running Modal sandbox with the same logical `id`) on its own `start()`.
243
+ * Use it when one configured sandbox acts as the template for a fleet of
244
+ * independent sandboxes (e.g. one per project).
245
+ *
246
+ * `options.idleTimeoutMinutes` maps to Modal's `timeoutMs` (wall-clock
247
+ * lifetime); `options.sandboxId` is ignored because Modal reconnects by
248
+ * logical `id`.
249
+ */
250
+ clone(options = {}) {
251
+ const { id: _id, ...base } = this._constructorOptions;
252
+ return new ModalSandbox({
253
+ ...base,
254
+ ...options.id !== void 0 && { id: options.id },
255
+ ...options.env !== void 0 && { env: options.env },
256
+ ...options.idleTimeoutMinutes !== void 0 && { timeoutMs: options.idleTimeoutMinutes * 6e4 }
257
+ });
258
+ }
259
+ /**
260
+ * Get the underlying Modal Sandbox instance for direct SDK access.
261
+ *
262
+ * @throws {SandboxNotReadyError} If the sandbox has not been started.
263
+ */
264
+ get modal() {
265
+ if (!this._sb) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
266
+ return this._sb;
267
+ }
268
+ /** Reconnects to a running sandbox with this id if one exists, otherwise creates a new one. */
269
+ async start() {
270
+ if (this._sb) return;
271
+ const client = this._getClient();
272
+ try {
273
+ this._sb = await client.sandboxes.fromName(this.appName, this.id);
274
+ this._createdAt = /* @__PURE__ */ new Date();
275
+ this.logger.debug(`${LOG_PREFIX} Reconnected to running sandbox: ${this.id}`);
276
+ return;
277
+ } catch (error) {
278
+ if (!(error instanceof modal.NotFoundError)) throw error;
279
+ }
280
+ const app = await client.apps.fromName(this.appName, { createIfMissing: true });
281
+ if (this._imageSnapshot) {
282
+ this.logger.debug(`${LOG_PREFIX} Rebooting from snapshot: ${this.id}`);
283
+ this._sb = await client.sandboxes.create(app, this._imageSnapshot, {
284
+ name: this.id,
285
+ timeoutMs: this.timeoutMs,
286
+ env: Object.keys(this.env).length > 0 ? this.env : void 0,
287
+ workdir: this.workdir
288
+ });
289
+ this._createdAt = /* @__PURE__ */ new Date();
290
+ this.logger.debug(`${LOG_PREFIX} Created new sandbox from snapshot: ${this._sb?.sandboxId}`);
291
+ return;
292
+ }
293
+ const image = client.images.fromRegistry(this.baseImage);
294
+ this.logger.debug(`${LOG_PREFIX} Creating sandbox: ${this.id} (baseImage: ${this.baseImage})`);
295
+ this._sb = await client.sandboxes.create(app, image, {
296
+ name: this.id,
297
+ timeoutMs: this.timeoutMs,
298
+ env: Object.keys(this.env).length > 0 ? this.env : void 0,
299
+ workdir: this.workdir
300
+ });
301
+ this._createdAt = /* @__PURE__ */ new Date();
302
+ this.logger.debug(`${LOG_PREFIX} Created sandbox: ${this._sb.sandboxId}`);
303
+ }
304
+ /**
305
+ * Snapshot the sandbox filesystem before terminating it.
306
+ * Future starts will create net-new sandboxes from the snapshot.
307
+ */
308
+ async stop() {
309
+ if (!this._sb) return;
310
+ try {
311
+ const procs = await this.processes.list();
312
+ await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
313
+ } catch {}
314
+ try {
315
+ this._imageSnapshot = await this._sb.snapshotFilesystem();
316
+ this.logger.debug(`${LOG_PREFIX} Snapshot created: ${this._imageSnapshot.imageId}`);
317
+ } catch (error) {
318
+ this.logger.debug(`${LOG_PREFIX} Snapshot failed, terminating without snapshot:`, error);
319
+ }
320
+ try {
321
+ await this._sb.terminate({ wait: true });
322
+ this.logger.debug(`${LOG_PREFIX} Sandbox terminated: ${this._sb.sandboxId}`);
323
+ this._sb = null;
324
+ } catch (error) {
325
+ if (this.isSandboxDeadError(error)) this._sb = null;
326
+ else throw error;
327
+ }
328
+ }
329
+ /** Terminates the sandbox, ending its lifetime. Unlike stop(), no snapshot is preserved. */
330
+ async destroy() {
331
+ if (this._sb) {
332
+ try {
333
+ const procs = await this.processes.list();
334
+ await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
335
+ } catch {}
336
+ try {
337
+ await this._sb.terminate();
338
+ this.logger.debug(`${LOG_PREFIX} Sandbox terminated: ${this._sb.sandboxId}`);
339
+ } catch {}
340
+ this._sb = null;
341
+ }
342
+ this._imageSnapshot = null;
343
+ }
344
+ async getInfo() {
345
+ return {
346
+ id: this.id,
347
+ name: this.name,
348
+ provider: this.provider,
349
+ status: this.status,
350
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
351
+ metadata: {
352
+ appName: this.appName,
353
+ image: this._imageSnapshot?.imageId ?? this.baseImage,
354
+ timeoutMs: this.timeoutMs
355
+ }
356
+ };
357
+ }
358
+ getInstructions() {
359
+ const defaultInstructions = this._getDefaultInstructions();
360
+ if (this._instructionsOverride === void 0) return defaultInstructions;
361
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
362
+ return this._instructionsOverride({ defaultInstructions });
363
+ }
364
+ _getDefaultInstructions() {
365
+ return `Modal cloud sandbox running ${this.baseImage}. Use executeCommand() to run shell commands.`;
366
+ }
367
+ isSandboxDeadError(error) {
368
+ if (!error) return false;
369
+ if (error instanceof modal.ClientClosedError) return true;
370
+ if (error instanceof modal.NotFoundError) return true;
371
+ const errorStr = String(error);
372
+ return errorStr.includes("sandbox not found") || errorStr.includes("has been terminated") || errorStr.includes("already completed") || errorStr.includes("was cancelled") || /status[:\s]+5\b/.test(errorStr) || errorStr.includes("NOT_FOUND");
373
+ }
374
+ handleSandboxDead() {
375
+ this._sb = null;
376
+ this.status = "stopped";
377
+ }
378
+ /** @internal Retries fn() once after restarting if the sandbox is dead. */
379
+ async retryOnDead(fn) {
380
+ try {
381
+ return await fn();
382
+ } catch (error) {
383
+ if (this.isSandboxDeadError(error) && !this._isRetrying) {
384
+ this.handleSandboxDead();
385
+ this._isRetrying = true;
386
+ try {
387
+ await this.ensureRunning();
388
+ return await fn();
389
+ } finally {
390
+ this._isRetrying = false;
391
+ }
392
+ }
393
+ throw error;
394
+ }
395
+ }
396
+ _generateId() {
397
+ return `modal-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
398
+ }
399
+ _getClient() {
400
+ if (!this._client) this._client = new modal.ModalClient({
401
+ ...this.tokenId && { tokenId: this.tokenId },
402
+ ...this.tokenSecret && { tokenSecret: this.tokenSecret }
403
+ });
404
+ return this._client;
405
+ }
414
406
  };
415
-
407
+ //#endregion
416
408
  exports.ModalProcessManager = ModalProcessManager;
417
409
  exports.ModalSandbox = ModalSandbox;
418
- //# sourceMappingURL=index.cjs.map
410
+
419
411
  //# sourceMappingURL=index.cjs.map