@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/cjs/files.js
CHANGED
|
@@ -9,7 +9,12 @@ const http_js_1 = require("./http.js");
|
|
|
9
9
|
const outcome_js_1 = require("./outcome.js");
|
|
10
10
|
const shell_js_1 = require("./shell.js");
|
|
11
11
|
const toolbox_js_1 = require("./toolbox.js");
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The sandbox's file transfers take at most this long, retries included:
|
|
14
|
+
* the edge's own limit on one transfer. A write's upload sent again (a
|
|
15
|
+
* staged one is, after a lost answer or a 502, 503 or 504) is sent only
|
|
16
|
+
* while time is left, so a transfer that timed out is not.
|
|
17
|
+
*/
|
|
13
18
|
exports.FILE_TIMEOUT_MS = 10 * 60_000;
|
|
14
19
|
function byteSize(data) {
|
|
15
20
|
if (typeof data === 'string')
|
|
@@ -45,12 +50,16 @@ exports.EXIT_TARGET_NOT_REGULAR = 48;
|
|
|
45
50
|
const DISCARD_MS = 10_000;
|
|
46
51
|
/** A write's own commands: a rename, a chmod or an rm. */
|
|
47
52
|
const WRITE_COMMAND_MS = 60_000;
|
|
48
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* For the staged upload: the target is changed only by the command that
|
|
55
|
+
* follows it. The upload may have been sent again, so it says only what to
|
|
56
|
+
* do (langchain-prized's staged_upload_effect words it the same).
|
|
57
|
+
*/
|
|
49
58
|
function stagedEffect(path) {
|
|
50
59
|
return {
|
|
51
60
|
maybe: `${path} was not changed (its new bytes may have reached the sandbox, but were not put in place)`,
|
|
52
61
|
did: `${path} was not changed yet`,
|
|
53
|
-
check: '
|
|
62
|
+
check: 'Write it again.',
|
|
54
63
|
};
|
|
55
64
|
}
|
|
56
65
|
/** For the command that puts the staged upload in place: a 2xx proves it ran, not how it ended. */
|
|
@@ -170,9 +179,13 @@ class Files {
|
|
|
170
179
|
* A file that is replaced keeps its mode, so a script made executable
|
|
171
180
|
* stays executable; a new file gets the sandbox's default (0600 in
|
|
172
181
|
* prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
|
|
173
|
-
*
|
|
174
|
-
* lost
|
|
175
|
-
*
|
|
182
|
+
* The bytes go up under a temporary name beside the file, sent again if
|
|
183
|
+
* need be (a lost answer, a 502, 503 or 504; all copies within the
|
|
184
|
+
* transfer's 10 minutes),
|
|
185
|
+
* and one command that is never sent again puts them in place: a write
|
|
186
|
+
* whose answer to that command was lost throws, since it may have landed
|
|
187
|
+
* and the sandbox written the file since (read it back, or write again,
|
|
188
|
+
* as suits the file).
|
|
176
189
|
*/
|
|
177
190
|
async write(path, data, options = {}) {
|
|
178
191
|
const sandboxId = this.toolbox.sandboxId;
|
|
@@ -184,14 +197,18 @@ class Files {
|
|
|
184
197
|
const target = (0, shell_js_1.sandboxPath)(path);
|
|
185
198
|
const plan = stagingFor(target);
|
|
186
199
|
if (plan === null)
|
|
187
|
-
return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target));
|
|
200
|
+
return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target), 'exec');
|
|
188
201
|
const { clean, staging } = plan;
|
|
189
202
|
try {
|
|
190
|
-
|
|
203
|
+
// Every copy to the same fresh name, which only the command below puts in place.
|
|
204
|
+
await this.send(staging, data, size, clean, stagedEffect(clean), 'staged');
|
|
191
205
|
}
|
|
192
206
|
catch (error) {
|
|
193
207
|
// A 2xx whose body could not be read: the staged bytes are all there.
|
|
194
208
|
if (!landed(error)) {
|
|
209
|
+
// Removed only if a copy may have landed: a refusal, or copies that
|
|
210
|
+
// never arrived, left nothing, and a command would wake a sandbox
|
|
211
|
+
// the edge's refusal left paused.
|
|
195
212
|
if ((0, outcome_js_1.mayHaveActed)(error))
|
|
196
213
|
await this.discard(staging);
|
|
197
214
|
throw error;
|
|
@@ -217,7 +234,7 @@ class Files {
|
|
|
217
234
|
return { path: clean, size };
|
|
218
235
|
if (result.exitCode === exports.EXIT_TARGET_IS_LINK) {
|
|
219
236
|
// A link: the files API follows it or refuses it, as for any upload.
|
|
220
|
-
const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean));
|
|
237
|
+
const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean), 'exec');
|
|
221
238
|
if (mode !== null)
|
|
222
239
|
await this.chmod(clean, mode);
|
|
223
240
|
return info;
|
|
@@ -228,16 +245,20 @@ class Files {
|
|
|
228
245
|
const denied = /Permission denied|Operation not permitted/.test(result.stderr);
|
|
229
246
|
throw new errors_js_1.PrizedError(denied ? 'permission_denied' : 'command_failed', `Could not write ${clean}: ${commandDetail(result)}`, { sandboxId });
|
|
230
247
|
}
|
|
231
|
-
/**
|
|
232
|
-
|
|
248
|
+
/**
|
|
249
|
+
* One upload to `path`, as the files API takes it; `target` is the file the
|
|
250
|
+
* caller asked for, and `effect` what the upload does to it. `exec` for an
|
|
251
|
+
* upload straight to the file: like a command, a resend after the answer
|
|
252
|
+
* was lost could overwrite what the sandbox wrote to the path since.
|
|
253
|
+
* `staged` for one to a fresh name: sent again like a read.
|
|
254
|
+
*/
|
|
255
|
+
async send(path, data, size, target, effect, retry) {
|
|
233
256
|
const result = await (0, http_js_1.request)(this.toolbox.config, {
|
|
234
257
|
method: 'POST',
|
|
235
258
|
url: this.url('upload', path),
|
|
236
259
|
body: data,
|
|
237
260
|
contentType: 'application/octet-stream',
|
|
238
|
-
|
|
239
|
-
// what the sandbox wrote to the path since.
|
|
240
|
-
retry: 'exec',
|
|
261
|
+
retry,
|
|
241
262
|
effect,
|
|
242
263
|
timeoutMs: exports.FILE_TIMEOUT_MS,
|
|
243
264
|
deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
|
package/dist/cjs/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
|
package/dist/cjs/http.js
CHANGED
|
@@ -33,7 +33,15 @@ function envApiKey() {
|
|
|
33
33
|
function isLoopback(hostname) {
|
|
34
34
|
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* https anywhere, plain http only to this machine: the API key rides in
|
|
38
|
+
* every request's Authorization header. The same rule for the options and
|
|
39
|
+
* for an edge the API names.
|
|
40
|
+
*/
|
|
41
|
+
function secureEndpoint(url) {
|
|
42
|
+
return url.protocol === 'https:' || (url.protocol === 'http:' && isLoopback(url.hostname));
|
|
43
|
+
}
|
|
44
|
+
/** An https (or loopback http) origin, trailing slashes and a trailing /api/v1 dropped. */
|
|
37
45
|
function normalizeOrigin(name, raw) {
|
|
38
46
|
let url;
|
|
39
47
|
try {
|
|
@@ -45,6 +53,9 @@ function normalizeOrigin(name, raw) {
|
|
|
45
53
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
46
54
|
throw new errors_js_1.PrizedError('invalid_argument', `${name} must be an http(s) URL`);
|
|
47
55
|
}
|
|
56
|
+
if (!secureEndpoint(url)) {
|
|
57
|
+
throw new errors_js_1.PrizedError('invalid_argument', `${name} must use https (plain http only for localhost, 127.0.0.1 or [::1])`);
|
|
58
|
+
}
|
|
48
59
|
if (url.username || url.password || url.search || url.hash) {
|
|
49
60
|
throw new errors_js_1.PrizedError('invalid_argument', `${name} must not carry credentials, a query or a fragment`);
|
|
50
61
|
}
|
|
@@ -69,10 +80,10 @@ function abortableSleep(ms, signal) {
|
|
|
69
80
|
});
|
|
70
81
|
}
|
|
71
82
|
/**
|
|
72
|
-
* How long a streamed or long exec
|
|
73
|
-
* before it
|
|
74
|
-
*
|
|
75
|
-
* DEFAULT_POLL_OUTAGE.
|
|
83
|
+
* How long a wait (a streamed or long exec, a handle's wait()) keeps polling
|
|
84
|
+
* while its polls fail before it gives up (pollOutageMs), and a streamed or
|
|
85
|
+
* long exec stops its command: long enough to ride out a dropped connection
|
|
86
|
+
* or an edge that restarts. The Python SDK's DEFAULT_POLL_OUTAGE.
|
|
76
87
|
*/
|
|
77
88
|
exports.DEFAULT_POLL_OUTAGE_MS = 120_000;
|
|
78
89
|
/** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
|
|
@@ -116,20 +127,51 @@ function resolveConfig(options = {}) {
|
|
|
116
127
|
random: options.random ?? Math.random,
|
|
117
128
|
};
|
|
118
129
|
}
|
|
130
|
+
/** Prized's own domain: its API and its edges are hosts under it. */
|
|
131
|
+
const PRIZED_DOMAIN = 'prized.dev';
|
|
132
|
+
function under(host, domain) {
|
|
133
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
134
|
+
}
|
|
135
|
+
/** A URL's hostname (lowercase, as URL gives it), or '' if it does not parse. */
|
|
136
|
+
function hostOf(raw) {
|
|
137
|
+
try {
|
|
138
|
+
return new URL(raw).hostname;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Where a toolboxBase may point, given the API's host: the API host itself;
|
|
146
|
+
* prized.dev's own hosts when the API is one of them; loopback when the API
|
|
147
|
+
* is. Never a domain guessed from the API host's name (a public suffix such
|
|
148
|
+
* as co.uk would trust everyone under it). The Python SDK's _trusted_edge.
|
|
149
|
+
*/
|
|
150
|
+
function trustedEdge(edgeHost, apiHost) {
|
|
151
|
+
if (edgeHost === '' || apiHost === '')
|
|
152
|
+
return false;
|
|
153
|
+
if (isLoopback(apiHost) || isLoopback(edgeHost))
|
|
154
|
+
return isLoopback(apiHost) && isLoopback(edgeHost);
|
|
155
|
+
return edgeHost === apiHost || (under(apiHost, PRIZED_DOMAIN) && under(edgeHost, PRIZED_DOMAIN));
|
|
156
|
+
}
|
|
119
157
|
/**
|
|
120
158
|
* Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
|
|
121
159
|
* API's own `toolboxBase` when it has the expected shape (https, or plain
|
|
122
|
-
* http on loopback for local development)
|
|
160
|
+
* http on loopback for local development, ending /v1/sandbox/<id>) on a host
|
|
161
|
+
* trusted beside the API's (term.prized.dev beside api.prized.dev; see
|
|
162
|
+
* trustedEdge); else the default edge, for Prized's own API only. The API key
|
|
163
|
+
* is never sent anywhere else: discovery is routing, not authority.
|
|
123
164
|
*/
|
|
124
165
|
function toolboxBaseFor(config, sandboxId, toolboxBase) {
|
|
125
166
|
const path = `/v1/sandbox/${encodeURIComponent(sandboxId)}`;
|
|
126
167
|
if (config.edgeUrl !== null)
|
|
127
168
|
return `${config.edgeUrl}${path}`;
|
|
169
|
+
const apiHost = hostOf(config.baseUrl);
|
|
128
170
|
if (toolboxBase) {
|
|
129
171
|
try {
|
|
130
172
|
const url = new URL(toolboxBase);
|
|
131
|
-
|
|
132
|
-
|
|
173
|
+
if (secureEndpoint(url) &&
|
|
174
|
+
trustedEdge(url.hostname, apiHost) &&
|
|
133
175
|
!url.username &&
|
|
134
176
|
!url.password &&
|
|
135
177
|
!url.search &&
|
|
@@ -142,7 +184,11 @@ function toolboxBaseFor(config, sandboxId, toolboxBase) {
|
|
|
142
184
|
// fall through to the default
|
|
143
185
|
}
|
|
144
186
|
}
|
|
145
|
-
|
|
187
|
+
// Prized's own edge only for Prized's own API: a key for another
|
|
188
|
+
// deployment is never sent to it. That deployment names its edge.
|
|
189
|
+
if (under(apiHost, PRIZED_DOMAIN))
|
|
190
|
+
return `${exports.DEFAULT_EDGE_URL}${path}`;
|
|
191
|
+
throw new errors_js_1.PrizedError('edge_unknown', `The API at ${config.baseUrl} named no command endpoint this SDK trusts for ${sandboxId}; pass edgeUrl for that deployment.`, { sandboxId });
|
|
146
192
|
}
|
|
147
193
|
/**
|
|
148
194
|
* The edge this config's sandboxes answer on, before the API has named one:
|
|
@@ -239,7 +285,7 @@ function retryable(error, mode, method) {
|
|
|
239
285
|
// have done a write's work, so a write is left to its caller (a create
|
|
240
286
|
// that may have made a sandbox finds and deletes it). A GET did nothing:
|
|
241
287
|
// a poll that meets one is sent again rather than failing its create.
|
|
242
|
-
if (error.status === 500 && method === 'GET')
|
|
288
|
+
if (mode === 'safe' && error.status === 500 && method === 'GET')
|
|
243
289
|
return true;
|
|
244
290
|
return error.status === 429 || error.status === 502 || error.status === 503 || error.status === 504;
|
|
245
291
|
}
|
|
@@ -427,8 +473,9 @@ function outcomeOf(error) {
|
|
|
427
473
|
/**
|
|
428
474
|
* The error a request throws. With an `effect`, it says whether the sandbox
|
|
429
475
|
* acted on the request: by this attempt, or by an earlier one that was sent
|
|
430
|
-
* again (`acted`: only a keyed request, the background start,
|
|
431
|
-
* after
|
|
476
|
+
* again (`acted`: only a keyed request, the background start, and a staged
|
|
477
|
+
* upload are sent again after they may have arrived). `cutStatus`: the 2xx
|
|
478
|
+
* whose body broke off.
|
|
432
479
|
*/
|
|
433
480
|
function raised(failure, outcome, acted, effect, cutStatus = null) {
|
|
434
481
|
if (effect === undefined)
|
|
@@ -443,7 +490,8 @@ async function request(config, spec) {
|
|
|
443
490
|
const timeoutMs = spec.timeoutMs ?? config.requestTimeoutMs;
|
|
444
491
|
const sandboxId = spec.sandboxId ?? null;
|
|
445
492
|
const effect = spec.effect ?? (spec.retry === 'exec' ? (0, outcome_js_1.commandEffect)() : undefined);
|
|
446
|
-
|
|
493
|
+
// Whether an attempt that was sent again may have reached the sandbox.
|
|
494
|
+
let acted = false;
|
|
447
495
|
// Names the last attempt's failure, if any, so a call cut short by its
|
|
448
496
|
// deadline still says what it was waiting out (a 503, a rate limit).
|
|
449
497
|
const pastDeadline = (last) => {
|
|
@@ -451,9 +499,10 @@ async function request(config, spec) {
|
|
|
451
499
|
const tail = what === null ? '' : ` The last attempt failed with ${what}.`;
|
|
452
500
|
let error = new errors_js_1.PrizedError('timeout', `The time allowed for this call ran out.${tail}`, { sandboxId, cause: last });
|
|
453
501
|
// A command is sent again only when it provably never arrived, so every
|
|
454
|
-
// attempt of one cut short this way never reached the sandbox.
|
|
455
|
-
|
|
456
|
-
|
|
502
|
+
// attempt of one cut short this way never reached the sandbox. A staged
|
|
503
|
+
// upload's copies may have, unless each provably never arrived.
|
|
504
|
+
if (effect !== undefined && spec.retry !== 'safe')
|
|
505
|
+
error = (0, outcome_js_1.withOutcome)(error, effect, spec.retry === 'staged' && acted ? 'maybe' : 'never');
|
|
457
506
|
cutShort.add(error);
|
|
458
507
|
return error;
|
|
459
508
|
};
|
|
@@ -462,8 +511,6 @@ async function request(config, spec) {
|
|
|
462
511
|
return error;
|
|
463
512
|
};
|
|
464
513
|
let last;
|
|
465
|
-
// Whether an attempt that was sent again may have reached the sandbox.
|
|
466
|
-
let acted = false;
|
|
467
514
|
// An abort with an `effect` says whether the sandbox may have acted:
|
|
468
515
|
// before an attempt, or while waiting to send one again, nothing was sent
|
|
469
516
|
// that might have, unless an earlier attempt of a keyed request (`acted`)
|
|
@@ -500,8 +547,9 @@ async function request(config, spec) {
|
|
|
500
547
|
// The status came and the body did not: the sandbox acted on it.
|
|
501
548
|
const cut = outcome.status !== null && outcome.status >= 200 && outcome.status < 300;
|
|
502
549
|
const said = cut ? 'did' : unsentAttempt ? 'never' : 'maybe';
|
|
503
|
-
// A command
|
|
504
|
-
|
|
550
|
+
// A command is sent again only if it never left; a staged upload
|
|
551
|
+
// unless its 2xx came (its bytes are there).
|
|
552
|
+
const resend = spec.retry === 'safe' || (spec.retry === 'exec' && unsentAttempt) || (spec.retry === 'staged' && !cut);
|
|
505
553
|
if (!resend || attempt >= config.maxRetries)
|
|
506
554
|
throw raised(failure, said, acted, effect, cut ? outcome.status : null);
|
|
507
555
|
acted = acted || said !== 'never';
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export { CommandHandle, DEFAULT_EXEC_TIMEOUT_MS, MAX_BLOCKING_EXEC_MS } from './
|
|
|
4
4
|
export type { CommandStatus, ExecOptions, WaitOptions } from './commands.js';
|
|
5
5
|
export { Files } from './files.js';
|
|
6
6
|
export type { FileData, WriteOptions } from './files.js';
|
|
7
|
-
export { CommandTimeoutError, PrizedError } from './errors.js';
|
|
8
|
-
export type { PrizedErrorInit } from './errors.js';
|
|
7
|
+
export { CommandTimeoutError, OutputReadError, PrizedError } from './errors.js';
|
|
8
|
+
export type { OutputReadErrorInit, PrizedErrorInit } from './errors.js';
|
|
9
9
|
export { DEFAULT_BASE_URL, DEFAULT_EDGE_URL } from './http.js';
|
|
10
10
|
export type { ClientOptions } from './http.js';
|
|
11
|
-
export type { ExecResult, FileInfo, Resources, ResourcesInput, SandboxInfo, SandboxState, Template, WriteInfo, } from './types.js';
|
|
11
|
+
export type { ExecResult, FileInfo, Resources, ResourcesInput, SandboxInfo, SandboxState, Template, TruncatedReason, WriteInfo, } from './types.js';
|
|
12
12
|
export { VERSION } from './version.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VERSION = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = exports.PrizedError = exports.CommandTimeoutError = exports.Files = exports.MAX_BLOCKING_EXEC_MS = exports.DEFAULT_EXEC_TIMEOUT_MS = exports.CommandHandle = exports.PREVIEW_KEY_HEADER = exports.DEFAULT_TEMPLATE = exports.Sandbox = void 0;
|
|
3
|
+
exports.VERSION = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = exports.PrizedError = exports.OutputReadError = exports.CommandTimeoutError = exports.Files = exports.MAX_BLOCKING_EXEC_MS = exports.DEFAULT_EXEC_TIMEOUT_MS = exports.CommandHandle = exports.PREVIEW_KEY_HEADER = exports.DEFAULT_TEMPLATE = exports.Sandbox = void 0;
|
|
4
4
|
// Prized Sandboxes (beta) from TypeScript: the public surface.
|
|
5
5
|
var sandbox_js_1 = require("./sandbox.js");
|
|
6
6
|
Object.defineProperty(exports, "Sandbox", { enumerable: true, get: function () { return sandbox_js_1.Sandbox; } });
|
|
@@ -14,6 +14,7 @@ var files_js_1 = require("./files.js");
|
|
|
14
14
|
Object.defineProperty(exports, "Files", { enumerable: true, get: function () { return files_js_1.Files; } });
|
|
15
15
|
var errors_js_1 = require("./errors.js");
|
|
16
16
|
Object.defineProperty(exports, "CommandTimeoutError", { enumerable: true, get: function () { return errors_js_1.CommandTimeoutError; } });
|
|
17
|
+
Object.defineProperty(exports, "OutputReadError", { enumerable: true, get: function () { return errors_js_1.OutputReadError; } });
|
|
17
18
|
Object.defineProperty(exports, "PrizedError", { enumerable: true, get: function () { return errors_js_1.PrizedError; } });
|
|
18
19
|
var http_js_1 = require("./http.js");
|
|
19
20
|
Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return http_js_1.DEFAULT_BASE_URL; } });
|
package/dist/cjs/outcome.d.ts
CHANGED
|
@@ -30,11 +30,12 @@ export declare function helperEffect(): Effect;
|
|
|
30
30
|
export type Outcome = 'never' | 'maybe' | 'did' | null;
|
|
31
31
|
/** Whether withOutcome said the sandbox may have acted on the request. */
|
|
32
32
|
export declare function mayHaveActed(error: unknown): error is PrizedError;
|
|
33
|
-
/** `error` with another message (and, given, another status, code, sandbox or cause), every other field kept. */
|
|
33
|
+
/** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
|
|
34
34
|
export declare function restate(error: PrizedError, message: string, init?: {
|
|
35
35
|
status?: number;
|
|
36
36
|
code?: string;
|
|
37
37
|
sandboxId?: string;
|
|
38
|
+
exitCode?: number;
|
|
38
39
|
cause?: unknown;
|
|
39
40
|
}): PrizedError;
|
|
40
41
|
/** `error` (same code, status and body), its message saying what it means for `effect`. */
|
package/dist/cjs/outcome.js
CHANGED
|
@@ -14,9 +14,11 @@ exports.startAbandoned = startAbandoned;
|
|
|
14
14
|
exports.runAborted = runAborted;
|
|
15
15
|
exports.pollFailed = pollFailed;
|
|
16
16
|
// What an error says about a command that may have run. A command, a file
|
|
17
|
-
// write, a remove or a signal (`exec` retry mode) is never sent again
|
|
18
|
-
// may have reached the sandbox, so its error has to say what the
|
|
19
|
-
// see: whether it happened.
|
|
17
|
+
// write's rename, a remove or a signal (`exec` retry mode) is never sent again
|
|
18
|
+
// once it may have reached the sandbox, so its error has to say what the
|
|
19
|
+
// caller cannot see: whether it happened. A write's staged upload (`staged`)
|
|
20
|
+
// may have been sent again, and its error says whether a copy may have
|
|
21
|
+
// reached the sandbox. The Python SDK words these the same (_core.py).
|
|
20
22
|
const errors_js_1 = require("./errors.js");
|
|
21
23
|
const NOT_SENT_AGAIN = 'It was not sent again';
|
|
22
24
|
function commandEffect() {
|
|
@@ -65,7 +67,7 @@ const acted = new WeakSet();
|
|
|
65
67
|
function mayHaveActed(error) {
|
|
66
68
|
return error instanceof errors_js_1.PrizedError && acted.has(error);
|
|
67
69
|
}
|
|
68
|
-
/** `error` with another message (and, given, another status, code, sandbox or cause), every other field kept. */
|
|
70
|
+
/** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
|
|
69
71
|
function restate(error, message, init = {}) {
|
|
70
72
|
return new errors_js_1.PrizedError(init.code ?? error.code, message, {
|
|
71
73
|
status: init.status ?? error.status,
|
|
@@ -73,6 +75,7 @@ function restate(error, message, init = {}) {
|
|
|
73
75
|
sandboxId: error.sandboxId ?? init.sandboxId ?? null,
|
|
74
76
|
body: error.body,
|
|
75
77
|
retryAfter: error.retryAfter,
|
|
78
|
+
exitCode: init.exitCode ?? error.exitCode,
|
|
76
79
|
cause: 'cause' in init ? init.cause : error.cause,
|
|
77
80
|
});
|
|
78
81
|
}
|
package/dist/cjs/output.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Toolbox } from './toolbox.js';
|
|
2
|
-
import type { ExecResult } from './types.js';
|
|
2
|
+
import type { ExecResult, TruncatedReason } from './types.js';
|
|
3
3
|
export declare const OUTPUT_DIR = "/home/prized/.prized/exec";
|
|
4
4
|
export declare const OUTPUT_ID_RE: RegExp;
|
|
5
5
|
/** Each stream of a fullOutput command keeps at most this many bytes: one download. */
|
|
@@ -22,17 +22,22 @@ export declare const RUN_SCRIPT = "eval \"${PRIZED_EXEC_SCRIPT:?}\"";
|
|
|
22
22
|
* Nothing is assigned before it runs but the positional parameters, so every
|
|
23
23
|
* variable the caller's env sets reaches it unchanged, PATH included; and
|
|
24
24
|
* every step holds under an errexit the env may switch on (SHELLOPTS). As in
|
|
25
|
-
* a plain exec, the
|
|
25
|
+
* a plain exec, the relays stop 5 s after the command ends if a process it
|
|
26
26
|
* left running still holds its output, keeping what came through. A command
|
|
27
|
-
* a signal ends exits 128+n, as a background command does.
|
|
28
|
-
*
|
|
27
|
+
* a signal ends exits 128+n, as a background command does. A reader that
|
|
28
|
+
* could not write its file (failedLines) sends the output to the files API
|
|
29
|
+
* whatever its size, with the marker saying which. Without /bin/sh, setsid,
|
|
30
|
+
* cat or a directory to write to, the command runs as a plain exec.
|
|
29
31
|
*/
|
|
30
32
|
export declare function captureScript(id: string): string;
|
|
31
33
|
/**
|
|
32
34
|
* What a fullOutput command left when the script could not print it (its
|
|
33
35
|
* time limit, or a signal, ended the script): the marker line, or `gone`,
|
|
34
36
|
* with the directory removed, when there is nothing to read (it was never
|
|
35
|
-
* made, or its output was already printed back).
|
|
37
|
+
* made, or its output was already printed back). A failed write is read from
|
|
38
|
+
* the reader's flag, before the size, which is final once the flag is there
|
|
39
|
+
* (the script's wait for the reader's exit status went with the script); a
|
|
40
|
+
* reader killed, or one that could not make its flag, is not seen here.
|
|
36
41
|
*/
|
|
37
42
|
export declare function collectScript(id: string): string;
|
|
38
43
|
/**
|
|
@@ -41,11 +46,17 @@ export declare function collectScript(id: string): string;
|
|
|
41
46
|
* answer to remove its own.
|
|
42
47
|
*/
|
|
43
48
|
export declare function removeOutputScript(id: string): string;
|
|
44
|
-
/**
|
|
45
|
-
export
|
|
49
|
+
/** What the marker line says: each stream's byte count (one cut at FULL_OUTPUT_BYTES reads more), and whether its reader failed to write it. */
|
|
50
|
+
export interface Marker {
|
|
46
51
|
out: number;
|
|
47
52
|
err: number;
|
|
48
|
-
|
|
53
|
+
outFailed: boolean;
|
|
54
|
+
errFailed: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** The marker (one without failure flags, as an earlier SDK's collect printed, has none), `gone`, or null for no marker. */
|
|
57
|
+
export declare function parseMarker(stdout: string, id: string): Marker | 'gone' | null;
|
|
58
|
+
/** Why the read-back output is cut, if it is: a failed write first, since it is the one the caller cannot foresee. */
|
|
59
|
+
export declare function markerTruncation(marker: Marker): TruncatedReason | null;
|
|
49
60
|
/**
|
|
50
61
|
* A blocking exec that keeps the whole output (up to FULL_OUTPUT_BYTES of
|
|
51
62
|
* each stream). The result is a plain exec's, timeouts included: the caller
|