@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 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 streamed or long `exec` keeps polling through a dropped connection (or a 5xx, or a rate limit) before it stops the command. `exec` takes it per call too. |
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 must
325
- stay inside `/home/prized`, are at most 100 MiB each, and do not follow
326
- symbolic links. A write replaces the file whole, and a file it replaces keeps
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, an upload (`files.write`, `files.upload`), a `files.remove` or
541
- a `kill` is sent again only when it provably never arrived: the connection never
542
- opened, or the answer says so (rate limited, the sandbox not ready). If an
543
- answer is lost after it was delivered, the SDK throws
544
- (`sandbox_unreachable`, `network_error`, `timeout`) rather than risk running a command
545
- twice or overwriting what the sandbox wrote to the file since.
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 `kill` says the same of its signal
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
@@ -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
- * (exec's pollOutageMs), from when the first poll's retries ran out: polled
23
- * again with backoff, each poll and its retries ending before the last look,
24
- * which goes out LAST_LOOK_MAX_MS before the window's end (half the window,
25
- * for one shorter than twice that) with room for its own retries. So an
26
- * outage that ends inside the window is always seen to end. The Python
27
- * SDK's Outage.
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 readonly room;
35
- constructor(windowMs: number);
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
- /** Stop waiting after this many ms (the command keeps running). Default: no limit. */
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` (exec's pollOutageMs; a wait with no limit only):
145
- * how long polls may fail before the wait gives up (Outage).
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?: WaitOptions, outageMs?: number): Promise<ExecResult>;
178
+ waitRaw(options: WaitOptions, outageMs: number): Promise<ExecResult>;
148
179
  private waitTimeout;
149
180
  private result;
150
181
  /**
@@ -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
- * (exec's pollOutageMs), from when the first poll's retries ran out: polled
74
- * again with backoff, each poll and its retries ending before the last look,
75
- * which goes out LAST_LOOK_MAX_MS before the window's end (half the window,
76
- * for one shorter than twice that) with room for its own retries. So an
77
- * outage that ends inside the window is always seen to end. The Python
78
- * SDK's Outage.
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
- constructor(windowMs) {
87
+ room = 0;
88
+ finished = false;
89
+ constructor(windowMs, waitDeadline = null) {
87
90
  this.windowMs = windowMs;
88
- this.room = Math.min(http_js_1.LAST_LOOK_MAX_MS, windowMs / 2);
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 ??= now + this.windowMs;
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
- return finished(await this.waitRaw(options), this.timeoutMs, this.sandboxId);
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` (exec's pollOutageMs; a wait with no limit only):
289
- * how long polls may fail before the wait gives up (Outage).
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 = {}, outageMs = 0) {
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(deadline === null ? outageMs : 0);
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
- if (deadline !== null && error instanceof errors_js_1.PrizedError && error.code === 'timeout')
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, config.now(), (0, http_js_1.cutByDeadline)(error), (tries) => (0, http_js_1.backoffMs)(config, tries));
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) ||
@@ -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`, `network_error`, `timeout`, `aborted`,
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
  /**
@@ -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; otherwise make one at ${exports.API_KEYS_URL}.`,
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`, `network_error`, `timeout`, `aborted`,
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
  }
@@ -11,7 +11,12 @@ export interface WriteOptions {
11
11
  */
12
12
  mode?: number;
13
13
  }
14
- /** The sandbox's file transfers take at most this long, retries included. */
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
- * Sent again only if it provably never arrived: a write whose answer was
58
- * lost throws, since it may have landed and the sandbox written the file
59
- * since (read it back, or write again, as suits the file).
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
- /** One upload to `path`, as the files API takes it; `target` is the file the caller asked for, and `effect` what the upload does to it. */
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