withruntime 0.10.0 → 0.11.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 +40 -34
- package/E2B.md +56 -57
- package/VERCEL.md +21 -19
- package/compatibility-lock.json +8 -84
- package/dist/cli.js +73 -2
- package/dist/client.d.ts +12 -0
- package/dist/client.js +27 -0
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- package/dist/daytona/computer-use.d.ts +162 -0
- package/dist/daytona/computer-use.js +324 -0
- package/dist/daytona/context.d.ts +10 -0
- package/dist/daytona/context.js +24 -0
- package/dist/daytona/daytona.d.ts +20 -0
- package/dist/daytona/daytona.js +123 -16
- package/dist/daytona/filesystem.js +8 -0
- package/dist/daytona/index.d.ts +2 -26
- package/dist/daytona/index.js +2 -7
- package/dist/daytona/process.d.ts +63 -6
- package/dist/daytona/process.js +224 -23
- package/dist/daytona/sandbox.d.ts +19 -5
- package/dist/daytona/sandbox.js +39 -25
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +5 -1
- package/dist/e2b/commands.d.ts +15 -6
- package/dist/e2b/commands.js +83 -18
- package/dist/e2b/errors.d.ts +7 -0
- package/dist/e2b/errors.js +20 -0
- package/dist/e2b/filesystem.d.ts +2 -1
- package/dist/e2b/filesystem.js +43 -6
- package/dist/e2b/index.d.ts +2 -2
- package/dist/e2b/index.js +1 -1
- package/dist/e2b/pty.js +6 -3
- package/dist/e2b/sandbox.d.ts +15 -5
- package/dist/e2b/sandbox.js +190 -28
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/index.d.ts +1 -0
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/network-cli.js +28 -0
- package/dist/products/browser.d.ts +60 -0
- package/dist/products/browser.js +37 -0
- package/dist/products/desktop.d.ts +2 -2
- package/dist/products/desktop.js +2 -2
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/sandbox.d.ts +22 -0
- package/dist/sandbox.js +47 -0
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/types.d.ts +5 -2
- package/dist/vercel/index.d.ts +1 -7
- package/dist/vercel/index.js +1 -3
- package/dist/vercel/sandbox-user.d.ts +70 -0
- package/dist/vercel/sandbox-user.js +206 -0
- package/dist/vercel/sandbox.d.ts +47 -12
- package/dist/vercel/sandbox.js +201 -42
- package/package.json +1 -1
package/dist/e2b/sandbox.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clientFor, request, } from "./client.js";
|
|
2
2
|
import { Commands } from "./commands.js";
|
|
3
|
-
import { guard, InvalidArgumentError, NotSupportedError, SandboxError, SandboxNotFoundError, TemplateError, } from "./errors.js";
|
|
3
|
+
import { guard, InvalidArgumentError, NotSupportedError, PublicPreviewNotAllowedError, SandboxError, SandboxNotFoundError, TemplateError, } from "./errors.js";
|
|
4
4
|
import { Filesystem } from "./filesystem.js";
|
|
5
5
|
import { Pty } from "./pty.js";
|
|
6
6
|
/* E2B's Sandbox over Runtime's. Every call is one or a few calls of the
|
|
@@ -14,16 +14,23 @@ export const DEFAULT_TIMEOUT_MS = 300_000;
|
|
|
14
14
|
/** Runtime's lease bounds (MAX_TIMEOUT_SECONDS in the API). */
|
|
15
15
|
const MIN_LEASE_SECONDS = 60;
|
|
16
16
|
const MAX_LEASE_SECONDS = 3600;
|
|
17
|
+
/** E2B's longest sandbox timeout, 24 hours (its Pro plan). */
|
|
18
|
+
const LONGEST_TIMEOUT_MS = 86_400_000;
|
|
17
19
|
/** E2B template names that mean "the stock environment": Runtime's default image. */
|
|
18
20
|
const STOCK_TEMPLATES = new Set(["base", "code-interpreter-v1", "code-interpreter"]);
|
|
19
21
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
22
|
let warnedShortLease = false;
|
|
23
|
+
/** E2B's own bound since 2.52.0: refused before anything happens. */
|
|
24
|
+
const MAX_FORK_COUNT = 20;
|
|
25
|
+
function checkForkCount(count) {
|
|
26
|
+
if (count !== undefined && (!Number.isInteger(count) || count < 1 || count > MAX_FORK_COUNT))
|
|
27
|
+
throw new InvalidArgumentError(`count must be an integer between 1 and ${MAX_FORK_COUNT}`);
|
|
28
|
+
}
|
|
29
|
+
/** The first lease for `timeoutMs`: up to an hour; `keepUntil` carries a
|
|
30
|
+
* longer timeout on from there. */
|
|
21
31
|
function leaseSeconds(timeoutMs) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const seconds = Math.ceil(timeoutMs / 1000);
|
|
25
|
-
if (seconds > MAX_LEASE_SECONDS)
|
|
26
|
-
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.");
|
|
32
|
+
checkTimeout(timeoutMs);
|
|
33
|
+
const seconds = Math.min(Math.ceil(timeoutMs / 1000), MAX_LEASE_SECONDS);
|
|
27
34
|
if (seconds < MIN_LEASE_SECONDS) {
|
|
28
35
|
if (!warnedShortLease && typeof process !== "undefined") {
|
|
29
36
|
warnedShortLease = true;
|
|
@@ -33,6 +40,85 @@ function leaseSeconds(timeoutMs) {
|
|
|
33
40
|
}
|
|
34
41
|
return seconds;
|
|
35
42
|
}
|
|
43
|
+
function checkTimeout(timeoutMs) {
|
|
44
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
45
|
+
throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
|
|
46
|
+
if (timeoutMs > LONGEST_TIMEOUT_MS)
|
|
47
|
+
throw new InvalidArgumentError(`timeoutMs is at most 24 hours (86_400_000 ms), as on E2B, not ${timeoutMs}.`);
|
|
48
|
+
}
|
|
49
|
+
/* Timeouts over an hour. Runtime's lease reaches at most an hour ahead of
|
|
50
|
+
now, and E2B's timeout up to a day. While this process runs, the adapter
|
|
51
|
+
moves the lease on every five minutes, as far as an hour ahead and never
|
|
52
|
+
past the end asked for. One keeper per sandbox, whichever object asked.
|
|
53
|
+
If the process ends first, the sandbox ends when its lease does, at most
|
|
54
|
+
an hour later; it is never kept past the time asked for. */
|
|
55
|
+
const KEEP_EVERY_MS = 5 * 60_000;
|
|
56
|
+
/** Kept under the API's hour, for the clocks and the trip. */
|
|
57
|
+
const KEEP_AHEAD_MS = (MAX_LEASE_SECONDS - 30) * 1000;
|
|
58
|
+
const keepers = new Map();
|
|
59
|
+
let warnedKeeper = false;
|
|
60
|
+
/** Keeps `runtime` until `until` (epoch ms), or stops keeping it when that
|
|
61
|
+
* is within the hour its lease can already reach. */
|
|
62
|
+
function keepUntil(runtime, until) {
|
|
63
|
+
const keeper = keepers.get(runtime.id);
|
|
64
|
+
if (until - Date.now() <= MAX_LEASE_SECONDS * 1000)
|
|
65
|
+
return stopKeeping(runtime.id);
|
|
66
|
+
if (keeper) {
|
|
67
|
+
keeper.until = until;
|
|
68
|
+
keeper.runtime = runtime;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const made = { until, runtime };
|
|
72
|
+
keepers.set(runtime.id, made);
|
|
73
|
+
schedule(made);
|
|
74
|
+
}
|
|
75
|
+
function stopKeeping(sandboxId) {
|
|
76
|
+
const keeper = keepers.get(sandboxId);
|
|
77
|
+
if (keeper?.timer !== undefined)
|
|
78
|
+
clearTimeout(keeper.timer);
|
|
79
|
+
keepers.delete(sandboxId);
|
|
80
|
+
}
|
|
81
|
+
function schedule(keeper) {
|
|
82
|
+
keeper.timer = setTimeout(() => void renew(keeper), KEEP_EVERY_MS);
|
|
83
|
+
// Never what keeps a Node or Bun process running.
|
|
84
|
+
keeper.timer.unref?.();
|
|
85
|
+
}
|
|
86
|
+
async function renew(keeper) {
|
|
87
|
+
const id = keeper.runtime.id;
|
|
88
|
+
if (keepers.get(id) !== keeper)
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
await keeper.runtime.refresh();
|
|
92
|
+
const current = state(keeper.runtime);
|
|
93
|
+
if (current === "stopped")
|
|
94
|
+
return stopKeeping(id);
|
|
95
|
+
if (current === "running") {
|
|
96
|
+
const end = Date.parse(keeper.runtime.info.expiresAt);
|
|
97
|
+
const want = Math.min(keeper.until, Date.now() + KEEP_AHEAD_MS);
|
|
98
|
+
const later = Math.floor((want - end) / 1000);
|
|
99
|
+
if (later >= 1)
|
|
100
|
+
await keeper.runtime.extend(later);
|
|
101
|
+
if (Date.parse(keeper.runtime.info.expiresAt) >= keeper.until - 1000)
|
|
102
|
+
return stopKeeping(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!warnedKeeper && typeof process !== "undefined" && process.emitWarning) {
|
|
107
|
+
warnedKeeper = true;
|
|
108
|
+
process.emitWarning(`Could not extend sandbox ${id}'s lease toward its timeout: ${error instanceof Error ? error.message : String(error)}. Trying again in five minutes.`, { code: "RUNTIME_E2B_LEASE" });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (keepers.get(id) === keeper)
|
|
112
|
+
schedule(keeper);
|
|
113
|
+
}
|
|
114
|
+
/** Test hook: renew every kept lease now, as the five-minute timer would. */
|
|
115
|
+
export async function renewLeases() {
|
|
116
|
+
await Promise.all([...keepers.values()].map((keeper) => renew(keeper)));
|
|
117
|
+
}
|
|
118
|
+
/** Test hook: the end each kept sandbox is kept until. */
|
|
119
|
+
export function keptLeases() {
|
|
120
|
+
return new Map([...keepers].map(([id, keeper]) => [id, keeper.until]));
|
|
121
|
+
}
|
|
36
122
|
function onLeaseEnd(lifecycle) {
|
|
37
123
|
if (!lifecycle)
|
|
38
124
|
return "stop";
|
|
@@ -215,6 +301,7 @@ export class Sandbox {
|
|
|
215
301
|
options.signal?.throwIfAborted();
|
|
216
302
|
refuseCreate(opts);
|
|
217
303
|
const client = clientFor(opts);
|
|
304
|
+
const asked = Date.now() + (opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
218
305
|
const input = {
|
|
219
306
|
timeoutSeconds: leaseSeconds(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
220
307
|
onLeaseEnd: onLeaseEnd(opts.lifecycle),
|
|
@@ -237,6 +324,7 @@ export class Sandbox {
|
|
|
237
324
|
...opts.runtime?.create,
|
|
238
325
|
};
|
|
239
326
|
const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
|
|
327
|
+
keepUntil(runtime, asked);
|
|
240
328
|
return new this(runtime, client, opts.requestTimeoutMs);
|
|
241
329
|
}
|
|
242
330
|
/** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
|
|
@@ -261,6 +349,7 @@ export class Sandbox {
|
|
|
261
349
|
/** Stops the sandbox. False when it was not found or had already ended. */
|
|
262
350
|
static async kill(sandboxId, opts = {}) {
|
|
263
351
|
const client = clientFor(opts);
|
|
352
|
+
stopKeeping(sandboxId);
|
|
264
353
|
try {
|
|
265
354
|
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
266
355
|
return await stop(runtime, opts);
|
|
@@ -272,6 +361,7 @@ export class Sandbox {
|
|
|
272
361
|
}
|
|
273
362
|
}
|
|
274
363
|
async kill(opts = {}) {
|
|
364
|
+
stopKeeping(this.sandboxId);
|
|
275
365
|
try {
|
|
276
366
|
return await stop(this.#runtime, opts);
|
|
277
367
|
}
|
|
@@ -281,8 +371,9 @@ export class Sandbox {
|
|
|
281
371
|
throw error;
|
|
282
372
|
}
|
|
283
373
|
}
|
|
284
|
-
/** Sets the sandbox to end `timeoutMs` from now
|
|
285
|
-
*
|
|
374
|
+
/** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
|
|
375
|
+
* hour the lease is moved on while this process runs. Runtime cannot end a
|
|
376
|
+
* lease early, so an end sooner than the lease's is refused. */
|
|
286
377
|
static async setTimeout(sandboxId, timeoutMs, opts = {}) {
|
|
287
378
|
const client = clientFor(opts);
|
|
288
379
|
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
@@ -330,6 +421,7 @@ export class Sandbox {
|
|
|
330
421
|
/** Copies of this sandbox, memory and all. While Runtime's forks are
|
|
331
422
|
* switched off this throws NotSupportedError with Runtime's own words. */
|
|
332
423
|
static async fork(sandboxId, opts = {}) {
|
|
424
|
+
checkForkCount(opts.count);
|
|
333
425
|
const { count: _count, timeoutMs: _timeoutMs, ...connection } = opts;
|
|
334
426
|
const source = await this.connect(sandboxId, connection);
|
|
335
427
|
return source.fork(opts);
|
|
@@ -339,6 +431,7 @@ export class Sandbox {
|
|
|
339
431
|
// A fork's lease is Runtime's to set; one shorter than asked could not be
|
|
340
432
|
// honoured after the forks exist, so this is refused before any are made.
|
|
341
433
|
throw new NotSupportedError("A timeout for forks (fork timeoutMs)", "Fork without it, then call setTimeout(ms) on each fork.");
|
|
434
|
+
checkForkCount(opts.count);
|
|
342
435
|
const copies = await guard("sandbox", () => this.#runtime.fork({ count: opts.count ?? 1, ...request(opts) }));
|
|
343
436
|
// Copies keep the source's environment on Runtime's side.
|
|
344
437
|
const Kind = this.constructor;
|
|
@@ -378,6 +471,10 @@ export class Sandbox {
|
|
|
378
471
|
getHost(port) {
|
|
379
472
|
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
380
473
|
throw new InvalidArgumentError(`port must be a whole number from 1 to 65535, not ${port}.`);
|
|
474
|
+
// A trial sandbox's ports are private: a host alone would answer 404 or
|
|
475
|
+
// 401 to everyone. Say so now, with the address that works.
|
|
476
|
+
if (this.#runtime.info.funding === "trial")
|
|
477
|
+
throw new PublicPreviewNotAllowedError(this.sandboxId, port);
|
|
381
478
|
void this.#share(port).catch((error) => {
|
|
382
479
|
if (typeof process !== "undefined" && process.emitWarning)
|
|
383
480
|
process.emitWarning(`Sharing port ${port} for getHost failed: ${error instanceof Error ? error.message : String(error)}. getPublicHost(${port}) says why.`, { code: "RUNTIME_E2B_GET_HOST" });
|
|
@@ -388,6 +485,8 @@ export class Sandbox {
|
|
|
388
485
|
* its host, `<port>-<id>.<domain>`, once the share has landed. Anyone with
|
|
389
486
|
* the address can reach it; a browser sees a one-time page naming Runtime. */
|
|
390
487
|
async getPublicHost(port) {
|
|
488
|
+
if (this.#runtime.info.funding === "trial")
|
|
489
|
+
throw new PublicPreviewNotAllowedError(this.sandboxId, port);
|
|
391
490
|
return this.#share(port);
|
|
392
491
|
}
|
|
393
492
|
#share(port) {
|
|
@@ -439,18 +538,21 @@ async function resume(runtime, opts) {
|
|
|
439
538
|
const current = state(runtime);
|
|
440
539
|
if (current === "stopped")
|
|
441
540
|
throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
|
|
541
|
+
const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
|
|
442
542
|
if (current === "paused") {
|
|
443
543
|
await guard("sandbox", () => runtime.wake({
|
|
444
544
|
...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
|
|
445
545
|
...request(opts),
|
|
446
546
|
}));
|
|
447
|
-
return;
|
|
448
547
|
}
|
|
449
|
-
if (opts.timeoutMs !== undefined) {
|
|
450
|
-
|
|
548
|
+
else if (opts.timeoutMs !== undefined) {
|
|
549
|
+
checkTimeout(opts.timeoutMs);
|
|
550
|
+
const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - Date.parse(runtime.info.expiresAt);
|
|
451
551
|
if (later > 1000)
|
|
452
552
|
await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
|
|
453
553
|
}
|
|
554
|
+
if (asked !== undefined && asked > (keepers.get(runtime.id)?.until ?? 0))
|
|
555
|
+
keepUntil(runtime, asked);
|
|
454
556
|
}
|
|
455
557
|
async function stop(runtime, opts) {
|
|
456
558
|
if (state(runtime) === "stopped")
|
|
@@ -459,13 +561,15 @@ async function stop(runtime, opts) {
|
|
|
459
561
|
return true;
|
|
460
562
|
}
|
|
461
563
|
async function extendTo(runtime, timeoutMs, opts) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
throw new NotSupportedError("
|
|
564
|
+
checkTimeout(timeoutMs);
|
|
565
|
+
const asked = Date.now() + timeoutMs;
|
|
566
|
+
const end = Date.parse(runtime.info.expiresAt);
|
|
567
|
+
if (asked - end < -1000)
|
|
568
|
+
throw new NotSupportedError("Ending a sandbox sooner than its lease (a shorter setTimeout)", "Runtime cannot end a lease early. Call kill() when the work is done.");
|
|
569
|
+
const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - end;
|
|
467
570
|
if (later > 1000)
|
|
468
571
|
await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
|
|
572
|
+
keepUntil(runtime, asked);
|
|
469
573
|
}
|
|
470
574
|
async function pause(runtime, opts) {
|
|
471
575
|
if (opts.keepMemory === false)
|
|
@@ -480,51 +584,109 @@ async function pause(runtime, opts) {
|
|
|
480
584
|
}
|
|
481
585
|
const RUNNING = ["starting", "running", "resuming"];
|
|
482
586
|
const PAUSED = ["pausing", "paused"];
|
|
483
|
-
/** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`.
|
|
587
|
+
/** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`.
|
|
588
|
+
* Runtime filters by state and metadata itself. A template, `startedAfter`,
|
|
589
|
+
* newest-first order or a saved `nextToken` are done here: every match is
|
|
590
|
+
* read once, then served a page at a time. */
|
|
484
591
|
export class SandboxPaginator {
|
|
485
592
|
#client;
|
|
486
593
|
#request;
|
|
487
594
|
#filter;
|
|
595
|
+
#opts;
|
|
596
|
+
#pageSize;
|
|
597
|
+
#local;
|
|
488
598
|
#page;
|
|
599
|
+
#all;
|
|
600
|
+
#offset;
|
|
489
601
|
#hasNext = true;
|
|
490
602
|
constructor(client, opts) {
|
|
491
603
|
this.#client = client;
|
|
492
604
|
this.#request = request(opts);
|
|
605
|
+
this.#opts = opts;
|
|
493
606
|
const query = opts.query ?? {};
|
|
494
|
-
if (
|
|
495
|
-
throw new
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
607
|
+
if (opts.limit !== undefined && (!Number.isSafeInteger(opts.limit) || opts.limit < 1))
|
|
608
|
+
throw new InvalidArgumentError(`limit must be a positive whole number, not ${opts.limit}.`);
|
|
609
|
+
this.#pageSize = opts.limit ?? 100;
|
|
610
|
+
this.#offset = opts.nextToken === undefined ? 0 : tokenOffset(opts.nextToken);
|
|
611
|
+
this.#local =
|
|
612
|
+
query.template !== undefined ||
|
|
613
|
+
query.startedAfter !== undefined ||
|
|
614
|
+
opts.order === "desc" ||
|
|
615
|
+
opts.nextToken !== undefined;
|
|
502
616
|
const states = query.state?.length ? query.state : ["running", "paused"];
|
|
503
617
|
this.#filter = {
|
|
504
618
|
state: states.flatMap((one) => (one === "running" ? [...RUNNING] : [...PAUSED])),
|
|
505
619
|
...(query.metadata && Object.keys(query.metadata).length ? { labels: query.metadata } : {}),
|
|
506
|
-
|
|
620
|
+
limit: this.#local ? 100 : Math.min(this.#pageSize, 100),
|
|
507
621
|
};
|
|
508
622
|
}
|
|
509
623
|
get hasNext() {
|
|
510
624
|
return this.#hasNext;
|
|
511
625
|
}
|
|
626
|
+
/** Where the next page starts; `Sandbox.list({ nextToken })` resumes there. */
|
|
512
627
|
get nextToken() {
|
|
513
|
-
return this.#
|
|
628
|
+
return this.#hasNext && this.#offset > 0 ? `runtime:${this.#offset}` : undefined;
|
|
514
629
|
}
|
|
515
630
|
async nextItems() {
|
|
516
631
|
this.#request.signal?.throwIfAborted();
|
|
517
632
|
if (!this.#hasNext)
|
|
518
633
|
throw new SandboxError("No more items to fetch.");
|
|
634
|
+
if (this.#local) {
|
|
635
|
+
const all = (this.#all ??= await guard("other", () => this.#everyMatch()));
|
|
636
|
+
const items = all.slice(this.#offset, this.#offset + this.#pageSize);
|
|
637
|
+
this.#offset += items.length;
|
|
638
|
+
this.#hasNext = this.#offset < all.length;
|
|
639
|
+
return items;
|
|
640
|
+
}
|
|
519
641
|
const previous = this.#page;
|
|
520
642
|
const page = await guard("other", async () => previous
|
|
521
643
|
? await previous.next()
|
|
522
644
|
: await this.#client.sandboxes.list(this.#filter, this.#request));
|
|
523
645
|
this.#page = page ?? undefined;
|
|
524
646
|
this.#hasNext = Boolean(page?.hasMore);
|
|
525
|
-
|
|
647
|
+
const items = (page?.data ?? []).map(infoOf);
|
|
648
|
+
this.#offset += items.length;
|
|
649
|
+
return items;
|
|
650
|
+
}
|
|
651
|
+
async #everyMatch() {
|
|
652
|
+
const query = this.#opts.query ?? {};
|
|
653
|
+
const templates = query.template === undefined
|
|
654
|
+
? undefined
|
|
655
|
+
: await templateIds(this.#client, query.template, this.#request);
|
|
656
|
+
const after = query.startedAfter?.getTime();
|
|
657
|
+
const all = [];
|
|
658
|
+
let page = await this.#client.sandboxes.list(this.#filter, this.#request);
|
|
659
|
+
while (page) {
|
|
660
|
+
for (const one of page.data) {
|
|
661
|
+
const info = infoOf(one);
|
|
662
|
+
if (templates && !templates.has(info.templateId))
|
|
663
|
+
continue;
|
|
664
|
+
if (after !== undefined && info.startedAt.getTime() < after)
|
|
665
|
+
continue;
|
|
666
|
+
all.push(info);
|
|
667
|
+
}
|
|
668
|
+
page = page.hasMore ? await page.next() : null;
|
|
669
|
+
}
|
|
670
|
+
const sign = this.#opts.order === "desc" ? -1 : 1;
|
|
671
|
+
return all.sort((a, b) => sign * (a.startedAt.getTime() - b.startedAt.getTime()));
|
|
526
672
|
}
|
|
527
673
|
}
|
|
674
|
+
function tokenOffset(token) {
|
|
675
|
+
const match = /^runtime:(\d+)$/.exec(token);
|
|
676
|
+
if (!match)
|
|
677
|
+
throw new InvalidArgumentError(`"${token}" is not a nextToken this package gave; pass the paginator's own nextToken.`);
|
|
678
|
+
return Number(match[1]);
|
|
679
|
+
}
|
|
680
|
+
/** The template ids a template name stands for: "base" for the stock names,
|
|
681
|
+
* the id itself for a UUID, and every Runtime image of that name. */
|
|
682
|
+
async function templateIds(client, template, options) {
|
|
683
|
+
if (STOCK_TEMPLATES.has(template))
|
|
684
|
+
return new Set(["base"]);
|
|
685
|
+
if (UUID.test(template))
|
|
686
|
+
return new Set([template]);
|
|
687
|
+
const page = await client.images.list({ name: template, limit: 100 }, options);
|
|
688
|
+
return new Set(page.data.map((image) => image.id));
|
|
689
|
+
}
|
|
528
690
|
async function e2bMetrics(runtime, opts) {
|
|
529
691
|
const start = opts.start === undefined ? Date.now() - 15 * 60_000 : new Date(opts.start).getTime();
|
|
530
692
|
const end = opts.end === undefined ? Date.now() : new Date(opts.end).getTime();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FileEntry } from "../types.js";
|
|
2
|
+
import type { RequestOptions } from "../transport.js";
|
|
3
|
+
import type { SandboxContext, Username } from "./commands.js";
|
|
4
|
+
/** The Linux user a call runs as, or undefined for the sandbox's own. Refuses
|
|
5
|
+
* a user the sandbox does not have, once per user and sandbox. */
|
|
6
|
+
export declare function runAs(ctx: SandboxContext, user: Username | undefined, options: RequestOptions): Promise<string | undefined>;
|
|
7
|
+
/** `bash -c script` as `user`, keeping the environment Runtime gives the
|
|
8
|
+
* command. Without a `cwd` it starts in the user's home, as E2B's does. */
|
|
9
|
+
export declare function commandAs(user: string, script: string, cwdGiven: boolean): string[];
|
|
10
|
+
/** An interactive login shell as `user`, for a PTY. */
|
|
11
|
+
export declare function shellAs(user: string, cwdGiven: boolean): string[];
|
|
12
|
+
/** E2B's file calls as another Linux user: each one a short script run with
|
|
13
|
+
* `sudo -u`, since Runtime's file API acts as the sandbox's own user. Output
|
|
14
|
+
* streams, so a read of any size comes back whole. */
|
|
15
|
+
export declare class FilesAs {
|
|
16
|
+
#private;
|
|
17
|
+
private readonly ctx;
|
|
18
|
+
private readonly user;
|
|
19
|
+
private readonly options;
|
|
20
|
+
constructor(ctx: SandboxContext, user: string, options: RequestOptions);
|
|
21
|
+
read(path: string): Promise<Uint8Array>;
|
|
22
|
+
write(path: string, bytes: Uint8Array): Promise<void>;
|
|
23
|
+
list(path: string, depth: number): Promise<FileEntry[]>;
|
|
24
|
+
stat(path: string): Promise<FileEntry>;
|
|
25
|
+
exists(path: string): Promise<boolean>;
|
|
26
|
+
/** False when it was there already. */
|
|
27
|
+
makeDir(path: string): Promise<boolean>;
|
|
28
|
+
rename(from: string, to: string): Promise<void>;
|
|
29
|
+
remove(path: string): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { FileNotFoundError, guard, InvalidArgumentError, SandboxError } from "./errors.js";
|
|
2
|
+
/* E2B's `user` option, on Runtime. A sandbox's own user is `runtime`, with
|
|
3
|
+
passwordless sudo; E2B's default user is called "user". So "user" (or no
|
|
4
|
+
user) runs as the sandbox's own user, and any other name runs through
|
|
5
|
+
`sudo -u <name>`. The user must already exist: the adapter never makes one,
|
|
6
|
+
and a name the sandbox does not know is refused before anything runs. */
|
|
7
|
+
const NAME = /^[a-z_][a-z0-9_.-]{0,31}$/i;
|
|
8
|
+
/** Exit codes the scripts below use to say what went wrong. */
|
|
9
|
+
const MISSING = 44;
|
|
10
|
+
const NOT_DIRECTORY = 45;
|
|
11
|
+
const IS_DIRECTORY = 46;
|
|
12
|
+
const EXISTS = 47;
|
|
13
|
+
/** Base64 text per exec: under the 1 MiB an exec's input may carry. */
|
|
14
|
+
const WRITE_CHUNK = 786_432;
|
|
15
|
+
/** Only the caller's signal: an exec's own `timeoutMs` is the command's. */
|
|
16
|
+
const signalOf = (options) => (options.signal ? { signal: options.signal } : {});
|
|
17
|
+
const checked = new WeakMap();
|
|
18
|
+
/** The Linux user a call runs as, or undefined for the sandbox's own. Refuses
|
|
19
|
+
* a user the sandbox does not have, once per user and sandbox. */
|
|
20
|
+
export async function runAs(ctx, user, options) {
|
|
21
|
+
if (user === undefined || user === "user")
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof user !== "string" || !NAME.test(user))
|
|
24
|
+
throw new InvalidArgumentError(`"${String(user)}" is not a Linux user name.`);
|
|
25
|
+
if (user === "root")
|
|
26
|
+
return user;
|
|
27
|
+
let users = checked.get(ctx);
|
|
28
|
+
if (!users)
|
|
29
|
+
checked.set(ctx, (users = new Map()));
|
|
30
|
+
let check = users.get(user);
|
|
31
|
+
if (!check) {
|
|
32
|
+
check = (async () => {
|
|
33
|
+
const found = await guard("sandbox", () => ctx.runtime.exec(["id", "-u", "--", user], signalOf(options)));
|
|
34
|
+
if (found.exitCode !== 0)
|
|
35
|
+
throw new InvalidArgumentError(`This sandbox has no user "${user}", and Runtime never creates one for you. ` +
|
|
36
|
+
`Make it first, as root: sandbox.commands.run("useradd -m ${user}", { user: "root" }). ` +
|
|
37
|
+
`"user" is the sandbox's own user and "root" is root.`);
|
|
38
|
+
})();
|
|
39
|
+
users.set(user, check);
|
|
40
|
+
check.catch(() => users.delete(user));
|
|
41
|
+
}
|
|
42
|
+
await check;
|
|
43
|
+
return user;
|
|
44
|
+
}
|
|
45
|
+
/** `bash -c script` as `user`, keeping the environment Runtime gives the
|
|
46
|
+
* command. Without a `cwd` it starts in the user's home, as E2B's does. */
|
|
47
|
+
export function commandAs(user, script, cwdGiven) {
|
|
48
|
+
return [
|
|
49
|
+
"sudo",
|
|
50
|
+
"-n",
|
|
51
|
+
"-E",
|
|
52
|
+
"-H",
|
|
53
|
+
"-u",
|
|
54
|
+
user,
|
|
55
|
+
"--",
|
|
56
|
+
"/bin/bash",
|
|
57
|
+
"-c",
|
|
58
|
+
cwdGiven ? script : `cd ~ 2>/dev/null\n${script}`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
/** An interactive login shell as `user`, for a PTY. */
|
|
62
|
+
export function shellAs(user, cwdGiven) {
|
|
63
|
+
return commandAs(user, "exec /bin/bash -i -l", cwdGiven);
|
|
64
|
+
}
|
|
65
|
+
/** One `sh -c` script as `user`, with its arguments passed apart, never
|
|
66
|
+
* pasted into the script. */
|
|
67
|
+
function script(user, text, ...args) {
|
|
68
|
+
return ["sudo", "-n", "-u", user, "--", "/bin/sh", "-c", text, "sh", ...args];
|
|
69
|
+
}
|
|
70
|
+
/** Each entry `find -printf` prints: eight fields, each ended by NUL. */
|
|
71
|
+
const FIELDS = "%y\\0%s\\0%m\\0%u\\0%g\\0%T@\\0%p\\0%l\\0";
|
|
72
|
+
const TYPES = { f: "file", d: "directory", l: "symlink" };
|
|
73
|
+
function entries(output) {
|
|
74
|
+
const fields = output.split("\0");
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i + 8 <= fields.length; i += 8) {
|
|
77
|
+
const [kind, size, mode, owner, group, mtime, path, link] = fields.slice(i, i + 8);
|
|
78
|
+
out.push({
|
|
79
|
+
name: path.split("/").filter(Boolean).pop() ?? path,
|
|
80
|
+
path,
|
|
81
|
+
type: TYPES[kind] ?? "other",
|
|
82
|
+
size: Number(size) || 0,
|
|
83
|
+
mode: `0${mode}`,
|
|
84
|
+
modifiedAt: new Date(Math.round(Number(mtime) * 1000)).toISOString(),
|
|
85
|
+
owner,
|
|
86
|
+
group,
|
|
87
|
+
...(kind === "l" && link ? { symlinkTarget: link } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** E2B's file calls as another Linux user: each one a short script run with
|
|
93
|
+
* `sudo -u`, since Runtime's file API acts as the sandbox's own user. Output
|
|
94
|
+
* streams, so a read of any size comes back whole. */
|
|
95
|
+
export class FilesAs {
|
|
96
|
+
ctx;
|
|
97
|
+
user;
|
|
98
|
+
options;
|
|
99
|
+
constructor(ctx, user, options) {
|
|
100
|
+
this.ctx = ctx;
|
|
101
|
+
this.user = user;
|
|
102
|
+
this.options = options;
|
|
103
|
+
}
|
|
104
|
+
async #run(argv, stdin) {
|
|
105
|
+
const result = await guard("sandbox", () => this.ctx.runtime.exec(argv, {
|
|
106
|
+
...(stdin === undefined ? {} : { stdin }),
|
|
107
|
+
// A callback makes the exec stream, so its output is never cut.
|
|
108
|
+
onStdout: () => undefined,
|
|
109
|
+
...signalOf(this.options),
|
|
110
|
+
}));
|
|
111
|
+
if (result.stdoutTruncated)
|
|
112
|
+
throw new SandboxError("Part of the answer was lost on the way; try again.");
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
#fail(path, result) {
|
|
116
|
+
if (result.exitCode === MISSING)
|
|
117
|
+
throw new FileNotFoundError(`${path} does not exist.`);
|
|
118
|
+
if (result.exitCode === NOT_DIRECTORY)
|
|
119
|
+
throw new InvalidArgumentError(`${path} is not a directory.`);
|
|
120
|
+
if (result.exitCode === IS_DIRECTORY)
|
|
121
|
+
throw new InvalidArgumentError(`${path} is a directory.`);
|
|
122
|
+
throw new SandboxError(`As the user "${this.user}": ${result.stderr.trim() || `exit status ${result.exitCode}`}`);
|
|
123
|
+
}
|
|
124
|
+
async read(path) {
|
|
125
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || exit ${MISSING}; [ -d "$1" ] && exit ${IS_DIRECTORY}; exec base64 -w 0 -- "$1"`, path));
|
|
126
|
+
if (result.exitCode !== 0)
|
|
127
|
+
this.#fail(path, result);
|
|
128
|
+
return Uint8Array.from(atob(result.stdout.trim()), (c) => c.charCodeAt(0));
|
|
129
|
+
}
|
|
130
|
+
async write(path, bytes) {
|
|
131
|
+
let text = "";
|
|
132
|
+
for (let i = 0; i < bytes.length; i += 32_768)
|
|
133
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 32_768));
|
|
134
|
+
const encoded = btoa(text);
|
|
135
|
+
let offset = 0;
|
|
136
|
+
do {
|
|
137
|
+
const chunk = encoded.slice(offset, offset + WRITE_CHUNK);
|
|
138
|
+
const result = await this.#run(script(this.user, offset === 0
|
|
139
|
+
? `mkdir -p -- "$(dirname -- "$1")" && base64 -d > "$1"`
|
|
140
|
+
: `base64 -d >> "$1"`, path), chunk);
|
|
141
|
+
if (result.exitCode !== 0)
|
|
142
|
+
this.#fail(path, result);
|
|
143
|
+
offset += WRITE_CHUNK;
|
|
144
|
+
} while (offset < encoded.length);
|
|
145
|
+
}
|
|
146
|
+
async list(path, depth) {
|
|
147
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || exit ${MISSING}; [ -d "$1" ] || exit ${NOT_DIRECTORY}; ` +
|
|
148
|
+
`exec find "$1" -mindepth 1 -maxdepth "$2" -printf '${FIELDS}'`, path, String(depth)));
|
|
149
|
+
if (result.exitCode !== 0)
|
|
150
|
+
this.#fail(path, result);
|
|
151
|
+
return entries(result.stdout);
|
|
152
|
+
}
|
|
153
|
+
async stat(path) {
|
|
154
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ] || exit ${MISSING}; exec find "$1" -maxdepth 0 -printf '${FIELDS}'`, path));
|
|
155
|
+
if (result.exitCode !== 0)
|
|
156
|
+
this.#fail(path, result);
|
|
157
|
+
const [entry] = entries(result.stdout);
|
|
158
|
+
if (!entry)
|
|
159
|
+
throw new FileNotFoundError(`${path} does not exist.`);
|
|
160
|
+
return entry;
|
|
161
|
+
}
|
|
162
|
+
async exists(path) {
|
|
163
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ]`, path));
|
|
164
|
+
return result.exitCode === 0;
|
|
165
|
+
}
|
|
166
|
+
/** False when it was there already. */
|
|
167
|
+
async makeDir(path) {
|
|
168
|
+
const result = await this.#run(script(this.user, `[ -d "$1" ] && exit ${EXISTS}; mkdir -p -- "$1"`, path));
|
|
169
|
+
if (result.exitCode === EXISTS)
|
|
170
|
+
return false;
|
|
171
|
+
if (result.exitCode !== 0)
|
|
172
|
+
this.#fail(path, result);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
async rename(from, to) {
|
|
176
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ] || exit ${MISSING}; mv -fT -- "$1" "$2"`, from, to));
|
|
177
|
+
if (result.exitCode !== 0)
|
|
178
|
+
this.#fail(from, result);
|
|
179
|
+
}
|
|
180
|
+
async remove(path) {
|
|
181
|
+
const result = await this.#run(script(this.user, `rm -rf -- "$1"`, path));
|
|
182
|
+
if (result.exitCode !== 0)
|
|
183
|
+
this.#fail(path, result);
|
|
184
|
+
}
|
|
185
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type { TailscaleJoin, TailscaleJoined, TailscaleNode, TailscaleStatus, }
|
|
|
25
25
|
export type { Volume, VolumeBackup, CreateVolume, VolumeAttachment } from "./products/volumes.js";
|
|
26
26
|
export type { CreateJob, Job, JobCompute, JobDefinition, JobLogs, JobRun, JobRunState, JobSchedule, } from "./products/jobs.js";
|
|
27
27
|
export type { CheckedDomain, DnsRecord, Domain, EgressAddress, TcpPort, Tunnel as PrivateTunnel, TunnelPeer, TunnelPeerCreated, SetUpstreamProxy, UpstreamProxy, } from "./products/network-products.js";
|
|
28
|
+
export type { PrivateNetwork } from "./products/private-network.js";
|
|
28
29
|
export { verifyWebhook, WebhookVerificationError, type MetricPoint, type MetricRange, type OtelExport, type RuntimeEvent, type SandboxMetrics, type Webhook, type WebhookDelivery, type WebhookEventType, } from "./products/observability.js";
|
|
29
30
|
export { Snapshots, type Snapshot, type SnapshotOptions } from "./snapshots.js";
|
|
30
31
|
export { Transport, VERSION, apiOrigin, type RequestOptions, type Call } from "./transport.js";
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,7 +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.
|
|
4
|
+
* unchanged, and nothing here keeps a second catalogue. The one change: the
|
|
5
|
+
* initialize answer's serverInfo.version is this package's. Starting it costs a
|
|
5
6
|
* process and one TLS connection (undici loads only behind a proxy).
|
|
6
7
|
*
|
|
7
8
|
* With no key on this machine it does not fail: it serves one tool,
|
package/dist/mcp.js
CHANGED
|
@@ -14,7 +14,8 @@ const connectTool = {
|
|
|
14
14
|
/** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
|
|
15
15
|
* Newline-delimited JSON-RPC in, the same out; every message is forwarded as
|
|
16
16
|
* it is, so the server's tools, schemas and `instructions` reach the client
|
|
17
|
-
* unchanged, and nothing here keeps a second catalogue.
|
|
17
|
+
* unchanged, and nothing here keeps a second catalogue. The one change: the
|
|
18
|
+
* initialize answer's serverInfo.version is this package's. Starting it costs a
|
|
18
19
|
* process and one TLS connection (undici loads only behind a proxy).
|
|
19
20
|
*
|
|
20
21
|
* With no key on this machine it does not fail: it serves one tool,
|
|
@@ -79,6 +80,10 @@ export async function serveMcp(env = process.env, options = {}) {
|
|
|
79
80
|
const result = reply.result;
|
|
80
81
|
if (result?.protocolVersion)
|
|
81
82
|
protocolVersion = result.protocolVersion;
|
|
83
|
+
// The client runs this package: it reports the package's version, as
|
|
84
|
+
// before connecting, not the hosted API's contract version (0.2.0).
|
|
85
|
+
if (result?.serverInfo)
|
|
86
|
+
result.serverInfo.version = VERSION;
|
|
82
87
|
}
|
|
83
88
|
return { status: response.status, messages, text };
|
|
84
89
|
}
|