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