@mastra/railway 0.4.0 → 0.4.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,656 +1,624 @@
1
- 'use strict';
2
-
3
- var workspace = require('@mastra/core/workspace');
4
- var railway = require('railway');
5
-
6
- // src/sandbox/index.ts
7
-
8
- // src/utils/shell-quote.ts
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _mastra_core_workspace = require("@mastra/core/workspace");
3
+ let railway = require("railway");
4
+ //#region src/utils/shell-quote.ts
5
+ /**
6
+ * Shell-quote a single argument for safe use in a command string.
7
+ *
8
+ * Arguments containing only safe characters are returned as-is.
9
+ * All others are wrapped in single quotes with embedded single quotes escaped.
10
+ */
9
11
  function shellQuote(arg) {
10
- if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
11
- return "'" + arg.replace(/'/g, "'\\''") + "'";
12
+ if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
13
+ return "'" + arg.replace(/'/g, "'\\''") + "'";
12
14
  }
13
- var LOG_PREFIX = "[RailwaySandbox]";
14
- var RailwayProcessHandle = class extends workspace.ProcessHandle {
15
- pid;
16
- _execHandle;
17
- _startTime;
18
- _exitCode;
19
- _waitPromise = null;
20
- _killed = false;
21
- constructor(pid, execHandle, startTime, options) {
22
- super(options);
23
- this.pid = pid;
24
- this._execHandle = execHandle;
25
- this._startTime = startTime;
26
- void this._execHandle.then(
27
- (result) => {
28
- this._exitCode = result.exitCode ?? (this._killed ? 137 : -1);
29
- },
30
- () => {
31
- if (this._exitCode === void 0) {
32
- this._exitCode = 1;
33
- }
34
- }
35
- );
36
- }
37
- get exitCode() {
38
- return this._exitCode;
39
- }
40
- async wait() {
41
- if (!this._waitPromise) {
42
- this._waitPromise = this._doWait();
43
- }
44
- return this._waitPromise;
45
- }
46
- async _doWait() {
47
- try {
48
- const result = await this._execHandle;
49
- const exitCode = result.exitCode ?? (this._killed ? 137 : -1);
50
- this._exitCode = exitCode;
51
- if (result.stdout && !this.stdout) this.emitStdout(result.stdout);
52
- if (result.stderr && !this.stderr) this.emitStderr(result.stderr);
53
- return {
54
- success: exitCode === 0,
55
- exitCode,
56
- stdout: this.stdout,
57
- stderr: this.stderr,
58
- executionTimeMs: Date.now() - this._startTime,
59
- killed: this._killed,
60
- timedOut: result.timedOut
61
- };
62
- } catch (error) {
63
- const exitCode = this._exitCode ?? 1;
64
- this._exitCode = exitCode;
65
- return {
66
- success: false,
67
- exitCode,
68
- stdout: this.stdout,
69
- stderr: this.stderr || (error instanceof Error ? error.message : String(error)),
70
- executionTimeMs: Date.now() - this._startTime,
71
- killed: this._killed
72
- };
73
- }
74
- }
75
- async kill() {
76
- if (this._exitCode !== void 0) return false;
77
- this._killed = true;
78
- try {
79
- return await this._execHandle.kill("TERM");
80
- } catch {
81
- return false;
82
- }
83
- }
84
- async sendStdin(_data) {
85
- throw new Error(`${LOG_PREFIX} sending stdin is not supported by the Railway sandbox provider`);
86
- }
15
+ //#endregion
16
+ //#region src/sandbox/process-manager.ts
17
+ /**
18
+ * Railway Process Manager
19
+ *
20
+ * Implements SandboxProcessManager for Railway sandboxes.
21
+ * Wraps the Railway SDK's `Sandbox.exec()` API.
22
+ *
23
+ * Railway's `exec(command, options)` accepts per-call `cwd` and `env` options
24
+ * (since SDK v3.3.1) and returns an `ExecHandle` that runs the command
25
+ * server-side, independently of the client. Each spawn() starts one exec.
26
+ * The handle streams output via `onStdout`/`onStderr` callbacks wired to
27
+ * `emitStdout`/`emitStderr`, exposes a durable `sessionName`, and can be
28
+ * terminated with `kill(signal)`.
29
+ */
30
+ const LOG_PREFIX = "[RailwaySandbox]";
31
+ /**
32
+ * Wraps a Railway ExecHandle to conform to Mastra's ProcessHandle.
33
+ * Not exported — internal to this module.
34
+ */
35
+ var RailwayProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
36
+ pid;
37
+ _execHandle;
38
+ _startTime;
39
+ _exitCode;
40
+ _waitPromise = null;
41
+ _killed = false;
42
+ constructor(pid, execHandle, startTime, options) {
43
+ super(options);
44
+ this.pid = pid;
45
+ this._execHandle = execHandle;
46
+ this._startTime = startTime;
47
+ this._execHandle.then((result) => {
48
+ this._exitCode = result.exitCode ?? (this._killed ? 137 : -1);
49
+ }, () => {
50
+ if (this._exitCode === void 0) this._exitCode = 1;
51
+ });
52
+ }
53
+ get exitCode() {
54
+ return this._exitCode;
55
+ }
56
+ async wait() {
57
+ if (!this._waitPromise) this._waitPromise = this._doWait();
58
+ return this._waitPromise;
59
+ }
60
+ async _doWait() {
61
+ try {
62
+ const result = await this._execHandle;
63
+ const exitCode = result.exitCode ?? (this._killed ? 137 : -1);
64
+ this._exitCode = exitCode;
65
+ if (result.stdout && !this.stdout) this.emitStdout(result.stdout);
66
+ if (result.stderr && !this.stderr) this.emitStderr(result.stderr);
67
+ return {
68
+ success: exitCode === 0,
69
+ exitCode,
70
+ stdout: this.stdout,
71
+ stderr: this.stderr,
72
+ executionTimeMs: Date.now() - this._startTime,
73
+ killed: this._killed,
74
+ timedOut: result.timedOut
75
+ };
76
+ } catch (error) {
77
+ const exitCode = this._exitCode ?? 1;
78
+ this._exitCode = exitCode;
79
+ return {
80
+ success: false,
81
+ exitCode,
82
+ stdout: this.stdout,
83
+ stderr: this.stderr || (error instanceof Error ? error.message : String(error)),
84
+ executionTimeMs: Date.now() - this._startTime,
85
+ killed: this._killed
86
+ };
87
+ }
88
+ }
89
+ async kill() {
90
+ if (this._exitCode !== void 0) return false;
91
+ this._killed = true;
92
+ try {
93
+ return await this._execHandle.kill("TERM");
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ async sendStdin(_data) {
99
+ throw new Error(`${LOG_PREFIX} sending stdin is not supported by the Railway sandbox provider`);
100
+ }
87
101
  };
88
- var RailwayProcessManager = class extends workspace.SandboxProcessManager {
89
- _spawnCounter = 0;
90
- constructor(opts = {}) {
91
- super({ env: opts.env });
92
- }
93
- async spawn(command, options = {}) {
94
- const railway = this.sandbox.railway;
95
- const mergedEnv = { ...this.env, ...options.env };
96
- const env = Object.fromEntries(
97
- Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
98
- );
99
- const pid = `railway-proc-${Date.now().toString(36)}-${(this._spawnCounter++).toString(36)}`;
100
- let handle;
101
- const execHandle = railway.exec(command, {
102
- ...options.timeout !== void 0 && { timeoutSec: Math.ceil(options.timeout / 1e3) },
103
- ...options.cwd !== void 0 && { cwd: options.cwd },
104
- ...Object.keys(env).length > 0 && { env },
105
- onStdout: (chunk) => handle.emitStdout(chunk),
106
- onStderr: (chunk) => handle.emitStderr(chunk)
107
- });
108
- handle = new RailwayProcessHandle(pid, execHandle, Date.now(), options);
109
- this._tracked.set(handle.pid, handle);
110
- return handle;
111
- }
112
- /**
113
- * List tracked processes.
114
- *
115
- * Railway has no API to enumerate running exec sessions by sandbox, so this
116
- * reports the processes this manager spawned.
117
- */
118
- async list() {
119
- return Array.from(this._tracked.values()).map((handle) => ({
120
- pid: handle.pid,
121
- command: handle.command,
122
- running: handle.exitCode === void 0,
123
- ...handle.exitCode !== void 0 && { exitCode: handle.exitCode }
124
- }));
125
- }
102
+ /**
103
+ * Railway implementation of SandboxProcessManager.
104
+ * Uses the Railway SDK's `Sandbox.exec()` with one exec per spawned process.
105
+ */
106
+ var RailwayProcessManager = class extends _mastra_core_workspace.SandboxProcessManager {
107
+ _spawnCounter = 0;
108
+ constructor(opts = {}) {
109
+ super({ env: opts.env });
110
+ }
111
+ async spawn(command, options = {}) {
112
+ const railway = this.sandbox.railway;
113
+ const mergedEnv = {
114
+ ...this.env,
115
+ ...options.env
116
+ };
117
+ const env = Object.fromEntries(Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0));
118
+ const pid = `railway-proc-${Date.now().toString(36)}-${(this._spawnCounter++).toString(36)}`;
119
+ let handle;
120
+ handle = new RailwayProcessHandle(pid, railway.exec(command, {
121
+ ...options.timeout !== void 0 && { timeoutSec: Math.ceil(options.timeout / 1e3) },
122
+ ...options.cwd !== void 0 && { cwd: options.cwd },
123
+ ...Object.keys(env).length > 0 && { env },
124
+ onStdout: (chunk) => handle.emitStdout(chunk),
125
+ onStderr: (chunk) => handle.emitStderr(chunk)
126
+ }), Date.now(), options);
127
+ this._tracked.set(handle.pid, handle);
128
+ return handle;
129
+ }
130
+ /**
131
+ * List tracked processes.
132
+ *
133
+ * Railway has no API to enumerate running exec sessions by sandbox, so this
134
+ * reports the processes this manager spawned.
135
+ */
136
+ async list() {
137
+ return Array.from(this._tracked.values()).map((handle) => ({
138
+ pid: handle.pid,
139
+ command: handle.command,
140
+ running: handle.exitCode === void 0,
141
+ ...handle.exitCode !== void 0 && { exitCode: handle.exitCode }
142
+ }));
143
+ }
126
144
  };
127
-
128
- // src/sandbox/index.ts
129
- var RailwaySandbox = class _RailwaySandbox extends workspace.MastraSandbox {
130
- id;
131
- name = "RailwaySandbox";
132
- provider = "railway";
133
- status = "pending";
134
- _sandbox = null;
135
- _createdAt = null;
136
- _checkpointRefreshTimer = null;
137
- _checkpointRefreshInFlight = null;
138
- _token;
139
- _environmentId;
140
- _sandboxId;
141
- _checkpointName;
142
- _idleTimeoutMinutes;
143
- _networkIsolation;
144
- _env;
145
- _timeout;
146
- _instructionsOverride;
147
- _templateOption;
148
- constructor(options = {}) {
149
- super({
150
- ...options,
151
- name: "RailwaySandbox",
152
- processes: new RailwayProcessManager({ env: options.env })
153
- });
154
- this.id = options.id ?? this.generateId();
155
- this._token = options.token ?? process.env.RAILWAY_API_TOKEN;
156
- this._environmentId = options.environmentId ?? process.env.RAILWAY_ENVIRONMENT_ID;
157
- this._sandboxId = options.sandboxId;
158
- this._checkpointName = options.checkpointName;
159
- this._idleTimeoutMinutes = options.idleTimeoutMinutes;
160
- this._networkIsolation = options.networkIsolation;
161
- this._env = options.env ?? {};
162
- this._timeout = options.timeout;
163
- this._instructionsOverride = options.instructions;
164
- this._templateOption = options.template;
165
- }
166
- generateId() {
167
- return `railway-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
168
- }
169
- /**
170
- * Get the underlying Railway Sandbox instance for direct SDK access.
171
- *
172
- * @throws {SandboxNotReadyError} If the sandbox has not been started.
173
- */
174
- get railway() {
175
- if (!this._sandbox) {
176
- throw new workspace.SandboxNotReadyError(this.id);
177
- }
178
- return this._sandbox;
179
- }
180
- // ---------------------------------------------------------------------------
181
- // Lifecycle
182
- // ---------------------------------------------------------------------------
183
- /**
184
- * Start the Railway sandbox.
185
- *
186
- * Reattaches to an existing sandbox when `sandboxId` is configured,
187
- * otherwise provisions a new one. Resolves once the sandbox is RUNNING.
188
- */
189
- async start() {
190
- if (this._sandbox) {
191
- return;
192
- }
193
- await this._startRailwaySandbox({ reconnectSandboxId: this._sandboxId, fallbackToCreate: false });
194
- }
195
- async restart() {
196
- const reconnectSandboxId = this._sandbox?.id ?? this._sandboxId;
197
- this._cancelCheckpointRefresh();
198
- await this._checkpointRefreshInFlight?.catch((error) => {
199
- this.logger.warn(`${LOG_PREFIX} Failed to flush in-flight checkpoint before restart:`, error);
200
- });
201
- this._sandbox = null;
202
- this._createdAt = null;
203
- this.status = "starting";
204
- try {
205
- await this._startRailwaySandbox({ reconnectSandboxId, fallbackToCreate: true });
206
- this.status = "running";
207
- } catch (error) {
208
- this.status = "error";
209
- throw error;
210
- }
211
- }
212
- async withRestartRetry(operation) {
213
- await this.ensureRunning();
214
- try {
215
- return await operation();
216
- } catch (error) {
217
- if (!this.isSandboxUnavailableError(error)) {
218
- throw error;
219
- }
220
- await this.restart();
221
- return await operation();
222
- } finally {
223
- this._scheduleCheckpointRefresh();
224
- }
225
- }
226
- async _startRailwaySandbox({
227
- reconnectSandboxId,
228
- fallbackToCreate
229
- }) {
230
- const clientConfig = this._clientConfig();
231
- const createOptions = this._createOptions(clientConfig);
232
- this._sandbox = reconnectSandboxId ? await this._reconnectSandbox(reconnectSandboxId, fallbackToCreate, clientConfig, createOptions) : await this._createNewSandbox(createOptions);
233
- this._createdAt = this._sandbox.createdAt ? new Date(this._sandbox.createdAt) : /* @__PURE__ */ new Date();
234
- this.logger.debug(`${LOG_PREFIX} Railway sandbox ${this._sandbox.id} ready for logical ID: ${this.id}`);
235
- this._scheduleCheckpointRefresh();
236
- }
237
- /**
238
- * Reconnect to an existing Railway sandbox, creating a fresh one when
239
- * `fallbackToCreate` is set and the sandbox is unavailable or not running.
240
- */
241
- async _reconnectSandbox(reconnectSandboxId, fallbackToCreate, clientConfig, createOptions) {
242
- this.logger.debug(`${LOG_PREFIX} Reconnecting to Railway sandbox ${reconnectSandboxId}...`);
243
- let connectedSandbox;
244
- try {
245
- connectedSandbox = await railway.Sandbox.connect(reconnectSandboxId, clientConfig);
246
- } catch (error) {
247
- if (!fallbackToCreate || !this.isSandboxUnavailableError(error)) {
248
- throw error;
249
- }
250
- return this._createNewSandbox(createOptions);
251
- }
252
- if (connectedSandbox.status === "RUNNING") {
253
- return connectedSandbox;
254
- }
255
- if (!fallbackToCreate) {
256
- throw new Error(`Railway sandbox ${reconnectSandboxId} is not running (status: ${connectedSandbox.status})`);
257
- }
258
- return this._createNewSandbox(createOptions);
259
- }
260
- _clientConfig() {
261
- return {
262
- ...this._token !== void 0 && { token: this._token },
263
- ...this._environmentId !== void 0 && { environmentId: this._environmentId }
264
- };
265
- }
266
- _createOptions(clientConfig) {
267
- return {
268
- ...clientConfig,
269
- ...this._idleTimeoutMinutes !== void 0 && { idleTimeoutMinutes: this._idleTimeoutMinutes },
270
- ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
271
- ...Object.keys(this._env).length > 0 && { env: this._env }
272
- };
273
- }
274
- async _createNewSandbox(createOptions) {
275
- const checkpointSandbox = await this._tryCreateFromCheckpoint(createOptions);
276
- if (checkpointSandbox) {
277
- return checkpointSandbox;
278
- }
279
- if (this._templateOption) {
280
- const template = this._resolveTemplate();
281
- this.logger.debug(`${LOG_PREFIX} Creating Railway sandbox from template for: ${this.id}`);
282
- const sandbox2 = await railway.Sandbox.create(template, createOptions);
283
- await this._checkpointSandbox(sandbox2);
284
- return sandbox2;
285
- }
286
- this.logger.debug(`${LOG_PREFIX} Creating Railway sandbox for: ${this.id}`);
287
- const sandbox = await railway.Sandbox.create(createOptions);
288
- await this._checkpointSandbox(sandbox);
289
- return sandbox;
290
- }
291
- async _tryCreateFromCheckpoint(createOptions) {
292
- if (!this._checkpointName) {
293
- return void 0;
294
- }
295
- this.logger.debug(`${LOG_PREFIX} Creating Railway sandbox from checkpoint ${this._checkpointName} for: ${this.id}`);
296
- try {
297
- const sandbox = await railway.Sandbox.create(this._checkpointName, createOptions);
298
- return sandbox;
299
- } catch (error) {
300
- if (!this.isCheckpointUnavailableError(error)) {
301
- throw error;
302
- }
303
- return void 0;
304
- }
305
- }
306
- async _checkpointSandbox(sandbox) {
307
- if (!this._checkpointName) {
308
- return;
309
- }
310
- try {
311
- this.logger.debug(`${LOG_PREFIX} Capturing Railway sandbox checkpoint ${this._checkpointName} for: ${this.id}`);
312
- await sandbox.checkpoint(this._checkpointName);
313
- } catch (error) {
314
- if (!this.isCheckpointAlreadyExistsError(error)) {
315
- throw error;
316
- }
317
- await this._deleteCheckpointByName(this._checkpointName);
318
- await sandbox.checkpoint(this._checkpointName);
319
- }
320
- }
321
- async _deleteCheckpointByName(name) {
322
- try {
323
- const checkpoint = (await railway.Sandbox.checkpoints(this._clientConfig())).find((checkpoint2) => checkpoint2.key === name);
324
- if (!checkpoint) {
325
- return;
326
- }
327
- await railway.Sandbox.deleteCheckpoint(checkpoint.id, this._clientConfig());
328
- } catch (error) {
329
- if (!this.isCheckpointUnavailableError(error)) {
330
- throw error;
331
- }
332
- }
333
- }
334
- _scheduleCheckpointRefresh() {
335
- if (!this._checkpointName || !this._sandbox) {
336
- return;
337
- }
338
- const idleTimeoutMinutes = this._idleTimeoutMinutes ?? this._sandbox.idleTimeoutMinutes;
339
- if (!idleTimeoutMinutes) {
340
- return;
341
- }
342
- if (this._checkpointRefreshTimer) {
343
- clearTimeout(this._checkpointRefreshTimer);
344
- }
345
- const delayMs = Math.max(1e3, idleTimeoutMinutes * 6e4 - 1e4);
346
- this._checkpointRefreshTimer = setTimeout(() => {
347
- this._checkpointRefreshTimer = null;
348
- const sandbox = this._sandbox;
349
- if (!sandbox) {
350
- return;
351
- }
352
- const refresh = this._checkpointSandbox(sandbox).finally(() => {
353
- if (this._checkpointRefreshInFlight === refresh) {
354
- this._checkpointRefreshInFlight = null;
355
- }
356
- });
357
- this._checkpointRefreshInFlight = refresh;
358
- this._checkpointRefreshInFlight.catch((error) => {
359
- this.logger.warn(`${LOG_PREFIX} Failed to refresh Railway sandbox checkpoint ${this._checkpointName}:`, error);
360
- });
361
- }, delayMs);
362
- this._checkpointRefreshTimer.unref?.();
363
- }
364
- _cancelCheckpointRefresh() {
365
- if (this._checkpointRefreshTimer) {
366
- clearTimeout(this._checkpointRefreshTimer);
367
- this._checkpointRefreshTimer = null;
368
- }
369
- }
370
- async _flushCheckpointRefresh() {
371
- this._cancelCheckpointRefresh();
372
- if (this._checkpointRefreshInFlight) {
373
- await this._checkpointRefreshInFlight;
374
- return;
375
- }
376
- if (this._sandbox) {
377
- await this._checkpointSandbox(this._sandbox);
378
- }
379
- }
380
- isCheckpointUnavailableError(error) {
381
- if (!(error instanceof Error)) {
382
- return false;
383
- }
384
- const message = error.message.toLowerCase();
385
- return message.includes("checkpoint") && ["not found", "does not exist", "missing", "unknown", "no checkpoint"].some((phrase) => message.includes(phrase));
386
- }
387
- isCheckpointAlreadyExistsError(error) {
388
- if (!(error instanceof Error)) {
389
- return false;
390
- }
391
- const message = error.message.toLowerCase();
392
- return message.includes("checkpoint") && (["already exists", "must be unused", "unique"].some((phrase) => message.includes(phrase)) || message.includes("name") && message.includes("used"));
393
- }
394
- isSandboxUnavailableError(error, seen = /* @__PURE__ */ new Set()) {
395
- if (error && typeof error === "object") {
396
- if (seen.has(error)) return false;
397
- seen.add(error);
398
- }
399
- if (error instanceof railway.SandboxNotFoundError || error instanceof railway.SandboxFailedError || error instanceof railway.SandboxTimeoutError && error.resource === "sandbox") {
400
- return true;
401
- }
402
- if (error && typeof error === "object") {
403
- const errorLike = error;
404
- const name = typeof errorLike.name === "string" ? errorLike.name : "";
405
- const message = typeof errorLike.message === "string" ? errorLike.message.toLowerCase() : "";
406
- if (name === "SandboxNotFoundError" || name === "SandboxFailedError" || name === "SandboxTimeoutError" && errorLike.resource === "sandbox") {
407
- return true;
408
- }
409
- if (message.includes("sandbox") && ["not found", "destroyed", "failed", "not running", "unavailable"].some((phrase) => message.includes(phrase))) {
410
- return true;
411
- }
412
- if (errorLike.cause !== void 0) {
413
- return this.isSandboxUnavailableError(errorLike.cause, seen);
414
- }
415
- }
416
- return false;
417
- }
418
- /**
419
- * Stop the Railway sandbox.
420
- *
421
- * Railway sandboxes have no separate "stopped" state — they're either
422
- * running or destroyed — so stopping destroys the sandbox.
423
- */
424
- async stop() {
425
- await this._teardown();
426
- }
427
- /**
428
- * Destroy the Railway sandbox and release its resources.
429
- */
430
- async destroy() {
431
- await this._teardown();
432
- }
433
- async _teardown() {
434
- if (!this._sandbox) {
435
- this._cancelCheckpointRefresh();
436
- return;
437
- }
438
- const sandbox = this._sandbox;
439
- try {
440
- await this._flushCheckpointRefresh();
441
- } catch (error) {
442
- this.logger.warn(`${LOG_PREFIX} Failed to flush checkpoint before teardown:`, error);
443
- }
444
- this._sandbox = null;
445
- try {
446
- await sandbox.destroy();
447
- } catch (error) {
448
- this.logger.warn(`${LOG_PREFIX} Failed to destroy Railway sandbox ${sandbox.id}:`, error);
449
- }
450
- }
451
- /**
452
- * Resolve the configured template into a `SandboxTemplate` that Railway
453
- * builds during `Sandbox.create()`. Accepts either a pre-built
454
- * `SandboxTemplate` or a builder callback over `Sandbox.template()`.
455
- */
456
- _resolveTemplate() {
457
- const option = this._templateOption;
458
- return typeof option === "function" ? option(railway.Sandbox.template()) : option;
459
- }
460
- /**
461
- * Fork this running sandbox into a new, independent `RailwaySandbox`.
462
- *
463
- * Clones the filesystem (a fresh boot, not live processes) into the same
464
- * environment. The returned sandbox is already started and reattached to the
465
- * forked Railway sandbox; it inherits this sandbox's credentials and defaults
466
- * unless overridden via `options`.
467
- *
468
- * @throws {SandboxNotReadyError} If this sandbox has not been started.
469
- */
470
- async fork(options = {}) {
471
- const source = this.railway;
472
- const forked = await source.fork({
473
- ...options.idleTimeoutMinutes !== void 0 && { idleTimeoutMinutes: options.idleTimeoutMinutes },
474
- ...options.networkIsolation !== void 0 && { networkIsolation: options.networkIsolation },
475
- ...options.env !== void 0 && { env: options.env }
476
- });
477
- const child = new _RailwaySandbox({
478
- ...options.id !== void 0 && { id: options.id },
479
- ...this._token !== void 0 && { token: this._token },
480
- ...this._environmentId !== void 0 && { environmentId: this._environmentId },
481
- sandboxId: forked.id,
482
- idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
483
- networkIsolation: options.networkIsolation ?? this._networkIsolation,
484
- env: options.env ?? this._env,
485
- timeout: this._timeout
486
- });
487
- await child._start();
488
- return child;
489
- }
490
- /**
491
- * Construct a sibling `RailwaySandbox` that inherits this sandbox's
492
- * credentials and defaults (token, environment, checkpoint, network
493
- * isolation, timeout, template, instructions) with per-instance overrides.
494
- *
495
- * Unlike {@link fork}, `clone` performs no I/O and does not require this
496
- * sandbox to be started — the returned sandbox is not started and provisions
497
- * (or reattaches, when `sandboxId` is set) on its own `start()`. Use it when
498
- * one configured sandbox acts as the template for a fleet of independent
499
- * sandboxes (e.g. one per project).
500
- */
501
- clone(options = {}) {
502
- return new _RailwaySandbox({
503
- ...options.id !== void 0 && { id: options.id },
504
- ...this._token !== void 0 && { token: this._token },
505
- ...this._environmentId !== void 0 && { environmentId: this._environmentId },
506
- ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
507
- ...(options.checkpointName ?? this._checkpointName) !== void 0 && {
508
- checkpointName: options.checkpointName ?? this._checkpointName
509
- },
510
- idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
511
- ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
512
- env: options.env ?? this._env,
513
- ...this._templateOption !== void 0 && { template: this._templateOption },
514
- ...this._timeout !== void 0 && { timeout: this._timeout },
515
- ...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride }
516
- });
517
- }
518
- /**
519
- * Whether a Railway API token was resolved at construction (explicit option
520
- * or the `RAILWAY_API_TOKEN` env fallback). Lets callers gate features on a
521
- * usable configuration without provisioning a sandbox.
522
- */
523
- get hasCredentials() {
524
- return this._token !== void 0 && this._token !== "";
525
- }
526
- /** The configured idle teardown window in minutes, if any. */
527
- get idleTimeoutMinutes() {
528
- return this._idleTimeoutMinutes;
529
- }
530
- // ---------------------------------------------------------------------------
531
- // Info & Instructions
532
- // ---------------------------------------------------------------------------
533
- async getInfo() {
534
- return {
535
- id: this.id,
536
- name: this.name,
537
- provider: this.provider,
538
- status: this.status,
539
- createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
540
- metadata: {
541
- ...this._sandbox && {
542
- railwaySandboxId: this._sandbox.id,
543
- environmentId: this._sandbox.environmentId,
544
- region: this._sandbox.region,
545
- networkIsolation: this._sandbox.networkIsolation,
546
- ...this._sandbox.idleTimeoutMinutes != null && {
547
- idleTimeoutMinutes: this._sandbox.idleTimeoutMinutes
548
- }
549
- }
550
- }
551
- };
552
- }
553
- getInstructions() {
554
- const defaultInstructions = this._buildDefaultInstructions();
555
- if (typeof this._instructionsOverride === "string") {
556
- return this._instructionsOverride;
557
- }
558
- if (typeof this._instructionsOverride === "function") {
559
- return this._instructionsOverride({ defaultInstructions });
560
- }
561
- return defaultInstructions;
562
- }
563
- _buildDefaultInstructions() {
564
- const parts = [];
565
- parts.push("Railway cloud sandbox: an isolated Debian Linux VM with outbound internet access.");
566
- if (this._networkIsolation === "PRIVATE") {
567
- parts.push("Joined to the environment private network.");
568
- }
569
- if (this._timeout !== void 0) {
570
- parts.push(`Default command timeout: ${Math.ceil(this._timeout / 1e3)}s.`);
571
- } else {
572
- parts.push("Commands run until they exit unless a timeout is set.");
573
- }
574
- if (this._idleTimeoutMinutes !== void 0) {
575
- parts.push(`Idle timeout: ${this._idleTimeoutMinutes} minute(s).`);
576
- }
577
- return parts.join(" ");
578
- }
579
- // ---------------------------------------------------------------------------
580
- // Command Execution
581
- // ---------------------------------------------------------------------------
582
- /**
583
- * Execute a command in the sandbox and return the result.
584
- */
585
- async executeCommand(command, args = [], options = {}) {
586
- return this.withRestartRetry(async () => {
587
- const fullCommand = args.length > 0 ? `${command} ${args.map(shellQuote).join(" ")}` : command;
588
- const timeout = options.timeout ?? this._timeout;
589
- const env = options.env ? Object.fromEntries(
590
- Object.entries(options.env).filter((entry) => entry[1] !== void 0)
591
- ) : void 0;
592
- const startedAt = Date.now();
593
- const result = await this.railway.exec(fullCommand, {
594
- ...timeout !== void 0 && { timeoutSec: Math.ceil(timeout / 1e3) },
595
- ...options.cwd !== void 0 && { cwd: options.cwd },
596
- ...env !== void 0 && { env }
597
- });
598
- const exitCode = result.exitCode ?? -1;
599
- return {
600
- success: exitCode === 0,
601
- exitCode,
602
- stdout: result.stdout,
603
- stderr: result.stderr,
604
- executionTimeMs: Date.now() - startedAt,
605
- command,
606
- args,
607
- timedOut: result.timedOut
608
- };
609
- });
610
- }
145
+ //#endregion
146
+ //#region src/sandbox/index.ts
147
+ /**
148
+ * Safety margin subtracted from the sandbox's idle timeout when scheduling the
149
+ * pre-reap checkpoint refresh. Sized to comfortably exceed Cloud Run cold-start
150
+ * / recycle windows so a scale event during the refresh doesn't cause the timer
151
+ * to lose the race with Railway's idle destroy. If a caller reduces the idle
152
+ * timeout below this margin, the refresh falls back to the 1-second floor and
153
+ * fires almost immediately after start — surfacing the misconfiguration rather
154
+ * than silently skipping the refresh.
155
+ */
156
+ const CHECKPOINT_REFRESH_MARGIN_MS = 18e4;
157
+ /**
158
+ * Railway sandbox provider for Mastra workspaces.
159
+ *
160
+ * Features:
161
+ * - Ephemeral, isolated Linux VM via the Railway TypeScript SDK
162
+ * - Command execution with streaming output and timeouts
163
+ * - Configurable idle timeout and network isolation
164
+ * - Reattach to an existing sandbox by Railway ID
165
+ *
166
+ * @example Basic usage
167
+ * ```typescript
168
+ * import { Workspace } from '@mastra/core/workspace';
169
+ * import { RailwaySandbox } from '@mastra/railway';
170
+ *
171
+ * const sandbox = new RailwaySandbox({
172
+ * // token + environmentId read from RAILWAY_API_TOKEN / RAILWAY_ENVIRONMENT_ID
173
+ * idleTimeoutMinutes: 30,
174
+ * });
175
+ *
176
+ * const workspace = new Workspace({ sandbox });
177
+ * const result = await workspace.executeCode('console.log("Hello!")');
178
+ * ```
179
+ *
180
+ * @example Private networking
181
+ * ```typescript
182
+ * const sandbox = new RailwaySandbox({
183
+ * networkIsolation: 'PRIVATE',
184
+ * env: { NODE_ENV: 'production' },
185
+ * });
186
+ * ```
187
+ */
188
+ var RailwaySandbox = class RailwaySandbox extends _mastra_core_workspace.MastraSandbox {
189
+ id;
190
+ name = "RailwaySandbox";
191
+ provider = "railway";
192
+ status = "pending";
193
+ _sandbox = null;
194
+ _createdAt = null;
195
+ _checkpointRefreshTimer = null;
196
+ _checkpointRefreshInFlight = null;
197
+ _sandboxId;
198
+ _startInFlight = null;
199
+ _token;
200
+ _environmentId;
201
+ _checkpointName;
202
+ _idleTimeoutMinutes;
203
+ _networkIsolation;
204
+ _env;
205
+ _timeout;
206
+ _instructionsOverride;
207
+ _templateOption;
208
+ constructor(options = {}) {
209
+ super({
210
+ ...options,
211
+ name: "RailwaySandbox",
212
+ processes: new RailwayProcessManager({ env: options.env })
213
+ });
214
+ this.id = options.id ?? this.generateId();
215
+ this._token = options.token ?? process.env.RAILWAY_API_TOKEN;
216
+ this._environmentId = options.environmentId ?? process.env.RAILWAY_ENVIRONMENT_ID;
217
+ this._sandboxId = options.sandboxId;
218
+ this._checkpointName = options.checkpointName;
219
+ this._idleTimeoutMinutes = options.idleTimeoutMinutes;
220
+ this._networkIsolation = options.networkIsolation;
221
+ this._env = options.env ?? {};
222
+ this._timeout = options.timeout;
223
+ this._instructionsOverride = options.instructions;
224
+ this._templateOption = options.template;
225
+ }
226
+ generateId() {
227
+ return `railway-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
228
+ }
229
+ /**
230
+ * Get the underlying Railway Sandbox instance for direct SDK access.
231
+ *
232
+ * @throws {SandboxNotReadyError} If the sandbox has not been started.
233
+ */
234
+ get railway() {
235
+ if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
236
+ return this._sandbox;
237
+ }
238
+ /**
239
+ * Start the Railway sandbox.
240
+ *
241
+ * Reattaches to an existing sandbox when `sandboxId` is configured,
242
+ * otherwise provisions a new one. Resolves once the sandbox is RUNNING.
243
+ */
244
+ async start() {
245
+ if (this._sandbox) return;
246
+ const clientConfig = this._clientConfig();
247
+ const createOptions = this._createOptions(clientConfig);
248
+ if (this._sandboxId) {
249
+ const sandboxId = this._sandboxId;
250
+ this._startInFlight ??= (async () => {
251
+ try {
252
+ this._sandbox = await this._reconnectSandbox(sandboxId, clientConfig);
253
+ } catch (error) {
254
+ if (!(error instanceof railway.SandboxNotFoundError)) throw error;
255
+ this._sandbox = await this._createNewSandbox(createOptions);
256
+ }
257
+ })().finally(() => {
258
+ this._startInFlight = null;
259
+ });
260
+ } else this._startInFlight ??= (async () => {
261
+ this._sandbox = await this._createNewSandbox(createOptions);
262
+ })().finally(() => {
263
+ this._startInFlight = null;
264
+ });
265
+ await this._startInFlight;
266
+ if (!this._sandbox) throw new Error("Failed to start Railway sandbox");
267
+ const sandbox = this._sandbox;
268
+ this._sandboxId = sandbox.id;
269
+ this._createdAt = sandbox.createdAt ? new Date(sandbox.createdAt) : /* @__PURE__ */ new Date();
270
+ this.logger.debug(`${LOG_PREFIX} Railway sandbox ${sandbox.id} ready for logical ID: ${this.id}`);
271
+ this._scheduleCheckpointRefresh();
272
+ }
273
+ /**
274
+ * Create a new Railway sandbox.
275
+ */
276
+ async _createNewSandbox(createOptions) {
277
+ this.logger.debug(`${LOG_PREFIX} Creating Railway sandbox for: ${this.id}`);
278
+ try {
279
+ let checkpoinAlreadyExists = false;
280
+ if (this._checkpointName) checkpoinAlreadyExists = (await railway.Sandbox.checkpoints(this._clientConfig())).some((checkpoint) => checkpoint.key === this._checkpointName);
281
+ let sandbox;
282
+ if (checkpoinAlreadyExists) sandbox = await railway.Sandbox.create(this._checkpointName, createOptions);
283
+ else sandbox = await railway.Sandbox.create(createOptions);
284
+ return sandbox;
285
+ } catch (error) {
286
+ throw error;
287
+ }
288
+ }
289
+ /**
290
+ * Reconnect to an existing Railway sandbox, creating a fresh one when
291
+ */
292
+ async _reconnectSandbox(reconnectSandboxId, clientConfig) {
293
+ this.logger.debug(`${LOG_PREFIX} Reconnecting to Railway sandbox ${reconnectSandboxId}...`);
294
+ let connectedSandbox = await railway.Sandbox.connect(reconnectSandboxId, clientConfig);
295
+ if (connectedSandbox.status !== "RUNNING") throw new railway.SandboxNotFoundError({
296
+ id: reconnectSandboxId,
297
+ environmentId: clientConfig.environmentId ?? ""
298
+ });
299
+ return connectedSandbox;
300
+ }
301
+ _clientConfig() {
302
+ return {
303
+ ...this._token !== void 0 && { token: this._token },
304
+ ...this._environmentId !== void 0 && { environmentId: this._environmentId }
305
+ };
306
+ }
307
+ _createOptions(clientConfig) {
308
+ return {
309
+ ...clientConfig,
310
+ ...this._idleTimeoutMinutes !== void 0 && { idleTimeoutMinutes: this._idleTimeoutMinutes },
311
+ ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
312
+ ...Object.keys(this._env).length > 0 && { env: this._env }
313
+ };
314
+ }
315
+ /**
316
+ * Capture the sandbox's checkpoint on demand, outside the idle-timer schedule.
317
+ *
318
+ * Intended for callers (e.g. a factory-side scheduler) that want to refresh
319
+ * the recovery checkpoint at semantic moments — turn end, session-idle,
320
+ * pre-teardown — rather than only just before Railway's idle destroy.
321
+ *
322
+ * Coalesces with any in-flight timer-driven refresh: concurrent callers join
323
+ * the same underlying `Sandbox.checkpoint` call and receive
324
+ * `{ status: 'coalesced', checkpointName }`. Both `captured` and `coalesced`
325
+ * carry the checkpoint name inline so callers can persist a session→
326
+ * checkpoint binding without a second, non-atomic read against the sandbox.
327
+ * Returns `{ status: 'skipped', reason }` when there's nothing to capture
328
+ * (no `checkpointName` configured, or the sandbox isn't running yet).
329
+ *
330
+ * On successful capture, restarts the idle-timer countdown so the next
331
+ * timer-driven refresh is scheduled from this capture.
332
+ *
333
+ * Never captures without a `checkpointName` and never mutates status — safe
334
+ * to invoke concurrently with `executeCommand`, `restart`, or `stop`.
335
+ */
336
+ async captureCheckpoint() {
337
+ const checkpointName = this._checkpointName;
338
+ if (!checkpointName) return {
339
+ status: "skipped",
340
+ reason: "no-checkpoint-name-configured"
341
+ };
342
+ const sandbox = this._sandbox;
343
+ if (!sandbox) return {
344
+ status: "skipped",
345
+ reason: "sandbox-not-running"
346
+ };
347
+ if (this._checkpointRefreshInFlight) {
348
+ await this._checkpointRefreshInFlight;
349
+ return {
350
+ status: "coalesced",
351
+ checkpointName
352
+ };
353
+ }
354
+ const capture = this._checkpointSandbox(sandbox).finally(() => {
355
+ if (this._checkpointRefreshInFlight === capture) this._checkpointRefreshInFlight = null;
356
+ });
357
+ this._checkpointRefreshInFlight = capture;
358
+ await capture;
359
+ this._scheduleCheckpointRefresh();
360
+ return {
361
+ status: "captured",
362
+ checkpointName
363
+ };
364
+ }
365
+ async _checkpointSandbox(sandbox) {
366
+ if (!this._checkpointName) return;
367
+ this.logger.debug(`${LOG_PREFIX} Capturing Railway sandbox checkpoint ${this._checkpointName} for: ${this.id}`);
368
+ if ((await railway.Sandbox.checkpoints(this._clientConfig())).some((checkpoint) => checkpoint.key === this._checkpointName)) await railway.Sandbox.deleteCheckpoint(this._checkpointName, this._clientConfig());
369
+ await sandbox.checkpoint(this._checkpointName);
370
+ }
371
+ _scheduleCheckpointRefresh() {
372
+ if (!this._checkpointName || !this._sandbox) return;
373
+ const idleTimeoutMinutes = this._idleTimeoutMinutes ?? this._sandbox.idleTimeoutMinutes;
374
+ if (!idleTimeoutMinutes) return;
375
+ this._cancelCheckpointRefresh();
376
+ const delayMs = Math.max(1e3, idleTimeoutMinutes * 6e4 - CHECKPOINT_REFRESH_MARGIN_MS);
377
+ this._checkpointRefreshTimer = setTimeout(() => {
378
+ this._checkpointRefreshTimer = null;
379
+ const sandbox = this._sandbox;
380
+ if (!sandbox || this._checkpointRefreshInFlight) return;
381
+ const refresh = this._checkpointSandbox(sandbox).finally(() => {
382
+ if (this._checkpointRefreshInFlight === refresh) this._checkpointRefreshInFlight = null;
383
+ });
384
+ this._checkpointRefreshInFlight = refresh;
385
+ refresh.catch((error) => {
386
+ this.logger.warn(`${LOG_PREFIX} Failed to refresh Railway sandbox checkpoint ${this._checkpointName}:`, error);
387
+ });
388
+ }, delayMs);
389
+ this._checkpointRefreshTimer.unref?.();
390
+ }
391
+ _cancelCheckpointRefresh() {
392
+ if (this._checkpointRefreshTimer) {
393
+ clearTimeout(this._checkpointRefreshTimer);
394
+ this._checkpointRefreshTimer = null;
395
+ }
396
+ }
397
+ /**
398
+ * Stop the Railway sandbox.
399
+ *
400
+ * Railway sandboxes have no separate "stopped" state — they're either
401
+ * running or destroyed — so stopping destroys the sandbox but we keep a checkpoint of the filesystem.
402
+ */
403
+ async stop() {
404
+ if (this._checkpointName) await this._flushCheckpointRefresh().catch((error) => {
405
+ this.logger.warn(`${LOG_PREFIX} Failed to checkpoint Railway sandbox ${this._sandbox?.id}:`, error);
406
+ });
407
+ await this._teardown();
408
+ }
409
+ /**
410
+ * Destroy the Railway sandbox and release its resources including the checkpoint.
411
+ */
412
+ async destroy() {
413
+ this._cancelCheckpointRefresh();
414
+ if (this._checkpointName) {
415
+ await this._checkpointRefreshInFlight;
416
+ await railway.Sandbox.deleteCheckpoint(this._checkpointName, this._clientConfig()).catch((error) => {
417
+ this.logger.warn(`${LOG_PREFIX} Failed to delete Railway checkpoint ${this._checkpointName}:`, error);
418
+ });
419
+ }
420
+ await this._teardown();
421
+ }
422
+ async _flushCheckpointRefresh() {
423
+ this._cancelCheckpointRefresh();
424
+ if (this._checkpointRefreshInFlight) {
425
+ await this._checkpointRefreshInFlight;
426
+ return;
427
+ }
428
+ if (this._sandbox) await this._checkpointSandbox(this._sandbox);
429
+ }
430
+ async _teardown() {
431
+ this._cancelCheckpointRefresh();
432
+ if (!this._sandbox) return;
433
+ await this._checkpointRefreshInFlight;
434
+ const sandbox = this._sandbox;
435
+ this._sandbox = null;
436
+ try {
437
+ await sandbox.destroy();
438
+ } catch (error) {
439
+ this.logger.warn(`${LOG_PREFIX} Failed to destroy Railway sandbox ${sandbox.id}:`, error);
440
+ }
441
+ }
442
+ /**
443
+ * Fork this running sandbox into a new, independent `RailwaySandbox`.
444
+ *
445
+ * Clones the filesystem (a fresh boot, not live processes) into the same
446
+ * environment. The returned sandbox is already started and reattached to the
447
+ * forked Railway sandbox; it inherits this sandbox's credentials and defaults
448
+ * unless overridden via `options`.
449
+ *
450
+ * @throws {SandboxNotReadyError} If this sandbox has not been started.
451
+ */
452
+ async fork(options = {}) {
453
+ await this.railway.fork({
454
+ ...options.idleTimeoutMinutes !== void 0 && { idleTimeoutMinutes: options.idleTimeoutMinutes },
455
+ ...options.networkIsolation !== void 0 && { networkIsolation: options.networkIsolation },
456
+ ...options.env !== void 0 && { env: options.env }
457
+ });
458
+ const child = new RailwaySandbox({
459
+ ...options.id !== void 0 && { id: options.id },
460
+ ...this._token !== void 0 && { token: this._token },
461
+ ...this._environmentId !== void 0 && { environmentId: this._environmentId },
462
+ idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
463
+ networkIsolation: options.networkIsolation ?? this._networkIsolation,
464
+ env: options.env ?? this._env,
465
+ timeout: this._timeout
466
+ });
467
+ await child._start();
468
+ return child;
469
+ }
470
+ /**
471
+ * Construct a sibling `RailwaySandbox` that inherits this sandbox's
472
+ * credentials and defaults (token, environment, checkpoint, network
473
+ * isolation, timeout, template, instructions) with per-instance overrides.
474
+ *
475
+ * Unlike {@link fork}, `clone` performs no I/O and does not require this
476
+ * sandbox to be started — the returned sandbox is not started and provisions
477
+ * (or reattaches, when `sandboxId` is set) on its own `start()`. Use it when
478
+ * one configured sandbox acts as the template for a fleet of independent
479
+ * sandboxes (e.g. one per project).
480
+ */
481
+ clone(options = {}) {
482
+ return new RailwaySandbox({
483
+ ...options.id !== void 0 && { id: options.id },
484
+ ...this._token !== void 0 && { token: this._token },
485
+ ...this._environmentId !== void 0 && { environmentId: this._environmentId },
486
+ ...(options.checkpointName ?? this._checkpointName) !== void 0 && { checkpointName: options.checkpointName ?? this._checkpointName },
487
+ idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
488
+ ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
489
+ env: options.env ?? this._env,
490
+ ...this._templateOption !== void 0 && { template: this._templateOption },
491
+ ...this._timeout !== void 0 && { timeout: this._timeout },
492
+ ...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride }
493
+ });
494
+ }
495
+ /**
496
+ * Whether a Railway API token was resolved at construction (explicit option
497
+ * or the `RAILWAY_API_TOKEN` env fallback). Lets callers gate features on a
498
+ * usable configuration without provisioning a sandbox.
499
+ */
500
+ get hasCredentials() {
501
+ return this._token !== void 0 && this._token !== "";
502
+ }
503
+ /** The configured idle teardown window in minutes, if any. */
504
+ get idleTimeoutMinutes() {
505
+ return this._idleTimeoutMinutes;
506
+ }
507
+ async getInfo() {
508
+ return {
509
+ id: this.id,
510
+ name: this.name,
511
+ provider: this.provider,
512
+ status: this.status,
513
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
514
+ metadata: { ...this._sandbox && {
515
+ railwaySandboxId: this._sandbox.id,
516
+ environmentId: this._sandbox.environmentId,
517
+ region: this._sandbox.region,
518
+ networkIsolation: this._sandbox.networkIsolation,
519
+ ...this._sandbox.idleTimeoutMinutes != null && { idleTimeoutMinutes: this._sandbox.idleTimeoutMinutes }
520
+ } }
521
+ };
522
+ }
523
+ getInstructions() {
524
+ const defaultInstructions = this._buildDefaultInstructions();
525
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
526
+ if (typeof this._instructionsOverride === "function") return this._instructionsOverride({ defaultInstructions });
527
+ return defaultInstructions;
528
+ }
529
+ _buildDefaultInstructions() {
530
+ const parts = [];
531
+ parts.push("Railway cloud sandbox: an isolated Debian Linux VM with outbound internet access.");
532
+ if (this._networkIsolation === "PRIVATE") parts.push("Joined to the environment private network.");
533
+ if (this._timeout !== void 0) parts.push(`Default command timeout: ${Math.ceil(this._timeout / 1e3)}s.`);
534
+ else parts.push("Commands run until they exit unless a timeout is set.");
535
+ if (this._idleTimeoutMinutes !== void 0) parts.push(`Idle timeout: ${this._idleTimeoutMinutes} minute(s).`);
536
+ return parts.join(" ");
537
+ }
538
+ /**
539
+ * Execute a command in the sandbox and return the result.
540
+ */
541
+ async executeCommand(command, args = [], options = {}) {
542
+ if (this._sandbox?.status !== "RUNNING") {
543
+ if (!this._checkpointName) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
544
+ this._sandbox = null;
545
+ await this.start();
546
+ }
547
+ const fullCommand = args.length > 0 ? `${command} ${args.map(shellQuote).join(" ")}` : command;
548
+ const timeout = options.timeout ?? this._timeout;
549
+ const env = options.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
550
+ const startedAt = Date.now();
551
+ const result = await this.railway.exec(fullCommand, {
552
+ ...timeout !== void 0 && { timeoutSec: Math.ceil(timeout / 1e3) },
553
+ ...options.cwd !== void 0 && { cwd: options.cwd },
554
+ ...env !== void 0 && { env }
555
+ });
556
+ const exitCode = result.exitCode ?? -1;
557
+ return {
558
+ success: exitCode === 0,
559
+ exitCode,
560
+ stdout: result.stdout,
561
+ stderr: result.stderr,
562
+ executionTimeMs: Date.now() - startedAt,
563
+ command,
564
+ args,
565
+ timedOut: result.timedOut
566
+ };
567
+ }
611
568
  };
612
-
613
- // src/provider.ts
614
- var railwaySandboxProvider = {
615
- id: "railway",
616
- name: "Railway Sandbox",
617
- description: "Ephemeral, isolated Linux VM powered by Railway",
618
- configSchema: {
619
- type: "object",
620
- properties: {
621
- token: { type: "string", description: "Railway API token (falls back to RAILWAY_API_TOKEN)" },
622
- environmentId: {
623
- type: "string",
624
- description: "Railway environment ID (falls back to RAILWAY_ENVIRONMENT_ID)"
625
- },
626
- sandboxId: { type: "string", description: "Reattach to an existing Railway sandbox by ID" },
627
- idleTimeoutMinutes: {
628
- type: "number",
629
- description: "Minutes a sandbox can sit idle before Railway destroys it"
630
- },
631
- networkIsolation: {
632
- type: "string",
633
- description: "Network isolation mode",
634
- enum: ["ISOLATED", "PRIVATE"],
635
- default: "ISOLATED"
636
- },
637
- env: {
638
- type: "object",
639
- description: "Environment variables",
640
- additionalProperties: { type: "string" }
641
- },
642
- timeout: { type: "number", description: "Default command timeout in ms" }
643
- }
644
- },
645
- createSandbox: (config) => new RailwaySandbox(config)
569
+ //#endregion
570
+ //#region src/provider.ts
571
+ const railwaySandboxProvider = {
572
+ id: "railway",
573
+ name: "Railway Sandbox",
574
+ description: "Ephemeral, isolated Linux VM powered by Railway",
575
+ configSchema: {
576
+ type: "object",
577
+ properties: {
578
+ token: {
579
+ type: "string",
580
+ description: "Railway API token (falls back to RAILWAY_API_TOKEN)"
581
+ },
582
+ environmentId: {
583
+ type: "string",
584
+ description: "Railway environment ID (falls back to RAILWAY_ENVIRONMENT_ID)"
585
+ },
586
+ sandboxId: {
587
+ type: "string",
588
+ description: "Reattach to an existing Railway sandbox by ID"
589
+ },
590
+ idleTimeoutMinutes: {
591
+ type: "number",
592
+ description: "Minutes a sandbox can sit idle before Railway destroys it"
593
+ },
594
+ networkIsolation: {
595
+ type: "string",
596
+ description: "Network isolation mode",
597
+ enum: ["ISOLATED", "PRIVATE"],
598
+ default: "ISOLATED"
599
+ },
600
+ env: {
601
+ type: "object",
602
+ description: "Environment variables",
603
+ additionalProperties: { type: "string" }
604
+ },
605
+ timeout: {
606
+ type: "number",
607
+ description: "Default command timeout in ms"
608
+ }
609
+ }
610
+ },
611
+ createSandbox: (config) => new RailwaySandbox(config)
646
612
  };
647
-
648
- Object.defineProperty(exports, "SandboxFileNotFoundError", {
649
- enumerable: true,
650
- get: function () { return railway.SandboxFileNotFoundError; }
651
- });
613
+ //#endregion
652
614
  exports.RailwayProcessManager = RailwayProcessManager;
653
615
  exports.RailwaySandbox = RailwaySandbox;
616
+ Object.defineProperty(exports, "SandboxFileNotFoundError", {
617
+ enumerable: true,
618
+ get: function() {
619
+ return railway.SandboxFileNotFoundError;
620
+ }
621
+ });
654
622
  exports.railwaySandboxProvider = railwaySandboxProvider;
655
- //# sourceMappingURL=index.cjs.map
623
+
656
624
  //# sourceMappingURL=index.cjs.map