@prized/sdk 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +666 -2
  3. package/dist/cjs/commands.d.ts +199 -0
  4. package/dist/cjs/commands.js +597 -0
  5. package/dist/cjs/errors.d.ts +113 -0
  6. package/dist/cjs/errors.js +306 -0
  7. package/dist/cjs/files.d.ts +101 -0
  8. package/dist/cjs/files.js +387 -0
  9. package/dist/cjs/http.d.ts +226 -0
  10. package/dist/cjs/http.js +618 -0
  11. package/dist/cjs/index.d.ts +12 -0
  12. package/dist/cjs/index.js +23 -0
  13. package/dist/cjs/outcome.d.ts +76 -0
  14. package/dist/cjs/outcome.js +170 -0
  15. package/dist/cjs/output.d.ts +70 -0
  16. package/dist/cjs/output.js +383 -0
  17. package/dist/cjs/package.json +1 -0
  18. package/dist/cjs/sandbox.d.ts +218 -0
  19. package/dist/cjs/sandbox.js +836 -0
  20. package/dist/cjs/shell.d.ts +152 -0
  21. package/dist/cjs/shell.js +393 -0
  22. package/dist/cjs/toolbox.d.ts +48 -0
  23. package/dist/cjs/toolbox.js +88 -0
  24. package/dist/cjs/types.d.ts +111 -0
  25. package/dist/cjs/types.js +102 -0
  26. package/dist/cjs/version.d.ts +2 -0
  27. package/dist/cjs/version.js +5 -0
  28. package/dist/esm/commands.d.ts +199 -0
  29. package/dist/esm/commands.js +590 -0
  30. package/dist/esm/errors.d.ts +113 -0
  31. package/dist/esm/errors.js +297 -0
  32. package/dist/esm/files.d.ts +101 -0
  33. package/dist/esm/files.js +381 -0
  34. package/dist/esm/http.d.ts +226 -0
  35. package/dist/esm/http.js +601 -0
  36. package/dist/esm/index.d.ts +12 -0
  37. package/dist/esm/index.js +7 -0
  38. package/dist/esm/outcome.d.ts +76 -0
  39. package/dist/esm/outcome.js +155 -0
  40. package/dist/esm/output.d.ts +70 -0
  41. package/dist/esm/output.js +374 -0
  42. package/dist/esm/sandbox.d.ts +218 -0
  43. package/dist/esm/sandbox.js +832 -0
  44. package/dist/esm/shell.d.ts +152 -0
  45. package/dist/esm/shell.js +379 -0
  46. package/dist/esm/toolbox.d.ts +48 -0
  47. package/dist/esm/toolbox.js +83 -0
  48. package/dist/esm/types.d.ts +111 -0
  49. package/dist/esm/types.js +95 -0
  50. package/dist/esm/version.d.ts +2 -0
  51. package/dist/esm/version.js +2 -0
  52. package/package.json +53 -4
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUN_SCRIPT = exports.COMMAND_VAR = exports.SCRIPT_VAR = exports.FULL_OUTPUT_BYTES = exports.OUTPUT_ID_RE = exports.OUTPUT_DIR = void 0;
4
+ exports.captureScript = captureScript;
5
+ exports.collectScript = collectScript;
6
+ exports.removeOutputScript = removeOutputScript;
7
+ exports.parseMarker = parseMarker;
8
+ exports.markerTruncation = markerTruncation;
9
+ exports.execFullOutput = execFullOutput;
10
+ // exec(..., { fullOutput: true }): one call that keeps a command's whole
11
+ // output, not just the first 4 MiB of each stream the sandbox answers with.
12
+ //
13
+ // The command's stdout and stderr stay pipes, as in a plain exec. A relay on
14
+ // each passes what comes through to a reader that copies it to a file in the
15
+ // exec's own directory, at a fixed path under the sandbox user's home, and
16
+ // drops what passes 100 MiB, so a runaway command cannot fill the disk:
17
+ //
18
+ // ~/.prized/exec/<exec id>/stdout
19
+ // ~/.prized/exec/<exec id>/stderr
20
+ //
21
+ // Both under 4 MiB: the script prints them back and removes the directory,
22
+ // so the answer carries the same output a plain exec's does. Bigger, or a
23
+ // reader that could not write its file (a full disk): it prints one marker
24
+ // line instead, and the SDK downloads both files through the files API and
25
+ // removes the directory. A stream whose write failed comes back as far as it
26
+ // got, `truncated` with `truncatedReason: 'write_failed'`. The scripts are the Python SDK's
27
+ // (sdk/python/src/prized/_output.py), rendered the same; both SDKs check
28
+ // them against sdk/output-scripts/.
29
+ const errors_js_1 = require("./errors.js");
30
+ const http_js_1 = require("./http.js");
31
+ const outcome_js_1 = require("./outcome.js");
32
+ const shell_js_1 = require("./shell.js");
33
+ const toolbox_js_1 = require("./toolbox.js");
34
+ exports.OUTPUT_DIR = '/home/prized/.prized/exec';
35
+ exports.OUTPUT_ID_RE = /^exec_[0-9a-f]{16}$/;
36
+ /** Each stream of a fullOutput command keeps at most this many bytes: one download. */
37
+ exports.FULL_OUTPUT_BYTES = toolbox_js_1.MAX_FILE_BYTES;
38
+ /**
39
+ * The variables that carry the script and the caller's command, so neither
40
+ * is in the argv of any process but the command's own shell: `pkill -f`
41
+ * finds what it finds in a plain exec. The script unsets both first.
42
+ */
43
+ exports.SCRIPT_VAR = 'PRIZED_EXEC_SCRIPT';
44
+ exports.COMMAND_VAR = 'PRIZED_EXEC_COMMAND';
45
+ /** What a fullOutput exec sends as its command: the script, from SCRIPT_VAR. */
46
+ exports.RUN_SCRIPT = `eval "\${${exports.SCRIPT_VAR}:?}"`;
47
+ /** How long the readers may run on after the command (the sandbox's own wait for a plain exec's pipes). */
48
+ const GRACE_S = 5;
49
+ /**
50
+ * A directory older than this is one an earlier exec could not remove: no
51
+ * exec runs it that long (MAX_BLOCKING_EXEC_MS, then GRACE_S and READ_MS).
52
+ */
53
+ const STALE_MIN = 60;
54
+ /** The bound on each request the SDK makes on its own to collect or remove the files. */
55
+ const TEARDOWN_MS = 10_000;
56
+ /** The bound on reading the files back, retries included (files.ts FILE_TIMEOUT_MS). */
57
+ const READ_MS = 10 * 60_000;
58
+ function checkId(id) {
59
+ if (!exports.OUTPUT_ID_RE.test(id))
60
+ throw new Error(`unreachable: bad exec id ${id}`);
61
+ }
62
+ /**
63
+ * The reader's own shell (`$1` the file, `$2` its flag): cat writes the file
64
+ * until a write fails; then the flag, an empty file (it takes no free block),
65
+ * then the rest drained into /dev/null, and exit 1. `true`, not `:`: a
66
+ * redirection that fails on a special builtin ends the shell, and with it
67
+ * the draining.
68
+ */
69
+ const READER_SHELL = `'cat > "$1" || { true > "$2" || :; cat > /dev/null; exit 1; }'`;
70
+ /**
71
+ * The reader on one stream: cat copies what the relay passes it to the file
72
+ * as it comes (each read written at once) until the file reaches
73
+ * FULL_OUTPUT_BYTES and one 512-byte block more, which tells a cut stream
74
+ * from a full one. The file size limit (ulimit -f, in 512-byte blocks in
75
+ * POSIX mode) stops the file there with EFBIG, not SIGXFSZ, and the reader
76
+ * goes on draining into /dev/null, so the command never meets a closed pipe
77
+ * and a runaway one cannot fill the disk. A write that fails short of that (a
78
+ * full disk) stops the file the same way. Either leaves `<name>.failed` at
79
+ * once, which collect reads when a time limit ended the script before it
80
+ * could wait for the reader, and the reader exits 1 once its input ends. A
81
+ * shell per stream (exec), in a session of its own (setsid, same pid): a time
82
+ * limit or a `kill 0` that ends the command's process group leaves it to
83
+ * drain what the pipe still holds.
84
+ */
85
+ function reader(d, name, fifo) {
86
+ const blocks = exports.FULL_OUTPUT_BYTES / 512 + 1;
87
+ return (`( exec > /dev/null 2>&1; PATH=${shell_js_1.LAUNCH_PATH}; trap '' XFSZ; ( set -o posix ) && set -o posix; ` +
88
+ `ulimit -f ${blocks} || :; exec setsid /bin/sh -c ${READER_SHELL} sh ${d}/${name} ${d}/${name}.failed < ${d}/${fifo} ) &`);
89
+ }
90
+ /**
91
+ * The relay on one stream: cat reads the command's pipe and passes it on to
92
+ * the reader's. It is what the watchdog stops, never the reader, so the
93
+ * reader always ends on the end of its input, and its exit status says
94
+ * whether every write to its file went through. In a session of its own
95
+ * (setsid, same pid), as the reader is.
96
+ */
97
+ function relay(d, from, to) {
98
+ return `( exec > ${d}/${to} 2> /dev/null < /dev/null; PATH=${shell_js_1.LAUNCH_PATH}; exec setsid cat ${d}/${from} ) &`;
99
+ }
100
+ /**
101
+ * A shell test: process `pid` (shell text) is this exec's relay on `fifo`,
102
+ * not another process that reused its pid. Its arguments must be exactly
103
+ * `cat <fifo>` (as setsid execs it), not merely name the exec's directory
104
+ * somewhere in the command line, as a process the command started may
105
+ * (`tail -f ~/.prized/exec/<id>/stdout`). /proc ends each argument with a
106
+ * NUL; here each is one line (a NUL becomes a newline, a newline a space),
107
+ * as the run scripts' isRun (shell.ts) reads them.
108
+ */
109
+ function isRelay(pid, fifo) {
110
+ return `[ "$(tr "\\0\\n" "\\n " < "/proc/${pid}/cmdline" 2> /dev/null | tr "\\n" " ")" = "cat ${fifo} " ]`;
111
+ }
112
+ /**
113
+ * The bound on waiting for the readers once the command has ended: GRACE_S
114
+ * seconds, as the sandbox waits for a plain exec's pipes, then the relays
115
+ * still running stop (each pid checked to be this exec's relay on its own
116
+ * pipe first, never one reused since): its reader then ends with what came
117
+ * through, and a process the command left holding its output meets a closed
118
+ * pipe, as it would there. Stopped early (TERM, once the readers are done),
119
+ * it takes its sleep with it.
120
+ */
121
+ function watchdog(d) {
122
+ return (`( s=; trap '[ -z "$s" ] || kill "$s" 2> /dev/null; exit 0' TERM; sleep ${GRACE_S} & s=$!; wait "$s" || :; ` +
123
+ `if ${isRelay('$4', `${d}/o`)}; then kill "$4" 2> /dev/null || :; fi; ` +
124
+ `if ${isRelay('$5', `${d}/e`)}; then kill "$5" 2> /dev/null || :; fi ) > /dev/null 2>&1 &`);
125
+ }
126
+ /**
127
+ * Sizes, the cut past FULL_OUTPUT_BYTES and the marker line, with `d` set to
128
+ * the directory and each reader's write failure in `x` and `y` (1 or 0,
129
+ * failedLines). A cut leaves `<name>.cut`, so collect sent again still reads
130
+ * the stream as past the bound (its flag is still there, its size no longer
131
+ * says so). `true`, not `:`, for the same reason as in READER_SHELL.
132
+ */
133
+ function reportLines(id) {
134
+ const max = exports.FULL_OUTPUT_BYTES;
135
+ return {
136
+ sizes: ['o=$(stat -c %s "$d/stdout" 2> /dev/null || echo 0)', 'e=$(stat -c %s "$d/stderr" 2> /dev/null || echo 0)'],
137
+ cut: [
138
+ `if [ "$o" -gt ${max} ]; then truncate -s ${max} "$d/stdout" && true > "$d/stdout.cut" || :; fi`,
139
+ `if [ "$e" -gt ${max} ]; then truncate -s ${max} "$d/stderr" && true > "$d/stderr.cut" || :; fi`,
140
+ `echo "prized-output ${id} $o $e $x $y"`,
141
+ ],
142
+ };
143
+ }
144
+ /**
145
+ * Whether each reader failed to write its file (`x` for stdout, `y` for
146
+ * stderr: nonzero from its exit status, or its flag, after `sizes`): 1 when
147
+ * it ended in error short of the bound (a full disk or a quota, a file it
148
+ * could not create, a reader killed), else 0. Past the bound its error is the
149
+ * cut (EFBIG). The watchdog stops relays, never readers, so a reader's status
150
+ * is its own.
151
+ */
152
+ function failedLines() {
153
+ const max = exports.FULL_OUTPUT_BYTES;
154
+ return [
155
+ `if [ "$x" -eq 0 ] || [ "$o" -gt ${max} ]; then x=0; else x=1; fi`,
156
+ `if [ "$y" -eq 0 ] || [ "$e" -gt ${max} ]; then y=0; else y=1; fi`,
157
+ ];
158
+ }
159
+ /**
160
+ * The script a fullOutput exec runs (RUN_SCRIPT, from SCRIPT_VAR). The
161
+ * caller's command, from COMMAND_VAR, runs as a plain exec runs it (`bash
162
+ * -c`, or `sh -c` where there is no bash, stdin from /dev/null), with the
163
+ * request's cwd and env.
164
+ *
165
+ * Nothing is assigned before it runs but the positional parameters, so every
166
+ * variable the caller's env sets reaches it unchanged, PATH included; and
167
+ * every step holds under an errexit the env may switch on (SHELLOPTS). As in
168
+ * a plain exec, the relays stop 5 s after the command ends if a process it
169
+ * left running still holds its output, keeping what came through. A command
170
+ * a signal ends exits 128+n, as a background command does. A reader that
171
+ * could not write its file (failedLines) sends the output to the files API
172
+ * whatever its size, with the marker saying which. Without /bin/sh, setsid,
173
+ * cat or a directory to write to, the command runs as a plain exec.
174
+ */
175
+ function captureScript(id) {
176
+ checkId(id);
177
+ const d = `${exports.OUTPUT_DIR}/${id}`;
178
+ const { sizes, cut } = reportLines(id);
179
+ return [
180
+ `set -- "\${${exports.COMMAND_VAR}:?}" /bin/bash; unset ${exports.COMMAND_VAR} ${exports.SCRIPT_VAR}; [ -x "$2" ] || set -- "$1" /bin/sh`,
181
+ `if ( PATH=${shell_js_1.LAUNCH_PATH}; mkdir -p ${exports.OUTPUT_DIR} && mkdir ${d} ) 2> /dev/null; then`,
182
+ `if ( PATH=${shell_js_1.LAUNCH_PATH}; [ -x /bin/sh ] && command -v setsid && command -v cat && mkfifo ${d}/o ${d}/e ${d}/p ${d}/q ) > /dev/null 2>&1; then`,
183
+ // The command writes to o and e; a relay passes each on to p and q, where its reader is.
184
+ reader(d, 'stdout', 'p'),
185
+ 'set -- "$1" "$2" "$!"',
186
+ reader(d, 'stderr', 'q'),
187
+ 'set -- "$1" "$2" "$3" "$!"',
188
+ relay(d, 'o', 'p'),
189
+ 'set -- "$1" "$2" "$3" "$4" "$!"',
190
+ relay(d, 'e', 'q'),
191
+ 'set -- "$1" "$2" "$3" "$4" "$5" "$!"',
192
+ // bash's "Killed" line for a command a signal ended goes to the braces' /dev/null.
193
+ `if { "$2" -c "$1" > ${d}/o 2> ${d}/e < /dev/null; } 2> /dev/null; then set -- 0 "$3" "$4" "$5" "$6"; else set -- "$?" "$3" "$4" "$5" "$6"; fi`,
194
+ `PATH=${shell_js_1.LAUNCH_PATH}`,
195
+ `c=$1; d=${d}`,
196
+ watchdog(d),
197
+ 'w=$!',
198
+ 'if wait "$2" 2> /dev/null; then x=0; else x=$?; fi',
199
+ 'if wait "$3" 2> /dev/null; then y=0; else y=$?; fi',
200
+ 'kill "$w" 2> /dev/null || :',
201
+ ...sizes,
202
+ ...failedLines(),
203
+ `if [ "$o" -lt ${toolbox_js_1.MAX_OUTPUT_BYTES} ] && [ "$e" -lt ${toolbox_js_1.MAX_OUTPUT_BYTES} ] && [ "$x$y" = 00 ]; then ` +
204
+ 'cat "$d/stdout" 2> /dev/null || :; cat "$d/stderr" >&2 2> /dev/null || :; rm -rf "$d" || :; exit "$c"; fi',
205
+ ...cut,
206
+ 'exit "$c"',
207
+ 'fi',
208
+ `( PATH=${shell_js_1.LAUNCH_PATH}; rm -rf ${d} ) 2> /dev/null || :`,
209
+ 'fi',
210
+ 'exec "$2" -c "$1" < /dev/null',
211
+ ].join('\n');
212
+ }
213
+ /**
214
+ * What a fullOutput command left when the script could not print it (its
215
+ * time limit, or a signal, ended the script): the marker line, or `gone`,
216
+ * with the directory removed, when there is nothing to read (it was never
217
+ * made, or its output was already printed back). A failed write is read from
218
+ * the reader's flag, before the size, which is final once the flag is there
219
+ * (the script's wait for the reader's exit status went with the script); a
220
+ * reader killed, or one that could not make its flag, is not seen here.
221
+ */
222
+ function collectScript(id) {
223
+ checkId(id);
224
+ const { sizes, cut } = reportLines(id);
225
+ return [
226
+ `d=${exports.OUTPUT_DIR}/${id}`,
227
+ `PATH=${shell_js_1.LAUNCH_PATH}`,
228
+ `if [ ! -f "$d/stdout" ] || [ ! -f "$d/stderr" ]; then rm -rf "$d" || :; echo "prized-output ${id} gone"; exit 0; fi`,
229
+ 'if [ -e "$d/stdout.failed" ]; then x=1; else x=0; fi',
230
+ 'if [ -e "$d/stderr.failed" ]; then y=1; else y=0; fi',
231
+ ...sizes,
232
+ // Cut by an earlier collect whose answer was lost: as long as a cut stream (the bound and a block).
233
+ `if [ -e "$d/stdout.cut" ]; then o=$((${exports.FULL_OUTPUT_BYTES} + 512)); fi`,
234
+ `if [ -e "$d/stderr.cut" ]; then e=$((${exports.FULL_OUTPUT_BYTES} + 512)); fi`,
235
+ ...failedLines(),
236
+ ...cut,
237
+ ].join('\n');
238
+ }
239
+ /**
240
+ * Remove a fullOutput command's directory (keyed by the SDK's own id, safe to
241
+ * send again), and any an earlier one left over an hour ago, when it got no
242
+ * answer to remove its own.
243
+ */
244
+ function removeOutputScript(id) {
245
+ checkId(id);
246
+ return (`PATH=${shell_js_1.LAUNCH_PATH}; rm -rf ${exports.OUTPUT_DIR}/${id}; ` +
247
+ `find ${exports.OUTPUT_DIR} -mindepth 1 -maxdepth 1 -name 'exec_*' -mmin +${STALE_MIN} -exec rm -rf -- {} + 2> /dev/null || :`);
248
+ }
249
+ /** The marker (one without failure flags, as an earlier SDK's collect printed, has none), `gone`, or null for no marker. */
250
+ function parseMarker(stdout, id) {
251
+ const m = new RegExp(`(?:^|\\n)prized-output ${id} (?:([0-9]+) ([0-9]+)(?: ([01]) ([01]))?|(gone))\\n$`).exec(stdout);
252
+ if (m === null)
253
+ return null;
254
+ if (m[5] === 'gone')
255
+ return 'gone';
256
+ return { out: Number(m[1]), err: Number(m[2]), outFailed: m[3] === '1', errFailed: m[4] === '1' };
257
+ }
258
+ /** Why the read-back output is cut, if it is: a failed write first, since it is the one the caller cannot foresee. */
259
+ function markerTruncation(marker) {
260
+ if (marker.outFailed || marker.errFailed)
261
+ return 'write_failed';
262
+ if (marker.out > exports.FULL_OUTPUT_BYTES || marker.err > exports.FULL_OUTPUT_BYTES)
263
+ return 'output_limit';
264
+ return null;
265
+ }
266
+ /** A request the SDK sends on its own to collect or remove the files: best effort, within TEARDOWN_MS. */
267
+ async function teardown(toolbox, script) {
268
+ try {
269
+ return await (0, toolbox_js_1.helperExec)(toolbox, script, { deadline: toolbox.config.now() + TEARDOWN_MS });
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ }
275
+ async function download(toolbox, path, signal) {
276
+ const result = await (0, http_js_1.request)(toolbox.config, {
277
+ method: 'GET',
278
+ url: `${toolbox.base()}/files/download?path=${encodeURIComponent(path)}`,
279
+ retry: 'safe',
280
+ read: 'bytes',
281
+ timeoutMs: READ_MS,
282
+ deadline: toolbox.config.now() + READ_MS,
283
+ signal,
284
+ sandboxId: toolbox.sandboxId,
285
+ });
286
+ // A leading BOM is the command's own output, as in the answer.
287
+ return new TextDecoder('utf-8', { ignoreBOM: true }).decode(result.bytes ?? new Uint8Array(0));
288
+ }
289
+ /** The files' contents in place of the answer's output. */
290
+ async function readBack(toolbox, id, marker, result, signal) {
291
+ const dir = `${exports.OUTPUT_DIR}/${id}`;
292
+ // Both at once; one that fails stops the other, as the caller's abort stops both.
293
+ const both = new AbortController();
294
+ const stop = () => both.abort();
295
+ if (signal?.aborted)
296
+ stop();
297
+ signal?.addEventListener('abort', stop, { once: true });
298
+ const read = (name, size) => size > 0
299
+ ? download(toolbox, `${dir}/${name}`, both.signal).catch((error) => {
300
+ stop();
301
+ throw error;
302
+ })
303
+ : Promise.resolve('');
304
+ let stdout;
305
+ let stderr;
306
+ try {
307
+ [stdout, stderr] = await Promise.all([read('stdout', marker.out), read('stderr', marker.err)]);
308
+ }
309
+ finally {
310
+ signal?.removeEventListener('abort', stop);
311
+ }
312
+ const reason = markerTruncation(marker);
313
+ const { exitCode, durationMs, timedOut } = result;
314
+ const full = { stdout, stderr, exitCode, durationMs, timedOut, truncated: reason !== null };
315
+ if (reason !== null)
316
+ full.truncatedReason = reason;
317
+ return full;
318
+ }
319
+ /**
320
+ * A blocking exec that keeps the whole output (up to FULL_OUTPUT_BYTES of
321
+ * each stream). The result is a plain exec's, timeouts included: the caller
322
+ * (execCommand) turns a timed-out one into CommandTimeoutError.
323
+ */
324
+ async function execFullOutput(toolbox, id, req, signal) {
325
+ const raw = {
326
+ command: exports.RUN_SCRIPT,
327
+ env: { ...req.env, [exports.SCRIPT_VAR]: captureScript(id), [exports.COMMAND_VAR]: req.command },
328
+ timeoutMs: req.timeoutMs,
329
+ };
330
+ if (req.cwd !== undefined)
331
+ raw.cwd = req.cwd;
332
+ let result;
333
+ try {
334
+ // A plain exec's retry rules: sent again only if it provably never arrived.
335
+ result = await (0, toolbox_js_1.rawExec)(toolbox, raw, { retry: 'exec', signal });
336
+ }
337
+ catch (error) {
338
+ // The caller has no way to read what it printed: its files go.
339
+ await teardown(toolbox, removeOutputScript(id));
340
+ throw error;
341
+ }
342
+ let marker = parseMarker(result.stdout, id);
343
+ if (marker === null && (result.timedOut || result.exitCode === -1)) {
344
+ // Its time limit (or a signal) ended the script with the command, before
345
+ // it printed anything back: what the command printed is in its files.
346
+ const collected = await teardown(toolbox, collectScript(id));
347
+ marker = collected === null ? null : parseMarker(collected.stdout, id);
348
+ if (marker === null)
349
+ return result; // no answer: nothing more to be done
350
+ }
351
+ // No marker: the output came back in the answer (or the command ran as a
352
+ // plain exec), and the directory is already gone.
353
+ if (marker === null || marker === 'gone')
354
+ return result;
355
+ let full;
356
+ try {
357
+ full = await readBack(toolbox, id, marker, result, signal);
358
+ }
359
+ catch (error) {
360
+ const aborted = !(error instanceof errors_js_1.PrizedError) || error.code === 'aborted';
361
+ if (aborted || result.timedOut) {
362
+ // Leaving, or the timeout is the caller's error: the files go.
363
+ await teardown(toolbox, removeOutputScript(id));
364
+ if (aborted)
365
+ throw error;
366
+ return result;
367
+ }
368
+ if (error.code === 'sandbox_deleted') {
369
+ // Nothing is kept: the output went with the sandbox, as its code says for any call on it.
370
+ // The command ran: its exit code goes on the error too, so a caller
371
+ // that replaces the sandbox can tell not to run it again.
372
+ throw (0, outcome_js_1.restate)(error, `The command ran (exit ${result.exitCode}), but the sandbox was deleted before its output could be read back.`, {
373
+ cause: error,
374
+ exitCode: result.exitCode,
375
+ });
376
+ }
377
+ const dir = `${exports.OUTPUT_DIR}/${id}`;
378
+ throw new errors_js_1.OutputReadError(`The command ran (exit ${result.exitCode}), but its output could not be read back (${error.message.replace(/[.\s]+$/, '')}). ` +
379
+ `It is kept in ${dir}/stdout and ${dir}/stderr for an hour: read them with files.read(), then remove ${dir}.`, { cause: error, exitCode: result.exitCode, stdoutPath: `${dir}/stdout`, stderrPath: `${dir}/stderr` });
380
+ }
381
+ await teardown(toolbox, removeOutputScript(id));
382
+ return full;
383
+ }
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,218 @@
1
+ import { CommandHandle, type ExecOptions } from './commands.js';
2
+ import { Files } from './files.js';
3
+ import { type ClientOptions } from './http.js';
4
+ import { type ExecResult, type Resources, type ResourcesInput, type SandboxInfo, type SandboxState, type Template } from './types.js';
5
+ /** The header a request to a sandbox port's URL carries the API key in. */
6
+ export declare const PREVIEW_KEY_HEADER = "Prized-Key";
7
+ /** The template a create uses when it names none. */
8
+ export declare const DEFAULT_TEMPLATE = "prized-base-v1";
9
+ /** How long create, resume and fork wait for `running` by default. */
10
+ export declare const DEFAULT_START_TIMEOUT_MS = 180000;
11
+ /** How long pause and delete wait by default. */
12
+ export declare const DEFAULT_SETTLE_TIMEOUT_MS = 120000;
13
+ /** Cleanups still running after the call that started them has answered.
14
+ * Not exported from the package; the tests await it. */
15
+ export declare const backgroundCleanups: Set<Promise<void>>;
16
+ /** Edge warm-ups (warmEdge) not yet settled. Not exported from the package;
17
+ * the tests await it. */
18
+ export declare const backgroundWarmups: Set<Promise<void>>;
19
+ export interface CreateOptions extends ClientOptions {
20
+ /** Template id. Default `prized-base-v1`; `Sandbox.templates()` lists them. */
21
+ template?: string;
22
+ /** vCPU, memory and disk. Anything left out takes the template's default. */
23
+ resources?: ResourcesInput;
24
+ /** Environment variables for every command in the sandbox. */
25
+ envVars?: Record<string, string>;
26
+ /** Your own key/value tags (up to 32), filterable in `Sandbox.list`. */
27
+ labels?: Record<string, string>;
28
+ /**
29
+ * Run time in minutes; the sandbox is deleted when it ends. Paid plans:
30
+ * 1 440 (24 hours) by default, up to 7 days (10 080; a custom plan has no
31
+ * maximum), and every resume restarts the clock. The free tier: 60
32
+ * minutes of running at most (paused time does not count). Change it later
33
+ * with `prized sandbox extend` or the API's `extend` and `timeout` routes.
34
+ */
35
+ maxRuntimeMin?: number;
36
+ /** Pause after this many idle minutes (0: never). */
37
+ autoPauseMin?: number;
38
+ /** Delete this many minutes after pausing (0: never). */
39
+ autoDeleteMin?: number;
40
+ /**
41
+ * How long the create may take, in ms: the request, its retries and
42
+ * (unless `wait: false`) the wait for `running`. Default 180 000.
43
+ * Deleting a sandbox that failed (up to 5 s) comes on top.
44
+ */
45
+ timeoutMs?: number;
46
+ /**
47
+ * Makes a retried create return the same sandbox instead of a second one.
48
+ * The SDK sends a fresh key per call when you give none, so its own
49
+ * retries are always safe.
50
+ */
51
+ idempotencyKey?: string;
52
+ /** Resolve as soon as the API accepts the create, without waiting for `running`. */
53
+ wait?: boolean;
54
+ signal?: AbortSignal;
55
+ }
56
+ export interface ListOptions extends ClientOptions {
57
+ /** Only sandboxes carrying every one of these labels. */
58
+ labels?: Record<string, string>;
59
+ /** Only sandboxes in this state (or any of these states). */
60
+ state?: SandboxState | SandboxState[];
61
+ }
62
+ export interface WaitableOptions {
63
+ /** Wait for the change to land. Default true (false for delete). */
64
+ wait?: boolean;
65
+ /**
66
+ * How long the call may take, in ms: the request, its retries and
67
+ * (unless `wait: false`) the wait. Default 180 000 for resume and fork,
68
+ * 120 000 for pause and delete. Deleting a fork's copies that failed
69
+ * comes on top.
70
+ */
71
+ timeoutMs?: number;
72
+ signal?: AbortSignal;
73
+ }
74
+ export interface ForkOptions extends WaitableOptions {
75
+ /** How many copies. Default 1. */
76
+ count?: number;
77
+ /** Labels for the copies, in place of the parent's. Default: the parent's labels. */
78
+ labels?: Record<string, string>;
79
+ /**
80
+ * Each copy's lifetime in minutes, up to the workspace's maximum. Default:
81
+ * on a paid workspace, the parent's runtime, starting at the fork; on the
82
+ * free tier, copies keep the parent's deletion time (this may only shorten it).
83
+ */
84
+ maxRuntimeMin?: number;
85
+ /** Makes a retried fork return the same copies. The SDK sends one per call when you give none. */
86
+ idempotencyKey?: string;
87
+ }
88
+ /**
89
+ * A Prized sandbox: a microVM your code creates, runs commands in, and
90
+ * pauses, resumes, forks and deletes.
91
+ *
92
+ * const sandbox = await Sandbox.create({ template: 'prized-base-v1' });
93
+ * const { stdout } = await sandbox.exec('python3 --version');
94
+ * await sandbox.delete();
95
+ */
96
+ export declare class Sandbox {
97
+ /** The sandbox id (`sbx_…`). */
98
+ readonly id: string;
99
+ /** Read, write, list, remove, upload and download files. */
100
+ readonly files: Files;
101
+ private readonly config;
102
+ private current;
103
+ private readonly toolbox;
104
+ private constructor();
105
+ /** Everything the API said about this sandbox, as of the last call. */
106
+ get info(): SandboxInfo;
107
+ /** The state as of the last call; `refresh()` reads it fresh. */
108
+ get state(): SandboxState;
109
+ get template(): string | null;
110
+ get resources(): Resources;
111
+ get labels(): Record<string, string>;
112
+ get createdAt(): Date | null;
113
+ get expiresAt(): Date | null;
114
+ get errorReason(): string | null;
115
+ toJSON(): Omit<SandboxInfo, 'raw'>;
116
+ /**
117
+ * Create a sandbox and wait until it is running, ready for commands.
118
+ * If it fails or the wait runs out, the sandbox is deleted and the error
119
+ * thrown. Throws PrizedError: `paid_balance_required`, `insufficient_balance`,
120
+ * `sandbox_quota_exceeded` (see `reason`), `template_not_found`,
121
+ * `invalid_resources`, `sandbox_error`, `timeout`, …
122
+ */
123
+ static create(options?: CreateOptions): Promise<Sandbox>;
124
+ /** An existing sandbox by id, in whatever state it is in. */
125
+ static connect(id: string, options?: ClientOptions): Promise<Sandbox>;
126
+ /** The workspace's sandboxes (deleted ones excluded), newest first; `labels` is filtered by the API. */
127
+ static list(options?: ListOptions): Promise<Sandbox[]>;
128
+ /**
129
+ * Delete every sandbox carrying all of `labels` (at least one) and resolve
130
+ * with their ids (the API takes up to 250 a request; this repeats it until
131
+ * none are left). Safe to repeat: sandboxes already being
132
+ * deleted are in the answer again, and none matching resolves `[]`. Each
133
+ * counts toward the workspace's quotas until it is gone;
134
+ * `Sandbox.list({ labels })` coming back empty means they all are.
135
+ */
136
+ static deleteByLabels(labels: Record<string, string>, options?: ClientOptions): Promise<string[]>;
137
+ /** The templates you can create sandboxes from, with their default, minimum and maximum shapes. */
138
+ static templates(options?: ClientOptions): Promise<Template[]>;
139
+ /**
140
+ * Run a command (a shell string, or an argv array the SDK quotes) and
141
+ * return its output. A non-zero exit is not an error: read `exitCode`.
142
+ * With `background: true` it returns a CommandHandle at once.
143
+ */
144
+ exec(command: string | readonly string[], options: ExecOptions & {
145
+ background: true;
146
+ }): Promise<CommandHandle>;
147
+ exec(command: string | readonly string[], options?: ExecOptions & {
148
+ background?: false;
149
+ }): Promise<ExecResult>;
150
+ exec(command: string | readonly string[], options?: ExecOptions): Promise<ExecResult | CommandHandle>;
151
+ /** Read the sandbox again: `state` and the other fields update. Does not count as activity. */
152
+ refresh(): Promise<this>;
153
+ /**
154
+ * Count as activity now: the `autoPauseMin` idle clock starts over.
155
+ * Commands and file transfers already count; this is for idle stretches.
156
+ */
157
+ keepAlive(): Promise<void>;
158
+ /**
159
+ * Pause: memory and disk are kept, and only storage is billed. Waits for
160
+ * `paused` unless `wait: false`.
161
+ */
162
+ pause(options?: WaitableOptions): Promise<void>;
163
+ /** Resume a paused sandbox, with every process where it left off. Waits for `running` unless `wait: false`. */
164
+ resume(options?: WaitableOptions): Promise<void>;
165
+ /**
166
+ * Fork a running or paused sandbox into `count` new sandboxes, each with
167
+ * every process of this one running. Resolves once every copy is running,
168
+ * ready for commands (unless `wait: false`); if any copy fails or the
169
+ * wait runs out, every copy is deleted and the error thrown. The parent is
170
+ * left as it was: running, or paused. `timeoutMs` bounds the request and
171
+ * the wait together.
172
+ */
173
+ fork(options?: ForkOptions): Promise<Sandbox[]>;
174
+ /**
175
+ * Delete the sandbox and everything in it. Returns once the API accepts;
176
+ * `wait: true` also waits until it is gone (a deleting sandbox still
177
+ * counts toward the workspace's quota). `timeoutMs` (default 120 000)
178
+ * bounds the whole call, its retries and the wait included.
179
+ */
180
+ delete(options?: WaitableOptions): Promise<void>;
181
+ /**
182
+ * The https URL `port` of the sandbox answers at, for a request that
183
+ * carries a workspace-wide API key with Sandboxes: Write in the
184
+ * `Prized-Key` header (`previewHeaders()`): a web app, an API or a dev
185
+ * server listening on that port in the sandbox on 127.0.0.1 or 0.0.0.0. A
186
+ * request to a paused sandbox resumes it and waits up to 10 seconds, as
187
+ * `exec` does. Nothing is created: the URL is the sandbox's own. Throws
188
+ * `preview_unavailable` when the deployment has sandbox ingress off.
189
+ */
190
+ previewUrl(port: number): string;
191
+ /** The headers a request to `previewUrl(port)` sends: this client's API key in
192
+ * `Prized-Key`. The header never reaches the app. Send them with
193
+ * `redirect: 'manual'`: fetch resends custom headers to whatever host a
194
+ * redirect names, so only ever send them to the sandbox's preview URL. */
195
+ previewHeaders(): Record<string, string>;
196
+ /** POST the verb, the call and its retries within `deadline`; with `hold`,
197
+ * held by the API until the sandbox is that (each attempt asks only for
198
+ * what is left). */
199
+ private verb;
200
+ /**
201
+ * Poll until the sandbox reaches `want`; throws on error/deleted or the
202
+ * deadline. The first read goes out at once (a held answer that has not
203
+ * settled is usually a breath away), then 100 ms, doubling to
204
+ * pollIntervalMs, and the last just before the deadline (Polls).
205
+ * `reportedMs` is the caller's whole budget, for the error.
206
+ */
207
+ private waitFor;
208
+ /** Best-effort delete after a failed wait, bounded (as the Python SDK's); never throws. */
209
+ private discard;
210
+ /**
211
+ * Poll the list until every fork copy is running or has failed, by
212
+ * `deadline`: one list call per poll, however many copies, every
213
+ * pollIntervalMs, and the last just before the deadline (Polls),
214
+ * so even a budget shorter than one interval polls once. `reportedMs` is
215
+ * the caller's whole budget, for the error.
216
+ */
217
+ private static settle;
218
+ }