@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/CHANGELOG.md +51 -0
- package/dist/index.cjs +705 -646
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +696 -639
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +41 -0
- package/dist/sandbox/index.d.ts.map +1 -1
- package/package.json +9 -8
package/dist/index.js
CHANGED
|
@@ -1,649 +1,706 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Sandbox,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
+
if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
|
|
12
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
11
13
|
}
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|