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
@@ -1,5 +1,9 @@
1
- import { CommandExitError, guard, InvalidArgumentError, NotSupportedError, SandboxError, TimeoutError, translate, } from "./errors.js";
1
+ import { CommandExitError, guard, InvalidArgumentError, SandboxError, TimeoutError, translate, } from "./errors.js";
2
+ import { commandAs, runAs } from "./users.js";
2
3
  const DEFAULT_TIMEOUT_MS = 60_000;
4
+ /** How long a command may run on Runtime: a day, E2B's longest sandbox. E2B's
5
+ * own `timeoutMs` bounds only the connection, so the process gets this. */
6
+ const PROCESS_LIFETIME_MS = 86_400_000;
3
7
  /** E2B's pids are numbers; Runtime's process ids are strings. The number is a
4
8
  * 31-bit FNV-1a hash of the id, so every client derives the same one. */
5
9
  export function pidOf(processId) {
@@ -10,18 +14,53 @@ export function pidOf(processId) {
10
14
  }
11
15
  return hash >>> 1 || 1;
12
16
  }
13
- function refuseUser(user) {
14
- if (user !== undefined && user !== "user")
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
- }
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,116 @@ 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();
66
- 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);
123
+ const opening = connectionRequest(opts, this.#ctx.requestTimeoutMs);
124
+ const handle = await (async () => {
125
+ const user = await runAs(this.#ctx, opts.user, opening.request);
126
+ await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`, opening.request);
127
+ opening.request.signal?.throwIfAborted();
128
+ const command = user ? commandAs(user, cmd, opts.cwd !== undefined) : cmd;
129
+ // A command waited on here is read from its first byte by the request
130
+ // that starts it, so Runtime holds a fast writer back rather than
131
+ // dropping what it printed before the first read: the result is whole.
132
+ return opts.background || opts.stdin
133
+ ? this.#start(command, opts, opening.timeoutMs, opening.request, opening.deadline)
134
+ : this.#stream(command, opts, opening.timeoutMs, opening.request, opening.deadline);
135
+ })().catch(opening.failure);
70
136
  return opts.background ? handle : handle.wait();
71
137
  }
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);
138
+ async #stream(cmd, opts, timeoutMs, request, deadline) {
139
+ const abort = new AbortController();
140
+ const signal = opts.signal ? AbortSignal.any([opts.signal, abort.signal]) : abort.signal;
141
+ const stopOpening = () => abort.abort(request.signal?.reason);
142
+ request.signal?.addEventListener("abort", stopOpening, { once: true });
143
+ const stream = this.#ctx.runtime.execStream(cmd, {
144
+ ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
145
+ ...this.#env(opts.envs),
146
+ timeoutMs: PROCESS_LIFETIME_MS,
147
+ signal,
148
+ });
149
+ const events = stream[Symbol.asyncIterator]();
150
+ try {
151
+ const first = await guard("sandbox", () => events.next());
152
+ if (first.done || first.value.type !== "start")
153
+ throw new SandboxError("The command's output began without a start.");
154
+ const runtime = this.#ctx.runtime;
155
+ const processId = first.value.processId;
156
+ return new CommandHandle({
157
+ started: true,
158
+ id: processId,
159
+ events,
160
+ abort,
161
+ process: () => runtime.processes.get(processId, commandRequest({}, this.#ctx.requestTimeoutMs)),
162
+ }, {
163
+ stdin: false,
164
+ timeoutMs,
165
+ deadline,
166
+ requestTimeoutMs: this.#ctx.requestTimeoutMs,
167
+ ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
168
+ ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
169
+ });
170
+ }
171
+ catch (error) {
172
+ abort.abort();
173
+ throw error;
174
+ }
175
+ finally {
176
+ request.signal?.removeEventListener("abort", stopOpening);
177
+ }
178
+ }
179
+ async #start(cmd, opts, timeoutMs, request, deadline) {
180
+ request.signal?.throwIfAborted();
78
181
  const process = await guard("sandbox", () => this.#ctx.runtime.spawn(cmd, {
79
182
  ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
80
183
  ...this.#env(opts.envs),
81
184
  ...(opts.stdin ? { stdin: "pipe" } : {}),
82
- ...(signals.length ? { signal: AbortSignal.any(signals) } : {}),
185
+ request,
83
186
  }));
84
187
  return new CommandHandle(process, {
85
188
  stdin: opts.stdin === true,
86
189
  timeoutMs,
190
+ deadline,
191
+ requestTimeoutMs: this.#ctx.requestTimeoutMs,
87
192
  signal: opts.signal,
88
193
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
89
194
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
90
195
  });
91
196
  }
92
197
  /** Running commands. */
93
- async list(_opts = {}) {
94
- const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list());
198
+ async list(opts = {}) {
199
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
200
+ const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list(request));
95
201
  return processes.filter((info) => info.state === "running").map(describe);
96
202
  }
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
203
  /** 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;
204
+ async kill(pid, opts = {}) {
205
+ return killProcess(this.#ctx, pid, commandRequest(opts, this.#ctx.requestTimeoutMs));
115
206
  }
116
207
  /** Sends input to a command started with `stdin: true`. */
117
- async sendStdin(pid, data, _opts = {}) {
118
- const process = await this.#process(pid);
208
+ async sendStdin(pid, data, opts = {}) {
209
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
210
+ const process = await resolveProcess(this.#ctx, pid, request);
119
211
  if (!process.info.stdinOpen)
120
212
  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));
213
+ request.signal?.throwIfAborted();
214
+ await guard("sandbox", () => process.write(data, request));
122
215
  }
123
- async closeStdin(pid, _opts = {}) {
124
- const process = await this.#process(pid);
125
- await guard("sandbox", () => process.write("", { eof: true }));
216
+ async closeStdin(pid, opts = {}) {
217
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
218
+ const process = await resolveProcess(this.#ctx, pid, request);
219
+ request.signal?.throwIfAborted();
220
+ await guard("sandbox", () => process.write("", { ...request, eof: true }));
126
221
  }
127
222
  /** Attaches to a running command; output from now on reaches the handle. */
128
223
  async connect(pid, opts = {}) {
129
- const process = await this.#process(pid);
224
+ const opening = connectionRequest(opts, this.#ctx.requestTimeoutMs);
225
+ const process = await resolveProcess(this.#ctx, pid, opening.request).catch(opening.failure);
130
226
  return new CommandHandle(process, {
131
227
  stdin: process.info.stdinOpen,
132
- timeoutMs: timeoutFor(opts.timeoutMs),
228
+ timeoutMs: opening.timeoutMs,
229
+ deadline: opening.deadline,
133
230
  signal: opts.signal,
134
231
  cursor: process.info.outputBytes,
232
+ requestTimeoutMs: this.#ctx.requestTimeoutMs,
135
233
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
136
234
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
137
235
  });
@@ -146,15 +244,39 @@ function describe(info) {
146
244
  cwd: info.cwd,
147
245
  };
148
246
  }
247
+ /** Reader cancellation also releases a callback whose promise never settles. */
248
+ async function deliver(signal, callback) {
249
+ signal.throwIfAborted();
250
+ let cancelled;
251
+ const aborted = new Promise((_, reject) => {
252
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
253
+ cancelled = () => reject(signal.reason);
254
+ signal.addEventListener("abort", cancelled, { once: true });
255
+ });
256
+ try {
257
+ await Promise.race([
258
+ Promise.resolve().then(() => {
259
+ signal.throwIfAborted();
260
+ return callback();
261
+ }),
262
+ aborted,
263
+ ]);
264
+ }
265
+ finally {
266
+ signal.removeEventListener("abort", cancelled);
267
+ }
268
+ }
149
269
  /** A command started in the background: E2B's CommandHandle over a Runtime
150
270
  * process. Output streams into `stdout`/`stderr` and the callbacks as it
151
271
  * arrives. */
152
272
  export class CommandHandle {
153
273
  pid;
274
+ #source;
154
275
  #process;
155
276
  #stdin;
156
277
  #timeoutMs;
157
- #abort = new AbortController();
278
+ #requestTimeoutMs;
279
+ #abort;
158
280
  #done;
159
281
  #stdout = "";
160
282
  #stderr = "";
@@ -163,18 +285,22 @@ export class CommandHandle {
163
285
  #failure;
164
286
  #disconnected = false;
165
287
  #deadline;
166
- constructor(process, options) {
167
- this.#process = process;
168
- this.pid = pidOf(process.id);
288
+ constructor(source, options) {
289
+ this.#source = source;
290
+ this.#abort = "started" in source ? source.abort : new AbortController();
291
+ this.pid = pidOf(source.id);
169
292
  this.#stdin = options.stdin;
170
293
  this.#timeoutMs = options.timeoutMs;
294
+ this.#requestTimeoutMs = options.requestTimeoutMs;
171
295
  // E2B's timeout limits the subscription. It never asks us to kill the
172
296
  // process: the sandbox lease still bounds the workload after disconnect.
173
297
  if (options.timeoutMs > 0) {
174
298
  this.#deadline = setTimeout(() => {
175
299
  this.#failure = timedOut(options.timeoutMs);
176
300
  this.#abort.abort();
177
- }, options.timeoutMs);
301
+ }, options.deadline === undefined
302
+ ? options.timeoutMs
303
+ : Math.max(0, options.deadline - performance.now()));
178
304
  this.#deadline.unref?.();
179
305
  }
180
306
  this.#done = this.#follow(options);
@@ -183,26 +309,39 @@ export class CommandHandle {
183
309
  const signal = options.signal
184
310
  ? AbortSignal.any([options.signal, this.#abort.signal])
185
311
  : this.#abort.signal;
312
+ const source = this.#source;
186
313
  try {
187
- for await (const event of this.#process.output({
314
+ const outputOptions = {
188
315
  ...(options.cursor ? { cursor: options.cursor } : {}),
189
316
  signal,
190
- })) {
191
- if (this.#disconnected || signal.aborted)
317
+ };
318
+ const output = "started" in source
319
+ ? { [Symbol.asyncIterator]: () => source.events }
320
+ : options.pty
321
+ ? source.outputBytes(outputOptions)
322
+ : source.output(outputOptions);
323
+ for await (const event of output) {
324
+ if (this.#disconnected)
192
325
  break;
193
- if (event.type === "stdout") {
326
+ signal.throwIfAborted();
327
+ if ((event.type === "stdout" || event.type === "stderr") &&
328
+ event.data instanceof Uint8Array) {
329
+ await deliver(signal, () => options.onData?.(event.data));
330
+ }
331
+ else if (event.type === "stdout" && typeof event.data === "string") {
194
332
  this.#stdout += event.data;
195
- await options.onStdout?.(event.data);
333
+ await deliver(signal, () => options.onStdout?.(event.data));
196
334
  }
197
- else if (event.type === "stderr") {
335
+ else if (event.type === "stderr" && typeof event.data === "string") {
198
336
  this.#stderr += event.data;
199
- await options.onStderr?.(event.data);
337
+ await deliver(signal, () => options.onStderr?.(event.data));
200
338
  }
201
339
  else if (event.type === "truncated") {
202
340
  this.#truncated = true;
203
341
  }
204
342
  else if (event.type === "exit") {
205
- this.#exit = { exitCode: event.exitCode, timedOut: event.timedOut };
343
+ const exit = event;
344
+ this.#exit = { exitCode: exit.exitCode, timedOut: exit.timedOut };
206
345
  }
207
346
  }
208
347
  }
@@ -262,12 +401,22 @@ export class CommandHandle {
262
401
  this.#abort.abort();
263
402
  await Promise.resolve();
264
403
  }
404
+ /** The Runtime process, fetched once when a started stream needs it. */
405
+ #runtimeProcess() {
406
+ const source = this.#source;
407
+ if (!("started" in source))
408
+ return Promise.resolve(source);
409
+ this.#process ??= guard("sandbox", source.process);
410
+ this.#process.catch(() => (this.#process = undefined));
411
+ return this.#process;
412
+ }
265
413
  /** Kills the command with SIGKILL. False when it had already ended. */
266
414
  async kill() {
267
415
  if (this.#exit)
268
416
  return false;
269
417
  try {
270
- await guard("sandbox", () => this.#process.kill("SIGKILL"));
418
+ const process = await this.#runtimeProcess();
419
+ await guard("sandbox", () => process.kill("SIGKILL", commandRequest({}, this.#requestTimeoutMs)));
271
420
  return true;
272
421
  }
273
422
  catch (error) {
@@ -276,14 +425,18 @@ export class CommandHandle {
276
425
  throw error;
277
426
  }
278
427
  }
279
- async sendStdin(data, _opts = {}) {
428
+ async sendStdin(data, opts = {}) {
429
+ const request = commandRequest(opts, this.#requestTimeoutMs);
280
430
  if (!this.#stdin)
281
431
  throw new InvalidArgumentError("The command was not started with stdin: true, so its input is closed.");
282
- await guard("sandbox", () => this.#process.write(data));
432
+ const process = await this.#runtimeProcess();
433
+ await guard("sandbox", () => process.write(data, request));
283
434
  }
284
- async closeStdin(_opts = {}) {
435
+ async closeStdin(opts = {}) {
436
+ const request = commandRequest(opts, this.#requestTimeoutMs);
285
437
  if (!this.#stdin)
286
438
  throw new InvalidArgumentError("The command was not started with stdin: true.");
287
- await guard("sandbox", () => this.#process.write("", { eof: true }));
439
+ const process = await this.#runtimeProcess();
440
+ await guard("sandbox", () => process.write("", { ...request, eof: true }));
288
441
  }
289
442
  }
@@ -46,6 +46,13 @@ export declare class NotSupportedError extends SandboxError {
46
46
  readonly alternative: string;
47
47
  constructor(feature: string, alternative: string, message?: string);
48
48
  }
49
+ /** Runtime's addition, for `getHost` and `getPublicHost`: a free-trial
50
+ * sandbox shares a port only privately, so a request needs the port's token,
51
+ * and a host name alone cannot carry one. The message says what works. A
52
+ * NotSupportedError, so code that catches those catches this too. */
53
+ export declare class PublicPreviewNotAllowedError extends NotSupportedError {
54
+ constructor(sandboxId: string, port: number | undefined, message?: string);
55
+ }
49
56
  /** What `commands.run` resolves with, as in E2B. */
50
57
  export interface CommandResult {
51
58
  exitCode: number;
@@ -72,6 +72,23 @@ export class NotSupportedError extends SandboxError {
72
72
  this.code = "not_supported";
73
73
  }
74
74
  }
75
+ /** Runtime's addition, for `getHost` and `getPublicHost`: a free-trial
76
+ * sandbox shares a port only privately, so a request needs the port's token,
77
+ * and a host name alone cannot carry one. The message says what works. A
78
+ * NotSupportedError, so code that catches those catches this too. */
79
+ export class PublicPreviewNotAllowedError extends NotSupportedError {
80
+ constructor(sandboxId, port, message) {
81
+ const where = port === undefined ? "<port>" : String(port);
82
+ const alternative = `For an address that works on the trial, use (await sandbox.runtime.previews.create(${where})).urlWithToken: ` +
83
+ "it carries the token, in a browser, fetch or curl. For other paths on it, send the token as the " +
84
+ "x-runtime-preview-token header or the runtime_preview_token query parameter. A public host needs a paid " +
85
+ "sandbox, which is the account owner's decision.";
86
+ super("A public address on a free-trial sandbox", alternative, message ??
87
+ `Sandbox ${sandboxId} runs on the free trial, where a shared port is private: every request needs the ` +
88
+ `port's token, and a host name alone cannot carry one. ${alternative}`);
89
+ this.code = "public_preview_not_allowed";
90
+ }
91
+ }
75
92
  /** Thrown by `commands.run` and `CommandHandle.wait` when the command exits
76
93
  * with a code other than 0. It carries the result, as in E2B. */
77
94
  export class CommandExitError extends SandboxError {
@@ -113,6 +130,9 @@ export function translate(error, subject = "other") {
113
130
  // A product switched off on purpose (forks, previews): Runtime's own words
114
131
  // say what is off and what to use meanwhile.
115
132
  out = new NotSupportedError(error.code.replace(/_unavailable$/, ""), error.hint ?? "", message);
133
+ else if (error.code === "public_preview_not_allowed")
134
+ // A 403 that is about the sandbox's funding, not the key.
135
+ out = new PublicPreviewNotAllowedError("", undefined, message);
116
136
  else if (error.status === 401 || error.status === 403)
117
137
  out = new AuthenticationError(message);
118
138
  else if (error.code === "file_not_found" || (error.status === 404 && subject === "file"))
@@ -51,7 +51,8 @@ export type WriteEntry = {
51
51
  export interface FilesystemRequestOpts {
52
52
  requestTimeoutMs?: number;
53
53
  signal?: AbortSignal;
54
- /** Only the default user; see CommandStartOpts.user. */
54
+ /** "user" (the default) acts as the sandbox's own user through Runtime's
55
+ * file API; any other existing user, `root` included, through `sudo -u`. */
55
56
  user?: Username;
56
57
  }
57
58
  export interface FilesystemReadOpts extends FilesystemRequestOpts {
@@ -1,5 +1,6 @@
1
1
  import { request } from "./client.js";
2
2
  import { FileNotFoundError, guard, InvalidArgumentError, NotSupportedError, TimeoutError, } from "./errors.js";
3
+ import { FilesAs, runAs } from "./users.js";
3
4
  export var FileType;
4
5
  (function (FileType) {
5
6
  FileType["FILE"] = "file";
@@ -20,8 +21,6 @@ function absolute(path) {
20
21
  return path.startsWith("/") ? path : `${HOME}/${path.replace(/^\.\//, "")}`;
21
22
  }
22
23
  function refuse(opts) {
23
- if (opts.user !== undefined && opts.user !== "user")
24
- throw new NotSupportedError(`File access as the user "${opts.user}"`, "Runtime's file calls act as the sandbox owner; use commands.run('sudo ...') for root-owned paths.");
25
24
  if (opts.metadata && Object.keys(opts.metadata).length)
26
25
  throw new NotSupportedError("File metadata (user.e2b.* extended attributes)", "Keep the metadata beside the file, for example in a JSON file.");
27
26
  }
@@ -80,12 +79,25 @@ export class Filesystem {
80
79
  opts.signal?.throwIfAborted();
81
80
  return absolute(path);
82
81
  }
82
+ /** The calls as another Linux user, or undefined for the sandbox's own. */
83
+ async #as(opts) {
84
+ if (opts.user === undefined || opts.user === "user")
85
+ return undefined;
86
+ const options = request(opts);
87
+ const user = await runAs(this.#ctx, opts.user, options);
88
+ return user ? new FilesAs(this.#ctx, user, options) : undefined;
89
+ }
83
90
  async read(path, opts = {}) {
84
91
  const file = await this.#path(path, opts);
92
+ const as = await this.#as(opts);
85
93
  // A real stream, any size, never the whole file held first.
86
- if (opts.format === "stream")
94
+ if (opts.format === "stream" && !as)
87
95
  return guard("file", () => this.#files.readStream(file, request(opts)));
88
- const bytes = await guard("file", () => this.#files.read(file, request(opts)));
96
+ const bytes = as
97
+ ? await as.read(file)
98
+ : await guard("file", () => this.#files.read(file, request(opts)));
99
+ if (opts.format === "stream")
100
+ return new Blob([bytes]).stream();
89
101
  switch (opts.format) {
90
102
  case "bytes":
91
103
  return bytes;
@@ -109,19 +121,29 @@ export class Filesystem {
109
121
  }
110
122
  async #writeOne(path, data, opts) {
111
123
  const file = await this.#path(path, opts);
124
+ const as = await this.#as(opts);
112
125
  const bytes = await bytesOf(data);
113
- await guard("file", () => this.#files.write(file, bytes, request(opts)));
126
+ if (as)
127
+ await as.write(file, bytes);
128
+ else
129
+ await guard("file", () => this.#files.write(file, bytes, request(opts)));
114
130
  return { name: basename(file), type: FileType.FILE, path: file };
115
131
  }
116
132
  /** A directory's entries, hidden ones included; `depth` goes deeper. */
117
133
  async list(path, opts = {}) {
118
134
  const dir = await this.#path(path, opts);
135
+ const as = await this.#as(opts);
136
+ if (as)
137
+ return (await as.list(dir, opts.depth ?? 1)).map(entryInfo);
119
138
  const entries = await guard("file", () => this.#files.list(dir, { depth: opts.depth ?? 1, hidden: true, ...request(opts) }));
120
139
  return entries.map(entryInfo);
121
140
  }
122
141
  /** Makes a directory and its parents. False when it already existed. */
123
142
  async makeDir(path, opts = {}) {
124
143
  const dir = await this.#path(path, opts);
144
+ const as = await this.#as(opts);
145
+ if (as)
146
+ return as.makeDir(dir);
125
147
  if (await guard("file", () => this.#files.exists(dir, request(opts))))
126
148
  return false;
127
149
  await guard("file", () => this.#files.mkdir(dir, { parents: true, ...request(opts) }));
@@ -131,20 +153,33 @@ export class Filesystem {
131
153
  async rename(oldPath, newPath, opts = {}) {
132
154
  const from = await this.#path(oldPath, opts);
133
155
  const to = await this.#path(newPath, opts);
134
- await guard("file", () => this.#files.rename(from, to, { overwrite: true, ...request(opts) }));
156
+ const as = await this.#as(opts);
157
+ if (as)
158
+ await as.rename(from, to);
159
+ else
160
+ await guard("file", () => this.#files.rename(from, to, { overwrite: true, ...request(opts) }));
135
161
  return this.getInfo(to, opts);
136
162
  }
137
163
  /** Removes a file, or a directory with everything in it. */
138
164
  async remove(path, opts = {}) {
139
165
  const target = await this.#path(path, opts);
166
+ const as = await this.#as(opts);
167
+ if (as)
168
+ return as.remove(target);
140
169
  await guard("file", () => this.#files.remove(target, { recursive: true, ...request(opts) }));
141
170
  }
142
171
  async exists(path, opts = {}) {
143
172
  const target = await this.#path(path, opts);
173
+ const as = await this.#as(opts);
174
+ if (as)
175
+ return as.exists(target);
144
176
  return guard("file", () => this.#files.exists(target, request(opts)));
145
177
  }
146
178
  async getInfo(path, opts = {}) {
147
179
  const target = await this.#path(path, opts);
180
+ const as = await this.#as(opts);
181
+ if (as)
182
+ return entryInfo(await as.stat(target));
148
183
  const stat = await guard("file", () => this.#files.stat(target, request(opts)));
149
184
  if (!stat.exists)
150
185
  throw new FileNotFoundError(`${target} does not exist.`);
@@ -154,6 +189,8 @@ export class Filesystem {
154
189
  * subsecond subscriptions; 0 runs until stopped or the sandbox ends. */
155
190
  async watchDir(path, onEvent, opts = {}) {
156
191
  opts.signal?.throwIfAborted();
192
+ if (opts.user !== undefined && opts.user !== "user")
193
+ throw new NotSupportedError(`Watching a directory as the user "${opts.user}"`, "Watch it as the sandbox's own user (leave user out); Runtime's watch reports changes made by anyone.");
157
194
  if (opts.allowNetworkMounts)
158
195
  throw new NotSupportedError("Watching network mounts", "Watch a local sandbox directory instead.");
159
196
  const lifetime = opts.timeoutMs ?? 60000;
@@ -10,8 +10,9 @@ import { Sandbox } from "./sandbox.js";
10
10
  export { Sandbox, SandboxPaginator, DEFAULT_SHAPE, DEFAULT_TIMEOUT_MS, type SandboxOpts, type SandboxConnectOpts, type SandboxApiOpts, type SandboxInfo, type SandboxListOpts, type SandboxState, type SandboxLifecycle, type SandboxOnTimeout, type SnapshotInfo, } from "./sandbox.js";
11
11
  export { Commands, CommandHandle, pidOf, type CommandStartOpts, type CommandConnectOpts, type CommandRequestOpts, type ProcessInfo, type Username, } from "./commands.js";
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
- export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, type CommandResult, } from "./errors.js";
14
- export type { ConnectionOpts, RuntimeOpts } from "./client.js";
13
+ export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, PublicPreviewNotAllowedError, type CommandResult, } from "./errors.js";
14
+ export type { ConnectionOpts, HttpVersion, 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
@@ -3,7 +3,8 @@ import { Sandbox } from "./sandbox.js";
3
3
  export { Sandbox, SandboxPaginator, DEFAULT_SHAPE, DEFAULT_TIMEOUT_MS, } from "./sandbox.js";
4
4
  export { Commands, CommandHandle, pidOf, } from "./commands.js";
5
5
  export { Filesystem, FileType, FilesystemEventType, } from "./filesystem.js";
6
- export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, } from "./errors.js";
6
+ export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, PublicPreviewNotAllowedError, } 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
+ }