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.
@@ -0,0 +1,458 @@
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
+ if (lifecycle.autoResume)
39
+ throw new NotSupportedError("Auto-resume on traffic (lifecycle.autoResume)", "Wake a paused sandbox with Sandbox.connect(id) before sending it work.");
40
+ const action = typeof lifecycle.onTimeout === "string" ? lifecycle.onTimeout : lifecycle.onTimeout.action;
41
+ if (typeof lifecycle.onTimeout === "object" && lifecycle.onTimeout.keepMemory === false)
42
+ throw new NotSupportedError("A files-only pause (keepMemory: false)", "Runtime's pause keeps memory and files; leave keepMemory out.");
43
+ return action === "pause" ? "pause" : "stop";
44
+ }
45
+ function refuseCreate(opts) {
46
+ const refusals = [
47
+ [
48
+ "mcp",
49
+ "E2B's MCP gateway (mcp)",
50
+ "Run MCP servers yourself with commands.run(..., { background: true }).",
51
+ ],
52
+ [
53
+ "network",
54
+ "E2B's network rules (network)",
55
+ "Pass Runtime's rules instead: runtime: { create: { network: { internet: true, allow: [...], deny: [...] } } }.",
56
+ ],
57
+ ["iam", "E2B workload identity (iam)", "Pass credentials with envs."],
58
+ [
59
+ "volumeMounts",
60
+ "E2B volumes (volumeMounts)",
61
+ "Use a Runtime volume: runtime: { create: { volumes: [{ volumeId, path }] } }.",
62
+ ],
63
+ ];
64
+ for (const [field, feature, alternative] of refusals)
65
+ if (opts[field] !== undefined)
66
+ throw new NotSupportedError(feature, alternative);
67
+ }
68
+ /** Where a template sends the create: the stock image, a Runtime image by
69
+ * name or id, or a Runtime snapshot by id. */
70
+ async function resolveTemplate(client, template) {
71
+ if (STOCK_TEMPLATES.has(template))
72
+ return {};
73
+ if (UUID.test(template)) {
74
+ try {
75
+ await client.images.get(template);
76
+ return { image: template };
77
+ }
78
+ catch (error) {
79
+ if (error.status !== 404)
80
+ throw error;
81
+ }
82
+ return { snapshot: template };
83
+ }
84
+ const page = await client.images.list({ name: template, state: "ready", limit: 1 });
85
+ const image = page.data[0];
86
+ if (image)
87
+ return { image: image.id };
88
+ const error = new TemplateError(`No Runtime image is named "${template}". E2B templates do not run on Runtime; build the same ` +
89
+ `environment as a Runtime image with that name and this call starts from it: ` +
90
+ `\`npx withruntime image build --dockerfile e2b.Dockerfile --name ${template}\`, or ` +
91
+ `runtime.images.build({ name: "${template}", dockerfile }).`);
92
+ error.code = "template_not_found";
93
+ throw error;
94
+ }
95
+ function state(runtime) {
96
+ const current = runtime.state;
97
+ if (current === "paused" || current === "pausing")
98
+ return "paused";
99
+ if (current === "stopped" || current === "stopping")
100
+ return "stopped";
101
+ return "running";
102
+ }
103
+ function infoOf(runtime) {
104
+ const info = runtime.info;
105
+ const templateId = typeof info.image === "string"
106
+ ? info.image
107
+ : typeof info.snapshot === "string"
108
+ ? info.snapshot
109
+ : "base";
110
+ const current = state(runtime);
111
+ return {
112
+ sandboxId: info.id,
113
+ templateId,
114
+ ...(info.name ? { name: info.name } : {}),
115
+ metadata: info.labels ?? {},
116
+ startedAt: new Date(info.createdAt),
117
+ endAt: new Date(info.expiresAt),
118
+ state: current === "stopped" ? "paused" : current,
119
+ cpuCount: info.vcpu,
120
+ memoryMB: info.memoryMiB,
121
+ envdVersion: "runtime",
122
+ lifecycle: { onTimeout: info.onLeaseEnd === "stop" ? "kill" : "pause", autoResume: false },
123
+ };
124
+ }
125
+ function unsupported(feature, alternative) {
126
+ return Promise.reject(new NotSupportedError(feature, alternative));
127
+ }
128
+ /** A sandbox, with E2B's methods. `sandbox.runtime` is the Runtime sandbox
129
+ * underneath, for anything E2B has no name for. */
130
+ export class Sandbox {
131
+ static defaultTemplate = "base";
132
+ files;
133
+ commands;
134
+ #client;
135
+ #runtime;
136
+ #envs;
137
+ #home;
138
+ /** Use Sandbox.create or Sandbox.connect. */
139
+ constructor(runtime, client, envs = {}) {
140
+ this.#runtime = runtime;
141
+ this.#client = client;
142
+ this.#envs = envs;
143
+ const ctx = {
144
+ envs,
145
+ ensureHome: (text) => this.#ensureHome(text),
146
+ };
147
+ // A getter, so the modules see the sandbox as it is after each refresh.
148
+ Object.defineProperty(ctx, "runtime", { get: () => this.#runtime });
149
+ this.files = new Filesystem(ctx);
150
+ this.commands = new Commands(ctx);
151
+ }
152
+ get sandboxId() {
153
+ return this.#runtime.id;
154
+ }
155
+ /** The Runtime sandbox underneath: previews, network, desktop, interpreter,
156
+ * processes, terminal and the rest of Runtime's SDK. */
157
+ get runtime() {
158
+ return this.#runtime;
159
+ }
160
+ get client() {
161
+ return this.#client;
162
+ }
163
+ get envs() {
164
+ return this.#envs;
165
+ }
166
+ /** E2B's home is /home/user and Runtime's is /workspace. The first time a
167
+ * path or command names /home/user, it is made a link to /workspace (unless
168
+ * something is already there), so paths written for E2B work. */
169
+ #ensureHome(text) {
170
+ if (this.#home || !text?.includes("/home/user"))
171
+ return this.#home ?? Promise.resolve();
172
+ this.#home = (async () => {
173
+ const result = await guard("sandbox", () => this.#runtime.exec("[ -e /home/user ] || sudo ln -s /workspace /home/user"));
174
+ if (result.exitCode !== 0 && typeof process !== "undefined")
175
+ process.emitWarning(`Could not link /home/user to /workspace: ${result.stderr.trim()}`, {
176
+ code: "RUNTIME_E2B_HOME",
177
+ });
178
+ })();
179
+ return this.#home;
180
+ }
181
+ static async create(templateOrOpts, maybeOpts) {
182
+ const opts = (typeof templateOrOpts === "string" ? maybeOpts : templateOrOpts) ?? {};
183
+ const template = (typeof templateOrOpts === "string" ? templateOrOpts : opts.template) ?? this.defaultTemplate;
184
+ refuseCreate(opts);
185
+ const client = clientFor(opts);
186
+ const input = {
187
+ timeoutSeconds: leaseSeconds(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
188
+ onLeaseEnd: onLeaseEnd(opts.lifecycle),
189
+ ...(opts.metadata && Object.keys(opts.metadata).length ? { labels: opts.metadata } : {}),
190
+ ...(opts.allowInternetAccess === false ? { network: { internet: false } } : {}),
191
+ };
192
+ const source = await guard("other", () => resolveTemplate(client, template));
193
+ const create = {
194
+ // A snapshot carries its own machine; everything else gets E2B's shape.
195
+ ...(source.snapshot ? {} : DEFAULT_SHAPE),
196
+ ...input,
197
+ ...source,
198
+ ...opts.runtime?.create,
199
+ };
200
+ const runtime = await guard("sandbox", () => client.sandboxes.create(create, request(opts)));
201
+ return new this(runtime, client, { ...opts.envs });
202
+ }
203
+ /** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
204
+ * `timeoutMs` moves a running sandbox's end later, never earlier. */
205
+ static async connect(sandboxId, opts = {}) {
206
+ const client = clientFor(opts);
207
+ const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
208
+ await resume(runtime, opts);
209
+ return new this(runtime, client);
210
+ }
211
+ /** Wakes this sandbox if it is paused. */
212
+ async connect(opts = {}) {
213
+ await guard("sandbox", () => this.#runtime.refresh(request(opts)));
214
+ await resume(this.#runtime, opts);
215
+ return this;
216
+ }
217
+ /** Running and paused sandboxes, a page at a time, as E2B's paginator. */
218
+ static list(opts = {}) {
219
+ return new SandboxPaginator(clientFor(opts), opts);
220
+ }
221
+ // ---- lifecycle --------------------------------------------------------
222
+ /** Stops the sandbox. False when it was not found or had already ended. */
223
+ static async kill(sandboxId, opts = {}) {
224
+ const client = clientFor(opts);
225
+ try {
226
+ const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
227
+ return await stop(runtime, opts);
228
+ }
229
+ catch (error) {
230
+ if (error instanceof SandboxNotFoundError)
231
+ return false;
232
+ throw error;
233
+ }
234
+ }
235
+ async kill(opts = {}) {
236
+ try {
237
+ return await stop(this.#runtime, opts);
238
+ }
239
+ catch (error) {
240
+ if (error instanceof SandboxNotFoundError)
241
+ return false;
242
+ throw error;
243
+ }
244
+ }
245
+ /** Sets the sandbox to end `timeoutMs` from now. Runtime leases only move
246
+ * later: a shorter timeout than the one it has is refused. */
247
+ static async setTimeout(sandboxId, timeoutMs, opts = {}) {
248
+ const client = clientFor(opts);
249
+ const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
250
+ await extendTo(runtime, timeoutMs, opts);
251
+ }
252
+ async setTimeout(timeoutMs, opts = {}) {
253
+ await guard("sandbox", () => this.#runtime.refresh(request(opts)));
254
+ await extendTo(this.#runtime, timeoutMs, opts);
255
+ }
256
+ static async getInfo(sandboxId, opts = {}) {
257
+ const client = clientFor(opts);
258
+ return infoOf(await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts))));
259
+ }
260
+ async getInfo(opts = {}) {
261
+ await guard("sandbox", () => this.#runtime.refresh(request(opts)));
262
+ return infoOf(this.#runtime);
263
+ }
264
+ async isRunning(opts = {}) {
265
+ try {
266
+ await guard("sandbox", () => this.#runtime.refresh(request(opts)));
267
+ }
268
+ catch (error) {
269
+ if (error instanceof SandboxNotFoundError)
270
+ return false;
271
+ throw error;
272
+ }
273
+ return this.#runtime.state === "running";
274
+ }
275
+ /** Pauses the sandbox, keeping memory and files. False when it was paused. */
276
+ static async pause(sandboxId, opts = {}) {
277
+ const client = clientFor(opts);
278
+ return pause(await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts))), opts);
279
+ }
280
+ static betaPause(sandboxId, opts = {}) {
281
+ return this.pause(sandboxId, opts);
282
+ }
283
+ async pause(opts = {}) {
284
+ await guard("sandbox", () => this.#runtime.refresh(request(opts)));
285
+ return pause(this.#runtime, opts);
286
+ }
287
+ async betaPause(opts = {}) {
288
+ return this.pause(opts);
289
+ }
290
+ // ---- forks and snapshots ---------------------------------------------
291
+ /** Copies of this sandbox, memory and all. While Runtime's forks are
292
+ * switched off this throws NotSupportedError with Runtime's own words. */
293
+ static async fork(sandboxId, opts = {}) {
294
+ const { count: _count, timeoutMs: _timeoutMs, ...connection } = opts;
295
+ const source = await this.connect(sandboxId, connection);
296
+ return source.fork(opts);
297
+ }
298
+ async fork(opts = {}) {
299
+ if (opts.timeoutMs !== undefined)
300
+ // A fork's lease is Runtime's to set; one shorter than asked could not be
301
+ // honoured after the forks exist, so this is refused before any are made.
302
+ throw new NotSupportedError("A timeout for forks (fork timeoutMs)", "Fork without it, then call setTimeout(ms) on each fork.");
303
+ const copies = await guard("sandbox", () => this.#runtime.fork({ count: opts.count ?? 1, ...request(opts) }));
304
+ const Kind = this.constructor;
305
+ return copies.map((copy) => new Kind(copy, this.#client, { ...this.#envs }));
306
+ }
307
+ /** Keeps the sandbox's whole machine as a Runtime snapshot; start from it
308
+ * with Sandbox.create(snapshotId). */
309
+ async createSnapshot(opts = {}) {
310
+ const snapshot = await guard("sandbox", () => this.#runtime.snapshot({ ...(opts.name ? { name: opts.name } : {}), ...request(opts) }));
311
+ return { snapshotId: snapshot.id, names: snapshot.name ? [snapshot.name] : [] };
312
+ }
313
+ static async createSnapshot(sandboxId, opts = {}) {
314
+ const client = clientFor(opts);
315
+ const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
316
+ return new Sandbox(runtime, client).createSnapshot(opts);
317
+ }
318
+ static async deleteSnapshot(snapshotId, opts = {}) {
319
+ const client = clientFor(opts);
320
+ try {
321
+ await guard("other", () => client.snapshots.delete(snapshotId, request(opts)));
322
+ return true;
323
+ }
324
+ catch (error) {
325
+ if (error instanceof SandboxError && error.statusCode === 404)
326
+ return false;
327
+ throw error;
328
+ }
329
+ }
330
+ // ---- ports ------------------------------------------------------------
331
+ /** Refused: E2B answers getHost without a call, and a Runtime port becomes
332
+ * reachable only once it is shared. Use getPublicHost. */
333
+ getHost(port) {
334
+ 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.`);
335
+ }
336
+ /** Shares `port` at a public HTTPS address (a Runtime preview) and returns
337
+ * its host, `<port>-<id>.<domain>`, like E2B's getHost. Anyone with the
338
+ * address can reach it; a browser sees a one-time page naming Runtime. */
339
+ async getPublicHost(port) {
340
+ const preview = await guard("sandbox", () => this.#runtime.previews.create(port, { visibility: "public" }));
341
+ return new URL(preview.url).host;
342
+ }
343
+ // ---- what Runtime does differently -----------------------------------
344
+ get pty() {
345
+ throw new NotSupportedError("E2B's pty module", "Use `await sandbox.runtime.terminal({ cols, rows, onData })` for an interactive terminal, or runtime.spawn(cmd, { pty: {} }).");
346
+ }
347
+ get git() {
348
+ throw new NotSupportedError("E2B's git module (deprecated by E2B too)", "Run git with sandbox.commands.run('git ...'); git is installed.");
349
+ }
350
+ getMetrics() {
351
+ return unsupported("Sandbox metrics (getMetrics)", "Runtime reports CPU, memory and charges per sandbox in `npx withruntime usage` and runtime.usage().");
352
+ }
353
+ static getMetrics() {
354
+ return unsupported("Sandbox metrics (getMetrics)", "Runtime reports CPU, memory and charges per sandbox in `npx withruntime usage` and runtime.usage().");
355
+ }
356
+ updateNetwork() {
357
+ return unsupported("E2B's network rules (updateNetwork)", "Use sandbox.runtime.network.set({ internet, allow, deny }).");
358
+ }
359
+ uploadUrl() {
360
+ return unsupported("Signed upload URLs", "Use sandbox.files.write(path, data).");
361
+ }
362
+ downloadUrl() {
363
+ return unsupported("Signed download URLs", "Use sandbox.files.read(path, { format: 'bytes' }).");
364
+ }
365
+ getMcpUrl() {
366
+ throw new NotSupportedError("E2B's MCP gateway", "Runtime's own MCP server is `npx withruntime mcp`.");
367
+ }
368
+ getMcpToken() {
369
+ return unsupported("E2B's MCP gateway", "Runtime's own MCP server is `npx withruntime mcp`.");
370
+ }
371
+ }
372
+ async function resume(runtime, opts) {
373
+ if (opts.onResume === "reboot")
374
+ 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.");
375
+ const current = state(runtime);
376
+ if (current === "stopped")
377
+ throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
378
+ if (current === "paused") {
379
+ await guard("sandbox", () => runtime.wake({
380
+ ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
381
+ ...request(opts),
382
+ }));
383
+ return;
384
+ }
385
+ if (opts.timeoutMs !== undefined) {
386
+ const later = Date.now() + opts.timeoutMs - Date.parse(runtime.info.expiresAt);
387
+ if (later > 1000)
388
+ await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
389
+ }
390
+ }
391
+ async function stop(runtime, opts) {
392
+ if (state(runtime) === "stopped")
393
+ return false;
394
+ await guard("sandbox", () => runtime.stop({ wait: false, ...request(opts) }));
395
+ return true;
396
+ }
397
+ async function extendTo(runtime, timeoutMs, opts) {
398
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
399
+ throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
400
+ const later = Date.now() + timeoutMs - Date.parse(runtime.info.expiresAt);
401
+ if (later < -1000)
402
+ throw new NotSupportedError("Shortening a sandbox's timeout", "Runtime leases only move later. Call kill() when the work is done.");
403
+ if (later > 1000)
404
+ await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
405
+ }
406
+ async function pause(runtime, opts) {
407
+ if (opts.keepMemory === false)
408
+ throw new NotSupportedError("A files-only pause (keepMemory: false)", "Runtime's pause keeps memory and files; leave keepMemory out.");
409
+ const current = state(runtime);
410
+ if (current === "paused")
411
+ return false;
412
+ if (current === "stopped")
413
+ throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
414
+ await guard("sandbox", () => runtime.pause(request(opts)));
415
+ return true;
416
+ }
417
+ const RUNNING = ["starting", "running", "resuming"];
418
+ const PAUSED = ["pausing", "paused"];
419
+ /** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`. */
420
+ export class SandboxPaginator {
421
+ #client;
422
+ #filter;
423
+ #page;
424
+ #hasNext = true;
425
+ constructor(client, opts) {
426
+ this.#client = client;
427
+ const query = opts.query ?? {};
428
+ if (query.template !== undefined && !STOCK_TEMPLATES.has(query.template))
429
+ throw new NotSupportedError("Listing by template", "Filter by metadata instead: give sandboxes a metadata key when you create them.");
430
+ if (query.startedAfter !== undefined)
431
+ throw new NotSupportedError("Listing by start time (startedAfter)", "List them all and filter on getInfo().startedAt.");
432
+ if (opts.order === "desc")
433
+ throw new NotSupportedError("Newest-first listing (order: 'desc')", "Runtime lists oldest first; reverse the list yourself.");
434
+ if (opts.nextToken !== undefined)
435
+ throw new NotSupportedError("Starting a list from a saved nextToken", "Keep the paginator and call nextItems() again.");
436
+ const states = query.state?.length ? query.state : ["running", "paused"];
437
+ this.#filter = {
438
+ state: states.flatMap((one) => (one === "running" ? [...RUNNING] : [...PAUSED])),
439
+ ...(query.metadata && Object.keys(query.metadata).length ? { labels: query.metadata } : {}),
440
+ ...(opts.limit ? { limit: Math.min(opts.limit, 100) } : {}),
441
+ };
442
+ }
443
+ get hasNext() {
444
+ return this.#hasNext;
445
+ }
446
+ get nextToken() {
447
+ return this.#page?.nextCursor ?? undefined;
448
+ }
449
+ async nextItems() {
450
+ if (!this.#hasNext)
451
+ throw new SandboxError("No more items to fetch.");
452
+ const previous = this.#page;
453
+ const page = await guard("other", async () => previous ? await previous.next() : await this.#client.sandboxes.list(this.#filter));
454
+ this.#page = page ?? undefined;
455
+ this.#hasNext = Boolean(page?.hasMore);
456
+ return (page?.data ?? []).map(infoOf);
457
+ }
458
+ }
@@ -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/index.d.ts CHANGED
@@ -12,6 +12,7 @@ import type { RequestOptions } from "./transport.js";
12
12
  export { Runtime, Sandboxes, FeedbackApi, SupportApi, type RuntimeOptions, type SupportReply, } from "./client.js";
13
13
  export { Files, Process, Processes, Terminal } from "./sandbox.js";
14
14
  export { Page } from "./page.js";
15
+ export type { KeyLimits } from "./products/limits.js";
15
16
  export { Snapshots, type Snapshot, type SnapshotOptions } from "./snapshots.js";
16
17
  export { Transport, VERSION, apiOrigin, type RequestOptions, type Call } from "./transport.js";
17
18
  export * from "./errors.js";
package/dist/login.js CHANGED
@@ -3,6 +3,7 @@ import { generateKeyPair, privateDecrypt } from "node:crypto";
3
3
  import { promisify } from "node:util";
4
4
  import { Runtime } from "./index.js";
5
5
  import { connectionOrigins, connectionStore } from "./credentials.js";
6
+ import { describeRoute, envFetch } from "./proxy.js";
6
7
  class ConnectionError extends Error {
7
8
  status;
8
9
  constructor(status, message) {
@@ -71,11 +72,12 @@ async function browser(url) {
71
72
  function connection(env, options) {
72
73
  const origins = connectionOrigins(env);
73
74
  const store = connectionStore(env);
74
- const fetcher = options.fetch ?? fetch;
75
+ const fetcher = options.fetch ?? envFetch;
75
76
  async function request(path, body, key) {
76
77
  let response;
78
+ const address = `${origins.auth}/api/connect/${path}`;
77
79
  try {
78
- response = await fetcher(`${origins.auth}/api/connect/${path}`, {
80
+ response = await fetcher(address, {
79
81
  method: "POST",
80
82
  redirect: "error",
81
83
  signal: AbortSignal.timeout(20_000),
@@ -86,8 +88,15 @@ function connection(env, options) {
86
88
  body: JSON.stringify(body ?? {}),
87
89
  });
88
90
  }
89
- catch {
90
- throw new ConnectionError(503, "Runtime could not be reached. Retry the connection.");
91
+ catch (cause) {
92
+ const route = options.fetch ? { via: "" } : describeRoute(address, cause, env);
93
+ throw new ConnectionError(503, route.hint
94
+ ? `Runtime could not be reached at ${origins.auth}${route.via}. ${route.hint}`
95
+ : "Runtime could not be reached. Retry the connection.");
96
+ }
97
+ if (response.status === 407 && !options.fetch) {
98
+ const route = describeRoute(address, 407, env);
99
+ throw new ConnectionError(503, `Runtime could not be reached at ${origins.auth}${route.via}. ${route.hint ?? ""}`.trim());
91
100
  }
92
101
  if (!response.ok)
93
102
  throw new ConnectionError(response.status, response.status === 401
package/dist/mcp.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
2
2
  * Newline-delimited JSON-RPC in, the same out; every message is forwarded as
3
3
  * it is, so the server's tools, schemas and `instructions` reach the client
4
- * unchanged, and nothing here keeps a second catalogue. No dependencies:
5
- * starting it costs a process and one TLS connection.
4
+ * unchanged, and nothing here keeps a second catalogue. Starting it costs a
5
+ * process and one TLS connection (undici loads only behind a proxy).
6
6
  *
7
7
  * With no key on this machine it does not fail: it serves one tool,
8
8
  * runtime_connect, that walks the agent and its person through the browser
package/dist/mcp.js CHANGED
@@ -2,6 +2,7 @@ import process from "node:process";
2
2
  import { resolveCredential } from "./credentials.js";
3
3
  import { RuntimeError } from "./errors.js";
4
4
  import { beginDeviceLogin } from "./login.js";
5
+ import { envFetch } from "./proxy.js";
5
6
  import { apiOrigin, VERSION } from "./transport.js";
6
7
  const CONNECT_INSTRUCTIONS = "Runtime is not connected on this machine yet. Call runtime_connect: it gives you a link and a code for the person you work for to approve in their browser (no key to copy). Once they have, call runtime_connect again; Runtime's tools then appear (sandboxes, commands, files and the rest).";
7
8
  const connectTool = {
@@ -13,8 +14,8 @@ const connectTool = {
13
14
  /** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
14
15
  * Newline-delimited JSON-RPC in, the same out; every message is forwarded as
15
16
  * it is, so the server's tools, schemas and `instructions` reach the client
16
- * unchanged, and nothing here keeps a second catalogue. No dependencies:
17
- * starting it costs a process and one TLS connection.
17
+ * unchanged, and nothing here keeps a second catalogue. Starting it costs a
18
+ * process and one TLS connection (undici loads only behind a proxy).
18
19
  *
19
20
  * With no key on this machine it does not fail: it serves one tool,
20
21
  * runtime_connect, that walks the agent and its person through the browser
@@ -22,7 +23,7 @@ const connectTool = {
22
23
  * So `claude mcp add runtime -- npx -y withruntime mcp` is the whole
23
24
  * setup. */
24
25
  export async function serveMcp(env = process.env, options = {}) {
25
- const fetcher = options.fetch ?? fetch;
26
+ const fetcher = options.fetch ?? envFetch;
26
27
  let key;
27
28
  try {
28
29
  key = await resolveCredential(env);
@@ -6,11 +6,13 @@ import { sandboxInterpreter } from "./interpreter.js";
6
6
  import { sandboxNetwork } from "./network.js";
7
7
  import { sandboxPreviews } from "./previews.js";
8
8
  import { referrals } from "./referrals.js";
9
+ import { limits } from "./limits.js";
9
10
  import { volumes } from "./volumes.js";
10
11
  export declare const clientExtensions: {
11
12
  images: typeof images;
12
13
  volumes: typeof volumes;
13
14
  referrals: typeof referrals;
15
+ limits: typeof limits;
14
16
  };
15
17
  export declare const sandboxExtensions: {
16
18
  interpreter: typeof sandboxInterpreter;
@@ -4,6 +4,7 @@ import { sandboxInterpreter } from "./interpreter.js";
4
4
  import { sandboxNetwork } from "./network.js";
5
5
  import { sandboxPreviews } from "./previews.js";
6
6
  import { referrals } from "./referrals.js";
7
+ import { limits } from "./limits.js";
7
8
  import { volumes } from "./volumes.js";
8
9
  /* HOW A LANE ADDS A PRODUCT TO THE SDK
9
10
  *
@@ -20,7 +21,7 @@ import { volumes } from "./volumes.js";
20
21
  * 2. Add ONE line to the matching object below. The Runtime client and every
21
22
  * Sandbox pick it up, typed, with the shared auth, retries, keys and errors.
22
23
  */
23
- export const clientExtensions = { images, volumes, referrals };
24
+ export const clientExtensions = { images, volumes, referrals, limits };
24
25
  export const sandboxExtensions = {
25
26
  interpreter: sandboxInterpreter,
26
27
  previews: sandboxPreviews,
@@ -0,0 +1,25 @@
1
+ import type { RequestOptions, Transport } from "../transport.js";
2
+ /** What this key may do, and the daily spending limit on its agent. Money is
3
+ * integer microdollars in strings (1,000,000 = $1). */
4
+ export type KeyLimits = {
5
+ /** full: every action. read: every read and nothing else (a read-only key).
6
+ * selected: the actions the key names. */
7
+ access: "full" | "read" | "selected" | null;
8
+ daily: {
9
+ /** The most this agent may commit in any 24 hours, or null for no limit. */
10
+ limitMicros: string | null;
11
+ /** Committed in the last 24 hours: charges settled plus money on hold. */
12
+ usedMicros: string;
13
+ /** What is left under the limit now, or null for no limit. */
14
+ remainingMicros: string | null;
15
+ window: "24h";
16
+ };
17
+ };
18
+ /** `runtime.limits.get()`: whether this key is read-only, and what its agent
19
+ * may still spend today. An owner sets the limit at
20
+ * https://withruntime.com/account/keys; a key can read it, never change it.
21
+ * Past it, a create, wake, extension or renewal fails with
22
+ * `spending_limit_reached` (HTTP 402). */
23
+ export declare function limits(t: Transport): {
24
+ get: (options?: RequestOptions) => Promise<KeyLimits>;
25
+ };
@@ -0,0 +1,10 @@
1
+ /** `runtime.limits.get()`: whether this key is read-only, and what its agent
2
+ * may still spend today. An owner sets the limit at
3
+ * https://withruntime.com/account/keys; a key can read it, never change it.
4
+ * Past it, a create, wake, extension or renewal fails with
5
+ * `spending_limit_reached` (HTTP 402). */
6
+ export function limits(t) {
7
+ return {
8
+ get: (options) => t.json({ method: "GET", path: "/v1/limits", ...options }),
9
+ };
10
+ }