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
package/dist/transport.js CHANGED
@@ -1,17 +1,52 @@
1
- import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
1
+ import { ConnectionError, DELIBERATE, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
- export const VERSION = "0.8.4";
3
+ export const VERSION = "0.10.0";
4
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
5
+ /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
6
+ * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
7
+ * on that host, whose addresses a sandbox cannot reach directly. Plain HTTP
8
+ * because the hop never leaves the machine: it goes from the program to the
9
+ * guest's own proxy and over the sandbox's private channel to its host. */
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";
13
+ /** True in a Runtime sandbox. Node and Bun only; a browser is never one. */
14
+ export function inRuntimeSandbox(marker = SANDBOX_MARKER) {
15
+ try {
16
+ const host = globalThis.process;
17
+ return host?.getBuiltinModule?.("node:fs")?.existsSync?.(marker) === true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /** Where calls go when no base URL is given: RUNTIME_API_URL, then Runtime's
24
+ * public API, which `reachable` turns into runtime.internal in a sandbox. */
25
+ export function defaultBaseUrl(env = processEnv(), inSandbox = inRuntimeSandbox) {
26
+ return reachable(env.RUNTIME_API_URL || DEFAULT_BASE_URL, inSandbox);
27
+ }
28
+ /** The origin calls for `origin` are sent to from here. In a sandbox the
29
+ * public API is its own host, which it cannot reach directly, so calls for it
30
+ * go to runtime.internal; every other origin is left as it is. */
31
+ export function reachable(origin, inSandbox = inRuntimeSandbox) {
32
+ return apiOrigin(origin) === DEFAULT_BASE_URL && inSandbox() ? SANDBOX_BASE_URL : origin;
33
+ }
34
+ function processEnv() {
35
+ return (globalThis.process?.env ?? {});
36
+ }
5
37
  export function apiOrigin(value) {
6
38
  const url = new URL(value);
7
- const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
39
+ // runtime.internal is reserved and never resolves outside a sandbox, so a
40
+ // key sent there in plain HTTP never leaves the sandbox's host.
41
+ const local = ["localhost", "127.0.0.1", "[::1]", "runtime.internal"].includes(url.hostname);
8
42
  if ((url.protocol !== "https:" && !(local && url.protocol === "http:")) ||
43
+ (url.hostname === "runtime.internal" && (url.protocol !== "http:" || url.port !== "")) ||
9
44
  url.username ||
10
45
  url.password ||
11
46
  url.search ||
12
47
  url.hash ||
13
48
  url.pathname !== "/")
14
- throw new Error("Use an HTTPS API origin (or http://localhost for tests).");
49
+ throw new Error("Use an HTTPS API origin (or http://runtime.internal inside a sandbox, http://localhost for tests).");
15
50
  return url.origin;
16
51
  }
17
52
  export function encodeQuery(query) {
@@ -30,10 +65,6 @@ export function encodeQuery(query) {
30
65
  const text = params.toString();
31
66
  return text ? `?${text}` : "";
32
67
  }
33
- /** 503s that are a deliberate state, not a passing one: a product switched
34
- * off here, or paused on purpose. Retrying cannot change them, so they fail
35
- * at once with their own words. host_unavailable and busy are still retried. */
36
- const DELIBERATE = new Set(["fork_unavailable", "previews_unavailable", "unavailable"]);
37
68
  const sleep = (ms, signal) => new Promise((resolve, reject) => {
38
69
  const timer = setTimeout(done, ms);
39
70
  function done() {
@@ -49,18 +80,57 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
49
80
  else
50
81
  signal?.addEventListener("abort", abort, { once: true });
51
82
  });
83
+ /** A credential lookup can finish independently, but must not keep its
84
+ * caller waiting after cancellation or hold that caller's queue slot. */
85
+ function untilAborted(pending, signal) {
86
+ if (!signal)
87
+ return pending;
88
+ return new Promise((resolve, reject) => {
89
+ const abort = () => reject(abortReason(signal));
90
+ if (signal.aborted)
91
+ abort();
92
+ else
93
+ signal.addEventListener("abort", abort, { once: true });
94
+ pending.then((value) => {
95
+ signal.removeEventListener("abort", abort);
96
+ resolve(value);
97
+ }, (cause) => {
98
+ signal.removeEventListener("abort", abort);
99
+ reject(cause instanceof Error ? cause : new Error("Credential lookup failed.", { cause }));
100
+ });
101
+ });
102
+ }
103
+ function abortReason(signal) {
104
+ return signal?.reason instanceof Error
105
+ ? signal.reason
106
+ : new Error("Aborted", { cause: signal?.reason });
107
+ }
52
108
  /** At most `maximum` holders at once; the rest wait their turn, first come
53
109
  * first served. */
54
110
  function slots(maximum) {
55
111
  let active = 0;
56
112
  const waiting = [];
57
113
  return {
58
- async take() {
114
+ async take(signal) {
115
+ signal?.throwIfAborted();
59
116
  if (active < maximum) {
60
117
  active++;
61
118
  return;
62
119
  }
63
- await new Promise((resolve) => waiting.push(resolve));
120
+ await new Promise((resolve, reject) => {
121
+ const ready = () => {
122
+ signal?.removeEventListener("abort", abort);
123
+ resolve();
124
+ };
125
+ const abort = () => {
126
+ const at = waiting.indexOf(ready);
127
+ if (at >= 0)
128
+ waiting.splice(at, 1);
129
+ reject(abortReason(signal));
130
+ };
131
+ waiting.push(ready);
132
+ signal?.addEventListener("abort", abort, { once: true });
133
+ });
64
134
  },
65
135
  give() {
66
136
  const next = waiting.shift();
@@ -99,7 +169,7 @@ export class Transport {
99
169
  throw missingKey();
100
170
  else
101
171
  this.#apiKey = options.apiKey;
102
- this.baseUrl = apiOrigin(options.baseUrl ?? DEFAULT_BASE_URL);
172
+ this.baseUrl = apiOrigin(options.baseUrl ? reachable(options.baseUrl) : defaultBaseUrl());
103
173
  // The environment's proxy (HTTPS_PROXY, NO_PROXY) on Node and Bun alike;
104
174
  // a fetch passed in is used as it is.
105
175
  this.#fetch = options.fetch ?? envFetch;
@@ -139,21 +209,71 @@ export class Transport {
139
209
  * the same key every time, and waits out a full house for a call that sets
140
210
  * waitForCapacityMs. */
141
211
  async send(call) {
142
- const apiKey = await this.#key();
143
- const write = call.method !== "GET";
144
- const key = write ? (call.idempotencyKey ?? crypto.randomUUID()) : undefined;
145
- // Time spent waiting for room is added to the call's deadline, not taken
146
- // from it: once admitted, a create still has its whole timeoutMs.
212
+ return this.#send(this.#prepare(call), async (response) => response);
213
+ }
214
+ /** Start the deadline before waiting for a connection; keep one key and
215
+ * signal through every retry and the complete response body. */
216
+ #prepare(call) {
217
+ const timeoutMs = call.timeoutMs ?? this.#timeoutMs;
218
+ const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
219
+ const deadline = timeoutMs === 0 ? undefined : AbortSignal.timeout(timeoutMs + roomMs);
220
+ const signal = deadline
221
+ ? call.signal
222
+ ? AbortSignal.any([call.signal, deadline])
223
+ : deadline
224
+ : call.signal;
225
+ return {
226
+ ...call,
227
+ timeoutMs: 0,
228
+ ...(signal ? { signal } : {}),
229
+ ...(call.method !== "GET"
230
+ ? { idempotencyKey: call.idempotencyKey ?? crypto.randomUUID() }
231
+ : {}),
232
+ };
233
+ }
234
+ #connectionError(call, cause) {
235
+ const cancelled = call.signal?.aborted === true;
236
+ const route = this.#routed && !cancelled
237
+ ? describeRoute(`${this.baseUrl}${call.path}`, cause)
238
+ : { via: "" };
239
+ return new ConnectionError({
240
+ message: cancelled
241
+ ? "The call was cancelled or ran past its deadline."
242
+ : call.method !== "GET"
243
+ ? `No answer from Runtime at ${this.baseUrl}${route.via}. The change may have happened; retrying with the same idempotencyKey is safe.`
244
+ : `No answer from Runtime at ${this.baseUrl}${route.via}.`,
245
+ hint: cancelled
246
+ ? "Raise timeoutMs for a long call, or retry with the same idempotencyKey."
247
+ : (route.hint ?? "Check the network, and RUNTIME_API_URL if you set it."),
248
+ code: cancelled ? "timeout" : "connection_error",
249
+ status: 0,
250
+ ...(call.idempotencyKey ? { idempotencyKey: call.idempotencyKey } : {}),
251
+ cause,
252
+ });
253
+ }
254
+ async #send(call, consume) {
255
+ let apiKey;
256
+ try {
257
+ call.signal?.throwIfAborted();
258
+ apiKey = await untilAborted(this.#key(), call.signal);
259
+ }
260
+ catch (cause) {
261
+ if (call.signal?.aborted)
262
+ throw this.#connectionError(call, cause);
263
+ throw cause;
264
+ }
265
+ const key = call.method !== "GET" ? call.idempotencyKey : undefined;
147
266
  const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
148
267
  const roomUntil = performance.now() + roomMs;
149
268
  let roomAttempt = 0;
150
- const deadline = AbortSignal.timeout((call.timeoutMs ?? this.#timeoutMs) + roomMs);
151
- const signal = call.signal ? AbortSignal.any([call.signal, deadline]) : deadline;
269
+ const signal = call.signal;
152
270
  const url = `${this.baseUrl}${call.path}${encodeQuery(call.query)}`;
153
271
  const body = call.bytes ?? (call.body === undefined ? undefined : JSON.stringify(call.body));
154
272
  const retry = call.retry ?? true;
155
- for (let attempt = 0;; attempt++) {
273
+ let attempt = 0;
274
+ for (;;) {
156
275
  let response;
276
+ let inCallback = false;
157
277
  // Called as a plain function: Cloudflare Workers' fetch throws "Illegal
158
278
  // invocation" when called as a method of another object (this.#fetch()).
159
279
  const send = this.#fetch;
@@ -161,6 +281,7 @@ export class Transport {
161
281
  // "manual", not "error": Workers accept only "follow" and "manual".
162
282
  // The API never redirects, so a redirect is refused here exactly as
163
283
  // "error" refused it: thrown, retried, then a connection_error.
284
+ signal?.throwIfAborted();
164
285
  response = await send(url, {
165
286
  method: call.method,
166
287
  headers: this.#headers(call, key, apiKey),
@@ -173,136 +294,132 @@ export class Transport {
173
294
  await response.body?.cancel().catch(() => { });
174
295
  throw new TypeError(`fetch failed: Runtime answered a redirect (${response.status}).`);
175
296
  }
176
- }
177
- catch (cause) {
178
- // A proxy variable the SDK cannot use: retrying cannot change it.
179
- if (cause instanceof RuntimeError)
180
- throw cause;
181
- if (signal.aborted || !retry || attempt >= this.#maxRetries) {
182
- const route = this.#routed && !signal.aborted ? describeRoute(url, cause) : { via: "" };
297
+ /* An answer that took longer than the API holds its headers (90 s)
298
+ comes as a 200 sent early, and then its JSON: the answer, or
299
+ {"error": ...} when the work failed after the status went
300
+ (runtime-late-answer, api.md). Either way it is read here, so a
301
+ failure is thrown as the error it is, never returned as a success. */
302
+ let late;
303
+ if (response.ok && response.headers.get("runtime-late-answer") === "true") {
304
+ const text = await response.text();
305
+ const failed = lateFailure(text);
306
+ if (failed === undefined)
307
+ return await consume(new Response(text, { headers: response.headers }));
308
+ late = { status: failed, text };
309
+ }
310
+ else if (response.ok) {
311
+ inCallback = true;
312
+ call.onResponse?.(response);
313
+ inCallback = false;
314
+ return await consume(response);
315
+ }
316
+ const status = late?.status ?? response.status;
317
+ // Runtime never answers 407: it is a proxy refusing the tunnel, which
318
+ // Bun's fetch hands back as a response rather than an error.
319
+ if (response.status === 407 && this.#routed) {
320
+ await response.body?.cancel();
321
+ const route = describeRoute(url, 407);
183
322
  throw new ConnectionError({
184
- message: signal.aborted
185
- ? "The call was cancelled or ran past its deadline."
186
- : write
187
- ? `No answer from Runtime at ${this.baseUrl}${route.via}. The change may have happened; retrying with the same idempotencyKey is safe.`
188
- : `No answer from Runtime at ${this.baseUrl}${route.via}.`,
189
- hint: signal.aborted
190
- ? "Raise timeoutMs for a long call, or retry with the same idempotencyKey."
191
- : (route.hint ?? "Check the network, and RUNTIME_API_URL if you set it."),
192
- code: signal.aborted ? "timeout" : "connection_error",
323
+ message: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
324
+ code: "connection_error",
193
325
  status: 0,
326
+ ...(route.hint ? { hint: route.hint } : {}),
194
327
  ...(key ? { idempotencyKey: key } : {}),
195
- cause,
196
328
  });
197
329
  }
198
- await sleep(backoff(attempt), signal);
199
- continue;
200
- }
201
- /* An answer that took longer than the API holds its headers (90 s)
202
- comes as a 200 sent early, and then its JSON: the answer, or
203
- {"error": ...} when the work failed after the status went
204
- (runtime-late-answer, api.md). Either way it is read here, so a
205
- failure is thrown as the error it is, never returned as a success. */
206
- let late;
207
- if (response.ok && response.headers.get("runtime-late-answer") === "true") {
208
- const text = await response.text();
209
- const failed = lateFailure(text);
210
- if (failed === undefined)
211
- return new Response(text, { headers: response.headers });
212
- late = { status: failed, text };
330
+ const text = late?.text ?? (await response.text());
331
+ let parsed = null;
332
+ try {
333
+ parsed = JSON.parse(text);
334
+ }
335
+ catch {
336
+ parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
337
+ }
338
+ const error = errorFor(status, parsed, key);
339
+ // A full house: wait for a slot or for room, then send the same call
340
+ // again. A request that can never fit (a fork of more copies than the
341
+ // trial runs) is not waited for.
342
+ if (retry &&
343
+ roomMs > 0 &&
344
+ WAITS_FOR_ROOM.has(error.code) &&
345
+ error.details?.field !== "count") {
346
+ const left = roomUntil - performance.now();
347
+ if (left <= 0)
348
+ throw error;
349
+ const pause = Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++));
350
+ inCallback = true;
351
+ call.onCapacityWait?.(error, pause);
352
+ inCallback = false;
353
+ await sleep(pause, signal);
354
+ continue;
355
+ }
356
+ const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
357
+ if (!retry || !retryable || attempt >= this.#maxRetries)
358
+ throw error;
359
+ const header = Number(response.headers.get("retry-after"));
360
+ const wait = error.retryAfterMs ??
361
+ (Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
362
+ await sleep(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal);
213
363
  }
214
- else if (response.ok)
215
- return response;
216
- const status = late?.status ?? response.status;
217
- // Runtime never answers 407: it is a proxy refusing the tunnel, which
218
- // Bun's fetch hands back as a response rather than an error.
219
- if (response.status === 407 && this.#routed) {
220
- await response.body?.cancel();
221
- const route = describeRoute(url, 407);
222
- throw new ConnectionError({
223
- message: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
224
- code: "connection_error",
225
- status: 0,
226
- ...(route.hint ? { hint: route.hint } : {}),
227
- ...(key ? { idempotencyKey: key } : {}),
364
+ catch (cause) {
365
+ // Deliberate API refusals and unusable proxy settings cannot be
366
+ // fixed by repeating the request. Body failures are transport
367
+ // failures too: a write replays under its original key.
368
+ if (inCallback) {
369
+ await response?.body?.cancel().catch(() => undefined);
370
+ throw cause;
371
+ }
372
+ if (cause instanceof RuntimeError)
373
+ throw cause;
374
+ if (signal?.aborted || !retry || attempt >= this.#maxRetries)
375
+ throw this.#connectionError(call, cause);
376
+ await sleep(backoff(attempt), signal).catch((cause) => {
377
+ throw this.#connectionError(call, cause);
228
378
  });
229
379
  }
230
- const text = late?.text ?? (await response.text());
231
- let parsed = null;
232
- try {
233
- parsed = JSON.parse(text);
234
- }
235
- catch {
236
- parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
237
- }
238
- const error = errorFor(status, parsed, key);
239
- // A full house: wait for a slot or for room, then send the same call
240
- // again. A request that can never fit (a fork of more copies than the
241
- // trial runs) is not waited for.
242
- if (retry &&
243
- roomMs > 0 &&
244
- WAITS_FOR_ROOM.has(error.code) &&
245
- error.details?.field !== "count") {
246
- const left = roomUntil - performance.now();
247
- if (left <= 0)
248
- throw error;
249
- const pause = Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++));
250
- call.onCapacityWait?.(error, pause);
251
- await sleep(pause, signal);
252
- continue;
253
- }
254
- const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
255
- if (!retry || !retryable || attempt >= this.#maxRetries)
256
- throw error;
257
- const header = Number(response.headers.get("retry-after"));
258
- const wait = error.retryAfterMs ??
259
- (Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
260
- await sleep(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal);
380
+ attempt++;
261
381
  }
262
382
  }
263
- async json(call) {
264
- await this.#slots.take();
383
+ async #queued(call, consume) {
265
384
  try {
266
- const text = await (await this.send(call)).text();
267
- return (text ? JSON.parse(text) : null);
385
+ await this.#slots.take(call.signal);
268
386
  }
269
- finally {
270
- this.#slots.give();
387
+ catch (cause) {
388
+ throw this.#connectionError(call, cause);
271
389
  }
272
- }
273
- async bytes(call) {
274
- await this.#slots.take();
275
390
  try {
276
- return new Uint8Array(await (await this.send(call)).arrayBuffer());
391
+ return await this.#send(call, consume);
277
392
  }
278
393
  finally {
279
394
  this.#slots.give();
280
395
  }
281
396
  }
282
- /** A file's bytes, read whole and checked: the API says a body's length
283
- * (x-content-length) and, when small, its SHA-256 (x-content-sha256) before
284
- * sending it, and a body that falls short or differs is read again, twice,
285
- * then refused. Nothing between the API and here reliably turns a stream
286
- * cut part way into an error: on 25 September 2026 a 50 MB file came back
287
- * 29 MB long, exit 0. */
397
+ async json(call) {
398
+ return this.#queued(this.#prepare(call), async (response) => {
399
+ const text = await response.text();
400
+ return (text ? JSON.parse(text) : null);
401
+ });
402
+ }
403
+ async bytes(call) {
404
+ return this.#queued(this.#prepare(call), async (response) => new Uint8Array(await response.arrayBuffer()));
405
+ }
406
+ /** A file's bytes, read whole and checked against its promised length and
407
+ * SHA-256. A short or different body is read again twice, within the same
408
+ * call deadline. */
288
409
  async fileBytes(call) {
410
+ call = this.#prepare(call);
289
411
  for (let attempt = 0;; attempt++) {
290
- await this.#slots.take();
291
- let failure;
292
- try {
293
- const response = await this.send(call);
412
+ const { body, problem } = await this.#queued(call, async (response) => {
294
413
  const body = new Uint8Array(await response.arrayBuffer());
295
- const problem = await checkBody(response.headers, body);
296
- if (!problem)
297
- return body;
298
- failure = problem;
299
- }
300
- finally {
301
- this.#slots.give();
302
- }
303
- if (attempt >= 2 || call.signal?.aborted)
304
- throw failure;
305
- await sleep(backoff(attempt), call.signal);
414
+ return { body, problem: await checkBody(response.headers, body) };
415
+ });
416
+ if (!problem)
417
+ return body;
418
+ if (attempt >= 2)
419
+ throw problem;
420
+ await sleep(backoff(attempt), call.signal).catch((cause) => {
421
+ throw this.#connectionError(call, cause);
422
+ });
306
423
  }
307
424
  }
308
425
  /** A file's bytes as they arrive, for files too big to hold. The stream
@@ -336,12 +453,14 @@ export class Transport {
336
453
  let buffer = "";
337
454
  try {
338
455
  for (;;) {
456
+ call.signal?.throwIfAborted();
339
457
  const { value, done } = await reader.read();
340
458
  if (done)
341
459
  break;
342
460
  buffer += value;
343
461
  let newline = buffer.indexOf("\n");
344
462
  while (newline >= 0) {
463
+ call.signal?.throwIfAborted();
345
464
  const line = buffer.slice(0, newline).trim();
346
465
  buffer = buffer.slice(newline + 1);
347
466
  if (line)
@@ -349,6 +468,7 @@ export class Transport {
349
468
  newline = buffer.indexOf("\n");
350
469
  }
351
470
  }
471
+ call.signal?.throwIfAborted();
352
472
  if (buffer.trim())
353
473
  yield JSON.parse(buffer);
354
474
  }
package/dist/types.d.ts CHANGED
@@ -39,8 +39,14 @@ export type SandboxInfo = {
39
39
  heldMicros: number;
40
40
  simulated?: boolean;
41
41
  replayed?: boolean;
42
+ /** The names of its own environment variables (`env`), on a create, an
43
+ * update and a read of one sandbox. Values are never shown. */
44
+ envNames?: string[];
42
45
  /** On a create from an image with a start command: whether it started,
43
- * became ready (readyMs), timed out, or exited first. */
46
+ * became ready (readyMs), timed out, or exited first. The create's answer
47
+ * alone carries it, and the Sandbox that create returned keeps it through
48
+ * later reads; `sandboxes.get` and `list` have none, because the API keeps
49
+ * no record of it. */
44
50
  start?: {
45
51
  state: "started" | "ready" | "timeout" | "exited" | "not_running";
46
52
  processId?: string;
@@ -54,6 +60,11 @@ export type SandboxInfo = {
54
60
  export type CreateSandbox = {
55
61
  name?: string;
56
62
  labels?: Record<string, string>;
63
+ /** Environment variables for every command, process, terminal, SSH session
64
+ * and the image's start command in this sandbox, under each command's own
65
+ * `env`. At most 32 variables and 32 KiB. Values are never shown again;
66
+ * answers list only `envNames`. Copies made by fork() keep them. */
67
+ env?: Record<string, string>;
57
68
  /** Omit to use the free trial while it lasts, then prepaid credit. */
58
69
  funding?: "trial" | "paid";
59
70
  region?: string;
@@ -103,6 +114,13 @@ export type CreateSandbox = {
103
114
  path: string;
104
115
  mode?: "rw" | "snapshot";
105
116
  }[];
117
+ /** Paid only: join your Tailscale network once running. `authKeySecret`
118
+ * names a job secret holding the auth key. */
119
+ tailscale?: {
120
+ authKeySecret: string;
121
+ hostname?: string;
122
+ tags?: string[];
123
+ };
106
124
  };
107
125
  export type CommandResult = {
108
126
  exitCode: number | null;
@@ -124,8 +142,8 @@ export type ExecOptions = {
124
142
  /** Default 60 000, or 24 hours when the output streams (onStdout, onStderr or
125
143
  * execStream); up to 24 hours. A timeout is a result (timedOut), not an error. */
126
144
  timeoutMs?: number;
127
- onStdout?: (text: string) => void;
128
- onStderr?: (text: string) => void;
145
+ onStdout?: (text: string) => unknown;
146
+ onStderr?: (text: string) => unknown;
129
147
  /** Throw CommandError when the exit code is not 0. */
130
148
  check?: boolean;
131
149
  signal?: AbortSignal;
@@ -141,6 +159,7 @@ export type ProcessInfo = {
141
159
  pty: boolean;
142
160
  stdinOpen: boolean;
143
161
  stdinOffset: number;
162
+ outputEncoding?: "utf8" | "base64";
144
163
  startedAt: string;
145
164
  endedAt: string | null;
146
165
  timeoutMs: number | null;
@@ -155,6 +174,7 @@ export type OutputEvent = {
155
174
  type: "stdout" | "stderr";
156
175
  data: string;
157
176
  offset: number;
177
+ base64?: string;
158
178
  } | {
159
179
  type: "exit";
160
180
  exitCode: number | null;
@@ -177,6 +197,13 @@ export type OutputEvent = {
177
197
  requestId?: string;
178
198
  };
179
199
  };
200
+ export type BinaryOutputEvent = Exclude<OutputEvent, {
201
+ type: "stdout" | "stderr";
202
+ }> | {
203
+ type: "stdout" | "stderr";
204
+ data: Uint8Array;
205
+ offset: number;
206
+ };
180
207
  export type FileEntry = {
181
208
  name: string;
182
209
  path: string;
@@ -184,6 +211,11 @@ export type FileEntry = {
184
211
  size: number;
185
212
  mode: string;
186
213
  modifiedAt: string;
214
+ uid?: number;
215
+ gid?: number;
216
+ owner?: string;
217
+ group?: string;
218
+ symlinkTarget?: string;
187
219
  };
188
220
  /** GET /v1/usage. Money is integer microdollars in strings (1,000,000 = $1),
189
221
  * exact past 2^53: available = credited - spent - expired - held. */
@@ -226,6 +258,9 @@ export type FeedbackKind = "bug" | "missing_feature" | "competitor_gap" | "migra
226
258
  export type SandboxSettings = {
227
259
  name?: string;
228
260
  labels?: Record<string, string>;
261
+ /** Changes its environment: a value sets a variable, null removes it, and
262
+ * the rest stay. Commands started afterwards get the change. */
263
+ env?: Record<string, string | null>;
229
264
  /** A request to a paused sandbox wakes it. */
230
265
  autoWake?: boolean;
231
266
  /** Pause after this many seconds with no activity, counted from now; 0
@@ -236,6 +271,20 @@ export type SandboxSettings = {
236
271
  /** Lifetime cap in microdollars; null removes it. */
237
272
  maxTotalCostMicros?: number | null;
238
273
  };
274
+ /** A deleted sandbox, as `delete()` answers it the first time and every time
275
+ * after. It reads nowhere else afterwards. */
276
+ export type DeletedSandbox = {
277
+ id: string;
278
+ kind: "sandbox";
279
+ name: string | null;
280
+ labels: Record<string, string>;
281
+ status: "deleted";
282
+ /** "stopping" until its machine has stopped, usually well under a second. */
283
+ state: string;
284
+ deletedAt: string;
285
+ endedAt: string | null;
286
+ replayed?: boolean;
287
+ };
239
288
  /** How `sandbox.keepAlive()` extends the lease. */
240
289
  export type KeepAliveOptions = {
241
290
  /** How often it checks, in seconds. Default 60. */
@@ -57,7 +57,7 @@ export declare class Command {
57
57
  signal?: AbortSignal;
58
58
  }): Promise<string>;
59
59
  /** Sends a signal, SIGTERM by default. */
60
- kill(signal?: Signal, _opts?: {
60
+ kill(signal?: Signal, opts?: {
61
61
  abortSignal?: AbortSignal;
62
62
  }): Promise<void>;
63
63
  }