withruntime 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/DAYTONA.md +40 -34
  2. package/E2B.md +56 -57
  3. package/VERCEL.md +21 -19
  4. package/compatibility-lock.json +8 -84
  5. package/dist/cli.js +73 -2
  6. package/dist/client.d.ts +12 -0
  7. package/dist/client.js +27 -0
  8. package/dist/compat/lease.d.ts +36 -0
  9. package/dist/compat/lease.js +87 -0
  10. package/dist/daytona/computer-use.d.ts +162 -0
  11. package/dist/daytona/computer-use.js +324 -0
  12. package/dist/daytona/context.d.ts +10 -0
  13. package/dist/daytona/context.js +24 -0
  14. package/dist/daytona/daytona.d.ts +20 -0
  15. package/dist/daytona/daytona.js +123 -16
  16. package/dist/daytona/filesystem.js +8 -0
  17. package/dist/daytona/index.d.ts +2 -26
  18. package/dist/daytona/index.js +2 -7
  19. package/dist/daytona/process.d.ts +63 -6
  20. package/dist/daytona/process.js +224 -23
  21. package/dist/daytona/sandbox.d.ts +19 -5
  22. package/dist/daytona/sandbox.js +39 -25
  23. package/dist/e2b/client.d.ts +5 -0
  24. package/dist/e2b/client.js +5 -1
  25. package/dist/e2b/commands.d.ts +15 -6
  26. package/dist/e2b/commands.js +83 -18
  27. package/dist/e2b/errors.d.ts +7 -0
  28. package/dist/e2b/errors.js +20 -0
  29. package/dist/e2b/filesystem.d.ts +2 -1
  30. package/dist/e2b/filesystem.js +43 -6
  31. package/dist/e2b/index.d.ts +2 -2
  32. package/dist/e2b/index.js +1 -1
  33. package/dist/e2b/pty.js +6 -3
  34. package/dist/e2b/sandbox.d.ts +15 -5
  35. package/dist/e2b/sandbox.js +190 -28
  36. package/dist/e2b/users.d.ts +30 -0
  37. package/dist/e2b/users.js +185 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/mcp.d.ts +2 -1
  40. package/dist/mcp.js +6 -1
  41. package/dist/network-cli.js +28 -0
  42. package/dist/products/browser.d.ts +60 -0
  43. package/dist/products/browser.js +37 -0
  44. package/dist/products/desktop.d.ts +2 -2
  45. package/dist/products/desktop.js +2 -2
  46. package/dist/products/images.d.ts +3 -1
  47. package/dist/products/images.js +14 -2
  48. package/dist/products/index.d.ts +2 -0
  49. package/dist/products/index.js +2 -0
  50. package/dist/products/network-products.d.ts +7 -0
  51. package/dist/products/network-products.js +3 -0
  52. package/dist/products/private-network.d.ts +22 -0
  53. package/dist/products/private-network.js +10 -0
  54. package/dist/sandbox.d.ts +22 -0
  55. package/dist/sandbox.js +47 -0
  56. package/dist/transport.d.ts +1 -1
  57. package/dist/transport.js +1 -1
  58. package/dist/types.d.ts +5 -2
  59. package/dist/vercel/index.d.ts +1 -7
  60. package/dist/vercel/index.js +1 -3
  61. package/dist/vercel/sandbox-user.d.ts +70 -0
  62. package/dist/vercel/sandbox-user.js +206 -0
  63. package/dist/vercel/sandbox.d.ts +47 -12
  64. package/dist/vercel/sandbox.js +201 -42
  65. package/package.json +1 -1
@@ -3,6 +3,8 @@ import { Runtime } from "../client.js";
3
3
  export type RuntimeSandbox = Awaited<ReturnType<Runtime["sandboxes"]["create"]>>;
4
4
  /** What `Sandbox.create` sends to Runtime. */
5
5
  export type RuntimeCreate = NonNullable<Parameters<Runtime["sandboxes"]["create"]>[0]>;
6
+ /** E2B's HTTP version for its API and sandboxes (e2b 2.52.0). */
7
+ export type HttpVersion = "1.1" | "2";
6
8
  /** E2B's connection options, as far as they mean anything on Runtime. */
7
9
  export interface ConnectionOpts {
8
10
  /** A Runtime key. Default: RUNTIME_API_KEY, then E2B_API_KEY if it holds a
@@ -18,6 +20,9 @@ export interface ConnectionOpts {
18
20
  retries?: number;
19
21
  /** Accepted and ignored: Runtime's SDK does not take a logger. */
20
22
  logger?: unknown;
23
+ /** Checked as E2B checks it (and E2B_HTTP_VERSION), then left to Runtime's
24
+ * SDK, which picks its own transport. */
25
+ httpVersion?: HttpVersion;
21
26
  /** E2B endpoints. Refused: this package only talks to Runtime. */
22
27
  domain?: string;
23
28
  apiUrl?: string;
@@ -1,5 +1,5 @@
1
1
  import { Runtime } from "../client.js";
2
- import { AuthenticationError, NotSupportedError } from "./errors.js";
2
+ import { AuthenticationError, InvalidArgumentError, NotSupportedError } from "./errors.js";
3
3
  const env = (name) => typeof process === "undefined" ? undefined : process.env[name];
4
4
  const isE2BKey = (key) => key.startsWith("e2b_");
5
5
  /** Refuses the E2B-only connection options: they point at E2B, and quietly
@@ -18,6 +18,10 @@ export function refuseE2BConnection(opts = {}) {
18
18
  ? "Set HTTPS_PROXY in the environment instead; Runtime's SDK reads it."
19
19
  : "Remove it: this package talks only to Runtime (RUNTIME_API_URL overrides the API origin).");
20
20
  }
21
+ // E2B refuses any other version, from the option or the environment.
22
+ const version = opts.httpVersion ?? (env("E2B_HTTP_VERSION") || undefined);
23
+ if (version !== undefined && version !== "1.1" && version !== "2")
24
+ throw new InvalidArgumentError(`${opts.httpVersion === undefined ? "E2B_HTTP_VERSION" : "httpVersion"} must be '1.1' or '2', got '${version}'`);
21
25
  if (opts.debug)
22
26
  throw new NotSupportedError("E2B's debug mode (a local envd)", "Remove debug: true; there is no local Runtime guest to connect to.");
23
27
  }
@@ -1,5 +1,5 @@
1
1
  import type { Process } from "../sandbox.js";
2
- import type { ProcessInfo as RuntimeProcessInfo } from "../types.js";
2
+ import type { OutputEvent, ProcessInfo as RuntimeProcessInfo } from "../types.js";
3
3
  import type { RequestOptions } from "../transport.js";
4
4
  import type { RuntimeSandbox } from "./client.js";
5
5
  import { TimeoutError, type CommandResult } from "./errors.js";
@@ -12,9 +12,9 @@ export interface CommandRequestOpts {
12
12
  export interface CommandStartOpts extends CommandRequestOpts {
13
13
  background?: boolean;
14
14
  cwd?: string;
15
- /** Only the default user. Runtime runs commands as `runtime`, the sandbox's
16
- * owner, with passwordless sudo; E2B's `root` is refused rather than
17
- * silently run as someone else. */
15
+ /** "user" (the default) is the sandbox's own user, `runtime`; any other
16
+ * user that exists in the sandbox, `root` included, runs it through
17
+ * `sudo -u`. An unknown user is refused, never created. */
18
18
  user?: Username;
19
19
  envs?: Record<string, string>;
20
20
  onStdout?: (data: string) => void | Promise<void>;
@@ -47,7 +47,6 @@ export interface SandboxContext {
47
47
  /** E2B's pids are numbers; Runtime's process ids are strings. The number is a
48
48
  * 31-bit FNV-1a hash of the id, so every client derives the same one. */
49
49
  export declare function pidOf(processId: string): number;
50
- export declare function refuseUser(user: Username | undefined): void;
51
50
  export declare function timeoutFor(timeoutMs: number | undefined): number;
52
51
  /** One budget for lookup and mutation, including retries and queued input. */
53
52
  export declare function commandRequest(opts: CommandRequestOpts, defaultTimeoutMs?: number): RequestOptions;
@@ -88,13 +87,23 @@ export declare class Commands {
88
87
  /** Attaches to a running command; output from now on reaches the handle. */
89
88
  connect(pid: number, opts?: CommandConnectOpts): Promise<CommandHandle>;
90
89
  }
90
+ /** A command whose output this handle reads from the request that started
91
+ * it: its process id, the rest of its events, the controller that ends the
92
+ * read, and the Runtime process, fetched only when something needs it. */
93
+ export interface StartedStream {
94
+ started: true;
95
+ id: string;
96
+ events: AsyncIterator<OutputEvent>;
97
+ abort: AbortController;
98
+ process: () => Promise<Process>;
99
+ }
91
100
  /** A command started in the background: E2B's CommandHandle over a Runtime
92
101
  * process. Output streams into `stdout`/`stderr` and the callbacks as it
93
102
  * arrives. */
94
103
  export declare class CommandHandle {
95
104
  #private;
96
105
  readonly pid: number;
97
- constructor(process: Process, options: {
106
+ constructor(source: Process | StartedStream, options: {
98
107
  stdin: boolean;
99
108
  timeoutMs: number;
100
109
  cursor?: number;
@@ -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,10 +14,6 @@ export function pidOf(processId) {
10
14
  }
11
15
  return hash >>> 1 || 1;
12
16
  }
13
- export 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
17
  export function timeoutFor(timeoutMs) {
18
18
  if (timeoutMs === undefined)
19
19
  return DEFAULT_TIMEOUT_MS;
@@ -121,14 +121,61 @@ export class Commands {
121
121
  }
122
122
  async run(cmd, opts = {}) {
123
123
  const opening = connectionRequest(opts, this.#ctx.requestTimeoutMs);
124
- refuseUser(opts.user);
125
124
  const handle = await (async () => {
125
+ const user = await runAs(this.#ctx, opts.user, opening.request);
126
126
  await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`, opening.request);
127
127
  opening.request.signal?.throwIfAborted();
128
- return this.#start(cmd, opts, opening.timeoutMs, opening.request, opening.deadline);
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);
129
135
  })().catch(opening.failure);
130
136
  return opts.background ? handle : handle.wait();
131
137
  }
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
+ }
132
179
  async #start(cmd, opts, timeoutMs, request, deadline) {
133
180
  request.signal?.throwIfAborted();
134
181
  const process = await guard("sandbox", () => this.#ctx.runtime.spawn(cmd, {
@@ -224,11 +271,12 @@ async function deliver(signal, callback) {
224
271
  * arrives. */
225
272
  export class CommandHandle {
226
273
  pid;
274
+ #source;
227
275
  #process;
228
276
  #stdin;
229
277
  #timeoutMs;
230
278
  #requestTimeoutMs;
231
- #abort = new AbortController();
279
+ #abort;
232
280
  #done;
233
281
  #stdout = "";
234
282
  #stderr = "";
@@ -237,9 +285,10 @@ export class CommandHandle {
237
285
  #failure;
238
286
  #disconnected = false;
239
287
  #deadline;
240
- constructor(process, options) {
241
- this.#process = process;
242
- 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);
243
292
  this.#stdin = options.stdin;
244
293
  this.#timeoutMs = options.timeoutMs;
245
294
  this.#requestTimeoutMs = options.requestTimeoutMs;
@@ -260,14 +309,17 @@ export class CommandHandle {
260
309
  const signal = options.signal
261
310
  ? AbortSignal.any([options.signal, this.#abort.signal])
262
311
  : this.#abort.signal;
312
+ const source = this.#source;
263
313
  try {
264
314
  const outputOptions = {
265
315
  ...(options.cursor ? { cursor: options.cursor } : {}),
266
316
  signal,
267
317
  };
268
- const output = options.pty
269
- ? this.#process.outputBytes(outputOptions)
270
- : this.#process.output(outputOptions);
318
+ const output = "started" in source
319
+ ? { [Symbol.asyncIterator]: () => source.events }
320
+ : options.pty
321
+ ? source.outputBytes(outputOptions)
322
+ : source.output(outputOptions);
271
323
  for await (const event of output) {
272
324
  if (this.#disconnected)
273
325
  break;
@@ -288,7 +340,8 @@ export class CommandHandle {
288
340
  this.#truncated = true;
289
341
  }
290
342
  else if (event.type === "exit") {
291
- this.#exit = { exitCode: event.exitCode, timedOut: event.timedOut };
343
+ const exit = event;
344
+ this.#exit = { exitCode: exit.exitCode, timedOut: exit.timedOut };
292
345
  }
293
346
  }
294
347
  }
@@ -348,12 +401,22 @@ export class CommandHandle {
348
401
  this.#abort.abort();
349
402
  await Promise.resolve();
350
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
+ }
351
413
  /** Kills the command with SIGKILL. False when it had already ended. */
352
414
  async kill() {
353
415
  if (this.#exit)
354
416
  return false;
355
417
  try {
356
- await guard("sandbox", () => this.#process.kill("SIGKILL", commandRequest({}, this.#requestTimeoutMs)));
418
+ const process = await this.#runtimeProcess();
419
+ await guard("sandbox", () => process.kill("SIGKILL", commandRequest({}, this.#requestTimeoutMs)));
357
420
  return true;
358
421
  }
359
422
  catch (error) {
@@ -366,12 +429,14 @@ export class CommandHandle {
366
429
  const request = commandRequest(opts, this.#requestTimeoutMs);
367
430
  if (!this.#stdin)
368
431
  throw new InvalidArgumentError("The command was not started with stdin: true, so its input is closed.");
369
- await guard("sandbox", () => this.#process.write(data, request));
432
+ const process = await this.#runtimeProcess();
433
+ await guard("sandbox", () => process.write(data, request));
370
434
  }
371
435
  async closeStdin(opts = {}) {
372
436
  const request = commandRequest(opts, this.#requestTimeoutMs);
373
437
  if (!this.#stdin)
374
438
  throw new InvalidArgumentError("The command was not started with stdin: true.");
375
- await guard("sandbox", () => this.#process.write("", { ...request, eof: true }));
439
+ const process = await this.#runtimeProcess();
440
+ await guard("sandbox", () => process.write("", { ...request, eof: true }));
376
441
  }
377
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,8 @@ 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
15
  export { Pty, type PtyCreateOpts, type PtyConnectOpts } from "./pty.js";
16
16
  export { Template, TemplateBase, Volume, Secret, waitForPort, waitForURL, waitForProcess, waitForFile, waitForTimeout, } from "./unsupported.js";
17
17
  export default Sandbox;
package/dist/e2b/index.js CHANGED
@@ -3,7 +3,7 @@ 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
7
  export { Pty } from "./pty.js";
8
8
  export { Template, TemplateBase, Volume, Secret, waitForPort, waitForURL, waitForProcess, waitForFile, waitForTimeout, } from "./unsupported.js";
9
9
  export default Sandbox;
package/dist/e2b/pty.js CHANGED
@@ -1,5 +1,6 @@
1
- import { CommandHandle, commandRequest, connectionRequest, killProcess, refuseUser, resolveProcess, } from "./commands.js";
1
+ import { CommandHandle, commandRequest, connectionRequest, killProcess, resolveProcess, } from "./commands.js";
2
2
  import { guard, InvalidArgumentError, NotSupportedError } from "./errors.js";
3
+ import { runAs, shellAs } from "./users.js";
3
4
  function dimensions(size) {
4
5
  for (const value of [size.cols, size.rows])
5
6
  if (!Number.isSafeInteger(value) || value <= 0)
@@ -13,16 +14,18 @@ export class Pty {
13
14
  this.ctx = ctx;
14
15
  }
15
16
  async create(opts) {
16
- refuseUser(opts.user);
17
17
  const pty = dimensions(opts);
18
18
  const opening = connectionRequest(opts, this.ctx.requestTimeoutMs);
19
19
  if (typeof opts.onData !== "function")
20
20
  throw new InvalidArgumentError("onData must be a function.");
21
21
  const env = { TERM: "xterm-256color", LANG: "C.UTF-8", LC_ALL: "C.UTF-8", ...opts.envs };
22
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);
23
25
  await this.ctx.ensureHome(opts.cwd, opening.request);
24
26
  opening.request.signal?.throwIfAborted();
25
- return this.ctx.runtime.spawn(["/bin/bash", "-i", "-l"], {
27
+ const shell = user ? shellAs(user, opts.cwd !== undefined) : ["/bin/bash", "-i", "-l"];
28
+ return this.ctx.runtime.spawn(shell, {
26
29
  pty,
27
30
  stdin: "pipe",
28
31
  outputEncoding: "base64",
@@ -30,8 +30,9 @@ export interface SandboxOpts extends ConnectionOpts {
30
30
  /** The sandbox's own environment: every command, terminal and code run in it
31
31
  * gets these, whoever connects. Values are never shown again. */
32
32
  envs?: Record<string, string>;
33
- /** Default 300 000. Runtime's leases run 60 s to 1 hour; shorter rounds up
34
- * 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. */
35
36
  timeoutMs?: number;
36
37
  /** Accepted and ignored, as E2B ignores it. */
37
38
  secure?: boolean;
@@ -88,6 +89,10 @@ export type SandboxListOpts = SandboxApiOpts & {
88
89
  limit?: number;
89
90
  nextToken?: string;
90
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>;
91
96
  export declare class Sandbox {
92
97
  #private;
93
98
  protected readonly requestTimeoutMs: number;
@@ -118,8 +123,9 @@ export declare class Sandbox {
118
123
  /** Stops the sandbox. False when it was not found or had already ended. */
119
124
  static kill(sandboxId: string, opts?: SandboxApiOpts): Promise<boolean>;
120
125
  kill(opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<boolean>;
121
- /** Sets the sandbox to end `timeoutMs` from now. Runtime leases only move
122
- * 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. */
123
129
  static setTimeout(sandboxId: string, timeoutMs: number, opts?: SandboxApiOpts): Promise<void>;
124
130
  setTimeout(timeoutMs: number, opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<void>;
125
131
  static getInfo(sandboxId: string, opts?: SandboxApiOpts): Promise<SandboxInfo>;
@@ -183,11 +189,15 @@ export declare class Sandbox {
183
189
  getMcpUrl(): never;
184
190
  getMcpToken(): Promise<never>;
185
191
  }
186
- /** 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. */
187
196
  export declare class SandboxPaginator {
188
197
  #private;
189
198
  constructor(client: Runtime, opts: SandboxListOpts);
190
199
  get hasNext(): boolean;
200
+ /** Where the next page starts; `Sandbox.list({ nextToken })` resumes there. */
191
201
  get nextToken(): string | undefined;
192
202
  nextItems(): Promise<SandboxInfo[]>;
193
203
  }