@prized/sdk 0.1.0 → 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.
- package/README.md +56 -15
- package/dist/cjs/commands.d.ts +44 -13
- package/dist/cjs/commands.js +49 -19
- package/dist/cjs/errors.d.ts +43 -3
- package/dist/cjs/errors.js +54 -6
- package/dist/cjs/files.d.ts +20 -5
- package/dist/cjs/files.js +35 -14
- package/dist/cjs/http.d.ts +29 -13
- package/dist/cjs/http.js +68 -20
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/outcome.d.ts +2 -1
- package/dist/cjs/outcome.js +7 -4
- package/dist/cjs/output.d.ts +19 -8
- package/dist/cjs/output.js +151 -56
- package/dist/cjs/sandbox.js +28 -2
- package/dist/cjs/toolbox.js +3 -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 +44 -13
- package/dist/esm/commands.js +49 -19
- package/dist/esm/errors.d.ts +43 -3
- package/dist/esm/errors.js +52 -5
- package/dist/esm/files.d.ts +20 -5
- package/dist/esm/files.js +35 -14
- package/dist/esm/http.d.ts +29 -13
- package/dist/esm/http.js +68 -20
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/outcome.d.ts +2 -1
- package/dist/esm/outcome.js +7 -4
- package/dist/esm/output.d.ts +19 -8
- package/dist/esm/output.js +151 -57
- package/dist/esm/sandbox.js +28 -2
- package/dist/esm/toolbox.js +3 -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_…' });
|
|
@@ -64,13 +66,23 @@ client options:
|
|
|
64
66
|
| Option | Default | |
|
|
65
67
|
|---|---|---|
|
|
66
68
|
| `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. |
|
|
69
|
+
| `baseUrl` | `https://api.prized.dev` | The API origin: `https`, or plain `http` on `localhost`, `127.0.0.1` or `[::1]`. |
|
|
70
|
+
| `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
71
|
| `fetch` | global `fetch` | Bring your own (for a proxy, custom agent, tests). |
|
|
70
72
|
| `maxRetries` | `3` | See [Retries](#retries-and-timeouts). |
|
|
71
73
|
| `requestTimeoutMs` | `60000` | Per API request. |
|
|
72
74
|
| `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
|
|
75
|
+
| `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. `exec` and `wait()` take it per call too. |
|
|
76
|
+
|
|
77
|
+
Your API key goes with every request, so a plain `http` URL to a host other
|
|
78
|
+
than this machine is refused with `invalid_argument` before anything is sent,
|
|
79
|
+
whatever `fetch` you pass. Without `edgeUrl`, a sandbox's `toolboxBase` is used
|
|
80
|
+
only on the API's own host, on Prized's own hosts beside Prized's API, or on
|
|
81
|
+
this machine beside a local API. Otherwise commands and files go to Prized's
|
|
82
|
+
edge for Prized's API; for any other API they throw `edge_unknown`, so its key
|
|
83
|
+
never reaches Prized's edge. Take `baseUrl`, `edgeUrl` and `fetch` only from
|
|
84
|
+
configuration you trust: what they name gets your key, your commands and your
|
|
85
|
+
files.
|
|
74
86
|
|
|
75
87
|
A sandbox remembers the options it was made with: its methods need none.
|
|
76
88
|
|
|
@@ -212,6 +224,7 @@ r.stderr; // string, kept separate
|
|
|
212
224
|
r.exitCode; // number
|
|
213
225
|
r.durationMs;
|
|
214
226
|
r.truncated; // true if stdout or stderr hit the 4 MiB per-call cap
|
|
227
|
+
r.truncatedReason; // why, when truncated: 'output_limit' or 'write_failed'
|
|
215
228
|
|
|
216
229
|
await sandbox.exec(['git', 'commit', '-m', "it's done"]); // quoted for you
|
|
217
230
|
```
|
|
@@ -225,6 +238,14 @@ small; past 4 MiB the SDK reads it back from files in
|
|
|
225
238
|
ends gets `exitCode` 128+n (a plain exec may give -1), as a background command
|
|
226
239
|
does.
|
|
227
240
|
|
|
241
|
+
`truncatedReason` says why a result is cut: `output_limit` for a stream
|
|
242
|
+
that reached its cap, `write_failed` when the sandbox could not save a
|
|
243
|
+
stream as the command printed it (most often a full disk), so it stops where
|
|
244
|
+
the write failed. If the output can't be read back afterwards, the call
|
|
245
|
+
throws `OutputReadError` (`code` `output_read_failed`): the command ran
|
|
246
|
+
(`err.exitCode`), `err.reason` says what stopped the read, and the output
|
|
247
|
+
stays at `err.stdoutPath` and `err.stderrPath` for an hour.
|
|
248
|
+
|
|
228
249
|
```ts
|
|
229
250
|
const { stdout, exitCode } = await sandbox.exec('npm test', { timeoutMs: 180_000, fullOutput: true });
|
|
230
251
|
```
|
|
@@ -306,6 +327,13 @@ result.exitCode;
|
|
|
306
327
|
await job.cleanup(); // remove its output files from the sandbox
|
|
307
328
|
```
|
|
308
329
|
|
|
330
|
+
`wait()` rides out a dropped connection too: while its polls fail (no
|
|
331
|
+
answer, a 5xx, a rate limit) it keeps polling for up to `pollOutageMs` (2
|
|
332
|
+
minutes by default, on the call or the client; while the command runs,
|
|
333
|
+
never past its `timeoutMs`) before it throws what they met. A refused key,
|
|
334
|
+
or an answer that settles the run, throws at once. Either way the command
|
|
335
|
+
keeps running and the handle stays good: call `wait()` again.
|
|
336
|
+
|
|
309
337
|
A background command has no time limit unless you give `timeoutMs`
|
|
310
338
|
(enforced to the second: the command gets SIGTERM, then SIGKILL five seconds
|
|
311
339
|
later, and `wait()` throws `CommandTimeoutError`). Its output is kept under
|
|
@@ -321,9 +349,10 @@ sandbox's disk there, so redirect such output yourself.
|
|
|
321
349
|
## Files
|
|
322
350
|
|
|
323
351
|
`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
|
-
|
|
352
|
+
relative to `/home/prized/work`. Writes, reads, uploads and downloads are at
|
|
353
|
+
most 100 MiB each, and their paths in the sandbox must stay inside
|
|
354
|
+
`/home/prized` and not go through a symbolic link (`symlink_not_allowed`).
|
|
355
|
+
A write replaces the file whole, and a file it replaces keeps
|
|
327
356
|
its mode, so a script made executable stays executable; a new file gets the
|
|
328
357
|
sandbox's default (`0600` in `prized-base-v1` and `prized-agent-v1`). `mode` sets the permission
|
|
329
358
|
bits.
|
|
@@ -511,12 +540,14 @@ try {
|
|
|
511
540
|
| `file_too_large` | 413 | Over the 100 MiB transfer limit. |
|
|
512
541
|
| `idempotency_conflict` | 409 | The idempotency key was used for a different request. |
|
|
513
542
|
| `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. |
|
|
543
|
+
| `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
544
|
| `command_timeout` | | A command ran past its own `timeoutMs` and was killed. Thrown as `CommandTimeoutError`; `err.result` has what it printed. |
|
|
545
|
+
| `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
546
|
| `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
547
|
| `network_error` | | No answer: the API or the sandbox could not be reached, or the connection dropped. |
|
|
518
548
|
| `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
549
|
| `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. |
|
|
550
|
+
| `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
551
|
| `command_failed` | | A file helper (`list`, `remove`, `exists`) failed in the sandbox; `message` has its stderr. |
|
|
521
552
|
|
|
522
553
|
## Retries and timeouts
|
|
@@ -537,12 +568,17 @@ try {
|
|
|
537
568
|
`keepAlive`, and 10 minutes for a file transfer. `Retry-After` is honoured
|
|
538
569
|
up to a minute. A retry that could not start in time is not sent: the call
|
|
539
570
|
throws `timeout`. A wait's last poll goes out just before its deadline.
|
|
540
|
-
- A command,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
571
|
+
- A command, a `files.remove` or a `kill` is sent again only when it
|
|
572
|
+
provably never arrived: the connection never opened, or the answer says so
|
|
573
|
+
(rate limited, the sandbox not ready). If an answer is lost after it was
|
|
574
|
+
delivered, the SDK throws (`sandbox_unreachable`, `network_error`,
|
|
575
|
+
`timeout`) rather than risk running a command twice.
|
|
576
|
+
- A file write (`files.write`, `files.upload`) goes up under a temporary name
|
|
577
|
+
beside the file, which is sent again like a read (a lost answer, a 502, 503
|
|
578
|
+
or 504), every copy to the same name and none after the transfer's 10
|
|
579
|
+
minutes, so one that timed out is not sent again. One command that is never
|
|
580
|
+
sent again then puts it in place: if that command's answer is lost, the SDK
|
|
581
|
+
throws rather than risk overwriting what the sandbox wrote to the file since.
|
|
546
582
|
- Did it run? When a command answered in one call (`exec` without
|
|
547
583
|
`background`, `onStdout` or `onStderr`, and a `timeoutMs` other than 0 and
|
|
548
584
|
at most 4 minutes), a file write or a `files.remove` fails without a plain refusal,
|
|
@@ -552,7 +588,10 @@ try {
|
|
|
552
588
|
clear answer came back (a dropped connection, a timeout, or any other 5xx):
|
|
553
589
|
it may or may not have run, and the message says how to check (read the
|
|
554
590
|
file back, `files.exists()`). A 2xx came back that could not be read
|
|
555
|
-
(`bad_response`): it ran. A
|
|
591
|
+
(`bad_response`): it ran. A write whose upload got no clear answer says the file was
|
|
592
|
+
not changed (its new bytes may have reached the sandbox under the
|
|
593
|
+
temporary name, which the SDK then removes): write it again. A `kill`
|
|
594
|
+
says the same of its signal
|
|
556
595
|
(`status()` tells you whether the command is still running). A background
|
|
557
596
|
command (or a streamed or long `exec`, which runs as one) whose start got
|
|
558
597
|
no clear answer is stopped by the SDK, and the error says so, or, when the
|
|
@@ -563,6 +602,8 @@ try {
|
|
|
563
602
|
poll's own error as its `cause`, and says whether the SDK stopped the
|
|
564
603
|
command or it may still be running. An error that settles it (the
|
|
565
604
|
sandbox deleted or not running, the run not found) is thrown as it came.
|
|
605
|
+
A handle's `wait()` whose polls fail for longer than `pollOutageMs` throws
|
|
606
|
+
the poll's own error; it stops nothing.
|
|
566
607
|
- Deleting what a failed create or fork made (a few seconds) comes on top
|
|
567
608
|
of `timeoutMs`.
|
|
568
609
|
- `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. */
|
|
@@ -95,11 +109,26 @@ export interface ExecOptions {
|
|
|
95
109
|
pollOutageMs?: number;
|
|
96
110
|
}
|
|
97
111
|
export interface WaitOptions {
|
|
98
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Stop waiting after this many ms (the command keeps running). Default: no
|
|
114
|
+
* limit. Once the command has ended, reading the rest of its output is not
|
|
115
|
+
* held to it.
|
|
116
|
+
*/
|
|
99
117
|
timeoutMs?: number;
|
|
100
118
|
onStdout?: (chunk: string) => void;
|
|
101
119
|
onStderr?: (chunk: string) => void;
|
|
102
120
|
signal?: AbortSignal;
|
|
121
|
+
/**
|
|
122
|
+
* How long the wait keeps polling while its polls fail (no answer, a 5xx,
|
|
123
|
+
* a rate limit), in ms, before it gives up and throws what the polls met.
|
|
124
|
+
* While the command runs, never past `timeoutMs`. Default: the client's
|
|
125
|
+
* `pollOutageMs` (120 000).
|
|
126
|
+
* `0` gives up once one poll's retries run out. A refused key, or an
|
|
127
|
+
* answer that settles the run (the sandbox deleted or not running, the
|
|
128
|
+
* run gone), ends the wait at once. Giving up stops nothing: the command
|
|
129
|
+
* keeps running, and wait() can be called again.
|
|
130
|
+
*/
|
|
131
|
+
pollOutageMs?: number;
|
|
103
132
|
}
|
|
104
133
|
export interface CommandStatus {
|
|
105
134
|
running: boolean;
|
|
@@ -136,15 +165,17 @@ export declare class CommandHandle {
|
|
|
136
165
|
* `onStdout`/`onStderr` receive the output as it arrives, from the start.
|
|
137
166
|
* A `timeoutMs` here bounds the wait only; the command keeps running. If
|
|
138
167
|
* the command ran past its own timeout (the one given to exec), it was
|
|
139
|
-
* killed and this throws CommandTimeoutError with what it printed.
|
|
168
|
+
* killed and this throws CommandTimeoutError with what it printed. While
|
|
169
|
+
* polls fail (a dropped connection, a 5xx), it keeps polling for up to
|
|
170
|
+
* `pollOutageMs` (the client's, 120 000 by default) before it throws.
|
|
140
171
|
*/
|
|
141
172
|
wait(options?: WaitOptions): Promise<ExecResult>;
|
|
142
173
|
/**
|
|
143
174
|
* @internal wait() without the timeout check: exec cleans up before it
|
|
144
|
-
* throws. `outageMs` (
|
|
145
|
-
*
|
|
175
|
+
* throws. `outageMs` (pollOutageMs): how long polls may fail before the
|
|
176
|
+
* wait gives up (Outage), never past the wait's own deadline.
|
|
146
177
|
*/
|
|
147
|
-
waitRaw(options
|
|
178
|
+
waitRaw(options: WaitOptions, outageMs: number): Promise<ExecResult>;
|
|
148
179
|
private waitTimeout;
|
|
149
180
|
private result;
|
|
150
181
|
/**
|
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) ||
|
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
|
/**
|
package/dist/cjs/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CommandTimeoutError = exports.PrizedError = exports.FREE_TIER_URL = exports.SIGN_UP_URL = exports.API_KEYS_URL = void 0;
|
|
3
|
+
exports.OutputReadError = exports.CommandTimeoutError = exports.PrizedError = exports.FREE_TIER_URL = exports.AGENT_SIGN_UP_URL = exports.SIGN_UP_URL = exports.API_KEYS_URL = void 0;
|
|
4
4
|
exports.defaultMessage = defaultMessage;
|
|
5
5
|
exports.parseRetryAfter = parseRetryAfter;
|
|
6
6
|
exports.errorFromResponse = errorFromResponse;
|
|
@@ -8,15 +8,22 @@ exports.errorFromResponse = errorFromResponse;
|
|
|
8
8
|
exports.API_KEYS_URL = 'https://prized.dev/dashboard/workspace/api-keys';
|
|
9
9
|
/** Where a newcomer signs up (free credit, no card) and gets one. */
|
|
10
10
|
exports.SIGN_UP_URL = 'https://prized.dev/onboarding/sandboxes';
|
|
11
|
+
/** What a coding agent follows to sign its person up and get a key itself:
|
|
12
|
+
* their email address, then the 6-digit code Prized emails them. The SDK's
|
|
13
|
+
* own key errors name it; a refused key's 401 carries the API's own line
|
|
14
|
+
* (`signUp`), which follows the deployment. */
|
|
15
|
+
exports.AGENT_SIGN_UP_URL = 'https://prized.dev/auth.md';
|
|
16
|
+
const AGENT_SIGN_UP = `No Prized account yet? If you are an agent, you can sign your person up with their email and a 6-digit code: follow ${exports.AGENT_SIGN_UP_URL}`;
|
|
11
17
|
/** The free tier's limits, and what lifts them. */
|
|
12
18
|
exports.FREE_TIER_URL = 'https://prized.dev/docs/sandboxes#free-tier';
|
|
13
19
|
/** A sentence for the codes a caller is most likely to meet, used when the
|
|
14
20
|
* answer carries no `message` of its own. */
|
|
15
21
|
const MESSAGES = {
|
|
16
22
|
// The SDK's own codes.
|
|
17
|
-
missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${exports.SIGN_UP_URL} signs you up and makes one
|
|
18
|
-
invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${exports.API_KEYS_URL}
|
|
23
|
+
missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${exports.SIGN_UP_URL} signs you up and makes one, or, if you are an agent, you can sign your person up with their email and a 6-digit code: follow ${exports.AGENT_SIGN_UP_URL}, keep the key file it writes, and pass that key as apiKey at run time, never in source. Otherwise make one at ${exports.API_KEYS_URL}.`,
|
|
24
|
+
invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${exports.API_KEYS_URL}. ${AGENT_SIGN_UP}`,
|
|
19
25
|
invalid_argument: 'An argument was not valid.',
|
|
26
|
+
edge_unknown: 'The API named no command endpoint this SDK trusts; pass edgeUrl for that deployment.',
|
|
20
27
|
network_error: 'Could not reach the Prized API.',
|
|
21
28
|
timeout: 'Timed out.',
|
|
22
29
|
aborted: 'The operation was aborted.',
|
|
@@ -27,6 +34,7 @@ const MESSAGES = {
|
|
|
27
34
|
sandbox_deleted: 'The sandbox was deleted.',
|
|
28
35
|
command_failed: 'The command failed in the sandbox.',
|
|
29
36
|
command_timeout: 'The command ran past its timeout and was killed.',
|
|
37
|
+
output_read_failed: 'The command ran, but its output could not be read back from the sandbox.',
|
|
30
38
|
not_directory: 'Not a directory.',
|
|
31
39
|
// The API's and the exec edge's codes.
|
|
32
40
|
unauthorized: `The API key was refused. Check that it is a live key from ${exports.API_KEYS_URL}.`,
|
|
@@ -83,13 +91,21 @@ function defaultMessage(code) {
|
|
|
83
91
|
* - `code`: the API's error string, e.g. `paid_balance_required`,
|
|
84
92
|
* `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
|
|
85
93
|
* `rate_limited`; or one of the SDK's own: `missing_api_key`,
|
|
86
|
-
* `invalid_api_key`, `invalid_argument`, `
|
|
94
|
+
* `invalid_api_key`, `invalid_argument`, `edge_unknown` (another
|
|
95
|
+
* deployment's API named no command endpoint this SDK sends the key to),
|
|
96
|
+
* `network_error`, `timeout`, `aborted`,
|
|
87
97
|
* `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
|
|
88
|
-
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError)
|
|
98
|
+
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
|
|
99
|
+
* `output_read_failed` (thrown as OutputReadError).
|
|
89
100
|
* - `reason`: the finer-grained reason, when there is one (for
|
|
90
101
|
* `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
|
|
91
102
|
* `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
|
|
92
|
-
* `max_runtime`
|
|
103
|
+
* `max_runtime`; for `output_read_failed`, the code of the failure that
|
|
104
|
+
* stopped the read).
|
|
105
|
+
* - `exitCode`: the command's exit code when the error came after it ran: a
|
|
106
|
+
* `fullOutput` command whose output could not be read back
|
|
107
|
+
* (`output_read_failed`, or `sandbox_deleted` when the sandbox was deleted
|
|
108
|
+
* first). Don't run such a command again blindly. Null otherwise.
|
|
93
109
|
*/
|
|
94
110
|
class PrizedError extends Error {
|
|
95
111
|
status;
|
|
@@ -98,6 +114,7 @@ class PrizedError extends Error {
|
|
|
98
114
|
sandboxId;
|
|
99
115
|
body;
|
|
100
116
|
retryAfter;
|
|
117
|
+
exitCode;
|
|
101
118
|
constructor(code, message, init = {}) {
|
|
102
119
|
super(message || defaultMessage(code), init.cause === undefined ? undefined : { cause: init.cause });
|
|
103
120
|
this.name = 'PrizedError';
|
|
@@ -107,6 +124,7 @@ class PrizedError extends Error {
|
|
|
107
124
|
this.sandboxId = init.sandboxId ?? null;
|
|
108
125
|
this.body = init.body ?? null;
|
|
109
126
|
this.retryAfter = init.retryAfter ?? null;
|
|
127
|
+
this.exitCode = init.exitCode ?? null;
|
|
110
128
|
}
|
|
111
129
|
toString() {
|
|
112
130
|
const head = this.status === null ? this.code : `${this.status} ${this.code}`;
|
|
@@ -143,6 +161,31 @@ class CommandTimeoutError extends PrizedError {
|
|
|
143
161
|
}
|
|
144
162
|
}
|
|
145
163
|
exports.CommandTimeoutError = CommandTimeoutError;
|
|
164
|
+
/**
|
|
165
|
+
* A `fullOutput` command ran, but its output could not be read back from the
|
|
166
|
+
* files it was copied to (`code` is `output_read_failed`). The command did
|
|
167
|
+
* run (`exitCode` is its exit code), so do not run it again blindly; and the
|
|
168
|
+
* sandbox is not known to be gone (a deleted one throws `sandbox_deleted`
|
|
169
|
+
* instead). `reason` is the code of the failure that stopped the read (say
|
|
170
|
+
* `not_found`, `network_error`), and `cause` is that error, with its HTTP
|
|
171
|
+
* status; `status` is null, as the command's own call succeeded. The output
|
|
172
|
+
* stays in the sandbox for an hour at `stdoutPath` and `stderrPath`: read it
|
|
173
|
+
* with files.read(), then remove their directory.
|
|
174
|
+
*/
|
|
175
|
+
class OutputReadError extends PrizedError {
|
|
176
|
+
exitCode;
|
|
177
|
+
stdoutPath;
|
|
178
|
+
stderrPath;
|
|
179
|
+
constructor(message, init) {
|
|
180
|
+
const { cause } = init;
|
|
181
|
+
super('output_read_failed', message, { reason: cause.code, sandboxId: cause.sandboxId, cause, exitCode: init.exitCode });
|
|
182
|
+
this.name = 'OutputReadError';
|
|
183
|
+
this.exitCode = init.exitCode;
|
|
184
|
+
this.stdoutPath = init.stdoutPath;
|
|
185
|
+
this.stderrPath = init.stderrPath;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.OutputReadError = OutputReadError;
|
|
146
189
|
function stringField(record, key) {
|
|
147
190
|
const value = record[key];
|
|
148
191
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
@@ -254,5 +297,10 @@ function errorFromResponse(status, text, retryAfter, sandboxId, path = '') {
|
|
|
254
297
|
const reason = doc ? stringField(doc, 'reason') ?? stringField(doc, 'field') ?? stringField(doc, 'detail') : null;
|
|
255
298
|
if (doc && doc.tier === 'free' && !(message ?? '').includes('free tier'))
|
|
256
299
|
message = freeTierMessage(code, message, reason, doc.limit);
|
|
300
|
+
// A refused key's 401 may say how an agent signs its person up (the API
|
|
301
|
+
// sends it where agent sign-in answers, with that deployment's address).
|
|
302
|
+
const signUp = code === 'unauthorized' && doc ? stringField(doc, 'signUp') : null;
|
|
303
|
+
if (signUp !== null)
|
|
304
|
+
message = `${message ?? defaultMessage(code)} ${signUp.slice(0, 300)}`;
|
|
257
305
|
return new PrizedError(code, message, { status, reason, sandboxId, body: doc, retryAfter });
|
|
258
306
|
}
|
package/dist/cjs/files.d.ts
CHANGED
|
@@ -11,7 +11,12 @@ export interface WriteOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
mode?: number;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The sandbox's file transfers take at most this long, retries included:
|
|
16
|
+
* the edge's own limit on one transfer. A write's upload sent again (a
|
|
17
|
+
* staged one is, after a lost answer or a 502, 503 or 504) is sent only
|
|
18
|
+
* while time is left, so a transfer that timed out is not.
|
|
19
|
+
*/
|
|
15
20
|
export declare const FILE_TIMEOUT_MS: number;
|
|
16
21
|
/** The publish script's own exits (the Python SDK's too). */
|
|
17
22
|
export declare const EXIT_TARGET_IS_LINK = 47;
|
|
@@ -54,12 +59,22 @@ export declare class Files {
|
|
|
54
59
|
* A file that is replaced keeps its mode, so a script made executable
|
|
55
60
|
* stays executable; a new file gets the sandbox's default (0600 in
|
|
56
61
|
* prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
|
|
57
|
-
*
|
|
58
|
-
* lost
|
|
59
|
-
*
|
|
62
|
+
* The bytes go up under a temporary name beside the file, sent again if
|
|
63
|
+
* need be (a lost answer, a 502, 503 or 504; all copies within the
|
|
64
|
+
* transfer's 10 minutes),
|
|
65
|
+
* and one command that is never sent again puts them in place: a write
|
|
66
|
+
* whose answer to that command was lost throws, since it may have landed
|
|
67
|
+
* and the sandbox written the file since (read it back, or write again,
|
|
68
|
+
* as suits the file).
|
|
60
69
|
*/
|
|
61
70
|
write(path: string, data: FileData, options?: WriteOptions): Promise<WriteInfo>;
|
|
62
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* One upload to `path`, as the files API takes it; `target` is the file the
|
|
73
|
+
* caller asked for, and `effect` what the upload does to it. `exec` for an
|
|
74
|
+
* upload straight to the file: like a command, a resend after the answer
|
|
75
|
+
* was lost could overwrite what the sandbox wrote to the path since.
|
|
76
|
+
* `staged` for one to a fresh name: sent again like a read.
|
|
77
|
+
*/
|
|
63
78
|
private send;
|
|
64
79
|
/**
|
|
65
80
|
* Remove a staged upload that was not put in place (best effort: the
|