@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,76 @@
|
|
|
1
|
+
import { PrizedError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* What one request does in the sandbox, in the words its errors use: `maybe`
|
|
4
|
+
* when no clear answer came back (a lost connection, a timeout, a 5xx other
|
|
5
|
+
* than the ones that prove it never arrived), `did` when a 2xx came back that
|
|
6
|
+
* could not be read (the sandbox acted on it), and `check`, how to find out
|
|
7
|
+
* which.
|
|
8
|
+
*/
|
|
9
|
+
export interface Effect {
|
|
10
|
+
readonly maybe: string;
|
|
11
|
+
readonly did: string;
|
|
12
|
+
readonly check: string;
|
|
13
|
+
/** What the sandbox does once the connection closes, said when the caller aborts after it may have been sent (withAbort). */
|
|
14
|
+
readonly onClose?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function commandEffect(): Effect;
|
|
17
|
+
export declare function writeEffect(path: string): Effect;
|
|
18
|
+
export declare function removeEffect(path: string): Effect;
|
|
19
|
+
/** For CommandHandle.kill (`signal` without its SIG prefix). */
|
|
20
|
+
export declare function signalEffect(runId: string, signal: string): Effect;
|
|
21
|
+
/** For a background start (or a streamed or long exec, which runs as one): said before what the SDK did about it (startAbandoned). A 2xx proves only that the start ran. */
|
|
22
|
+
export declare function startEffect(): Effect;
|
|
23
|
+
/** For an SDK helper sent in `exec` mode that names no effect of its own. */
|
|
24
|
+
export declare function helperEffect(): Effect;
|
|
25
|
+
/**
|
|
26
|
+
* How a request that is not sent again ended: `never` reached the sandbox,
|
|
27
|
+
* `maybe` acted (no clear answer), `did` act (a 2xx that could not be read),
|
|
28
|
+
* or null for a refusal whose own words say what happened.
|
|
29
|
+
*/
|
|
30
|
+
export type Outcome = 'never' | 'maybe' | 'did' | null;
|
|
31
|
+
/** Whether withOutcome said the sandbox may have acted on the request. */
|
|
32
|
+
export declare function mayHaveActed(error: unknown): error is PrizedError;
|
|
33
|
+
/** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
|
|
34
|
+
export declare function restate(error: PrizedError, message: string, init?: {
|
|
35
|
+
status?: number;
|
|
36
|
+
code?: string;
|
|
37
|
+
sandboxId?: string;
|
|
38
|
+
exitCode?: number;
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
}): PrizedError;
|
|
41
|
+
/** `error` (same code, status and body), its message saying what it means for `effect`. */
|
|
42
|
+
export declare function withOutcome(error: PrizedError, effect: Effect, outcome: Outcome): PrizedError;
|
|
43
|
+
/**
|
|
44
|
+
* `error` (code `aborted`) for a request with an `effect`: `never` when
|
|
45
|
+
* nothing that may have reached the sandbox was sent (the signal was
|
|
46
|
+
* aborted before an attempt, or while the SDK waited to send one again
|
|
47
|
+
* after attempts that never arrived), `did` when a 2xx status came before
|
|
48
|
+
* the abort cut its body, else `maybe`, with what the sandbox does once a
|
|
49
|
+
* connection closes. TypeScript only: the Python SDK takes no signal.
|
|
50
|
+
*/
|
|
51
|
+
export declare function withAbort(error: PrizedError, effect: Effect, outcome: 'never' | 'maybe' | 'did'): PrizedError;
|
|
52
|
+
/**
|
|
53
|
+
* A background start that may have started (`error`, from withOutcome or
|
|
54
|
+
* withAbort) and that the SDK then stopped by its run id (`stopped`: the
|
|
55
|
+
* abandon script answered exit 0, so the run can no longer start, was
|
|
56
|
+
* killed with its group, or had already ended), or could not confirm it did
|
|
57
|
+
* (no answer, or exit 3: no pid came, so the command may yet run). The
|
|
58
|
+
* Python SDK's start_abandoned.
|
|
59
|
+
*/
|
|
60
|
+
export declare function startAbandoned(error: PrizedError, runId: string, stopped: boolean): PrizedError;
|
|
61
|
+
/**
|
|
62
|
+
* A streamed or long exec the caller aborted after its run started
|
|
63
|
+
* (`error`, code `aborted`, from `sandboxId`), saying whether the SDK then
|
|
64
|
+
* stopped the run by its id (`stopped`: its KILL was sent, or the run had
|
|
65
|
+
* already ended), or could not confirm it did.
|
|
66
|
+
*/
|
|
67
|
+
export declare function runAborted(error: PrizedError, runId: string, stopped: boolean, sandboxId: string): PrizedError;
|
|
68
|
+
/**
|
|
69
|
+
* A streamed or long exec whose poll failed after its run started (`error`:
|
|
70
|
+
* a lost connection, a 5xx, an answer that could not be read, a refused
|
|
71
|
+
* key), saying whether the SDK then stopped the run by its id (`stopped`),
|
|
72
|
+
* or could not confirm it did: with the network down, the command may still
|
|
73
|
+
* be running. Same code, status and body (and `sandboxId` when it names
|
|
74
|
+
* none); `error` itself is the cause. The Python SDK's poll_failed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function pollFailed(error: PrizedError, runId: string, stopped: boolean, sandboxId: string): PrizedError;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// What an error says about a command that may have run. A command, a file
|
|
2
|
+
// write's rename, a remove or a signal (`exec` retry mode) is never sent again
|
|
3
|
+
// once it may have reached the sandbox, so its error has to say what the
|
|
4
|
+
// caller cannot see: whether it happened. A write's staged upload (`staged`)
|
|
5
|
+
// may have been sent again, and its error says whether a copy may have
|
|
6
|
+
// reached the sandbox. The Python SDK words these the same (_core.py).
|
|
7
|
+
import { defaultMessage, PrizedError } from './errors.js';
|
|
8
|
+
const NOT_SENT_AGAIN = 'It was not sent again';
|
|
9
|
+
export function commandEffect() {
|
|
10
|
+
return {
|
|
11
|
+
maybe: 'the command may or may not have run',
|
|
12
|
+
did: 'The command ran',
|
|
13
|
+
check: `${NOT_SENT_AGAIN}: check for its effects (the files it writes, the processes it starts) before you run it again.`,
|
|
14
|
+
// The sandbox runs a command under its request, in a process group of
|
|
15
|
+
// its own that it KILLs when the request ends, and the edge forwards the
|
|
16
|
+
// request under the caller's connection: a closed connection kills it.
|
|
17
|
+
onClose: 'The sandbox stops a command (and its process group) once its connection closes, so it may have run in part.',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function writeEffect(path) {
|
|
21
|
+
return {
|
|
22
|
+
maybe: `${path} may or may not have been written`,
|
|
23
|
+
did: `${path} was written`,
|
|
24
|
+
check: `${NOT_SENT_AGAIN}: read it back with files.read() to see which.`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function removeEffect(path) {
|
|
28
|
+
return {
|
|
29
|
+
maybe: `${path} may or may not have been removed`,
|
|
30
|
+
did: `The remove of ${path} ran`,
|
|
31
|
+
check: `${NOT_SENT_AGAIN}: files.exists() tells you which.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** For CommandHandle.kill (`signal` without its SIG prefix). */
|
|
35
|
+
export function signalEffect(runId, signal) {
|
|
36
|
+
return {
|
|
37
|
+
maybe: `SIG${signal} may or may not have reached ${runId}`,
|
|
38
|
+
did: `The kill of ${runId} ran`,
|
|
39
|
+
check: `${NOT_SENT_AGAIN}: status() tells you whether the command is still running.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** For a background start (or a streamed or long exec, which runs as one): said before what the SDK did about it (startAbandoned). A 2xx proves only that the start ran. */
|
|
43
|
+
export function startEffect() {
|
|
44
|
+
return { maybe: 'the command may or may not have started', did: 'The sandbox ran the start', check: '' };
|
|
45
|
+
}
|
|
46
|
+
/** For an SDK helper sent in `exec` mode that names no effect of its own. */
|
|
47
|
+
export function helperEffect() {
|
|
48
|
+
return { maybe: 'it may or may not have taken effect', did: 'It ran', check: `${NOT_SENT_AGAIN}.` };
|
|
49
|
+
}
|
|
50
|
+
const acted = new WeakSet();
|
|
51
|
+
/** Whether withOutcome said the sandbox may have acted on the request. */
|
|
52
|
+
export function mayHaveActed(error) {
|
|
53
|
+
return error instanceof PrizedError && acted.has(error);
|
|
54
|
+
}
|
|
55
|
+
/** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
|
|
56
|
+
export function restate(error, message, init = {}) {
|
|
57
|
+
return new PrizedError(init.code ?? error.code, message, {
|
|
58
|
+
status: init.status ?? error.status,
|
|
59
|
+
reason: error.reason,
|
|
60
|
+
sandboxId: error.sandboxId ?? init.sandboxId ?? null,
|
|
61
|
+
body: error.body,
|
|
62
|
+
retryAfter: error.retryAfter,
|
|
63
|
+
exitCode: init.exitCode ?? error.exitCode,
|
|
64
|
+
cause: 'cause' in init ? init.cause : error.cause,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** `error` restated with `message` (and, given, another sandbox or cause), as one after which the sandbox may have acted. */
|
|
68
|
+
function acting(error, message, init = {}) {
|
|
69
|
+
const restated = restate(error, message, init);
|
|
70
|
+
acted.add(restated);
|
|
71
|
+
return restated;
|
|
72
|
+
}
|
|
73
|
+
function trimmed(text) {
|
|
74
|
+
return text.replace(/[.\s]+$/, '');
|
|
75
|
+
}
|
|
76
|
+
/** The failure in a few words, for inside a sentence. */
|
|
77
|
+
function detail(error) {
|
|
78
|
+
const message = trimmed(error.message);
|
|
79
|
+
if (error.status === null || error.code === 'http_error' || error.code === 'bad_response')
|
|
80
|
+
return message;
|
|
81
|
+
const head = `HTTP ${error.status} ${error.code}`;
|
|
82
|
+
return message === trimmed(defaultMessage(error.code)) ? head : `${head}: ${message}`;
|
|
83
|
+
}
|
|
84
|
+
/** What follows "so" when no clear answer came: what may have happened, then (`closed`: the caller aborted) what a closed connection does, then how to check. */
|
|
85
|
+
function maybeTail(effect, closed) {
|
|
86
|
+
let tail = `${effect.maybe}.`;
|
|
87
|
+
if (closed && effect.onClose)
|
|
88
|
+
tail += ` ${effect.onClose}`;
|
|
89
|
+
if (effect.check)
|
|
90
|
+
tail += ` ${effect.check}`;
|
|
91
|
+
return tail;
|
|
92
|
+
}
|
|
93
|
+
/** `error` (same code, status and body), its message saying what it means for `effect`. */
|
|
94
|
+
export function withOutcome(error, effect, outcome) {
|
|
95
|
+
if (outcome === null)
|
|
96
|
+
return error;
|
|
97
|
+
if (outcome === 'never')
|
|
98
|
+
return restate(error, `${trimmed(error.message)}. It never reached the sandbox, so it had no effect.`);
|
|
99
|
+
if (outcome === 'did')
|
|
100
|
+
return acting(error, `${effect.did}, but the answer could not be read (${detail(error)}).`);
|
|
101
|
+
return acting(error, `No clear answer came back (${detail(error)}), so ${maybeTail(effect, false)}`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `error` (code `aborted`) for a request with an `effect`: `never` when
|
|
105
|
+
* nothing that may have reached the sandbox was sent (the signal was
|
|
106
|
+
* aborted before an attempt, or while the SDK waited to send one again
|
|
107
|
+
* after attempts that never arrived), `did` when a 2xx status came before
|
|
108
|
+
* the abort cut its body, else `maybe`, with what the sandbox does once a
|
|
109
|
+
* connection closes. TypeScript only: the Python SDK takes no signal.
|
|
110
|
+
*/
|
|
111
|
+
export function withAbort(error, effect, outcome) {
|
|
112
|
+
if (outcome !== 'maybe')
|
|
113
|
+
return withOutcome(error, effect, outcome);
|
|
114
|
+
return acting(error, `Aborted before a clear answer came, so ${maybeTail(effect, true)}`);
|
|
115
|
+
}
|
|
116
|
+
/** What the SDK did about a run it stopped by its id (`stopped`), or could not confirm it did. */
|
|
117
|
+
function stopTail(runId, stopped) {
|
|
118
|
+
return stopped
|
|
119
|
+
? ` The SDK stopped ${runId}, or it had already ended.`
|
|
120
|
+
: ` The SDK could not confirm it stopped: it may still be running as ${runId} (exec('ps -ef | grep ${runId}') finds it).`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A background start that may have started (`error`, from withOutcome or
|
|
124
|
+
* withAbort) and that the SDK then stopped by its run id (`stopped`: the
|
|
125
|
+
* abandon script answered exit 0, so the run can no longer start, was
|
|
126
|
+
* killed with its group, or had already ended), or could not confirm it did
|
|
127
|
+
* (no answer, or exit 3: no pid came, so the command may yet run). The
|
|
128
|
+
* Python SDK's start_abandoned.
|
|
129
|
+
*/
|
|
130
|
+
export function startAbandoned(error, runId, stopped) {
|
|
131
|
+
const said = error.message.trimEnd();
|
|
132
|
+
return acting(error, (/[.!?]$/.test(said) ? said : `${said}.`) + stopTail(runId, stopped));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A streamed or long exec the caller aborted after its run started
|
|
136
|
+
* (`error`, code `aborted`, from `sandboxId`), saying whether the SDK then
|
|
137
|
+
* stopped the run by its id (`stopped`: its KILL was sent, or the run had
|
|
138
|
+
* already ended), or could not confirm it did.
|
|
139
|
+
*/
|
|
140
|
+
export function runAborted(error, runId, stopped, sandboxId) {
|
|
141
|
+
return acting(error, `${trimmed(error.message)}.${stopTail(runId, stopped)}`, { sandboxId });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A streamed or long exec whose poll failed after its run started (`error`:
|
|
145
|
+
* a lost connection, a 5xx, an answer that could not be read, a refused
|
|
146
|
+
* key), saying whether the SDK then stopped the run by its id (`stopped`),
|
|
147
|
+
* or could not confirm it did: with the network down, the command may still
|
|
148
|
+
* be running. Same code, status and body (and `sandboxId` when it names
|
|
149
|
+
* none); `error` itself is the cause. The Python SDK's poll_failed.
|
|
150
|
+
*/
|
|
151
|
+
export function pollFailed(error, runId, stopped, sandboxId) {
|
|
152
|
+
// One sentence, whatever lines the poll's own error carries (a script's stderr).
|
|
153
|
+
const said = detail(error).replace(/\s+/g, ' ');
|
|
154
|
+
return acting(error, `Polling the command failed (${said}).${stopTail(runId, stopped)}`, { sandboxId, cause: error });
|
|
155
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type Toolbox } from './toolbox.js';
|
|
2
|
+
import type { ExecResult, TruncatedReason } from './types.js';
|
|
3
|
+
export declare const OUTPUT_DIR = "/home/prized/.prized/exec";
|
|
4
|
+
export declare const OUTPUT_ID_RE: RegExp;
|
|
5
|
+
/** Each stream of a fullOutput command keeps at most this many bytes: one download. */
|
|
6
|
+
export declare const FULL_OUTPUT_BYTES: number;
|
|
7
|
+
/**
|
|
8
|
+
* The variables that carry the script and the caller's command, so neither
|
|
9
|
+
* is in the argv of any process but the command's own shell: `pkill -f`
|
|
10
|
+
* finds what it finds in a plain exec. The script unsets both first.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SCRIPT_VAR = "PRIZED_EXEC_SCRIPT";
|
|
13
|
+
export declare const COMMAND_VAR = "PRIZED_EXEC_COMMAND";
|
|
14
|
+
/** What a fullOutput exec sends as its command: the script, from SCRIPT_VAR. */
|
|
15
|
+
export declare const RUN_SCRIPT = "eval \"${PRIZED_EXEC_SCRIPT:?}\"";
|
|
16
|
+
/**
|
|
17
|
+
* The script a fullOutput exec runs (RUN_SCRIPT, from SCRIPT_VAR). The
|
|
18
|
+
* caller's command, from COMMAND_VAR, runs as a plain exec runs it (`bash
|
|
19
|
+
* -c`, or `sh -c` where there is no bash, stdin from /dev/null), with the
|
|
20
|
+
* request's cwd and env.
|
|
21
|
+
*
|
|
22
|
+
* Nothing is assigned before it runs but the positional parameters, so every
|
|
23
|
+
* variable the caller's env sets reaches it unchanged, PATH included; and
|
|
24
|
+
* every step holds under an errexit the env may switch on (SHELLOPTS). As in
|
|
25
|
+
* a plain exec, the relays stop 5 s after the command ends if a process it
|
|
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. 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.
|
|
31
|
+
*/
|
|
32
|
+
export declare function captureScript(id: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* What a fullOutput command left when the script could not print it (its
|
|
35
|
+
* time limit, or a signal, ended the script): the marker line, or `gone`,
|
|
36
|
+
* with the directory removed, when there is nothing to read (it was never
|
|
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.
|
|
41
|
+
*/
|
|
42
|
+
export declare function collectScript(id: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Remove a fullOutput command's directory (keyed by the SDK's own id, safe to
|
|
45
|
+
* send again), and any an earlier one left over an hour ago, when it got no
|
|
46
|
+
* answer to remove its own.
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeOutputScript(id: string): string;
|
|
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 {
|
|
51
|
+
out: number;
|
|
52
|
+
err: number;
|
|
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;
|
|
60
|
+
/**
|
|
61
|
+
* A blocking exec that keeps the whole output (up to FULL_OUTPUT_BYTES of
|
|
62
|
+
* each stream). The result is a plain exec's, timeouts included: the caller
|
|
63
|
+
* (execCommand) turns a timed-out one into CommandTimeoutError.
|
|
64
|
+
*/
|
|
65
|
+
export declare function execFullOutput(toolbox: Toolbox, id: string, req: {
|
|
66
|
+
command: string;
|
|
67
|
+
cwd?: string | undefined;
|
|
68
|
+
env?: Record<string, string> | undefined;
|
|
69
|
+
timeoutMs: number;
|
|
70
|
+
}, signal: AbortSignal | undefined): Promise<ExecResult>;
|