withruntime 0.14.0 → 0.16.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 (62) hide show
  1. package/BLAXEL.md +97 -32
  2. package/COMPATIBILITY.md +120 -5
  3. package/DAYTONA.md +34 -3
  4. package/E2B.md +34 -5
  5. package/README.md +33 -0
  6. package/README.src.md +33 -0
  7. package/VERCEL.md +52 -5
  8. package/compatibility-lock.json +71 -111
  9. package/dist/blaxel/client.d.ts +11 -0
  10. package/dist/blaxel/client.js +28 -0
  11. package/dist/blaxel/errors.d.ts +65 -0
  12. package/dist/blaxel/errors.js +114 -0
  13. package/dist/blaxel/index.d.ts +10 -3
  14. package/dist/blaxel/index.js +23 -4
  15. package/dist/blaxel/preview.d.ts +29 -4
  16. package/dist/blaxel/preview.js +79 -5
  17. package/dist/blaxel/sandbox.js +1 -0
  18. package/dist/blaxel/session.js +4 -2
  19. package/dist/cli-extras.js +2 -1
  20. package/dist/cli-words.js +1 -0
  21. package/dist/cli.d.ts +1 -1
  22. package/dist/cli.js +54 -21
  23. package/dist/client.d.ts +1 -1
  24. package/dist/daytona/client.d.ts +4 -0
  25. package/dist/daytona/client.js +8 -3
  26. package/dist/daytona/daytona.d.ts +3 -0
  27. package/dist/daytona/daytona.js +2 -1
  28. package/dist/e2b/sandbox.d.ts +6 -5
  29. package/dist/e2b/sandbox.js +9 -12
  30. package/dist/freestyle/index.d.ts +77 -5
  31. package/dist/freestyle/index.js +380 -48
  32. package/dist/identity.js +36 -18
  33. package/dist/index.d.ts +2 -2
  34. package/dist/machine.d.ts +24 -12
  35. package/dist/machine.js +46 -14
  36. package/dist/mcp.js +16 -3
  37. package/dist/modal/index.d.ts +15 -0
  38. package/dist/modal/index.js +35 -7
  39. package/dist/morph/index.d.ts +255 -0
  40. package/dist/morph/index.js +641 -0
  41. package/dist/network-cli.js +2 -2
  42. package/dist/product-names.d.ts +2 -2
  43. package/dist/product-names.js +2 -2
  44. package/dist/products/limits.d.ts +16 -2
  45. package/dist/products/limits.js +2 -2
  46. package/dist/products/mcp.d.ts +5 -0
  47. package/dist/products/network-products.d.ts +21 -3
  48. package/dist/products/network-products.js +3 -1
  49. package/dist/products/previews.d.ts +6 -3
  50. package/dist/products/previews.js +1 -1
  51. package/dist/snapshots.d.ts +4 -0
  52. package/dist/ssh.d.ts +35 -0
  53. package/dist/ssh.js +93 -1
  54. package/dist/transport.d.ts +11 -8
  55. package/dist/transport.js +46 -12
  56. package/dist/types.d.ts +24 -0
  57. package/dist/vercel/command.d.ts +48 -1
  58. package/dist/vercel/command.js +237 -6
  59. package/dist/vercel/sandbox-user.js +2 -1
  60. package/dist/vercel/sandbox.d.ts +16 -2
  61. package/dist/vercel/sandbox.js +45 -2
  62. package/package.json +6 -1
@@ -1,4 +1,4 @@
1
- import { guard, StreamError, translate } from "./errors.js";
1
+ import { APIError, guard, StreamError, translate } from "./errors.js";
2
2
  const SIGNAL_NUMBERS = {
3
3
  1: "SIGHUP",
4
4
  2: "SIGINT",
@@ -22,6 +22,11 @@ function join(lines, stream) {
22
22
  .map((line) => line.data)
23
23
  .join("");
24
24
  }
25
+ /** One stdin request carries at most this much of a write (Runtime's input
26
+ * request size); a client error on it means none of it was applied. */
27
+ const STDIN_REQUEST_BYTES = 1_048_576;
28
+ /** Reading a piped stream pauses once this much waits behind a write. */
29
+ const STDIN_BUFFERED_BYTES = 512 * 1024;
25
30
  /** A command in a sandbox, as Vercel's `Command`. A detached one runs on:
26
31
  * `wait()` for its end, `logs()` for its output as it happens. `cmdId` is
27
32
  * Runtime's process id. */
@@ -30,6 +35,13 @@ export class Command {
30
35
  durationMs;
31
36
  init;
32
37
  #finished;
38
+ /** Pending stdin writes, so concurrent calls reach the process in call order. */
39
+ #stdinQueue = Promise.resolve();
40
+ /** Set when a write failed after possibly writing part of it; later writes
41
+ * are refused so nothing is appended to a partial message. */
42
+ #stdinError = null;
43
+ /** Set when the command was started with a `Readable` for stdin. */
44
+ stdinPipe = null;
33
45
  constructor(init) {
34
46
  this.init = init;
35
47
  }
@@ -93,10 +105,20 @@ export class Command {
93
105
  [Symbol.dispose]: () => abort.abort(),
94
106
  });
95
107
  }
96
- /** Waits for the command to end. */
108
+ /** Waits for the command to end. With a `Readable` piped to its stdin,
109
+ * rejects if piping fails, and stops piping once the command exits. */
97
110
  wait(params = {}) {
98
111
  this.#finished ??= this.#follow(params);
99
- return this.#finished;
112
+ const pipe = this.stdinPipe;
113
+ if (!pipe)
114
+ return this.#finished;
115
+ return waitWithStdinPipe(this.#finished, pipe, {
116
+ ...(params.signal ? { signal: params.signal } : {}),
117
+ onPipeError: () => {
118
+ if (this.stdinPipe === pipe)
119
+ this.stdinPipe = null;
120
+ },
121
+ });
100
122
  }
101
123
  async #follow(params) {
102
124
  const lines = [];
@@ -105,8 +127,11 @@ export class Command {
105
127
  for await (const event of this.init.process.output(params.signal ? { signal: params.signal } : {})) {
106
128
  if (event.type === "truncated")
107
129
  throw new StreamError("output_truncated", "Some command output is no longer available.", this.init.process.id);
108
- if (event.type === "stdout" || event.type === "stderr")
109
- lines.push({ stream: event.type, data: event.data });
130
+ if (event.type === "stdout" || event.type === "stderr") {
131
+ const line = { stream: event.type, data: event.data };
132
+ lines.push(line);
133
+ this.init.onLine?.(line);
134
+ }
110
135
  else if (event.type === "exit")
111
136
  exit = {
112
137
  exitCode: event.exitCode,
@@ -120,7 +145,8 @@ export class Command {
120
145
  this.exitCode = exitCodeOf(exit.exitCode, exit.timedOut);
121
146
  if (exit.durationMs !== undefined)
122
147
  this.durationMs = exit.durationMs;
123
- return new CommandFinished({ ...this.init, lines }, this.exitCode, exit.durationMs ?? undefined);
148
+ const { onLine: _onLine, ...init } = this.init;
149
+ return new CommandFinished({ ...init, lines }, this.exitCode, exit.durationMs ?? undefined);
124
150
  }
125
151
  /** stdout, stderr, or both in the order they were written. For a detached
126
152
  * command this waits for it to end. */
@@ -145,6 +171,203 @@ export class Command {
145
171
  throw new RangeError(`Signal ${signal} has no name Runtime knows; use SIGTERM, SIGKILL, SIGINT, SIGHUP, SIGQUIT, SIGUSR1 or SIGUSR2.`);
146
172
  await guard(() => this.init.process.kill(name, opts.abortSignal ? { signal: opts.abortSignal } : {}), this.init.sandboxName);
147
173
  }
174
+ /** Writes to the stdin of a command started with `stdin: true` and
175
+ * `detached: true`. Writes reach the process in call order and resolve once
176
+ * it has accepted the data; Runtime's input offsets make every resend exact,
177
+ * so no byte is written twice. If a write fails, part of it may have been
178
+ * written, and later writes are refused; closeStdin still works. */
179
+ async writeStdin(data, opts = {}) {
180
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
181
+ if (bytes.length === 0)
182
+ return;
183
+ await this.#enqueueStdin(async (process) => {
184
+ if (this.#stdinError)
185
+ throw new Error("Stdin is in an unknown state after an earlier write failed. Close stdin or kill the command.", { cause: this.#stdinError });
186
+ opts.abortSignal?.throwIfAborted();
187
+ try {
188
+ await guard(() => process.write(bytes, opts.abortSignal ? { signal: opts.abortSignal } : {}), this.init.sandboxName);
189
+ }
190
+ catch (error) {
191
+ const rejectedWhole = error instanceof APIError &&
192
+ error.response.status >= 400 &&
193
+ error.response.status < 500 &&
194
+ bytes.length <= STDIN_REQUEST_BYTES;
195
+ if (!rejectedWhole)
196
+ this.#stdinError = error;
197
+ throw error;
198
+ }
199
+ }, opts.abortSignal);
200
+ }
201
+ /** Closes the command's stdin, so it reads EOF once it has consumed what
202
+ * was written. */
203
+ async closeStdin(opts = {}) {
204
+ await this.#enqueueStdin((process) => guard(() => process.write(new Uint8Array(0), {
205
+ eof: true,
206
+ ...(opts.abortSignal ? { signal: opts.abortSignal } : {}),
207
+ }), this.init.sandboxName), opts.abortSignal);
208
+ }
209
+ #enqueueStdin(work, signal) {
210
+ const previous = this.#stdinQueue;
211
+ const run = (async () => {
212
+ await waitForTurn(previous, signal);
213
+ return work(this.init.process);
214
+ })();
215
+ this.#stdinQueue = Promise.allSettled([previous, run]).then(() => { });
216
+ return run;
217
+ }
218
+ }
219
+ function waitForTurn(previous, signal) {
220
+ if (!signal)
221
+ return previous;
222
+ signal.throwIfAborted();
223
+ return new Promise((resolve, reject) => {
224
+ const onAbort = () => reject(signal.reason);
225
+ signal.addEventListener("abort", onAbort, { once: true });
226
+ void previous.then(() => {
227
+ signal.removeEventListener("abort", onAbort);
228
+ resolve();
229
+ });
230
+ });
231
+ }
232
+ /** The command exited or closed its input: piping ends quietly, as a closed
233
+ * pipe does in a shell. */
234
+ function inputNoLongerAccepted(error) {
235
+ return (error instanceof APIError &&
236
+ (error.response.status === 404 ||
237
+ (error.response.status === 409 && error.code === "stdin_closed")));
238
+ }
239
+ /** Writes a stream to a command's stdin in order, closing stdin when it
240
+ * ends. What is read while a write is in flight goes in the next write, and
241
+ * reading pauses while too much waits. The stream is never ended or
242
+ * destroyed, and every listener is removed once piping stops. */
243
+ export function pipeStdin(command, stream, signal) {
244
+ let stopped = false;
245
+ let writing = false;
246
+ let ended = false;
247
+ let waiting = [];
248
+ let waitingBytes = 0;
249
+ let resolveDone;
250
+ let rejectDone;
251
+ const done = new Promise((resolve, reject) => {
252
+ resolveDone = resolve;
253
+ rejectDone = reject;
254
+ });
255
+ done.catch(() => { });
256
+ const stop = () => {
257
+ if (stopped)
258
+ return false;
259
+ stopped = true;
260
+ stream.off("data", onData);
261
+ stream.off("end", onEnd);
262
+ stream.off("error", onError);
263
+ stream.off("close", onClose);
264
+ signal?.removeEventListener("abort", onAbort);
265
+ stream.pause();
266
+ return true;
267
+ };
268
+ const settle = (error) => {
269
+ if (!stop())
270
+ return;
271
+ if (error === undefined)
272
+ resolveDone();
273
+ else
274
+ rejectDone(error);
275
+ };
276
+ const failed = (error) => settle(inputNoLongerAccepted(error) ? undefined : error);
277
+ const flush = () => {
278
+ if (stopped || writing)
279
+ return;
280
+ if (waiting.length) {
281
+ writing = true;
282
+ const data = concat(waiting);
283
+ waiting = [];
284
+ waitingBytes = 0;
285
+ command.writeStdin(data).then(() => {
286
+ writing = false;
287
+ if (stopped)
288
+ return;
289
+ if (!ended)
290
+ stream.resume();
291
+ flush();
292
+ }, failed);
293
+ }
294
+ else if (ended) {
295
+ writing = true;
296
+ command.closeStdin().then(() => settle(), failed);
297
+ }
298
+ };
299
+ function onData(chunk) {
300
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
301
+ waiting.push(bytes);
302
+ waitingBytes += bytes.length;
303
+ if (waitingBytes >= STDIN_BUFFERED_BYTES)
304
+ stream.pause();
305
+ flush();
306
+ }
307
+ function onEnd() {
308
+ ended = true;
309
+ flush();
310
+ }
311
+ function onError(error) {
312
+ if (!stop())
313
+ return;
314
+ void command
315
+ .closeStdin()
316
+ .catch(() => { })
317
+ .finally(() => rejectDone(error));
318
+ }
319
+ function onClose() {
320
+ if (!stream.readableEnded)
321
+ onError(new Error("The stdin stream closed before it ended"));
322
+ }
323
+ function onAbort() {
324
+ settle();
325
+ }
326
+ if (signal?.aborted)
327
+ settle();
328
+ else {
329
+ signal?.addEventListener("abort", onAbort, { once: true });
330
+ stream.on("error", onError);
331
+ stream.on("close", onClose);
332
+ if (stream.destroyed && !stream.readableEnded)
333
+ onClose();
334
+ else {
335
+ stream.on("end", onEnd);
336
+ stream.on("data", onData);
337
+ if (stream.readableEnded)
338
+ onEnd();
339
+ }
340
+ }
341
+ return { done, stop: () => settle() };
342
+ }
343
+ function concat(chunks) {
344
+ if (chunks.length === 1)
345
+ return chunks[0];
346
+ const out = new Uint8Array(chunks.reduce((n, chunk) => n + chunk.length, 0));
347
+ let at = 0;
348
+ for (const chunk of chunks) {
349
+ out.set(chunk, at);
350
+ at += chunk.length;
351
+ }
352
+ return out;
353
+ }
354
+ /** Waits for a command to finish, rejecting early if piping its stdin fails,
355
+ * and stops piping once it has exited. Aborting `signal` stops waiting but
356
+ * leaves the pipe running. */
357
+ export async function waitWithStdinPipe(finished, pipe, opts = {}) {
358
+ if (!pipe)
359
+ return finished;
360
+ finished.then(() => pipe.stop(), () => {
361
+ if (!opts.signal?.aborted)
362
+ pipe.stop();
363
+ });
364
+ return Promise.race([
365
+ finished,
366
+ pipe.done.then(() => finished, (error) => {
367
+ opts.onPipeError?.();
368
+ throw error;
369
+ }),
370
+ ]);
148
371
  }
149
372
  /** A command that has ended; `exitCode` is set. */
150
373
  export class CommandFinished extends Command {
@@ -157,4 +380,12 @@ export class CommandFinished extends Command {
157
380
  wait() {
158
381
  return Promise.resolve(this);
159
382
  }
383
+ /** Always throws: the command has already exited. */
384
+ async writeStdin(_data, _opts) {
385
+ throw new Error("Cannot write to stdin: the command has already finished.");
386
+ }
387
+ /** Always throws: the command has already exited. */
388
+ async closeStdin(_opts) {
389
+ throw new Error("Cannot close stdin: the command has already finished.");
390
+ }
160
391
  }
@@ -76,7 +76,8 @@ export class SandboxUser {
76
76
  return this.#sandbox.runCommand({
77
77
  cmd: wrapped.cmd,
78
78
  args: wrapped.args,
79
- ...(params.detached ? { detached: true } : {}),
79
+ ...(params.detached ? { detached: true } : { onStart: params.onStart }),
80
+ ...(params.stdin ? { stdin: params.stdin } : {}),
80
81
  ...(params.stdout ? { stdout: params.stdout } : {}),
81
82
  ...(params.stderr ? { stderr: params.stderr } : {}),
82
83
  ...(params.signal ? { signal: params.signal } : {}),
@@ -1,4 +1,4 @@
1
- import type { Writable } from "node:stream";
1
+ import type { Readable, Writable } from "node:stream";
2
2
  import type { Runtime } from "../client.js";
3
3
  import { type Credentials, type RuntimeCreate, type RuntimeSandbox, type WithRuntime } from "./client.js";
4
4
  import { Command, CommandFinished } from "./command.js";
@@ -82,7 +82,7 @@ export interface CreateSandboxParams extends Credentials {
82
82
  /** Runtime-only: an explicit client, or fields for the create. */
83
83
  withruntime?: WithRuntime;
84
84
  }
85
- export interface RunCommandParams {
85
+ interface RunCommandParamsBase {
86
86
  cmd: string;
87
87
  args?: string[];
88
88
  cwd?: string;
@@ -90,11 +90,25 @@ export interface RunCommandParams {
90
90
  /** Runs the command under `sudo`, which needs no password. */
91
91
  sudo?: boolean;
92
92
  detached?: boolean;
93
+ /** `true` keeps stdin open for `writeStdin` and `closeStdin` (needs
94
+ * `detached: true`). A `Readable` is written to stdin, which closes when it
95
+ * ends; the stream is never ended or destroyed. If piping fails, an
96
+ * attached command is killed and `runCommand` rejects; a detached one's
97
+ * `wait()` rejects. Unset or false: stdin is empty. */
98
+ stdin?: boolean | Readable;
93
99
  stdout?: Writable;
94
100
  stderr?: Writable;
95
101
  signal?: AbortSignal;
96
102
  timeoutMs?: number;
97
103
  }
104
+ export type RunCommandParams = RunCommandParamsBase & ({
105
+ onStart?: never;
106
+ } | {
107
+ detached?: false;
108
+ /** Called with the Command as soon as it starts, before it exits;
109
+ * awaited before `runCommand` resolves. */
110
+ onStart?: (command: Command) => void | Promise<void>;
111
+ });
98
112
  export interface SandboxRoute {
99
113
  port: number;
100
114
  subdomain: string;
@@ -1,7 +1,7 @@
1
1
  import { extensionSeconds, LeaseKeeper, LEASE_MAX_SECONDS } from "../compat/lease.js";
2
2
  import { RuntimeError } from "../errors.js";
3
3
  import { clientFor, signalOf, } from "./client.js";
4
- import { Command, CommandFinished, exitCodeOf } from "./command.js";
4
+ import { Command, CommandFinished, exitCodeOf, pipeStdin, waitWithStdinPipe, } from "./command.js";
5
5
  import { APIError, guard, NotSupportedError, StreamError, translate } from "./errors.js";
6
6
  import { FileSystem } from "./filesystem.js";
7
7
  import { rootOrThrow, SandboxUser, SandboxUserAlreadyExistsError, validateName, } from "./sandbox-user.js";
@@ -479,6 +479,9 @@ export class Sandbox {
479
479
  const params = typeof commandOrParams === "string"
480
480
  ? { cmd: commandOrParams, ...(args ? { args } : {}), ...opts }
481
481
  : commandOrParams;
482
+ if (params.stdin === true && !params.detached)
483
+ throw new TypeError("`stdin: true` requires `detached: true`, otherwise the command would wait for input that can never be written");
484
+ const stdinStream = typeof params.stdin === "object" && params.stdin !== null ? params.stdin : null;
482
485
  const argv = [
483
486
  ...(params.sudo ? ["sudo", "--preserve-env"] : []),
484
487
  params.cmd,
@@ -497,8 +500,9 @@ export class Sandbox {
497
500
  };
498
501
  const startedAt = Date.now();
499
502
  const vercelCwd = params.cwd === undefined ? HOME : toVercelPath(params.cwd);
503
+ const spawn = () => this.#withResume((runtime) => runtime.spawn(argv, { ...common, ...(params.stdin ? { stdin: "pipe" } : {}) }), params);
500
504
  if (params.detached) {
501
- const process = await this.#withResume((runtime) => runtime.spawn(argv, common), params);
505
+ const process = await spawn();
502
506
  const command = new Command({
503
507
  id: process.id,
504
508
  cwd: vercelCwd,
@@ -506,10 +510,38 @@ export class Sandbox {
506
510
  sandboxName: this.name,
507
511
  process,
508
512
  });
513
+ if (stdinStream)
514
+ command.stdinPipe = pipeStdin(command, stdinStream, params.signal);
509
515
  if (params.stdout || params.stderr)
510
516
  void pipe(command, params);
511
517
  return command;
512
518
  }
519
+ if (stdinStream || params.onStart) {
520
+ // Started as a process so its Command exists before it exits: for
521
+ // onStart, and as the target of the piped stdin.
522
+ const process = await spawn();
523
+ const command = new Command({
524
+ id: process.id,
525
+ cwd: vercelCwd,
526
+ startedAt,
527
+ sandboxName: this.name,
528
+ process,
529
+ onLine: (line) => (line.stream === "stdout" ? params.stdout : params.stderr)?.write(line.data),
530
+ });
531
+ const stdinPipe = stdinStream && pipeStdinOrKill(command, stdinStream, params.signal);
532
+ const finished = waitWithStdinPipe(command.wait(params.signal ? { signal: params.signal } : {}), stdinPipe);
533
+ // The command can fail while onStart is pending or after it throws.
534
+ finished.catch(() => { });
535
+ try {
536
+ await params.onStart?.(command);
537
+ return await finished;
538
+ }
539
+ catch (error) {
540
+ if (params.signal?.aborted)
541
+ await command.kill("SIGTERM").catch(() => { });
542
+ throw error;
543
+ }
544
+ }
513
545
  const lines = [];
514
546
  const record = (stream, writer) => (data) => {
515
547
  lines.push({ stream, data });
@@ -855,6 +887,17 @@ export function retentionDays(ms) {
855
887
  return 365;
856
888
  return Math.min(365, Math.max(1, Math.ceil(ms / 86_400_000)));
857
889
  }
890
+ /** Pipes stdin for a command the caller waits on, killing the command if
891
+ * piping fails, since the caller has no handle to stop it otherwise. */
892
+ function pipeStdinOrKill(command, stream, signal) {
893
+ const stdinPipe = pipeStdin(command, stream, signal);
894
+ const done = stdinPipe.done.catch((error) => {
895
+ command.kill().catch(() => { });
896
+ throw error;
897
+ });
898
+ done.catch(() => { });
899
+ return { done, stop: () => stdinPipe.stop() };
900
+ }
858
901
  async function pipe(command, params) {
859
902
  try {
860
903
  for await (const line of command.logs())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "license": "Apache-2.0",
5
5
  "mcpName": "com.withruntime/runtime",
6
6
  "type": "module",
@@ -125,6 +125,11 @@
125
125
  "import": "./dist/freestyle/index.js",
126
126
  "default": "./dist/freestyle/index.js"
127
127
  },
128
+ "./morph": {
129
+ "types": "./dist/morph/index.d.ts",
130
+ "import": "./dist/morph/index.js",
131
+ "default": "./dist/morph/index.js"
132
+ },
128
133
  "./modal": {
129
134
  "types": "./dist/modal/index.d.ts",
130
135
  "import": "./dist/modal/index.js",