@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/dist/esm/commands.js
CHANGED
|
@@ -65,22 +65,42 @@ export function ridesOut(error) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* The failed polls of a wait that rides out an outage of up to `windowMs`
|
|
68
|
-
* (
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
68
|
+
* (pollOutageMs), from when the first poll's retries ran out, and, while the
|
|
69
|
+
* command runs, never past the wait's own deadline (`waitDeadline`, epoch
|
|
70
|
+
* ms; null for none): polled again with backoff, each poll and its retries
|
|
71
|
+
* ending before the last look, which goes out LAST_LOOK_MAX_MS before the
|
|
72
|
+
* window's end (half the window, for one shorter than twice that) with room
|
|
73
|
+
* for its own retries. So an outage that ends inside the window is always
|
|
74
|
+
* seen to end. The Python SDK's Outage.
|
|
74
75
|
*/
|
|
75
76
|
export class Outage {
|
|
76
77
|
windowMs;
|
|
78
|
+
waitDeadline;
|
|
77
79
|
end = null;
|
|
78
80
|
last = null;
|
|
79
81
|
tries = 0;
|
|
80
|
-
room;
|
|
81
|
-
|
|
82
|
+
room = 0;
|
|
83
|
+
finished = false;
|
|
84
|
+
constructor(windowMs, waitDeadline = null) {
|
|
82
85
|
this.windowMs = windowMs;
|
|
83
|
-
this.
|
|
86
|
+
this.waitDeadline = waitDeadline;
|
|
87
|
+
}
|
|
88
|
+
/** The command has ended: draining its output, and an outage meanwhile, are not held to the deadline. */
|
|
89
|
+
finish() {
|
|
90
|
+
this.finished = true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether a wait's failed poll (`error`, at `now`; `cut`: its bound cut it
|
|
94
|
+
* short) means the wait's own time ran out: its deadline cut the poll
|
|
95
|
+
* short while the command ran, with no outage being ridden out, so the
|
|
96
|
+
* command was still running when last seen. With no window, any timeout
|
|
97
|
+
* while it runs says so, as it always has. Never once the command has
|
|
98
|
+
* ended. The Python SDK's Outage.ran_out.
|
|
99
|
+
*/
|
|
100
|
+
ranOut(error, now, cut) {
|
|
101
|
+
if (this.waitDeadline === null || this.finished || error.code !== 'timeout')
|
|
102
|
+
return false;
|
|
103
|
+
return this.windowMs <= 0 || (this.end === null && (cut || now >= this.waitDeadline));
|
|
84
104
|
}
|
|
85
105
|
/** 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. */
|
|
86
106
|
deadline(now) {
|
|
@@ -104,7 +124,10 @@ export class Outage {
|
|
|
104
124
|
failed(error, sentAt, now, cut, backoff) {
|
|
105
125
|
if (this.windowMs <= 0 || !ridesOut(error))
|
|
106
126
|
return { pause: null, failure: error };
|
|
107
|
-
this.end
|
|
127
|
+
if (this.end === null) {
|
|
128
|
+
this.end = Math.min(now + this.windowMs, this.finished || this.waitDeadline === null ? Infinity : this.waitDeadline);
|
|
129
|
+
this.room = Math.max(0, Math.min(LAST_LOOK_MAX_MS, (this.end - now) / 2));
|
|
130
|
+
}
|
|
108
131
|
const met = error.cause instanceof PrizedError ? error.cause : null;
|
|
109
132
|
if (!cut) {
|
|
110
133
|
this.last = error;
|
|
@@ -272,17 +295,20 @@ export class CommandHandle {
|
|
|
272
295
|
* `onStdout`/`onStderr` receive the output as it arrives, from the start.
|
|
273
296
|
* A `timeoutMs` here bounds the wait only; the command keeps running. If
|
|
274
297
|
* the command ran past its own timeout (the one given to exec), it was
|
|
275
|
-
* killed and this throws CommandTimeoutError with what it printed.
|
|
298
|
+
* killed and this throws CommandTimeoutError with what it printed. While
|
|
299
|
+
* polls fail (a dropped connection, a 5xx), it keeps polling for up to
|
|
300
|
+
* `pollOutageMs` (the client's, 120 000 by default) before it throws.
|
|
276
301
|
*/
|
|
277
302
|
async wait(options = {}) {
|
|
278
|
-
|
|
303
|
+
const outageMs = validOutage(options.pollOutageMs) ?? this.toolbox.config.pollOutageMs;
|
|
304
|
+
return finished(await this.waitRaw(options, outageMs), this.timeoutMs, this.sandboxId);
|
|
279
305
|
}
|
|
280
306
|
/**
|
|
281
307
|
* @internal wait() without the timeout check: exec cleans up before it
|
|
282
|
-
* throws. `outageMs` (
|
|
283
|
-
*
|
|
308
|
+
* throws. `outageMs` (pollOutageMs): how long polls may fail before the
|
|
309
|
+
* wait gives up (Outage), never past the wait's own deadline.
|
|
284
310
|
*/
|
|
285
|
-
async waitRaw(options
|
|
311
|
+
async waitRaw(options, outageMs) {
|
|
286
312
|
const config = this.toolbox.config;
|
|
287
313
|
const deadline = options.timeoutMs ? config.now() + options.timeoutMs : null;
|
|
288
314
|
const out = new StreamReader(options.onStdout);
|
|
@@ -290,7 +316,7 @@ export class CommandHandle {
|
|
|
290
316
|
let delay = POLL_MIN_MS;
|
|
291
317
|
let finished = false;
|
|
292
318
|
const polls = deadline === null ? null : new Polls(config, deadline);
|
|
293
|
-
const outage = new Outage(
|
|
319
|
+
const outage = new Outage(outageMs, deadline);
|
|
294
320
|
for (;;) {
|
|
295
321
|
let report;
|
|
296
322
|
const sentAt = config.now();
|
|
@@ -305,13 +331,15 @@ export class CommandHandle {
|
|
|
305
331
|
polls?.answered();
|
|
306
332
|
}
|
|
307
333
|
catch (error) {
|
|
308
|
-
|
|
334
|
+
const now = config.now();
|
|
335
|
+
if (error instanceof PrizedError && outage.ranOut(error, now, cutByDeadline(error))) {
|
|
309
336
|
throw this.waitTimeout(options.timeoutMs);
|
|
337
|
+
}
|
|
310
338
|
let failure = error;
|
|
311
339
|
if (error instanceof PrizedError && !options.signal?.aborted) {
|
|
312
340
|
// A dropped connection, an edge restarting: the command may be
|
|
313
341
|
// fine. Poll again until the window runs out, then give up.
|
|
314
|
-
const step = outage.failed(error, sentAt,
|
|
342
|
+
const step = outage.failed(error, sentAt, now, cutByDeadline(error), (tries) => backoffMs(config, tries));
|
|
315
343
|
if (step.pause !== null) {
|
|
316
344
|
await config.sleep(step.pause, options.signal);
|
|
317
345
|
continue;
|
|
@@ -323,8 +351,10 @@ export class CommandHandle {
|
|
|
323
351
|
throw failure;
|
|
324
352
|
}
|
|
325
353
|
outage.answered();
|
|
326
|
-
if (report.state !== 'running')
|
|
354
|
+
if (report.state !== 'running') {
|
|
327
355
|
finished = true;
|
|
356
|
+
outage.finish();
|
|
357
|
+
}
|
|
328
358
|
out.push(report.stdout);
|
|
329
359
|
err.push(report.stderr);
|
|
330
360
|
const more = (report.stdout.length > 0 && out.offset < report.outSize) ||
|
package/dist/esm/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/esm/errors.js
CHANGED
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
export const API_KEYS_URL = 'https://prized.dev/dashboard/workspace/api-keys';
|
|
3
3
|
/** Where a newcomer signs up (free credit, no card) and gets one. */
|
|
4
4
|
export const SIGN_UP_URL = 'https://prized.dev/onboarding/sandboxes';
|
|
5
|
+
/** What a coding agent follows to sign its person up and get a key itself:
|
|
6
|
+
* their email address, then the 6-digit code Prized emails them. The SDK's
|
|
7
|
+
* own key errors name it; a refused key's 401 carries the API's own line
|
|
8
|
+
* (`signUp`), which follows the deployment. */
|
|
9
|
+
export const AGENT_SIGN_UP_URL = 'https://prized.dev/auth.md';
|
|
10
|
+
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 ${AGENT_SIGN_UP_URL}`;
|
|
5
11
|
/** The free tier's limits, and what lifts them. */
|
|
6
12
|
export const FREE_TIER_URL = 'https://prized.dev/docs/sandboxes#free-tier';
|
|
7
13
|
/** A sentence for the codes a caller is most likely to meet, used when the
|
|
8
14
|
* answer carries no `message` of its own. */
|
|
9
15
|
const MESSAGES = {
|
|
10
16
|
// The SDK's own codes.
|
|
11
|
-
missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${SIGN_UP_URL} signs you up and makes one
|
|
12
|
-
invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${API_KEYS_URL}
|
|
17
|
+
missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${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 ${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 ${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 ${API_KEYS_URL}. ${AGENT_SIGN_UP}`,
|
|
13
19
|
invalid_argument: 'An argument was not valid.',
|
|
20
|
+
edge_unknown: 'The API named no command endpoint this SDK trusts; pass edgeUrl for that deployment.',
|
|
14
21
|
network_error: 'Could not reach the Prized API.',
|
|
15
22
|
timeout: 'Timed out.',
|
|
16
23
|
aborted: 'The operation was aborted.',
|
|
@@ -21,6 +28,7 @@ const MESSAGES = {
|
|
|
21
28
|
sandbox_deleted: 'The sandbox was deleted.',
|
|
22
29
|
command_failed: 'The command failed in the sandbox.',
|
|
23
30
|
command_timeout: 'The command ran past its timeout and was killed.',
|
|
31
|
+
output_read_failed: 'The command ran, but its output could not be read back from the sandbox.',
|
|
24
32
|
not_directory: 'Not a directory.',
|
|
25
33
|
// The API's and the exec edge's codes.
|
|
26
34
|
unauthorized: `The API key was refused. Check that it is a live key from ${API_KEYS_URL}.`,
|
|
@@ -77,13 +85,21 @@ export function defaultMessage(code) {
|
|
|
77
85
|
* - `code`: the API's error string, e.g. `paid_balance_required`,
|
|
78
86
|
* `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
|
|
79
87
|
* `rate_limited`; or one of the SDK's own: `missing_api_key`,
|
|
80
|
-
* `invalid_api_key`, `invalid_argument`, `
|
|
88
|
+
* `invalid_api_key`, `invalid_argument`, `edge_unknown` (another
|
|
89
|
+
* deployment's API named no command endpoint this SDK sends the key to),
|
|
90
|
+
* `network_error`, `timeout`, `aborted`,
|
|
81
91
|
* `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
|
|
82
|
-
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError)
|
|
92
|
+
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
|
|
93
|
+
* `output_read_failed` (thrown as OutputReadError).
|
|
83
94
|
* - `reason`: the finer-grained reason, when there is one (for
|
|
84
95
|
* `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
|
|
85
96
|
* `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
|
|
86
|
-
* `max_runtime`
|
|
97
|
+
* `max_runtime`; for `output_read_failed`, the code of the failure that
|
|
98
|
+
* stopped the read).
|
|
99
|
+
* - `exitCode`: the command's exit code when the error came after it ran: a
|
|
100
|
+
* `fullOutput` command whose output could not be read back
|
|
101
|
+
* (`output_read_failed`, or `sandbox_deleted` when the sandbox was deleted
|
|
102
|
+
* first). Don't run such a command again blindly. Null otherwise.
|
|
87
103
|
*/
|
|
88
104
|
export class PrizedError extends Error {
|
|
89
105
|
status;
|
|
@@ -92,6 +108,7 @@ export class PrizedError extends Error {
|
|
|
92
108
|
sandboxId;
|
|
93
109
|
body;
|
|
94
110
|
retryAfter;
|
|
111
|
+
exitCode;
|
|
95
112
|
constructor(code, message, init = {}) {
|
|
96
113
|
super(message || defaultMessage(code), init.cause === undefined ? undefined : { cause: init.cause });
|
|
97
114
|
this.name = 'PrizedError';
|
|
@@ -101,6 +118,7 @@ export class PrizedError extends Error {
|
|
|
101
118
|
this.sandboxId = init.sandboxId ?? null;
|
|
102
119
|
this.body = init.body ?? null;
|
|
103
120
|
this.retryAfter = init.retryAfter ?? null;
|
|
121
|
+
this.exitCode = init.exitCode ?? null;
|
|
104
122
|
}
|
|
105
123
|
toString() {
|
|
106
124
|
const head = this.status === null ? this.code : `${this.status} ${this.code}`;
|
|
@@ -135,6 +153,30 @@ export class CommandTimeoutError extends PrizedError {
|
|
|
135
153
|
return this.result.stderr;
|
|
136
154
|
}
|
|
137
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* A `fullOutput` command ran, but its output could not be read back from the
|
|
158
|
+
* files it was copied to (`code` is `output_read_failed`). The command did
|
|
159
|
+
* run (`exitCode` is its exit code), so do not run it again blindly; and the
|
|
160
|
+
* sandbox is not known to be gone (a deleted one throws `sandbox_deleted`
|
|
161
|
+
* instead). `reason` is the code of the failure that stopped the read (say
|
|
162
|
+
* `not_found`, `network_error`), and `cause` is that error, with its HTTP
|
|
163
|
+
* status; `status` is null, as the command's own call succeeded. The output
|
|
164
|
+
* stays in the sandbox for an hour at `stdoutPath` and `stderrPath`: read it
|
|
165
|
+
* with files.read(), then remove their directory.
|
|
166
|
+
*/
|
|
167
|
+
export class OutputReadError extends PrizedError {
|
|
168
|
+
exitCode;
|
|
169
|
+
stdoutPath;
|
|
170
|
+
stderrPath;
|
|
171
|
+
constructor(message, init) {
|
|
172
|
+
const { cause } = init;
|
|
173
|
+
super('output_read_failed', message, { reason: cause.code, sandboxId: cause.sandboxId, cause, exitCode: init.exitCode });
|
|
174
|
+
this.name = 'OutputReadError';
|
|
175
|
+
this.exitCode = init.exitCode;
|
|
176
|
+
this.stdoutPath = init.stdoutPath;
|
|
177
|
+
this.stderrPath = init.stderrPath;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
138
180
|
function stringField(record, key) {
|
|
139
181
|
const value = record[key];
|
|
140
182
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
@@ -246,5 +288,10 @@ export function errorFromResponse(status, text, retryAfter, sandboxId, path = ''
|
|
|
246
288
|
const reason = doc ? stringField(doc, 'reason') ?? stringField(doc, 'field') ?? stringField(doc, 'detail') : null;
|
|
247
289
|
if (doc && doc.tier === 'free' && !(message ?? '').includes('free tier'))
|
|
248
290
|
message = freeTierMessage(code, message, reason, doc.limit);
|
|
291
|
+
// A refused key's 401 may say how an agent signs its person up (the API
|
|
292
|
+
// sends it where agent sign-in answers, with that deployment's address).
|
|
293
|
+
const signUp = code === 'unauthorized' && doc ? stringField(doc, 'signUp') : null;
|
|
294
|
+
if (signUp !== null)
|
|
295
|
+
message = `${message ?? defaultMessage(code)} ${signUp.slice(0, 300)}`;
|
|
249
296
|
return new PrizedError(code, message, { status, reason, sandboxId, body: doc, retryAfter });
|
|
250
297
|
}
|
package/dist/esm/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
|
package/dist/esm/files.js
CHANGED
|
@@ -4,7 +4,12 @@ import { request } from './http.js';
|
|
|
4
4
|
import { mayHaveActed, removeEffect, withOutcome, writeEffect } from './outcome.js';
|
|
5
5
|
import { existsScript, listScript, removeScript, sandboxPath, shellQuote, WORKSPACE_DIR } from './shell.js';
|
|
6
6
|
import { helperExec, MAX_FILE_BYTES, rawExec } from './toolbox.js';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The sandbox's file transfers take at most this long, retries included:
|
|
9
|
+
* the edge's own limit on one transfer. A write's upload sent again (a
|
|
10
|
+
* staged one is, after a lost answer or a 502, 503 or 504) is sent only
|
|
11
|
+
* while time is left, so a transfer that timed out is not.
|
|
12
|
+
*/
|
|
8
13
|
export const FILE_TIMEOUT_MS = 10 * 60_000;
|
|
9
14
|
function byteSize(data) {
|
|
10
15
|
if (typeof data === 'string')
|
|
@@ -40,12 +45,16 @@ export const EXIT_TARGET_NOT_REGULAR = 48;
|
|
|
40
45
|
const DISCARD_MS = 10_000;
|
|
41
46
|
/** A write's own commands: a rename, a chmod or an rm. */
|
|
42
47
|
const WRITE_COMMAND_MS = 60_000;
|
|
43
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* For the staged upload: the target is changed only by the command that
|
|
50
|
+
* follows it. The upload may have been sent again, so it says only what to
|
|
51
|
+
* do (langchain-prized's staged_upload_effect words it the same).
|
|
52
|
+
*/
|
|
44
53
|
function stagedEffect(path) {
|
|
45
54
|
return {
|
|
46
55
|
maybe: `${path} was not changed (its new bytes may have reached the sandbox, but were not put in place)`,
|
|
47
56
|
did: `${path} was not changed yet`,
|
|
48
|
-
check: '
|
|
57
|
+
check: 'Write it again.',
|
|
49
58
|
};
|
|
50
59
|
}
|
|
51
60
|
/** For the command that puts the staged upload in place: a 2xx proves it ran, not how it ended. */
|
|
@@ -165,9 +174,13 @@ export class Files {
|
|
|
165
174
|
* A file that is replaced keeps its mode, so a script made executable
|
|
166
175
|
* stays executable; a new file gets the sandbox's default (0600 in
|
|
167
176
|
* prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
|
|
168
|
-
*
|
|
169
|
-
* lost
|
|
170
|
-
*
|
|
177
|
+
* The bytes go up under a temporary name beside the file, sent again if
|
|
178
|
+
* need be (a lost answer, a 502, 503 or 504; all copies within the
|
|
179
|
+
* transfer's 10 minutes),
|
|
180
|
+
* and one command that is never sent again puts them in place: a write
|
|
181
|
+
* whose answer to that command was lost throws, since it may have landed
|
|
182
|
+
* and the sandbox written the file since (read it back, or write again,
|
|
183
|
+
* as suits the file).
|
|
171
184
|
*/
|
|
172
185
|
async write(path, data, options = {}) {
|
|
173
186
|
const sandboxId = this.toolbox.sandboxId;
|
|
@@ -179,14 +192,18 @@ export class Files {
|
|
|
179
192
|
const target = sandboxPath(path);
|
|
180
193
|
const plan = stagingFor(target);
|
|
181
194
|
if (plan === null)
|
|
182
|
-
return this.send(target, data, size, target, writeEffect(target));
|
|
195
|
+
return this.send(target, data, size, target, writeEffect(target), 'exec');
|
|
183
196
|
const { clean, staging } = plan;
|
|
184
197
|
try {
|
|
185
|
-
|
|
198
|
+
// Every copy to the same fresh name, which only the command below puts in place.
|
|
199
|
+
await this.send(staging, data, size, clean, stagedEffect(clean), 'staged');
|
|
186
200
|
}
|
|
187
201
|
catch (error) {
|
|
188
202
|
// A 2xx whose body could not be read: the staged bytes are all there.
|
|
189
203
|
if (!landed(error)) {
|
|
204
|
+
// Removed only if a copy may have landed: a refusal, or copies that
|
|
205
|
+
// never arrived, left nothing, and a command would wake a sandbox
|
|
206
|
+
// the edge's refusal left paused.
|
|
190
207
|
if (mayHaveActed(error))
|
|
191
208
|
await this.discard(staging);
|
|
192
209
|
throw error;
|
|
@@ -212,7 +229,7 @@ export class Files {
|
|
|
212
229
|
return { path: clean, size };
|
|
213
230
|
if (result.exitCode === EXIT_TARGET_IS_LINK) {
|
|
214
231
|
// A link: the files API follows it or refuses it, as for any upload.
|
|
215
|
-
const info = await this.send(clean, data, size, clean, writeEffect(clean));
|
|
232
|
+
const info = await this.send(clean, data, size, clean, writeEffect(clean), 'exec');
|
|
216
233
|
if (mode !== null)
|
|
217
234
|
await this.chmod(clean, mode);
|
|
218
235
|
return info;
|
|
@@ -223,16 +240,20 @@ export class Files {
|
|
|
223
240
|
const denied = /Permission denied|Operation not permitted/.test(result.stderr);
|
|
224
241
|
throw new PrizedError(denied ? 'permission_denied' : 'command_failed', `Could not write ${clean}: ${commandDetail(result)}`, { sandboxId });
|
|
225
242
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
243
|
+
/**
|
|
244
|
+
* One upload to `path`, as the files API takes it; `target` is the file the
|
|
245
|
+
* caller asked for, and `effect` what the upload does to it. `exec` for an
|
|
246
|
+
* upload straight to the file: like a command, a resend after the answer
|
|
247
|
+
* was lost could overwrite what the sandbox wrote to the path since.
|
|
248
|
+
* `staged` for one to a fresh name: sent again like a read.
|
|
249
|
+
*/
|
|
250
|
+
async send(path, data, size, target, effect, retry) {
|
|
228
251
|
const result = await request(this.toolbox.config, {
|
|
229
252
|
method: 'POST',
|
|
230
253
|
url: this.url('upload', path),
|
|
231
254
|
body: data,
|
|
232
255
|
contentType: 'application/octet-stream',
|
|
233
|
-
|
|
234
|
-
// what the sandbox wrote to the path since.
|
|
235
|
-
retry: 'exec',
|
|
256
|
+
retry,
|
|
236
257
|
effect,
|
|
237
258
|
timeoutMs: FILE_TIMEOUT_MS,
|
|
238
259
|
deadline: this.toolbox.config.now() + FILE_TIMEOUT_MS,
|
package/dist/esm/http.d.ts
CHANGED
|
@@ -6,11 +6,16 @@ export declare const DEFAULT_EDGE_URL = "https://term.prized.dev";
|
|
|
6
6
|
export interface ClientOptions {
|
|
7
7
|
/** Your API key (`dcp_…`). Default: the `PRIZED_API_KEY` environment variable. */
|
|
8
8
|
apiKey?: string;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* The API origin: https, or plain http only on this machine (`localhost`,
|
|
11
|
+
* `127.0.0.1`, `[::1]`). Default: `https://api.prized.dev`.
|
|
12
|
+
*/
|
|
10
13
|
baseUrl?: string;
|
|
11
14
|
/**
|
|
12
|
-
* The origin that serves exec and files
|
|
13
|
-
* returns for each sandbox (today
|
|
15
|
+
* The origin that serves exec and files, by the same rule as `baseUrl`.
|
|
16
|
+
* Default: the `toolboxBase` the API returns for each sandbox (today
|
|
17
|
+
* `https://term.prized.dev`), on a host trusted beside the API's
|
|
18
|
+
* (toolboxBaseFor).
|
|
14
19
|
*/
|
|
15
20
|
edgeUrl?: string;
|
|
16
21
|
/** A fetch implementation. Default: the global `fetch`. */
|
|
@@ -23,10 +28,11 @@ export interface ClientOptions {
|
|
|
23
28
|
* the request itself and the API's hold of a create, pause or resume. Default 500. */
|
|
24
29
|
pollIntervalMs?: number;
|
|
25
30
|
/**
|
|
26
|
-
* How long a
|
|
27
|
-
*
|
|
28
|
-
* the command. Default 120 000. `0`
|
|
29
|
-
* out. `exec`
|
|
31
|
+
* How long a wait keeps polling while its polls fail (no answer, a 5xx, a
|
|
32
|
+
* rate limit), in ms, before it gives up: a streamed or long `exec` then
|
|
33
|
+
* stops the command, a handle's `wait()` throws. Default 120 000. `0`
|
|
34
|
+
* gives up once one poll's retries run out. `exec` and `wait()` take it
|
|
35
|
+
* per call too.
|
|
30
36
|
*/
|
|
31
37
|
pollOutageMs?: number;
|
|
32
38
|
/** Sleep seam (tests inject one that never really waits). */
|
|
@@ -51,10 +57,10 @@ export interface Config {
|
|
|
51
57
|
}
|
|
52
58
|
export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
53
59
|
/**
|
|
54
|
-
* How long a streamed or long exec
|
|
55
|
-
* before it
|
|
56
|
-
*
|
|
57
|
-
* DEFAULT_POLL_OUTAGE.
|
|
60
|
+
* How long a wait (a streamed or long exec, a handle's wait()) keeps polling
|
|
61
|
+
* while its polls fail before it gives up (pollOutageMs), and a streamed or
|
|
62
|
+
* long exec stops its command: long enough to ride out a dropped connection
|
|
63
|
+
* or an edge that restarts. The Python SDK's DEFAULT_POLL_OUTAGE.
|
|
58
64
|
*/
|
|
59
65
|
export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
|
|
60
66
|
/** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
|
|
@@ -63,7 +69,10 @@ export declare function resolveConfig(options?: ClientOptions): Config;
|
|
|
63
69
|
/**
|
|
64
70
|
* Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
|
|
65
71
|
* API's own `toolboxBase` when it has the expected shape (https, or plain
|
|
66
|
-
* http on loopback for local development)
|
|
72
|
+
* http on loopback for local development, ending /v1/sandbox/<id>) on a host
|
|
73
|
+
* trusted beside the API's (term.prized.dev beside api.prized.dev; see
|
|
74
|
+
* trustedEdge); else the default edge, for Prized's own API only. The API key
|
|
75
|
+
* is never sent anywhere else: discovery is routing, not authority.
|
|
67
76
|
*/
|
|
68
77
|
export declare function toolboxBaseFor(config: Config, sandboxId: string, toolboxBase: string | null): string;
|
|
69
78
|
/**
|
|
@@ -93,8 +102,15 @@ export declare function warmEdge(config: Config): Promise<void>;
|
|
|
93
102
|
* provably never arrived: a connection that never opened, or an answer
|
|
94
103
|
* saying so (the edge's rate limit, its lookup failing). A lost answer
|
|
95
104
|
* is never retried: it may already have run.
|
|
105
|
+
* - `staged`: a write's upload to a fresh name beside its target, which only
|
|
106
|
+
* a command after it puts in place (files.ts). Retried as `safe` (a lost
|
|
107
|
+
* answer, a timeout, 429/502/503/504) and `exec` both, a GET's 500 aside:
|
|
108
|
+
* every copy writes the same bytes under the same name, which nothing else
|
|
109
|
+
* writes, so a second one changes no file of the caller's. A 2xx whose body
|
|
110
|
+
* broke off is not sent again: the bytes are there. Its error says whether
|
|
111
|
+
* a copy may have reached the sandbox.
|
|
96
112
|
*/
|
|
97
|
-
export type RetryMode = 'safe' | 'exec';
|
|
113
|
+
export type RetryMode = 'safe' | 'exec' | 'staged';
|
|
98
114
|
/**
|
|
99
115
|
* Whether a fetch that threw never sent its request: the connection was
|
|
100
116
|
* refused, the name did not resolve, or connecting timed out. Anything else
|