withruntime 0.8.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/BLAXEL.md +4 -4
  2. package/COMPATIBILITY.md +145 -0
  3. package/DAYTONA.md +13 -7
  4. package/E2B.md +34 -20
  5. package/README.md +22 -11
  6. package/README.src.md +22 -11
  7. package/VERCEL.md +13 -3
  8. package/compatibility-lock.json +357 -0
  9. package/dist/ai-harness/index.js +2 -2
  10. package/dist/api-defaults.d.ts +17 -0
  11. package/dist/api-defaults.js +22 -0
  12. package/dist/blaxel/errors.js +1 -1
  13. package/dist/blaxel/lookup.d.ts +3 -2
  14. package/dist/blaxel/lookup.js +3 -2
  15. package/dist/blaxel/pagination.d.ts +2 -2
  16. package/dist/blaxel/pagination.js +45 -17
  17. package/dist/blaxel/preview.d.ts +3 -2
  18. package/dist/blaxel/preview.js +16 -14
  19. package/dist/blaxel/process.d.ts +1 -1
  20. package/dist/blaxel/process.js +66 -11
  21. package/dist/blaxel/sandbox.d.ts +2 -0
  22. package/dist/blaxel/sandbox.js +2 -0
  23. package/dist/blaxel/snapshot.js +3 -5
  24. package/dist/cli-words.d.ts +27 -0
  25. package/dist/cli-words.js +135 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +351 -85
  28. package/dist/client.d.ts +33 -4
  29. package/dist/client.js +47 -12
  30. package/dist/cloudflare/identity.d.ts +7 -0
  31. package/dist/cloudflare/identity.js +13 -0
  32. package/dist/cloudflare/index.d.ts +303 -0
  33. package/dist/cloudflare/index.js +592 -0
  34. package/dist/cloudflare/worker.d.ts +419 -0
  35. package/dist/cloudflare/worker.js +447 -0
  36. package/dist/codesandbox/guest-service.d.ts +31 -0
  37. package/dist/codesandbox/guest-service.js +490 -0
  38. package/dist/codesandbox/index.d.ts +386 -0
  39. package/dist/codesandbox/index.js +1347 -0
  40. package/dist/compat/core.d.ts +40 -0
  41. package/dist/compat/core.js +238 -0
  42. package/dist/compat/files.d.ts +26 -0
  43. package/dist/compat/files.js +133 -0
  44. package/dist/compat/shell.d.ts +4 -0
  45. package/dist/compat/shell.js +165 -0
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +107 -38
  48. package/dist/daytona/context.js +2 -1
  49. package/dist/daytona/daytona.js +11 -4
  50. package/dist/daytona/filesystem.d.ts +17 -4
  51. package/dist/daytona/filesystem.js +121 -38
  52. package/dist/daytona/index.d.ts +1 -1
  53. package/dist/daytona/process.d.ts +1 -1
  54. package/dist/daytona/process.js +27 -4
  55. package/dist/daytona/sandbox.d.ts +1 -1
  56. package/dist/daytona/sandbox.js +16 -7
  57. package/dist/e2b/client.js +1 -1
  58. package/dist/e2b/code-interpreter.d.ts +4 -5
  59. package/dist/e2b/code-interpreter.js +54 -56
  60. package/dist/e2b/commands.d.ts +35 -12
  61. package/dist/e2b/commands.js +169 -73
  62. package/dist/e2b/errors.js +1 -1
  63. package/dist/e2b/filesystem.d.ts +8 -6
  64. package/dist/e2b/filesystem.js +127 -28
  65. package/dist/e2b/index.d.ts +1 -0
  66. package/dist/e2b/index.js +1 -0
  67. package/dist/e2b/pty.d.ts +24 -0
  68. package/dist/e2b/pty.js +80 -0
  69. package/dist/e2b/sandbox.d.ts +6 -4
  70. package/dist/e2b/sandbox.js +59 -25
  71. package/dist/errors.d.ts +8 -0
  72. package/dist/errors.js +15 -2
  73. package/dist/file-source.d.ts +16 -0
  74. package/dist/file-source.js +162 -0
  75. package/dist/freestyle/index.d.ts +309 -0
  76. package/dist/freestyle/index.js +343 -0
  77. package/dist/index.d.ts +3 -1
  78. package/dist/login.js +16 -10
  79. package/dist/mcp.js +2 -2
  80. package/dist/modal/index.d.ts +205 -0
  81. package/dist/modal/index.js +486 -0
  82. package/dist/network-cli.js +112 -44
  83. package/dist/openai-agents/index.js +18 -71
  84. package/dist/page.d.ts +1 -1
  85. package/dist/page.js +2 -2
  86. package/dist/products/audit.d.ts +3 -0
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/interpreter.d.ts +11 -4
  90. package/dist/products/interpreter.js +33 -9
  91. package/dist/products/jobs.js +3 -2
  92. package/dist/products/limits.d.ts +10 -0
  93. package/dist/products/observability.d.ts +44 -0
  94. package/dist/products/observability.js +6 -0
  95. package/dist/products/previews.d.ts +18 -4
  96. package/dist/products/previews.js +14 -5
  97. package/dist/products/tailscale.d.ts +57 -0
  98. package/dist/products/tailscale.js +17 -0
  99. package/dist/products/volumes.d.ts +42 -0
  100. package/dist/products/volumes.js +36 -0
  101. package/dist/products/watch.d.ts +25 -4
  102. package/dist/products/watch.js +132 -18
  103. package/dist/proxy.js +3 -1
  104. package/dist/runloop/index.d.ts +326 -0
  105. package/dist/runloop/index.js +535 -0
  106. package/dist/runloop/pagination.d.ts +46 -0
  107. package/dist/runloop/pagination.js +82 -0
  108. package/dist/runloop/snapshots.d.ts +52 -0
  109. package/dist/runloop/snapshots.js +244 -0
  110. package/dist/sandbox.d.ts +76 -16
  111. package/dist/sandbox.js +696 -135
  112. package/dist/snapshots.d.ts +20 -5
  113. package/dist/snapshots.js +11 -1
  114. package/dist/sprites/index.d.ts +251 -0
  115. package/dist/sprites/index.js +588 -0
  116. package/dist/tar.d.ts +10 -1
  117. package/dist/tar.js +311 -35
  118. package/dist/tools/index.js +2 -1
  119. package/dist/transport.d.ts +23 -8
  120. package/dist/transport.js +247 -127
  121. package/dist/types.d.ts +52 -3
  122. package/dist/vercel/command.d.ts +1 -1
  123. package/dist/vercel/command.js +15 -3
  124. package/dist/vercel/errors.d.ts +2 -2
  125. package/dist/vercel/errors.js +19 -2
  126. package/dist/vercel/filesystem.d.ts +21 -18
  127. package/dist/vercel/filesystem.js +95 -48
  128. package/dist/vercel/sandbox.d.ts +8 -8
  129. package/dist/vercel/sandbox.js +159 -64
  130. package/dist/vercel/snapshot.d.ts +3 -2
  131. package/dist/vercel/snapshot.js +9 -5
  132. package/package.json +46 -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
  }
@@ -1,6 +1,8 @@
1
1
  import type { Process } from "../sandbox.js";
2
+ import type { ProcessInfo as RuntimeProcessInfo } from "../types.js";
3
+ import type { RequestOptions } from "../transport.js";
2
4
  import type { RuntimeSandbox } from "./client.js";
3
- import { type CommandResult } from "./errors.js";
5
+ import { TimeoutError, type CommandResult } from "./errors.js";
4
6
  export type { CommandResult };
5
7
  export type Username = "user" | "root" | (string & {});
6
8
  export interface CommandRequestOpts {
@@ -18,7 +20,7 @@ export interface CommandStartOpts extends CommandRequestOpts {
18
20
  onStdout?: (data: string) => void | Promise<void>;
19
21
  onStderr?: (data: string) => void | Promise<void>;
20
22
  stdin?: boolean;
21
- /** Default 60 000, as in E2B. 0 is no limit (Runtime's longest, 24 hours). */
23
+ /** Stream connection deadline, 60 000 by default. Zero waits without a deadline. */
22
24
  timeoutMs?: number;
23
25
  }
24
26
  export type CommandConnectOpts = Pick<CommandStartOpts, "onStderr" | "onStdout" | "timeoutMs"> & CommandRequestOpts;
@@ -32,18 +34,34 @@ export interface ProcessInfo {
32
34
  envs: Record<string, string>;
33
35
  cwd?: string;
34
36
  }
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. */
37
+ /** The shared state a sandbox's modules need: the Runtime sandbox and the
38
+ * one-time /home/user link. The environment given at create is Runtime's to
39
+ * keep: it is the sandbox's own `env`, added to every command there. */
37
40
  export interface SandboxContext {
38
41
  readonly runtime: RuntimeSandbox;
39
- readonly envs: Record<string, string>;
42
+ readonly requestTimeoutMs?: number;
40
43
  /** Makes /home/user (E2B's home) lead to /workspace (Runtime's), once, when
41
44
  * something names it. */
42
- ensureHome(text: string | undefined): Promise<void>;
45
+ ensureHome(text: string | undefined, options?: RequestOptions): Promise<void>;
43
46
  }
44
47
  /** E2B's pids are numbers; Runtime's process ids are strings. The number is a
45
48
  * 31-bit FNV-1a hash of the id, so every client derives the same one. */
46
49
  export declare function pidOf(processId: string): number;
50
+ export declare function refuseUser(user: Username | undefined): void;
51
+ export declare function timeoutFor(timeoutMs: number | undefined): number;
52
+ /** One budget for lookup and mutation, including retries and queued input. */
53
+ export declare function commandRequest(opts: CommandRequestOpts, defaultTimeoutMs?: number): RequestOptions;
54
+ export declare function findProcess(ctx: SandboxContext, pid: number, options: RequestOptions): Promise<RuntimeProcessInfo | undefined>;
55
+ export declare function resolveProcess(ctx: SandboxContext, pid: number, options: RequestOptions): Promise<Process>;
56
+ export declare function killProcess(ctx: SandboxContext, pid: number, options: RequestOptions): Promise<boolean>;
57
+ export declare function timedOut(timeoutMs: number): TimeoutError;
58
+ /** The subscription deadline includes the opening handshake. */
59
+ export declare function connectionRequest(opts: CommandConnectOpts, defaultTimeoutMs?: number): {
60
+ timeoutMs: number;
61
+ deadline: number | undefined;
62
+ request: RequestOptions;
63
+ failure: (error: unknown) => never;
64
+ };
47
65
  /** `sandbox.commands`: E2B's command module over Runtime's exec and processes. */
48
66
  export declare class Commands {
49
67
  #private;
@@ -61,12 +79,12 @@ export declare class Commands {
61
79
  background?: boolean;
62
80
  }): Promise<CommandHandle | CommandResult>;
63
81
  /** Running commands. */
64
- list(_opts?: CommandRequestOpts): Promise<ProcessInfo[]>;
82
+ list(opts?: CommandRequestOpts): Promise<ProcessInfo[]>;
65
83
  /** Kills a command with SIGKILL, as E2B does. False when there is none. */
66
- kill(pid: number, _opts?: CommandRequestOpts): Promise<boolean>;
84
+ kill(pid: number, opts?: CommandRequestOpts): Promise<boolean>;
67
85
  /** Sends input to a command started with `stdin: true`. */
68
- sendStdin(pid: number, data: string | Uint8Array, _opts?: CommandRequestOpts): Promise<void>;
69
- closeStdin(pid: number, _opts?: CommandRequestOpts): Promise<void>;
86
+ sendStdin(pid: number, data: string | Uint8Array, opts?: CommandRequestOpts): Promise<void>;
87
+ closeStdin(pid: number, opts?: CommandRequestOpts): Promise<void>;
70
88
  /** Attaches to a running command; output from now on reaches the handle. */
71
89
  connect(pid: number, opts?: CommandConnectOpts): Promise<CommandHandle>;
72
90
  }
@@ -80,8 +98,13 @@ export declare class CommandHandle {
80
98
  stdin: boolean;
81
99
  timeoutMs: number;
82
100
  cursor?: number;
101
+ signal?: AbortSignal;
83
102
  onStdout?: (data: string) => void | Promise<void>;
84
103
  onStderr?: (data: string) => void | Promise<void>;
104
+ onData?: (data: Uint8Array) => void | Promise<void>;
105
+ pty?: boolean;
106
+ requestTimeoutMs?: number;
107
+ deadline?: number;
85
108
  });
86
109
  /** The exit code, or undefined while the command runs. */
87
110
  get exitCode(): number | undefined;
@@ -95,6 +118,6 @@ export declare class CommandHandle {
95
118
  disconnect(): Promise<void>;
96
119
  /** Kills the command with SIGKILL. False when it had already ended. */
97
120
  kill(): Promise<boolean>;
98
- sendStdin(data: string | Uint8Array, _opts?: CommandRequestOpts): Promise<void>;
99
- closeStdin(_opts?: CommandRequestOpts): Promise<void>;
121
+ sendStdin(data: string | Uint8Array, opts?: CommandRequestOpts): Promise<void>;
122
+ closeStdin(opts?: CommandRequestOpts): Promise<void>;
100
123
  }
@@ -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) {
@@ -12,18 +10,57 @@ export function pidOf(processId) {
12
10
  }
13
11
  return hash >>> 1 || 1;
14
12
  }
15
- function refuseUser(user) {
13
+ export function refuseUser(user) {
16
14
  if (user !== undefined && user !== "user")
17
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.');
18
16
  }
19
- function timeoutFor(timeoutMs) {
17
+ export 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
- 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) {
27
64
  return new TimeoutError(`Command timed out after ${timeoutMs} ms: this error is likely due to exceeding 'timeoutMs'. ` +
28
65
  "Pass a larger 'timeoutMs', or 0 for no limit.");
29
66
  }
@@ -37,13 +74,32 @@ function toResult(result) {
37
74
  stdout: result.stdout,
38
75
  stderr: result.stderr,
39
76
  ...(exitCode === 0
40
- ? {}
77
+ ? { error: "" }
41
78
  : {
42
79
  error: result.exitCode === null ? "terminated by a signal" : `exit status ${exitCode}`,
43
80
  }),
44
81
  ...(result.truncated ? { truncated: true } : {}),
45
82
  };
46
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
+ }
47
103
  function settle(result, timeoutMs) {
48
104
  if (result.timedOut)
49
105
  throw timedOut(timeoutMs);
@@ -61,88 +117,72 @@ export class Commands {
61
117
  this.#ctx = ctx;
62
118
  }
63
119
  #env(envs) {
64
- const merged = { ...this.#ctx.envs, ...envs };
65
- return Object.keys(merged).length ? { env: merged } : {};
120
+ return envs && Object.keys(envs).length ? { env: envs } : {};
66
121
  }
67
122
  async run(cmd, opts = {}) {
123
+ const opening = connectionRequest(opts, this.#ctx.requestTimeoutMs);
68
124
  refuseUser(opts.user);
69
- await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`);
70
- 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);
125
+ const handle = await (async () => {
126
+ await this.#ctx.ensureHome(`${cmd}\n${opts.cwd ?? ""}`, opening.request);
127
+ opening.request.signal?.throwIfAborted();
128
+ return this.#start(cmd, opts, opening.timeoutMs, opening.request, opening.deadline);
129
+ })().catch(opening.failure);
130
+ return opts.background ? handle : handle.wait();
88
131
  }
89
- async #start(cmd, opts, timeoutMs) {
132
+ async #start(cmd, opts, timeoutMs, request, deadline) {
133
+ request.signal?.throwIfAborted();
90
134
  const process = await guard("sandbox", () => this.#ctx.runtime.spawn(cmd, {
91
135
  ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
92
136
  ...this.#env(opts.envs),
93
- timeoutMs,
94
137
  ...(opts.stdin ? { stdin: "pipe" } : {}),
95
- ...request(opts),
138
+ request,
96
139
  }));
97
140
  return new CommandHandle(process, {
98
141
  stdin: opts.stdin === true,
99
142
  timeoutMs,
143
+ deadline,
144
+ requestTimeoutMs: this.#ctx.requestTimeoutMs,
145
+ signal: opts.signal,
100
146
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
101
147
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
102
148
  });
103
149
  }
104
150
  /** Running commands. */
105
- async list(_opts = {}) {
106
- const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list());
151
+ async list(opts = {}) {
152
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
153
+ const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list(request));
107
154
  return processes.filter((info) => info.state === "running").map(describe);
108
155
  }
109
- async #find(pid) {
110
- const processes = await guard("sandbox", () => this.#ctx.runtime.processes.list());
111
- return processes.find((info) => pidOf(info.id) === pid && info.state === "running");
112
- }
113
- async #process(pid) {
114
- const info = await this.#find(pid);
115
- if (!info)
116
- throw new SandboxError(`No running command with pid ${pid}.`);
117
- return guard("sandbox", () => this.#ctx.runtime.processes.get(info.id));
118
- }
119
156
  /** Kills a command with SIGKILL, as E2B does. False when there is none. */
120
- async kill(pid, _opts = {}) {
121
- const info = await this.#find(pid);
122
- if (!info)
123
- return false;
124
- const process = await guard("sandbox", () => this.#ctx.runtime.processes.get(info.id));
125
- await guard("sandbox", () => process.kill("SIGKILL"));
126
- return true;
157
+ async kill(pid, opts = {}) {
158
+ return killProcess(this.#ctx, pid, commandRequest(opts, this.#ctx.requestTimeoutMs));
127
159
  }
128
160
  /** Sends input to a command started with `stdin: true`. */
129
- async sendStdin(pid, data, _opts = {}) {
130
- const process = await this.#process(pid);
161
+ async sendStdin(pid, data, opts = {}) {
162
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
163
+ const process = await resolveProcess(this.#ctx, pid, request);
131
164
  if (!process.info.stdinOpen)
132
165
  throw new InvalidArgumentError(`The command with pid ${pid} was not started with stdin: true, so its input is closed.`);
133
- await guard("sandbox", () => process.write(data));
166
+ request.signal?.throwIfAborted();
167
+ await guard("sandbox", () => process.write(data, request));
134
168
  }
135
- async closeStdin(pid, _opts = {}) {
136
- const process = await this.#process(pid);
137
- await guard("sandbox", () => process.write("", { eof: true }));
169
+ async closeStdin(pid, opts = {}) {
170
+ const request = commandRequest(opts, this.#ctx.requestTimeoutMs);
171
+ const process = await resolveProcess(this.#ctx, pid, request);
172
+ request.signal?.throwIfAborted();
173
+ await guard("sandbox", () => process.write("", { ...request, eof: true }));
138
174
  }
139
175
  /** Attaches to a running command; output from now on reaches the handle. */
140
176
  async connect(pid, opts = {}) {
141
- const process = await this.#process(pid);
177
+ const opening = connectionRequest(opts, this.#ctx.requestTimeoutMs);
178
+ const process = await resolveProcess(this.#ctx, pid, opening.request).catch(opening.failure);
142
179
  return new CommandHandle(process, {
143
180
  stdin: process.info.stdinOpen,
144
- timeoutMs: timeoutFor(opts.timeoutMs),
181
+ timeoutMs: opening.timeoutMs,
182
+ deadline: opening.deadline,
183
+ signal: opts.signal,
145
184
  cursor: process.info.outputBytes,
185
+ requestTimeoutMs: this.#ctx.requestTimeoutMs,
146
186
  ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
147
187
  ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
148
188
  });
@@ -157,6 +197,28 @@ function describe(info) {
157
197
  cwd: info.cwd,
158
198
  };
159
199
  }
200
+ /** Reader cancellation also releases a callback whose promise never settles. */
201
+ async function deliver(signal, callback) {
202
+ signal.throwIfAborted();
203
+ let cancelled;
204
+ const aborted = new Promise((_, reject) => {
205
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
206
+ cancelled = () => reject(signal.reason);
207
+ signal.addEventListener("abort", cancelled, { once: true });
208
+ });
209
+ try {
210
+ await Promise.race([
211
+ Promise.resolve().then(() => {
212
+ signal.throwIfAborted();
213
+ return callback();
214
+ }),
215
+ aborted,
216
+ ]);
217
+ }
218
+ finally {
219
+ signal.removeEventListener("abort", cancelled);
220
+ }
221
+ }
160
222
  /** A command started in the background: E2B's CommandHandle over a Runtime
161
223
  * process. Output streams into `stdout`/`stderr` and the callbacks as it
162
224
  * arrives. */
@@ -165,6 +227,7 @@ export class CommandHandle {
165
227
  #process;
166
228
  #stdin;
167
229
  #timeoutMs;
230
+ #requestTimeoutMs;
168
231
  #abort = new AbortController();
169
232
  #done;
170
233
  #stdout = "";
@@ -173,26 +236,53 @@ export class CommandHandle {
173
236
  #truncated = false;
174
237
  #failure;
175
238
  #disconnected = false;
239
+ #deadline;
176
240
  constructor(process, options) {
177
241
  this.#process = process;
178
242
  this.pid = pidOf(process.id);
179
243
  this.#stdin = options.stdin;
180
244
  this.#timeoutMs = options.timeoutMs;
245
+ this.#requestTimeoutMs = options.requestTimeoutMs;
246
+ // E2B's timeout limits the subscription. It never asks us to kill the
247
+ // process: the sandbox lease still bounds the workload after disconnect.
248
+ if (options.timeoutMs > 0) {
249
+ this.#deadline = setTimeout(() => {
250
+ this.#failure = timedOut(options.timeoutMs);
251
+ this.#abort.abort();
252
+ }, options.deadline === undefined
253
+ ? options.timeoutMs
254
+ : Math.max(0, options.deadline - performance.now()));
255
+ this.#deadline.unref?.();
256
+ }
181
257
  this.#done = this.#follow(options);
182
258
  }
183
259
  async #follow(options) {
260
+ const signal = options.signal
261
+ ? AbortSignal.any([options.signal, this.#abort.signal])
262
+ : this.#abort.signal;
184
263
  try {
185
- for await (const event of this.#process.output({
264
+ const outputOptions = {
186
265
  ...(options.cursor ? { cursor: options.cursor } : {}),
187
- signal: this.#abort.signal,
188
- })) {
189
- if (event.type === "stdout") {
266
+ signal,
267
+ };
268
+ const output = options.pty
269
+ ? this.#process.outputBytes(outputOptions)
270
+ : this.#process.output(outputOptions);
271
+ for await (const event of output) {
272
+ if (this.#disconnected)
273
+ break;
274
+ signal.throwIfAborted();
275
+ if ((event.type === "stdout" || event.type === "stderr") &&
276
+ event.data instanceof Uint8Array) {
277
+ await deliver(signal, () => options.onData?.(event.data));
278
+ }
279
+ else if (event.type === "stdout" && typeof event.data === "string") {
190
280
  this.#stdout += event.data;
191
- await options.onStdout?.(event.data);
281
+ await deliver(signal, () => options.onStdout?.(event.data));
192
282
  }
193
- else if (event.type === "stderr") {
283
+ else if (event.type === "stderr" && typeof event.data === "string") {
194
284
  this.#stderr += event.data;
195
- await options.onStderr?.(event.data);
285
+ await deliver(signal, () => options.onStderr?.(event.data));
196
286
  }
197
287
  else if (event.type === "truncated") {
198
288
  this.#truncated = true;
@@ -203,12 +293,16 @@ export class CommandHandle {
203
293
  }
204
294
  }
205
295
  catch (error) {
206
- if (!this.#disconnected) {
296
+ if (!this.#disconnected && !this.#failure) {
207
297
  const translated = translate(error, "sandbox");
208
298
  this.#failure =
209
299
  translated instanceof Error ? translated : new SandboxError(String(translated));
210
300
  }
211
301
  }
302
+ finally {
303
+ if (this.#deadline !== undefined)
304
+ clearTimeout(this.#deadline);
305
+ }
212
306
  }
213
307
  /** The exit code, or undefined while the command runs. */
214
308
  get exitCode() {
@@ -259,7 +353,7 @@ export class CommandHandle {
259
353
  if (this.#exit)
260
354
  return false;
261
355
  try {
262
- await guard("sandbox", () => this.#process.kill("SIGKILL"));
356
+ await guard("sandbox", () => this.#process.kill("SIGKILL", commandRequest({}, this.#requestTimeoutMs)));
263
357
  return true;
264
358
  }
265
359
  catch (error) {
@@ -268,14 +362,16 @@ export class CommandHandle {
268
362
  throw error;
269
363
  }
270
364
  }
271
- async sendStdin(data, _opts = {}) {
365
+ async sendStdin(data, opts = {}) {
366
+ const request = commandRequest(opts, this.#requestTimeoutMs);
272
367
  if (!this.#stdin)
273
368
  throw new InvalidArgumentError("The command was not started with stdin: true, so its input is closed.");
274
- await guard("sandbox", () => this.#process.write(data));
369
+ await guard("sandbox", () => this.#process.write(data, request));
275
370
  }
276
- async closeStdin(_opts = {}) {
371
+ async closeStdin(opts = {}) {
372
+ const request = commandRequest(opts, this.#requestTimeoutMs);
277
373
  if (!this.#stdin)
278
374
  throw new InvalidArgumentError("The command was not started with stdin: true.");
279
- await guard("sandbox", () => this.#process.write("", { eof: true }));
375
+ await guard("sandbox", () => this.#process.write("", { ...request, eof: true }));
280
376
  }
281
377
  }
@@ -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() {