withruntime 0.9.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 (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
@@ -0,0 +1,83 @@
1
+ import { CommandHandle, commandRequest, connectionRequest, killProcess, resolveProcess, } from "./commands.js";
2
+ import { guard, InvalidArgumentError, NotSupportedError } from "./errors.js";
3
+ import { runAs, shellAs } from "./users.js";
4
+ function dimensions(size) {
5
+ for (const value of [size.cols, size.rows])
6
+ if (!Number.isSafeInteger(value) || value <= 0)
7
+ throw new InvalidArgumentError("PTY rows and columns must be positive whole numbers.");
8
+ return { cols: size.cols, rows: size.rows };
9
+ }
10
+ /** E2B's PTY methods over native terminal processes with lossless output. */
11
+ export class Pty {
12
+ ctx;
13
+ constructor(ctx) {
14
+ this.ctx = ctx;
15
+ }
16
+ async create(opts) {
17
+ const pty = dimensions(opts);
18
+ const opening = connectionRequest(opts, this.ctx.requestTimeoutMs);
19
+ if (typeof opts.onData !== "function")
20
+ throw new InvalidArgumentError("onData must be a function.");
21
+ const env = { TERM: "xterm-256color", LANG: "C.UTF-8", LC_ALL: "C.UTF-8", ...opts.envs };
22
+ const process = await guard("sandbox", async () => {
23
+ // Another user's terminal is a login shell of theirs, through sudo -u.
24
+ const user = await runAs(this.ctx, opts.user, opening.request);
25
+ await this.ctx.ensureHome(opts.cwd, opening.request);
26
+ opening.request.signal?.throwIfAborted();
27
+ const shell = user ? shellAs(user, opts.cwd !== undefined) : ["/bin/bash", "-i", "-l"];
28
+ return this.ctx.runtime.spawn(shell, {
29
+ pty,
30
+ stdin: "pipe",
31
+ outputEncoding: "base64",
32
+ env,
33
+ ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
34
+ request: opening.request,
35
+ });
36
+ }).catch(opening.failure);
37
+ return new CommandHandle(process, {
38
+ stdin: false,
39
+ pty: true,
40
+ onData: opts.onData,
41
+ timeoutMs: opening.timeoutMs,
42
+ deadline: opening.deadline,
43
+ signal: opts.signal,
44
+ requestTimeoutMs: this.ctx.requestTimeoutMs,
45
+ });
46
+ }
47
+ async connect(pid, opts) {
48
+ const opening = connectionRequest(opts ?? {}, this.ctx.requestTimeoutMs);
49
+ const process = await resolveProcess(this.ctx, pid, opening.request).catch(opening.failure);
50
+ if (!process.info.pty || process.info.outputEncoding !== "base64")
51
+ throw new NotSupportedError("Connecting to a legacy text-output PTY", "Create the PTY through sandbox.pty.create first.");
52
+ return new CommandHandle(process, {
53
+ stdin: false,
54
+ pty: true,
55
+ onData: opts?.onData,
56
+ timeoutMs: opening.timeoutMs,
57
+ deadline: opening.deadline,
58
+ cursor: process.info.outputBytes,
59
+ signal: opts?.signal,
60
+ requestTimeoutMs: this.ctx.requestTimeoutMs,
61
+ });
62
+ }
63
+ async sendInput(pid, data, opts = {}) {
64
+ const request = commandRequest(opts, this.ctx.requestTimeoutMs);
65
+ const process = await resolveProcess(this.ctx, pid, request);
66
+ if (!process.info.pty)
67
+ throw new InvalidArgumentError(`The process with pid ${pid} is not a PTY.`);
68
+ request.signal?.throwIfAborted();
69
+ await guard("sandbox", () => process.write(data, request));
70
+ }
71
+ async resize(pid, size, opts = {}) {
72
+ const pty = dimensions(size);
73
+ const request = commandRequest(opts, this.ctx.requestTimeoutMs);
74
+ const process = await resolveProcess(this.ctx, pid, request);
75
+ if (!process.info.pty)
76
+ throw new InvalidArgumentError(`The process with pid ${pid} is not a PTY.`);
77
+ request.signal?.throwIfAborted();
78
+ await guard("sandbox", () => process.resize(pty.cols, pty.rows, request));
79
+ }
80
+ kill(pid, opts = {}) {
81
+ return killProcess(this.ctx, pid, commandRequest(opts, this.ctx.requestTimeoutMs));
82
+ }
83
+ }
@@ -2,6 +2,7 @@ import type { Runtime } from "../client.js";
2
2
  import { type ConnectionOpts, type RuntimeOpts, type RuntimeSandbox } from "./client.js";
3
3
  import { Commands } from "./commands.js";
4
4
  import { Filesystem } from "./filesystem.js";
5
+ import { Pty } from "./pty.js";
5
6
  /** E2B's default machine: 2 vCPU and 512 MiB (docs.e2b.dev/billing, checked
6
7
  * 23 September 2026). Pass `runtime: { create: { memoryMiB } }` for another. */
7
8
  export declare const DEFAULT_SHAPE: {
@@ -29,8 +30,9 @@ export interface SandboxOpts extends ConnectionOpts {
29
30
  /** The sandbox's own environment: every command, terminal and code run in it
30
31
  * gets these, whoever connects. Values are never shown again. */
31
32
  envs?: Record<string, string>;
32
- /** Default 300 000. Runtime's leases run 60 s to 1 hour; shorter rounds up
33
- * to 60 s (with a warning), longer is refused. */
33
+ /** Default 300 000, at most 24 hours. Runtime's leases run 60 s to 1 hour:
34
+ * shorter rounds up to 60 s (with a warning); longer gets an hour, moved on
35
+ * while this process runs, up to the time asked for. */
34
36
  timeoutMs?: number;
35
37
  /** Accepted and ignored, as E2B ignores it. */
36
38
  secure?: boolean;
@@ -87,12 +89,17 @@ export type SandboxListOpts = SandboxApiOpts & {
87
89
  limit?: number;
88
90
  nextToken?: string;
89
91
  };
92
+ /** Test hook: renew every kept lease now, as the five-minute timer would. */
93
+ export declare function renewLeases(): Promise<void>;
94
+ /** Test hook: the end each kept sandbox is kept until. */
95
+ export declare function keptLeases(): Map<string, number>;
90
96
  export declare class Sandbox {
91
97
  #private;
92
98
  protected readonly requestTimeoutMs: number;
93
99
  protected static readonly defaultTemplate: string;
94
100
  readonly files: Filesystem;
95
101
  readonly commands: Commands;
102
+ readonly pty: Pty;
96
103
  /** Use Sandbox.create or Sandbox.connect. */
97
104
  constructor(runtime: RuntimeSandbox, client: Runtime, requestTimeoutMs?: number);
98
105
  get sandboxId(): string;
@@ -116,8 +123,9 @@ export declare class Sandbox {
116
123
  /** Stops the sandbox. False when it was not found or had already ended. */
117
124
  static kill(sandboxId: string, opts?: SandboxApiOpts): Promise<boolean>;
118
125
  kill(opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<boolean>;
119
- /** Sets the sandbox to end `timeoutMs` from now. Runtime leases only move
120
- * later: a shorter timeout than the one it has is refused. */
126
+ /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
127
+ * hour the lease is moved on while this process runs. Runtime cannot end a
128
+ * lease early, so an end sooner than the lease's is refused. */
121
129
  static setTimeout(sandboxId: string, timeoutMs: number, opts?: SandboxApiOpts): Promise<void>;
122
130
  setTimeout(timeoutMs: number, opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<void>;
123
131
  static getInfo(sandboxId: string, opts?: SandboxApiOpts): Promise<SandboxInfo>;
@@ -167,7 +175,6 @@ export declare class Sandbox {
167
175
  * its host, `<port>-<id>.<domain>`, once the share has landed. Anyone with
168
176
  * the address can reach it; a browser sees a one-time page naming Runtime. */
169
177
  getPublicHost(port: number): Promise<string>;
170
- get pty(): never;
171
178
  get git(): never;
172
179
  /** E2B's metrics, from Runtime's measured readings (`sbx.metrics()`): CPU
173
180
  * as a percent of the sandbox's vCPUs and resident memory, one entry per
@@ -182,11 +189,15 @@ export declare class Sandbox {
182
189
  getMcpUrl(): never;
183
190
  getMcpToken(): Promise<never>;
184
191
  }
185
- /** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`. */
192
+ /** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`.
193
+ * Runtime filters by state and metadata itself. A template, `startedAfter`,
194
+ * newest-first order or a saved `nextToken` are done here: every match is
195
+ * read once, then served a page at a time. */
186
196
  export declare class SandboxPaginator {
187
197
  #private;
188
198
  constructor(client: Runtime, opts: SandboxListOpts);
189
199
  get hasNext(): boolean;
200
+ /** Where the next page starts; `Sandbox.list({ nextToken })` resumes there. */
190
201
  get nextToken(): string | undefined;
191
202
  nextItems(): Promise<SandboxInfo[]>;
192
203
  }
@@ -1,7 +1,8 @@
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
+ import { Pty } from "./pty.js";
5
6
  /* E2B's Sandbox over Runtime's. Every call is one or a few calls of the
6
7
  withruntime SDK; nothing here speaks HTTP. What Runtime cannot do the same
7
8
  way throws NotSupportedError before anything happens. */
@@ -13,16 +14,23 @@ export const DEFAULT_TIMEOUT_MS = 300_000;
13
14
  /** Runtime's lease bounds (MAX_TIMEOUT_SECONDS in the API). */
14
15
  const MIN_LEASE_SECONDS = 60;
15
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;
16
19
  /** E2B template names that mean "the stock environment": Runtime's default image. */
17
20
  const STOCK_TEMPLATES = new Set(["base", "code-interpreter-v1", "code-interpreter"]);
18
21
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
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. */
20
31
  function leaseSeconds(timeoutMs) {
21
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
22
- throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
23
- const seconds = Math.ceil(timeoutMs / 1000);
24
- if (seconds > MAX_LEASE_SECONDS)
25
- throw new NotSupportedError(`A sandbox timeout of ${timeoutMs} ms (over one hour)`, "Runtime leases last up to an hour (3_600_000 ms); call sandbox.setTimeout(...) before it ends to keep going, as often as needed.");
32
+ checkTimeout(timeoutMs);
33
+ const seconds = Math.min(Math.ceil(timeoutMs / 1000), MAX_LEASE_SECONDS);
26
34
  if (seconds < MIN_LEASE_SECONDS) {
27
35
  if (!warnedShortLease && typeof process !== "undefined") {
28
36
  warnedShortLease = true;
@@ -32,6 +40,85 @@ function leaseSeconds(timeoutMs) {
32
40
  }
33
41
  return seconds;
34
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
+ }
35
122
  function onLeaseEnd(lifecycle) {
36
123
  if (!lifecycle)
37
124
  return "stop";
@@ -65,12 +152,13 @@ function refuseCreate(opts) {
65
152
  }
66
153
  /** Where a template sends the create: the stock image, a Runtime image by
67
154
  * name or id, or a Runtime snapshot by id. */
68
- async function resolveTemplate(client, template) {
155
+ async function resolveTemplate(client, template, options) {
156
+ options.signal?.throwIfAborted();
69
157
  if (STOCK_TEMPLATES.has(template))
70
158
  return {};
71
159
  if (UUID.test(template)) {
72
160
  try {
73
- await client.images.get(template);
161
+ await client.images.get(template, options);
74
162
  return { image: template };
75
163
  }
76
164
  catch (error) {
@@ -79,7 +167,7 @@ async function resolveTemplate(client, template) {
79
167
  }
80
168
  return { snapshot: template };
81
169
  }
82
- const page = await client.images.list({ name: template, state: "ready", limit: 1 });
170
+ const page = await client.images.list({ name: template, state: "ready", limit: 1 }, options);
83
171
  const image = page.data[0];
84
172
  if (image)
85
173
  return { image: image.id };
@@ -135,6 +223,7 @@ export class Sandbox {
135
223
  static defaultTemplate = "base";
136
224
  files;
137
225
  commands;
226
+ pty;
138
227
  #client;
139
228
  #runtime;
140
229
  #home;
@@ -146,12 +235,14 @@ export class Sandbox {
146
235
  this.#runtime = runtime;
147
236
  this.#client = client;
148
237
  const ctx = {
149
- ensureHome: (text) => this.#ensureHome(text),
238
+ ensureHome: (text, options) => this.#ensureHome(text, options),
239
+ requestTimeoutMs: this.requestTimeoutMs,
150
240
  };
151
241
  // A getter, so the modules see the sandbox as it is after each refresh.
152
242
  Object.defineProperty(ctx, "runtime", { get: () => this.#runtime });
153
243
  this.files = new Filesystem(ctx);
154
244
  this.commands = new Commands(ctx);
245
+ this.pty = new Pty(ctx);
155
246
  }
156
247
  get sandboxId() {
157
248
  return this.#runtime.id;
@@ -167,23 +258,50 @@ export class Sandbox {
167
258
  /** E2B's home is /home/user and Runtime's is /workspace. The first time a
168
259
  * path or command names /home/user, it is made a link to /workspace (unless
169
260
  * something is already there), so paths written for E2B work. */
170
- #ensureHome(text) {
261
+ #ensureHome(text, options = {}) {
262
+ const signal = options.signal;
263
+ signal?.throwIfAborted();
264
+ const wait = (pending) => {
265
+ if (!signal)
266
+ return pending;
267
+ return new Promise((resolve, reject) => {
268
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
269
+ const abort = () => reject(signal.reason);
270
+ signal.addEventListener("abort", abort, { once: true });
271
+ pending.then(() => {
272
+ signal.removeEventListener("abort", abort);
273
+ resolve();
274
+ }, (error) => {
275
+ signal.removeEventListener("abort", abort);
276
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- passes the pending call's own failure through unchanged
277
+ reject(error);
278
+ });
279
+ });
280
+ };
171
281
  if (this.#home || !text?.includes("/home/user"))
172
- return this.#home ?? Promise.resolve();
173
- this.#home = (async () => {
174
- const result = await guard("sandbox", () => this.#runtime.exec("[ -e /home/user ] || sudo ln -s /workspace /home/user"));
282
+ return wait(this.#home ?? Promise.resolve());
283
+ const pending = (async () => {
284
+ const result = await guard("sandbox", () => this.#runtime.exec("[ -e /home/user ] || sudo ln -s /workspace /home/user", { signal }));
175
285
  if (result.exitCode !== 0 && typeof process !== "undefined")
176
286
  process.emitWarning(`Could not link /home/user to /workspace: ${result.stderr.trim()}`, {
177
287
  code: "RUNTIME_E2B_HOME",
178
288
  });
179
289
  })();
180
- return this.#home;
290
+ this.#home = pending;
291
+ pending.catch(() => {
292
+ if (this.#home === pending)
293
+ this.#home = undefined;
294
+ });
295
+ return wait(pending);
181
296
  }
182
297
  static async create(templateOrOpts, maybeOpts) {
183
298
  const opts = (typeof templateOrOpts === "string" ? maybeOpts : templateOrOpts) ?? {};
184
299
  const template = (typeof templateOrOpts === "string" ? templateOrOpts : opts.template) ?? this.defaultTemplate;
300
+ const options = request(opts);
301
+ options.signal?.throwIfAborted();
185
302
  refuseCreate(opts);
186
303
  const client = clientFor(opts);
304
+ const asked = Date.now() + (opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
187
305
  const input = {
188
306
  timeoutSeconds: leaseSeconds(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
189
307
  onLeaseEnd: onLeaseEnd(opts.lifecycle),
@@ -196,7 +314,8 @@ export class Sandbox {
196
314
  ...(opts.envs && Object.keys(opts.envs).length ? { env: opts.envs } : {}),
197
315
  ...(opts.allowInternetAccess === false ? { network: { internet: false } } : {}),
198
316
  };
199
- const source = await guard("other", () => resolveTemplate(client, template));
317
+ const source = await guard("other", () => resolveTemplate(client, template, options));
318
+ options.signal?.throwIfAborted();
200
319
  const create = {
201
320
  // A snapshot carries its own machine; everything else gets E2B's shape.
202
321
  ...(source.snapshot ? {} : DEFAULT_SHAPE),
@@ -204,7 +323,8 @@ export class Sandbox {
204
323
  ...source,
205
324
  ...opts.runtime?.create,
206
325
  };
207
- const runtime = await guard("sandbox", () => client.sandboxes.create(create, request(opts)));
326
+ const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
327
+ keepUntil(runtime, asked);
208
328
  return new this(runtime, client, opts.requestTimeoutMs);
209
329
  }
210
330
  /** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
@@ -229,6 +349,7 @@ export class Sandbox {
229
349
  /** Stops the sandbox. False when it was not found or had already ended. */
230
350
  static async kill(sandboxId, opts = {}) {
231
351
  const client = clientFor(opts);
352
+ stopKeeping(sandboxId);
232
353
  try {
233
354
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
234
355
  return await stop(runtime, opts);
@@ -240,6 +361,7 @@ export class Sandbox {
240
361
  }
241
362
  }
242
363
  async kill(opts = {}) {
364
+ stopKeeping(this.sandboxId);
243
365
  try {
244
366
  return await stop(this.#runtime, opts);
245
367
  }
@@ -249,8 +371,9 @@ export class Sandbox {
249
371
  throw error;
250
372
  }
251
373
  }
252
- /** Sets the sandbox to end `timeoutMs` from now. Runtime leases only move
253
- * 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. */
254
377
  static async setTimeout(sandboxId, timeoutMs, opts = {}) {
255
378
  const client = clientFor(opts);
256
379
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
@@ -298,6 +421,7 @@ export class Sandbox {
298
421
  /** Copies of this sandbox, memory and all. While Runtime's forks are
299
422
  * switched off this throws NotSupportedError with Runtime's own words. */
300
423
  static async fork(sandboxId, opts = {}) {
424
+ checkForkCount(opts.count);
301
425
  const { count: _count, timeoutMs: _timeoutMs, ...connection } = opts;
302
426
  const source = await this.connect(sandboxId, connection);
303
427
  return source.fork(opts);
@@ -307,6 +431,7 @@ export class Sandbox {
307
431
  // A fork's lease is Runtime's to set; one shorter than asked could not be
308
432
  // honoured after the forks exist, so this is refused before any are made.
309
433
  throw new NotSupportedError("A timeout for forks (fork timeoutMs)", "Fork without it, then call setTimeout(ms) on each fork.");
434
+ checkForkCount(opts.count);
310
435
  const copies = await guard("sandbox", () => this.#runtime.fork({ count: opts.count ?? 1, ...request(opts) }));
311
436
  // Copies keep the source's environment on Runtime's side.
312
437
  const Kind = this.constructor;
@@ -346,6 +471,10 @@ export class Sandbox {
346
471
  getHost(port) {
347
472
  if (!Number.isInteger(port) || port < 1 || port > 65_535)
348
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);
349
478
  void this.#share(port).catch((error) => {
350
479
  if (typeof process !== "undefined" && process.emitWarning)
351
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" });
@@ -356,6 +485,8 @@ export class Sandbox {
356
485
  * its host, `<port>-<id>.<domain>`, once the share has landed. Anyone with
357
486
  * the address can reach it; a browser sees a one-time page naming Runtime. */
358
487
  async getPublicHost(port) {
488
+ if (this.#runtime.info.funding === "trial")
489
+ throw new PublicPreviewNotAllowedError(this.sandboxId, port);
359
490
  return this.#share(port);
360
491
  }
361
492
  #share(port) {
@@ -369,9 +500,6 @@ export class Sandbox {
369
500
  return share;
370
501
  }
371
502
  // ---- what Runtime does differently -----------------------------------
372
- get pty() {
373
- throw new NotSupportedError("E2B's pty module", "Use `await sandbox.runtime.terminal({ cols, rows, onData })` for an interactive terminal, or runtime.spawn(cmd, { pty: {} }).");
374
- }
375
503
  get git() {
376
504
  throw new NotSupportedError("E2B's git module (deprecated by E2B too)", "Run git with sandbox.commands.run('git ...'); git is installed.");
377
505
  }
@@ -410,18 +538,21 @@ async function resume(runtime, opts) {
410
538
  const current = state(runtime);
411
539
  if (current === "stopped")
412
540
  throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
541
+ const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
413
542
  if (current === "paused") {
414
543
  await guard("sandbox", () => runtime.wake({
415
544
  ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
416
545
  ...request(opts),
417
546
  }));
418
- return;
419
547
  }
420
- if (opts.timeoutMs !== undefined) {
421
- 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);
422
551
  if (later > 1000)
423
552
  await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
424
553
  }
554
+ if (asked !== undefined && asked > (keepers.get(runtime.id)?.until ?? 0))
555
+ keepUntil(runtime, asked);
425
556
  }
426
557
  async function stop(runtime, opts) {
427
558
  if (state(runtime) === "stopped")
@@ -430,13 +561,15 @@ async function stop(runtime, opts) {
430
561
  return true;
431
562
  }
432
563
  async function extendTo(runtime, timeoutMs, opts) {
433
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
434
- throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
435
- const later = Date.now() + timeoutMs - Date.parse(runtime.info.expiresAt);
436
- if (later < -1000)
437
- 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;
438
570
  if (later > 1000)
439
571
  await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
572
+ keepUntil(runtime, asked);
440
573
  }
441
574
  async function pause(runtime, opts) {
442
575
  if (opts.keepMemory === false)
@@ -451,46 +584,109 @@ async function pause(runtime, opts) {
451
584
  }
452
585
  const RUNNING = ["starting", "running", "resuming"];
453
586
  const PAUSED = ["pausing", "paused"];
454
- /** 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. */
455
591
  export class SandboxPaginator {
456
592
  #client;
593
+ #request;
457
594
  #filter;
595
+ #opts;
596
+ #pageSize;
597
+ #local;
458
598
  #page;
599
+ #all;
600
+ #offset;
459
601
  #hasNext = true;
460
602
  constructor(client, opts) {
461
603
  this.#client = client;
604
+ this.#request = request(opts);
605
+ this.#opts = opts;
462
606
  const query = opts.query ?? {};
463
- if (query.template !== undefined && !STOCK_TEMPLATES.has(query.template))
464
- throw new NotSupportedError("Listing by template", "Filter by metadata instead: give sandboxes a metadata key when you create them.");
465
- if (query.startedAfter !== undefined)
466
- throw new NotSupportedError("Listing by start time (startedAfter)", "List them all and filter on getInfo().startedAt.");
467
- if (opts.order === "desc")
468
- throw new NotSupportedError("Newest-first listing (order: 'desc')", "Runtime lists oldest first; reverse the list yourself.");
469
- if (opts.nextToken !== undefined)
470
- 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;
471
616
  const states = query.state?.length ? query.state : ["running", "paused"];
472
617
  this.#filter = {
473
618
  state: states.flatMap((one) => (one === "running" ? [...RUNNING] : [...PAUSED])),
474
619
  ...(query.metadata && Object.keys(query.metadata).length ? { labels: query.metadata } : {}),
475
- ...(opts.limit ? { limit: Math.min(opts.limit, 100) } : {}),
620
+ limit: this.#local ? 100 : Math.min(this.#pageSize, 100),
476
621
  };
477
622
  }
478
623
  get hasNext() {
479
624
  return this.#hasNext;
480
625
  }
626
+ /** Where the next page starts; `Sandbox.list({ nextToken })` resumes there. */
481
627
  get nextToken() {
482
- return this.#page?.nextCursor ?? undefined;
628
+ return this.#hasNext && this.#offset > 0 ? `runtime:${this.#offset}` : undefined;
483
629
  }
484
630
  async nextItems() {
631
+ this.#request.signal?.throwIfAborted();
485
632
  if (!this.#hasNext)
486
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
+ }
487
641
  const previous = this.#page;
488
- const page = await guard("other", async () => previous ? await previous.next() : await this.#client.sandboxes.list(this.#filter));
642
+ const page = await guard("other", async () => previous
643
+ ? await previous.next()
644
+ : await this.#client.sandboxes.list(this.#filter, this.#request));
489
645
  this.#page = page ?? undefined;
490
646
  this.#hasNext = Boolean(page?.hasMore);
491
- 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()));
492
672
  }
493
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
+ }
494
690
  async function e2bMetrics(runtime, opts) {
495
691
  const start = opts.start === undefined ? Date.now() - 15 * 60_000 : new Date(opts.start).getTime();
496
692
  const end = opts.end === undefined ? Date.now() : new Date(opts.end).getTime();