@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.
Files changed (52) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +666 -2
  3. package/dist/cjs/commands.d.ts +199 -0
  4. package/dist/cjs/commands.js +597 -0
  5. package/dist/cjs/errors.d.ts +113 -0
  6. package/dist/cjs/errors.js +306 -0
  7. package/dist/cjs/files.d.ts +101 -0
  8. package/dist/cjs/files.js +387 -0
  9. package/dist/cjs/http.d.ts +226 -0
  10. package/dist/cjs/http.js +618 -0
  11. package/dist/cjs/index.d.ts +12 -0
  12. package/dist/cjs/index.js +23 -0
  13. package/dist/cjs/outcome.d.ts +76 -0
  14. package/dist/cjs/outcome.js +170 -0
  15. package/dist/cjs/output.d.ts +70 -0
  16. package/dist/cjs/output.js +383 -0
  17. package/dist/cjs/package.json +1 -0
  18. package/dist/cjs/sandbox.d.ts +218 -0
  19. package/dist/cjs/sandbox.js +836 -0
  20. package/dist/cjs/shell.d.ts +152 -0
  21. package/dist/cjs/shell.js +393 -0
  22. package/dist/cjs/toolbox.d.ts +48 -0
  23. package/dist/cjs/toolbox.js +88 -0
  24. package/dist/cjs/types.d.ts +111 -0
  25. package/dist/cjs/types.js +102 -0
  26. package/dist/cjs/version.d.ts +2 -0
  27. package/dist/cjs/version.js +5 -0
  28. package/dist/esm/commands.d.ts +199 -0
  29. package/dist/esm/commands.js +590 -0
  30. package/dist/esm/errors.d.ts +113 -0
  31. package/dist/esm/errors.js +297 -0
  32. package/dist/esm/files.d.ts +101 -0
  33. package/dist/esm/files.js +381 -0
  34. package/dist/esm/http.d.ts +226 -0
  35. package/dist/esm/http.js +601 -0
  36. package/dist/esm/index.d.ts +12 -0
  37. package/dist/esm/index.js +7 -0
  38. package/dist/esm/outcome.d.ts +76 -0
  39. package/dist/esm/outcome.js +155 -0
  40. package/dist/esm/output.d.ts +70 -0
  41. package/dist/esm/output.js +374 -0
  42. package/dist/esm/sandbox.d.ts +218 -0
  43. package/dist/esm/sandbox.js +832 -0
  44. package/dist/esm/shell.d.ts +152 -0
  45. package/dist/esm/shell.js +379 -0
  46. package/dist/esm/toolbox.d.ts +48 -0
  47. package/dist/esm/toolbox.js +83 -0
  48. package/dist/esm/types.d.ts +111 -0
  49. package/dist/esm/types.js +95 -0
  50. package/dist/esm/version.d.ts +2 -0
  51. package/dist/esm/version.js +2 -0
  52. 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,297 @@
1
+ /** Where a member makes an API key. */
2
+ export const API_KEYS_URL = 'https://prized.dev/dashboard/workspace/api-keys';
3
+ /** Where a newcomer signs up (free credit, no card) and gets one. */
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}`;
11
+ /** The free tier's limits, and what lifts them. */
12
+ export const FREE_TIER_URL = 'https://prized.dev/docs/sandboxes#free-tier';
13
+ /** A sentence for the codes a caller is most likely to meet, used when the
14
+ * answer carries no `message` of its own. */
15
+ const MESSAGES = {
16
+ // The SDK's own codes.
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}`,
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.',
21
+ network_error: 'Could not reach the Prized API.',
22
+ timeout: 'Timed out.',
23
+ aborted: 'The operation was aborted.',
24
+ bad_response: 'The API answered with something the SDK could not read.',
25
+ http_error: 'The API answered with an error the SDK does not recognise.',
26
+ unexpected_redirect: 'The API answered with a redirect; the SDK does not follow redirects with your key.',
27
+ sandbox_error: 'The sandbox failed.',
28
+ sandbox_deleted: 'The sandbox was deleted.',
29
+ command_failed: 'The command failed in the sandbox.',
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.',
32
+ not_directory: 'Not a directory.',
33
+ // The API's and the exec edge's codes.
34
+ unauthorized: `The API key was refused. Check that it is a live key from ${API_KEYS_URL}.`,
35
+ token_expired: `The API key has expired. Make a new one at ${API_KEYS_URL}.`,
36
+ key_access_denied: 'This API key does not have the Sandboxes permission it needs.',
37
+ not_found: 'Not found (or not visible to this API key).',
38
+ bad_request: 'The request was not valid.',
39
+ unknown_field: 'The request carried a field the API does not accept.',
40
+ template_not_found: 'No such template. Sandbox.templates() lists the ones you can use.',
41
+ template_inactive: 'That template is no longer offered. Sandbox.templates() lists the current ones.',
42
+ invalid_resources: 'The requested shape is outside what the template allows.',
43
+ invalid_max_runtime: 'maxRuntimeMin must be a positive whole number of minutes.',
44
+ paid_balance_required: 'Sandboxes need a paid balance: add funds or pick a paid plan, then retry. Free trial credit cannot start a sandbox.',
45
+ insufficient_balance: 'The workspace balance is empty. Add funds, then retry.',
46
+ sandbox_quota_exceeded: 'The workspace is at a sandbox quota. Delete sandboxes you no longer need (paused ones still count), then retry.',
47
+ sandboxes_disabled: 'Sandboxes are not available right now.',
48
+ sandbox_create_disabled: 'Creating sandboxes is not available right now.',
49
+ sandbox_fork_disabled: 'Forking is paused for maintenance. Retry later.',
50
+ idempotency_conflict: 'This idempotency key was already used for a different request.',
51
+ invalid_idempotency_key: 'Idempotency keys are 1 to 128 printable characters with no spaces.',
52
+ rate_limited: 'Too many requests. Slow down and retry.',
53
+ sandbox_not_running: 'The sandbox is not running and could not be woken (it is resizing, failed, or being deleted).',
54
+ sandbox_not_ready: 'The sandbox did not finish starting in time. Nothing ran; retry.',
55
+ sandbox_unreachable: 'The sandbox did not answer. If this was a command, it may or may not have run.',
56
+ resolve_failed: 'The exec service could not look the sandbox up. Retry shortly.',
57
+ parent_not_running: 'Only a running or paused sandbox can be forked.',
58
+ parent_not_forkable: 'This sandbox cannot be forked.',
59
+ fork_capacity: "The parent sandbox's machine has no room for that many copies right now.",
60
+ 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.",
61
+ invalid_count: 'Fork count is out of range.',
62
+ body_too_large: 'The request body is over the size limit.',
63
+ file_too_large: 'The file is over the 100 MiB transfer limit.',
64
+ missing_path: 'A file path is required.',
65
+ path_outside_root: 'File paths must be inside the sandbox user\'s home directory (/home/prized).',
66
+ path_not_absolute: 'File paths must be absolute.',
67
+ invalid_path: 'File paths may not contain ".." or NUL.',
68
+ symlink_not_allowed: 'File transfers cannot follow a symbolic link in this path (one leading outside /home/prized, or a loop).',
69
+ not_regular_file: 'Only regular files can be downloaded.',
70
+ permission_denied: 'Permission denied in the sandbox.',
71
+ missing_command: 'The command is empty.',
72
+ cwd_not_absolute: 'cwd must be an absolute path.',
73
+ bad_env: 'An environment variable name or value is not valid.',
74
+ spawn_failed: 'The command could not be started (is cwd a directory that exists?).',
75
+ };
76
+ /** The sentence an error with this code and no message of its own carries. */
77
+ export function defaultMessage(code) {
78
+ return MESSAGES[code] || code.replace(/_/g, ' ');
79
+ }
80
+ /**
81
+ * Every failure the SDK throws.
82
+ *
83
+ * - `status`: the HTTP status, or `null` when nothing came back (a network
84
+ * failure, a wait that timed out, a refused argument).
85
+ * - `code`: the API's error string, e.g. `paid_balance_required`,
86
+ * `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
87
+ * `rate_limited`; or one of the SDK's own: `missing_api_key`,
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`,
91
+ * `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
92
+ * `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
93
+ * `output_read_failed` (thrown as OutputReadError).
94
+ * - `reason`: the finer-grained reason, when there is one (for
95
+ * `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
96
+ * `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
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.
103
+ */
104
+ export class PrizedError extends Error {
105
+ status;
106
+ code;
107
+ reason;
108
+ sandboxId;
109
+ body;
110
+ retryAfter;
111
+ exitCode;
112
+ constructor(code, message, init = {}) {
113
+ super(message || defaultMessage(code), init.cause === undefined ? undefined : { cause: init.cause });
114
+ this.name = 'PrizedError';
115
+ this.code = code;
116
+ this.status = init.status ?? null;
117
+ this.reason = init.reason ?? null;
118
+ this.sandboxId = init.sandboxId ?? null;
119
+ this.body = init.body ?? null;
120
+ this.retryAfter = init.retryAfter ?? null;
121
+ this.exitCode = init.exitCode ?? null;
122
+ }
123
+ toString() {
124
+ const head = this.status === null ? this.code : `${this.status} ${this.code}`;
125
+ const extra = this.reason ? ` (${this.reason})` : '';
126
+ return `${this.name}: ${head}${extra}: ${this.message}`;
127
+ }
128
+ }
129
+ /**
130
+ * A command ran past its timeout and was killed (`code` is
131
+ * `command_timeout`). The sandbox answered as it does for any command that
132
+ * ends, so nothing is lost: `result` is that answer, with what the command
133
+ * printed before it was killed, `exitCode: -1` and `timedOut: true`.
134
+ * `timeoutMs` is the limit, or null when the SDK did not set one.
135
+ */
136
+ export class CommandTimeoutError extends PrizedError {
137
+ result;
138
+ timeoutMs;
139
+ constructor(result, timeoutMs, sandboxId = null) {
140
+ const limit = timeoutMs === null ? 'its timeout' : `its ${timeoutMs} ms timeout`;
141
+ super('command_timeout', `The command ran past ${limit} and was killed; this error's result has what it printed. ` +
142
+ 'Pass a larger timeoutMs (0 for no limit) to give it longer.', { sandboxId });
143
+ this.name = 'CommandTimeoutError';
144
+ this.result = result;
145
+ this.timeoutMs = timeoutMs;
146
+ }
147
+ /** What the command printed to stdout before it was killed. */
148
+ get stdout() {
149
+ return this.result.stdout;
150
+ }
151
+ /** What the command printed to stderr before it was killed. */
152
+ get stderr() {
153
+ return this.result.stderr;
154
+ }
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
+ }
180
+ function stringField(record, key) {
181
+ const value = record[key];
182
+ return typeof value === 'string' && value !== '' ? value : null;
183
+ }
184
+ /**
185
+ * fork_capacity's sentence from the answer's `fits` (the API's own, worded
186
+ * for the SDK), for an answer without one: fewer copies help only when some
187
+ * fit.
188
+ */
189
+ function forkCapacityMessage(fits) {
190
+ if (typeof fits !== 'number' || !Number.isInteger(fits) || fits < 0)
191
+ return null;
192
+ const newParent = 'start a new sandbox (it starts fresh) and fork that one, keeping the parent until it runs';
193
+ const atLimit = 'The new sandbox and each copy count toward your sandbox limit: at it, delete sandboxes you no longer need ' +
194
+ 'and wait until they are gone (copy out what you need first, and keep the parent while a copy forked from it ' +
195
+ 'is still starting), or, on the free tier, top up to raise it.';
196
+ if (fits === 0) {
197
+ return `The parent sandbox's machine is full, so no copy of it fits right now. Try again later, or ${newParent}. ${atLimit}`;
198
+ }
199
+ const copies = fits === 1 ? '1 more copy' : `${fits} more copies`;
200
+ return `The parent sandbox's machine has room for ${copies} right now. Fork ${fits} or fewer, or ${newParent}. ${atLimit}`;
201
+ }
202
+ /** The unit of a free-tier refusal's `limit`, by its reason (or, for a shape, its field): the API's own units. */
203
+ const FREE_UNITS = {
204
+ sandbox_limit: 'sandboxes at once',
205
+ paused_limit: 'paused sandboxes',
206
+ running_vcpu: 'vCPU running at once',
207
+ running_memory: 'MB of memory running at once',
208
+ retained_disk: 'GB of disk',
209
+ max_runtime: 'minutes of run time',
210
+ vcpu: 'vCPU per sandbox',
211
+ memoryMb: 'MB of memory per sandbox',
212
+ };
213
+ /** A free-tier refusal's sentence: what refused it, and what lifts it. */
214
+ function freeTierMessage(code, message, reason, limit) {
215
+ if (message === null && code === 'invalid_resources')
216
+ message = "The sandbox's shape is over the free tier's limit for one sandbox.";
217
+ else if (message === null && reason === 'max_runtime')
218
+ message = 'The run time asked for is longer than the free tier allows.';
219
+ const base = message ?? MESSAGES[code] ?? code.replace(/_/g, ' ');
220
+ const unit = reason === null ? undefined : FREE_UNITS[reason];
221
+ const which = unit !== undefined && typeof limit === 'number' && Number.isInteger(limit) ? ` (${limit} ${unit})` : '';
222
+ return `${base} It is a free-tier limit${which}: a top-up or a plan lifts it (${FREE_TIER_URL}).`;
223
+ }
224
+ /**
225
+ * A 404 about a sandbox the caller named: most often one deleted when its
226
+ * run time ended. A file call's 404 may be the file instead, and the edge
227
+ * answers both the same way.
228
+ */
229
+ function notFoundMessage(sandboxId, path) {
230
+ const gone = 'it was deleted, or its run time ended (60 minutes of running on the free tier)';
231
+ if (path.includes('/files/'))
232
+ return `Not found: no such file in sandbox ${sandboxId}, or the sandbox is gone (${gone}).`;
233
+ return `No sandbox ${sandboxId} for this API key: ${gone}, or it is in another workspace. Sandbox.create() makes a new one.`;
234
+ }
235
+ /** Retry-After as seconds: delta-seconds or an HTTP date. */
236
+ export function parseRetryAfter(value, nowMs) {
237
+ if (value === null || value.trim() === '')
238
+ return null;
239
+ const seconds = Number(value);
240
+ if (Number.isFinite(seconds) && seconds >= 0)
241
+ return seconds;
242
+ const at = Date.parse(value);
243
+ return Number.isNaN(at) ? null : Math.max(0, (at - nowMs) / 1000);
244
+ }
245
+ /**
246
+ * The error document → a PrizedError. Two envelopes are read: today's flat
247
+ * `{ "error": "code", "message"?, "reason"?, "field"? }` and the nested
248
+ * `{ "error": { "code", "message" } }`. Anything else (a proxy's HTML page)
249
+ * becomes `http_error` with the status. `path` is the request's URL path,
250
+ * which tells a file call's 404 from a sandbox's.
251
+ */
252
+ export function errorFromResponse(status, text, retryAfter, sandboxId, path = '') {
253
+ let doc = null;
254
+ try {
255
+ const parsed = text ? JSON.parse(text) : null;
256
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
257
+ doc = parsed;
258
+ }
259
+ }
260
+ catch {
261
+ // Not JSON: handled below.
262
+ }
263
+ let code = null;
264
+ let message = null;
265
+ if (doc) {
266
+ const envelope = doc.error;
267
+ if (typeof envelope === 'string' && envelope !== '') {
268
+ code = envelope;
269
+ }
270
+ else if (envelope !== null && typeof envelope === 'object' && !Array.isArray(envelope)) {
271
+ code = stringField(envelope, 'code');
272
+ message = stringField(envelope, 'message');
273
+ }
274
+ message = message ?? stringField(doc, 'message');
275
+ }
276
+ if (code === null) {
277
+ const snippet = text.replace(/\s+/g, ' ').trim().slice(0, 160);
278
+ return new PrizedError('http_error', `HTTP ${status}${snippet ? `: ${snippet}` : ''}`, {
279
+ status,
280
+ sandboxId,
281
+ retryAfter,
282
+ });
283
+ }
284
+ if (message === null && code === 'fork_capacity' && doc)
285
+ message = forkCapacityMessage(doc.fits);
286
+ if (message === null && code === 'not_found' && status === 404 && sandboxId !== null)
287
+ message = notFoundMessage(sandboxId, path);
288
+ const reason = doc ? stringField(doc, 'reason') ?? stringField(doc, 'field') ?? stringField(doc, 'detail') : null;
289
+ if (doc && doc.tier === 'free' && !(message ?? '').includes('free tier'))
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)}`;
296
+ return new PrizedError(code, message, { status, reason, sandboxId, body: doc, retryAfter });
297
+ }
@@ -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
+ }