@prized/sdk 0.1.0 → 0.1.2
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/README.md +76 -21
- package/dist/cjs/commands.d.ts +47 -14
- package/dist/cjs/commands.js +94 -26
- package/dist/cjs/errors.d.ts +43 -3
- package/dist/cjs/errors.js +54 -6
- package/dist/cjs/files.d.ts +21 -5
- package/dist/cjs/files.js +38 -14
- package/dist/cjs/http.d.ts +74 -15
- package/dist/cjs/http.js +208 -32
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/outcome.d.ts +11 -1
- package/dist/cjs/outcome.js +28 -5
- package/dist/cjs/output.d.ts +19 -8
- package/dist/cjs/output.js +152 -56
- package/dist/cjs/sandbox.d.ts +3 -2
- package/dist/cjs/sandbox.js +40 -5
- package/dist/cjs/shell.d.ts +2 -1
- package/dist/cjs/shell.js +2 -1
- package/dist/cjs/toolbox.d.ts +2 -0
- package/dist/cjs/toolbox.js +4 -1
- package/dist/cjs/types.d.ts +13 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/commands.d.ts +47 -14
- package/dist/esm/commands.js +95 -27
- package/dist/esm/errors.d.ts +43 -3
- package/dist/esm/errors.js +52 -5
- package/dist/esm/files.d.ts +21 -5
- package/dist/esm/files.js +39 -15
- package/dist/esm/http.d.ts +74 -15
- package/dist/esm/http.js +207 -31
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/outcome.d.ts +11 -1
- package/dist/esm/outcome.js +27 -5
- package/dist/esm/output.d.ts +19 -8
- package/dist/esm/output.js +153 -58
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +40 -5
- package/dist/esm/shell.d.ts +2 -1
- package/dist/esm/shell.js +2 -1
- package/dist/esm/toolbox.d.ts +2 -0
- package/dist/esm/toolbox.js +4 -1
- package/dist/esm/types.d.ts +13 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,9 @@ No runtime dependencies. ESM and CommonJS, with type declarations.
|
|
|
47
47
|
Make an API key on the [API keys page](https://prized.dev/dashboard/workspace/api-keys)
|
|
48
48
|
and put it in `PRIZED_API_KEY`, or pass it in. New to Prized?
|
|
49
49
|
[Open Sandboxes](https://prized.dev/onboarding/sandboxes) signs you up (free
|
|
50
|
-
credit, no card) and makes one.
|
|
50
|
+
credit, no card) and makes one. Or your coding agent can sign you up itself
|
|
51
|
+
with your email and a 6-digit code: have it follow
|
|
52
|
+
[prized.dev/auth.md](https://prized.dev/auth.md).
|
|
51
53
|
|
|
52
54
|
```ts
|
|
53
55
|
const sandbox = await Sandbox.create({ apiKey: 'dcp_…' });
|
|
@@ -58,19 +60,31 @@ to fewer permissions needs **Sandboxes: Write** to create, run commands and
|
|
|
58
60
|
move files; **Sandboxes: Read** is enough for `list`, `connect`, `refresh` and
|
|
59
61
|
`templates`.
|
|
60
62
|
|
|
63
|
+
Your coding agent can install the Prized skill with `npx skills add prized-dev/skills -g -y` (it should ask you first), so it knows when and how to use Prized the next time you ask for a sandbox.
|
|
64
|
+
|
|
61
65
|
Every entry point (`create`, `connect`, `list`, `deleteByLabels`, `templates`) takes the same
|
|
62
66
|
client options:
|
|
63
67
|
|
|
64
68
|
| Option | Default | |
|
|
65
69
|
|---|---|---|
|
|
66
70
|
| `apiKey` | `process.env.PRIZED_API_KEY` | |
|
|
67
|
-
| `baseUrl` | `https://api.prized.dev` | The API origin. |
|
|
68
|
-
| `edgeUrl` | the sandbox's own `toolboxBase` | Where commands and files go. |
|
|
71
|
+
| `baseUrl` | `https://api.prized.dev` | The API origin: `https`, or plain `http` on `localhost`, `127.0.0.1` or `[::1]`. |
|
|
72
|
+
| `edgeUrl` | the sandbox's own `toolboxBase` | Where commands and files go, by the same rule. Pass it for another deployment whose API names its command endpoint on another host. |
|
|
69
73
|
| `fetch` | global `fetch` | Bring your own (for a proxy, custom agent, tests). |
|
|
70
74
|
| `maxRetries` | `3` | See [Retries](#retries-and-timeouts). |
|
|
71
75
|
| `requestTimeoutMs` | `60000` | Per API request. |
|
|
72
76
|
| `pollIntervalMs` | `500` | The longest the SDK's own polling goes between checks of the sandbox (the first come sooner). It does not cover the request itself, or the time the API holds a create, pause or resume for you (up to 8 seconds). |
|
|
73
|
-
| `pollOutageMs` | `120000` | How long a
|
|
77
|
+
| `pollOutageMs` | `120000` | How long a wait keeps polling through a dropped connection (or a 5xx, or a rate limit) before it gives up: a streamed or long `exec` then stops the command, a handle's `wait()` throws. A background, streamed or long `exec` keeps sending its start for as long. `exec` and `wait()` take it per call too. |
|
|
78
|
+
|
|
79
|
+
Your API key goes with every request, so a plain `http` URL to a host other
|
|
80
|
+
than this machine is refused with `invalid_argument` before anything is sent,
|
|
81
|
+
whatever `fetch` you pass. Without `edgeUrl`, a sandbox's `toolboxBase` is used
|
|
82
|
+
only on the API's own host, on Prized's own hosts beside Prized's API, or on
|
|
83
|
+
this machine beside a local API. Otherwise commands and files go to Prized's
|
|
84
|
+
edge for Prized's API; for any other API they throw `edge_unknown`, so its key
|
|
85
|
+
never reaches Prized's edge. Take `baseUrl`, `edgeUrl` and `fetch` only from
|
|
86
|
+
configuration you trust: what they name gets your key, your commands and your
|
|
87
|
+
files.
|
|
74
88
|
|
|
75
89
|
A sandbox remembers the options it was made with: its methods need none.
|
|
76
90
|
|
|
@@ -169,8 +183,10 @@ await Promise.all(mine.map((s) => s.delete()));
|
|
|
169
183
|
### `Sandbox.deleteByLabels(labels, options?)`
|
|
170
184
|
|
|
171
185
|
Deletes every sandbox carrying all of `labels` (at least one) in one call and
|
|
172
|
-
resolves with their ids.
|
|
173
|
-
|
|
186
|
+
resolves with their ids. Each request selects the current matches, including
|
|
187
|
+
sandboxes created since an earlier request. If an answer is lost, the SDK
|
|
188
|
+
throws without repeating the delete; list the matches before deleting again.
|
|
189
|
+
A sandbox being deleted still counts
|
|
174
190
|
toward your quota until `Sandbox.list({ labels })` no longer returns it.
|
|
175
191
|
|
|
176
192
|
```ts
|
|
@@ -212,6 +228,7 @@ r.stderr; // string, kept separate
|
|
|
212
228
|
r.exitCode; // number
|
|
213
229
|
r.durationMs;
|
|
214
230
|
r.truncated; // true if stdout or stderr hit the 4 MiB per-call cap
|
|
231
|
+
r.truncatedReason; // why, when truncated: 'output_limit' or 'write_failed'
|
|
215
232
|
|
|
216
233
|
await sandbox.exec(['git', 'commit', '-m', "it's done"]); // quoted for you
|
|
217
234
|
```
|
|
@@ -225,6 +242,14 @@ small; past 4 MiB the SDK reads it back from files in
|
|
|
225
242
|
ends gets `exitCode` 128+n (a plain exec may give -1), as a background command
|
|
226
243
|
does.
|
|
227
244
|
|
|
245
|
+
`truncatedReason` says why a result is cut: `output_limit` for a stream
|
|
246
|
+
that reached its cap, `write_failed` when the sandbox could not save a
|
|
247
|
+
stream as the command printed it (most often a full disk), so it stops where
|
|
248
|
+
the write failed. If the output can't be read back afterwards, the call
|
|
249
|
+
throws `OutputReadError` (`code` `output_read_failed`): the command ran
|
|
250
|
+
(`err.exitCode`), `err.reason` says what stopped the read, and the output
|
|
251
|
+
stays at `err.stdoutPath` and `err.stderrPath` for an hour.
|
|
252
|
+
|
|
228
253
|
```ts
|
|
229
254
|
const { stdout, exitCode } = await sandbox.exec('npm test', { timeoutMs: 180_000, fullOutput: true });
|
|
230
255
|
```
|
|
@@ -266,7 +291,9 @@ off by an idle connection and its output is never truncated. The result is
|
|
|
266
291
|
the same either way. If the connection drops meanwhile, the SDK keeps
|
|
267
292
|
polling for up to 2 minutes (`pollOutageMs`, on the call or the client)
|
|
268
293
|
before it gives up and stops the command; a refused key, or an answer that
|
|
269
|
-
settles it (the sandbox deleted or not running), stops it at once.
|
|
294
|
+
settles it (the sandbox deleted or not running), stops it at once. Its
|
|
295
|
+
start rides one out the same way: it is sent again for up to `pollOutageMs`,
|
|
296
|
+
and a resend finds the first start, so the command runs once.
|
|
270
297
|
Background and streamed commands run with `PYTHONUNBUFFERED=1` unless
|
|
271
298
|
their environment already sets it (`env: { PYTHONUNBUFFERED: '' }` keeps
|
|
272
299
|
Python's buffering), so a Python program's output arrives as it prints.
|
|
@@ -280,7 +307,11 @@ you need into your home: Python packages into a venv
|
|
|
280
307
|
|
|
281
308
|
## Background commands
|
|
282
309
|
|
|
283
|
-
`background: true` starts the command and returns a `CommandHandle`
|
|
310
|
+
`background: true` starts the command and returns a `CommandHandle` once it
|
|
311
|
+
has started. A start that meets a dropped connection or a 5xx is sent again
|
|
312
|
+
for up to `pollOutageMs` once its own retries run out (a resend finds the
|
|
313
|
+
first start, so the command runs once), so the call can take that long while
|
|
314
|
+
the connection is down.
|
|
284
315
|
The command runs in its own session: it keeps running after your process
|
|
285
316
|
exits, survives a pause and resume, and while it runs, a kill reaches every
|
|
286
317
|
process it started (not one that left its process group, such as a daemon).
|
|
@@ -306,6 +337,13 @@ result.exitCode;
|
|
|
306
337
|
await job.cleanup(); // remove its output files from the sandbox
|
|
307
338
|
```
|
|
308
339
|
|
|
340
|
+
`wait()` rides out a dropped connection too: while its polls fail (no
|
|
341
|
+
answer, a 5xx, a rate limit) it keeps polling for up to `pollOutageMs` (2
|
|
342
|
+
minutes by default, on the call or the client; while the command runs,
|
|
343
|
+
never past its `timeoutMs`) before it throws what they met. A refused key,
|
|
344
|
+
or an answer that settles the run, throws at once. Either way the command
|
|
345
|
+
keeps running and the handle stays good: call `wait()` again.
|
|
346
|
+
|
|
309
347
|
A background command has no time limit unless you give `timeoutMs`
|
|
310
348
|
(enforced to the second: the command gets SIGTERM, then SIGKILL five seconds
|
|
311
349
|
later, and `wait()` throws `CommandTimeoutError`). Its output is kept under
|
|
@@ -321,9 +359,10 @@ sandbox's disk there, so redirect such output yourself.
|
|
|
321
359
|
## Files
|
|
322
360
|
|
|
323
361
|
`sandbox.files` reads and writes the sandbox's disk. Paths are absolute or
|
|
324
|
-
relative to `/home/prized/work`. Writes, reads, uploads and downloads
|
|
325
|
-
|
|
326
|
-
|
|
362
|
+
relative to `/home/prized/work`. Writes, reads, uploads and downloads are at
|
|
363
|
+
most 100 MiB each, and their paths in the sandbox must stay inside
|
|
364
|
+
`/home/prized` and not go through a symbolic link (`symlink_not_allowed`).
|
|
365
|
+
A write replaces the file whole, and a file it replaces keeps
|
|
327
366
|
its mode, so a script made executable stays executable; a new file gets the
|
|
328
367
|
sandbox's default (`0600` in `prized-base-v1` and `prized-agent-v1`). `mode` sets the permission
|
|
329
368
|
bits.
|
|
@@ -511,12 +550,14 @@ try {
|
|
|
511
550
|
| `file_too_large` | 413 | Over the 100 MiB transfer limit. |
|
|
512
551
|
| `idempotency_conflict` | 409 | The idempotency key was used for a different request. |
|
|
513
552
|
| `sandbox_error` | | The sandbox failed while you waited; `reason` says why. |
|
|
514
|
-
| `sandbox_deleted` | 410 | The sandbox was deleted before or while a command or file call ran; a command that was running stopped with it. `pause()` and `resume()` of a sandbox being deleted throw it too. Not retried. A wait that finds the sandbox deleted throws it too, with no status. |
|
|
553
|
+
| `sandbox_deleted` | 410 | The sandbox was deleted before or while a command or file call ran; a command that was running stopped with it. `pause()` and `resume()` of a sandbox being deleted throw it too. Not retried. A wait that finds the sandbox deleted throws it too, with no status. When a `fullOutput` command ran and the sandbox went before its output was read back, `err.exitCode` is the command's exit code (null otherwise): it ran, so don't run it again blindly in a new sandbox. |
|
|
515
554
|
| `command_timeout` | | A command ran past its own `timeoutMs` and was killed. Thrown as `CommandTimeoutError`; `err.result` has what it printed. |
|
|
555
|
+
| `output_read_failed` | | A `fullOutput` command ran, but its output could not be read back. Thrown as `OutputReadError`: `err.exitCode` is the command's, `err.reason` the code of what stopped the read (also in `cause`), and the output stays at `err.stdoutPath` / `err.stderrPath` for an hour. The sandbox is not known to be gone (a deleted one throws `sandbox_deleted`); don't run the command again blindly. |
|
|
516
556
|
| `timeout` | | A wait ran past its `timeoutMs`, or a call past its bound ([Retries](#retries-and-timeouts)). One that ended because a retry could not start in time says what the last attempt met (also in `cause`). |
|
|
517
557
|
| `network_error` | | No answer: the API or the sandbox could not be reached, or the connection dropped. |
|
|
518
558
|
| `bad_response` | 2xx | An answer the SDK could not read. For a command or file write it did happen, and the message says so. |
|
|
519
559
|
| `missing_api_key` / `invalid_api_key` / `invalid_argument` | | Fix the call. `invalid_api_key`: the key has characters no key has (the docs' placeholder, a stray character); it is never quoted back. |
|
|
560
|
+
| `edge_unknown` | | A `baseUrl` other than Prized's whose API named no command endpoint the SDK trusts (on the API's own host, or on this machine beside a local API): pass `edgeUrl`. The command or file call was not sent. |
|
|
520
561
|
| `command_failed` | | A file helper (`list`, `remove`, `exists`) failed in the sandbox; `message` has its stderr. |
|
|
521
562
|
|
|
522
563
|
## Retries and timeouts
|
|
@@ -534,15 +575,23 @@ try {
|
|
|
534
575
|
`resume`, `fork` and `delete` (the request, its retries and the wait, its
|
|
535
576
|
polls included; with `wait: false` too), twice `requestTimeoutMs` (2
|
|
536
577
|
minutes by default) for `connect`, `list`, `templates`, `refresh` and
|
|
537
|
-
`keepAlive`, and 10 minutes for a file transfer
|
|
578
|
+
`keepAlive`, and 10 minutes for a file transfer, whose attempt is given up
|
|
579
|
+
after 2 minutes with no bytes moving (a dropped connection) and sent again
|
|
580
|
+
by the rules below (on Node's own `fetch`; elsewhere, or with a `fetch` you
|
|
581
|
+
pass, an upload is watched only once its answer starts). `Retry-After` is honoured
|
|
538
582
|
up to a minute. A retry that could not start in time is not sent: the call
|
|
539
583
|
throws `timeout`. A wait's last poll goes out just before its deadline.
|
|
540
|
-
- A command,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
584
|
+
- A command, a `files.remove`, a `kill` or `deleteByLabels` is sent again only when it
|
|
585
|
+
provably never arrived: the connection never opened, or the answer says so
|
|
586
|
+
(rate limited, the sandbox not ready). If an answer is lost after it was
|
|
587
|
+
delivered, the SDK throws (`sandbox_unreachable`, `network_error`,
|
|
588
|
+
`timeout`) rather than risk repeating the operation on work that changed.
|
|
589
|
+
- A file write (`files.write`, `files.upload`) goes up under a temporary name
|
|
590
|
+
beside the file, which is sent again like a read (a lost answer, a 502, 503
|
|
591
|
+
or 504, 2 minutes with no bytes moving), every copy to the same name and
|
|
592
|
+
none after the transfer's 10 minutes. One command that is never
|
|
593
|
+
sent again then puts it in place: if that command's answer is lost, the SDK
|
|
594
|
+
throws rather than risk overwriting what the sandbox wrote to the file since.
|
|
546
595
|
- Did it run? When a command answered in one call (`exec` without
|
|
547
596
|
`background`, `onStdout` or `onStderr`, and a `timeoutMs` other than 0 and
|
|
548
597
|
at most 4 minutes), a file write or a `files.remove` fails without a plain refusal,
|
|
@@ -552,10 +601,14 @@ try {
|
|
|
552
601
|
clear answer came back (a dropped connection, a timeout, or any other 5xx):
|
|
553
602
|
it may or may not have run, and the message says how to check (read the
|
|
554
603
|
file back, `files.exists()`). A 2xx came back that could not be read
|
|
555
|
-
(`bad_response`): it ran. A
|
|
604
|
+
(`bad_response`): it ran. A write whose upload got no clear answer says the file was
|
|
605
|
+
not changed (its new bytes may have reached the sandbox under the
|
|
606
|
+
temporary name, which the SDK then removes): write it again. A `kill`
|
|
607
|
+
says the same of its signal
|
|
556
608
|
(`status()` tells you whether the command is still running). A background
|
|
557
609
|
command (or a streamed or long `exec`, which runs as one) whose start got
|
|
558
|
-
no clear answer is stopped by the SDK, and
|
|
610
|
+
no clear answer for longer than `pollOutageMs` is stopped by the SDK, and
|
|
611
|
+
the error says so, or, when the
|
|
559
612
|
SDK could not confirm the stop, that it may still be running and under
|
|
560
613
|
which run id. So is a streamed or long `exec` whose polling fails while
|
|
561
614
|
it runs (a dropped connection or a 5xx for longer than `pollOutageMs`, a
|
|
@@ -563,6 +616,8 @@ try {
|
|
|
563
616
|
poll's own error as its `cause`, and says whether the SDK stopped the
|
|
564
617
|
command or it may still be running. An error that settles it (the
|
|
565
618
|
sandbox deleted or not running, the run not found) is thrown as it came.
|
|
619
|
+
A handle's `wait()` whose polls fail for longer than `pollOutageMs` throws
|
|
620
|
+
the poll's own error; it stops nothing.
|
|
566
621
|
- Deleting what a failed create or fork made (a few seconds) comes on top
|
|
567
622
|
of `timeoutMs`.
|
|
568
623
|
- `create`, `exec` and waits take an `AbortSignal` (`signal`). Aborting a
|
package/dist/cjs/commands.d.ts
CHANGED
|
@@ -19,20 +19,34 @@ export declare const MAX_BLOCKING_EXEC_MS = 240000;
|
|
|
19
19
|
export declare function ridesOut(error: PrizedError): boolean;
|
|
20
20
|
/**
|
|
21
21
|
* The failed polls of a wait that rides out an outage of up to `windowMs`
|
|
22
|
-
* (
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* (pollOutageMs), from when the first poll's retries ran out, and, while the
|
|
23
|
+
* command runs, never past the wait's own deadline (`waitDeadline`, epoch
|
|
24
|
+
* ms; null for none): polled again with backoff, each poll and its retries
|
|
25
|
+
* ending before the last look, which goes out LAST_LOOK_MAX_MS before the
|
|
26
|
+
* window's end (half the window, for one shorter than twice that) with room
|
|
27
|
+
* for its own retries. So an outage that ends inside the window is always
|
|
28
|
+
* seen to end. The Python SDK's Outage.
|
|
28
29
|
*/
|
|
29
30
|
export declare class Outage {
|
|
30
31
|
private readonly windowMs;
|
|
32
|
+
private readonly waitDeadline;
|
|
31
33
|
private end;
|
|
32
34
|
private last;
|
|
33
35
|
private tries;
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
+
private room;
|
|
37
|
+
private finished;
|
|
38
|
+
constructor(windowMs: number, waitDeadline?: number | null);
|
|
39
|
+
/** The command has ended: draining its output, and an outage meanwhile, are not held to the deadline. */
|
|
40
|
+
finish(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Whether a wait's failed poll (`error`, at `now`; `cut`: its bound cut it
|
|
43
|
+
* short) means the wait's own time ran out: its deadline cut the poll
|
|
44
|
+
* short while the command ran, with no outage being ridden out, so the
|
|
45
|
+
* command was still running when last seen. With no window, any timeout
|
|
46
|
+
* while it runs says so, as it always has. Never once the command has
|
|
47
|
+
* ended. The Python SDK's Outage.ran_out.
|
|
48
|
+
*/
|
|
49
|
+
ranOut(error: PrizedError, now: number, cut: boolean): boolean;
|
|
36
50
|
/** The bound on a poll sent `now`: undefined unless polls are failing; else the last look's start, or the window's end for the last look. */
|
|
37
51
|
deadline(now: number): number | undefined;
|
|
38
52
|
/** A poll was answered: the outage, if any, is over. */
|
|
@@ -90,16 +104,33 @@ export interface ExecOptions {
|
|
|
90
104
|
* its polls fail (no answer, a 5xx, a rate limit), in ms, before it gives
|
|
91
105
|
* up and stops the command. Default: the client's `pollOutageMs`
|
|
92
106
|
* (120 000). A refused key, or an answer that settles the run (the
|
|
93
|
-
* sandbox deleted or not running, the run gone), stops it at once.
|
|
107
|
+
* sandbox deleted or not running, the run gone), stops it at once. The
|
|
108
|
+
* start of a background, streamed or long command is sent again for as
|
|
109
|
+
* long (a resend finds the first start, so the command runs once).
|
|
94
110
|
*/
|
|
95
111
|
pollOutageMs?: number;
|
|
96
112
|
}
|
|
97
113
|
export interface WaitOptions {
|
|
98
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Stop waiting after this many ms (the command keeps running). Default: no
|
|
116
|
+
* limit. Once the command has ended, reading the rest of its output is not
|
|
117
|
+
* held to it.
|
|
118
|
+
*/
|
|
99
119
|
timeoutMs?: number;
|
|
100
120
|
onStdout?: (chunk: string) => void;
|
|
101
121
|
onStderr?: (chunk: string) => void;
|
|
102
122
|
signal?: AbortSignal;
|
|
123
|
+
/**
|
|
124
|
+
* How long the wait keeps polling while its polls fail (no answer, a 5xx,
|
|
125
|
+
* a rate limit), in ms, before it gives up and throws what the polls met.
|
|
126
|
+
* While the command runs, never past `timeoutMs`. Default: the client's
|
|
127
|
+
* `pollOutageMs` (120 000).
|
|
128
|
+
* `0` gives up once one poll's retries run out. A refused key, or an
|
|
129
|
+
* answer that settles the run (the sandbox deleted or not running, the
|
|
130
|
+
* run gone), ends the wait at once. Giving up stops nothing: the command
|
|
131
|
+
* keeps running, and wait() can be called again.
|
|
132
|
+
*/
|
|
133
|
+
pollOutageMs?: number;
|
|
103
134
|
}
|
|
104
135
|
export interface CommandStatus {
|
|
105
136
|
running: boolean;
|
|
@@ -136,15 +167,17 @@ export declare class CommandHandle {
|
|
|
136
167
|
* `onStdout`/`onStderr` receive the output as it arrives, from the start.
|
|
137
168
|
* A `timeoutMs` here bounds the wait only; the command keeps running. If
|
|
138
169
|
* the command ran past its own timeout (the one given to exec), it was
|
|
139
|
-
* killed and this throws CommandTimeoutError with what it printed.
|
|
170
|
+
* killed and this throws CommandTimeoutError with what it printed. While
|
|
171
|
+
* polls fail (a dropped connection, a 5xx), it keeps polling for up to
|
|
172
|
+
* `pollOutageMs` (the client's, 120 000 by default) before it throws.
|
|
140
173
|
*/
|
|
141
174
|
wait(options?: WaitOptions): Promise<ExecResult>;
|
|
142
175
|
/**
|
|
143
176
|
* @internal wait() without the timeout check: exec cleans up before it
|
|
144
|
-
* throws. `outageMs` (
|
|
145
|
-
*
|
|
177
|
+
* throws. `outageMs` (pollOutageMs): how long polls may fail before the
|
|
178
|
+
* wait gives up (Outage), never past the wait's own deadline.
|
|
146
179
|
*/
|
|
147
|
-
waitRaw(options
|
|
180
|
+
waitRaw(options: WaitOptions, outageMs: number): Promise<ExecResult>;
|
|
148
181
|
private waitTimeout;
|
|
149
182
|
private result;
|
|
150
183
|
/**
|
package/dist/cjs/commands.js
CHANGED
|
@@ -70,22 +70,42 @@ function ridesOut(error) {
|
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* The failed polls of a wait that rides out an outage of up to `windowMs`
|
|
73
|
-
* (
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
73
|
+
* (pollOutageMs), from when the first poll's retries ran out, and, while the
|
|
74
|
+
* command runs, never past the wait's own deadline (`waitDeadline`, epoch
|
|
75
|
+
* ms; null for none): polled again with backoff, each poll and its retries
|
|
76
|
+
* ending before the last look, which goes out LAST_LOOK_MAX_MS before the
|
|
77
|
+
* window's end (half the window, for one shorter than twice that) with room
|
|
78
|
+
* for its own retries. So an outage that ends inside the window is always
|
|
79
|
+
* seen to end. The Python SDK's Outage.
|
|
79
80
|
*/
|
|
80
81
|
class Outage {
|
|
81
82
|
windowMs;
|
|
83
|
+
waitDeadline;
|
|
82
84
|
end = null;
|
|
83
85
|
last = null;
|
|
84
86
|
tries = 0;
|
|
85
|
-
room;
|
|
86
|
-
|
|
87
|
+
room = 0;
|
|
88
|
+
finished = false;
|
|
89
|
+
constructor(windowMs, waitDeadline = null) {
|
|
87
90
|
this.windowMs = windowMs;
|
|
88
|
-
this.
|
|
91
|
+
this.waitDeadline = waitDeadline;
|
|
92
|
+
}
|
|
93
|
+
/** The command has ended: draining its output, and an outage meanwhile, are not held to the deadline. */
|
|
94
|
+
finish() {
|
|
95
|
+
this.finished = true;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether a wait's failed poll (`error`, at `now`; `cut`: its bound cut it
|
|
99
|
+
* short) means the wait's own time ran out: its deadline cut the poll
|
|
100
|
+
* short while the command ran, with no outage being ridden out, so the
|
|
101
|
+
* command was still running when last seen. With no window, any timeout
|
|
102
|
+
* while it runs says so, as it always has. Never once the command has
|
|
103
|
+
* ended. The Python SDK's Outage.ran_out.
|
|
104
|
+
*/
|
|
105
|
+
ranOut(error, now, cut) {
|
|
106
|
+
if (this.waitDeadline === null || this.finished || error.code !== 'timeout')
|
|
107
|
+
return false;
|
|
108
|
+
return this.windowMs <= 0 || (this.end === null && (cut || now >= this.waitDeadline));
|
|
89
109
|
}
|
|
90
110
|
/** The bound on a poll sent `now`: undefined unless polls are failing; else the last look's start, or the window's end for the last look. */
|
|
91
111
|
deadline(now) {
|
|
@@ -109,7 +129,10 @@ class Outage {
|
|
|
109
129
|
failed(error, sentAt, now, cut, backoff) {
|
|
110
130
|
if (this.windowMs <= 0 || !ridesOut(error))
|
|
111
131
|
return { pause: null, failure: error };
|
|
112
|
-
this.end
|
|
132
|
+
if (this.end === null) {
|
|
133
|
+
this.end = Math.min(now + this.windowMs, this.finished || this.waitDeadline === null ? Infinity : this.waitDeadline);
|
|
134
|
+
this.room = Math.max(0, Math.min(http_js_1.LAST_LOOK_MAX_MS, (this.end - now) / 2));
|
|
135
|
+
}
|
|
113
136
|
const met = error.cause instanceof errors_js_1.PrizedError ? error.cause : null;
|
|
114
137
|
if (!cut) {
|
|
115
138
|
this.last = error;
|
|
@@ -278,17 +301,20 @@ class CommandHandle {
|
|
|
278
301
|
* `onStdout`/`onStderr` receive the output as it arrives, from the start.
|
|
279
302
|
* A `timeoutMs` here bounds the wait only; the command keeps running. If
|
|
280
303
|
* the command ran past its own timeout (the one given to exec), it was
|
|
281
|
-
* killed and this throws CommandTimeoutError with what it printed.
|
|
304
|
+
* killed and this throws CommandTimeoutError with what it printed. While
|
|
305
|
+
* polls fail (a dropped connection, a 5xx), it keeps polling for up to
|
|
306
|
+
* `pollOutageMs` (the client's, 120 000 by default) before it throws.
|
|
282
307
|
*/
|
|
283
308
|
async wait(options = {}) {
|
|
284
|
-
|
|
309
|
+
const outageMs = (0, http_js_1.validOutage)(options.pollOutageMs) ?? this.toolbox.config.pollOutageMs;
|
|
310
|
+
return finished(await this.waitRaw(options, outageMs), this.timeoutMs, this.sandboxId);
|
|
285
311
|
}
|
|
286
312
|
/**
|
|
287
313
|
* @internal wait() without the timeout check: exec cleans up before it
|
|
288
|
-
* throws. `outageMs` (
|
|
289
|
-
*
|
|
314
|
+
* throws. `outageMs` (pollOutageMs): how long polls may fail before the
|
|
315
|
+
* wait gives up (Outage), never past the wait's own deadline.
|
|
290
316
|
*/
|
|
291
|
-
async waitRaw(options
|
|
317
|
+
async waitRaw(options, outageMs) {
|
|
292
318
|
const config = this.toolbox.config;
|
|
293
319
|
const deadline = options.timeoutMs ? config.now() + options.timeoutMs : null;
|
|
294
320
|
const out = new StreamReader(options.onStdout);
|
|
@@ -296,7 +322,7 @@ class CommandHandle {
|
|
|
296
322
|
let delay = POLL_MIN_MS;
|
|
297
323
|
let finished = false;
|
|
298
324
|
const polls = deadline === null ? null : new http_js_1.Polls(config, deadline);
|
|
299
|
-
const outage = new Outage(
|
|
325
|
+
const outage = new Outage(outageMs, deadline);
|
|
300
326
|
for (;;) {
|
|
301
327
|
let report;
|
|
302
328
|
const sentAt = config.now();
|
|
@@ -311,13 +337,15 @@ class CommandHandle {
|
|
|
311
337
|
polls?.answered();
|
|
312
338
|
}
|
|
313
339
|
catch (error) {
|
|
314
|
-
|
|
340
|
+
const now = config.now();
|
|
341
|
+
if (error instanceof errors_js_1.PrizedError && outage.ranOut(error, now, (0, http_js_1.cutByDeadline)(error))) {
|
|
315
342
|
throw this.waitTimeout(options.timeoutMs);
|
|
343
|
+
}
|
|
316
344
|
let failure = error;
|
|
317
345
|
if (error instanceof errors_js_1.PrizedError && !options.signal?.aborted) {
|
|
318
346
|
// A dropped connection, an edge restarting: the command may be
|
|
319
347
|
// fine. Poll again until the window runs out, then give up.
|
|
320
|
-
const step = outage.failed(error, sentAt,
|
|
348
|
+
const step = outage.failed(error, sentAt, now, (0, http_js_1.cutByDeadline)(error), (tries) => (0, http_js_1.backoffMs)(config, tries));
|
|
321
349
|
if (step.pause !== null) {
|
|
322
350
|
await config.sleep(step.pause, options.signal);
|
|
323
351
|
continue;
|
|
@@ -329,8 +357,10 @@ class CommandHandle {
|
|
|
329
357
|
throw failure;
|
|
330
358
|
}
|
|
331
359
|
outage.answered();
|
|
332
|
-
if (report.state !== 'running')
|
|
360
|
+
if (report.state !== 'running') {
|
|
333
361
|
finished = true;
|
|
362
|
+
outage.finish();
|
|
363
|
+
}
|
|
334
364
|
out.push(report.stdout);
|
|
335
365
|
err.push(report.stderr);
|
|
336
366
|
const more = (report.stdout.length > 0 && out.offset < report.outSize) ||
|
|
@@ -447,7 +477,48 @@ async function cleanupOnceGone(toolbox, id) {
|
|
|
447
477
|
}
|
|
448
478
|
}
|
|
449
479
|
}
|
|
450
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Send a start script. It is keyed by a run id the SDK chose, so a resend
|
|
482
|
+
* finds the first start instead of starting the command twice. Once one
|
|
483
|
+
* copy's retries run out, it is sent again through an outage of up to
|
|
484
|
+
* `outageMs` (pollOutageMs), paced as a wait's polls are (Outage): an edge
|
|
485
|
+
* that restarts mid-start does not cost the command, as it does not cost a
|
|
486
|
+
* running one its wait. Each copy's error counts the copies before it
|
|
487
|
+
* (`acted`), so one that gives up says the command may have started when any
|
|
488
|
+
* copy may have reached the sandbox. The Python SDK's _send_start.
|
|
489
|
+
*/
|
|
490
|
+
async function sendStart(toolbox, req, signal, outageMs) {
|
|
491
|
+
const config = toolbox.config;
|
|
492
|
+
const outage = new Outage(outageMs);
|
|
493
|
+
// Whether an earlier copy may have reached the sandbox: an abort between
|
|
494
|
+
// copies then says the command may have started, else that it never did.
|
|
495
|
+
let acted = false;
|
|
496
|
+
const aborted = (cause) => (0, outcome_js_1.withAbort)(new errors_js_1.PrizedError('aborted', null, { sandboxId: toolbox.sandboxId, cause }), (0, outcome_js_1.startEffect)(), acted ? 'maybe' : 'never');
|
|
497
|
+
for (;;) {
|
|
498
|
+
const sentAt = config.now();
|
|
499
|
+
try {
|
|
500
|
+
return await (0, toolbox_js_1.rawExec)(toolbox, req, { retry: 'safe', signal, deadline: outage.deadline(sentAt), effect: (0, outcome_js_1.startEffect)(), acted });
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
if (!(error instanceof errors_js_1.PrizedError))
|
|
504
|
+
throw error;
|
|
505
|
+
acted = acted || (0, outcome_js_1.mayHaveActed)(error);
|
|
506
|
+
if (signal?.aborted)
|
|
507
|
+
throw error;
|
|
508
|
+
const step = outage.failed(error, sentAt, config.now(), (0, http_js_1.cutByDeadline)(error), (tries) => (0, http_js_1.backoffMs)(config, tries));
|
|
509
|
+
// What a copy met, not the window's cut.
|
|
510
|
+
if (step.pause === null)
|
|
511
|
+
throw step.failure === error ? error : (0, outcome_js_1.startGaveUp)(step.failure, acted);
|
|
512
|
+
try {
|
|
513
|
+
await config.sleep(step.pause, signal);
|
|
514
|
+
}
|
|
515
|
+
catch (abort) {
|
|
516
|
+
throw abort instanceof errors_js_1.PrizedError && abort.code === 'aborted' ? aborted(step.failure) : abort;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, unbuffered, outageMs) {
|
|
451
522
|
// Nothing sent yet: nothing to stop.
|
|
452
523
|
if (signal?.aborted)
|
|
453
524
|
throw (0, outcome_js_1.withOutcome)(new errors_js_1.PrizedError('aborted', null, { sandboxId: toolbox.sandboxId }), (0, outcome_js_1.startEffect)(), 'never');
|
|
@@ -459,10 +530,7 @@ async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, un
|
|
|
459
530
|
// Launched, but no pid came within the start's own wait: it may yet run.
|
|
460
531
|
let unsettled = false;
|
|
461
532
|
try {
|
|
462
|
-
const result = await (
|
|
463
|
-
// Keyed by a run id this call chose, so a resend finds the first start
|
|
464
|
-
// instead of starting the command twice.
|
|
465
|
-
{ retry: 'safe', signal, effect: (0, outcome_js_1.startEffect)() });
|
|
533
|
+
const result = await sendStart(toolbox, { command: (0, shell_js_1.startScript)(id, command, timeoutMs === null ? null : timeoutMs / 1000, unbuffered), cwd, env, timeoutMs: 60_000 }, signal, outageMs);
|
|
466
534
|
pid = Number(result.stdout.trim());
|
|
467
535
|
if (result.exitCode !== 0 || !Number.isInteger(pid) || pid <= 0) {
|
|
468
536
|
unsettled = result.exitCode === shell_js_1.NO_PID_EXIT;
|
|
@@ -508,7 +576,7 @@ async function execCommand(toolbox, command, options = {}) {
|
|
|
508
576
|
const cwd = options.cwd === undefined ? undefined : (0, shell_js_1.sandboxPath)(options.cwd);
|
|
509
577
|
if (options.background) {
|
|
510
578
|
const limit = options.timeoutMs ? options.timeoutMs : null;
|
|
511
|
-
const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, true);
|
|
579
|
+
const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, true, outageMs);
|
|
512
580
|
handle.timeoutMs = limit;
|
|
513
581
|
return handle;
|
|
514
582
|
}
|
|
@@ -516,7 +584,7 @@ async function execCommand(toolbox, command, options = {}) {
|
|
|
516
584
|
const streamed = options.onStdout !== undefined || options.onStderr !== undefined;
|
|
517
585
|
if (streamed || timeoutMs === 0 || timeoutMs > exports.MAX_BLOCKING_EXEC_MS) {
|
|
518
586
|
const limit = timeoutMs === 0 ? null : timeoutMs;
|
|
519
|
-
const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, streamed);
|
|
587
|
+
const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, streamed, outageMs);
|
|
520
588
|
const waitOptions = {};
|
|
521
589
|
if (options.onStdout)
|
|
522
590
|
waitOptions.onStdout = options.onStdout;
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import type { ExecResult } from './types.js';
|
|
|
3
3
|
export declare const API_KEYS_URL = "https://prized.dev/dashboard/workspace/api-keys";
|
|
4
4
|
/** Where a newcomer signs up (free credit, no card) and gets one. */
|
|
5
5
|
export declare const SIGN_UP_URL = "https://prized.dev/onboarding/sandboxes";
|
|
6
|
+
/** What a coding agent follows to sign its person up and get a key itself:
|
|
7
|
+
* their email address, then the 6-digit code Prized emails them. The SDK's
|
|
8
|
+
* own key errors name it; a refused key's 401 carries the API's own line
|
|
9
|
+
* (`signUp`), which follows the deployment. */
|
|
10
|
+
export declare const AGENT_SIGN_UP_URL = "https://prized.dev/auth.md";
|
|
6
11
|
/** The free tier's limits, and what lifts them. */
|
|
7
12
|
export declare const FREE_TIER_URL = "https://prized.dev/docs/sandboxes#free-tier";
|
|
8
13
|
/** The sentence an error with this code and no message of its own carries. */
|
|
@@ -17,6 +22,8 @@ export interface PrizedErrorInit {
|
|
|
17
22
|
body?: Record<string, unknown> | null;
|
|
18
23
|
/** Seconds the API asked the caller to wait (Retry-After). */
|
|
19
24
|
retryAfter?: number | null;
|
|
25
|
+
/** The command's exit code, for an error that came after it ran. */
|
|
26
|
+
exitCode?: number | null;
|
|
20
27
|
cause?: unknown;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
@@ -27,13 +34,21 @@ export interface PrizedErrorInit {
|
|
|
27
34
|
* - `code`: the API's error string, e.g. `paid_balance_required`,
|
|
28
35
|
* `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
|
|
29
36
|
* `rate_limited`; or one of the SDK's own: `missing_api_key`,
|
|
30
|
-
* `invalid_api_key`, `invalid_argument`, `
|
|
37
|
+
* `invalid_api_key`, `invalid_argument`, `edge_unknown` (another
|
|
38
|
+
* deployment's API named no command endpoint this SDK sends the key to),
|
|
39
|
+
* `network_error`, `timeout`, `aborted`,
|
|
31
40
|
* `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
|
|
32
|
-
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError)
|
|
41
|
+
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
|
|
42
|
+
* `output_read_failed` (thrown as OutputReadError).
|
|
33
43
|
* - `reason`: the finer-grained reason, when there is one (for
|
|
34
44
|
* `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
|
|
35
45
|
* `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
|
|
36
|
-
* `max_runtime`
|
|
46
|
+
* `max_runtime`; for `output_read_failed`, the code of the failure that
|
|
47
|
+
* stopped the read).
|
|
48
|
+
* - `exitCode`: the command's exit code when the error came after it ran: a
|
|
49
|
+
* `fullOutput` command whose output could not be read back
|
|
50
|
+
* (`output_read_failed`, or `sandbox_deleted` when the sandbox was deleted
|
|
51
|
+
* first). Don't run such a command again blindly. Null otherwise.
|
|
37
52
|
*/
|
|
38
53
|
export declare class PrizedError extends Error {
|
|
39
54
|
readonly status: number | null;
|
|
@@ -42,6 +57,7 @@ export declare class PrizedError extends Error {
|
|
|
42
57
|
readonly sandboxId: string | null;
|
|
43
58
|
readonly body: Record<string, unknown> | null;
|
|
44
59
|
readonly retryAfter: number | null;
|
|
60
|
+
readonly exitCode: number | null;
|
|
45
61
|
constructor(code: string, message?: string | null, init?: PrizedErrorInit);
|
|
46
62
|
toString(): string;
|
|
47
63
|
}
|
|
@@ -61,6 +77,30 @@ export declare class CommandTimeoutError extends PrizedError {
|
|
|
61
77
|
/** What the command printed to stderr before it was killed. */
|
|
62
78
|
get stderr(): string;
|
|
63
79
|
}
|
|
80
|
+
export interface OutputReadErrorInit {
|
|
81
|
+
/** The failure that stopped the read: a download's error. */
|
|
82
|
+
cause: PrizedError;
|
|
83
|
+
exitCode: number;
|
|
84
|
+
stdoutPath: string;
|
|
85
|
+
stderrPath: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A `fullOutput` command ran, but its output could not be read back from the
|
|
89
|
+
* files it was copied to (`code` is `output_read_failed`). The command did
|
|
90
|
+
* run (`exitCode` is its exit code), so do not run it again blindly; and the
|
|
91
|
+
* sandbox is not known to be gone (a deleted one throws `sandbox_deleted`
|
|
92
|
+
* instead). `reason` is the code of the failure that stopped the read (say
|
|
93
|
+
* `not_found`, `network_error`), and `cause` is that error, with its HTTP
|
|
94
|
+
* status; `status` is null, as the command's own call succeeded. The output
|
|
95
|
+
* stays in the sandbox for an hour at `stdoutPath` and `stderrPath`: read it
|
|
96
|
+
* with files.read(), then remove their directory.
|
|
97
|
+
*/
|
|
98
|
+
export declare class OutputReadError extends PrizedError {
|
|
99
|
+
readonly exitCode: number;
|
|
100
|
+
readonly stdoutPath: string;
|
|
101
|
+
readonly stderrPath: string;
|
|
102
|
+
constructor(message: string, init: OutputReadErrorInit);
|
|
103
|
+
}
|
|
64
104
|
/** Retry-After as seconds: delta-seconds or an HTTP date. */
|
|
65
105
|
export declare function parseRetryAfter(value: string | null, nowMs: number): number | null;
|
|
66
106
|
/**
|