withruntime 0.9.0 → 0.10.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 (90) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +13 -7
  3. package/E2B.md +12 -8
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +13 -3
  7. package/compatibility-lock.json +84 -8
  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 +189 -54
  21. package/dist/client.d.ts +29 -3
  22. package/dist/client.js +40 -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/credentials.d.ts +6 -1
  35. package/dist/credentials.js +107 -38
  36. package/dist/daytona/daytona.js +11 -4
  37. package/dist/daytona/filesystem.d.ts +17 -4
  38. package/dist/daytona/filesystem.js +121 -38
  39. package/dist/daytona/index.d.ts +1 -1
  40. package/dist/daytona/sandbox.d.ts +1 -1
  41. package/dist/daytona/sandbox.js +16 -7
  42. package/dist/e2b/client.js +1 -1
  43. package/dist/e2b/commands.d.ts +30 -8
  44. package/dist/e2b/commands.js +142 -54
  45. package/dist/e2b/index.d.ts +1 -0
  46. package/dist/e2b/index.js +1 -0
  47. package/dist/e2b/pty.d.ts +24 -0
  48. package/dist/e2b/pty.js +80 -0
  49. package/dist/e2b/sandbox.d.ts +2 -1
  50. package/dist/e2b/sandbox.js +49 -15
  51. package/dist/file-source.d.ts +16 -0
  52. package/dist/file-source.js +162 -0
  53. package/dist/freestyle/index.d.ts +9 -1
  54. package/dist/freestyle/index.js +38 -5
  55. package/dist/index.d.ts +2 -1
  56. package/dist/login.js +16 -10
  57. package/dist/modal/index.js +2 -1
  58. package/dist/network-cli.js +111 -43
  59. package/dist/openai-agents/index.js +9 -45
  60. package/dist/page.d.ts +1 -1
  61. package/dist/page.js +2 -2
  62. package/dist/products/index.d.ts +12 -0
  63. package/dist/products/index.js +10 -0
  64. package/dist/products/observability.d.ts +44 -0
  65. package/dist/products/observability.js +6 -0
  66. package/dist/products/previews.d.ts +8 -2
  67. package/dist/products/previews.js +11 -3
  68. package/dist/products/volumes.d.ts +42 -0
  69. package/dist/products/volumes.js +36 -0
  70. package/dist/products/watch.js +61 -8
  71. package/dist/proxy.js +3 -1
  72. package/dist/runloop/index.d.ts +13 -27
  73. package/dist/runloop/index.js +149 -52
  74. package/dist/sandbox.d.ts +37 -8
  75. package/dist/sandbox.js +432 -68
  76. package/dist/snapshots.d.ts +9 -4
  77. package/dist/sprites/index.js +69 -33
  78. package/dist/tar.d.ts +3 -1
  79. package/dist/tar.js +140 -14
  80. package/dist/transport.d.ts +4 -7
  81. package/dist/transport.js +205 -122
  82. package/dist/vercel/command.d.ts +1 -1
  83. package/dist/vercel/command.js +3 -2
  84. package/dist/vercel/errors.d.ts +1 -1
  85. package/dist/vercel/errors.js +7 -1
  86. package/dist/vercel/sandbox.d.ts +8 -8
  87. package/dist/vercel/sandbox.js +154 -59
  88. package/dist/vercel/snapshot.d.ts +3 -2
  89. package/dist/vercel/snapshot.js +9 -5
  90. package/package.json +2 -1
@@ -10,18 +10,57 @@ export function pidOf(processId) {
10
10
  }
11
11
  return hash >>> 1 || 1;
12
12
  }
13
- function refuseUser(user) {
13
+ export function refuseUser(user) {
14
14
  if (user !== undefined && user !== "user")
15
15
  throw new NotSupportedError(`Running as the user "${user}"`, 'Runtime runs commands as its sandbox owner, with passwordless sudo: prefix the command with "sudo" to run it as root.');
16
16
  }
17
- function timeoutFor(timeoutMs) {
17
+ export function timeoutFor(timeoutMs) {
18
18
  if (timeoutMs === undefined)
19
19
  return DEFAULT_TIMEOUT_MS;
20
20
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0)
21
21
  throw new InvalidArgumentError("timeoutMs must be a nonnegative whole number.");
22
22
  return timeoutMs;
23
23
  }
24
- function timedOut(timeoutMs) {
24
+ /** One budget for lookup and mutation, including retries and queued input. */
25
+ export function commandRequest(opts, defaultTimeoutMs = 60_000) {
26
+ opts.signal?.throwIfAborted();
27
+ const timeoutMs = timeoutFor(opts.requestTimeoutMs ?? defaultTimeoutMs);
28
+ const deadline = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
29
+ const signal = opts.signal && deadline ? AbortSignal.any([opts.signal, deadline]) : (opts.signal ?? deadline);
30
+ return { timeoutMs: 0, ...(signal ? { signal } : {}) };
31
+ }
32
+ export async function findProcess(ctx, pid, options) {
33
+ options.signal?.throwIfAborted();
34
+ const processes = await guard("sandbox", () => ctx.runtime.processes.list(options));
35
+ options.signal?.throwIfAborted();
36
+ const matches = processes.filter((info) => pidOf(info.id) === pid && info.state === "running");
37
+ if (matches.length > 1)
38
+ throw new SandboxError(`More than one running command has pid ${pid}; use the native process id instead.`);
39
+ return matches[0];
40
+ }
41
+ export async function resolveProcess(ctx, pid, options) {
42
+ const info = await findProcess(ctx, pid, options);
43
+ if (!info)
44
+ throw new SandboxError(`No running command with pid ${pid}.`);
45
+ return guard("sandbox", () => ctx.runtime.processes.get(info.id, options));
46
+ }
47
+ export async function killProcess(ctx, pid, options) {
48
+ const info = await findProcess(ctx, pid, options);
49
+ if (!info)
50
+ return false;
51
+ try {
52
+ const process = await guard("sandbox", () => ctx.runtime.processes.get(info.id, options));
53
+ options.signal?.throwIfAborted();
54
+ await guard("sandbox", () => process.kill("SIGKILL", options));
55
+ return true;
56
+ }
57
+ catch (error) {
58
+ if (error instanceof SandboxError && error.statusCode === 404)
59
+ return false;
60
+ throw error;
61
+ }
62
+ }
63
+ export function timedOut(timeoutMs) {
25
64
  return new TimeoutError(`Command timed out after ${timeoutMs} ms: this error is likely due to exceeding 'timeoutMs'. ` +
26
65
  "Pass a larger 'timeoutMs', or 0 for no limit.");
27
66
  }
@@ -42,6 +81,25 @@ function toResult(result) {
42
81
  ...(result.truncated ? { truncated: true } : {}),
43
82
  };
44
83
  }
84
+ /** The subscription deadline includes the opening handshake. */
85
+ export function connectionRequest(opts, defaultTimeoutMs) {
86
+ const timeoutMs = timeoutFor(opts.timeoutMs);
87
+ const deadline = timeoutMs > 0 ? performance.now() + timeoutMs : undefined;
88
+ const connectionTimeout = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
89
+ const signal = connectionTimeout && opts.signal
90
+ ? AbortSignal.any([connectionTimeout, opts.signal])
91
+ : (connectionTimeout ?? opts.signal);
92
+ return {
93
+ timeoutMs,
94
+ deadline,
95
+ request: commandRequest({ ...opts, signal }, defaultTimeoutMs),
96
+ failure: (error) => {
97
+ if (connectionTimeout?.aborted)
98
+ throw timedOut(timeoutMs);
99
+ throw error;
100
+ },
101
+ };
102
+ }
45
103
  function settle(result, timeoutMs) {
46
104
  if (result.timedOut)
47
105
  throw timedOut(timeoutMs);
@@ -62,76 +120,69 @@ export class Commands {
62
120
  return envs && Object.keys(envs).length ? { env: envs } : {};
63
121
  }
64
122
  async run(cmd, opts = {}) {
65
- opts.signal?.throwIfAborted();
123
+ const opening = connectionRequest(opts, this.#ctx.requestTimeoutMs);
66
124
  refuseUser(opts.user);
67
- await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`);
68
- const timeoutMs = timeoutFor(opts.timeoutMs);
69
- const handle = await this.#start(cmd, opts, timeoutMs);
125
+ const handle = await (async () => {
126
+ await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`, opening.request);
127
+ opening.request.signal?.throwIfAborted();
128
+ return this.#start(cmd, opts, opening.timeoutMs, opening.request, opening.deadline);
129
+ })().catch(opening.failure);
70
130
  return opts.background ? handle : handle.wait();
71
131
  }
72
- async #start(cmd, opts, timeoutMs) {
73
- opts.signal?.throwIfAborted();
74
- const requestDeadline = opts.requestTimeoutMs
75
- ? AbortSignal.timeout(opts.requestTimeoutMs)
76
- : undefined;
77
- const signals = [opts.signal, requestDeadline].filter((signal) => signal !== undefined);
132
+ async #start(cmd, opts, timeoutMs, request, deadline) {
133
+ request.signal?.throwIfAborted();
78
134
  const process = await guard("sandbox", () => this.#ctx.runtime.spawn(cmd, {
79
135
  ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
80
136
  ...this.#env(opts.envs),
81
137
  ...(opts.stdin ? { stdin: "pipe" } : {}),
82
- ...(signals.length ? { signal: AbortSignal.any(signals) } : {}),
138
+ request,
83
139
  }));
84
140
  return new CommandHandle(process, {
85
141
  stdin: opts.stdin === true,
86
142
  timeoutMs,
143
+ deadline,
144
+ requestTimeoutMs: this.#ctx.requestTimeoutMs,
87
145
  signal: opts.signal,
88
146
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
89
147
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
90
148
  });
91
149
  }
92
150
  /** Running commands. */
93
- async list(_opts = {}) {
94
- const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list());
151
+ async list(opts = {}) {
152
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
153
+ const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list(request));
95
154
  return processes.filter((info) => info.state === "running").map(describe);
96
155
  }
97
- async #find(pid) {
98
- const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list());
99
- return processes.find((info) => pidOf(info.id) === pid && info.state === "running");
100
- }
101
- async #process(pid) {
102
- const info = await this.#find(pid);
103
- if (!info)
104
- throw new SandboxError(`No running command with pid ${pid}.`);
105
- return guard("sandbox", () => this.#ctx.runtime.processes.get(info.id));
106
- }
107
156
  /** Kills a command with SIGKILL, as E2B does. False when there is none. */
108
- async kill(pid, _opts = {}) {
109
- const info = await this.#find(pid);
110
- if (!info)
111
- return false;
112
- const process = await guard("sandbox", () => this.#ctx.runtime.processes.get(info.id));
113
- await guard("sandbox", () => process.kill("SIGKILL"));
114
- return true;
157
+ async kill(pid, opts = {}) {
158
+ return killProcess(this.#ctx, pid, commandRequest(opts, this.#ctx.requestTimeoutMs));
115
159
  }
116
160
  /** Sends input to a command started with `stdin: true`. */
117
- async sendStdin(pid, data, _opts = {}) {
118
- const process = await this.#process(pid);
161
+ async sendStdin(pid, data, opts = {}) {
162
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
163
+ const process = await resolveProcess(this.#ctx, pid, request);
119
164
  if (!process.info.stdinOpen)
120
165
  throw new InvalidArgumentError(`The command with pid ${pid} was not started with stdin: true, so its input is closed.`);
121
- await guard("sandbox", () => process.write(data));
166
+ request.signal?.throwIfAborted();
167
+ await guard("sandbox", () => process.write(data, request));
122
168
  }
123
- async closeStdin(pid, _opts = {}) {
124
- const process = await this.#process(pid);
125
- await guard("sandbox", () => process.write("", { eof: true }));
169
+ async closeStdin(pid, opts = {}) {
170
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
171
+ const process = await resolveProcess(this.#ctx, pid, request);
172
+ request.signal?.throwIfAborted();
173
+ await guard("sandbox", () => process.write("", { ...request, eof: true }));
126
174
  }
127
175
  /** Attaches to a running command; output from now on reaches the handle. */
128
176
  async connect(pid, opts = {}) {
129
- const process = await this.#process(pid);
177
+ const opening = connectionRequest(opts, this.#ctx.requestTimeoutMs);
178
+ const process = await resolveProcess(this.#ctx, pid, opening.request).catch(opening.failure);
130
179
  return new CommandHandle(process, {
131
180
  stdin: process.info.stdinOpen,
132
- timeoutMs: timeoutFor(opts.timeoutMs),
181
+ timeoutMs: opening.timeoutMs,
182
+ deadline: opening.deadline,
133
183
  signal: opts.signal,
134
184
  cursor: process.info.outputBytes,
185
+ requestTimeoutMs: this.#ctx.requestTimeoutMs,
135
186
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
136
187
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
137
188
  });
@@ -146,6 +197,28 @@ function describe(info) {
146
197
  cwd: info.cwd,
147
198
  };
148
199
  }
200
+ /** Reader cancellation also releases a callback whose promise never settles. */
201
+ async function deliver(signal, callback) {
202
+ signal.throwIfAborted();
203
+ let cancelled;
204
+ const aborted = new Promise((_, reject) => {
205
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
206
+ cancelled = () => reject(signal.reason);
207
+ signal.addEventListener("abort", cancelled, { once: true });
208
+ });
209
+ try {
210
+ await Promise.race([
211
+ Promise.resolve().then(() => {
212
+ signal.throwIfAborted();
213
+ return callback();
214
+ }),
215
+ aborted,
216
+ ]);
217
+ }
218
+ finally {
219
+ signal.removeEventListener("abort", cancelled);
220
+ }
221
+ }
149
222
  /** A command started in the background: E2B's CommandHandle over a Runtime
150
223
  * process. Output streams into `stdout`/`stderr` and the callbacks as it
151
224
  * arrives. */
@@ -154,6 +227,7 @@ export class CommandHandle {
154
227
  #process;
155
228
  #stdin;
156
229
  #timeoutMs;
230
+ #requestTimeoutMs;
157
231
  #abort = new AbortController();
158
232
  #done;
159
233
  #stdout = "";
@@ -168,13 +242,16 @@ export class CommandHandle {
168
242
  this.pid = pidOf(process.id);
169
243
  this.#stdin = options.stdin;
170
244
  this.#timeoutMs = options.timeoutMs;
245
+ this.#requestTimeoutMs = options.requestTimeoutMs;
171
246
  // E2B's timeout limits the subscription. It never asks us to kill the
172
247
  // process: the sandbox lease still bounds the workload after disconnect.
173
248
  if (options.timeoutMs > 0) {
174
249
  this.#deadline = setTimeout(() => {
175
250
  this.#failure = timedOut(options.timeoutMs);
176
251
  this.#abort.abort();
177
- }, options.timeoutMs);
252
+ }, options.deadline === undefined
253
+ ? options.timeoutMs
254
+ : Math.max(0, options.deadline - performance.now()));
178
255
  this.#deadline.unref?.();
179
256
  }
180
257
  this.#done = this.#follow(options);
@@ -184,19 +261,28 @@ export class CommandHandle {
184
261
  ? AbortSignal.any([options.signal, this.#abort.signal])
185
262
  : this.#abort.signal;
186
263
  try {
187
- for await (const event of this.#process.output({
264
+ const outputOptions = {
188
265
  ...(options.cursor ? { cursor: options.cursor } : {}),
189
266
  signal,
190
- })) {
191
- if (this.#disconnected || signal.aborted)
267
+ };
268
+ const output = options.pty
269
+ ? this.#process.outputBytes(outputOptions)
270
+ : this.#process.output(outputOptions);
271
+ for await (const event of output) {
272
+ if (this.#disconnected)
192
273
  break;
193
- if (event.type === "stdout") {
274
+ signal.throwIfAborted();
275
+ if ((event.type === "stdout" || event.type === "stderr") &&
276
+ event.data instanceof Uint8Array) {
277
+ await deliver(signal, () => options.onData?.(event.data));
278
+ }
279
+ else if (event.type === "stdout" && typeof event.data === "string") {
194
280
  this.#stdout += event.data;
195
- await options.onStdout?.(event.data);
281
+ await deliver(signal, () => options.onStdout?.(event.data));
196
282
  }
197
- else if (event.type === "stderr") {
283
+ else if (event.type === "stderr" && typeof event.data === "string") {
198
284
  this.#stderr += event.data;
199
- await options.onStderr?.(event.data);
285
+ await deliver(signal, () => options.onStderr?.(event.data));
200
286
  }
201
287
  else if (event.type === "truncated") {
202
288
  this.#truncated = true;
@@ -267,7 +353,7 @@ export class CommandHandle {
267
353
  if (this.#exit)
268
354
  return false;
269
355
  try {
270
- await guard("sandbox", () => this.#process.kill("SIGKILL"));
356
+ await guard("sandbox", () => this.#process.kill("SIGKILL", commandRequest({}, this.#requestTimeoutMs)));
271
357
  return true;
272
358
  }
273
359
  catch (error) {
@@ -276,14 +362,16 @@ export class CommandHandle {
276
362
  throw error;
277
363
  }
278
364
  }
279
- async sendStdin(data, _opts = {}) {
365
+ async sendStdin(data, opts = {}) {
366
+ const request = commandRequest(opts, this.#requestTimeoutMs);
280
367
  if (!this.#stdin)
281
368
  throw new InvalidArgumentError("The command was not started with stdin: true, so its input is closed.");
282
- await guard("sandbox", () => this.#process.write(data));
369
+ await guard("sandbox", () => this.#process.write(data, request));
283
370
  }
284
- async closeStdin(_opts = {}) {
371
+ async closeStdin(opts = {}) {
372
+ const request = commandRequest(opts, this.#requestTimeoutMs);
285
373
  if (!this.#stdin)
286
374
  throw new InvalidArgumentError("The command was not started with stdin: true.");
287
- await guard("sandbox", () => this.#process.write("", { eof: true }));
375
+ await guard("sandbox", () => this.#process.write("", { ...request, eof: true }));
288
376
  }
289
377
  }
@@ -12,6 +12,7 @@ export { Commands, CommandHandle, pidOf, type CommandStartOpts, type CommandConn
12
12
  export { Filesystem, FileType, type EntryInfo, type WriteInfo, type WriteEntry, type FilesystemRequestOpts, type FilesystemReadOpts, type FilesystemWriteOpts, type FilesystemListOpts, FilesystemEventType, type FilesystemEvent, type WatchOpts, type E2BWatchHandle as WatchHandle, } from "./filesystem.js";
13
13
  export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, type CommandResult, } from "./errors.js";
14
14
  export type { ConnectionOpts, RuntimeOpts } from "./client.js";
15
+ export { Pty, type PtyCreateOpts, type PtyConnectOpts } from "./pty.js";
15
16
  export { Template, TemplateBase, Volume, Secret, waitForPort, waitForURL, waitForProcess, waitForFile, waitForTimeout, } from "./unsupported.js";
16
17
  export default Sandbox;
17
18
  /** E2B's client with bound options: `new E2B({ apiKey }).Sandbox.create()`. */
package/dist/e2b/index.js CHANGED
@@ -4,6 +4,7 @@ export { Sandbox, SandboxPaginator, DEFAULT_SHAPE, DEFAULT_TIMEOUT_MS, } from ".
4
4
  export { Commands, CommandHandle, pidOf, } from "./commands.js";
5
5
  export { Filesystem, FileType, FilesystemEventType, } from "./filesystem.js";
6
6
  export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, } from "./errors.js";
7
+ export { Pty } from "./pty.js";
7
8
  export { Template, TemplateBase, Volume, Secret, waitForPort, waitForURL, waitForProcess, waitForFile, waitForTimeout, } from "./unsupported.js";
8
9
  export default Sandbox;
9
10
  /** E2B's client with bound options: `new E2B({ apiKey }).Sandbox.create()`. */
@@ -0,0 +1,24 @@
1
+ import { CommandHandle, type CommandRequestOpts, type SandboxContext, type Username } from "./commands.js";
2
+ export interface PtyCreateOpts extends CommandRequestOpts {
3
+ cols: number;
4
+ rows: number;
5
+ onData: (data: Uint8Array) => void | Promise<void>;
6
+ timeoutMs?: number;
7
+ user?: Username;
8
+ envs?: Record<string, string>;
9
+ cwd?: string;
10
+ }
11
+ export type PtyConnectOpts = CommandRequestOpts & Pick<PtyCreateOpts, "onData" | "timeoutMs">;
12
+ /** E2B's PTY methods over native terminal processes with lossless output. */
13
+ export declare class Pty {
14
+ private readonly ctx;
15
+ constructor(ctx: SandboxContext);
16
+ create(opts: PtyCreateOpts): Promise<CommandHandle>;
17
+ connect(pid: number, opts?: PtyConnectOpts): Promise<CommandHandle>;
18
+ sendInput(pid: number, data: Uint8Array, opts?: CommandRequestOpts): Promise<void>;
19
+ resize(pid: number, size: {
20
+ cols: number;
21
+ rows: number;
22
+ }, opts?: CommandRequestOpts): Promise<void>;
23
+ kill(pid: number, opts?: CommandRequestOpts): Promise<boolean>;
24
+ }
@@ -0,0 +1,80 @@
1
+ import { CommandHandle, commandRequest, connectionRequest, killProcess, refuseUser, resolveProcess, } from "./commands.js";
2
+ import { guard, InvalidArgumentError, NotSupportedError } from "./errors.js";
3
+ function dimensions(size) {
4
+ for (const value of [size.cols, size.rows])
5
+ if (!Number.isSafeInteger(value) || value <= 0)
6
+ throw new InvalidArgumentError("PTY rows and columns must be positive whole numbers.");
7
+ return { cols: size.cols, rows: size.rows };
8
+ }
9
+ /** E2B's PTY methods over native terminal processes with lossless output. */
10
+ export class Pty {
11
+ ctx;
12
+ constructor(ctx) {
13
+ this.ctx = ctx;
14
+ }
15
+ async create(opts) {
16
+ refuseUser(opts.user);
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
+ await this.ctx.ensureHome(opts.cwd, opening.request);
24
+ opening.request.signal?.throwIfAborted();
25
+ return this.ctx.runtime.spawn(["/bin/bash", "-i", "-l"], {
26
+ pty,
27
+ stdin: "pipe",
28
+ outputEncoding: "base64",
29
+ env,
30
+ ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
31
+ request: opening.request,
32
+ });
33
+ }).catch(opening.failure);
34
+ return new CommandHandle(process, {
35
+ stdin: false,
36
+ pty: true,
37
+ onData: opts.onData,
38
+ timeoutMs: opening.timeoutMs,
39
+ deadline: opening.deadline,
40
+ signal: opts.signal,
41
+ requestTimeoutMs: this.ctx.requestTimeoutMs,
42
+ });
43
+ }
44
+ async connect(pid, opts) {
45
+ const opening = connectionRequest(opts ?? {}, this.ctx.requestTimeoutMs);
46
+ const process = await resolveProcess(this.ctx, pid, opening.request).catch(opening.failure);
47
+ if (!process.info.pty || process.info.outputEncoding !== "base64")
48
+ throw new NotSupportedError("Connecting to a legacy text-output PTY", "Create the PTY through sandbox.pty.create first.");
49
+ return new CommandHandle(process, {
50
+ stdin: false,
51
+ pty: true,
52
+ onData: opts?.onData,
53
+ timeoutMs: opening.timeoutMs,
54
+ deadline: opening.deadline,
55
+ cursor: process.info.outputBytes,
56
+ signal: opts?.signal,
57
+ requestTimeoutMs: this.ctx.requestTimeoutMs,
58
+ });
59
+ }
60
+ async sendInput(pid, data, opts = {}) {
61
+ const request = commandRequest(opts, this.ctx.requestTimeoutMs);
62
+ const process = await resolveProcess(this.ctx, pid, request);
63
+ if (!process.info.pty)
64
+ throw new InvalidArgumentError(`The process with pid ${pid} is not a PTY.`);
65
+ request.signal?.throwIfAborted();
66
+ await guard("sandbox", () => process.write(data, request));
67
+ }
68
+ async resize(pid, size, opts = {}) {
69
+ const pty = dimensions(size);
70
+ const request = commandRequest(opts, this.ctx.requestTimeoutMs);
71
+ const process = await resolveProcess(this.ctx, pid, request);
72
+ if (!process.info.pty)
73
+ throw new InvalidArgumentError(`The process with pid ${pid} is not a PTY.`);
74
+ request.signal?.throwIfAborted();
75
+ await guard("sandbox", () => process.resize(pty.cols, pty.rows, request));
76
+ }
77
+ kill(pid, opts = {}) {
78
+ return killProcess(this.ctx, pid, commandRequest(opts, this.ctx.requestTimeoutMs));
79
+ }
80
+ }
@@ -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: {
@@ -93,6 +94,7 @@ export declare class Sandbox {
93
94
  protected static readonly defaultTemplate: string;
94
95
  readonly files: Filesystem;
95
96
  readonly commands: Commands;
97
+ readonly pty: Pty;
96
98
  /** Use Sandbox.create or Sandbox.connect. */
97
99
  constructor(runtime: RuntimeSandbox, client: Runtime, requestTimeoutMs?: number);
98
100
  get sandboxId(): string;
@@ -167,7 +169,6 @@ export declare class Sandbox {
167
169
  * its host, `<port>-<id>.<domain>`, once the share has landed. Anyone with
168
170
  * the address can reach it; a browser sees a one-time page naming Runtime. */
169
171
  getPublicHost(port: number): Promise<string>;
170
- get pty(): never;
171
172
  get git(): never;
172
173
  /** E2B's metrics, from Runtime's measured readings (`sbx.metrics()`): CPU
173
174
  * as a percent of the sandbox's vCPUs and resident memory, one entry per
@@ -2,6 +2,7 @@ import { clientFor, request, } from "./client.js";
2
2
  import { Commands } from "./commands.js";
3
3
  import { guard, InvalidArgumentError, NotSupportedError, 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. */
@@ -65,12 +66,13 @@ function refuseCreate(opts) {
65
66
  }
66
67
  /** Where a template sends the create: the stock image, a Runtime image by
67
68
  * name or id, or a Runtime snapshot by id. */
68
- async function resolveTemplate(client, template) {
69
+ async function resolveTemplate(client, template, options) {
70
+ options.signal?.throwIfAborted();
69
71
  if (STOCK_TEMPLATES.has(template))
70
72
  return {};
71
73
  if (UUID.test(template)) {
72
74
  try {
73
- await client.images.get(template);
75
+ await client.images.get(template, options);
74
76
  return { image: template };
75
77
  }
76
78
  catch (error) {
@@ -79,7 +81,7 @@ async function resolveTemplate(client, template) {
79
81
  }
80
82
  return { snapshot: template };
81
83
  }
82
- const page = await client.images.list({ name: template, state: "ready", limit: 1 });
84
+ const page = await client.images.list({ name: template, state: "ready", limit: 1 }, options);
83
85
  const image = page.data[0];
84
86
  if (image)
85
87
  return { image: image.id };
@@ -135,6 +137,7 @@ export class Sandbox {
135
137
  static defaultTemplate = "base";
136
138
  files;
137
139
  commands;
140
+ pty;
138
141
  #client;
139
142
  #runtime;
140
143
  #home;
@@ -146,12 +149,14 @@ export class Sandbox {
146
149
  this.#runtime = runtime;
147
150
  this.#client = client;
148
151
  const ctx = {
149
- ensureHome: (text) => this.#ensureHome(text),
152
+ ensureHome: (text, options) => this.#ensureHome(text, options),
153
+ requestTimeoutMs: this.requestTimeoutMs,
150
154
  };
151
155
  // A getter, so the modules see the sandbox as it is after each refresh.
152
156
  Object.defineProperty(ctx, "runtime", { get: () => this.#runtime });
153
157
  this.files = new Filesystem(ctx);
154
158
  this.commands = new Commands(ctx);
159
+ this.pty = new Pty(ctx);
155
160
  }
156
161
  get sandboxId() {
157
162
  return this.#runtime.id;
@@ -167,21 +172,47 @@ export class Sandbox {
167
172
  /** E2B's home is /home/user and Runtime's is /workspace. The first time a
168
173
  * path or command names /home/user, it is made a link to /workspace (unless
169
174
  * something is already there), so paths written for E2B work. */
170
- #ensureHome(text) {
175
+ #ensureHome(text, options = {}) {
176
+ const signal = options.signal;
177
+ signal?.throwIfAborted();
178
+ const wait = (pending) => {
179
+ if (!signal)
180
+ return pending;
181
+ return new Promise((resolve, reject) => {
182
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
183
+ const abort = () => reject(signal.reason);
184
+ signal.addEventListener("abort", abort, { once: true });
185
+ pending.then(() => {
186
+ signal.removeEventListener("abort", abort);
187
+ resolve();
188
+ }, (error) => {
189
+ signal.removeEventListener("abort", abort);
190
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- passes the pending call's own failure through unchanged
191
+ reject(error);
192
+ });
193
+ });
194
+ };
171
195
  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"));
196
+ return wait(this.#home ?? Promise.resolve());
197
+ const pending = (async () => {
198
+ const result = await guard("sandbox", () => this.#runtime.exec("[ -e /home/user ] || sudo ln -s /workspace /home/user", { signal }));
175
199
  if (result.exitCode !== 0 && typeof process !== "undefined")
176
200
  process.emitWarning(`Could not link /home/user to /workspace: ${result.stderr.trim()}`, {
177
201
  code: "RUNTIME_E2B_HOME",
178
202
  });
179
203
  })();
180
- return this.#home;
204
+ this.#home = pending;
205
+ pending.catch(() => {
206
+ if (this.#home === pending)
207
+ this.#home = undefined;
208
+ });
209
+ return wait(pending);
181
210
  }
182
211
  static async create(templateOrOpts, maybeOpts) {
183
212
  const opts = (typeof templateOrOpts === "string" ? maybeOpts : templateOrOpts) ?? {};
184
213
  const template = (typeof templateOrOpts === "string" ? templateOrOpts : opts.template) ?? this.defaultTemplate;
214
+ const options = request(opts);
215
+ options.signal?.throwIfAborted();
185
216
  refuseCreate(opts);
186
217
  const client = clientFor(opts);
187
218
  const input = {
@@ -196,7 +227,8 @@ export class Sandbox {
196
227
  ...(opts.envs && Object.keys(opts.envs).length ? { env: opts.envs } : {}),
197
228
  ...(opts.allowInternetAccess === false ? { network: { internet: false } } : {}),
198
229
  };
199
- const source = await guard("other", () => resolveTemplate(client, template));
230
+ const source = await guard("other", () => resolveTemplate(client, template, options));
231
+ options.signal?.throwIfAborted();
200
232
  const create = {
201
233
  // A snapshot carries its own machine; everything else gets E2B's shape.
202
234
  ...(source.snapshot ? {} : DEFAULT_SHAPE),
@@ -204,7 +236,7 @@ export class Sandbox {
204
236
  ...source,
205
237
  ...opts.runtime?.create,
206
238
  };
207
- const runtime = await guard("sandbox", () => client.sandboxes.create(create, request(opts)));
239
+ const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
208
240
  return new this(runtime, client, opts.requestTimeoutMs);
209
241
  }
210
242
  /** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
@@ -369,9 +401,6 @@ export class Sandbox {
369
401
  return share;
370
402
  }
371
403
  // ---- 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
404
  get git() {
376
405
  throw new NotSupportedError("E2B's git module (deprecated by E2B too)", "Run git with sandbox.commands.run('git ...'); git is installed.");
377
406
  }
@@ -454,11 +483,13 @@ const PAUSED = ["pausing", "paused"];
454
483
  /** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`. */
455
484
  export class SandboxPaginator {
456
485
  #client;
486
+ #request;
457
487
  #filter;
458
488
  #page;
459
489
  #hasNext = true;
460
490
  constructor(client, opts) {
461
491
  this.#client = client;
492
+ this.#request = request(opts);
462
493
  const query = opts.query ?? {};
463
494
  if (query.template !== undefined && !STOCK_TEMPLATES.has(query.template))
464
495
  throw new NotSupportedError("Listing by template", "Filter by metadata instead: give sandboxes a metadata key when you create them.");
@@ -482,10 +513,13 @@ export class SandboxPaginator {
482
513
  return this.#page?.nextCursor ?? undefined;
483
514
  }
484
515
  async nextItems() {
516
+ this.#request.signal?.throwIfAborted();
485
517
  if (!this.#hasNext)
486
518
  throw new SandboxError("No more items to fetch.");
487
519
  const previous = this.#page;
488
- const page = await guard("other", async () => previous ? await previous.next() : await this.#client.sandboxes.list(this.#filter));
520
+ const page = await guard("other", async () => previous
521
+ ? await previous.next()
522
+ : await this.#client.sandboxes.list(this.#filter, this.#request));
489
523
  this.#page = page ?? undefined;
490
524
  this.#hasNext = Boolean(page?.hasMore);
491
525
  return (page?.data ?? []).map(infoOf);
@@ -0,0 +1,16 @@
1
+ export type StreamUploadSource = string | Blob | AsyncIterable<Uint8Array> | ReadableStream<Uint8Array>;
2
+ export type UploadScope = {
3
+ signal?: AbortSignal;
4
+ check(): void;
5
+ wait<T>(pending: Promise<T>): Promise<T>;
6
+ };
7
+ export type PreparedFileSource = {
8
+ size: number;
9
+ sha256: string;
10
+ chunkAt(offset: number, length: number): Promise<Uint8Array>;
11
+ close(): Promise<void>;
12
+ };
13
+ /** ARCHITECTURE.md section 10, Folders over HTTP: unknown-length inputs use
14
+ * bounded private disk staging; known files and Blobs remain repeatable inputs.
15
+ * Size and digest are established before the remote upload can begin. */
16
+ export declare function prepareFileSource(input: StreamUploadSource, scope: UploadScope): Promise<PreparedFileSource>;