@prized/sdk 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +666 -2
- package/dist/cjs/commands.d.ts +199 -0
- package/dist/cjs/commands.js +597 -0
- package/dist/cjs/errors.d.ts +113 -0
- package/dist/cjs/errors.js +306 -0
- package/dist/cjs/files.d.ts +101 -0
- package/dist/cjs/files.js +387 -0
- package/dist/cjs/http.d.ts +226 -0
- package/dist/cjs/http.js +618 -0
- package/dist/cjs/index.d.ts +12 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/outcome.d.ts +76 -0
- package/dist/cjs/outcome.js +170 -0
- package/dist/cjs/output.d.ts +70 -0
- package/dist/cjs/output.js +383 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/sandbox.d.ts +218 -0
- package/dist/cjs/sandbox.js +836 -0
- package/dist/cjs/shell.d.ts +152 -0
- package/dist/cjs/shell.js +393 -0
- package/dist/cjs/toolbox.d.ts +48 -0
- package/dist/cjs/toolbox.js +88 -0
- package/dist/cjs/types.d.ts +111 -0
- package/dist/cjs/types.js +102 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.js +5 -0
- package/dist/esm/commands.d.ts +199 -0
- package/dist/esm/commands.js +590 -0
- package/dist/esm/errors.d.ts +113 -0
- package/dist/esm/errors.js +297 -0
- package/dist/esm/files.d.ts +101 -0
- package/dist/esm/files.js +381 -0
- package/dist/esm/http.d.ts +226 -0
- package/dist/esm/http.js +601 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/outcome.d.ts +76 -0
- package/dist/esm/outcome.js +155 -0
- package/dist/esm/output.d.ts +70 -0
- package/dist/esm/output.js +374 -0
- package/dist/esm/sandbox.d.ts +218 -0
- package/dist/esm/sandbox.js +832 -0
- package/dist/esm/shell.d.ts +152 -0
- package/dist/esm/shell.js +379 -0
- package/dist/esm/toolbox.d.ts +48 -0
- package/dist/esm/toolbox.js +83 -0
- package/dist/esm/types.d.ts +111 -0
- package/dist/esm/types.js +95 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +2 -0
- package/package.json +53 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ExecResult } from './types.js';
|
|
2
|
+
/** Where a member makes an API key. */
|
|
3
|
+
export declare const API_KEYS_URL = "https://prized.dev/dashboard/workspace/api-keys";
|
|
4
|
+
/** Where a newcomer signs up (free credit, no card) and gets one. */
|
|
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";
|
|
11
|
+
/** The free tier's limits, and what lifts them. */
|
|
12
|
+
export declare const FREE_TIER_URL = "https://prized.dev/docs/sandboxes#free-tier";
|
|
13
|
+
/** The sentence an error with this code and no message of its own carries. */
|
|
14
|
+
export declare function defaultMessage(code: string): string;
|
|
15
|
+
export interface PrizedErrorInit {
|
|
16
|
+
/** The HTTP status, when an answer came back. */
|
|
17
|
+
status?: number | null;
|
|
18
|
+
/** The API's finer-grained reason, when it gave one. */
|
|
19
|
+
reason?: string | null;
|
|
20
|
+
sandboxId?: string | null;
|
|
21
|
+
/** The whole error document, when there was one. */
|
|
22
|
+
body?: Record<string, unknown> | null;
|
|
23
|
+
/** Seconds the API asked the caller to wait (Retry-After). */
|
|
24
|
+
retryAfter?: number | null;
|
|
25
|
+
/** The command's exit code, for an error that came after it ran. */
|
|
26
|
+
exitCode?: number | null;
|
|
27
|
+
cause?: unknown;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Every failure the SDK throws.
|
|
31
|
+
*
|
|
32
|
+
* - `status`: the HTTP status, or `null` when nothing came back (a network
|
|
33
|
+
* failure, a wait that timed out, a refused argument).
|
|
34
|
+
* - `code`: the API's error string, e.g. `paid_balance_required`,
|
|
35
|
+
* `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
|
|
36
|
+
* `rate_limited`; or one of the SDK's own: `missing_api_key`,
|
|
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`,
|
|
40
|
+
* `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
|
|
41
|
+
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
|
|
42
|
+
* `output_read_failed` (thrown as OutputReadError).
|
|
43
|
+
* - `reason`: the finer-grained reason, when there is one (for
|
|
44
|
+
* `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
|
|
45
|
+
* `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
|
|
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.
|
|
52
|
+
*/
|
|
53
|
+
export declare class PrizedError extends Error {
|
|
54
|
+
readonly status: number | null;
|
|
55
|
+
readonly code: string;
|
|
56
|
+
readonly reason: string | null;
|
|
57
|
+
readonly sandboxId: string | null;
|
|
58
|
+
readonly body: Record<string, unknown> | null;
|
|
59
|
+
readonly retryAfter: number | null;
|
|
60
|
+
readonly exitCode: number | null;
|
|
61
|
+
constructor(code: string, message?: string | null, init?: PrizedErrorInit);
|
|
62
|
+
toString(): string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A command ran past its timeout and was killed (`code` is
|
|
66
|
+
* `command_timeout`). The sandbox answered as it does for any command that
|
|
67
|
+
* ends, so nothing is lost: `result` is that answer, with what the command
|
|
68
|
+
* printed before it was killed, `exitCode: -1` and `timedOut: true`.
|
|
69
|
+
* `timeoutMs` is the limit, or null when the SDK did not set one.
|
|
70
|
+
*/
|
|
71
|
+
export declare class CommandTimeoutError extends PrizedError {
|
|
72
|
+
readonly result: ExecResult;
|
|
73
|
+
readonly timeoutMs: number | null;
|
|
74
|
+
constructor(result: ExecResult, timeoutMs: number | null, sandboxId?: string | null);
|
|
75
|
+
/** What the command printed to stdout before it was killed. */
|
|
76
|
+
get stdout(): string;
|
|
77
|
+
/** What the command printed to stderr before it was killed. */
|
|
78
|
+
get stderr(): string;
|
|
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
|
+
}
|
|
104
|
+
/** Retry-After as seconds: delta-seconds or an HTTP date. */
|
|
105
|
+
export declare function parseRetryAfter(value: string | null, nowMs: number): number | null;
|
|
106
|
+
/**
|
|
107
|
+
* The error document → a PrizedError. Two envelopes are read: today's flat
|
|
108
|
+
* `{ "error": "code", "message"?, "reason"?, "field"? }` and the nested
|
|
109
|
+
* `{ "error": { "code", "message" } }`. Anything else (a proxy's HTML page)
|
|
110
|
+
* becomes `http_error` with the status. `path` is the request's URL path,
|
|
111
|
+
* which tells a file call's 404 from a sandbox's.
|
|
112
|
+
*/
|
|
113
|
+
export declare function errorFromResponse(status: number, text: string, retryAfter: number | null, sandboxId: string | null, path?: string): PrizedError;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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
|
+
exports.defaultMessage = defaultMessage;
|
|
5
|
+
exports.parseRetryAfter = parseRetryAfter;
|
|
6
|
+
exports.errorFromResponse = errorFromResponse;
|
|
7
|
+
/** Where a member makes an API key. */
|
|
8
|
+
exports.API_KEYS_URL = 'https://prized.dev/dashboard/workspace/api-keys';
|
|
9
|
+
/** Where a newcomer signs up (free credit, no card) and gets one. */
|
|
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}`;
|
|
17
|
+
/** The free tier's limits, and what lifts them. */
|
|
18
|
+
exports.FREE_TIER_URL = 'https://prized.dev/docs/sandboxes#free-tier';
|
|
19
|
+
/** A sentence for the codes a caller is most likely to meet, used when the
|
|
20
|
+
* answer carries no `message` of its own. */
|
|
21
|
+
const MESSAGES = {
|
|
22
|
+
// The SDK's own codes.
|
|
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}`,
|
|
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.',
|
|
27
|
+
network_error: 'Could not reach the Prized API.',
|
|
28
|
+
timeout: 'Timed out.',
|
|
29
|
+
aborted: 'The operation was aborted.',
|
|
30
|
+
bad_response: 'The API answered with something the SDK could not read.',
|
|
31
|
+
http_error: 'The API answered with an error the SDK does not recognise.',
|
|
32
|
+
unexpected_redirect: 'The API answered with a redirect; the SDK does not follow redirects with your key.',
|
|
33
|
+
sandbox_error: 'The sandbox failed.',
|
|
34
|
+
sandbox_deleted: 'The sandbox was deleted.',
|
|
35
|
+
command_failed: 'The command failed in the sandbox.',
|
|
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.',
|
|
38
|
+
not_directory: 'Not a directory.',
|
|
39
|
+
// The API's and the exec edge's codes.
|
|
40
|
+
unauthorized: `The API key was refused. Check that it is a live key from ${exports.API_KEYS_URL}.`,
|
|
41
|
+
token_expired: `The API key has expired. Make a new one at ${exports.API_KEYS_URL}.`,
|
|
42
|
+
key_access_denied: 'This API key does not have the Sandboxes permission it needs.',
|
|
43
|
+
not_found: 'Not found (or not visible to this API key).',
|
|
44
|
+
bad_request: 'The request was not valid.',
|
|
45
|
+
unknown_field: 'The request carried a field the API does not accept.',
|
|
46
|
+
template_not_found: 'No such template. Sandbox.templates() lists the ones you can use.',
|
|
47
|
+
template_inactive: 'That template is no longer offered. Sandbox.templates() lists the current ones.',
|
|
48
|
+
invalid_resources: 'The requested shape is outside what the template allows.',
|
|
49
|
+
invalid_max_runtime: 'maxRuntimeMin must be a positive whole number of minutes.',
|
|
50
|
+
paid_balance_required: 'Sandboxes need a paid balance: add funds or pick a paid plan, then retry. Free trial credit cannot start a sandbox.',
|
|
51
|
+
insufficient_balance: 'The workspace balance is empty. Add funds, then retry.',
|
|
52
|
+
sandbox_quota_exceeded: 'The workspace is at a sandbox quota. Delete sandboxes you no longer need (paused ones still count), then retry.',
|
|
53
|
+
sandboxes_disabled: 'Sandboxes are not available right now.',
|
|
54
|
+
sandbox_create_disabled: 'Creating sandboxes is not available right now.',
|
|
55
|
+
sandbox_fork_disabled: 'Forking is paused for maintenance. Retry later.',
|
|
56
|
+
idempotency_conflict: 'This idempotency key was already used for a different request.',
|
|
57
|
+
invalid_idempotency_key: 'Idempotency keys are 1 to 128 printable characters with no spaces.',
|
|
58
|
+
rate_limited: 'Too many requests. Slow down and retry.',
|
|
59
|
+
sandbox_not_running: 'The sandbox is not running and could not be woken (it is resizing, failed, or being deleted).',
|
|
60
|
+
sandbox_not_ready: 'The sandbox did not finish starting in time. Nothing ran; retry.',
|
|
61
|
+
sandbox_unreachable: 'The sandbox did not answer. If this was a command, it may or may not have run.',
|
|
62
|
+
resolve_failed: 'The exec service could not look the sandbox up. Retry shortly.',
|
|
63
|
+
parent_not_running: 'Only a running or paused sandbox can be forked.',
|
|
64
|
+
parent_not_forkable: 'This sandbox cannot be forked.',
|
|
65
|
+
fork_capacity: "The parent sandbox's machine has no room for that many copies right now.",
|
|
66
|
+
fork_unsupported_host: "The parent sandbox's machine cannot start copies of it. Create a new sandbox, set it up again, and fork that one.",
|
|
67
|
+
invalid_count: 'Fork count is out of range.',
|
|
68
|
+
body_too_large: 'The request body is over the size limit.',
|
|
69
|
+
file_too_large: 'The file is over the 100 MiB transfer limit.',
|
|
70
|
+
missing_path: 'A file path is required.',
|
|
71
|
+
path_outside_root: 'File paths must be inside the sandbox user\'s home directory (/home/prized).',
|
|
72
|
+
path_not_absolute: 'File paths must be absolute.',
|
|
73
|
+
invalid_path: 'File paths may not contain ".." or NUL.',
|
|
74
|
+
symlink_not_allowed: 'File transfers cannot follow a symbolic link in this path (one leading outside /home/prized, or a loop).',
|
|
75
|
+
not_regular_file: 'Only regular files can be downloaded.',
|
|
76
|
+
permission_denied: 'Permission denied in the sandbox.',
|
|
77
|
+
missing_command: 'The command is empty.',
|
|
78
|
+
cwd_not_absolute: 'cwd must be an absolute path.',
|
|
79
|
+
bad_env: 'An environment variable name or value is not valid.',
|
|
80
|
+
spawn_failed: 'The command could not be started (is cwd a directory that exists?).',
|
|
81
|
+
};
|
|
82
|
+
/** The sentence an error with this code and no message of its own carries. */
|
|
83
|
+
function defaultMessage(code) {
|
|
84
|
+
return MESSAGES[code] || code.replace(/_/g, ' ');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every failure the SDK throws.
|
|
88
|
+
*
|
|
89
|
+
* - `status`: the HTTP status, or `null` when nothing came back (a network
|
|
90
|
+
* failure, a wait that timed out, a refused argument).
|
|
91
|
+
* - `code`: the API's error string, e.g. `paid_balance_required`,
|
|
92
|
+
* `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
|
|
93
|
+
* `rate_limited`; or one of the SDK's own: `missing_api_key`,
|
|
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`,
|
|
97
|
+
* `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
|
|
98
|
+
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
|
|
99
|
+
* `output_read_failed` (thrown as OutputReadError).
|
|
100
|
+
* - `reason`: the finer-grained reason, when there is one (for
|
|
101
|
+
* `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
|
|
102
|
+
* `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
|
|
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.
|
|
109
|
+
*/
|
|
110
|
+
class PrizedError extends Error {
|
|
111
|
+
status;
|
|
112
|
+
code;
|
|
113
|
+
reason;
|
|
114
|
+
sandboxId;
|
|
115
|
+
body;
|
|
116
|
+
retryAfter;
|
|
117
|
+
exitCode;
|
|
118
|
+
constructor(code, message, init = {}) {
|
|
119
|
+
super(message || defaultMessage(code), init.cause === undefined ? undefined : { cause: init.cause });
|
|
120
|
+
this.name = 'PrizedError';
|
|
121
|
+
this.code = code;
|
|
122
|
+
this.status = init.status ?? null;
|
|
123
|
+
this.reason = init.reason ?? null;
|
|
124
|
+
this.sandboxId = init.sandboxId ?? null;
|
|
125
|
+
this.body = init.body ?? null;
|
|
126
|
+
this.retryAfter = init.retryAfter ?? null;
|
|
127
|
+
this.exitCode = init.exitCode ?? null;
|
|
128
|
+
}
|
|
129
|
+
toString() {
|
|
130
|
+
const head = this.status === null ? this.code : `${this.status} ${this.code}`;
|
|
131
|
+
const extra = this.reason ? ` (${this.reason})` : '';
|
|
132
|
+
return `${this.name}: ${head}${extra}: ${this.message}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.PrizedError = PrizedError;
|
|
136
|
+
/**
|
|
137
|
+
* A command ran past its timeout and was killed (`code` is
|
|
138
|
+
* `command_timeout`). The sandbox answered as it does for any command that
|
|
139
|
+
* ends, so nothing is lost: `result` is that answer, with what the command
|
|
140
|
+
* printed before it was killed, `exitCode: -1` and `timedOut: true`.
|
|
141
|
+
* `timeoutMs` is the limit, or null when the SDK did not set one.
|
|
142
|
+
*/
|
|
143
|
+
class CommandTimeoutError extends PrizedError {
|
|
144
|
+
result;
|
|
145
|
+
timeoutMs;
|
|
146
|
+
constructor(result, timeoutMs, sandboxId = null) {
|
|
147
|
+
const limit = timeoutMs === null ? 'its timeout' : `its ${timeoutMs} ms timeout`;
|
|
148
|
+
super('command_timeout', `The command ran past ${limit} and was killed; this error's result has what it printed. ` +
|
|
149
|
+
'Pass a larger timeoutMs (0 for no limit) to give it longer.', { sandboxId });
|
|
150
|
+
this.name = 'CommandTimeoutError';
|
|
151
|
+
this.result = result;
|
|
152
|
+
this.timeoutMs = timeoutMs;
|
|
153
|
+
}
|
|
154
|
+
/** What the command printed to stdout before it was killed. */
|
|
155
|
+
get stdout() {
|
|
156
|
+
return this.result.stdout;
|
|
157
|
+
}
|
|
158
|
+
/** What the command printed to stderr before it was killed. */
|
|
159
|
+
get stderr() {
|
|
160
|
+
return this.result.stderr;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
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;
|
|
189
|
+
function stringField(record, key) {
|
|
190
|
+
const value = record[key];
|
|
191
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* fork_capacity's sentence from the answer's `fits` (the API's own, worded
|
|
195
|
+
* for the SDK), for an answer without one: fewer copies help only when some
|
|
196
|
+
* fit.
|
|
197
|
+
*/
|
|
198
|
+
function forkCapacityMessage(fits) {
|
|
199
|
+
if (typeof fits !== 'number' || !Number.isInteger(fits) || fits < 0)
|
|
200
|
+
return null;
|
|
201
|
+
const newParent = 'start a new sandbox (it starts fresh) and fork that one, keeping the parent until it runs';
|
|
202
|
+
const atLimit = 'The new sandbox and each copy count toward your sandbox limit: at it, delete sandboxes you no longer need ' +
|
|
203
|
+
'and wait until they are gone (copy out what you need first, and keep the parent while a copy forked from it ' +
|
|
204
|
+
'is still starting), or, on the free tier, top up to raise it.';
|
|
205
|
+
if (fits === 0) {
|
|
206
|
+
return `The parent sandbox's machine is full, so no copy of it fits right now. Try again later, or ${newParent}. ${atLimit}`;
|
|
207
|
+
}
|
|
208
|
+
const copies = fits === 1 ? '1 more copy' : `${fits} more copies`;
|
|
209
|
+
return `The parent sandbox's machine has room for ${copies} right now. Fork ${fits} or fewer, or ${newParent}. ${atLimit}`;
|
|
210
|
+
}
|
|
211
|
+
/** The unit of a free-tier refusal's `limit`, by its reason (or, for a shape, its field): the API's own units. */
|
|
212
|
+
const FREE_UNITS = {
|
|
213
|
+
sandbox_limit: 'sandboxes at once',
|
|
214
|
+
paused_limit: 'paused sandboxes',
|
|
215
|
+
running_vcpu: 'vCPU running at once',
|
|
216
|
+
running_memory: 'MB of memory running at once',
|
|
217
|
+
retained_disk: 'GB of disk',
|
|
218
|
+
max_runtime: 'minutes of run time',
|
|
219
|
+
vcpu: 'vCPU per sandbox',
|
|
220
|
+
memoryMb: 'MB of memory per sandbox',
|
|
221
|
+
};
|
|
222
|
+
/** A free-tier refusal's sentence: what refused it, and what lifts it. */
|
|
223
|
+
function freeTierMessage(code, message, reason, limit) {
|
|
224
|
+
if (message === null && code === 'invalid_resources')
|
|
225
|
+
message = "The sandbox's shape is over the free tier's limit for one sandbox.";
|
|
226
|
+
else if (message === null && reason === 'max_runtime')
|
|
227
|
+
message = 'The run time asked for is longer than the free tier allows.';
|
|
228
|
+
const base = message ?? MESSAGES[code] ?? code.replace(/_/g, ' ');
|
|
229
|
+
const unit = reason === null ? undefined : FREE_UNITS[reason];
|
|
230
|
+
const which = unit !== undefined && typeof limit === 'number' && Number.isInteger(limit) ? ` (${limit} ${unit})` : '';
|
|
231
|
+
return `${base} It is a free-tier limit${which}: a top-up or a plan lifts it (${exports.FREE_TIER_URL}).`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* A 404 about a sandbox the caller named: most often one deleted when its
|
|
235
|
+
* run time ended. A file call's 404 may be the file instead, and the edge
|
|
236
|
+
* answers both the same way.
|
|
237
|
+
*/
|
|
238
|
+
function notFoundMessage(sandboxId, path) {
|
|
239
|
+
const gone = 'it was deleted, or its run time ended (60 minutes of running on the free tier)';
|
|
240
|
+
if (path.includes('/files/'))
|
|
241
|
+
return `Not found: no such file in sandbox ${sandboxId}, or the sandbox is gone (${gone}).`;
|
|
242
|
+
return `No sandbox ${sandboxId} for this API key: ${gone}, or it is in another workspace. Sandbox.create() makes a new one.`;
|
|
243
|
+
}
|
|
244
|
+
/** Retry-After as seconds: delta-seconds or an HTTP date. */
|
|
245
|
+
function parseRetryAfter(value, nowMs) {
|
|
246
|
+
if (value === null || value.trim() === '')
|
|
247
|
+
return null;
|
|
248
|
+
const seconds = Number(value);
|
|
249
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
250
|
+
return seconds;
|
|
251
|
+
const at = Date.parse(value);
|
|
252
|
+
return Number.isNaN(at) ? null : Math.max(0, (at - nowMs) / 1000);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The error document → a PrizedError. Two envelopes are read: today's flat
|
|
256
|
+
* `{ "error": "code", "message"?, "reason"?, "field"? }` and the nested
|
|
257
|
+
* `{ "error": { "code", "message" } }`. Anything else (a proxy's HTML page)
|
|
258
|
+
* becomes `http_error` with the status. `path` is the request's URL path,
|
|
259
|
+
* which tells a file call's 404 from a sandbox's.
|
|
260
|
+
*/
|
|
261
|
+
function errorFromResponse(status, text, retryAfter, sandboxId, path = '') {
|
|
262
|
+
let doc = null;
|
|
263
|
+
try {
|
|
264
|
+
const parsed = text ? JSON.parse(text) : null;
|
|
265
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
266
|
+
doc = parsed;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Not JSON: handled below.
|
|
271
|
+
}
|
|
272
|
+
let code = null;
|
|
273
|
+
let message = null;
|
|
274
|
+
if (doc) {
|
|
275
|
+
const envelope = doc.error;
|
|
276
|
+
if (typeof envelope === 'string' && envelope !== '') {
|
|
277
|
+
code = envelope;
|
|
278
|
+
}
|
|
279
|
+
else if (envelope !== null && typeof envelope === 'object' && !Array.isArray(envelope)) {
|
|
280
|
+
code = stringField(envelope, 'code');
|
|
281
|
+
message = stringField(envelope, 'message');
|
|
282
|
+
}
|
|
283
|
+
message = message ?? stringField(doc, 'message');
|
|
284
|
+
}
|
|
285
|
+
if (code === null) {
|
|
286
|
+
const snippet = text.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
287
|
+
return new PrizedError('http_error', `HTTP ${status}${snippet ? `: ${snippet}` : ''}`, {
|
|
288
|
+
status,
|
|
289
|
+
sandboxId,
|
|
290
|
+
retryAfter,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (message === null && code === 'fork_capacity' && doc)
|
|
294
|
+
message = forkCapacityMessage(doc.fits);
|
|
295
|
+
if (message === null && code === 'not_found' && status === 404 && sandboxId !== null)
|
|
296
|
+
message = notFoundMessage(sandboxId, path);
|
|
297
|
+
const reason = doc ? stringField(doc, 'reason') ?? stringField(doc, 'field') ?? stringField(doc, 'detail') : null;
|
|
298
|
+
if (doc && doc.tier === 'free' && !(message ?? '').includes('free tier'))
|
|
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)}`;
|
|
305
|
+
return new PrizedError(code, message, { status, reason, sandboxId, body: doc, retryAfter });
|
|
306
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type Toolbox } from './toolbox.js';
|
|
2
|
+
import type { FileInfo, WriteInfo } from './types.js';
|
|
3
|
+
/** What files.write accepts. */
|
|
4
|
+
export type FileData = string | Uint8Array | ArrayBuffer | Blob;
|
|
5
|
+
/** Options for files.write() and files.upload(). */
|
|
6
|
+
export interface WriteOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The file's permission bits, 0 to 0o777 (`0o755` for a script). Without
|
|
9
|
+
* it, a file that is replaced keeps its mode, and a new file gets the
|
|
10
|
+
* sandbox's default (0600 in prized-base-v1 and prized-agent-v1).
|
|
11
|
+
*/
|
|
12
|
+
mode?: number;
|
|
13
|
+
}
|
|
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
|
+
*/
|
|
20
|
+
export declare const FILE_TIMEOUT_MS: number;
|
|
21
|
+
/** The publish script's own exits (the Python SDK's too). */
|
|
22
|
+
export declare const EXIT_TARGET_IS_LINK = 47;
|
|
23
|
+
export declare const EXIT_TARGET_NOT_REGULAR = 48;
|
|
24
|
+
/**
|
|
25
|
+
* Where a write to `target` is staged: `clean` is the target as the files
|
|
26
|
+
* API reads it (`.`, `//` and a trailing `/` gone), and `staging` a fresh
|
|
27
|
+
* name in the same directory. Null when the files API has to judge the path
|
|
28
|
+
* itself (outside /home/prized, `..`, NUL): it is then uploaded as it is, and
|
|
29
|
+
* refused as it always was.
|
|
30
|
+
*/
|
|
31
|
+
export declare function stagingFor(target: string, suffix?: string): {
|
|
32
|
+
clean: string;
|
|
33
|
+
staging: string;
|
|
34
|
+
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* The command that puts the staged upload `staging` in place at `target`
|
|
37
|
+
* (both absolute, in one directory) with one rename. A regular file there
|
|
38
|
+
* lends its permission bits (setuid, setgid and sticky are not carried) and,
|
|
39
|
+
* where the sandbox user may, its owner and group; `mode`, when given, sets
|
|
40
|
+
* the bits instead. With nothing there the staged file keeps the files API's
|
|
41
|
+
* mode for a new file. A link there exits 47 and a directory or other
|
|
42
|
+
* non-regular file exits 48, changing nothing. The command removes the
|
|
43
|
+
* staged file as it exits (unless it is killed).
|
|
44
|
+
*/
|
|
45
|
+
export declare function publishScript(staging: string, target: string, mode: number | null): string;
|
|
46
|
+
/** File operations on one sandbox. Paths are absolute, or relative to /home/prized/work. */
|
|
47
|
+
export declare class Files {
|
|
48
|
+
private readonly toolbox;
|
|
49
|
+
/** @internal */
|
|
50
|
+
constructor(toolbox: Toolbox);
|
|
51
|
+
private url;
|
|
52
|
+
/** A file's contents as UTF-8 text. */
|
|
53
|
+
read(path: string): Promise<string>;
|
|
54
|
+
/** A file's contents as bytes (at most 100 MiB). */
|
|
55
|
+
readBytes(path: string): Promise<Uint8Array>;
|
|
56
|
+
/**
|
|
57
|
+
* Write a file (at most 100 MiB), replacing it whole if it exists. Missing
|
|
58
|
+
* parent directories are created. The path must be inside /home/prized.
|
|
59
|
+
* A file that is replaced keeps its mode, so a script made executable
|
|
60
|
+
* stays executable; a new file gets the sandbox's default (0600 in
|
|
61
|
+
* prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
|
|
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).
|
|
69
|
+
*/
|
|
70
|
+
write(path: string, data: FileData, options?: WriteOptions): Promise<WriteInfo>;
|
|
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
|
+
*/
|
|
78
|
+
private send;
|
|
79
|
+
/**
|
|
80
|
+
* Remove a staged upload that was not put in place (best effort: the
|
|
81
|
+
* write's own error matters more). Sent again like a read, unlike other
|
|
82
|
+
* removes: the name is this call's own, so a second rm finds nothing
|
|
83
|
+
* anyone else made.
|
|
84
|
+
*/
|
|
85
|
+
private discard;
|
|
86
|
+
private chmod;
|
|
87
|
+
/** One of a write's own commands, run in the home, which is always there (the workspace may not be). */
|
|
88
|
+
private run;
|
|
89
|
+
/** A directory's entries, sorted by name (default: /home/prized/work). */
|
|
90
|
+
list(path?: string): Promise<FileInfo[]>;
|
|
91
|
+
/** Whether a path exists (a file, a directory, or a link, even a dangling one). */
|
|
92
|
+
exists(path: string): Promise<boolean>;
|
|
93
|
+
/** Remove a file, or a directory tree with `recursive: true`. A missing path is not an error. */
|
|
94
|
+
remove(path: string, options?: {
|
|
95
|
+
recursive?: boolean;
|
|
96
|
+
}): Promise<void>;
|
|
97
|
+
/** Copy a local file into the sandbox (Node, Bun, Deno), as write() does. */
|
|
98
|
+
upload(localPath: string, remotePath: string, options?: WriteOptions): Promise<WriteInfo>;
|
|
99
|
+
/** Copy a file out of the sandbox to a local path (Node, Bun, Deno). Returns the byte count. */
|
|
100
|
+
download(remotePath: string, localPath: string): Promise<number>;
|
|
101
|
+
}
|