@mastra/railway 0.4.0 → 0.4.1-alpha.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,656 +1,715 @@
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
+ _token;
198
+ _environmentId;
199
+ _sandboxId;
200
+ _checkpointName;
201
+ _idleTimeoutMinutes;
202
+ _networkIsolation;
203
+ _env;
204
+ _timeout;
205
+ _instructionsOverride;
206
+ _templateOption;
207
+ constructor(options = {}) {
208
+ super({
209
+ ...options,
210
+ name: "RailwaySandbox",
211
+ processes: new RailwayProcessManager({ env: options.env })
212
+ });
213
+ this.id = options.id ?? this.generateId();
214
+ this._token = options.token ?? process.env.RAILWAY_API_TOKEN;
215
+ this._environmentId = options.environmentId ?? process.env.RAILWAY_ENVIRONMENT_ID;
216
+ this._sandboxId = options.sandboxId;
217
+ this._checkpointName = options.checkpointName;
218
+ this._idleTimeoutMinutes = options.idleTimeoutMinutes;
219
+ this._networkIsolation = options.networkIsolation;
220
+ this._env = options.env ?? {};
221
+ this._timeout = options.timeout;
222
+ this._instructionsOverride = options.instructions;
223
+ this._templateOption = options.template;
224
+ }
225
+ generateId() {
226
+ return `railway-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
227
+ }
228
+ /**
229
+ * Get the underlying Railway Sandbox instance for direct SDK access.
230
+ *
231
+ * @throws {SandboxNotReadyError} If the sandbox has not been started.
232
+ */
233
+ get railway() {
234
+ if (!this._sandbox) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
235
+ return this._sandbox;
236
+ }
237
+ /**
238
+ * Start the Railway sandbox.
239
+ *
240
+ * Reattaches to an existing sandbox when `sandboxId` is configured,
241
+ * otherwise provisions a new one. Resolves once the sandbox is RUNNING.
242
+ */
243
+ async start() {
244
+ if (this._sandbox) return;
245
+ await this._startRailwaySandbox({
246
+ reconnectSandboxId: this._sandboxId,
247
+ fallbackToCreate: false
248
+ });
249
+ }
250
+ async restart() {
251
+ const reconnectSandboxId = this._sandbox?.id ?? this._sandboxId;
252
+ this._cancelCheckpointRefresh();
253
+ await this._checkpointRefreshInFlight?.catch((error) => {
254
+ this.logger.warn(`${LOG_PREFIX} Failed to flush in-flight checkpoint before restart:`, error);
255
+ });
256
+ this._sandbox = null;
257
+ this._createdAt = null;
258
+ this.status = "starting";
259
+ try {
260
+ await this._startRailwaySandbox({
261
+ reconnectSandboxId,
262
+ fallbackToCreate: true
263
+ });
264
+ this.status = "running";
265
+ } catch (error) {
266
+ this.status = "error";
267
+ throw error;
268
+ }
269
+ }
270
+ async withRestartRetry(operation) {
271
+ await this.ensureRunning();
272
+ try {
273
+ return await operation();
274
+ } catch (error) {
275
+ if (!this.isSandboxUnavailableError(error)) throw error;
276
+ await this.restart();
277
+ return await operation();
278
+ } finally {
279
+ this._scheduleCheckpointRefresh();
280
+ }
281
+ }
282
+ async _startRailwaySandbox({ reconnectSandboxId, fallbackToCreate }) {
283
+ const clientConfig = this._clientConfig();
284
+ const createOptions = this._createOptions(clientConfig);
285
+ this._sandbox = reconnectSandboxId ? await this._reconnectSandbox(reconnectSandboxId, fallbackToCreate, clientConfig, createOptions) : await this._createNewSandbox(createOptions);
286
+ this._createdAt = this._sandbox.createdAt ? new Date(this._sandbox.createdAt) : /* @__PURE__ */ new Date();
287
+ this.logger.debug(`${LOG_PREFIX} Railway sandbox ${this._sandbox.id} ready for logical ID: ${this.id}`);
288
+ this._scheduleCheckpointRefresh();
289
+ }
290
+ /**
291
+ * Reconnect to an existing Railway sandbox, creating a fresh one when
292
+ * `fallbackToCreate` is set and the sandbox is unavailable or not running.
293
+ */
294
+ async _reconnectSandbox(reconnectSandboxId, fallbackToCreate, clientConfig, createOptions) {
295
+ this.logger.debug(`${LOG_PREFIX} Reconnecting to Railway sandbox ${reconnectSandboxId}...`);
296
+ let connectedSandbox;
297
+ try {
298
+ connectedSandbox = await railway.Sandbox.connect(reconnectSandboxId, clientConfig);
299
+ } catch (error) {
300
+ if (!fallbackToCreate || !this.isSandboxUnavailableError(error)) throw error;
301
+ return this._createNewSandbox(createOptions);
302
+ }
303
+ if (connectedSandbox.status === "RUNNING") return connectedSandbox;
304
+ if (!fallbackToCreate) throw new Error(`Railway sandbox ${reconnectSandboxId} is not running (status: ${connectedSandbox.status})`);
305
+ return this._createNewSandbox(createOptions);
306
+ }
307
+ _clientConfig() {
308
+ return {
309
+ ...this._token !== void 0 && { token: this._token },
310
+ ...this._environmentId !== void 0 && { environmentId: this._environmentId }
311
+ };
312
+ }
313
+ _createOptions(clientConfig) {
314
+ return {
315
+ ...clientConfig,
316
+ ...this._idleTimeoutMinutes !== void 0 && { idleTimeoutMinutes: this._idleTimeoutMinutes },
317
+ ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
318
+ ...Object.keys(this._env).length > 0 && { env: this._env }
319
+ };
320
+ }
321
+ async _createNewSandbox(createOptions) {
322
+ const checkpointSandbox = await this._tryCreateFromCheckpoint(createOptions);
323
+ if (checkpointSandbox) return checkpointSandbox;
324
+ if (this._templateOption) {
325
+ const template = this._resolveTemplate();
326
+ this.logger.debug(`${LOG_PREFIX} Creating Railway sandbox from template for: ${this.id}`);
327
+ const sandbox = await railway.Sandbox.create(template, createOptions);
328
+ await this._checkpointSandbox(sandbox);
329
+ return sandbox;
330
+ }
331
+ this.logger.debug(`${LOG_PREFIX} Creating Railway sandbox for: ${this.id}`);
332
+ const sandbox = await railway.Sandbox.create(createOptions);
333
+ await this._checkpointSandbox(sandbox);
334
+ return sandbox;
335
+ }
336
+ async _tryCreateFromCheckpoint(createOptions) {
337
+ if (!this._checkpointName) return;
338
+ this.logger.debug(`${LOG_PREFIX} Creating Railway sandbox from checkpoint ${this._checkpointName} for: ${this.id}`);
339
+ try {
340
+ return await railway.Sandbox.create(this._checkpointName, createOptions);
341
+ } catch (error) {
342
+ if (!this.isCheckpointUnavailableError(error)) throw error;
343
+ return;
344
+ }
345
+ }
346
+ async _checkpointSandbox(sandbox) {
347
+ if (!this._checkpointName) return;
348
+ try {
349
+ this.logger.debug(`${LOG_PREFIX} Capturing Railway sandbox checkpoint ${this._checkpointName} for: ${this.id}`);
350
+ await sandbox.checkpoint(this._checkpointName);
351
+ } catch (error) {
352
+ if (!this.isCheckpointAlreadyExistsError(error)) throw error;
353
+ await this._deleteCheckpointByName(this._checkpointName);
354
+ await sandbox.checkpoint(this._checkpointName);
355
+ }
356
+ }
357
+ async _deleteCheckpointByName(name) {
358
+ try {
359
+ const checkpoint = (await railway.Sandbox.checkpoints(this._clientConfig())).find((checkpoint) => checkpoint.key === name);
360
+ if (!checkpoint) return;
361
+ await railway.Sandbox.deleteCheckpoint(checkpoint.id, this._clientConfig());
362
+ } catch (error) {
363
+ if (!this.isCheckpointUnavailableError(error)) throw error;
364
+ }
365
+ }
366
+ _scheduleCheckpointRefresh() {
367
+ if (!this._checkpointName || !this._sandbox) return;
368
+ const idleTimeoutMinutes = this._idleTimeoutMinutes ?? this._sandbox.idleTimeoutMinutes;
369
+ if (!idleTimeoutMinutes) return;
370
+ if (this._checkpointRefreshTimer) clearTimeout(this._checkpointRefreshTimer);
371
+ const delayMs = Math.max(1e3, idleTimeoutMinutes * 6e4 - CHECKPOINT_REFRESH_MARGIN_MS);
372
+ this._checkpointRefreshTimer = setTimeout(() => {
373
+ this._checkpointRefreshTimer = null;
374
+ const sandbox = this._sandbox;
375
+ if (!sandbox) return;
376
+ const refresh = this._checkpointSandbox(sandbox).finally(() => {
377
+ if (this._checkpointRefreshInFlight === refresh) this._checkpointRefreshInFlight = null;
378
+ });
379
+ this._checkpointRefreshInFlight = refresh;
380
+ this._checkpointRefreshInFlight.catch((error) => {
381
+ this.logger.warn(`${LOG_PREFIX} Failed to refresh Railway sandbox checkpoint ${this._checkpointName}:`, error);
382
+ });
383
+ }, delayMs);
384
+ this._checkpointRefreshTimer.unref?.();
385
+ }
386
+ _cancelCheckpointRefresh() {
387
+ if (this._checkpointRefreshTimer) {
388
+ clearTimeout(this._checkpointRefreshTimer);
389
+ this._checkpointRefreshTimer = null;
390
+ }
391
+ }
392
+ async _flushCheckpointRefresh() {
393
+ this._cancelCheckpointRefresh();
394
+ if (this._checkpointRefreshInFlight) {
395
+ await this._checkpointRefreshInFlight;
396
+ return;
397
+ }
398
+ if (this._sandbox) await this._checkpointSandbox(this._sandbox);
399
+ }
400
+ /**
401
+ * Capture the sandbox's checkpoint on demand, outside the idle-timer schedule.
402
+ *
403
+ * Intended for callers (e.g. a factory-side scheduler) that want to refresh
404
+ * the recovery checkpoint at semantic moments — turn end, session-idle,
405
+ * pre-teardown — rather than only just before Railway's idle destroy.
406
+ *
407
+ * Coalesces with any in-flight timer-driven refresh: concurrent callers join
408
+ * the same underlying `Sandbox.checkpoint` call and receive
409
+ * `{ status: 'coalesced', checkpointName }`. Both `captured` and `coalesced`
410
+ * carry the checkpoint name inline so callers can persist a session→
411
+ * checkpoint binding without a second, non-atomic read against the sandbox.
412
+ * Returns `{ status: 'skipped', reason }` when there's nothing to capture
413
+ * (no `checkpointName` configured, or the sandbox isn't running yet).
414
+ *
415
+ * On successful capture, restarts the idle-timer countdown so the next
416
+ * timer-driven refresh is scheduled from this capture.
417
+ *
418
+ * Never captures without a `checkpointName` and never mutates status — safe
419
+ * to invoke concurrently with `executeCommand`, `restart`, or `stop`.
420
+ */
421
+ async captureCheckpoint() {
422
+ const checkpointName = this._checkpointName;
423
+ if (!checkpointName) return {
424
+ status: "skipped",
425
+ reason: "no-checkpoint-name-configured"
426
+ };
427
+ const sandbox = this._sandbox;
428
+ if (!sandbox) return {
429
+ status: "skipped",
430
+ reason: "sandbox-not-running"
431
+ };
432
+ if (this._checkpointRefreshInFlight) {
433
+ await this._checkpointRefreshInFlight;
434
+ return {
435
+ status: "coalesced",
436
+ checkpointName
437
+ };
438
+ }
439
+ const capture = this._checkpointSandbox(sandbox).finally(() => {
440
+ if (this._checkpointRefreshInFlight === capture) this._checkpointRefreshInFlight = null;
441
+ });
442
+ this._checkpointRefreshInFlight = capture;
443
+ await capture;
444
+ this._scheduleCheckpointRefresh();
445
+ return {
446
+ status: "captured",
447
+ checkpointName
448
+ };
449
+ }
450
+ isCheckpointUnavailableError(error) {
451
+ if (!(error instanceof Error)) return false;
452
+ const message = error.message.toLowerCase();
453
+ return message.includes("checkpoint") && [
454
+ "not found",
455
+ "does not exist",
456
+ "missing",
457
+ "unknown",
458
+ "no checkpoint"
459
+ ].some((phrase) => message.includes(phrase));
460
+ }
461
+ isCheckpointAlreadyExistsError(error) {
462
+ if (!(error instanceof Error)) return false;
463
+ const message = error.message.toLowerCase();
464
+ return message.includes("checkpoint") && ([
465
+ "already exists",
466
+ "must be unused",
467
+ "unique"
468
+ ].some((phrase) => message.includes(phrase)) || message.includes("name") && message.includes("used"));
469
+ }
470
+ isSandboxUnavailableError(error, seen = /* @__PURE__ */ new Set()) {
471
+ if (error && typeof error === "object") {
472
+ if (seen.has(error)) return false;
473
+ seen.add(error);
474
+ }
475
+ if (error instanceof railway.SandboxNotFoundError || error instanceof railway.SandboxFailedError || error instanceof railway.SandboxTimeoutError && error.resource === "sandbox") return true;
476
+ if (error && typeof error === "object") {
477
+ const errorLike = error;
478
+ const name = typeof errorLike.name === "string" ? errorLike.name : "";
479
+ const message = typeof errorLike.message === "string" ? errorLike.message.toLowerCase() : "";
480
+ if (name === "SandboxNotFoundError" || name === "SandboxFailedError" || name === "SandboxTimeoutError" && errorLike.resource === "sandbox") return true;
481
+ if (message.includes("sandbox") && [
482
+ "not found",
483
+ "destroyed",
484
+ "failed",
485
+ "not running",
486
+ "unavailable"
487
+ ].some((phrase) => message.includes(phrase))) return true;
488
+ if (errorLike.cause !== void 0) return this.isSandboxUnavailableError(errorLike.cause, seen);
489
+ }
490
+ return false;
491
+ }
492
+ /**
493
+ * Stop the Railway sandbox.
494
+ *
495
+ * Railway sandboxes have no separate "stopped" state — they're either
496
+ * running or destroyed — so stopping destroys the sandbox.
497
+ */
498
+ async stop() {
499
+ await this._teardown();
500
+ }
501
+ /**
502
+ * Destroy the Railway sandbox and release its resources.
503
+ */
504
+ async destroy() {
505
+ await this._teardown();
506
+ }
507
+ async _teardown() {
508
+ if (!this._sandbox) {
509
+ this._cancelCheckpointRefresh();
510
+ return;
511
+ }
512
+ const sandbox = this._sandbox;
513
+ try {
514
+ await this._flushCheckpointRefresh();
515
+ } catch (error) {
516
+ this.logger.warn(`${LOG_PREFIX} Failed to flush checkpoint before teardown:`, error);
517
+ }
518
+ this._sandbox = null;
519
+ try {
520
+ await sandbox.destroy();
521
+ } catch (error) {
522
+ this.logger.warn(`${LOG_PREFIX} Failed to destroy Railway sandbox ${sandbox.id}:`, error);
523
+ }
524
+ }
525
+ /**
526
+ * Resolve the configured template into a `SandboxTemplate` that Railway
527
+ * builds during `Sandbox.create()`. Accepts either a pre-built
528
+ * `SandboxTemplate` or a builder callback over `Sandbox.template()`.
529
+ */
530
+ _resolveTemplate() {
531
+ const option = this._templateOption;
532
+ return typeof option === "function" ? option(railway.Sandbox.template()) : option;
533
+ }
534
+ /**
535
+ * Fork this running sandbox into a new, independent `RailwaySandbox`.
536
+ *
537
+ * Clones the filesystem (a fresh boot, not live processes) into the same
538
+ * environment. The returned sandbox is already started and reattached to the
539
+ * forked Railway sandbox; it inherits this sandbox's credentials and defaults
540
+ * unless overridden via `options`.
541
+ *
542
+ * @throws {SandboxNotReadyError} If this sandbox has not been started.
543
+ */
544
+ async fork(options = {}) {
545
+ const forked = await this.railway.fork({
546
+ ...options.idleTimeoutMinutes !== void 0 && { idleTimeoutMinutes: options.idleTimeoutMinutes },
547
+ ...options.networkIsolation !== void 0 && { networkIsolation: options.networkIsolation },
548
+ ...options.env !== void 0 && { env: options.env }
549
+ });
550
+ const child = new RailwaySandbox({
551
+ ...options.id !== void 0 && { id: options.id },
552
+ ...this._token !== void 0 && { token: this._token },
553
+ ...this._environmentId !== void 0 && { environmentId: this._environmentId },
554
+ sandboxId: forked.id,
555
+ idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
556
+ networkIsolation: options.networkIsolation ?? this._networkIsolation,
557
+ env: options.env ?? this._env,
558
+ timeout: this._timeout
559
+ });
560
+ await child._start();
561
+ return child;
562
+ }
563
+ /**
564
+ * Construct a sibling `RailwaySandbox` that inherits this sandbox's
565
+ * credentials and defaults (token, environment, checkpoint, network
566
+ * isolation, timeout, template, instructions) with per-instance overrides.
567
+ *
568
+ * Unlike {@link fork}, `clone` performs no I/O and does not require this
569
+ * sandbox to be started — the returned sandbox is not started and provisions
570
+ * (or reattaches, when `sandboxId` is set) on its own `start()`. Use it when
571
+ * one configured sandbox acts as the template for a fleet of independent
572
+ * sandboxes (e.g. one per project).
573
+ */
574
+ clone(options = {}) {
575
+ return new RailwaySandbox({
576
+ ...options.id !== void 0 && { id: options.id },
577
+ ...this._token !== void 0 && { token: this._token },
578
+ ...this._environmentId !== void 0 && { environmentId: this._environmentId },
579
+ ...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
580
+ ...(options.checkpointName ?? this._checkpointName) !== void 0 && { checkpointName: options.checkpointName ?? this._checkpointName },
581
+ idleTimeoutMinutes: options.idleTimeoutMinutes ?? this._idleTimeoutMinutes,
582
+ ...this._networkIsolation !== void 0 && { networkIsolation: this._networkIsolation },
583
+ env: options.env ?? this._env,
584
+ ...this._templateOption !== void 0 && { template: this._templateOption },
585
+ ...this._timeout !== void 0 && { timeout: this._timeout },
586
+ ...this._instructionsOverride !== void 0 && { instructions: this._instructionsOverride }
587
+ });
588
+ }
589
+ /**
590
+ * Whether a Railway API token was resolved at construction (explicit option
591
+ * or the `RAILWAY_API_TOKEN` env fallback). Lets callers gate features on a
592
+ * usable configuration without provisioning a sandbox.
593
+ */
594
+ get hasCredentials() {
595
+ return this._token !== void 0 && this._token !== "";
596
+ }
597
+ /** The configured idle teardown window in minutes, if any. */
598
+ get idleTimeoutMinutes() {
599
+ return this._idleTimeoutMinutes;
600
+ }
601
+ async getInfo() {
602
+ return {
603
+ id: this.id,
604
+ name: this.name,
605
+ provider: this.provider,
606
+ status: this.status,
607
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
608
+ metadata: { ...this._sandbox && {
609
+ railwaySandboxId: this._sandbox.id,
610
+ environmentId: this._sandbox.environmentId,
611
+ region: this._sandbox.region,
612
+ networkIsolation: this._sandbox.networkIsolation,
613
+ ...this._sandbox.idleTimeoutMinutes != null && { idleTimeoutMinutes: this._sandbox.idleTimeoutMinutes }
614
+ } }
615
+ };
616
+ }
617
+ getInstructions() {
618
+ const defaultInstructions = this._buildDefaultInstructions();
619
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
620
+ if (typeof this._instructionsOverride === "function") return this._instructionsOverride({ defaultInstructions });
621
+ return defaultInstructions;
622
+ }
623
+ _buildDefaultInstructions() {
624
+ const parts = [];
625
+ parts.push("Railway cloud sandbox: an isolated Debian Linux VM with outbound internet access.");
626
+ if (this._networkIsolation === "PRIVATE") parts.push("Joined to the environment private network.");
627
+ if (this._timeout !== void 0) parts.push(`Default command timeout: ${Math.ceil(this._timeout / 1e3)}s.`);
628
+ else parts.push("Commands run until they exit unless a timeout is set.");
629
+ if (this._idleTimeoutMinutes !== void 0) parts.push(`Idle timeout: ${this._idleTimeoutMinutes} minute(s).`);
630
+ return parts.join(" ");
631
+ }
632
+ /**
633
+ * Execute a command in the sandbox and return the result.
634
+ */
635
+ async executeCommand(command, args = [], options = {}) {
636
+ return this.withRestartRetry(async () => {
637
+ const fullCommand = args.length > 0 ? `${command} ${args.map(shellQuote).join(" ")}` : command;
638
+ const timeout = options.timeout ?? this._timeout;
639
+ const env = options.env ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== void 0)) : void 0;
640
+ const startedAt = Date.now();
641
+ const result = await this.railway.exec(fullCommand, {
642
+ ...timeout !== void 0 && { timeoutSec: Math.ceil(timeout / 1e3) },
643
+ ...options.cwd !== void 0 && { cwd: options.cwd },
644
+ ...env !== void 0 && { env }
645
+ });
646
+ const exitCode = result.exitCode ?? -1;
647
+ return {
648
+ success: exitCode === 0,
649
+ exitCode,
650
+ stdout: result.stdout,
651
+ stderr: result.stderr,
652
+ executionTimeMs: Date.now() - startedAt,
653
+ command,
654
+ args,
655
+ timedOut: result.timedOut
656
+ };
657
+ });
658
+ }
611
659
  };
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)
660
+ //#endregion
661
+ //#region src/provider.ts
662
+ const railwaySandboxProvider = {
663
+ id: "railway",
664
+ name: "Railway Sandbox",
665
+ description: "Ephemeral, isolated Linux VM powered by Railway",
666
+ configSchema: {
667
+ type: "object",
668
+ properties: {
669
+ token: {
670
+ type: "string",
671
+ description: "Railway API token (falls back to RAILWAY_API_TOKEN)"
672
+ },
673
+ environmentId: {
674
+ type: "string",
675
+ description: "Railway environment ID (falls back to RAILWAY_ENVIRONMENT_ID)"
676
+ },
677
+ sandboxId: {
678
+ type: "string",
679
+ description: "Reattach to an existing Railway sandbox by ID"
680
+ },
681
+ idleTimeoutMinutes: {
682
+ type: "number",
683
+ description: "Minutes a sandbox can sit idle before Railway destroys it"
684
+ },
685
+ networkIsolation: {
686
+ type: "string",
687
+ description: "Network isolation mode",
688
+ enum: ["ISOLATED", "PRIVATE"],
689
+ default: "ISOLATED"
690
+ },
691
+ env: {
692
+ type: "object",
693
+ description: "Environment variables",
694
+ additionalProperties: { type: "string" }
695
+ },
696
+ timeout: {
697
+ type: "number",
698
+ description: "Default command timeout in ms"
699
+ }
700
+ }
701
+ },
702
+ createSandbox: (config) => new RailwaySandbox(config)
646
703
  };
647
-
648
- Object.defineProperty(exports, "SandboxFileNotFoundError", {
649
- enumerable: true,
650
- get: function () { return railway.SandboxFileNotFoundError; }
651
- });
704
+ //#endregion
652
705
  exports.RailwayProcessManager = RailwayProcessManager;
653
706
  exports.RailwaySandbox = RailwaySandbox;
707
+ Object.defineProperty(exports, "SandboxFileNotFoundError", {
708
+ enumerable: true,
709
+ get: function() {
710
+ return railway.SandboxFileNotFoundError;
711
+ }
712
+ });
654
713
  exports.railwaySandboxProvider = railwaySandboxProvider;
655
- //# sourceMappingURL=index.cjs.map
714
+
656
715
  //# sourceMappingURL=index.cjs.map