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.
Files changed (65) hide show
  1. package/DAYTONA.md +40 -34
  2. package/E2B.md +56 -57
  3. package/VERCEL.md +21 -19
  4. package/compatibility-lock.json +8 -84
  5. package/dist/cli.js +73 -2
  6. package/dist/client.d.ts +12 -0
  7. package/dist/client.js +27 -0
  8. package/dist/compat/lease.d.ts +36 -0
  9. package/dist/compat/lease.js +87 -0
  10. package/dist/daytona/computer-use.d.ts +162 -0
  11. package/dist/daytona/computer-use.js +324 -0
  12. package/dist/daytona/context.d.ts +10 -0
  13. package/dist/daytona/context.js +24 -0
  14. package/dist/daytona/daytona.d.ts +20 -0
  15. package/dist/daytona/daytona.js +123 -16
  16. package/dist/daytona/filesystem.js +8 -0
  17. package/dist/daytona/index.d.ts +2 -26
  18. package/dist/daytona/index.js +2 -7
  19. package/dist/daytona/process.d.ts +63 -6
  20. package/dist/daytona/process.js +224 -23
  21. package/dist/daytona/sandbox.d.ts +19 -5
  22. package/dist/daytona/sandbox.js +39 -25
  23. package/dist/e2b/client.d.ts +5 -0
  24. package/dist/e2b/client.js +5 -1
  25. package/dist/e2b/commands.d.ts +15 -6
  26. package/dist/e2b/commands.js +83 -18
  27. package/dist/e2b/errors.d.ts +7 -0
  28. package/dist/e2b/errors.js +20 -0
  29. package/dist/e2b/filesystem.d.ts +2 -1
  30. package/dist/e2b/filesystem.js +43 -6
  31. package/dist/e2b/index.d.ts +2 -2
  32. package/dist/e2b/index.js +1 -1
  33. package/dist/e2b/pty.js +6 -3
  34. package/dist/e2b/sandbox.d.ts +15 -5
  35. package/dist/e2b/sandbox.js +190 -28
  36. package/dist/e2b/users.d.ts +30 -0
  37. package/dist/e2b/users.js +185 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/mcp.d.ts +2 -1
  40. package/dist/mcp.js +6 -1
  41. package/dist/network-cli.js +28 -0
  42. package/dist/products/browser.d.ts +60 -0
  43. package/dist/products/browser.js +37 -0
  44. package/dist/products/desktop.d.ts +2 -2
  45. package/dist/products/desktop.js +2 -2
  46. package/dist/products/images.d.ts +3 -1
  47. package/dist/products/images.js +14 -2
  48. package/dist/products/index.d.ts +2 -0
  49. package/dist/products/index.js +2 -0
  50. package/dist/products/network-products.d.ts +7 -0
  51. package/dist/products/network-products.js +3 -0
  52. package/dist/products/private-network.d.ts +22 -0
  53. package/dist/products/private-network.js +10 -0
  54. package/dist/sandbox.d.ts +22 -0
  55. package/dist/sandbox.js +47 -0
  56. package/dist/transport.d.ts +1 -1
  57. package/dist/transport.js +1 -1
  58. package/dist/types.d.ts +5 -2
  59. package/dist/vercel/index.d.ts +1 -7
  60. package/dist/vercel/index.js +1 -3
  61. package/dist/vercel/sandbox-user.d.ts +70 -0
  62. package/dist/vercel/sandbox-user.js +206 -0
  63. package/dist/vercel/sandbox.d.ts +47 -12
  64. package/dist/vercel/sandbox.js +201 -42
  65. package/package.json +1 -1
@@ -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
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
23
- throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
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. Runtime leases only move
285
- * later: a shorter timeout than the one it has is refused. */
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
- const later = Date.now() + opts.timeoutMs - Date.parse(runtime.info.expiresAt);
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
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
463
- throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
464
- const later = Date.now() + timeoutMs - Date.parse(runtime.info.expiresAt);
465
- if (later < -1000)
466
- throw new NotSupportedError("Shortening a sandbox's timeout", "Runtime leases only move later. Call kill() when the work is done.");
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 (query.template !== undefined && !STOCK_TEMPLATES.has(query.template))
495
- throw new NotSupportedError("Listing by template", "Filter by metadata instead: give sandboxes a metadata key when you create them.");
496
- if (query.startedAfter !== undefined)
497
- throw new NotSupportedError("Listing by start time (startedAfter)", "List them all and filter on getInfo().startedAt.");
498
- if (opts.order === "desc")
499
- throw new NotSupportedError("Newest-first listing (order: 'desc')", "Runtime lists oldest first; reverse the list yourself.");
500
- if (opts.nextToken !== undefined)
501
- throw new NotSupportedError("Starting a list from a saved nextToken", "Keep the paginator and call nextItems() again.");
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
- ...(opts.limit ? { limit: Math.min(opts.limit, 100) } : {}),
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.#page?.nextCursor ?? undefined;
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
- return (page?.data ?? []).map(infoOf);
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. Starting it costs a
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. Starting it costs a
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
  }