@prized/sdk 0.1.0 → 0.1.2
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 +76 -21
- package/dist/cjs/commands.d.ts +47 -14
- package/dist/cjs/commands.js +94 -26
- package/dist/cjs/errors.d.ts +43 -3
- package/dist/cjs/errors.js +54 -6
- package/dist/cjs/files.d.ts +21 -5
- package/dist/cjs/files.js +38 -14
- package/dist/cjs/http.d.ts +74 -15
- package/dist/cjs/http.js +208 -32
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/outcome.d.ts +11 -1
- package/dist/cjs/outcome.js +28 -5
- package/dist/cjs/output.d.ts +19 -8
- package/dist/cjs/output.js +152 -56
- package/dist/cjs/sandbox.d.ts +3 -2
- package/dist/cjs/sandbox.js +40 -5
- package/dist/cjs/shell.d.ts +2 -1
- package/dist/cjs/shell.js +2 -1
- package/dist/cjs/toolbox.d.ts +2 -0
- package/dist/cjs/toolbox.js +4 -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 +47 -14
- package/dist/esm/commands.js +95 -27
- package/dist/esm/errors.d.ts +43 -3
- package/dist/esm/errors.js +52 -5
- package/dist/esm/files.d.ts +21 -5
- package/dist/esm/files.js +39 -15
- package/dist/esm/http.d.ts +74 -15
- package/dist/esm/http.js +207 -31
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/outcome.d.ts +11 -1
- package/dist/esm/outcome.js +27 -5
- package/dist/esm/output.d.ts +19 -8
- package/dist/esm/output.js +153 -58
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +40 -5
- package/dist/esm/shell.d.ts +2 -1
- package/dist/esm/shell.js +2 -1
- package/dist/esm/toolbox.d.ts +2 -0
- package/dist/esm/toolbox.js +4 -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/files.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// sandbox.files: read, write, list, remove, upload, download.
|
|
2
2
|
import { PrizedError } from './errors.js';
|
|
3
|
-
import { request } from './http.js';
|
|
3
|
+
import { request, timing } 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. An attempt that moves no bytes for
|
|
10
|
+
* timing.transferStallMs (2 minutes) is given up; a download, or a write's
|
|
11
|
+
* staged upload, is then sent again (as after a lost answer or a 502, 503 or
|
|
12
|
+
* 504) while time is left.
|
|
13
|
+
*/
|
|
8
14
|
export const FILE_TIMEOUT_MS = 10 * 60_000;
|
|
9
15
|
function byteSize(data) {
|
|
10
16
|
if (typeof data === 'string')
|
|
@@ -40,12 +46,16 @@ export const EXIT_TARGET_NOT_REGULAR = 48;
|
|
|
40
46
|
const DISCARD_MS = 10_000;
|
|
41
47
|
/** A write's own commands: a rename, a chmod or an rm. */
|
|
42
48
|
const WRITE_COMMAND_MS = 60_000;
|
|
43
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* For the staged upload: the target is changed only by the command that
|
|
51
|
+
* follows it. The upload may have been sent again, so it says only what to
|
|
52
|
+
* do (langchain-prized's staged_upload_effect words it the same).
|
|
53
|
+
*/
|
|
44
54
|
function stagedEffect(path) {
|
|
45
55
|
return {
|
|
46
56
|
maybe: `${path} was not changed (its new bytes may have reached the sandbox, but were not put in place)`,
|
|
47
57
|
did: `${path} was not changed yet`,
|
|
48
|
-
check: '
|
|
58
|
+
check: 'Write it again.',
|
|
49
59
|
};
|
|
50
60
|
}
|
|
51
61
|
/** For the command that puts the staged upload in place: a 2xx proves it ran, not how it ended. */
|
|
@@ -154,6 +164,7 @@ export class Files {
|
|
|
154
164
|
retry: 'safe',
|
|
155
165
|
read: 'bytes',
|
|
156
166
|
timeoutMs: FILE_TIMEOUT_MS,
|
|
167
|
+
stallMs: timing.transferStallMs,
|
|
157
168
|
deadline: this.toolbox.config.now() + FILE_TIMEOUT_MS,
|
|
158
169
|
sandboxId: this.toolbox.sandboxId,
|
|
159
170
|
});
|
|
@@ -165,9 +176,13 @@ export class Files {
|
|
|
165
176
|
* A file that is replaced keeps its mode, so a script made executable
|
|
166
177
|
* stays executable; a new file gets the sandbox's default (0600 in
|
|
167
178
|
* prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
|
|
168
|
-
*
|
|
169
|
-
* lost
|
|
170
|
-
*
|
|
179
|
+
* The bytes go up under a temporary name beside the file, sent again if
|
|
180
|
+
* need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
|
|
181
|
+
* moving; all copies within the transfer's 10 minutes),
|
|
182
|
+
* and one command that is never sent again puts them in place: a write
|
|
183
|
+
* whose answer to that command was lost throws, since it may have landed
|
|
184
|
+
* and the sandbox written the file since (read it back, or write again,
|
|
185
|
+
* as suits the file).
|
|
171
186
|
*/
|
|
172
187
|
async write(path, data, options = {}) {
|
|
173
188
|
const sandboxId = this.toolbox.sandboxId;
|
|
@@ -179,14 +194,18 @@ export class Files {
|
|
|
179
194
|
const target = sandboxPath(path);
|
|
180
195
|
const plan = stagingFor(target);
|
|
181
196
|
if (plan === null)
|
|
182
|
-
return this.send(target, data, size, target, writeEffect(target));
|
|
197
|
+
return this.send(target, data, size, target, writeEffect(target), 'exec');
|
|
183
198
|
const { clean, staging } = plan;
|
|
184
199
|
try {
|
|
185
|
-
|
|
200
|
+
// Every copy to the same fresh name, which only the command below puts in place.
|
|
201
|
+
await this.send(staging, data, size, clean, stagedEffect(clean), 'staged');
|
|
186
202
|
}
|
|
187
203
|
catch (error) {
|
|
188
204
|
// A 2xx whose body could not be read: the staged bytes are all there.
|
|
189
205
|
if (!landed(error)) {
|
|
206
|
+
// Removed only if a copy may have landed: a refusal, or copies that
|
|
207
|
+
// never arrived, left nothing, and a command would wake a sandbox
|
|
208
|
+
// the edge's refusal left paused.
|
|
190
209
|
if (mayHaveActed(error))
|
|
191
210
|
await this.discard(staging);
|
|
192
211
|
throw error;
|
|
@@ -212,7 +231,7 @@ export class Files {
|
|
|
212
231
|
return { path: clean, size };
|
|
213
232
|
if (result.exitCode === EXIT_TARGET_IS_LINK) {
|
|
214
233
|
// 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));
|
|
234
|
+
const info = await this.send(clean, data, size, clean, writeEffect(clean), 'exec');
|
|
216
235
|
if (mode !== null)
|
|
217
236
|
await this.chmod(clean, mode);
|
|
218
237
|
return info;
|
|
@@ -223,18 +242,23 @@ export class Files {
|
|
|
223
242
|
const denied = /Permission denied|Operation not permitted/.test(result.stderr);
|
|
224
243
|
throw new PrizedError(denied ? 'permission_denied' : 'command_failed', `Could not write ${clean}: ${commandDetail(result)}`, { sandboxId });
|
|
225
244
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
245
|
+
/**
|
|
246
|
+
* One upload to `path`, as the files API takes it; `target` is the file the
|
|
247
|
+
* caller asked for, and `effect` what the upload does to it. `exec` for an
|
|
248
|
+
* upload straight to the file: like a command, a resend after the answer
|
|
249
|
+
* was lost could overwrite what the sandbox wrote to the path since.
|
|
250
|
+
* `staged` for one to a fresh name: sent again like a read.
|
|
251
|
+
*/
|
|
252
|
+
async send(path, data, size, target, effect, retry) {
|
|
228
253
|
const result = await request(this.toolbox.config, {
|
|
229
254
|
method: 'POST',
|
|
230
255
|
url: this.url('upload', path),
|
|
231
256
|
body: data,
|
|
232
257
|
contentType: 'application/octet-stream',
|
|
233
|
-
|
|
234
|
-
// what the sandbox wrote to the path since.
|
|
235
|
-
retry: 'exec',
|
|
258
|
+
retry,
|
|
236
259
|
effect,
|
|
237
260
|
timeoutMs: FILE_TIMEOUT_MS,
|
|
261
|
+
stallMs: timing.transferStallMs,
|
|
238
262
|
deadline: this.toolbox.config.now() + FILE_TIMEOUT_MS,
|
|
239
263
|
sandboxId: this.toolbox.sandboxId,
|
|
240
264
|
});
|
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,13 @@ 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. The start of a background, streamed or long `exec` is
|
|
36
|
+
* sent again for as long, so an edge that restarts mid-start does not
|
|
37
|
+
* cost the command.
|
|
30
38
|
*/
|
|
31
39
|
pollOutageMs?: number;
|
|
32
40
|
/** Sleep seam (tests inject one that never really waits). */
|
|
@@ -48,13 +56,21 @@ export interface Config {
|
|
|
48
56
|
sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
49
57
|
now: () => number;
|
|
50
58
|
random: () => number;
|
|
59
|
+
/**
|
|
60
|
+
* Whether an upload's body goes out as a stream the SDK feeds piece by
|
|
61
|
+
* piece, so a stalled one is seen (RequestSpec.stallMs): Node's own fetch
|
|
62
|
+
* only, which asks for each piece once the last has gone into the socket.
|
|
63
|
+
* Elsewhere (another runtime, a fetch you pass) the body goes as it is.
|
|
64
|
+
*/
|
|
65
|
+
streamBodies: boolean;
|
|
51
66
|
}
|
|
52
67
|
export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
53
68
|
/**
|
|
54
|
-
* How long a streamed or long exec
|
|
55
|
-
* before it
|
|
56
|
-
*
|
|
57
|
-
*
|
|
69
|
+
* How long a wait (a streamed or long exec, a handle's wait()) keeps polling
|
|
70
|
+
* while its polls fail before it gives up (pollOutageMs), and a streamed or
|
|
71
|
+
* long exec stops its command, and how long a start is sent again: long
|
|
72
|
+
* enough to ride out a dropped connection or an edge that restarts (about
|
|
73
|
+
* 70 s, the last time it crashed). The Python SDK's DEFAULT_POLL_OUTAGE.
|
|
58
74
|
*/
|
|
59
75
|
export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
|
|
60
76
|
/** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
|
|
@@ -63,7 +79,10 @@ export declare function resolveConfig(options?: ClientOptions): Config;
|
|
|
63
79
|
/**
|
|
64
80
|
* Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
|
|
65
81
|
* API's own `toolboxBase` when it has the expected shape (https, or plain
|
|
66
|
-
* http on loopback for local development)
|
|
82
|
+
* http on loopback for local development, ending /v1/sandbox/<id>) on a host
|
|
83
|
+
* trusted beside the API's (term.prized.dev beside api.prized.dev; see
|
|
84
|
+
* trustedEdge); else the default edge, for Prized's own API only. The API key
|
|
85
|
+
* is never sent anywhere else: discovery is routing, not authority.
|
|
67
86
|
*/
|
|
68
87
|
export declare function toolboxBaseFor(config: Config, sandboxId: string, toolboxBase: string | null): string;
|
|
69
88
|
/**
|
|
@@ -88,13 +107,20 @@ export declare function warmEdge(config: Config): Promise<void>;
|
|
|
88
107
|
* Retries network failures, 429/502/503/504, and everything `exec`
|
|
89
108
|
* retries (the edge's 500 control_plane_unavailable among them). A GET
|
|
90
109
|
* retries any 500 too (the API's `internal`).
|
|
91
|
-
* - `exec`: a command, an upload
|
|
92
|
-
*
|
|
110
|
+
* - `exec`: a command, an upload, a remove or a label-selected delete:
|
|
111
|
+
* a second copy could affect later work. Retried only when it
|
|
93
112
|
* provably never arrived: a connection that never opened, or an answer
|
|
94
113
|
* saying so (the edge's rate limit, its lookup failing). A lost answer
|
|
95
114
|
* is never retried: it may already have run.
|
|
115
|
+
* - `staged`: a write's upload to a fresh name beside its target, which only
|
|
116
|
+
* a command after it puts in place (files.ts). Retried as `safe` (a lost
|
|
117
|
+
* answer, a timeout, 429/502/503/504) and `exec` both, a GET's 500 aside:
|
|
118
|
+
* every copy writes the same bytes under the same name, which nothing else
|
|
119
|
+
* writes, so a second one changes no file of the caller's. A 2xx whose body
|
|
120
|
+
* broke off is not sent again: the bytes are there. Its error says whether
|
|
121
|
+
* a copy may have reached the sandbox.
|
|
96
122
|
*/
|
|
97
|
-
export type RetryMode = 'safe' | 'exec';
|
|
123
|
+
export type RetryMode = 'safe' | 'exec' | 'staged';
|
|
98
124
|
/**
|
|
99
125
|
* Whether a fetch that threw never sent its request: the connection was
|
|
100
126
|
* refused, the name did not resolve, or connecting timed out. Anything else
|
|
@@ -123,6 +149,14 @@ export interface RequestSpec {
|
|
|
123
149
|
sandboxId?: string | null;
|
|
124
150
|
/** How to read a 2xx body. Default `json`. */
|
|
125
151
|
read?: 'json' | 'bytes' | 'none';
|
|
152
|
+
/**
|
|
153
|
+
* A file transfer's no-progress bound: an attempt that moves no bytes
|
|
154
|
+
* either way for this long (a connection dropped without a reset) ends as
|
|
155
|
+
* a timeout, sent again by `retry`'s rules while `deadline` allows. An
|
|
156
|
+
* upload's sending is watched only where its body is streamed
|
|
157
|
+
* (Config.streamBodies); its answer always is.
|
|
158
|
+
*/
|
|
159
|
+
stallMs?: number;
|
|
126
160
|
/**
|
|
127
161
|
* Told of every attempt that failed (no answer, an error answer, or a 2xx
|
|
128
162
|
* that could not be read), before it is retried or thrown. A create or
|
|
@@ -137,6 +171,12 @@ export interface RequestSpec {
|
|
|
137
171
|
* its error says so too.
|
|
138
172
|
*/
|
|
139
173
|
effect?: Effect;
|
|
174
|
+
/**
|
|
175
|
+
* An earlier copy of this keyed request may have reached the sandbox (the
|
|
176
|
+
* background start, sent again through an outage), so its error says so
|
|
177
|
+
* too.
|
|
178
|
+
*/
|
|
179
|
+
acted?: boolean;
|
|
140
180
|
}
|
|
141
181
|
export interface HttpResult {
|
|
142
182
|
status: number;
|
|
@@ -199,6 +239,25 @@ export declare class Polls {
|
|
|
199
239
|
}
|
|
200
240
|
/** Delay before retry number `attempt` (0-based): exponential with jitter. */
|
|
201
241
|
export declare function backoffMs(config: Config, attempt: number): number;
|
|
242
|
+
/**
|
|
243
|
+
* How long a file transfer may move no bytes before the attempt is given up
|
|
244
|
+
* (sdk/retry-spec.json transferStallMs; the Python SDK's TRANSFER_STALL).
|
|
245
|
+
* The edge holds a request about 20 s at most before it reads the body (its
|
|
246
|
+
* admission queue, then waking a paused sandbox), and ends a body that brings
|
|
247
|
+
* nothing for 60 s itself: kept above that, a stalled copy is over at the
|
|
248
|
+
* edge before the next one goes to the same staged name, so the two never
|
|
249
|
+
* race. Once an upload's last byte is handed over, its answer is waited for
|
|
250
|
+
* that and a second more for every uploadAnswerBytesPerSecond it sent: what
|
|
251
|
+
* the socket's buffers, or a proxy that takes a whole body before it sends it
|
|
252
|
+
* on, still hold moves unseen, and must not read as a stall (the Python SDK's
|
|
253
|
+
* UPLOAD_ANSWER_RATE). Mutable for tests only.
|
|
254
|
+
*/
|
|
255
|
+
export declare const timing: {
|
|
256
|
+
transferStallMs: number;
|
|
257
|
+
uploadAnswerBytesPerSecond: number;
|
|
258
|
+
};
|
|
259
|
+
/** A streamed upload goes out this many bytes at a time. */
|
|
260
|
+
export declare const UPLOAD_CHUNK: number;
|
|
202
261
|
/**
|
|
203
262
|
* What an error answer (not a 2xx: request() judges those) says about
|
|
204
263
|
* whether the sandbox acted: `never` for the codes that prove the request
|