withruntime 0.6.1 → 0.7.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 (46) hide show
  1. package/E2B.md +32 -32
  2. package/README.md +3 -3
  3. package/dist/ai/index.d.ts +21 -5
  4. package/dist/ai/index.js +14 -21
  5. package/dist/ai-harness/index.js +15 -2
  6. package/dist/billing-cli.d.ts +21 -0
  7. package/dist/billing-cli.js +158 -0
  8. package/dist/cli-extras.d.ts +15 -0
  9. package/dist/cli-extras.js +60 -4
  10. package/dist/cli-name.d.ts +6 -0
  11. package/dist/cli-name.js +8 -0
  12. package/dist/cli.d.ts +17 -1
  13. package/dist/cli.js +535 -130
  14. package/dist/client.d.ts +35 -6
  15. package/dist/client.js +23 -0
  16. package/dist/credentials.js +1 -2
  17. package/dist/daytona/filesystem.d.ts +2 -0
  18. package/dist/daytona/filesystem.js +9 -7
  19. package/dist/e2b/filesystem.js +3 -2
  20. package/dist/e2b/sandbox.d.ts +9 -6
  21. package/dist/e2b/sandbox.js +31 -7
  22. package/dist/errors.d.ts +5 -0
  23. package/dist/errors.js +7 -0
  24. package/dist/index.d.ts +8 -6
  25. package/dist/index.js +1 -0
  26. package/dist/keys.js +1 -1
  27. package/dist/login.d.ts +2 -6
  28. package/dist/login.js +3 -8
  29. package/dist/network-cli.d.ts +1 -1
  30. package/dist/network-cli.js +64 -2
  31. package/dist/products/billing.d.ts +35 -0
  32. package/dist/products/billing.js +13 -0
  33. package/dist/products/index.d.ts +4 -1
  34. package/dist/products/index.js +4 -1
  35. package/dist/products/network-products.d.ts +45 -0
  36. package/dist/products/network-products.js +27 -0
  37. package/dist/products/previews.d.ts +1 -2
  38. package/dist/products/secrets.d.ts +14 -0
  39. package/dist/sandbox.d.ts +17 -3
  40. package/dist/sandbox.js +233 -62
  41. package/dist/transport.d.ts +17 -1
  42. package/dist/transport.js +124 -7
  43. package/dist/types.d.ts +0 -2
  44. package/dist/wallet.d.ts +30 -0
  45. package/dist/wallet.js +48 -0
  46. package/package.json +2 -2
package/dist/transport.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
- export const VERSION = "0.6.1";
3
+ export const VERSION = "0.7.0";
4
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
5
5
  export function apiOrigin(value) {
6
6
  const url = new URL(value);
@@ -198,8 +198,22 @@ export class Transport {
198
198
  await sleep(backoff(attempt), signal);
199
199
  continue;
200
200
  }
201
- if (response.ok)
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 };
213
+ }
214
+ else if (response.ok)
202
215
  return response;
216
+ const status = late?.status ?? response.status;
203
217
  // Runtime never answers 407: it is a proxy refusing the tunnel, which
204
218
  // Bun's fetch hands back as a response rather than an error.
205
219
  if (response.status === 407 && this.#routed) {
@@ -213,15 +227,15 @@ export class Transport {
213
227
  ...(key ? { idempotencyKey: key } : {}),
214
228
  });
215
229
  }
216
- const text = await response.text();
230
+ const text = late?.text ?? (await response.text());
217
231
  let parsed = null;
218
232
  try {
219
233
  parsed = JSON.parse(text);
220
234
  }
221
235
  catch {
222
- parsed = { error: { message: text.slice(0, 500) || `HTTP ${response.status}` } };
236
+ parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
223
237
  }
224
- const error = errorFor(response.status, parsed, key);
238
+ const error = errorFor(status, parsed, key);
225
239
  // A full house: wait for a slot or for room, then send the same call
226
240
  // again. A request that can never fit (a fork of more copies than the
227
241
  // trial runs) is not waited for.
@@ -232,10 +246,12 @@ export class Transport {
232
246
  const left = roomUntil - performance.now();
233
247
  if (left <= 0)
234
248
  throw error;
235
- await sleep(Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++)), signal);
249
+ const pause = Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++));
250
+ call.onCapacityWait?.(error, pause);
251
+ await sleep(pause, signal);
236
252
  continue;
237
253
  }
238
- const retryable = [429, 502, 503, 504].includes(response.status) && !DELIBERATE.has(error.code);
254
+ const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
239
255
  if (!retry || !retryable || attempt >= this.#maxRetries)
240
256
  throw error;
241
257
  const header = Number(response.headers.get("retry-after"));
@@ -263,6 +279,54 @@ export class Transport {
263
279
  this.#slots.give();
264
280
  }
265
281
  }
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. */
288
+ async fileBytes(call) {
289
+ for (let attempt = 0;; attempt++) {
290
+ await this.#slots.take();
291
+ let failure;
292
+ try {
293
+ const response = await this.send(call);
294
+ 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);
306
+ }
307
+ }
308
+ /** A file's bytes as they arrive, for files too big to hold. The stream
309
+ * errors with `download_incomplete` if it ends short of the length the API
310
+ * promised; it is never retried part way. */
311
+ async fileStream(call) {
312
+ const response = await this.send(call);
313
+ const expected = promisedLength(response.headers);
314
+ const body = response.body ?? new ReadableStream({ start: (c) => c.close() });
315
+ let got = 0;
316
+ return body.pipeThrough(new TransformStream({
317
+ transform(chunk, controller) {
318
+ got += chunk.byteLength;
319
+ if (expected !== undefined && got > expected)
320
+ controller.error(incompleteDownload(got, expected));
321
+ else
322
+ controller.enqueue(chunk);
323
+ },
324
+ flush(controller) {
325
+ if (expected !== undefined && got !== expected)
326
+ controller.error(incompleteDownload(got, expected));
327
+ },
328
+ }));
329
+ }
266
330
  /** Newline-delimited JSON events, as they arrive. */
267
331
  async *events(call) {
268
332
  const response = await this.send({ ...call, accept: "application/x-ndjson" });
@@ -305,6 +369,59 @@ export class Transport {
305
369
  });
306
370
  }
307
371
  }
372
+ /** The status a late answer's body failed with, or undefined when it is an
373
+ * answer. The body is the error the API would have sent as its status. */
374
+ function lateFailure(text) {
375
+ let body;
376
+ try {
377
+ body = JSON.parse(text);
378
+ }
379
+ catch {
380
+ return 502;
381
+ }
382
+ // An API error is a body of `error` alone, with a code; an answer may carry
383
+ // an `error` of its own (a code cell's exception), next to other fields.
384
+ if (!body || typeof body !== "object" || Object.keys(body).length !== 1)
385
+ return undefined;
386
+ const error = body.error;
387
+ if (!error || typeof error !== "object" || typeof error.code !== "string")
388
+ return undefined;
389
+ const status = error.status;
390
+ return typeof status === "number" && status >= 400 ? status : 500;
391
+ }
392
+ function promisedLength(headers) {
393
+ const value = headers.get("x-content-length");
394
+ return value !== null && /^\d+$/.test(value) ? Number(value) : undefined;
395
+ }
396
+ /** A download that ended short of (or past) the length the API promised. */
397
+ export function incompleteDownload(got, expected) {
398
+ return new RuntimeError({
399
+ message: `The download ended at ${got} of ${expected} bytes.`,
400
+ code: "download_incomplete",
401
+ status: 0,
402
+ hint: "Read the file again; nothing was written for this read. If it keeps happening, report it with `npx withruntime feedback`.",
403
+ details: { received: got, expected },
404
+ });
405
+ }
406
+ /** Why a whole body cannot be trusted, or undefined when it can. */
407
+ async function checkBody(headers, body) {
408
+ const expected = promisedLength(headers);
409
+ if (expected !== undefined && body.byteLength !== expected)
410
+ return incompleteDownload(body.byteLength, expected);
411
+ const sha256 = headers.get("x-content-sha256");
412
+ if (!sha256)
413
+ return undefined;
414
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", body));
415
+ const hex = Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
416
+ if (hex === sha256.toLowerCase())
417
+ return undefined;
418
+ return new RuntimeError({
419
+ message: "The file's bytes did not match the SHA-256 the API sent with them.",
420
+ code: "download_incomplete",
421
+ status: 0,
422
+ hint: "Read the file again. If it keeps happening, report it with `npx withruntime feedback`.",
423
+ });
424
+ }
308
425
  /** No key given, none in RUNTIME_API_KEY, and none saved by `runtime login`. */
309
426
  export function missingKey() {
310
427
  return new RuntimeError({
package/dist/types.d.ts CHANGED
@@ -100,8 +100,6 @@ export type CreateSandbox = {
100
100
  path: string;
101
101
  mode?: "rw" | "snapshot";
102
102
  }[];
103
- /** Anything a newer server accepts (image, snapshotId, volumes, network...). */
104
- [field: string]: unknown;
105
103
  };
106
104
  export type CommandResult = {
107
105
  exitCode: number | null;
@@ -0,0 +1,30 @@
1
+ import type { Topup } from "./products/billing.js";
2
+ export type WalletOptions = {
3
+ /** Default: RUNTIME_AUTH_URL, then https://withruntime.com. */
4
+ authUrl?: string;
5
+ fetch?: typeof fetch;
6
+ };
7
+ export type OpenedWalletAccount = Topup & {
8
+ orgId: string;
9
+ /** The only way to this account's key, shown once. Keep it secret. */
10
+ claimCode: string;
11
+ };
12
+ export type WalletClaim = {
13
+ status: "paid";
14
+ orgId: string;
15
+ apiKey: string;
16
+ } | {
17
+ status: Exclude<Topup["status"], "paid">;
18
+ orgId: string;
19
+ topup: Topup | null;
20
+ };
21
+ /** Opens an account and a stablecoin top-up of `usd` dollars ($10 to $10,000).
22
+ * `acceptTerms` must be true: opening one means agreeing to
23
+ * https://withruntime.com/legal/terms, so ask the person you work for first. */
24
+ export declare function openWalletAccount(input: {
25
+ usd: number;
26
+ acceptTerms: boolean;
27
+ }, options?: WalletOptions): Promise<OpenedWalletAccount>;
28
+ /** Exchanges a paid account's claim code for its API key, once. Until the
29
+ * payment arrives it answers the top-up's status instead; call it again. */
30
+ export declare function claimWalletAccount(claimCode: string, options?: WalletOptions): Promise<WalletClaim>;
package/dist/wallet.js ADDED
@@ -0,0 +1,48 @@
1
+ import { RuntimeError } from "./errors.js";
2
+ import { apiOrigin } from "./transport.js";
3
+ function origin(options) {
4
+ const env = typeof process === "undefined" ? {} : process.env;
5
+ return apiOrigin(options.authUrl ?? env.RUNTIME_AUTH_URL ?? "https://withruntime.com");
6
+ }
7
+ async function post(options, path, body) {
8
+ const fetcher = options.fetch ?? fetch;
9
+ let response;
10
+ try {
11
+ response = await fetcher(`${origin(options)}${path}`, {
12
+ method: "POST",
13
+ redirect: "error",
14
+ headers: { "content-type": "application/json" },
15
+ body: JSON.stringify(body),
16
+ });
17
+ }
18
+ catch (cause) {
19
+ throw new RuntimeError({
20
+ message: `No answer from ${origin(options)}.`,
21
+ code: "network",
22
+ status: 0,
23
+ hint: "Retry in a minute.",
24
+ cause,
25
+ });
26
+ }
27
+ const answer = (await response.json().catch(() => ({})));
28
+ if (!response.ok)
29
+ throw new RuntimeError({
30
+ message: answer.error ?? `The request failed (HTTP ${response.status}).`,
31
+ code: answer.code ?? (response.status === 429 ? "rate_limited" : "request_failed"),
32
+ status: response.status,
33
+ });
34
+ return [response.status, answer];
35
+ }
36
+ /** Opens an account and a stablecoin top-up of `usd` dollars ($10 to $10,000).
37
+ * `acceptTerms` must be true: opening one means agreeing to
38
+ * https://withruntime.com/legal/terms, so ask the person you work for first. */
39
+ export async function openWalletAccount(input, options = {}) {
40
+ const [, opened] = await post(options, "/api/wallet/topups", input);
41
+ return opened;
42
+ }
43
+ /** Exchanges a paid account's claim code for its API key, once. Until the
44
+ * payment arrives it answers the top-up's status instead; call it again. */
45
+ export async function claimWalletAccount(claimCode, options = {}) {
46
+ const [, claim] = await post(options, "/api/wallet/claim", { claimCode });
47
+ return claim;
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "mcpName": "com.withruntime/runtime",
6
6
  "type": "module",
@@ -69,7 +69,7 @@
69
69
  "LICENSE"
70
70
  ],
71
71
  "engines": {
72
- "node": ">=22"
72
+ "node": ">=22.12"
73
73
  },
74
74
  "scripts": {
75
75
  "build": "tsc -p tsconfig.build.json",