withruntime 0.10.0 → 0.11.1

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 (94) hide show
  1. package/BLAXEL.md +7 -8
  2. package/DAYTONA.md +40 -35
  3. package/E2B.md +70 -57
  4. package/README.md +3 -3
  5. package/README.src.md +3 -3
  6. package/VERCEL.md +23 -20
  7. package/compatibility-lock.json +63 -84
  8. package/dist/api-defaults.d.ts +1 -1
  9. package/dist/api-defaults.js +1 -1
  10. package/dist/blaxel/lookup.d.ts +2 -1
  11. package/dist/blaxel/lookup.js +2 -1
  12. package/dist/blaxel/sandbox.d.ts +7 -5
  13. package/dist/blaxel/sandbox.js +15 -10
  14. package/dist/cli.js +112 -17
  15. package/dist/client.d.ts +17 -1
  16. package/dist/client.js +46 -3
  17. package/dist/compat/files.js +5 -1
  18. package/dist/compat/lease.d.ts +37 -0
  19. package/dist/compat/lease.js +93 -0
  20. package/dist/credentials.d.ts +9 -0
  21. package/dist/credentials.js +26 -4
  22. package/dist/daytona/computer-use.d.ts +162 -0
  23. package/dist/daytona/computer-use.js +324 -0
  24. package/dist/daytona/context.d.ts +10 -0
  25. package/dist/daytona/context.js +24 -0
  26. package/dist/daytona/daytona.d.ts +20 -0
  27. package/dist/daytona/daytona.js +150 -26
  28. package/dist/daytona/filesystem.js +8 -0
  29. package/dist/daytona/index.d.ts +2 -26
  30. package/dist/daytona/index.js +2 -7
  31. package/dist/daytona/process.d.ts +63 -6
  32. package/dist/daytona/process.js +224 -23
  33. package/dist/daytona/sandbox.d.ts +25 -6
  34. package/dist/daytona/sandbox.js +60 -27
  35. package/dist/e2b/client.d.ts +5 -0
  36. package/dist/e2b/client.js +5 -1
  37. package/dist/e2b/commands.d.ts +15 -6
  38. package/dist/e2b/commands.js +83 -18
  39. package/dist/e2b/errors.d.ts +7 -0
  40. package/dist/e2b/errors.js +20 -0
  41. package/dist/e2b/filesystem.d.ts +2 -1
  42. package/dist/e2b/filesystem.js +43 -6
  43. package/dist/e2b/index.d.ts +2 -2
  44. package/dist/e2b/index.js +1 -1
  45. package/dist/e2b/pty.js +6 -3
  46. package/dist/e2b/sandbox.d.ts +20 -6
  47. package/dist/e2b/sandbox.js +207 -31
  48. package/dist/e2b/users.d.ts +30 -0
  49. package/dist/e2b/users.js +185 -0
  50. package/dist/errors.d.ts +6 -0
  51. package/dist/errors.js +6 -0
  52. package/dist/identity.js +2 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.js +2 -1
  55. package/dist/mcp.d.ts +2 -1
  56. package/dist/mcp.js +6 -1
  57. package/dist/modal/index.js +6 -2
  58. package/dist/network-cli.js +33 -5
  59. package/dist/products/browser.d.ts +61 -0
  60. package/dist/products/browser.js +27 -0
  61. package/dist/products/desktop.d.ts +4 -3
  62. package/dist/products/desktop.js +7 -32
  63. package/dist/products/images.d.ts +3 -1
  64. package/dist/products/images.js +14 -2
  65. package/dist/products/index.d.ts +2 -0
  66. package/dist/products/index.js +2 -0
  67. package/dist/products/jobs.d.ts +7 -5
  68. package/dist/products/jobs.js +7 -3
  69. package/dist/products/mcp.d.ts +5 -1
  70. package/dist/products/mcp.js +29 -8
  71. package/dist/products/network-products.d.ts +8 -1
  72. package/dist/products/network-products.js +4 -1
  73. package/dist/products/observability.d.ts +1 -1
  74. package/dist/products/private-network.d.ts +22 -0
  75. package/dist/products/private-network.js +10 -0
  76. package/dist/products/secrets.d.ts +2 -2
  77. package/dist/runloop/index.js +6 -2
  78. package/dist/sandbox.d.ts +44 -14
  79. package/dist/sandbox.js +162 -48
  80. package/dist/tar.d.ts +2 -0
  81. package/dist/tar.js +74 -16
  82. package/dist/transport.d.ts +33 -7
  83. package/dist/transport.js +171 -36
  84. package/dist/types.d.ts +59 -11
  85. package/dist/vercel/filesystem.js +3 -1
  86. package/dist/vercel/index.d.ts +1 -7
  87. package/dist/vercel/index.js +1 -3
  88. package/dist/vercel/sandbox-user.d.ts +70 -0
  89. package/dist/vercel/sandbox-user.js +206 -0
  90. package/dist/vercel/sandbox.d.ts +57 -15
  91. package/dist/vercel/sandbox.js +219 -47
  92. package/dist/wait.d.ts +12 -0
  93. package/dist/wait.js +47 -0
  94. 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
@@ -9,21 +9,30 @@ import { Pty } from "./pty.js";
9
9
  /** E2B's default machine: 2 vCPU and 512 MiB (docs.e2b.dev/billing, checked
10
10
  * 23 September 2026). Pass `runtime: { create: { memoryMiB } }` for another. */
11
11
  export const DEFAULT_SHAPE = { vcpu: 2, memoryMiB: 512 };
12
- /** E2B's default sandbox timeout, 300 s. */
12
+ /** E2B's default sandbox timeout, 300 s. Not sent: a sandbox created with no
13
+ * timeoutMs has no time limit on Runtime, running while it works and pausing
14
+ * when idle (0300). Kept for code that imports it. */
13
15
  export const DEFAULT_TIMEOUT_MS = 300_000;
14
16
  /** Runtime's lease bounds (MAX_TIMEOUT_SECONDS in the API). */
15
17
  const MIN_LEASE_SECONDS = 60;
16
18
  const MAX_LEASE_SECONDS = 3600;
19
+ /** E2B's longest sandbox timeout, 24 hours (its Pro plan). */
20
+ const LONGEST_TIMEOUT_MS = 86_400_000;
17
21
  /** E2B template names that mean "the stock environment": Runtime's default image. */
18
22
  const STOCK_TEMPLATES = new Set(["base", "code-interpreter-v1", "code-interpreter"]);
19
23
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
24
  let warnedShortLease = false;
25
+ /** E2B's own bound since 2.52.0: refused before anything happens. */
26
+ const MAX_FORK_COUNT = 20;
27
+ function checkForkCount(count) {
28
+ if (count !== undefined && (!Number.isInteger(count) || count < 1 || count > MAX_FORK_COUNT))
29
+ throw new InvalidArgumentError(`count must be an integer between 1 and ${MAX_FORK_COUNT}`);
30
+ }
31
+ /** The first lease for `timeoutMs`: up to an hour; `keepUntil` carries a
32
+ * longer timeout on from there. */
21
33
  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.");
34
+ checkTimeout(timeoutMs);
35
+ const seconds = Math.min(Math.ceil(timeoutMs / 1000), MAX_LEASE_SECONDS);
27
36
  if (seconds < MIN_LEASE_SECONDS) {
28
37
  if (!warnedShortLease && typeof process !== "undefined") {
29
38
  warnedShortLease = true;
@@ -33,6 +42,87 @@ function leaseSeconds(timeoutMs) {
33
42
  }
34
43
  return seconds;
35
44
  }
45
+ function checkTimeout(timeoutMs) {
46
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
47
+ throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
48
+ if (timeoutMs > LONGEST_TIMEOUT_MS)
49
+ throw new InvalidArgumentError(`timeoutMs is at most 24 hours (86_400_000 ms), as on E2B, not ${timeoutMs}.`);
50
+ }
51
+ /* Timeouts over an hour. Runtime's lease reaches at most an hour ahead of
52
+ now, and E2B's timeout up to a day. While this process runs, the adapter
53
+ moves the lease on every five minutes, as far as an hour ahead and never
54
+ past the end asked for. One keeper per sandbox, whichever object asked.
55
+ If the process ends first, the sandbox ends when its lease does, at most
56
+ an hour later; it is never kept past the time asked for. */
57
+ const KEEP_EVERY_MS = 5 * 60_000;
58
+ /** Kept under the API's hour, for the clocks and the trip. */
59
+ const KEEP_AHEAD_MS = (MAX_LEASE_SECONDS - 30) * 1000;
60
+ const keepers = new Map();
61
+ let warnedKeeper = false;
62
+ /** Keeps `runtime` until `until` (epoch ms), or stops keeping it when that
63
+ * is within the hour its lease can already reach. */
64
+ function keepUntil(runtime, until) {
65
+ const keeper = keepers.get(runtime.id);
66
+ if (until - Date.now() <= MAX_LEASE_SECONDS * 1000)
67
+ return stopKeeping(runtime.id);
68
+ if (keeper) {
69
+ keeper.until = until;
70
+ keeper.runtime = runtime;
71
+ return;
72
+ }
73
+ const made = { until, runtime };
74
+ keepers.set(runtime.id, made);
75
+ schedule(made);
76
+ }
77
+ function stopKeeping(sandboxId) {
78
+ const keeper = keepers.get(sandboxId);
79
+ if (keeper?.timer !== undefined)
80
+ clearTimeout(keeper.timer);
81
+ keepers.delete(sandboxId);
82
+ }
83
+ function schedule(keeper) {
84
+ keeper.timer = setTimeout(() => void renew(keeper), KEEP_EVERY_MS);
85
+ // Never what keeps a Node or Bun process running.
86
+ keeper.timer.unref?.();
87
+ }
88
+ async function renew(keeper) {
89
+ const id = keeper.runtime.id;
90
+ if (keepers.get(id) !== keeper)
91
+ return;
92
+ try {
93
+ await keeper.runtime.refresh();
94
+ const current = state(keeper.runtime);
95
+ if (current === "stopped")
96
+ return stopKeeping(id);
97
+ if (current === "running") {
98
+ if (keeper.runtime.info.endsAt === null)
99
+ return stopKeeping(id);
100
+ const end = Date.parse(keeper.runtime.info.expiresAt);
101
+ const want = Math.min(keeper.until, Date.now() + KEEP_AHEAD_MS);
102
+ const later = Math.floor((want - end) / 1000);
103
+ if (later >= 1)
104
+ await keeper.runtime.extend(later);
105
+ if (Date.parse(keeper.runtime.info.expiresAt) >= keeper.until - 1000)
106
+ return stopKeeping(id);
107
+ }
108
+ }
109
+ catch (error) {
110
+ if (!warnedKeeper && typeof process !== "undefined" && process.emitWarning) {
111
+ warnedKeeper = true;
112
+ 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" });
113
+ }
114
+ }
115
+ if (keepers.get(id) === keeper)
116
+ schedule(keeper);
117
+ }
118
+ /** Test hook: renew every kept lease now, as the five-minute timer would. */
119
+ export async function renewLeases() {
120
+ await Promise.all([...keepers.values()].map((keeper) => renew(keeper)));
121
+ }
122
+ /** Test hook: the end each kept sandbox is kept until. */
123
+ export function keptLeases() {
124
+ return new Map([...keepers].map(([id, keeper]) => [id, keeper.until]));
125
+ }
36
126
  function onLeaseEnd(lifecycle) {
37
127
  if (!lifecycle)
38
128
  return "stop";
@@ -114,7 +204,8 @@ function infoOf(runtime) {
114
204
  ...(info.name ? { name: info.name } : {}),
115
205
  metadata: info.labels ?? {},
116
206
  startedAt: new Date(info.createdAt),
117
- endAt: new Date(info.expiresAt),
207
+ // With no time limit, where it is paid up to: always ahead, moving on.
208
+ endAt: new Date(info.endsAt ?? info.expiresAt),
118
209
  state: current === "stopped" ? "paused" : current,
119
210
  cpuCount: info.vcpu,
120
211
  memoryMB: info.memoryMiB,
@@ -215,8 +306,10 @@ export class Sandbox {
215
306
  options.signal?.throwIfAborted();
216
307
  refuseCreate(opts);
217
308
  const client = clientFor(opts);
309
+ // No timeoutMs, no time limit: it runs while it works (0300).
310
+ const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
218
311
  const input = {
219
- timeoutSeconds: leaseSeconds(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
312
+ ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
220
313
  onLeaseEnd: onLeaseEnd(opts.lifecycle),
221
314
  // With a lifecycle, E2B resumes a paused sandbox on traffic only when
222
315
  // asked (autoResume); Runtime's automatic wake is the same thing (0093).
@@ -237,6 +330,8 @@ export class Sandbox {
237
330
  ...opts.runtime?.create,
238
331
  };
239
332
  const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
333
+ if (asked !== undefined)
334
+ keepUntil(runtime, asked);
240
335
  return new this(runtime, client, opts.requestTimeoutMs);
241
336
  }
242
337
  /** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
@@ -261,6 +356,7 @@ export class Sandbox {
261
356
  /** Stops the sandbox. False when it was not found or had already ended. */
262
357
  static async kill(sandboxId, opts = {}) {
263
358
  const client = clientFor(opts);
359
+ stopKeeping(sandboxId);
264
360
  try {
265
361
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
266
362
  return await stop(runtime, opts);
@@ -272,6 +368,7 @@ export class Sandbox {
272
368
  }
273
369
  }
274
370
  async kill(opts = {}) {
371
+ stopKeeping(this.sandboxId);
275
372
  try {
276
373
  return await stop(this.#runtime, opts);
277
374
  }
@@ -281,8 +378,10 @@ export class Sandbox {
281
378
  throw error;
282
379
  }
283
380
  }
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. */
381
+ /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
382
+ * hour the lease is moved on while this process runs. Runtime cannot end a
383
+ * lease early, so an end sooner than the lease's is refused. A sandbox
384
+ * created with no timeout has no end to move, and nothing changes. */
286
385
  static async setTimeout(sandboxId, timeoutMs, opts = {}) {
287
386
  const client = clientFor(opts);
288
387
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
@@ -330,6 +429,7 @@ export class Sandbox {
330
429
  /** Copies of this sandbox, memory and all. While Runtime's forks are
331
430
  * switched off this throws NotSupportedError with Runtime's own words. */
332
431
  static async fork(sandboxId, opts = {}) {
432
+ checkForkCount(opts.count);
333
433
  const { count: _count, timeoutMs: _timeoutMs, ...connection } = opts;
334
434
  const source = await this.connect(sandboxId, connection);
335
435
  return source.fork(opts);
@@ -339,6 +439,7 @@ export class Sandbox {
339
439
  // A fork's lease is Runtime's to set; one shorter than asked could not be
340
440
  // honoured after the forks exist, so this is refused before any are made.
341
441
  throw new NotSupportedError("A timeout for forks (fork timeoutMs)", "Fork without it, then call setTimeout(ms) on each fork.");
442
+ checkForkCount(opts.count);
342
443
  const copies = await guard("sandbox", () => this.#runtime.fork({ count: opts.count ?? 1, ...request(opts) }));
343
444
  // Copies keep the source's environment on Runtime's side.
344
445
  const Kind = this.constructor;
@@ -378,6 +479,10 @@ export class Sandbox {
378
479
  getHost(port) {
379
480
  if (!Number.isInteger(port) || port < 1 || port > 65_535)
380
481
  throw new InvalidArgumentError(`port must be a whole number from 1 to 65535, not ${port}.`);
482
+ // A trial sandbox's ports are private: a host alone would answer 404 or
483
+ // 401 to everyone. Say so now, with the address that works.
484
+ if (this.#runtime.info.funding === "trial")
485
+ throw new PublicPreviewNotAllowedError(this.sandboxId, port);
381
486
  void this.#share(port).catch((error) => {
382
487
  if (typeof process !== "undefined" && process.emitWarning)
383
488
  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 +493,8 @@ export class Sandbox {
388
493
  * its host, `<port>-<id>.<domain>`, once the share has landed. Anyone with
389
494
  * the address can reach it; a browser sees a one-time page naming Runtime. */
390
495
  async getPublicHost(port) {
496
+ if (this.#runtime.info.funding === "trial")
497
+ throw new PublicPreviewNotAllowedError(this.sandboxId, port);
391
498
  return this.#share(port);
392
499
  }
393
500
  #share(port) {
@@ -439,18 +546,24 @@ async function resume(runtime, opts) {
439
546
  const current = state(runtime);
440
547
  if (current === "stopped")
441
548
  throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
549
+ const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
442
550
  if (current === "paused") {
443
551
  await guard("sandbox", () => runtime.wake({
444
552
  ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
445
553
  ...request(opts),
446
554
  }));
447
- return;
448
555
  }
449
- if (opts.timeoutMs !== undefined) {
450
- const later = Date.now() + opts.timeoutMs - Date.parse(runtime.info.expiresAt);
556
+ else if (opts.timeoutMs !== undefined) {
557
+ checkTimeout(opts.timeoutMs);
558
+ // No time limit: no end to move (0300).
559
+ if (runtime.info.endsAt === null)
560
+ return;
561
+ const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - Date.parse(runtime.info.expiresAt);
451
562
  if (later > 1000)
452
563
  await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
453
564
  }
565
+ if (asked !== undefined && asked > (keepers.get(runtime.id)?.until ?? 0))
566
+ keepUntil(runtime, asked);
454
567
  }
455
568
  async function stop(runtime, opts) {
456
569
  if (state(runtime) === "stopped")
@@ -459,13 +572,18 @@ async function stop(runtime, opts) {
459
572
  return true;
460
573
  }
461
574
  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.");
575
+ checkTimeout(timeoutMs);
576
+ // No time limit: no end to move (0300).
577
+ if (runtime.info.endsAt === null)
578
+ return;
579
+ const asked = Date.now() + timeoutMs;
580
+ const end = Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt);
581
+ if (asked - end < -1000)
582
+ 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.");
583
+ const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - end;
467
584
  if (later > 1000)
468
585
  await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
586
+ keepUntil(runtime, asked);
469
587
  }
470
588
  async function pause(runtime, opts) {
471
589
  if (opts.keepMemory === false)
@@ -480,51 +598,109 @@ async function pause(runtime, opts) {
480
598
  }
481
599
  const RUNNING = ["starting", "running", "resuming"];
482
600
  const PAUSED = ["pausing", "paused"];
483
- /** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`. */
601
+ /** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`.
602
+ * Runtime filters by state and metadata itself. A template, `startedAfter`,
603
+ * newest-first order or a saved `nextToken` are done here: every match is
604
+ * read once, then served a page at a time. */
484
605
  export class SandboxPaginator {
485
606
  #client;
486
607
  #request;
487
608
  #filter;
609
+ #opts;
610
+ #pageSize;
611
+ #local;
488
612
  #page;
613
+ #all;
614
+ #offset;
489
615
  #hasNext = true;
490
616
  constructor(client, opts) {
491
617
  this.#client = client;
492
618
  this.#request = request(opts);
619
+ this.#opts = opts;
493
620
  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.");
621
+ if (opts.limit !== undefined && (!Number.isSafeInteger(opts.limit) || opts.limit < 1))
622
+ throw new InvalidArgumentError(`limit must be a positive whole number, not ${opts.limit}.`);
623
+ this.#pageSize = opts.limit ?? 100;
624
+ this.#offset = opts.nextToken === undefined ? 0 : tokenOffset(opts.nextToken);
625
+ this.#local =
626
+ query.template !== undefined ||
627
+ query.startedAfter !== undefined ||
628
+ opts.order === "desc" ||
629
+ opts.nextToken !== undefined;
502
630
  const states = query.state?.length ? query.state : ["running", "paused"];
503
631
  this.#filter = {
504
632
  state: states.flatMap((one) => (one === "running" ? [...RUNNING] : [...PAUSED])),
505
633
  ...(query.metadata && Object.keys(query.metadata).length ? { labels: query.metadata } : {}),
506
- ...(opts.limit ? { limit: Math.min(opts.limit, 100) } : {}),
634
+ limit: this.#local ? 100 : Math.min(this.#pageSize, 100),
507
635
  };
508
636
  }
509
637
  get hasNext() {
510
638
  return this.#hasNext;
511
639
  }
640
+ /** Where the next page starts; `Sandbox.list({ nextToken })` resumes there. */
512
641
  get nextToken() {
513
- return this.#page?.nextCursor ?? undefined;
642
+ return this.#hasNext && this.#offset > 0 ? `runtime:${this.#offset}` : undefined;
514
643
  }
515
644
  async nextItems() {
516
645
  this.#request.signal?.throwIfAborted();
517
646
  if (!this.#hasNext)
518
647
  throw new SandboxError("No more items to fetch.");
648
+ if (this.#local) {
649
+ const all = (this.#all ??= await guard("other", () => this.#everyMatch()));
650
+ const items = all.slice(this.#offset, this.#offset + this.#pageSize);
651
+ this.#offset += items.length;
652
+ this.#hasNext = this.#offset < all.length;
653
+ return items;
654
+ }
519
655
  const previous = this.#page;
520
656
  const page = await guard("other", async () => previous
521
657
  ? await previous.next()
522
658
  : await this.#client.sandboxes.list(this.#filter, this.#request));
523
659
  this.#page = page ?? undefined;
524
660
  this.#hasNext = Boolean(page?.hasMore);
525
- return (page?.data ?? []).map(infoOf);
661
+ const items = (page?.data ?? []).map(infoOf);
662
+ this.#offset += items.length;
663
+ return items;
664
+ }
665
+ async #everyMatch() {
666
+ const query = this.#opts.query ?? {};
667
+ const templates = query.template === undefined
668
+ ? undefined
669
+ : await templateIds(this.#client, query.template, this.#request);
670
+ const after = query.startedAfter?.getTime();
671
+ const all = [];
672
+ let page = await this.#client.sandboxes.list(this.#filter, this.#request);
673
+ while (page) {
674
+ for (const one of page.data) {
675
+ const info = infoOf(one);
676
+ if (templates && !templates.has(info.templateId))
677
+ continue;
678
+ if (after !== undefined && info.startedAt.getTime() < after)
679
+ continue;
680
+ all.push(info);
681
+ }
682
+ page = page.hasMore ? await page.next() : null;
683
+ }
684
+ const sign = this.#opts.order === "desc" ? -1 : 1;
685
+ return all.sort((a, b) => sign * (a.startedAt.getTime() - b.startedAt.getTime()));
526
686
  }
527
687
  }
688
+ function tokenOffset(token) {
689
+ const match = /^runtime:(\d+)$/.exec(token);
690
+ if (!match)
691
+ throw new InvalidArgumentError(`"${token}" is not a nextToken this package gave; pass the paginator's own nextToken.`);
692
+ return Number(match[1]);
693
+ }
694
+ /** The template ids a template name stands for: "base" for the stock names,
695
+ * the id itself for a UUID, and every Runtime image of that name. */
696
+ async function templateIds(client, template, options) {
697
+ if (STOCK_TEMPLATES.has(template))
698
+ return new Set(["base"]);
699
+ if (UUID.test(template))
700
+ return new Set([template]);
701
+ const page = await client.images.list({ name: template, limit: 100 }, options);
702
+ return new Set(page.data.map((image) => image.id));
703
+ }
528
704
  async function e2bMetrics(runtime, opts) {
529
705
  const start = opts.start === undefined ? Date.now() - 15 * 60_000 : new Date(opts.start).getTime();
530
706
  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/errors.d.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  * packages/cloud/src/api/respond.ts) and retrying cannot change them.
7
7
  * host_unavailable and busy pass, and are retried. */
8
8
  export declare const DELIBERATE: ReadonlySet<string>;
9
+ /** Refusals that guarantee nothing ran: a sandbox running as many commands
10
+ * as it can (`guest_busy`), and the API's own limits, which answer before any
11
+ * work starts (`busy`, `rate_limited`). Sending such a call again is always
12
+ * safe, so the SDK does, with backoff, until the call's deadline (five
13
+ * minutes when it has none), however many retries it allows otherwise. */
14
+ export declare const NOTHING_RAN: ReadonlySet<string>;
9
15
  /** Refusals of a create that clear when a sandbox stops or pauses, or a host
10
16
  * frees room: the trial's slots, an email domain's trial slots, the account's
11
17
  * quota and the region's capacity. `sandboxes.create` waits them out, retrying
package/dist/errors.js CHANGED
@@ -16,6 +16,12 @@ export const DELIBERATE = new Set([
16
16
  "identity_unavailable",
17
17
  "env_unavailable",
18
18
  ]);
19
+ /** Refusals that guarantee nothing ran: a sandbox running as many commands
20
+ * as it can (`guest_busy`), and the API's own limits, which answer before any
21
+ * work starts (`busy`, `rate_limited`). Sending such a call again is always
22
+ * safe, so the SDK does, with backoff, until the call's deadline (five
23
+ * minutes when it has none), however many retries it allows otherwise. */
24
+ export const NOTHING_RAN = new Set(["guest_busy", "busy", "rate_limited"]);
19
25
  /** Refusals that pass on their own: a host frees room, a trial slot frees. */
20
26
  const PASSING = new Set(["no_capacity", "trial_busy"]);
21
27
  /** Refusals of a create that clear when a sandbox stops or pauses, or a host
package/dist/identity.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
+ import { sandboxMarker } from "./transport.js";
2
3
  async function requestCredentials() {
3
4
  let url = process.env.RUNTIME_ID_TOKEN_REQUEST_URL;
4
5
  let token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
@@ -7,7 +8,7 @@ async function requestCredentials() {
7
8
  try {
8
9
  // Loaded here, so the SDK still loads where there is no file system.
9
10
  const { readFile } = await import("node:fs/promises");
10
- const env = JSON.parse(await readFile("/run/runtime/environment.json", "utf8"));
11
+ const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
11
12
  url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
12
13
  token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
13
14
  }