withruntime 0.3.0 → 0.4.0
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/E2B.md +146 -0
- package/README.md +41 -5
- package/dist/cli.js +43 -7
- package/dist/client.d.ts +5 -0
- package/dist/e2b/client.d.ts +58 -0
- package/dist/e2b/client.js +66 -0
- package/dist/e2b/code-interpreter.d.ts +144 -0
- package/dist/e2b/code-interpreter.js +244 -0
- package/dist/e2b/commands.d.ts +100 -0
- package/dist/e2b/commands.js +268 -0
- package/dist/e2b/errors.d.ts +72 -0
- package/dist/e2b/errors.js +151 -0
- package/dist/e2b/filesystem.d.ts +82 -0
- package/dist/e2b/filesystem.js +143 -0
- package/dist/e2b/index.d.ts +28 -0
- package/dist/e2b/index.js +61 -0
- package/dist/e2b/sandbox.d.ts +183 -0
- package/dist/e2b/sandbox.js +458 -0
- package/dist/e2b/unsupported.d.ts +19 -0
- package/dist/e2b/unsupported.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/login.js +13 -4
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +4 -3
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -1
- package/dist/products/limits.d.ts +25 -0
- package/dist/products/limits.js +10 -0
- package/dist/products/previews.d.ts +3 -0
- package/dist/proxy.d.ts +30 -0
- package/dist/proxy.js +178 -0
- package/dist/sandbox.d.ts +4 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +46 -13
- package/package.json +16 -3
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { guard, NotSupportedError, SandboxError, TimeoutError } from "./errors.js";
|
|
2
|
+
import { bind } from "./index.js";
|
|
3
|
+
import { Sandbox as BaseSandbox } from "./sandbox.js";
|
|
4
|
+
export * from "./index.js";
|
|
5
|
+
/** A line of output, as E2B's callbacks receive it. */
|
|
6
|
+
export class OutputMessage {
|
|
7
|
+
line;
|
|
8
|
+
timestamp;
|
|
9
|
+
error;
|
|
10
|
+
constructor(line,
|
|
11
|
+
/** Unix epoch in nanoseconds (from the client's clock). */
|
|
12
|
+
timestamp, error) {
|
|
13
|
+
this.line = line;
|
|
14
|
+
this.timestamp = timestamp;
|
|
15
|
+
this.error = error;
|
|
16
|
+
}
|
|
17
|
+
toString() {
|
|
18
|
+
return this.line;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class ExecutionError {
|
|
22
|
+
name;
|
|
23
|
+
value;
|
|
24
|
+
traceback;
|
|
25
|
+
constructor(name, value, traceback) {
|
|
26
|
+
this.name = name;
|
|
27
|
+
this.value = value;
|
|
28
|
+
this.traceback = traceback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const FORMATS = {
|
|
32
|
+
"text/plain": "text",
|
|
33
|
+
"text/html": "html",
|
|
34
|
+
"text/markdown": "markdown",
|
|
35
|
+
"image/svg+xml": "svg",
|
|
36
|
+
"image/png": "png",
|
|
37
|
+
"image/jpeg": "jpeg",
|
|
38
|
+
"application/pdf": "pdf",
|
|
39
|
+
"text/latex": "latex",
|
|
40
|
+
"application/json": "json",
|
|
41
|
+
"application/javascript": "javascript",
|
|
42
|
+
};
|
|
43
|
+
/** One rich result, as E2B's: text, html, png (base64) and the rest. `chart`
|
|
44
|
+
* and E2B's `data` (its own DataFrame format) are never set on Runtime; a
|
|
45
|
+
* DataFrame arrives in `extra` under application/vnd.runtime.table+json. */
|
|
46
|
+
export class Result {
|
|
47
|
+
isMainResult;
|
|
48
|
+
text;
|
|
49
|
+
html;
|
|
50
|
+
markdown;
|
|
51
|
+
svg;
|
|
52
|
+
png;
|
|
53
|
+
jpeg;
|
|
54
|
+
pdf;
|
|
55
|
+
latex;
|
|
56
|
+
json;
|
|
57
|
+
javascript;
|
|
58
|
+
data;
|
|
59
|
+
chart;
|
|
60
|
+
extra;
|
|
61
|
+
raw;
|
|
62
|
+
constructor(rawData, isMainResult) {
|
|
63
|
+
this.raw = rawData;
|
|
64
|
+
this.isMainResult = isMainResult;
|
|
65
|
+
const extra = {};
|
|
66
|
+
const fields = {};
|
|
67
|
+
for (const [mime, value] of Object.entries(rawData)) {
|
|
68
|
+
const field = FORMATS[mime];
|
|
69
|
+
if (field)
|
|
70
|
+
fields[field] = value;
|
|
71
|
+
else
|
|
72
|
+
extra[mime] = value;
|
|
73
|
+
}
|
|
74
|
+
Object.assign(this, fields);
|
|
75
|
+
if (Object.keys(extra).length)
|
|
76
|
+
this.extra = extra;
|
|
77
|
+
}
|
|
78
|
+
formats() {
|
|
79
|
+
return [
|
|
80
|
+
...Object.values(FORMATS).filter((field) => this[field] !== undefined),
|
|
81
|
+
...Object.keys(this.extra ?? {}),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
toJSON() {
|
|
85
|
+
return {
|
|
86
|
+
text: this.text,
|
|
87
|
+
html: this.html,
|
|
88
|
+
markdown: this.markdown,
|
|
89
|
+
svg: this.svg,
|
|
90
|
+
png: this.png,
|
|
91
|
+
jpeg: this.jpeg,
|
|
92
|
+
pdf: this.pdf,
|
|
93
|
+
latex: this.latex,
|
|
94
|
+
json: this.json,
|
|
95
|
+
javascript: this.javascript,
|
|
96
|
+
...(this.extra ? { extra: this.extra } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export class Execution {
|
|
101
|
+
results;
|
|
102
|
+
logs;
|
|
103
|
+
error;
|
|
104
|
+
executionCount;
|
|
105
|
+
constructor(results = [], logs = { stdout: [], stderr: [] }, error, executionCount) {
|
|
106
|
+
this.results = results;
|
|
107
|
+
this.logs = logs;
|
|
108
|
+
this.error = error;
|
|
109
|
+
this.executionCount = executionCount;
|
|
110
|
+
}
|
|
111
|
+
/** The main result's text. */
|
|
112
|
+
get text() {
|
|
113
|
+
return this.results.find((result) => result.isMainResult)?.text;
|
|
114
|
+
}
|
|
115
|
+
toJSON() {
|
|
116
|
+
return { results: this.results, logs: this.logs, error: this.error };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const TEXT_MIME = /^text\/|json|javascript|svg|latex/;
|
|
120
|
+
/** Output as E2B's logs: one entry per line, each keeping its newline. */
|
|
121
|
+
function lines(text) {
|
|
122
|
+
return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
123
|
+
}
|
|
124
|
+
function language(requested) {
|
|
125
|
+
if (requested === undefined || requested === "python")
|
|
126
|
+
return "python";
|
|
127
|
+
if (requested === "javascript" || requested === "js")
|
|
128
|
+
return "javascript";
|
|
129
|
+
throw new NotSupportedError(`Running ${requested} code in the interpreter`, requested === "bash"
|
|
130
|
+
? "Use sandbox.commands.run(code)."
|
|
131
|
+
: "Runtime's interpreter runs Python and JavaScript; install the language and run it with sandbox.commands.run(...).");
|
|
132
|
+
}
|
|
133
|
+
/** The code interpreter sandbox: everything the base Sandbox has, plus runCode
|
|
134
|
+
* and code contexts. */
|
|
135
|
+
export class Sandbox extends BaseSandbox {
|
|
136
|
+
static defaultTemplate = "code-interpreter-v1";
|
|
137
|
+
/** Contexts made to carry the sandbox's envs, one per language. */
|
|
138
|
+
#envContexts = new Map();
|
|
139
|
+
get #interpreter() {
|
|
140
|
+
return this.runtime.interpreter;
|
|
141
|
+
}
|
|
142
|
+
/** The context a run goes to: the one asked for; else, when the sandbox has
|
|
143
|
+
* envs, a context made once with them; else Runtime's default one. */
|
|
144
|
+
async #contextFor(lang, context) {
|
|
145
|
+
if (context)
|
|
146
|
+
return context.id;
|
|
147
|
+
if (!Object.keys(this.envs).length)
|
|
148
|
+
return undefined;
|
|
149
|
+
let id = this.#envContexts.get(lang);
|
|
150
|
+
if (!id) {
|
|
151
|
+
id = guard("sandbox", async () => {
|
|
152
|
+
const made = await this.#interpreter.contexts.create({
|
|
153
|
+
id: `e2b-${lang}`,
|
|
154
|
+
language: lang,
|
|
155
|
+
env: this.envs,
|
|
156
|
+
});
|
|
157
|
+
return made.id;
|
|
158
|
+
}).catch(async (error) => {
|
|
159
|
+
// Made already, by another object connected to this sandbox.
|
|
160
|
+
const existing = (await this.#interpreter.contexts.list()).find((one) => one.id === `e2b-${lang}`);
|
|
161
|
+
if (existing)
|
|
162
|
+
return existing.id;
|
|
163
|
+
throw error;
|
|
164
|
+
});
|
|
165
|
+
this.#envContexts.set(lang, id);
|
|
166
|
+
}
|
|
167
|
+
return id;
|
|
168
|
+
}
|
|
169
|
+
async #inline(result) {
|
|
170
|
+
const data = { ...result.data };
|
|
171
|
+
for (const [mime, ref] of Object.entries(result.refs ?? {})) {
|
|
172
|
+
if (data[mime] !== undefined)
|
|
173
|
+
continue;
|
|
174
|
+
const bytes = await guard("sandbox", () => this.#interpreter.result(ref));
|
|
175
|
+
data[mime] = TEXT_MIME.test(mime)
|
|
176
|
+
? new TextDecoder().decode(bytes)
|
|
177
|
+
: Buffer.from(bytes).toString("base64");
|
|
178
|
+
}
|
|
179
|
+
return data;
|
|
180
|
+
}
|
|
181
|
+
/** Runs code in a stateful context. Errors in the code come back in
|
|
182
|
+
* `execution.error`, as in E2B; a run past `timeoutMs` throws TimeoutError. */
|
|
183
|
+
async runCode(code, opts = {}) {
|
|
184
|
+
if (opts.language !== undefined && opts.context !== undefined)
|
|
185
|
+
throw new SandboxError("Pass language or context, not both.");
|
|
186
|
+
if (opts.envs && Object.keys(opts.envs).length)
|
|
187
|
+
throw new NotSupportedError("Environment variables for one run (runCode envs)", "Make a context with them: sandbox.runtime.interpreter.contexts.create({ env }), then runCode(code, { context: { id } }).");
|
|
188
|
+
const lang = language(opts.context?.language ?? opts.language);
|
|
189
|
+
const context = await this.#contextFor(lang, opts.context);
|
|
190
|
+
const now = () => Date.now() * 1_000_000;
|
|
191
|
+
const streaming = opts.onStdout || opts.onStderr || opts.onResult || opts.onError;
|
|
192
|
+
const execution = await guard("sandbox", () => this.#interpreter.run(code, {
|
|
193
|
+
...(context ? { context } : { language: lang }),
|
|
194
|
+
timeoutMs: opts.timeoutMs ?? 60_000,
|
|
195
|
+
...(streaming
|
|
196
|
+
? {
|
|
197
|
+
onStdout: (text) => void opts.onStdout?.(new OutputMessage(text, now(), false)),
|
|
198
|
+
onStderr: (text) => void opts.onStderr?.(new OutputMessage(text, now(), true)),
|
|
199
|
+
onError: (error) => void opts.onError?.(new ExecutionError(error.name, error.value, error.traceback)),
|
|
200
|
+
}
|
|
201
|
+
: {}),
|
|
202
|
+
}));
|
|
203
|
+
if (execution.status === "timeout")
|
|
204
|
+
throw new TimeoutError(`Execution timed out after ${opts.timeoutMs ?? 60_000} ms: pass a larger 'timeoutMs'.`);
|
|
205
|
+
if (execution.status === "lost")
|
|
206
|
+
throw new SandboxError("The interpreter lost this run (its context stopped); run it again.");
|
|
207
|
+
const results = await Promise.all(execution.results.map(async (result) => new Result(await this.#inline(result), result.main)));
|
|
208
|
+
if (opts.onResult)
|
|
209
|
+
for (const result of results)
|
|
210
|
+
await opts.onResult(result);
|
|
211
|
+
return new Execution(results, { stdout: lines(execution.stdout), stderr: lines(execution.stderr) }, execution.error
|
|
212
|
+
? new ExecutionError(execution.error.name, execution.error.value, execution.error.traceback)
|
|
213
|
+
: undefined, execution.executionCount ?? undefined);
|
|
214
|
+
}
|
|
215
|
+
async createCodeContext(opts = {}) {
|
|
216
|
+
const lang = language(opts.language);
|
|
217
|
+
const made = await guard("sandbox", () => this.#interpreter.contexts.create({
|
|
218
|
+
language: lang,
|
|
219
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
220
|
+
...(Object.keys(this.envs).length ? { env: this.envs } : {}),
|
|
221
|
+
}));
|
|
222
|
+
return { id: made.id, language: made.language, cwd: made.cwd };
|
|
223
|
+
}
|
|
224
|
+
async removeCodeContext(context) {
|
|
225
|
+
const id = typeof context === "string" ? context : context.id;
|
|
226
|
+
await guard("sandbox", () => this.#interpreter.contexts.remove(id));
|
|
227
|
+
}
|
|
228
|
+
async listCodeContexts() {
|
|
229
|
+
const contexts = await guard("sandbox", () => this.#interpreter.contexts.list());
|
|
230
|
+
return contexts.map((one) => ({ id: one.id, language: one.language, cwd: one.cwd }));
|
|
231
|
+
}
|
|
232
|
+
async restartCodeContext(context) {
|
|
233
|
+
const id = typeof context === "string" ? context : context.id;
|
|
234
|
+
await guard("sandbox", () => this.#interpreter.contexts.restart(id));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export default Sandbox;
|
|
238
|
+
/** E2B's client with bound options, for the code interpreter. */
|
|
239
|
+
export class E2B {
|
|
240
|
+
Sandbox;
|
|
241
|
+
constructor(opts = {}) {
|
|
242
|
+
this.Sandbox = bind(Sandbox, opts);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Process } from "../sandbox.js";
|
|
2
|
+
import type { RuntimeSandbox } from "./client.js";
|
|
3
|
+
import { type CommandResult } from "./errors.js";
|
|
4
|
+
export type { CommandResult };
|
|
5
|
+
export type Username = "user" | "root" | (string & {});
|
|
6
|
+
export interface CommandRequestOpts {
|
|
7
|
+
requestTimeoutMs?: number;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface CommandStartOpts extends CommandRequestOpts {
|
|
11
|
+
background?: boolean;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Only the default user. Runtime runs commands as `runtime`, the sandbox's
|
|
14
|
+
* owner, with passwordless sudo; E2B's `root` is refused rather than
|
|
15
|
+
* silently run as someone else. */
|
|
16
|
+
user?: Username;
|
|
17
|
+
envs?: Record<string, string>;
|
|
18
|
+
onStdout?: (data: string) => void | Promise<void>;
|
|
19
|
+
onStderr?: (data: string) => void | Promise<void>;
|
|
20
|
+
stdin?: boolean;
|
|
21
|
+
/** Default 60 000, as in E2B. 0 is no limit (Runtime's longest, 24 hours). */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export type CommandConnectOpts = Pick<CommandStartOpts, "onStderr" | "onStdout" | "timeoutMs"> & CommandRequestOpts;
|
|
25
|
+
/** A command as E2B lists it. `pid` is a number derived from Runtime's process
|
|
26
|
+
* id (stable for the life of the process), not the Linux pid. */
|
|
27
|
+
export interface ProcessInfo {
|
|
28
|
+
pid: number;
|
|
29
|
+
tag?: string;
|
|
30
|
+
cmd: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
envs: Record<string, string>;
|
|
33
|
+
cwd?: string;
|
|
34
|
+
}
|
|
35
|
+
/** The shared state a sandbox's modules need: the Runtime sandbox, the
|
|
36
|
+
* environment given at create, and the one-time /home/user link. */
|
|
37
|
+
export interface SandboxContext {
|
|
38
|
+
readonly runtime: RuntimeSandbox;
|
|
39
|
+
readonly envs: Record<string, string>;
|
|
40
|
+
/** Makes /home/user (E2B's home) lead to /workspace (Runtime's), once, when
|
|
41
|
+
* something names it. */
|
|
42
|
+
ensureHome(text: string | undefined): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/** E2B's pids are numbers; Runtime's process ids are strings. The number is a
|
|
45
|
+
* 31-bit FNV-1a hash of the id, so every client derives the same one. */
|
|
46
|
+
export declare function pidOf(processId: string): number;
|
|
47
|
+
/** `sandbox.commands`: E2B's command module over Runtime's exec and processes. */
|
|
48
|
+
export declare class Commands {
|
|
49
|
+
#private;
|
|
50
|
+
constructor(ctx: SandboxContext);
|
|
51
|
+
/** Runs a command. In the foreground it resolves with the result and throws
|
|
52
|
+
* CommandExitError on a non-zero exit and TimeoutError past `timeoutMs`; with
|
|
53
|
+
* `background: true` it resolves at once with a CommandHandle. */
|
|
54
|
+
run(cmd: string, opts?: CommandStartOpts & {
|
|
55
|
+
background?: false;
|
|
56
|
+
}): Promise<CommandResult>;
|
|
57
|
+
run(cmd: string, opts: CommandStartOpts & {
|
|
58
|
+
background: true;
|
|
59
|
+
}): Promise<CommandHandle>;
|
|
60
|
+
run(cmd: string, opts?: CommandStartOpts & {
|
|
61
|
+
background?: boolean;
|
|
62
|
+
}): Promise<CommandHandle | CommandResult>;
|
|
63
|
+
/** Running commands. */
|
|
64
|
+
list(_opts?: CommandRequestOpts): Promise<ProcessInfo[]>;
|
|
65
|
+
/** Kills a command with SIGKILL, as E2B does. False when there is none. */
|
|
66
|
+
kill(pid: number, _opts?: CommandRequestOpts): Promise<boolean>;
|
|
67
|
+
/** Sends input to a command started with `stdin: true`. */
|
|
68
|
+
sendStdin(pid: number, data: string | Uint8Array, _opts?: CommandRequestOpts): Promise<void>;
|
|
69
|
+
closeStdin(pid: number, _opts?: CommandRequestOpts): Promise<void>;
|
|
70
|
+
/** Attaches to a running command; output from now on reaches the handle. */
|
|
71
|
+
connect(pid: number, opts?: CommandConnectOpts): Promise<CommandHandle>;
|
|
72
|
+
}
|
|
73
|
+
/** A command started in the background: E2B's CommandHandle over a Runtime
|
|
74
|
+
* process. Output streams into `stdout`/`stderr` and the callbacks as it
|
|
75
|
+
* arrives. */
|
|
76
|
+
export declare class CommandHandle {
|
|
77
|
+
#private;
|
|
78
|
+
readonly pid: number;
|
|
79
|
+
constructor(process: Process, options: {
|
|
80
|
+
stdin: boolean;
|
|
81
|
+
timeoutMs: number;
|
|
82
|
+
cursor?: number;
|
|
83
|
+
onStdout?: (data: string) => void | Promise<void>;
|
|
84
|
+
onStderr?: (data: string) => void | Promise<void>;
|
|
85
|
+
});
|
|
86
|
+
/** The exit code, or undefined while the command runs. */
|
|
87
|
+
get exitCode(): number | undefined;
|
|
88
|
+
get error(): string | undefined;
|
|
89
|
+
get stdout(): string;
|
|
90
|
+
get stderr(): string;
|
|
91
|
+
/** Waits for the command to end. Throws CommandExitError on a non-zero exit,
|
|
92
|
+
* as E2B does. */
|
|
93
|
+
wait(): Promise<CommandResult>;
|
|
94
|
+
/** Stops receiving output; the command keeps running. */
|
|
95
|
+
disconnect(): Promise<void>;
|
|
96
|
+
/** Kills the command with SIGKILL. False when it had already ended. */
|
|
97
|
+
kill(): Promise<boolean>;
|
|
98
|
+
sendStdin(data: string | Uint8Array, _opts?: CommandRequestOpts): Promise<void>;
|
|
99
|
+
closeStdin(_opts?: CommandRequestOpts): Promise<void>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { request } from "./client.js";
|
|
2
|
+
import { CommandExitError, guard, InvalidArgumentError, NotSupportedError, SandboxError, TimeoutError, translate, } from "./errors.js";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
4
|
+
const LONGEST_MS = 86_400_000;
|
|
5
|
+
/** E2B's pids are numbers; Runtime's process ids are strings. The number is a
|
|
6
|
+
* 31-bit FNV-1a hash of the id, so every client derives the same one. */
|
|
7
|
+
export function pidOf(processId) {
|
|
8
|
+
let hash = 0x811c9dc5;
|
|
9
|
+
for (let i = 0; i < processId.length; i++) {
|
|
10
|
+
hash ^= processId.charCodeAt(i);
|
|
11
|
+
hash = Math.imul(hash, 0x01000193);
|
|
12
|
+
}
|
|
13
|
+
return hash >>> 1 || 1;
|
|
14
|
+
}
|
|
15
|
+
function refuseUser(user) {
|
|
16
|
+
if (user !== undefined && user !== "user")
|
|
17
|
+
throw new NotSupportedError(`Running as the user "${user}"`, 'Runtime runs commands as its sandbox owner, with passwordless sudo: prefix the command with "sudo" to run it as root.');
|
|
18
|
+
}
|
|
19
|
+
function timeoutFor(timeoutMs) {
|
|
20
|
+
if (timeoutMs === undefined)
|
|
21
|
+
return DEFAULT_TIMEOUT_MS;
|
|
22
|
+
if (timeoutMs === 0)
|
|
23
|
+
return LONGEST_MS;
|
|
24
|
+
return Math.min(timeoutMs, LONGEST_MS);
|
|
25
|
+
}
|
|
26
|
+
function timedOut(timeoutMs) {
|
|
27
|
+
return new TimeoutError(`Command timed out after ${timeoutMs} ms: this error is likely due to exceeding 'timeoutMs'. ` +
|
|
28
|
+
"Pass a larger 'timeoutMs', or 0 for no limit.");
|
|
29
|
+
}
|
|
30
|
+
/** A Runtime result as E2B's: a command killed by a signal has no exit code on
|
|
31
|
+
* Runtime; E2B reports -1. */
|
|
32
|
+
function toResult(result) {
|
|
33
|
+
const exitCode = result.exitCode ?? -1;
|
|
34
|
+
return {
|
|
35
|
+
exitCode,
|
|
36
|
+
stdout: result.stdout,
|
|
37
|
+
stderr: result.stderr,
|
|
38
|
+
...(exitCode === 0
|
|
39
|
+
? {}
|
|
40
|
+
: {
|
|
41
|
+
error: result.exitCode === null ? "terminated by a signal" : `exit status ${exitCode}`,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function settle(result, timeoutMs) {
|
|
46
|
+
if (result.timedOut)
|
|
47
|
+
throw timedOut(timeoutMs);
|
|
48
|
+
const out = toResult(result);
|
|
49
|
+
if (out.exitCode !== 0)
|
|
50
|
+
throw new CommandExitError(out);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** `sandbox.commands`: E2B's command module over Runtime's exec and processes. */
|
|
54
|
+
export class Commands {
|
|
55
|
+
#ctx;
|
|
56
|
+
constructor(ctx) {
|
|
57
|
+
this.#ctx = ctx;
|
|
58
|
+
}
|
|
59
|
+
#env(envs) {
|
|
60
|
+
const merged = { ...this.#ctx.envs, ...envs };
|
|
61
|
+
return Object.keys(merged).length ? { env: merged } : {};
|
|
62
|
+
}
|
|
63
|
+
async run(cmd, opts = {}) {
|
|
64
|
+
refuseUser(opts.user);
|
|
65
|
+
await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`);
|
|
66
|
+
const timeoutMs = timeoutFor(opts.timeoutMs);
|
|
67
|
+
if (opts.background || opts.stdin) {
|
|
68
|
+
const handle = await this.#start(cmd, opts, timeoutMs);
|
|
69
|
+
return opts.background ? handle : handle.wait();
|
|
70
|
+
}
|
|
71
|
+
const result = await guard("sandbox", () => this.#ctx.runtime.exec(cmd, {
|
|
72
|
+
...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
|
|
73
|
+
...this.#env(opts.envs),
|
|
74
|
+
timeoutMs,
|
|
75
|
+
...(opts.onStdout ? { onStdout: (text) => void opts.onStdout(text) } : {}),
|
|
76
|
+
...(opts.onStderr ? { onStderr: (text) => void opts.onStderr(text) } : {}),
|
|
77
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
78
|
+
}));
|
|
79
|
+
return settle(result, timeoutMs);
|
|
80
|
+
}
|
|
81
|
+
async #start(cmd, opts, timeoutMs) {
|
|
82
|
+
const process = await guard("sandbox", () => this.#ctx.runtime.spawn(cmd, {
|
|
83
|
+
...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
|
|
84
|
+
...this.#env(opts.envs),
|
|
85
|
+
timeoutMs,
|
|
86
|
+
...(opts.stdin ? { stdin: "pipe" } : {}),
|
|
87
|
+
...request(opts),
|
|
88
|
+
}));
|
|
89
|
+
return new CommandHandle(process, {
|
|
90
|
+
stdin: opts.stdin === true,
|
|
91
|
+
timeoutMs,
|
|
92
|
+
...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
|
|
93
|
+
...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Running commands. */
|
|
97
|
+
async list(_opts = {}) {
|
|
98
|
+
const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list());
|
|
99
|
+
return processes.filter((info) => info.state === "running").map(describe);
|
|
100
|
+
}
|
|
101
|
+
async #find(pid) {
|
|
102
|
+
const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list());
|
|
103
|
+
return processes.find((info) => pidOf(info.id) === pid && info.state === "running");
|
|
104
|
+
}
|
|
105
|
+
async #process(pid) {
|
|
106
|
+
const info = await this.#find(pid);
|
|
107
|
+
if (!info)
|
|
108
|
+
throw new SandboxError(`No running command with pid ${pid}.`);
|
|
109
|
+
return guard("sandbox", () => this.#ctx.runtime.processes.get(info.id));
|
|
110
|
+
}
|
|
111
|
+
/** Kills a command with SIGKILL, as E2B does. False when there is none. */
|
|
112
|
+
async kill(pid, _opts = {}) {
|
|
113
|
+
const info = await this.#find(pid);
|
|
114
|
+
if (!info)
|
|
115
|
+
return false;
|
|
116
|
+
const process = await guard("sandbox", () => this.#ctx.runtime.processes.get(info.id));
|
|
117
|
+
await guard("sandbox", () => process.kill("SIGKILL"));
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
/** Sends input to a command started with `stdin: true`. */
|
|
121
|
+
async sendStdin(pid, data, _opts = {}) {
|
|
122
|
+
const process = await this.#process(pid);
|
|
123
|
+
if (!process.info.stdinOpen)
|
|
124
|
+
throw new InvalidArgumentError(`The command with pid ${pid} was not started with stdin: true, so its input is closed.`);
|
|
125
|
+
await guard("sandbox", () => process.write(data));
|
|
126
|
+
}
|
|
127
|
+
async closeStdin(pid, _opts = {}) {
|
|
128
|
+
const process = await this.#process(pid);
|
|
129
|
+
await guard("sandbox", () => process.write("", { eof: true }));
|
|
130
|
+
}
|
|
131
|
+
/** Attaches to a running command; output from now on reaches the handle. */
|
|
132
|
+
async connect(pid, opts = {}) {
|
|
133
|
+
const process = await this.#process(pid);
|
|
134
|
+
return new CommandHandle(process, {
|
|
135
|
+
stdin: process.info.stdinOpen,
|
|
136
|
+
timeoutMs: timeoutFor(opts.timeoutMs),
|
|
137
|
+
cursor: process.info.outputBytes,
|
|
138
|
+
...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
|
|
139
|
+
...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function describe(info) {
|
|
144
|
+
return {
|
|
145
|
+
pid: pidOf(info.id),
|
|
146
|
+
cmd: "/bin/bash",
|
|
147
|
+
args: ["-c", info.command],
|
|
148
|
+
envs: {},
|
|
149
|
+
cwd: info.cwd,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** A command started in the background: E2B's CommandHandle over a Runtime
|
|
153
|
+
* process. Output streams into `stdout`/`stderr` and the callbacks as it
|
|
154
|
+
* arrives. */
|
|
155
|
+
export class CommandHandle {
|
|
156
|
+
pid;
|
|
157
|
+
#process;
|
|
158
|
+
#stdin;
|
|
159
|
+
#timeoutMs;
|
|
160
|
+
#abort = new AbortController();
|
|
161
|
+
#done;
|
|
162
|
+
#stdout = "";
|
|
163
|
+
#stderr = "";
|
|
164
|
+
#exit;
|
|
165
|
+
#failure;
|
|
166
|
+
#disconnected = false;
|
|
167
|
+
constructor(process, options) {
|
|
168
|
+
this.#process = process;
|
|
169
|
+
this.pid = pidOf(process.id);
|
|
170
|
+
this.#stdin = options.stdin;
|
|
171
|
+
this.#timeoutMs = options.timeoutMs;
|
|
172
|
+
this.#done = this.#follow(options);
|
|
173
|
+
}
|
|
174
|
+
async #follow(options) {
|
|
175
|
+
try {
|
|
176
|
+
for await (const event of this.#process.output({
|
|
177
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
178
|
+
signal: this.#abort.signal,
|
|
179
|
+
})) {
|
|
180
|
+
if (event.type === "stdout") {
|
|
181
|
+
this.#stdout += event.data;
|
|
182
|
+
await options.onStdout?.(event.data);
|
|
183
|
+
}
|
|
184
|
+
else if (event.type === "stderr") {
|
|
185
|
+
this.#stderr += event.data;
|
|
186
|
+
await options.onStderr?.(event.data);
|
|
187
|
+
}
|
|
188
|
+
else if (event.type === "exit") {
|
|
189
|
+
this.#exit = { exitCode: event.exitCode, timedOut: event.timedOut };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (!this.#disconnected) {
|
|
195
|
+
const translated = translate(error, "sandbox");
|
|
196
|
+
this.#failure =
|
|
197
|
+
translated instanceof Error ? translated : new SandboxError(String(translated));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** The exit code, or undefined while the command runs. */
|
|
202
|
+
get exitCode() {
|
|
203
|
+
return this.#exit ? (this.#exit.exitCode ?? -1) : undefined;
|
|
204
|
+
}
|
|
205
|
+
get error() {
|
|
206
|
+
return this.#exit ? this.#result().error : undefined;
|
|
207
|
+
}
|
|
208
|
+
get stdout() {
|
|
209
|
+
return this.#stdout;
|
|
210
|
+
}
|
|
211
|
+
get stderr() {
|
|
212
|
+
return this.#stderr;
|
|
213
|
+
}
|
|
214
|
+
#result() {
|
|
215
|
+
return toResult({
|
|
216
|
+
exitCode: this.#exit?.exitCode ?? null,
|
|
217
|
+
stdout: this.#stdout,
|
|
218
|
+
stderr: this.#stderr,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Waits for the command to end. Throws CommandExitError on a non-zero exit,
|
|
222
|
+
* as E2B does. */
|
|
223
|
+
async wait() {
|
|
224
|
+
await this.#done;
|
|
225
|
+
if (this.#failure)
|
|
226
|
+
throw this.#failure;
|
|
227
|
+
if (!this.#exit)
|
|
228
|
+
throw new SandboxError(this.#disconnected
|
|
229
|
+
? "Disconnected from the command before it ended; reconnect with commands.connect(pid)."
|
|
230
|
+
: "The command's output ended without an exit.");
|
|
231
|
+
return settle({
|
|
232
|
+
exitCode: this.#exit.exitCode,
|
|
233
|
+
stdout: this.#stdout,
|
|
234
|
+
stderr: this.#stderr,
|
|
235
|
+
timedOut: this.#exit.timedOut,
|
|
236
|
+
}, this.#timeoutMs);
|
|
237
|
+
}
|
|
238
|
+
/** Stops receiving output; the command keeps running. */
|
|
239
|
+
async disconnect() {
|
|
240
|
+
this.#disconnected = true;
|
|
241
|
+
this.#abort.abort();
|
|
242
|
+
await Promise.resolve();
|
|
243
|
+
}
|
|
244
|
+
/** Kills the command with SIGKILL. False when it had already ended. */
|
|
245
|
+
async kill() {
|
|
246
|
+
if (this.#exit)
|
|
247
|
+
return false;
|
|
248
|
+
try {
|
|
249
|
+
await guard("sandbox", () => this.#process.kill("SIGKILL"));
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof SandboxError && error.statusCode === 404)
|
|
254
|
+
return false;
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async sendStdin(data, _opts = {}) {
|
|
259
|
+
if (!this.#stdin)
|
|
260
|
+
throw new InvalidArgumentError("The command was not started with stdin: true, so its input is closed.");
|
|
261
|
+
await guard("sandbox", () => this.#process.write(data));
|
|
262
|
+
}
|
|
263
|
+
async closeStdin(_opts = {}) {
|
|
264
|
+
if (!this.#stdin)
|
|
265
|
+
throw new InvalidArgumentError("The command was not started with stdin: true.");
|
|
266
|
+
await guard("sandbox", () => this.#process.write("", { eof: true }));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Base class for E2B's sandbox errors. */
|
|
2
|
+
export declare class SandboxError extends Error {
|
|
3
|
+
statusCode?: number;
|
|
4
|
+
code?: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
requestId?: string;
|
|
7
|
+
constructor(message?: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class TimeoutError extends SandboxError {
|
|
10
|
+
}
|
|
11
|
+
export declare class InvalidArgumentError extends SandboxError {
|
|
12
|
+
}
|
|
13
|
+
export declare class NotEnoughSpaceError extends SandboxError {
|
|
14
|
+
}
|
|
15
|
+
export declare class NotFoundError extends SandboxError {
|
|
16
|
+
}
|
|
17
|
+
export declare class FileNotFoundError extends NotFoundError {
|
|
18
|
+
}
|
|
19
|
+
export declare class SandboxNotFoundError extends NotFoundError {
|
|
20
|
+
}
|
|
21
|
+
export declare class TemplateError extends SandboxError {
|
|
22
|
+
}
|
|
23
|
+
export declare class RateLimitError extends SandboxError {
|
|
24
|
+
}
|
|
25
|
+
/** E2B's AuthenticationError extends Error, not SandboxError. */
|
|
26
|
+
export declare class AuthenticationError extends Error {
|
|
27
|
+
code?: string;
|
|
28
|
+
hint?: string;
|
|
29
|
+
requestId?: string;
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
/** E2B's ServiceBusyError extends Error, not SandboxError. */
|
|
33
|
+
export declare class ServiceBusyError extends Error {
|
|
34
|
+
readonly statusCode = 503;
|
|
35
|
+
code?: string;
|
|
36
|
+
hint?: string;
|
|
37
|
+
requestId?: string;
|
|
38
|
+
constructor(message: string);
|
|
39
|
+
}
|
|
40
|
+
/** A call that E2B supports and Runtime does not, or not in the same way.
|
|
41
|
+
* Thrown before anything is done, never after doing something different.
|
|
42
|
+
* `feature` names what was asked for; `alternative` says what to use on
|
|
43
|
+
* Runtime instead. */
|
|
44
|
+
export declare class NotSupportedError extends SandboxError {
|
|
45
|
+
readonly feature: string;
|
|
46
|
+
readonly alternative: string;
|
|
47
|
+
constructor(feature: string, alternative: string, message?: string);
|
|
48
|
+
}
|
|
49
|
+
/** What `commands.run` resolves with, as in E2B. */
|
|
50
|
+
export interface CommandResult {
|
|
51
|
+
exitCode: number;
|
|
52
|
+
error?: string;
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
}
|
|
56
|
+
/** Thrown by `commands.run` and `CommandHandle.wait` when the command exits
|
|
57
|
+
* with a code other than 0. It carries the result, as in E2B. */
|
|
58
|
+
export declare class CommandExitError extends SandboxError implements CommandResult {
|
|
59
|
+
#private;
|
|
60
|
+
constructor(result: CommandResult);
|
|
61
|
+
get exitCode(): number;
|
|
62
|
+
get error(): string | undefined;
|
|
63
|
+
get stdout(): string;
|
|
64
|
+
get stderr(): string;
|
|
65
|
+
}
|
|
66
|
+
/** Which object a call was about, to name a 404 the way E2B would. */
|
|
67
|
+
export type Subject = "sandbox" | "file" | "other";
|
|
68
|
+
/** Turns a Runtime SDK error into the E2B error a caller written for E2B
|
|
69
|
+
* expects. Anything that is not a Runtime error passes through unchanged. */
|
|
70
|
+
export declare function translate(error: unknown, subject?: Subject): unknown;
|
|
71
|
+
/** Runs `work`, turning any Runtime error into its E2B counterpart. */
|
|
72
|
+
export declare function guard<T>(subject: Subject, work: () => Promise<T>): Promise<T>;
|