withruntime 0.8.4 → 0.9.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 (91) hide show
  1. package/COMPATIBILITY.md +145 -0
  2. package/E2B.md +23 -13
  3. package/README.md +8 -5
  4. package/README.src.md +8 -5
  5. package/compatibility-lock.json +281 -0
  6. package/dist/ai-harness/index.js +2 -2
  7. package/dist/api-defaults.d.ts +17 -0
  8. package/dist/api-defaults.js +22 -0
  9. package/dist/blaxel/preview.d.ts +1 -1
  10. package/dist/blaxel/preview.js +1 -1
  11. package/dist/blaxel/process.d.ts +1 -1
  12. package/dist/blaxel/process.js +11 -2
  13. package/dist/cli-words.d.ts +27 -0
  14. package/dist/cli-words.js +135 -0
  15. package/dist/cli.js +162 -31
  16. package/dist/client.d.ts +4 -1
  17. package/dist/client.js +8 -2
  18. package/dist/cloudflare/index.d.ts +294 -0
  19. package/dist/cloudflare/index.js +554 -0
  20. package/dist/cloudflare/worker.d.ts +415 -0
  21. package/dist/cloudflare/worker.js +297 -0
  22. package/dist/codesandbox/guest-service.d.ts +31 -0
  23. package/dist/codesandbox/guest-service.js +453 -0
  24. package/dist/codesandbox/index.d.ts +386 -0
  25. package/dist/codesandbox/index.js +1313 -0
  26. package/dist/compat/core.d.ts +39 -0
  27. package/dist/compat/core.js +199 -0
  28. package/dist/compat/files.d.ts +26 -0
  29. package/dist/compat/files.js +115 -0
  30. package/dist/compat/shell.d.ts +4 -0
  31. package/dist/compat/shell.js +165 -0
  32. package/dist/daytona/context.js +2 -1
  33. package/dist/daytona/process.d.ts +1 -1
  34. package/dist/daytona/process.js +27 -4
  35. package/dist/e2b/code-interpreter.d.ts +4 -5
  36. package/dist/e2b/code-interpreter.js +54 -56
  37. package/dist/e2b/commands.d.ts +5 -4
  38. package/dist/e2b/commands.js +37 -29
  39. package/dist/e2b/errors.js +1 -1
  40. package/dist/e2b/filesystem.d.ts +8 -6
  41. package/dist/e2b/filesystem.js +127 -28
  42. package/dist/e2b/sandbox.d.ts +4 -3
  43. package/dist/e2b/sandbox.js +10 -10
  44. package/dist/errors.d.ts +8 -0
  45. package/dist/errors.js +15 -2
  46. package/dist/freestyle/index.d.ts +301 -0
  47. package/dist/freestyle/index.js +310 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/mcp.js +2 -2
  50. package/dist/modal/index.d.ts +205 -0
  51. package/dist/modal/index.js +485 -0
  52. package/dist/network-cli.js +1 -1
  53. package/dist/openai-agents/index.js +9 -26
  54. package/dist/products/audit.d.ts +3 -0
  55. package/dist/products/index.d.ts +2 -0
  56. package/dist/products/index.js +2 -0
  57. package/dist/products/interpreter.d.ts +11 -4
  58. package/dist/products/interpreter.js +33 -9
  59. package/dist/products/jobs.js +3 -2
  60. package/dist/products/limits.d.ts +10 -0
  61. package/dist/products/previews.d.ts +10 -2
  62. package/dist/products/previews.js +3 -2
  63. package/dist/products/tailscale.d.ts +57 -0
  64. package/dist/products/tailscale.js +17 -0
  65. package/dist/products/watch.d.ts +25 -4
  66. package/dist/products/watch.js +75 -14
  67. package/dist/runloop/index.d.ts +340 -0
  68. package/dist/runloop/index.js +438 -0
  69. package/dist/runloop/pagination.d.ts +46 -0
  70. package/dist/runloop/pagination.js +82 -0
  71. package/dist/runloop/snapshots.d.ts +52 -0
  72. package/dist/runloop/snapshots.js +244 -0
  73. package/dist/sandbox.d.ts +40 -9
  74. package/dist/sandbox.js +273 -76
  75. package/dist/snapshots.d.ts +11 -1
  76. package/dist/snapshots.js +11 -1
  77. package/dist/sprites/index.d.ts +251 -0
  78. package/dist/sprites/index.js +552 -0
  79. package/dist/tar.d.ts +8 -1
  80. package/dist/tar.js +185 -35
  81. package/dist/tools/index.js +2 -1
  82. package/dist/transport.d.ts +20 -2
  83. package/dist/transport.js +48 -11
  84. package/dist/types.d.ts +52 -3
  85. package/dist/vercel/command.js +12 -1
  86. package/dist/vercel/errors.d.ts +2 -2
  87. package/dist/vercel/errors.js +12 -1
  88. package/dist/vercel/filesystem.d.ts +21 -18
  89. package/dist/vercel/filesystem.js +95 -48
  90. package/dist/vercel/sandbox.js +5 -5
  91. package/package.json +45 -3
@@ -3,8 +3,7 @@
3
3
  * import { Sandbox } from "withruntime/e2b/code-interpreter";
4
4
  *
5
5
  * runCode runs in Runtime's interpreter (sandbox.runtime.interpreter):
6
- * stateful Python and JavaScript contexts, as in E2B. Other languages throw
7
- * NotSupportedError. */
6
+ * stateful contexts in every E2B interpreter language. */
8
7
  import type { Runtime } from "../client.js";
9
8
  import type { ConnectionOpts } from "./client.js";
10
9
  import { Sandbox as BaseSandbox } from "./sandbox.js";
@@ -13,11 +12,11 @@ export type RunCodeLanguage = "python" | "javascript" | "typescript" | "r" | "ja
13
12
  /** A line of output, as E2B's callbacks receive it. */
14
13
  export declare class OutputMessage {
15
14
  readonly line: string;
16
- /** Unix epoch in nanoseconds (from the client's clock). */
15
+ /** Unix epoch in microseconds (from the client's clock), as in E2B. */
17
16
  readonly timestamp: number;
18
17
  readonly error: boolean;
19
18
  constructor(line: string,
20
- /** Unix epoch in nanoseconds (from the client's clock). */
19
+ /** Unix epoch in microseconds (from the client's clock), as in E2B. */
21
20
  timestamp: number, error: boolean);
22
21
  toString(): string;
23
22
  }
@@ -110,7 +109,7 @@ export interface RunCodeOpts {
110
109
  envs?: Record<string, string>;
111
110
  /** Default 60 000, as in E2B. */
112
111
  timeoutMs?: number;
113
- /** Accepted and ignored: Runtime's SDK allows the run its timeout plus a minute. */
112
+ /** Deadline for receiving response headers; 0 disables it. */
114
113
  requestTimeoutMs?: number;
115
114
  }
116
115
  export interface CreateCodeContextOpts {
@@ -8,7 +8,7 @@ export class OutputMessage {
8
8
  timestamp;
9
9
  error;
10
10
  constructor(line,
11
- /** Unix epoch in nanoseconds (from the client's clock). */
11
+ /** Unix epoch in microseconds (from the client's clock), as in E2B. */
12
12
  timestamp, error) {
13
13
  this.line = line;
14
14
  this.timestamp = timestamp;
@@ -122,50 +122,18 @@ function lines(text) {
122
122
  return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
123
123
  }
124
124
  function language(requested) {
125
- if (requested === undefined || requested === "python")
126
- return "python";
127
- if (requested === "javascript" || requested === "js")
128
- return "javascript";
129
- throw new NotSupportedError(`Running ${requested} code in the interpreter`, requested === "bash"
130
- ? "Use sandbox.commands.run(code)."
131
- : "Runtime's interpreter runs Python and JavaScript; install the language and run it with sandbox.commands.run(...).");
125
+ const selected = requested === "js" ? "javascript" : (requested ?? "python");
126
+ if (["python", "javascript", "typescript", "r", "java", "bash", "go"].includes(selected))
127
+ return selected;
128
+ throw new NotSupportedError(`Running ${requested} code in the interpreter`, "Use Python, JavaScript, TypeScript, R, Java, Bash or Go.");
132
129
  }
133
130
  /** The code interpreter sandbox: everything the base Sandbox has, plus runCode
134
131
  * and code contexts. */
135
132
  export class Sandbox extends BaseSandbox {
136
133
  static defaultTemplate = "code-interpreter-v1";
137
- /** Contexts made to carry the sandbox's envs, one per language. */
138
- #envContexts = new Map();
139
134
  get #interpreter() {
140
135
  return this.runtime.interpreter;
141
136
  }
142
- /** The context a run goes to: the one asked for; else, when the sandbox has
143
- * envs, a context made once with them; else Runtime's default one. */
144
- async #contextFor(lang, context) {
145
- if (context)
146
- return context.id;
147
- if (!Object.keys(this.envs).length)
148
- return undefined;
149
- let id = this.#envContexts.get(lang);
150
- if (!id) {
151
- id = guard("sandbox", async () => {
152
- const made = await this.#interpreter.contexts.create({
153
- id: `e2b-${lang}`,
154
- language: lang,
155
- env: this.envs,
156
- });
157
- return made.id;
158
- }).catch(async (error) => {
159
- // Made already, by another object connected to this sandbox.
160
- const existing = (await this.#interpreter.contexts.list()).find((one) => one.id === `e2b-${lang}`);
161
- if (existing)
162
- return existing.id;
163
- throw error;
164
- });
165
- this.#envContexts.set(lang, id);
166
- }
167
- return id;
168
- }
169
137
  async #inline(result) {
170
138
  const data = { ...result.data };
171
139
  for (const [mime, ref] of Object.entries(result.refs ?? {})) {
@@ -186,28 +154,59 @@ export class Sandbox extends BaseSandbox {
186
154
  if (opts.envs && Object.keys(opts.envs).length)
187
155
  throw new NotSupportedError("Environment variables for one run (runCode envs)", "Make a context with them: sandbox.runtime.interpreter.contexts.create({ env }), then runCode(code, { context: { id } }).");
188
156
  const lang = language(opts.context?.language ?? opts.language);
189
- const context = await this.#contextFor(lang, opts.context);
190
- const now = () => Date.now() * 1_000_000;
191
- const streaming = opts.onStdout || opts.onStderr || opts.onResult || opts.onError;
192
- const execution = await guard("sandbox", () => this.#interpreter.run(code, {
193
- ...(context ? { context } : { language: lang }),
194
- timeoutMs: opts.timeoutMs ?? 60_000,
195
- ...(streaming
196
- ? {
197
- onStdout: (text) => void opts.onStdout?.(new OutputMessage(text, now(), false)),
198
- onStderr: (text) => void opts.onStderr?.(new OutputMessage(text, now(), true)),
199
- onError: (error) => void opts.onError?.(new ExecutionError(error.name, error.value, error.traceback)),
200
- }
201
- : {}),
202
- }));
157
+ const context = opts.context?.id;
158
+ const now = () => Date.now() * 1_000;
159
+ const streamedResults = [];
160
+ const controller = new AbortController();
161
+ const requestTimeout = opts.requestTimeoutMs ?? this.requestTimeoutMs;
162
+ const timeout = opts.timeoutMs ?? 60_000;
163
+ let stage = "Request";
164
+ let timer;
165
+ const arm = (milliseconds) => {
166
+ clearTimeout(timer);
167
+ if (milliseconds)
168
+ timer = setTimeout(() => controller.abort(), milliseconds);
169
+ };
170
+ arm(requestTimeout);
171
+ let execution;
172
+ try {
173
+ execution = await guard("sandbox", () => this.#interpreter.run(code, {
174
+ ...(context ? { context } : { language: lang }),
175
+ // E2B times out its reader; it does not send a kernel deadline.
176
+ timeoutMs: 0,
177
+ requestTimeoutMs: 0,
178
+ interruptOnDisconnect: false,
179
+ signal: controller.signal,
180
+ onResponse: () => {
181
+ stage = "Execution";
182
+ arm(timeout);
183
+ },
184
+ // Stream even without callbacks: the header/body deadlines are separate.
185
+ onStdout: (text) => opts.onStdout?.(new OutputMessage(text, now(), false)),
186
+ onStderr: (text) => opts.onStderr?.(new OutputMessage(text, now(), true)),
187
+ onError: (error) => opts.onError?.(new ExecutionError(error.name, error.value, error.traceback)),
188
+ onResult: async (result) => {
189
+ const mapped = new Result(await this.#inline(result), result.main);
190
+ streamedResults.push(mapped);
191
+ await opts.onResult?.(mapped);
192
+ },
193
+ }));
194
+ }
195
+ catch (error) {
196
+ if (controller.signal.aborted)
197
+ throw new TimeoutError(`${stage} timed out — the '${stage === "Request" ? "requestTimeoutMs" : "timeoutMs"}' option can be used to increase this timeout`);
198
+ throw error;
199
+ }
200
+ finally {
201
+ clearTimeout(timer);
202
+ }
203
203
  if (execution.status === "timeout")
204
204
  throw new TimeoutError(`Execution timed out after ${opts.timeoutMs ?? 60_000} ms: pass a larger 'timeoutMs'.`);
205
205
  if (execution.status === "lost")
206
206
  throw new SandboxError("The interpreter lost this run (its context stopped); run it again.");
207
- const results = await Promise.all(execution.results.map(async (result) => new Result(await this.#inline(result), result.main)));
208
- if (opts.onResult)
209
- for (const result of results)
210
- await opts.onResult(result);
207
+ const results = streamedResults.length === execution.results.length
208
+ ? streamedResults
209
+ : await Promise.all(execution.results.map(async (result) => new Result(await this.#inline(result), result.main)));
211
210
  return new Execution(results, { stdout: lines(execution.stdout), stderr: lines(execution.stderr) }, execution.error
212
211
  ? new ExecutionError(execution.error.name, execution.error.value, execution.error.traceback)
213
212
  : undefined, execution.executionCount ?? undefined);
@@ -217,7 +216,6 @@ export class Sandbox extends BaseSandbox {
217
216
  const made = await guard("sandbox", () => this.#interpreter.contexts.create({
218
217
  language: lang,
219
218
  ...(opts.cwd ? { cwd: opts.cwd } : {}),
220
- ...(Object.keys(this.envs).length ? { env: this.envs } : {}),
221
219
  }));
222
220
  return { id: made.id, language: made.language, cwd: made.cwd };
223
221
  }
@@ -18,7 +18,7 @@ export interface CommandStartOpts extends CommandRequestOpts {
18
18
  onStdout?: (data: string) => void | Promise<void>;
19
19
  onStderr?: (data: string) => void | Promise<void>;
20
20
  stdin?: boolean;
21
- /** Default 60 000, as in E2B. 0 is no limit (Runtime's longest, 24 hours). */
21
+ /** Stream connection deadline, 60 000 by default. Zero waits without a deadline. */
22
22
  timeoutMs?: number;
23
23
  }
24
24
  export type CommandConnectOpts = Pick<CommandStartOpts, "onStderr" | "onStdout" | "timeoutMs"> & CommandRequestOpts;
@@ -32,11 +32,11 @@ export interface ProcessInfo {
32
32
  envs: Record<string, string>;
33
33
  cwd?: string;
34
34
  }
35
- /** The shared state a sandbox's modules need: the Runtime sandbox, the
36
- * environment given at create, and the one-time /home/user link. */
35
+ /** The shared state a sandbox's modules need: the Runtime sandbox and the
36
+ * one-time /home/user link. The environment given at create is Runtime's to
37
+ * keep: it is the sandbox's own `env`, added to every command there. */
37
38
  export interface SandboxContext {
38
39
  readonly runtime: RuntimeSandbox;
39
- readonly envs: Record<string, string>;
40
40
  /** Makes /home/user (E2B's home) lead to /workspace (Runtime's), once, when
41
41
  * something names it. */
42
42
  ensureHome(text: string | undefined): Promise<void>;
@@ -80,6 +80,7 @@ export declare class CommandHandle {
80
80
  stdin: boolean;
81
81
  timeoutMs: number;
82
82
  cursor?: number;
83
+ signal?: AbortSignal;
83
84
  onStdout?: (data: string) => void | Promise<void>;
84
85
  onStderr?: (data: string) => void | Promise<void>;
85
86
  });
@@ -1,7 +1,5 @@
1
- import { request } from "./client.js";
2
1
  import { CommandExitError, guard, InvalidArgumentError, NotSupportedError, SandboxError, TimeoutError, translate, } from "./errors.js";
3
2
  const DEFAULT_TIMEOUT_MS = 60_000;
4
- const LONGEST_MS = 86_400_000;
5
3
  /** E2B's pids are numbers; Runtime's process ids are strings. The number is a
6
4
  * 31-bit FNV-1a hash of the id, so every client derives the same one. */
7
5
  export function pidOf(processId) {
@@ -19,9 +17,9 @@ function refuseUser(user) {
19
17
  function timeoutFor(timeoutMs) {
20
18
  if (timeoutMs === undefined)
21
19
  return DEFAULT_TIMEOUT_MS;
22
- if (timeoutMs === 0)
23
- return LONGEST_MS;
24
- return Math.min(timeoutMs, LONGEST_MS);
20
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0)
21
+ throw new InvalidArgumentError("timeoutMs must be a nonnegative whole number.");
22
+ return timeoutMs;
25
23
  }
26
24
  function timedOut(timeoutMs) {
27
25
  return new TimeoutError(`Command timed out after ${timeoutMs} ms: this error is likely due to exceeding 'timeoutMs'. ` +
@@ -37,7 +35,7 @@ function toResult(result) {
37
35
  stdout: result.stdout,
38
36
  stderr: result.stderr,
39
37
  ...(exitCode === 0
40
- ? {}
38
+ ? { error: "" }
41
39
  : {
42
40
  error: result.exitCode === null ? "terminated by a signal" : `exit status ${exitCode}`,
43
41
  }),
@@ -61,42 +59,32 @@ export class Commands {
61
59
  this.#ctx = ctx;
62
60
  }
63
61
  #env(envs) {
64
- const merged = { ...this.#ctx.envs, ...envs };
65
- return Object.keys(merged).length ? { env: merged } : {};
62
+ return envs && Object.keys(envs).length ? { env: envs } : {};
66
63
  }
67
64
  async run(cmd, opts = {}) {
65
+ opts.signal?.throwIfAborted();
68
66
  refuseUser(opts.user);
69
67
  await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`);
70
68
  const timeoutMs = timeoutFor(opts.timeoutMs);
71
- if (opts.background || opts.stdin) {
72
- const handle = await this.#start(cmd, opts, timeoutMs);
73
- return opts.background ? handle : handle.wait();
74
- }
75
- /* Always streamed, so the result holds the whole output as E2B's does: an
76
- exec with no output callback returns at most 64 KiB of each stream. */
77
- const onStdout = opts.onStdout;
78
- const onStderr = opts.onStderr;
79
- const result = await guard("sandbox", () => this.#ctx.runtime.exec(cmd, {
80
- ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
81
- ...this.#env(opts.envs),
82
- timeoutMs,
83
- onStdout: (text) => void onStdout?.(text),
84
- ...(onStderr ? { onStderr: (text) => void onStderr(text) } : {}),
85
- ...(opts.signal ? { signal: opts.signal } : {}),
86
- }));
87
- return settle({ ...result, truncated: result.stdoutTruncated === true || result.stderrTruncated === true }, timeoutMs);
69
+ const handle = await this.#start(cmd, opts, timeoutMs);
70
+ return opts.background ? handle : handle.wait();
88
71
  }
89
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);
90
78
  const process = await guard("sandbox", () => this.#ctx.runtime.spawn(cmd, {
91
79
  ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
92
80
  ...this.#env(opts.envs),
93
- timeoutMs,
94
81
  ...(opts.stdin ? { stdin: "pipe" } : {}),
95
- ...request(opts),
82
+ ...(signals.length ? { signal: AbortSignal.any(signals) } : {}),
96
83
  }));
97
84
  return new CommandHandle(process, {
98
85
  stdin: opts.stdin === true,
99
86
  timeoutMs,
87
+ signal: opts.signal,
100
88
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
101
89
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
102
90
  });
@@ -142,6 +130,7 @@ export class Commands {
142
130
  return new CommandHandle(process, {
143
131
  stdin: process.info.stdinOpen,
144
132
  timeoutMs: timeoutFor(opts.timeoutMs),
133
+ signal: opts.signal,
145
134
  cursor: process.info.outputBytes,
146
135
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
147
136
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
@@ -173,19 +162,34 @@ export class CommandHandle {
173
162
  #truncated = false;
174
163
  #failure;
175
164
  #disconnected = false;
165
+ #deadline;
176
166
  constructor(process, options) {
177
167
  this.#process = process;
178
168
  this.pid = pidOf(process.id);
179
169
  this.#stdin = options.stdin;
180
170
  this.#timeoutMs = options.timeoutMs;
171
+ // E2B's timeout limits the subscription. It never asks us to kill the
172
+ // process: the sandbox lease still bounds the workload after disconnect.
173
+ if (options.timeoutMs > 0) {
174
+ this.#deadline = setTimeout(() => {
175
+ this.#failure = timedOut(options.timeoutMs);
176
+ this.#abort.abort();
177
+ }, options.timeoutMs);
178
+ this.#deadline.unref?.();
179
+ }
181
180
  this.#done = this.#follow(options);
182
181
  }
183
182
  async #follow(options) {
183
+ const signal = options.signal
184
+ ? AbortSignal.any([options.signal, this.#abort.signal])
185
+ : this.#abort.signal;
184
186
  try {
185
187
  for await (const event of this.#process.output({
186
188
  ...(options.cursor ? { cursor: options.cursor } : {}),
187
- signal: this.#abort.signal,
189
+ signal,
188
190
  })) {
191
+ if (this.#disconnected || signal.aborted)
192
+ break;
189
193
  if (event.type === "stdout") {
190
194
  this.#stdout += event.data;
191
195
  await options.onStdout?.(event.data);
@@ -203,12 +207,16 @@ export class CommandHandle {
203
207
  }
204
208
  }
205
209
  catch (error) {
206
- if (!this.#disconnected) {
210
+ if (!this.#disconnected && !this.#failure) {
207
211
  const translated = translate(error, "sandbox");
208
212
  this.#failure =
209
213
  translated instanceof Error ? translated : new SandboxError(String(translated));
210
214
  }
211
215
  }
216
+ finally {
217
+ if (this.#deadline !== undefined)
218
+ clearTimeout(this.#deadline);
219
+ }
212
220
  }
213
221
  /** The exit code, or undefined while the command runs. */
214
222
  get exitCode() {
@@ -77,7 +77,7 @@ export class NotSupportedError extends SandboxError {
77
77
  export class CommandExitError extends SandboxError {
78
78
  #result;
79
79
  constructor(result) {
80
- super(`Command exited with code ${result.exitCode} and error:\n${result.stderr}`);
80
+ super(result.error);
81
81
  this.#result = result;
82
82
  }
83
83
  get exitCode() {
@@ -14,9 +14,9 @@ export interface EntryInfo extends WriteInfo {
14
14
  size: number;
15
15
  mode: number;
16
16
  permissions: string;
17
- /** Runtime's listing does not say who owns a file: always "". */
17
+ /** File owner when returned by the native guest. */
18
18
  owner: string;
19
- /** Runtime's listing does not say a file's group: always "". */
19
+ /** File group when returned by the native guest. */
20
20
  group: string;
21
21
  modifiedTime?: Date;
22
22
  symlinkTarget?: string;
@@ -32,11 +32,14 @@ export interface FilesystemEvent {
32
32
  /** Relative to the watched directory. */
33
33
  name: string;
34
34
  type: FilesystemEventType;
35
+ entry?: EntryInfo;
35
36
  }
36
37
  export interface WatchOpts extends FilesystemRequestOpts {
37
- /** How long the watch runs; 0 is Runtime's maximum, 24 hours. Default 60000. */
38
+ /** How long the subscription runs; 0 has no subscription deadline. Default 60000. */
38
39
  timeoutMs?: number;
39
40
  recursive?: boolean;
41
+ includeEntry?: boolean;
42
+ allowNetworkMounts?: boolean;
40
43
  }
41
44
  export interface E2BWatchHandle {
42
45
  stop(): Promise<void>;
@@ -98,9 +101,8 @@ export declare class Filesystem {
98
101
  remove(path: string, opts?: FilesystemRequestOpts): Promise<void>;
99
102
  exists(path: string, opts?: FilesystemRequestOpts): Promise<boolean>;
100
103
  getInfo(path: string, opts?: FilesystemRequestOpts): Promise<EntryInfo>;
101
- /** E2B's watchDir on Runtime's file watch: the same events (CREATE, WRITE,
102
- * REMOVE, RENAME, CHMOD) with names relative to the directory, stopped
103
- * with `handle.stop()`. `timeoutMs` 0 is Runtime's 24-hour maximum. */
104
+ /** E2B's watchDir on Runtime's file watch. A local deadline preserves
105
+ * subsecond subscriptions; 0 runs until stopped or the sandbox ends. */
104
106
  watchDir(path: string, onEvent: (event: FilesystemEvent) => void | Promise<void>, opts?: WatchOpts & {
105
107
  onExit?: (err?: Error) => void | Promise<void>;
106
108
  }): Promise<E2BWatchHandle>;
@@ -1,5 +1,5 @@
1
1
  import { request } from "./client.js";
2
- import { FileNotFoundError, guard, NotSupportedError } from "./errors.js";
2
+ import { FileNotFoundError, guard, InvalidArgumentError, NotSupportedError, TimeoutError, } from "./errors.js";
3
3
  export var FileType;
4
4
  (function (FileType) {
5
5
  FileType["FILE"] = "file";
@@ -42,8 +42,9 @@ function entryInfo(entry) {
42
42
  size: entry.size,
43
43
  mode,
44
44
  permissions: PERMISSIONS[(bits >> 6) & 7] + PERMISSIONS[(bits >> 3) & 7] + PERMISSIONS[bits & 7],
45
- owner: "",
46
- group: "",
45
+ owner: entry.owner ?? "",
46
+ group: entry.group ?? "",
47
+ ...(entry.symlinkTarget ? { symlinkTarget: entry.symlinkTarget } : {}),
47
48
  modifiedTime: new Date(entry.modifiedAt),
48
49
  };
49
50
  }
@@ -73,8 +74,10 @@ export class Filesystem {
73
74
  return this.#ctx.runtime.files;
74
75
  }
75
76
  async #path(path, opts) {
77
+ opts.signal?.throwIfAborted();
76
78
  refuse(opts);
77
79
  await this.#ctx.ensureHome(path);
80
+ opts.signal?.throwIfAborted();
78
81
  return absolute(path);
79
82
  }
80
83
  async read(path, opts = {}) {
@@ -98,6 +101,7 @@ export class Filesystem {
98
101
  return this.#writeOne(pathOrFiles, dataOrOpts, maybeOpts ?? {});
99
102
  }
100
103
  async writeFiles(files, opts = {}) {
104
+ opts.signal?.throwIfAborted();
101
105
  const written = [];
102
106
  for (const file of files)
103
107
  written.push(await this.#writeOne(file.path, file.data, opts));
@@ -112,61 +116,156 @@ export class Filesystem {
112
116
  /** A directory's entries, hidden ones included; `depth` goes deeper. */
113
117
  async list(path, opts = {}) {
114
118
  const dir = await this.#path(path, opts);
115
- const entries = await guard("file", () => this.#files.list(dir, { depth: opts.depth ?? 1, hidden: true }));
119
+ const entries = await guard("file", () => this.#files.list(dir, { depth: opts.depth ?? 1, hidden: true, ...request(opts) }));
116
120
  return entries.map(entryInfo);
117
121
  }
118
122
  /** Makes a directory and its parents. False when it already existed. */
119
123
  async makeDir(path, opts = {}) {
120
124
  const dir = await this.#path(path, opts);
121
- if (await guard("file", () => this.#files.exists(dir)))
125
+ if (await guard("file", () => this.#files.exists(dir, request(opts))))
122
126
  return false;
123
- await guard("file", () => this.#files.mkdir(dir, { parents: true }));
127
+ await guard("file", () => this.#files.mkdir(dir, { parents: true, ...request(opts) }));
124
128
  return true;
125
129
  }
126
130
  /** Moves a file or directory, replacing a file at the new path, as E2B does. */
127
131
  async rename(oldPath, newPath, opts = {}) {
128
132
  const from = await this.#path(oldPath, opts);
129
133
  const to = await this.#path(newPath, opts);
130
- await guard("file", () => this.#files.rename(from, to, { overwrite: true }));
134
+ await guard("file", () => this.#files.rename(from, to, { overwrite: true, ...request(opts) }));
131
135
  return this.getInfo(to, opts);
132
136
  }
133
137
  /** Removes a file, or a directory with everything in it. */
134
138
  async remove(path, opts = {}) {
135
139
  const target = await this.#path(path, opts);
136
- await guard("file", () => this.#files.remove(target, { recursive: true }));
140
+ await guard("file", () => this.#files.remove(target, { recursive: true, ...request(opts) }));
137
141
  }
138
142
  async exists(path, opts = {}) {
139
143
  const target = await this.#path(path, opts);
140
- return guard("file", () => this.#files.exists(target));
144
+ return guard("file", () => this.#files.exists(target, request(opts)));
141
145
  }
142
146
  async getInfo(path, opts = {}) {
143
147
  const target = await this.#path(path, opts);
144
- const stat = await guard("file", () => this.#files.stat(target));
148
+ const stat = await guard("file", () => this.#files.stat(target, request(opts)));
145
149
  if (!stat.exists)
146
150
  throw new FileNotFoundError(`${target} does not exist.`);
147
151
  return entryInfo(stat);
148
152
  }
149
- /** E2B's watchDir on Runtime's file watch: the same events (CREATE, WRITE,
150
- * REMOVE, RENAME, CHMOD) with names relative to the directory, stopped
151
- * with `handle.stop()`. `timeoutMs` 0 is Runtime's 24-hour maximum. */
153
+ /** E2B's watchDir on Runtime's file watch. A local deadline preserves
154
+ * subsecond subscriptions; 0 runs until stopped or the sandbox ends. */
152
155
  async watchDir(path, onEvent, opts = {}) {
156
+ opts.signal?.throwIfAborted();
157
+ if (opts.allowNetworkMounts)
158
+ throw new NotSupportedError("Watching network mounts", "Watch a local sandbox directory instead.");
159
+ const lifetime = opts.timeoutMs ?? 60000;
160
+ if (!Number.isFinite(lifetime) || lifetime < 0)
161
+ throw new InvalidArgumentError("timeoutMs must be a nonnegative finite number.");
153
162
  const target = await this.#path(path, opts);
154
163
  const prefix = target.endsWith("/") ? target : `${target}/`;
155
- const lifetime = opts.timeoutMs === 0 ? 86_400_000 : (opts.timeoutMs ?? 60_000);
156
- const watch = await guard("file", () => this.#files.watch(target, (event) => onEvent({
157
- name: event.path.startsWith(prefix) ? event.path.slice(prefix.length) : event.path,
158
- type: event.type,
159
- }), {
160
- recursive: opts.recursive === true,
161
- timeoutMs: Math.max(1_000, Math.min(86_400_000, lifetime)),
162
- onExit: (reason) => {
163
- if (reason === "stopped")
164
+ let exited = false;
165
+ let native;
166
+ let stopping;
167
+ let timer;
168
+ const lifetimeAbort = new AbortController();
169
+ const signal = opts.signal
170
+ ? AbortSignal.any([opts.signal, lifetimeAbort.signal])
171
+ : lifetimeAbort.signal;
172
+ const exit = async (error) => {
173
+ if (exited)
174
+ return;
175
+ exited = true;
176
+ if (timer !== undefined)
177
+ clearTimeout(timer);
178
+ opts.signal?.removeEventListener("abort", onAbort);
179
+ try {
180
+ await opts.onExit?.(error);
181
+ }
182
+ catch {
183
+ /* Upstream treats a throwing terminal callback as handled. */
184
+ }
185
+ };
186
+ const stopRemote = () => {
187
+ if (!native)
188
+ return Promise.resolve();
189
+ return (stopping ??= native.stop());
190
+ };
191
+ const close = async (error) => {
192
+ const callback = exit(error);
193
+ try {
194
+ await stopRemote();
195
+ }
196
+ finally {
197
+ await callback;
198
+ }
199
+ };
200
+ const onAbort = () => {
201
+ const error = opts.signal?.reason instanceof Error
202
+ ? opts.signal.reason
203
+ : new Error("The watch was aborted.");
204
+ void close(error).catch(() => undefined);
205
+ };
206
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
207
+ if (opts.signal?.aborted)
208
+ onAbort();
209
+ if (lifetime > 0) {
210
+ const deadline = Date.now() + lifetime;
211
+ const schedule = () => {
212
+ timer = setTimeout(() => {
213
+ if (exited)
214
+ return;
215
+ if (Date.now() < deadline) {
216
+ schedule();
217
+ return;
218
+ }
219
+ const error = new TimeoutError("Filesystem watch timed out.");
220
+ // Mark the terminal result before native cancellation can report a
221
+ // clean end. The same signal also cancels creation still in flight.
222
+ void close(error).catch(() => undefined);
223
+ lifetimeAbort.abort(error);
224
+ }, Math.min(2147483647, Math.max(0, deadline - Date.now())));
225
+ timer.unref?.();
226
+ };
227
+ schedule();
228
+ }
229
+ try {
230
+ const watch = await guard("file", () => this.#files.watch(target, async (event) => {
231
+ if (exited)
164
232
  return;
165
- void opts.onExit?.(reason === "timeout" || reason === "paused"
166
- ? undefined
167
- : new Error(`The watch ended: ${reason}`));
168
- },
169
- }));
170
- return { stop: () => watch.stop() };
233
+ let entry;
234
+ if (opts.includeEntry) {
235
+ try {
236
+ entry = await this.getInfo(event.path, { ...opts, signal });
237
+ }
238
+ catch (error) {
239
+ if (!(error instanceof FileNotFoundError))
240
+ throw error;
241
+ }
242
+ }
243
+ if (!exited)
244
+ await onEvent({
245
+ name: event.path.startsWith(prefix) ? event.path.slice(prefix.length) : event.path,
246
+ type: event.type,
247
+ ...(entry ? { entry } : {}),
248
+ });
249
+ }, {
250
+ recursive: opts.recursive === true,
251
+ timeoutMs: 0,
252
+ signal,
253
+ onExit: (reason) => {
254
+ void exit(reason === "timeout" ? new TimeoutError("Filesystem watch timed out.") : undefined);
255
+ },
256
+ onNotice: (notice) => {
257
+ void close(new Error(`Filesystem events were lost (${notice.k}); rescan the directory and start a new watch.`)).catch(() => undefined);
258
+ },
259
+ }));
260
+ native = watch;
261
+ if (exited)
262
+ await stopRemote();
263
+ void watch.done?.then(() => exit(), (error) => close(error instanceof Error ? error : new Error(String(error))).catch(() => undefined));
264
+ return { stop: () => close() };
265
+ }
266
+ catch (error) {
267
+ await close(error instanceof Error ? error : new Error(String(error))).catch(() => undefined);
268
+ throw error;
269
+ }
171
270
  }
172
271
  }
@@ -26,7 +26,8 @@ export interface SandboxOpts extends ConnectionOpts {
26
26
  template?: string;
27
27
  /** Stored as the sandbox's labels (at most 32, values up to 256 characters). */
28
28
  metadata?: Record<string, string>;
29
- /** Given to every command and code run through this object. */
29
+ /** The sandbox's own environment: every command, terminal and code run in it
30
+ * gets these, whoever connects. Values are never shown again. */
30
31
  envs?: Record<string, string>;
31
32
  /** Default 300 000. Runtime's leases run 60 s to 1 hour; shorter rounds up
32
33
  * to 60 s (with a warning), longer is refused. */
@@ -88,17 +89,17 @@ export type SandboxListOpts = SandboxApiOpts & {
88
89
  };
89
90
  export declare class Sandbox {
90
91
  #private;
92
+ protected readonly requestTimeoutMs: number;
91
93
  protected static readonly defaultTemplate: string;
92
94
  readonly files: Filesystem;
93
95
  readonly commands: Commands;
94
96
  /** Use Sandbox.create or Sandbox.connect. */
95
- constructor(runtime: RuntimeSandbox, client: Runtime, envs?: Record<string, string>);
97
+ constructor(runtime: RuntimeSandbox, client: Runtime, requestTimeoutMs?: number);
96
98
  get sandboxId(): string;
97
99
  /** The Runtime sandbox underneath: previews, network, desktop, interpreter,
98
100
  * processes, terminal and the rest of Runtime's SDK. */
99
101
  get runtime(): RuntimeSandbox;
100
102
  protected get client(): Runtime;
101
- protected get envs(): Record<string, string>;
102
103
  /** Creates a sandbox from `template` (default "base", Runtime's stock image)
103
104
  * with E2B's default machine, 2 vCPU and 512 MiB, and waits until it runs.
104
105
  * Funding is left to Runtime: the free trial while the account has trial