withruntime 0.10.0 → 0.11.1

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 (94) hide show
  1. package/BLAXEL.md +7 -8
  2. package/DAYTONA.md +40 -35
  3. package/E2B.md +70 -57
  4. package/README.md +3 -3
  5. package/README.src.md +3 -3
  6. package/VERCEL.md +23 -20
  7. package/compatibility-lock.json +63 -84
  8. package/dist/api-defaults.d.ts +1 -1
  9. package/dist/api-defaults.js +1 -1
  10. package/dist/blaxel/lookup.d.ts +2 -1
  11. package/dist/blaxel/lookup.js +2 -1
  12. package/dist/blaxel/sandbox.d.ts +7 -5
  13. package/dist/blaxel/sandbox.js +15 -10
  14. package/dist/cli.js +112 -17
  15. package/dist/client.d.ts +17 -1
  16. package/dist/client.js +46 -3
  17. package/dist/compat/files.js +5 -1
  18. package/dist/compat/lease.d.ts +37 -0
  19. package/dist/compat/lease.js +93 -0
  20. package/dist/credentials.d.ts +9 -0
  21. package/dist/credentials.js +26 -4
  22. package/dist/daytona/computer-use.d.ts +162 -0
  23. package/dist/daytona/computer-use.js +324 -0
  24. package/dist/daytona/context.d.ts +10 -0
  25. package/dist/daytona/context.js +24 -0
  26. package/dist/daytona/daytona.d.ts +20 -0
  27. package/dist/daytona/daytona.js +150 -26
  28. package/dist/daytona/filesystem.js +8 -0
  29. package/dist/daytona/index.d.ts +2 -26
  30. package/dist/daytona/index.js +2 -7
  31. package/dist/daytona/process.d.ts +63 -6
  32. package/dist/daytona/process.js +224 -23
  33. package/dist/daytona/sandbox.d.ts +25 -6
  34. package/dist/daytona/sandbox.js +60 -27
  35. package/dist/e2b/client.d.ts +5 -0
  36. package/dist/e2b/client.js +5 -1
  37. package/dist/e2b/commands.d.ts +15 -6
  38. package/dist/e2b/commands.js +83 -18
  39. package/dist/e2b/errors.d.ts +7 -0
  40. package/dist/e2b/errors.js +20 -0
  41. package/dist/e2b/filesystem.d.ts +2 -1
  42. package/dist/e2b/filesystem.js +43 -6
  43. package/dist/e2b/index.d.ts +2 -2
  44. package/dist/e2b/index.js +1 -1
  45. package/dist/e2b/pty.js +6 -3
  46. package/dist/e2b/sandbox.d.ts +20 -6
  47. package/dist/e2b/sandbox.js +207 -31
  48. package/dist/e2b/users.d.ts +30 -0
  49. package/dist/e2b/users.js +185 -0
  50. package/dist/errors.d.ts +6 -0
  51. package/dist/errors.js +6 -0
  52. package/dist/identity.js +2 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.js +2 -1
  55. package/dist/mcp.d.ts +2 -1
  56. package/dist/mcp.js +6 -1
  57. package/dist/modal/index.js +6 -2
  58. package/dist/network-cli.js +33 -5
  59. package/dist/products/browser.d.ts +61 -0
  60. package/dist/products/browser.js +27 -0
  61. package/dist/products/desktop.d.ts +4 -3
  62. package/dist/products/desktop.js +7 -32
  63. package/dist/products/images.d.ts +3 -1
  64. package/dist/products/images.js +14 -2
  65. package/dist/products/index.d.ts +2 -0
  66. package/dist/products/index.js +2 -0
  67. package/dist/products/jobs.d.ts +7 -5
  68. package/dist/products/jobs.js +7 -3
  69. package/dist/products/mcp.d.ts +5 -1
  70. package/dist/products/mcp.js +29 -8
  71. package/dist/products/network-products.d.ts +8 -1
  72. package/dist/products/network-products.js +4 -1
  73. package/dist/products/observability.d.ts +1 -1
  74. package/dist/products/private-network.d.ts +22 -0
  75. package/dist/products/private-network.js +10 -0
  76. package/dist/products/secrets.d.ts +2 -2
  77. package/dist/runloop/index.js +6 -2
  78. package/dist/sandbox.d.ts +44 -14
  79. package/dist/sandbox.js +162 -48
  80. package/dist/tar.d.ts +2 -0
  81. package/dist/tar.js +74 -16
  82. package/dist/transport.d.ts +33 -7
  83. package/dist/transport.js +171 -36
  84. package/dist/types.d.ts +59 -11
  85. package/dist/vercel/filesystem.js +3 -1
  86. package/dist/vercel/index.d.ts +1 -7
  87. package/dist/vercel/index.js +1 -3
  88. package/dist/vercel/sandbox-user.d.ts +70 -0
  89. package/dist/vercel/sandbox-user.js +206 -0
  90. package/dist/vercel/sandbox.d.ts +57 -15
  91. package/dist/vercel/sandbox.js +219 -47
  92. package/dist/wait.d.ts +12 -0
  93. package/dist/wait.js +47 -0
  94. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.10.0";
2
+ export declare const VERSION = "0.11.1";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
5
5
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -7,6 +7,11 @@ export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
7
7
  * because the hop never leaves the machine: it goes from the program to the
8
8
  * guest's own proxy and over the sandbox's private channel to its host. */
9
9
  export declare const SANDBOX_BASE_URL = "http://runtime.internal";
10
+ /** Every Runtime sandbox has this file; guest-net.py keeps it current. Tests
11
+ * move it, so the machine running them does not decide what they see. */
12
+ export declare const sandboxMarker: {
13
+ path: string;
14
+ };
10
15
  /** True in a Runtime sandbox. Node and Bun only; a browser is never one. */
11
16
  export declare function inRuntimeSandbox(marker?: string): boolean;
12
17
  /** Where calls go when no base URL is given: RUNTIME_API_URL, then Runtime's
@@ -51,17 +56,34 @@ export type Call = RequestOptions & {
51
56
  /** Called each time a call waits for room, with the refusal it is waiting
52
57
  * out and how long it will sleep before sending the call again. */
53
58
  onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
59
+ /** Internal: when the call's own deadline ends (performance.now()), set
60
+ * once the call is prepared; absent when it has none. */
61
+ deadlineAt?: number;
54
62
  };
63
+ /** How long one attempt waits for its answer to start. The API starts every
64
+ * answer within 120 s: Caddy answers 503 itself past that, and a longer call
65
+ * gets its 200 at 90 s and then its JSON (runtime-late-answer). An attempt
66
+ * with no answer by then is talking to a server that is gone, such as a
67
+ * controller that died with the call in flight while its address moved to
68
+ * the new leader: a client that only waits sends nothing the new leader could
69
+ * refuse, so it waited out the whole deadline and failed (the leader drill on
70
+ * vin-5, 4 October 2026). It is sent again under the same key. */
71
+ export declare const ANSWER_START_MS = 125000;
55
72
  export declare function apiOrigin(value: string): string;
56
73
  export declare function encodeQuery(query: Query | undefined): string;
74
+ /** Connections a client holds at once by default: under the 64 one address
75
+ * may hold to the API (api.md, "Limits"), with room for a second client. */
76
+ export declare const DEFAULT_MAX_CONNECTIONS = 48;
57
77
  /** One connection pool, one retry policy, one error shape for every product.
58
78
  * Node's and Bun's fetch keep connections alive, so a client reused across
59
79
  * calls pays TLS once. Over HTTP/1.1 every call in flight holds its own
60
- * connection, so a hundred calls at once would open a hundred, past what one
61
- * address may hold at the edge (64). Answers read whole are therefore held to
62
- * `maxConnections` at once and the rest queue here, reusing those
63
- * connections; streams and terminals are long-lived and do not wait behind
64
- * them. */
80
+ * connection, and one address may hold 64 to the API before it has used a
81
+ * valid key: past that the API's edge holds new ones back until one closes.
82
+ * So a client holds at most `maxConnections` at once,
83
+ * answers and streams together, and the rest queue here, first come first
84
+ * served, and wait rather than fail. Streams (a command's output, a download,
85
+ * a watch) take at most `maxConnections` less KEPT_FOR_CALLS of them. A
86
+ * terminal or tunnel's WebSocket is not counted. */
65
87
  export declare class Transport {
66
88
  #private;
67
89
  readonly baseUrl: string;
@@ -73,9 +95,13 @@ export declare class Transport {
73
95
  baseUrl?: string;
74
96
  fetch?: typeof fetch;
75
97
  timeoutMs?: number;
98
+ /** How long one attempt waits for its answer to start before it is sent
99
+ * again under the same key. Default ANSWER_START_MS. */
100
+ answerStartMs?: number;
76
101
  maxRetries?: number;
77
102
  client?: string;
78
- /** Calls read whole that may be in flight at once. Default 32. */
103
+ /** Connections held at once, answers and streams together; the rest
104
+ * wait their turn. Default 48: one address may hold 64 to the API. */
79
105
  maxConnections?: number;
80
106
  /** How long a create waits for room. Default 120 000; 0 fails at once. */
81
107
  waitForCapacityMs?: number;
package/dist/transport.js CHANGED
@@ -1,6 +1,6 @@
1
- import { ConnectionError, DELIBERATE, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
1
+ import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
- export const VERSION = "0.10.0";
3
+ export const VERSION = "0.11.1";
4
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
5
5
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
6
6
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -8,10 +8,11 @@ export const DEFAULT_BASE_URL = "https://api.withruntime.com";
8
8
  * because the hop never leaves the machine: it goes from the program to the
9
9
  * guest's own proxy and over the sandbox's private channel to its host. */
10
10
  export const SANDBOX_BASE_URL = "http://runtime.internal";
11
- /** Every Runtime sandbox has this file; guest-net.py keeps it current. */
12
- const SANDBOX_MARKER = "/run/runtime/environment.json";
11
+ /** Every Runtime sandbox has this file; guest-net.py keeps it current. Tests
12
+ * move it, so the machine running them does not decide what they see. */
13
+ export const sandboxMarker = { path: "/run/runtime/environment.json" };
13
14
  /** True in a Runtime sandbox. Node and Bun only; a browser is never one. */
14
- export function inRuntimeSandbox(marker = SANDBOX_MARKER) {
15
+ export function inRuntimeSandbox(marker = sandboxMarker.path) {
15
16
  try {
16
17
  const host = globalThis.process;
17
18
  return host?.getBuiltinModule?.("node:fs")?.existsSync?.(marker) === true;
@@ -34,6 +35,18 @@ export function reachable(origin, inSandbox = inRuntimeSandbox) {
34
35
  function processEnv() {
35
36
  return (globalThis.process?.env ?? {});
36
37
  }
38
+ /** How long an answer that ran nothing (NOTHING_RAN) is retried for a call
39
+ * that has no deadline of its own. */
40
+ const NOTHING_RAN_MS = 300_000;
41
+ /** How long one attempt waits for its answer to start. The API starts every
42
+ * answer within 120 s: Caddy answers 503 itself past that, and a longer call
43
+ * gets its 200 at 90 s and then its JSON (runtime-late-answer). An attempt
44
+ * with no answer by then is talking to a server that is gone, such as a
45
+ * controller that died with the call in flight while its address moved to
46
+ * the new leader: a client that only waits sends nothing the new leader could
47
+ * refuse, so it waited out the whole deadline and failed (the leader drill on
48
+ * vin-5, 4 October 2026). It is sent again under the same key. */
49
+ export const ANSWER_START_MS = 125_000;
37
50
  export function apiOrigin(value) {
38
51
  const url = new URL(value);
39
52
  // runtime.internal is reserved and never resolves outside a sandbox, so a
@@ -141,14 +154,23 @@ function slots(maximum) {
141
154
  },
142
155
  };
143
156
  }
157
+ /** Connections a client holds at once by default: under the 64 one address
158
+ * may hold to the API (api.md, "Limits"), with room for a second client. */
159
+ export const DEFAULT_MAX_CONNECTIONS = 48;
160
+ /** Connections a client keeps for answers read whole, however many streams
161
+ * are open: a stream's reader that makes a call (an output callback writing a
162
+ * file) never waits behind streams, its own included. */
163
+ const KEPT_FOR_CALLS = 8;
144
164
  /** One connection pool, one retry policy, one error shape for every product.
145
165
  * Node's and Bun's fetch keep connections alive, so a client reused across
146
166
  * calls pays TLS once. Over HTTP/1.1 every call in flight holds its own
147
- * connection, so a hundred calls at once would open a hundred, past what one
148
- * address may hold at the edge (64). Answers read whole are therefore held to
149
- * `maxConnections` at once and the rest queue here, reusing those
150
- * connections; streams and terminals are long-lived and do not wait behind
151
- * them. */
167
+ * connection, and one address may hold 64 to the API before it has used a
168
+ * valid key: past that the API's edge holds new ones back until one closes.
169
+ * So a client holds at most `maxConnections` at once,
170
+ * answers and streams together, and the rest queue here, first come first
171
+ * served, and wait rather than fail. Streams (a command's output, a download,
172
+ * a watch) take at most `maxConnections` less KEPT_FOR_CALLS of them. A
173
+ * terminal or tunnel's WebSocket is not counted. */
152
174
  export class Transport {
153
175
  baseUrl;
154
176
  #apiKey;
@@ -157,9 +179,11 @@ export class Transport {
157
179
  /** Whether calls go through envFetch, so a failure can name its proxy. */
158
180
  #routed;
159
181
  #timeoutMs;
182
+ #answerStartMs;
160
183
  #maxRetries;
161
184
  #client;
162
185
  #slots;
186
+ #streams;
163
187
  /** What `sandboxes.create` waits for room by default. */
164
188
  waitForCapacityMs;
165
189
  constructor(options) {
@@ -175,9 +199,12 @@ export class Transport {
175
199
  this.#fetch = options.fetch ?? envFetch;
176
200
  this.#routed = options.fetch === undefined;
177
201
  this.#timeoutMs = options.timeoutMs ?? 300_000;
202
+ this.#answerStartMs = options.answerStartMs ?? ANSWER_START_MS;
178
203
  this.#maxRetries = options.maxRetries ?? 4;
179
204
  this.#client = options.client ?? `sdk-js/${VERSION}`;
180
- this.#slots = slots(Math.max(1, options.maxConnections ?? 32));
205
+ const most = Math.max(1, options.maxConnections ?? DEFAULT_MAX_CONNECTIONS);
206
+ this.#slots = slots(most);
207
+ this.#streams = slots(Math.max(1, most - KEPT_FOR_CALLS));
181
208
  this.waitForCapacityMs = Math.max(0, options.waitForCapacityMs ?? 120_000);
182
209
  }
183
210
  /** The key, found once on first use when it was not given. */
@@ -225,6 +252,7 @@ export class Transport {
225
252
  return {
226
253
  ...call,
227
254
  timeoutMs: 0,
255
+ ...(timeoutMs === 0 ? {} : { deadlineAt: performance.now() + timeoutMs + roomMs }),
228
256
  ...(signal ? { signal } : {}),
229
257
  ...(call.method !== "GET"
230
258
  ? { idempotencyKey: call.idempotencyKey ?? crypto.randomUUID() }
@@ -251,7 +279,17 @@ export class Transport {
251
279
  cause,
252
280
  });
253
281
  }
254
- async #send(call, consume) {
282
+ /** Sleeps between tries. A call holding a connection slot (`gate`) lends
283
+ * it out while it sleeps, since a sleep holds no connection, and takes a
284
+ * slot again before it sends: a thousand creates waiting out a full region
285
+ * or a 429 would otherwise keep every other call of this client, the stop
286
+ * that frees room included, queued behind them. */
287
+ async #rest(ms, signal, gate) {
288
+ gate?.lend();
289
+ await sleep(ms, signal);
290
+ await gate?.take();
291
+ }
292
+ async #send(call, consume, gate) {
255
293
  let apiKey;
256
294
  try {
257
295
  call.signal?.throwIfAborted();
@@ -271,6 +309,9 @@ export class Transport {
271
309
  const body = call.bytes ?? (call.body === undefined ? undefined : JSON.stringify(call.body));
272
310
  const retry = call.retry ?? true;
273
311
  let attempt = 0;
312
+ // An answer that ran nothing is sent again until the call's deadline.
313
+ const idleUntil = call.deadlineAt ?? performance.now() + NOTHING_RAN_MS;
314
+ let idleAttempt = 0;
274
315
  for (;;) {
275
316
  let response;
276
317
  let inCallback = false;
@@ -282,13 +323,20 @@ export class Transport {
282
323
  // The API never redirects, so a redirect is refused here exactly as
283
324
  // "error" refused it: thrown, retried, then a connection_error.
284
325
  signal?.throwIfAborted();
285
- response = await send(url, {
286
- method: call.method,
287
- headers: this.#headers(call, key, apiKey),
288
- redirect: "manual",
289
- signal,
290
- ...(body === undefined ? {} : { body: body }),
291
- });
326
+ const started = new AbortController();
327
+ const unanswered = setTimeout(() => started.abort(new DOMException("No answer started.", "TimeoutError")), this.#answerStartMs);
328
+ try {
329
+ response = await send(url, {
330
+ method: call.method,
331
+ headers: this.#headers(call, key, apiKey),
332
+ redirect: "manual",
333
+ signal: signal ? AbortSignal.any([signal, started.signal]) : started.signal,
334
+ ...(body === undefined ? {} : { body: body }),
335
+ });
336
+ }
337
+ finally {
338
+ clearTimeout(unanswered);
339
+ }
292
340
  if (response.type === "opaqueredirect" ||
293
341
  (response.status >= 300 && response.status < 400)) {
294
342
  await response.body?.cancel().catch(() => { });
@@ -350,7 +398,23 @@ export class Transport {
350
398
  inCallback = true;
351
399
  call.onCapacityWait?.(error, pause);
352
400
  inCallback = false;
353
- await sleep(pause, signal);
401
+ await this.#rest(pause, signal, gate);
402
+ continue;
403
+ }
404
+ // Nothing ran: safe to send again for any call, past maxRetries,
405
+ // with growing waits, until the deadline; then this refusal stands.
406
+ // A late answer's failure came after the work began, so only a
407
+ // guest's own refusal (`guest_busy`, before anything is journaled)
408
+ // still says nothing ran.
409
+ if ([429, 503].includes(status) &&
410
+ NOTHING_RAN.has(error.code) &&
411
+ (!late || error.code === "guest_busy")) {
412
+ const header = Number(response.headers.get("retry-after"));
413
+ const base = error.retryAfterMs ?? (Number.isFinite(header) && header > 0 ? header * 1000 : 250);
414
+ const pause = Math.min(30_000, base * 2 ** Math.min(idleAttempt++, 3)) * (0.9 + Math.random() * 0.2);
415
+ if (pause >= idleUntil - performance.now())
416
+ throw error;
417
+ await this.#rest(pause, signal, gate);
354
418
  continue;
355
419
  }
356
420
  const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
@@ -359,7 +423,7 @@ export class Transport {
359
423
  const header = Number(response.headers.get("retry-after"));
360
424
  const wait = error.retryAfterMs ??
361
425
  (Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
362
- await sleep(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal);
426
+ await this.#rest(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal, gate);
363
427
  }
364
428
  catch (cause) {
365
429
  // Deliberate API refusals and unusable proxy settings cannot be
@@ -373,25 +437,42 @@ export class Transport {
373
437
  throw cause;
374
438
  if (signal?.aborted || !retry || attempt >= this.#maxRetries)
375
439
  throw this.#connectionError(call, cause);
376
- await sleep(backoff(attempt), signal).catch((cause) => {
377
- throw this.#connectionError(call, cause);
440
+ await this.#rest(backoff(attempt), signal, gate).catch((cause) => {
441
+ throw cause instanceof RuntimeError ? cause : this.#connectionError(call, cause);
378
442
  });
379
443
  }
380
444
  attempt++;
381
445
  }
382
446
  }
447
+ /** A hold on `kinds` of slot, taken in order and given back together. */
448
+ #gate(call, kinds) {
449
+ let held = 0;
450
+ const lend = () => {
451
+ while (held > 0)
452
+ kinds[--held].give();
453
+ };
454
+ return {
455
+ take: async () => {
456
+ try {
457
+ for (; held < kinds.length; held++)
458
+ await kinds[held].take(call.signal);
459
+ }
460
+ catch (cause) {
461
+ lend();
462
+ throw this.#connectionError(call, cause);
463
+ }
464
+ },
465
+ lend,
466
+ };
467
+ }
383
468
  async #queued(call, consume) {
469
+ const gate = this.#gate(call, [this.#slots]);
470
+ await gate.take();
384
471
  try {
385
- await this.#slots.take(call.signal);
386
- }
387
- catch (cause) {
388
- throw this.#connectionError(call, cause);
389
- }
390
- try {
391
- return await this.#send(call, consume);
472
+ return await this.#send(call, consume, gate);
392
473
  }
393
474
  finally {
394
- this.#slots.give();
475
+ gate.lend();
395
476
  }
396
477
  }
397
478
  async json(call) {
@@ -426,11 +507,12 @@ export class Transport {
426
507
  * errors with `download_incomplete` if it ends short of the length the API
427
508
  * promised; it is never retried part way. */
428
509
  async fileStream(call) {
429
- const response = await this.send(call);
510
+ const { response, release } = await this.#opened(this.#prepare(call));
430
511
  const expected = promisedLength(response.headers);
431
512
  const body = response.body ?? new ReadableStream({ start: (c) => c.close() });
432
513
  let got = 0;
433
- return body.pipeThrough(new TransformStream({
514
+ const reader = body
515
+ .pipeThrough(new TransformStream({
434
516
  transform(chunk, controller) {
435
517
  got += chunk.byteLength;
436
518
  if (expected !== undefined && got > expected)
@@ -442,13 +524,65 @@ export class Transport {
442
524
  if (expected !== undefined && got !== expected)
443
525
  controller.error(incompleteDownload(got, expected));
444
526
  },
445
- }));
527
+ }))
528
+ .getReader();
529
+ // The connection is given back when the body ends, fails or is cancelled.
530
+ return new ReadableStream({
531
+ async pull(controller) {
532
+ try {
533
+ const { value, done } = await reader.read();
534
+ if (done) {
535
+ release();
536
+ controller.close();
537
+ }
538
+ else
539
+ controller.enqueue(value);
540
+ }
541
+ catch (error) {
542
+ release();
543
+ controller.error(error);
544
+ }
545
+ },
546
+ async cancel(reason) {
547
+ release();
548
+ await reader.cancel(reason);
549
+ },
550
+ });
551
+ }
552
+ /** Sends a call whose answer is a stream, holding a stream slot and a
553
+ * connection slot from before it is sent until `release`: once its body is
554
+ * read, failed or cancelled, or its deadline or signal ends it, whichever
555
+ * comes first, so a stream nobody finishes still gives its slots back at
556
+ * its deadline. */
557
+ async #opened(call) {
558
+ const gate = this.#gate(call, [this.#streams, this.#slots]);
559
+ await gate.take();
560
+ let released = false;
561
+ const release = () => {
562
+ if (released)
563
+ return;
564
+ released = true;
565
+ call.signal?.removeEventListener("abort", release);
566
+ gate.lend();
567
+ };
568
+ try {
569
+ const response = await this.#send(call, async (response) => response, gate);
570
+ if (call.signal?.aborted)
571
+ release();
572
+ else
573
+ call.signal?.addEventListener("abort", release, { once: true });
574
+ return { response, release };
575
+ }
576
+ catch (error) {
577
+ release();
578
+ throw error;
579
+ }
446
580
  }
447
581
  /** Newline-delimited JSON events, as they arrive. */
448
582
  async *events(call) {
449
- const response = await this.send({ ...call, accept: "application/x-ndjson" });
583
+ const { response, release } = await this.#opened(this.#prepare({ ...call, accept: "application/x-ndjson" }));
450
584
  if (!response.body)
451
- return;
585
+ return release();
452
586
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
453
587
  let buffer = "";
454
588
  try {
@@ -477,6 +611,7 @@ export class Transport {
477
611
  // Cleanup must not replace a parse, network or callback error.
478
612
  await reader.cancel().catch(() => undefined);
479
613
  reader.releaseLock();
614
+ release();
480
615
  }
481
616
  }
482
617
  /** A WebSocket to an API path, authenticated with the same key. */
package/dist/types.d.ts CHANGED
@@ -5,6 +5,8 @@ export type SandboxInfo = {
5
5
  kind: "sandbox";
6
6
  name: string | null;
7
7
  labels: Record<string, string>;
8
+ /** @deprecated Read `state`. An older, coarser name for it, still sent for
9
+ * older clients. */
8
10
  status: "pending" | "active" | "paused" | "stopping" | "stopped";
9
11
  state: "starting" | "running" | "pausing" | "paused" | "resuming" | "stopping" | "stopped";
10
12
  region: string;
@@ -14,7 +16,10 @@ export type SandboxInfo = {
14
16
  diskMiB: number;
15
17
  cpu: "shared" | "reserved";
16
18
  cpuFloorMillis: number;
19
+ /** @deprecated Always true for a sandbox created since 5 October 2026:
20
+ * every sandbox can pause. */
17
21
  pausable: boolean;
22
+ /** Its time limit in seconds; 0 is none: it runs while it works. */
18
23
  timeoutSeconds: number;
19
24
  /** Pauses after this many idle seconds; 0 is never. */
20
25
  idlePauseSeconds?: number;
@@ -23,27 +28,40 @@ export type SandboxInfo = {
23
28
  idlePauseUnusedOnly?: boolean;
24
29
  /** A request (exec, files, terminal, a visit to a shared port) wakes it when paused. */
25
30
  autoWake?: boolean;
26
- /** Its lease renews itself while credit lasts, and its disk is kept after a stop. */
31
+ /** Runs until stopped while credit lasts, never paused for idleness or a
32
+ * time limit, and its disk is kept after a stop. */
27
33
  persistent?: boolean;
28
34
  /** The last exec, file, terminal, desktop or preview request, to within a minute. */
29
35
  lastActiveAt?: string | null;
30
36
  onLeaseEnd: "pause" | "stop";
31
37
  createdAt: string;
32
38
  readyAt: string | null;
39
+ /** Paid up to: a time ahead of now that moves on by itself while it runs.
40
+ * Not when it ends; that is `endsAt`. */
33
41
  expiresAt: string;
42
+ /** When it stops or pauses by itself: its time limit, or where its funding
43
+ * ends once credit or a spending limit stops its renewal. Null when it
44
+ * never will, and when it is not running. Absent from older servers. */
45
+ endsAt?: string | null;
34
46
  endedAt: string | null;
47
+ /** Why it stopped or paused, such as `requested`, `idle` or `time_limit`
48
+ * (its `timeoutSeconds` ran out). A server before 4 October 2026 sent
49
+ * `lease_expired` for `time_limit`. */
35
50
  stopReason: string | null;
36
51
  pausedAt: string | null;
37
52
  pausedExpiresAt: string | null;
38
53
  chargedMicros: number;
54
+ /** @deprecated Read `chargedMicros` and `runtime.usage()`. Still sent for
55
+ * older clients. */
39
56
  heldMicros: number;
40
57
  simulated?: boolean;
41
58
  replayed?: boolean;
42
59
  /** The names of its own environment variables (`env`), on a create, an
43
60
  * update and a read of one sandbox. Values are never shown. */
44
61
  envNames?: string[];
45
- /** On a create from an image with a start command: whether it started,
46
- * became ready (readyMs), timed out, or exited first. The create's answer
62
+ /** On a create from an image with a start command: whether it started
63
+ * (and was still running half a second later), became ready (readyMs),
64
+ * timed out, or exited first (exitCode, and stderr's last lines). The create's answer
47
65
  * alone carries it, and the Sandbox that create returned keeps it through
48
66
  * later reads; `sandboxes.get` and `list` have none, because the API keeps
49
67
  * no record of it. */
@@ -52,6 +70,8 @@ export type SandboxInfo = {
52
70
  processId?: string;
53
71
  exitCode?: number | null;
54
72
  readyMs?: number;
73
+ /** With exited: the last lines the start command wrote to stderr. */
74
+ stderr?: string;
55
75
  };
56
76
  /** Present and true when getOrCreate answered a sandbox that already held the name. */
57
77
  reused?: boolean;
@@ -73,25 +93,31 @@ export type CreateSandbox = {
73
93
  diskMiB?: number;
74
94
  cpu?: "shared" | "reserved";
75
95
  cpuFloorMillis?: number;
76
- /** How long it may run before its lease ends. Default 1800. */
96
+ /** A time limit, 60 to 3600 seconds. Leave it out (or 0) for none: the
97
+ * sandbox runs while it works and pauses when idle, until you stop it or
98
+ * credit runs out. */
77
99
  timeoutSeconds?: number;
100
+ /** @deprecated Ignored since 5 October 2026: every sandbox can pause. */
78
101
  pausable?: boolean;
79
- /** What happens when timeoutSeconds runs out: "pause" (default) or "stop". */
102
+ /** What happens when its time limit or credit runs out: "pause" (default) or "stop". */
80
103
  onLeaseEnd?: "pause" | "stop";
81
104
  /** Pause after this many seconds in which nothing happens in it: no
82
105
  * request, no command or terminal running, no open connection, no network
83
- * traffic and no CPU use. Default 60 for a pausable sandbox; 0 never;
106
+ * traffic and no CPU use. Default 60 unless persistent; 0 never;
84
107
  * otherwise 10 to 86400. A request wakes it again. */
85
108
  idlePauseSeconds?: number;
86
109
  /** A request to a paused sandbox wakes it. Default true. */
87
110
  autoWake?: boolean;
88
- /** Keep it running while credit lasts (its lease renews itself) and keep its
89
- * disk after a stop, for restart(). Paid only. */
111
+ /** Keep it running until you stop it, while credit lasts, with no idle
112
+ * pause unless idlePauseSeconds asks for one. Its disk is billed as any
113
+ * sandbox's, and kept after a stop, for restart(), as any sandbox's is.
114
+ * Paid only. */
90
115
  persistent?: boolean;
91
116
  /** The most it may cost over its whole life, in microdollars. */
92
117
  maxTotalCostMicros?: number;
93
118
  /** With name: return the sandbox that already has the name, woken if paused. */
94
119
  getOrCreate?: boolean;
120
+ /** @deprecated Use `maxTotalCostMicros`. Still accepted for older clients. */
95
121
  maxCostMicros?: number;
96
122
  /** Network rules from the first start; the same shape as sandbox.network.set.
97
123
  * Omit for the public web on ports 80 and 443. */
@@ -194,6 +220,7 @@ export type OutputEvent = {
194
220
  error: {
195
221
  code: string;
196
222
  message: string;
223
+ status?: number;
197
224
  requestId?: string;
198
225
  };
199
226
  };
@@ -224,6 +251,8 @@ export type Usage = {
224
251
  unit: "microdollars";
225
252
  credited: string;
226
253
  spent: string;
254
+ /** What running sandboxes and the hour ahead of stored items are about to
255
+ * use, already taken out of `available`. */
227
256
  held: string;
228
257
  /** Credit that expired, or grant credit taken back. */
229
258
  expired: string;
@@ -234,6 +263,7 @@ export type Usage = {
234
263
  trial: {
235
264
  totalMs: number;
236
265
  usedMs: number;
266
+ /** @deprecated Read `availableMs`. Still sent for older clients. */
237
267
  reservedMs: number;
238
268
  availableMs: number;
239
269
  } | null;
@@ -250,6 +280,23 @@ export type Usage = {
250
280
  /** What the balance or a spending limit could not cover; never charged. */
251
281
  writtenOffMicros: string;
252
282
  } | null;
283
+ /** The account's pilot while it serves its sandboxes, a free run agreed
284
+ * with Runtime: `hours` sandbox-hours of running time, `usedMs` and
285
+ * `leftMs` of it used and left. Null on every other account. */
286
+ pilot?: {
287
+ id: string;
288
+ sandboxes: number;
289
+ vcpu: number;
290
+ memoryMiB: number;
291
+ diskMiB: number;
292
+ startsAt: string;
293
+ endsAt: string;
294
+ graceEndsAt: string;
295
+ endedAt: string | null;
296
+ hours: number;
297
+ usedMs: number;
298
+ leftMs: number;
299
+ } | null;
253
300
  resources: Array<Record<string, unknown>>;
254
301
  [key: string]: unknown;
255
302
  };
@@ -266,7 +313,8 @@ export type SandboxSettings = {
266
313
  /** Pause after this many seconds with no activity, counted from now; 0
267
314
  * never, otherwise 10 to 86400. */
268
315
  idlePauseSeconds?: number;
269
- /** Keep it running while credit lasts and keep its disk after a stop. Paid only. */
316
+ /** Keep it running until you stop it, while credit lasts, and keep its disk
317
+ * after a stop. Paid only. */
270
318
  persistent?: boolean;
271
319
  /** Lifetime cap in microdollars; null removes it. */
272
320
  maxTotalCostMicros?: number | null;
@@ -285,11 +333,11 @@ export type DeletedSandbox = {
285
333
  endedAt: string | null;
286
334
  replayed?: boolean;
287
335
  };
288
- /** How `sandbox.keepAlive()` extends the lease. */
336
+ /** How `sandbox.keepAlive()` moves on the time limit of a sandbox that has one. */
289
337
  export type KeepAliveOptions = {
290
338
  /** How often it checks, in seconds. Default 60. */
291
339
  everySeconds?: number;
292
- /** How much lease it keeps ahead of now, in seconds (60 to 3600). Default 600. */
340
+ /** How much of its time limit it keeps ahead of now, in seconds (60 to 3600). Default 600. */
293
341
  marginSeconds?: number;
294
342
  /** Called with an error an extension met; the loop carries on. */
295
343
  onError?: (error: unknown) => void;
@@ -119,7 +119,9 @@ export class FileSystem {
119
119
  const result = await this.#call(syscall, path, () => this.#ctx.run(argv, options));
120
120
  if (result.exitCode === 0)
121
121
  return result.stdout;
122
- if (/No such file or directory/.test(result.stderr))
122
+ // dash, the sh of Debian and Ubuntu (a Runtime sandbox's), says "cannot
123
+ // create ...: Directory nonexistent" for a missing parent.
124
+ if (/No such file or directory|Directory nonexistent/.test(result.stderr))
123
125
  throw fsError("ENOENT", syscall, path);
124
126
  if (syscall === "unlink" || syscall === "rm")
125
127
  throw fsError("EACCES", syscall, path);
@@ -1,6 +1,7 @@
1
1
  export { Sandbox, DEFAULT_TIMEOUT_MS, DEFAULT_VCPUS, MEMORY_MIB_PER_VCPU, networkRules, toRuntimePath, type CreateSandboxParams, type RunCommandParams, type NetworkPolicy, type SandboxRoute, } from "./sandbox.js";
2
2
  export { Command, CommandFinished, type CommandOutput, type LogOutputLine, type Signal, } from "./command.js";
3
3
  export { FileSystem, Stats, Dirent } from "./filesystem.js";
4
+ export { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.js";
4
5
  export { Snapshot } from "./snapshot.js";
5
6
  export { APIError, StreamError, AuthenticationError, NotSupportedError } from "./errors.js";
6
7
  export type { Credentials, WithRuntime } from "./client.js";
@@ -16,13 +17,6 @@ export declare const Session: {
16
17
  new (...args: unknown[]): never;
17
18
  readonly [key: string]: (...args: unknown[]) => never;
18
19
  };
19
- export declare const SandboxUser: {
20
- (...args: unknown[]): never;
21
- new (...args: unknown[]): never;
22
- readonly [key: string]: (...args: unknown[]) => never;
23
- };
24
- export declare class SandboxUserAlreadyExistsError extends Error {
25
- }
26
20
  export declare const defineSandboxProxy: {
27
21
  (...args: unknown[]): never;
28
22
  new (...args: unknown[]): never;
@@ -9,6 +9,7 @@ import { NotSupportedError } from "./errors.js";
9
9
  export { Sandbox, DEFAULT_TIMEOUT_MS, DEFAULT_VCPUS, MEMORY_MIB_PER_VCPU, networkRules, toRuntimePath, } from "./sandbox.js";
10
10
  export { Command, CommandFinished, } from "./command.js";
11
11
  export { FileSystem, Stats, Dirent } from "./filesystem.js";
12
+ export { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.js";
12
13
  export { Snapshot } from "./snapshot.js";
13
14
  export { APIError, StreamError, AuthenticationError, NotSupportedError } from "./errors.js";
14
15
  /** Vercel's default region; Runtime runs in one US region. */
@@ -32,7 +33,4 @@ function unsupportedExport(name, alternative) {
32
33
  }
33
34
  export const Drive = unsupportedExport("Drive", "Use a Runtime volume (runtime.volumes) and mount it with withruntime: { create: { volumes: [{ volumeId, path }] } }.");
34
35
  export const Session = unsupportedExport("Session", "A Runtime sandbox is its own session: call the methods on the sandbox.");
35
- export const SandboxUser = unsupportedExport("SandboxUser", "Commands run as the sandbox owner with passwordless sudo; create users with `sudo useradd` in runCommand.");
36
- export class SandboxUserAlreadyExistsError extends Error {
37
- }
38
36
  export const defineSandboxProxy = unsupportedExport("sandbox proxy (defineSandboxProxy)", "Share a port with sandbox.withruntime.previews.create(port) instead.");