withruntime 0.3.1 → 0.5.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/DAYTONA.md +129 -0
- package/E2B.md +146 -0
- package/README.md +84 -12
- package/VERCEL.md +127 -0
- package/dist/ai/index.d.ts +33 -0
- package/dist/ai/index.js +38 -0
- package/dist/claude-agent-sdk/index.d.ts +7 -0
- package/dist/claude-agent-sdk/index.js +65 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +949 -105
- package/dist/client.d.ts +25 -2
- package/dist/client.js +14 -1
- package/dist/credentials.d.ts +10 -0
- package/dist/credentials.js +56 -1
- package/dist/daytona/client.d.ts +44 -0
- package/dist/daytona/client.js +40 -0
- package/dist/daytona/context.d.ts +26 -0
- package/dist/daytona/context.js +22 -0
- package/dist/daytona/daytona.d.ts +185 -0
- package/dist/daytona/daytona.js +416 -0
- package/dist/daytona/errors.d.ts +106 -0
- package/dist/daytona/errors.js +182 -0
- package/dist/daytona/filesystem.d.ts +84 -0
- package/dist/daytona/filesystem.js +206 -0
- package/dist/daytona/git.d.ts +33 -0
- package/dist/daytona/git.js +128 -0
- package/dist/daytona/image.d.ts +45 -0
- package/dist/daytona/image.js +140 -0
- package/dist/daytona/index.d.ts +50 -0
- package/dist/daytona/index.js +50 -0
- package/dist/daytona/process.d.ts +86 -0
- package/dist/daytona/process.js +339 -0
- package/dist/daytona/sandbox.d.ts +196 -0
- package/dist/daytona/sandbox.js +448 -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 +203 -0
- package/dist/e2b/sandbox.js +490 -0
- package/dist/e2b/unsupported.d.ts +19 -0
- package/dist/e2b/unsupported.js +27 -0
- package/dist/errors.d.ts +10 -4
- package/dist/errors.js +26 -6
- package/dist/image-context.d.ts +39 -0
- package/dist/image-context.js +176 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +17 -2
- package/dist/keys.d.ts +43 -0
- package/dist/keys.js +196 -0
- package/dist/login.js +27 -6
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +4 -3
- package/dist/openai-agents/index.d.ts +132 -0
- package/dist/openai-agents/index.js +791 -0
- package/dist/products/audit.d.ts +48 -0
- package/dist/products/audit.js +10 -0
- package/dist/products/images.d.ts +113 -7
- package/dist/products/images.js +186 -27
- package/dist/products/index.d.ts +11 -0
- package/dist/products/index.js +17 -1
- package/dist/products/observability.d.ts +186 -0
- package/dist/products/observability.js +117 -0
- package/dist/products/secrets.d.ts +40 -0
- package/dist/products/secrets.js +24 -0
- package/dist/products/switching.d.ts +89 -0
- package/dist/products/switching.js +25 -0
- package/dist/proxy.d.ts +30 -0
- package/dist/proxy.js +178 -0
- package/dist/sandbox.d.ts +30 -2
- package/dist/sandbox.js +101 -1
- package/dist/ssh.d.ts +21 -0
- package/dist/ssh.js +312 -0
- package/dist/tar.d.ts +1 -0
- package/dist/tar.js +4 -4
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.js +180 -0
- package/dist/transport.d.ts +12 -2
- package/dist/transport.js +76 -16
- package/dist/tunnel.d.ts +67 -0
- package/dist/tunnel.js +311 -0
- package/dist/types.d.ts +57 -1
- package/dist/vercel/client.d.ts +40 -0
- package/dist/vercel/client.js +39 -0
- package/dist/vercel/command.d.ts +70 -0
- package/dist/vercel/command.js +148 -0
- package/dist/vercel/errors.d.ts +53 -0
- package/dist/vercel/errors.js +113 -0
- package/dist/vercel/filesystem.d.ts +150 -0
- package/dist/vercel/filesystem.js +248 -0
- package/dist/vercel/index.d.ts +30 -0
- package/dist/vercel/index.js +38 -0
- package/dist/vercel/sandbox.d.ts +317 -0
- package/dist/vercel/sandbox.js +726 -0
- package/dist/vercel/snapshot.d.ts +93 -0
- package/dist/vercel/snapshot.js +123 -0
- package/package.json +69 -5
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { clientFor, request, } from "./client.js";
|
|
2
|
+
import { Commands } from "./commands.js";
|
|
3
|
+
import { guard, InvalidArgumentError, NotSupportedError, SandboxError, SandboxNotFoundError, TemplateError, } from "./errors.js";
|
|
4
|
+
import { Filesystem } from "./filesystem.js";
|
|
5
|
+
/* E2B's Sandbox over Runtime's. Every call is one or a few calls of the
|
|
6
|
+
withruntime SDK; nothing here speaks HTTP. What Runtime cannot do the same
|
|
7
|
+
way throws NotSupportedError before anything happens. */
|
|
8
|
+
/** E2B's default machine: 2 vCPU and 512 MiB (docs.e2b.dev/billing, checked
|
|
9
|
+
* 23 September 2026). Pass `runtime: { create: { memoryMiB } }` for another. */
|
|
10
|
+
export const DEFAULT_SHAPE = { vcpu: 2, memoryMiB: 512 };
|
|
11
|
+
/** E2B's default sandbox timeout, 300 s. */
|
|
12
|
+
export const DEFAULT_TIMEOUT_MS = 300_000;
|
|
13
|
+
/** Runtime's lease bounds (MAX_TIMEOUT_SECONDS in the API). */
|
|
14
|
+
const MIN_LEASE_SECONDS = 60;
|
|
15
|
+
const MAX_LEASE_SECONDS = 3600;
|
|
16
|
+
/** E2B template names that mean "the stock environment": Runtime's default image. */
|
|
17
|
+
const STOCK_TEMPLATES = new Set(["base", "code-interpreter-v1", "code-interpreter"]);
|
|
18
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
|
+
let warnedShortLease = false;
|
|
20
|
+
function leaseSeconds(timeoutMs) {
|
|
21
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
22
|
+
throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
|
|
23
|
+
const seconds = Math.ceil(timeoutMs / 1000);
|
|
24
|
+
if (seconds > MAX_LEASE_SECONDS)
|
|
25
|
+
throw new NotSupportedError(`A sandbox timeout of ${timeoutMs} ms (over one hour)`, "Runtime leases last up to an hour (3_600_000 ms); call sandbox.setTimeout(...) before it ends to keep going, as often as needed.");
|
|
26
|
+
if (seconds < MIN_LEASE_SECONDS) {
|
|
27
|
+
if (!warnedShortLease && typeof process !== "undefined") {
|
|
28
|
+
warnedShortLease = true;
|
|
29
|
+
process.emitWarning(`timeoutMs ${timeoutMs} is under Runtime's shortest lease; the sandbox gets 60 s. Call kill() when done.`, { code: "RUNTIME_E2B_SHORT_TIMEOUT" });
|
|
30
|
+
}
|
|
31
|
+
return MIN_LEASE_SECONDS;
|
|
32
|
+
}
|
|
33
|
+
return seconds;
|
|
34
|
+
}
|
|
35
|
+
function onLeaseEnd(lifecycle) {
|
|
36
|
+
if (!lifecycle)
|
|
37
|
+
return "stop";
|
|
38
|
+
const action = typeof lifecycle.onTimeout === "string" ? lifecycle.onTimeout : lifecycle.onTimeout.action;
|
|
39
|
+
if (typeof lifecycle.onTimeout === "object" && lifecycle.onTimeout.keepMemory === false)
|
|
40
|
+
throw new NotSupportedError("A files-only pause (keepMemory: false)", "Runtime's pause keeps memory and files; leave keepMemory out.");
|
|
41
|
+
return action === "pause" ? "pause" : "stop";
|
|
42
|
+
}
|
|
43
|
+
function refuseCreate(opts) {
|
|
44
|
+
const refusals = [
|
|
45
|
+
[
|
|
46
|
+
"mcp",
|
|
47
|
+
"E2B's MCP gateway (mcp)",
|
|
48
|
+
"Run MCP servers yourself with commands.run(..., { background: true }).",
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"network",
|
|
52
|
+
"E2B's network rules (network)",
|
|
53
|
+
"Pass Runtime's rules instead: runtime: { create: { network: { internet: true, allow: [...], deny: [...] } } }.",
|
|
54
|
+
],
|
|
55
|
+
["iam", "E2B workload identity (iam)", "Pass credentials with envs."],
|
|
56
|
+
[
|
|
57
|
+
"volumeMounts",
|
|
58
|
+
"E2B volumes (volumeMounts)",
|
|
59
|
+
"Use a Runtime volume: runtime: { create: { volumes: [{ volumeId, path }] } }.",
|
|
60
|
+
],
|
|
61
|
+
];
|
|
62
|
+
for (const [field, feature, alternative] of refusals)
|
|
63
|
+
if (opts[field] !== undefined)
|
|
64
|
+
throw new NotSupportedError(feature, alternative);
|
|
65
|
+
}
|
|
66
|
+
/** Where a template sends the create: the stock image, a Runtime image by
|
|
67
|
+
* name or id, or a Runtime snapshot by id. */
|
|
68
|
+
async function resolveTemplate(client, template) {
|
|
69
|
+
if (STOCK_TEMPLATES.has(template))
|
|
70
|
+
return {};
|
|
71
|
+
if (UUID.test(template)) {
|
|
72
|
+
try {
|
|
73
|
+
await client.images.get(template);
|
|
74
|
+
return { image: template };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.status !== 404)
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
return { snapshot: template };
|
|
81
|
+
}
|
|
82
|
+
const page = await client.images.list({ name: template, state: "ready", limit: 1 });
|
|
83
|
+
const image = page.data[0];
|
|
84
|
+
if (image)
|
|
85
|
+
return { image: image.id };
|
|
86
|
+
const error = new TemplateError(`No Runtime image is named "${template}". E2B templates do not run on Runtime; build the same ` +
|
|
87
|
+
`environment as a Runtime image with that name and this call starts from it: ` +
|
|
88
|
+
`\`npx withruntime image build --dockerfile e2b.Dockerfile --name ${template}\`, or ` +
|
|
89
|
+
`runtime.images.build({ name: "${template}", dockerfile }).`);
|
|
90
|
+
error.code = "template_not_found";
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
function state(runtime) {
|
|
94
|
+
const current = runtime.state;
|
|
95
|
+
if (current === "paused" || current === "pausing")
|
|
96
|
+
return "paused";
|
|
97
|
+
if (current === "stopped" || current === "stopping")
|
|
98
|
+
return "stopped";
|
|
99
|
+
return "running";
|
|
100
|
+
}
|
|
101
|
+
function infoOf(runtime) {
|
|
102
|
+
const info = runtime.info;
|
|
103
|
+
const templateId = typeof info.image === "string"
|
|
104
|
+
? info.image
|
|
105
|
+
: typeof info.snapshot === "string"
|
|
106
|
+
? info.snapshot
|
|
107
|
+
: "base";
|
|
108
|
+
const current = state(runtime);
|
|
109
|
+
return {
|
|
110
|
+
sandboxId: info.id,
|
|
111
|
+
templateId,
|
|
112
|
+
...(info.name ? { name: info.name } : {}),
|
|
113
|
+
metadata: info.labels ?? {},
|
|
114
|
+
startedAt: new Date(info.createdAt),
|
|
115
|
+
endAt: new Date(info.expiresAt),
|
|
116
|
+
state: current === "stopped" ? "paused" : current,
|
|
117
|
+
cpuCount: info.vcpu,
|
|
118
|
+
memoryMB: info.memoryMiB,
|
|
119
|
+
envdVersion: "runtime",
|
|
120
|
+
lifecycle: {
|
|
121
|
+
onTimeout: info.onLeaseEnd === "stop" ? "kill" : "pause",
|
|
122
|
+
autoResume: info.autoWake === true,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function unsupported(feature, alternative) {
|
|
127
|
+
return Promise.reject(new NotSupportedError(feature, alternative));
|
|
128
|
+
}
|
|
129
|
+
/** A sandbox, with E2B's methods. `sandbox.runtime` is the Runtime sandbox
|
|
130
|
+
* underneath, for anything E2B has no name for. */
|
|
131
|
+
export class Sandbox {
|
|
132
|
+
static defaultTemplate = "base";
|
|
133
|
+
files;
|
|
134
|
+
commands;
|
|
135
|
+
#client;
|
|
136
|
+
#runtime;
|
|
137
|
+
#envs;
|
|
138
|
+
#home;
|
|
139
|
+
/** Use Sandbox.create or Sandbox.connect. */
|
|
140
|
+
constructor(runtime, client, envs = {}) {
|
|
141
|
+
this.#runtime = runtime;
|
|
142
|
+
this.#client = client;
|
|
143
|
+
this.#envs = envs;
|
|
144
|
+
const ctx = {
|
|
145
|
+
envs,
|
|
146
|
+
ensureHome: (text) => this.#ensureHome(text),
|
|
147
|
+
};
|
|
148
|
+
// A getter, so the modules see the sandbox as it is after each refresh.
|
|
149
|
+
Object.defineProperty(ctx, "runtime", { get: () => this.#runtime });
|
|
150
|
+
this.files = new Filesystem(ctx);
|
|
151
|
+
this.commands = new Commands(ctx);
|
|
152
|
+
}
|
|
153
|
+
get sandboxId() {
|
|
154
|
+
return this.#runtime.id;
|
|
155
|
+
}
|
|
156
|
+
/** The Runtime sandbox underneath: previews, network, desktop, interpreter,
|
|
157
|
+
* processes, terminal and the rest of Runtime's SDK. */
|
|
158
|
+
get runtime() {
|
|
159
|
+
return this.#runtime;
|
|
160
|
+
}
|
|
161
|
+
get client() {
|
|
162
|
+
return this.#client;
|
|
163
|
+
}
|
|
164
|
+
get envs() {
|
|
165
|
+
return this.#envs;
|
|
166
|
+
}
|
|
167
|
+
/** E2B's home is /home/user and Runtime's is /workspace. The first time a
|
|
168
|
+
* path or command names /home/user, it is made a link to /workspace (unless
|
|
169
|
+
* something is already there), so paths written for E2B work. */
|
|
170
|
+
#ensureHome(text) {
|
|
171
|
+
if (this.#home || !text?.includes("/home/user"))
|
|
172
|
+
return this.#home ?? Promise.resolve();
|
|
173
|
+
this.#home = (async () => {
|
|
174
|
+
const result = await guard("sandbox", () => this.#runtime.exec("[ -e /home/user ] || sudo ln -s /workspace /home/user"));
|
|
175
|
+
if (result.exitCode !== 0 && typeof process !== "undefined")
|
|
176
|
+
process.emitWarning(`Could not link /home/user to /workspace: ${result.stderr.trim()}`, {
|
|
177
|
+
code: "RUNTIME_E2B_HOME",
|
|
178
|
+
});
|
|
179
|
+
})();
|
|
180
|
+
return this.#home;
|
|
181
|
+
}
|
|
182
|
+
static async create(templateOrOpts, maybeOpts) {
|
|
183
|
+
const opts = (typeof templateOrOpts === "string" ? maybeOpts : templateOrOpts) ?? {};
|
|
184
|
+
const template = (typeof templateOrOpts === "string" ? templateOrOpts : opts.template) ?? this.defaultTemplate;
|
|
185
|
+
refuseCreate(opts);
|
|
186
|
+
const client = clientFor(opts);
|
|
187
|
+
const input = {
|
|
188
|
+
timeoutSeconds: leaseSeconds(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
189
|
+
onLeaseEnd: onLeaseEnd(opts.lifecycle),
|
|
190
|
+
// With a lifecycle, E2B resumes a paused sandbox on traffic only when
|
|
191
|
+
// asked (autoResume); Runtime's automatic wake is the same thing (0093).
|
|
192
|
+
...(opts.lifecycle ? { autoWake: opts.lifecycle.autoResume === true } : {}),
|
|
193
|
+
...(opts.metadata && Object.keys(opts.metadata).length ? { labels: opts.metadata } : {}),
|
|
194
|
+
...(opts.allowInternetAccess === false ? { network: { internet: false } } : {}),
|
|
195
|
+
};
|
|
196
|
+
const source = await guard("other", () => resolveTemplate(client, template));
|
|
197
|
+
const create = {
|
|
198
|
+
// A snapshot carries its own machine; everything else gets E2B's shape.
|
|
199
|
+
...(source.snapshot ? {} : DEFAULT_SHAPE),
|
|
200
|
+
...input,
|
|
201
|
+
...source,
|
|
202
|
+
...opts.runtime?.create,
|
|
203
|
+
};
|
|
204
|
+
const runtime = await guard("sandbox", () => client.sandboxes.create(create, request(opts)));
|
|
205
|
+
return new this(runtime, client, { ...opts.envs });
|
|
206
|
+
}
|
|
207
|
+
/** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
|
|
208
|
+
* `timeoutMs` moves a running sandbox's end later, never earlier. */
|
|
209
|
+
static async connect(sandboxId, opts = {}) {
|
|
210
|
+
const client = clientFor(opts);
|
|
211
|
+
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
212
|
+
await resume(runtime, opts);
|
|
213
|
+
return new this(runtime, client);
|
|
214
|
+
}
|
|
215
|
+
/** Wakes this sandbox if it is paused. */
|
|
216
|
+
async connect(opts = {}) {
|
|
217
|
+
await guard("sandbox", () => this.#runtime.refresh(request(opts)));
|
|
218
|
+
await resume(this.#runtime, opts);
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
/** Running and paused sandboxes, a page at a time, as E2B's paginator. */
|
|
222
|
+
static list(opts = {}) {
|
|
223
|
+
return new SandboxPaginator(clientFor(opts), opts);
|
|
224
|
+
}
|
|
225
|
+
// ---- lifecycle --------------------------------------------------------
|
|
226
|
+
/** Stops the sandbox. False when it was not found or had already ended. */
|
|
227
|
+
static async kill(sandboxId, opts = {}) {
|
|
228
|
+
const client = clientFor(opts);
|
|
229
|
+
try {
|
|
230
|
+
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
231
|
+
return await stop(runtime, opts);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (error instanceof SandboxNotFoundError)
|
|
235
|
+
return false;
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async kill(opts = {}) {
|
|
240
|
+
try {
|
|
241
|
+
return await stop(this.#runtime, opts);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (error instanceof SandboxNotFoundError)
|
|
245
|
+
return false;
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Sets the sandbox to end `timeoutMs` from now. Runtime leases only move
|
|
250
|
+
* later: a shorter timeout than the one it has is refused. */
|
|
251
|
+
static async setTimeout(sandboxId, timeoutMs, opts = {}) {
|
|
252
|
+
const client = clientFor(opts);
|
|
253
|
+
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
254
|
+
await extendTo(runtime, timeoutMs, opts);
|
|
255
|
+
}
|
|
256
|
+
async setTimeout(timeoutMs, opts = {}) {
|
|
257
|
+
await guard("sandbox", () => this.#runtime.refresh(request(opts)));
|
|
258
|
+
await extendTo(this.#runtime, timeoutMs, opts);
|
|
259
|
+
}
|
|
260
|
+
static async getInfo(sandboxId, opts = {}) {
|
|
261
|
+
const client = clientFor(opts);
|
|
262
|
+
return infoOf(await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts))));
|
|
263
|
+
}
|
|
264
|
+
async getInfo(opts = {}) {
|
|
265
|
+
await guard("sandbox", () => this.#runtime.refresh(request(opts)));
|
|
266
|
+
return infoOf(this.#runtime);
|
|
267
|
+
}
|
|
268
|
+
async isRunning(opts = {}) {
|
|
269
|
+
try {
|
|
270
|
+
await guard("sandbox", () => this.#runtime.refresh(request(opts)));
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (error instanceof SandboxNotFoundError)
|
|
274
|
+
return false;
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
return this.#runtime.state === "running";
|
|
278
|
+
}
|
|
279
|
+
/** Pauses the sandbox, keeping memory and files. False when it was paused. */
|
|
280
|
+
static async pause(sandboxId, opts = {}) {
|
|
281
|
+
const client = clientFor(opts);
|
|
282
|
+
return pause(await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts))), opts);
|
|
283
|
+
}
|
|
284
|
+
static betaPause(sandboxId, opts = {}) {
|
|
285
|
+
return this.pause(sandboxId, opts);
|
|
286
|
+
}
|
|
287
|
+
async pause(opts = {}) {
|
|
288
|
+
await guard("sandbox", () => this.#runtime.refresh(request(opts)));
|
|
289
|
+
return pause(this.#runtime, opts);
|
|
290
|
+
}
|
|
291
|
+
async betaPause(opts = {}) {
|
|
292
|
+
return this.pause(opts);
|
|
293
|
+
}
|
|
294
|
+
// ---- forks and snapshots ---------------------------------------------
|
|
295
|
+
/** Copies of this sandbox, memory and all. While Runtime's forks are
|
|
296
|
+
* switched off this throws NotSupportedError with Runtime's own words. */
|
|
297
|
+
static async fork(sandboxId, opts = {}) {
|
|
298
|
+
const { count: _count, timeoutMs: _timeoutMs, ...connection } = opts;
|
|
299
|
+
const source = await this.connect(sandboxId, connection);
|
|
300
|
+
return source.fork(opts);
|
|
301
|
+
}
|
|
302
|
+
async fork(opts = {}) {
|
|
303
|
+
if (opts.timeoutMs !== undefined)
|
|
304
|
+
// A fork's lease is Runtime's to set; one shorter than asked could not be
|
|
305
|
+
// honoured after the forks exist, so this is refused before any are made.
|
|
306
|
+
throw new NotSupportedError("A timeout for forks (fork timeoutMs)", "Fork without it, then call setTimeout(ms) on each fork.");
|
|
307
|
+
const copies = await guard("sandbox", () => this.#runtime.fork({ count: opts.count ?? 1, ...request(opts) }));
|
|
308
|
+
const Kind = this.constructor;
|
|
309
|
+
return copies.map((copy) => new Kind(copy, this.#client, { ...this.#envs }));
|
|
310
|
+
}
|
|
311
|
+
/** Keeps the sandbox's whole machine as a Runtime snapshot; start from it
|
|
312
|
+
* with Sandbox.create(snapshotId). */
|
|
313
|
+
async createSnapshot(opts = {}) {
|
|
314
|
+
const snapshot = await guard("sandbox", () => this.#runtime.snapshot({ ...(opts.name ? { name: opts.name } : {}), ...request(opts) }));
|
|
315
|
+
return { snapshotId: snapshot.id, names: snapshot.name ? [snapshot.name] : [] };
|
|
316
|
+
}
|
|
317
|
+
static async createSnapshot(sandboxId, opts = {}) {
|
|
318
|
+
const client = clientFor(opts);
|
|
319
|
+
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
320
|
+
return new Sandbox(runtime, client).createSnapshot(opts);
|
|
321
|
+
}
|
|
322
|
+
static async deleteSnapshot(snapshotId, opts = {}) {
|
|
323
|
+
const client = clientFor(opts);
|
|
324
|
+
try {
|
|
325
|
+
await guard("other", () => client.snapshots.delete(snapshotId, request(opts)));
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error instanceof SandboxError && error.statusCode === 404)
|
|
330
|
+
return false;
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// ---- ports ------------------------------------------------------------
|
|
335
|
+
/** Refused: E2B answers getHost without a call, and a Runtime port becomes
|
|
336
|
+
* reachable only once it is shared. Use getPublicHost. */
|
|
337
|
+
getHost(port) {
|
|
338
|
+
throw new NotSupportedError(`sandbox.getHost(${port}), which answers without asking the server`, `Use \`await sandbox.getPublicHost(${port})\`: it shares the port at a public HTTPS address and returns its host, as getHost does.`);
|
|
339
|
+
}
|
|
340
|
+
/** Shares `port` at a public HTTPS address (a Runtime preview) and returns
|
|
341
|
+
* its host, `<port>-<id>.<domain>`, like E2B's getHost. Anyone with the
|
|
342
|
+
* address can reach it; a browser sees a one-time page naming Runtime. */
|
|
343
|
+
async getPublicHost(port) {
|
|
344
|
+
const preview = await guard("sandbox", () => this.#runtime.previews.create(port, { visibility: "public" }));
|
|
345
|
+
return new URL(preview.url).host;
|
|
346
|
+
}
|
|
347
|
+
// ---- what Runtime does differently -----------------------------------
|
|
348
|
+
get pty() {
|
|
349
|
+
throw new NotSupportedError("E2B's pty module", "Use `await sandbox.runtime.terminal({ cols, rows, onData })` for an interactive terminal, or runtime.spawn(cmd, { pty: {} }).");
|
|
350
|
+
}
|
|
351
|
+
get git() {
|
|
352
|
+
throw new NotSupportedError("E2B's git module (deprecated by E2B too)", "Run git with sandbox.commands.run('git ...'); git is installed.");
|
|
353
|
+
}
|
|
354
|
+
/** E2B's metrics, from Runtime's measured readings (`sbx.metrics()`): CPU
|
|
355
|
+
* as a percent of the sandbox's vCPUs and resident memory, one entry per
|
|
356
|
+
* host reading (every minute by default) between `start` and `end`, kept
|
|
357
|
+
* 24 hours at that resolution. Disk use inside the sandbox is not measured:
|
|
358
|
+
* `diskUsed` is null and `diskTotal` is the disk's size. */
|
|
359
|
+
async getMetrics(opts = {}) {
|
|
360
|
+
return e2bMetrics(this.#runtime, opts);
|
|
361
|
+
}
|
|
362
|
+
static async getMetrics(sandboxId, opts = {}) {
|
|
363
|
+
const client = clientFor(opts);
|
|
364
|
+
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
365
|
+
return e2bMetrics(runtime, opts);
|
|
366
|
+
}
|
|
367
|
+
updateNetwork() {
|
|
368
|
+
return unsupported("E2B's network rules (updateNetwork)", "Use sandbox.runtime.network.set({ internet, allow, deny }).");
|
|
369
|
+
}
|
|
370
|
+
uploadUrl() {
|
|
371
|
+
return unsupported("Signed upload URLs", "Use sandbox.files.write(path, data).");
|
|
372
|
+
}
|
|
373
|
+
downloadUrl() {
|
|
374
|
+
return unsupported("Signed download URLs", "Use sandbox.files.read(path, { format: 'bytes' }).");
|
|
375
|
+
}
|
|
376
|
+
getMcpUrl() {
|
|
377
|
+
throw new NotSupportedError("E2B's MCP gateway", "Runtime's own MCP server is `npx withruntime mcp`.");
|
|
378
|
+
}
|
|
379
|
+
getMcpToken() {
|
|
380
|
+
return unsupported("E2B's MCP gateway", "Runtime's own MCP server is `npx withruntime mcp`.");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function resume(runtime, opts) {
|
|
384
|
+
if (opts.onResume === "reboot")
|
|
385
|
+
throw new NotSupportedError("Resuming by reboot (onResume: 'reboot')", "Runtime's wake restores memory; stop the sandbox and create a new one for a clean boot.");
|
|
386
|
+
const current = state(runtime);
|
|
387
|
+
if (current === "stopped")
|
|
388
|
+
throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
|
|
389
|
+
if (current === "paused") {
|
|
390
|
+
await guard("sandbox", () => runtime.wake({
|
|
391
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
|
|
392
|
+
...request(opts),
|
|
393
|
+
}));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (opts.timeoutMs !== undefined) {
|
|
397
|
+
const later = Date.now() + opts.timeoutMs - Date.parse(runtime.info.expiresAt);
|
|
398
|
+
if (later > 1000)
|
|
399
|
+
await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function stop(runtime, opts) {
|
|
403
|
+
if (state(runtime) === "stopped")
|
|
404
|
+
return false;
|
|
405
|
+
await guard("sandbox", () => runtime.stop({ wait: false, ...request(opts) }));
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
async function extendTo(runtime, timeoutMs, opts) {
|
|
409
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
410
|
+
throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
|
|
411
|
+
const later = Date.now() + timeoutMs - Date.parse(runtime.info.expiresAt);
|
|
412
|
+
if (later < -1000)
|
|
413
|
+
throw new NotSupportedError("Shortening a sandbox's timeout", "Runtime leases only move later. Call kill() when the work is done.");
|
|
414
|
+
if (later > 1000)
|
|
415
|
+
await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
|
|
416
|
+
}
|
|
417
|
+
async function pause(runtime, opts) {
|
|
418
|
+
if (opts.keepMemory === false)
|
|
419
|
+
throw new NotSupportedError("A files-only pause (keepMemory: false)", "Runtime's pause keeps memory and files; leave keepMemory out.");
|
|
420
|
+
const current = state(runtime);
|
|
421
|
+
if (current === "paused")
|
|
422
|
+
return false;
|
|
423
|
+
if (current === "stopped")
|
|
424
|
+
throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
|
|
425
|
+
await guard("sandbox", () => runtime.pause(request(opts)));
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
const RUNNING = ["starting", "running", "resuming"];
|
|
429
|
+
const PAUSED = ["pausing", "paused"];
|
|
430
|
+
/** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`. */
|
|
431
|
+
export class SandboxPaginator {
|
|
432
|
+
#client;
|
|
433
|
+
#filter;
|
|
434
|
+
#page;
|
|
435
|
+
#hasNext = true;
|
|
436
|
+
constructor(client, opts) {
|
|
437
|
+
this.#client = client;
|
|
438
|
+
const query = opts.query ?? {};
|
|
439
|
+
if (query.template !== undefined && !STOCK_TEMPLATES.has(query.template))
|
|
440
|
+
throw new NotSupportedError("Listing by template", "Filter by metadata instead: give sandboxes a metadata key when you create them.");
|
|
441
|
+
if (query.startedAfter !== undefined)
|
|
442
|
+
throw new NotSupportedError("Listing by start time (startedAfter)", "List them all and filter on getInfo().startedAt.");
|
|
443
|
+
if (opts.order === "desc")
|
|
444
|
+
throw new NotSupportedError("Newest-first listing (order: 'desc')", "Runtime lists oldest first; reverse the list yourself.");
|
|
445
|
+
if (opts.nextToken !== undefined)
|
|
446
|
+
throw new NotSupportedError("Starting a list from a saved nextToken", "Keep the paginator and call nextItems() again.");
|
|
447
|
+
const states = query.state?.length ? query.state : ["running", "paused"];
|
|
448
|
+
this.#filter = {
|
|
449
|
+
state: states.flatMap((one) => (one === "running" ? [...RUNNING] : [...PAUSED])),
|
|
450
|
+
...(query.metadata && Object.keys(query.metadata).length ? { labels: query.metadata } : {}),
|
|
451
|
+
...(opts.limit ? { limit: Math.min(opts.limit, 100) } : {}),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
get hasNext() {
|
|
455
|
+
return this.#hasNext;
|
|
456
|
+
}
|
|
457
|
+
get nextToken() {
|
|
458
|
+
return this.#page?.nextCursor ?? undefined;
|
|
459
|
+
}
|
|
460
|
+
async nextItems() {
|
|
461
|
+
if (!this.#hasNext)
|
|
462
|
+
throw new SandboxError("No more items to fetch.");
|
|
463
|
+
const previous = this.#page;
|
|
464
|
+
const page = await guard("other", async () => previous ? await previous.next() : await this.#client.sandboxes.list(this.#filter));
|
|
465
|
+
this.#page = page ?? undefined;
|
|
466
|
+
this.#hasNext = Boolean(page?.hasMore);
|
|
467
|
+
return (page?.data ?? []).map(infoOf);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function e2bMetrics(runtime, opts) {
|
|
471
|
+
const start = opts.start === undefined ? Date.now() - 15 * 60_000 : new Date(opts.start).getTime();
|
|
472
|
+
const end = opts.end === undefined ? Date.now() : new Date(opts.end).getTime();
|
|
473
|
+
const span = Date.now() - start;
|
|
474
|
+
const range = span <= 15 * 60_000 ? "15m" : span <= 3_600_000 ? "1h" : span <= 6 * 3_600_000 ? "6h" : "24h";
|
|
475
|
+
const metrics = await runtime.metrics({ range });
|
|
476
|
+
return metrics.points
|
|
477
|
+
.filter((point) => {
|
|
478
|
+
const at = Date.parse(point.at);
|
|
479
|
+
return at >= start - metrics.stepSeconds * 1000 && at <= end && point.cpuPercent !== null;
|
|
480
|
+
})
|
|
481
|
+
.map((point) => ({
|
|
482
|
+
timestamp: new Date(point.at),
|
|
483
|
+
cpuUsedPct: point.cpuPercent,
|
|
484
|
+
cpuCount: metrics.vcpu,
|
|
485
|
+
memUsed: point.memoryBytes,
|
|
486
|
+
memTotal: metrics.memoryLimitBytes,
|
|
487
|
+
diskUsed: null,
|
|
488
|
+
diskTotal: metrics.diskLimitBytes,
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A stand-in for an E2B export Runtime has no counterpart for. Importing it
|
|
2
|
+
* works, so a module that only mentions it still loads; calling it, building
|
|
3
|
+
* it or reading anything from it throws NotSupportedError naming the
|
|
4
|
+
* alternative. */
|
|
5
|
+
export type Unsupported = {
|
|
6
|
+
(...args: unknown[]): never;
|
|
7
|
+
new (...args: unknown[]): never;
|
|
8
|
+
readonly [key: string]: (...args: unknown[]) => never;
|
|
9
|
+
};
|
|
10
|
+
export declare function unsupportedExport(name: string, alternative: string): Unsupported;
|
|
11
|
+
export declare const Template: Unsupported;
|
|
12
|
+
export declare const TemplateBase: Unsupported;
|
|
13
|
+
export declare const waitForPort: Unsupported;
|
|
14
|
+
export declare const waitForURL: Unsupported;
|
|
15
|
+
export declare const waitForProcess: Unsupported;
|
|
16
|
+
export declare const waitForFile: Unsupported;
|
|
17
|
+
export declare const waitForTimeout: Unsupported;
|
|
18
|
+
export declare const Volume: Unsupported;
|
|
19
|
+
export declare const Secret: Unsupported;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NotSupportedError } from "./errors.js";
|
|
2
|
+
export function unsupportedExport(name, alternative) {
|
|
3
|
+
const fail = () => {
|
|
4
|
+
throw new NotSupportedError(`E2B's ${name}`, alternative);
|
|
5
|
+
};
|
|
6
|
+
return new Proxy(function unsupported() { }, {
|
|
7
|
+
apply: fail,
|
|
8
|
+
construct: fail,
|
|
9
|
+
get: (_target, property) => {
|
|
10
|
+
// Let the module system, inspectors and promise checks look at it
|
|
11
|
+
// without tripping: only a real use throws.
|
|
12
|
+
if (typeof property === "symbol" || property === "then" || property === "prototype")
|
|
13
|
+
return undefined;
|
|
14
|
+
return fail();
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const TEMPLATES = "Build a Runtime image instead: `npx withruntime image build --dockerfile e2b.Dockerfile --name <template>` or runtime.images.build({ name, dockerfile | recipe }), then Sandbox.create('<template>').";
|
|
19
|
+
export const Template = unsupportedExport("Template builder", TEMPLATES);
|
|
20
|
+
export const TemplateBase = unsupportedExport("TemplateBase", TEMPLATES);
|
|
21
|
+
export const waitForPort = unsupportedExport("waitForPort (a template ready check)", TEMPLATES);
|
|
22
|
+
export const waitForURL = unsupportedExport("waitForURL (a template ready check)", TEMPLATES);
|
|
23
|
+
export const waitForProcess = unsupportedExport("waitForProcess (a template ready check)", TEMPLATES);
|
|
24
|
+
export const waitForFile = unsupportedExport("waitForFile (a template ready check)", TEMPLATES);
|
|
25
|
+
export const waitForTimeout = unsupportedExport("waitForTimeout (a template ready check)", TEMPLATES);
|
|
26
|
+
export const Volume = unsupportedExport("Volume", "Use runtime.volumes (Runtime volumes) and mount one with runtime: { create: { volumes: [{ volumeId, path }] } }.");
|
|
27
|
+
export const Secret = unsupportedExport("Secret", "Pass secrets as envs at create or per command, never inside the command text.");
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
1
|
+
/** Refusals of a create that clear when a sandbox stops or pauses, or a host
|
|
2
|
+
* frees room: the trial's slots, an email domain's trial slots, the account's
|
|
3
|
+
* quota and the region's capacity. `sandboxes.create` waits them out, retrying
|
|
4
|
+
* with the same key and input, for up to `waitForCapacityMs`. */
|
|
5
|
+
export declare const WAITS_FOR_ROOM: ReadonlySet<string>;
|
|
4
6
|
export declare class RuntimeError extends Error {
|
|
5
7
|
readonly code: string;
|
|
6
8
|
readonly status: number;
|
|
@@ -21,7 +23,11 @@ export declare class RuntimeError extends Error {
|
|
|
21
23
|
retryAfterMs?: number;
|
|
22
24
|
cause?: unknown;
|
|
23
25
|
});
|
|
24
|
-
/** Retrying this exact call (the SDK keeps the key) is safe and may work.
|
|
26
|
+
/** Retrying this exact call (the SDK keeps the key) is safe and may work.
|
|
27
|
+
* True for no_capacity and trial_busy as well, which clear by themselves when
|
|
28
|
+
* a host frees room or a trial sandbox stops. `sandboxes.create` already
|
|
29
|
+
* waits for those (see `waitForCapacityMs`), so seeing one from a create
|
|
30
|
+
* means the wait ran out or was switched off. */
|
|
25
31
|
get retryable(): boolean;
|
|
26
32
|
toString(): string;
|
|
27
33
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
/** Every failure the SDK raises. `code` is stable and machine-readable,
|
|
2
2
|
* `hint` says what to do next, and `requestId` is what to quote in a report
|
|
3
3
|
* (runtime.feedback.submit, `runtime feedback`, or support). */
|
|
4
|
+
/** Switched off on purpose: retrying cannot change them. */
|
|
5
|
+
const DELIBERATE = new Set(["fork_unavailable", "previews_unavailable", "unavailable"]);
|
|
6
|
+
/** Refusals that pass on their own: a host frees room, a trial slot frees. */
|
|
7
|
+
const PASSING = new Set(["no_capacity", "trial_busy"]);
|
|
8
|
+
/** Refusals of a create that clear when a sandbox stops or pauses, or a host
|
|
9
|
+
* frees room: the trial's slots, an email domain's trial slots, the account's
|
|
10
|
+
* quota and the region's capacity. `sandboxes.create` waits them out, retrying
|
|
11
|
+
* with the same key and input, for up to `waitForCapacityMs`. */
|
|
12
|
+
export const WAITS_FOR_ROOM = new Set([
|
|
13
|
+
"trial_busy",
|
|
14
|
+
"trial_domain_limit",
|
|
15
|
+
"trial_capacity",
|
|
16
|
+
"quota_exceeded",
|
|
17
|
+
"no_capacity",
|
|
18
|
+
]);
|
|
4
19
|
export class RuntimeError extends Error {
|
|
5
20
|
code;
|
|
6
21
|
status;
|
|
@@ -26,13 +41,18 @@ export class RuntimeError extends Error {
|
|
|
26
41
|
if (init.retryAfterMs !== undefined)
|
|
27
42
|
this.retryAfterMs = init.retryAfterMs;
|
|
28
43
|
}
|
|
29
|
-
/** Retrying this exact call (the SDK keeps the key) is safe and may work.
|
|
44
|
+
/** Retrying this exact call (the SDK keeps the key) is safe and may work.
|
|
45
|
+
* True for no_capacity and trial_busy as well, which clear by themselves when
|
|
46
|
+
* a host frees room or a trial sandbox stops. `sandboxes.create` already
|
|
47
|
+
* waits for those (see `waitForCapacityMs`), so seeing one from a create
|
|
48
|
+
* means the wait ran out or was switched off. */
|
|
30
49
|
get retryable() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.
|
|
50
|
+
if (DELIBERATE.has(this.code))
|
|
51
|
+
return false;
|
|
52
|
+
// A fork asking for more copies than the trial runs at once never fits.
|
|
53
|
+
if (PASSING.has(this.code))
|
|
54
|
+
return this.details?.field !== "count";
|
|
55
|
+
return [0, 429, 502, 503, 504].includes(this.status);
|
|
36
56
|
}
|
|
37
57
|
toString() {
|
|
38
58
|
return `${this.name} [${this.code}${this.status ? ` ${this.status}` : ""}]: ${this.message}${this.hint ? `\nHint: ${this.hint}` : ""}${this.requestId ? `\nRequest: ${this.requestId}` : ""}`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const CONTEXT_LIMITS: {
|
|
2
|
+
/** The archive, compressed: what is uploaded. */
|
|
3
|
+
bytes: number;
|
|
4
|
+
files: number;
|
|
5
|
+
chunkBytes: number;
|
|
6
|
+
};
|
|
7
|
+
export type ContextFile = {
|
|
8
|
+
path: string;
|
|
9
|
+
sha256: string;
|
|
10
|
+
size: number;
|
|
11
|
+
mode: number;
|
|
12
|
+
};
|
|
13
|
+
export type PackedContext = {
|
|
14
|
+
archive: Uint8Array;
|
|
15
|
+
files: ContextFile[];
|
|
16
|
+
/** The .dockerignore that was applied, sent so the server applies it too. */
|
|
17
|
+
dockerignore?: string;
|
|
18
|
+
};
|
|
19
|
+
/** A .dockerignore as Docker reads it: `#` comments, `!` re-includes, `**`
|
|
20
|
+
* crosses directories, a pattern naming a directory excludes what is in it,
|
|
21
|
+
* and the last matching pattern decides. The same rules as the server's
|
|
22
|
+
* (packages/cloud/src/images/dockerfile.ts `dockerignoreFilter`). */
|
|
23
|
+
export declare function dockerignoreFilter(text: string | undefined): (path: string) => boolean;
|
|
24
|
+
/** The context of a folder: its .dockerignore (or `<Dockerfile>.dockerignore`)
|
|
25
|
+
* applied, `.git` left out when there is none. Symbolic links are left out:
|
|
26
|
+
* COPY reads files. */
|
|
27
|
+
export declare function packContext(folder: string, options?: {
|
|
28
|
+
dockerignore?: string;
|
|
29
|
+
dockerfile?: string;
|
|
30
|
+
}): Promise<PackedContext>;
|
|
31
|
+
/** The archive cut into the 1 MiB chunks the server stores by digest. */
|
|
32
|
+
export declare function chunkArchive(archive: Uint8Array): Promise<{
|
|
33
|
+
sha256: string;
|
|
34
|
+
size: number;
|
|
35
|
+
chunks: {
|
|
36
|
+
sha256: string;
|
|
37
|
+
bytes: Uint8Array;
|
|
38
|
+
}[];
|
|
39
|
+
}>;
|