withruntime 0.11.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 (60) hide show
  1. package/BLAXEL.md +7 -8
  2. package/DAYTONA.md +7 -8
  3. package/E2B.md +16 -2
  4. package/README.md +3 -3
  5. package/README.src.md +3 -3
  6. package/VERCEL.md +3 -2
  7. package/compatibility-lock.json +55 -0
  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 +39 -15
  15. package/dist/client.d.ts +5 -1
  16. package/dist/client.js +19 -3
  17. package/dist/compat/files.js +5 -1
  18. package/dist/compat/lease.d.ts +1 -0
  19. package/dist/compat/lease.js +14 -8
  20. package/dist/credentials.d.ts +9 -0
  21. package/dist/credentials.js +26 -4
  22. package/dist/daytona/daytona.js +29 -12
  23. package/dist/daytona/sandbox.d.ts +8 -3
  24. package/dist/daytona/sandbox.js +23 -4
  25. package/dist/e2b/sandbox.d.ts +7 -3
  26. package/dist/e2b/sandbox.js +21 -7
  27. package/dist/errors.d.ts +6 -0
  28. package/dist/errors.js +6 -0
  29. package/dist/identity.js +2 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +2 -1
  32. package/dist/modal/index.js +6 -2
  33. package/dist/network-cli.js +7 -7
  34. package/dist/products/browser.d.ts +2 -1
  35. package/dist/products/browser.js +9 -19
  36. package/dist/products/desktop.d.ts +2 -1
  37. package/dist/products/desktop.js +5 -30
  38. package/dist/products/jobs.d.ts +7 -5
  39. package/dist/products/jobs.js +7 -3
  40. package/dist/products/mcp.d.ts +5 -1
  41. package/dist/products/mcp.js +29 -8
  42. package/dist/products/network-products.d.ts +1 -1
  43. package/dist/products/network-products.js +1 -1
  44. package/dist/products/observability.d.ts +1 -1
  45. package/dist/products/private-network.d.ts +1 -1
  46. package/dist/products/secrets.d.ts +2 -2
  47. package/dist/runloop/index.js +6 -2
  48. package/dist/sandbox.d.ts +27 -19
  49. package/dist/sandbox.js +121 -54
  50. package/dist/tar.d.ts +2 -0
  51. package/dist/tar.js +74 -16
  52. package/dist/transport.d.ts +33 -7
  53. package/dist/transport.js +171 -36
  54. package/dist/types.d.ts +54 -9
  55. package/dist/vercel/filesystem.js +3 -1
  56. package/dist/vercel/sandbox.d.ts +17 -10
  57. package/dist/vercel/sandbox.js +26 -13
  58. package/dist/wait.d.ts +12 -0
  59. package/dist/wait.js +47 -0
  60. package/package.json +1 -1
package/dist/sandbox.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { KEEP_ALIVE_MARGIN_SECONDS, STREAMED_EXEC_TIMEOUT_MS, WAIT_FOR_TIMEOUT_SECONDS, } from "./api-defaults.js";
2
- import { CommandError, ConnectionError, NotFoundError, RuntimeError } from "./errors.js";
2
+ import { CommandError, ConnectionError, DELIBERATE, errorFor, NotFoundError, RateLimitError, RuntimeError, ServiceUnavailableError, } from "./errors.js";
3
3
  import { Page } from "./page.js";
4
4
  import { sandboxWatches, watchDirectory, } from "./products/watch.js";
5
5
  import { Transport } from "./transport.js";
6
6
  import { sandboxFactories } from "./products/index.js";
7
7
  import { Tunnel } from "./tunnel.js";
8
+ import { pause } from "./wait.js";
9
+ /** How long snapshot() waits, pause and capture together, unless told. */
10
+ const SNAPSHOT_DEADLINE_MS = 10 * 60_000;
8
11
  const CHUNK = 1_048_576;
9
12
  /** Chunks of a large write in flight at once. Each chunk's reply waits on the
10
13
  * API and the guest, and the link idles while every chunk in flight waits:
@@ -380,7 +383,8 @@ export class Sandbox {
380
383
  this.#keepAlive?.();
381
384
  return deleteSandbox(this.#t, this.id, options);
382
385
  }
383
- /** Carries on a paused sandbox; `timeoutSeconds` is its new lease. */
386
+ /** Carries on a paused sandbox; for one with a time limit,
387
+ * `timeoutSeconds` is its new one. */
384
388
  async wake(options = {}) {
385
389
  const { timeoutSeconds, ...rest } = options;
386
390
  try {
@@ -391,13 +395,15 @@ export class Sandbox {
391
395
  // lease asked for, the refusal stands: it was not given.
392
396
  if (!(error instanceof RuntimeError) || error.code !== "not_paused" || timeoutSeconds)
393
397
  throw error;
394
- await this.refresh(pick(rest));
398
+ await this.refresh(requestOnly(rest));
395
399
  if (this.state !== "running" && this.state !== "starting")
396
400
  throw error;
397
401
  return this;
398
402
  }
399
403
  }
400
- /** More time before the lease ends (at most an hour ahead of now). */
404
+ /** More time before its time limit ends (at most an hour ahead of now). A
405
+ * sandbox with no time limit needs none: the call answers at once and
406
+ * changes nothing. */
401
407
  async extend(seconds, options = {}) {
402
408
  return this.#lifecycle("extend", { ...options, wait: false }, { seconds });
403
409
  }
@@ -411,48 +417,51 @@ export class Sandbox {
411
417
  * its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
412
418
  * volumes, environment, name and previews. Its processes restart, and
413
419
  * everything else on its old disk (sudo installs, apt packages, /etc) is
414
- * lost, which `keep: "workspace"` says you know; snapshot it first to keep
415
- * everything. A running sandbox is paused first. A switch that fails is
416
- * undone (`switch_undone`), the sandbox on its old image with nothing lost.
417
- * Charged as a wake. */
418
- async switchImage(image, options) {
419
- const { keep, ...rest } = options;
420
+ * lost; snapshot it first to keep everything. A running sandbox is paused
421
+ * first. A switch that fails is undone (`switch_undone`), the sandbox on
422
+ * its old image with nothing lost. Charged as a wake. `keep` is optional:
423
+ * "workspace" is the only value, and it is sent either way, so an API from
424
+ * before it was optional accepts the call too. */
425
+ async switchImage(image, options = {}) {
426
+ const { keep = "workspace", ...rest } = options;
420
427
  this.#keep(await this.#t.json({
421
428
  method: "POST",
422
429
  path: `/v1/sandboxes/${enc(this.id)}:switch-image`,
423
430
  body: { image, keep },
424
431
  // A pause, two boots and the copy between them.
425
432
  wait: 120,
426
- ...pick(rest),
433
+ ...requestOnly(rest),
427
434
  }));
428
435
  return this;
429
436
  }
430
437
  /** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
431
438
  * whole disk, volumes, environment, name and previews stay, and its
432
- * programs stop, which `restart: true` says you know (snapshot it first to
433
- * keep its memory too). A running sandbox is paused, or stopped if it is
439
+ * programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
434
440
  * persistent, and comes back running at the new size on the same server; a
435
441
  * paused or stopped one is started. Memory bills on the new size from then.
436
442
  * Refused, with nothing changed, when the server has no room
437
443
  * (`no_capacity`), above your quota or the trial's 2 vCPU and 4 GiB, or
438
- * while a snapshot or fork of it is being taken. */
439
- async resize(size, options) {
440
- const { restart, ...rest } = options;
444
+ * while a snapshot or fork of it is being taken. `restart` is optional:
445
+ * true is the only value, and it is sent either way, so an API from before
446
+ * it was optional accepts the call too. */
447
+ async resize(size, options = {}) {
448
+ const { restart = true, ...rest } = options;
441
449
  this.#keep(await this.#t.json({
442
450
  method: "POST",
443
451
  path: `/v1/sandboxes/${enc(this.id)}:resize`,
444
452
  body: { ...size, restart },
445
453
  // A pause or stop and a cold boot.
446
454
  wait: 120,
447
- ...pick(rest),
455
+ ...requestOnly(rest),
448
456
  }));
449
457
  return this;
450
458
  }
451
- /** Keeps a running sandbox's lease ahead of now, in the background, until
452
- * stop() or the returned function ends it: every `everySeconds` (60) it
453
- * extends the lease so that `marginSeconds` (600) remain, never more than
454
- * the hour ahead the API allows. Running time is billed as it is used, as
455
- * for any extension. A paused sandbox is left paused (a request wakes it,
459
+ /** Keeps a sandbox with a time limit running past it, in the background,
460
+ * until stop() or the returned function ends it: every `everySeconds` (60)
461
+ * it extends the limit so that `marginSeconds` (600) remain, never more
462
+ * than the hour ahead the API allows. A sandbox with no time limit
463
+ * (`endsAt` null) needs none, and is only watched. Running time is billed
464
+ * as it is used. A paused sandbox is left paused (a request wakes it,
456
465
  * unless autoWake is off); a stopped one ends the loop. It does not keep a
457
466
  * Node or Bun process alive by itself. */
458
467
  keepAlive(options = {}) {
@@ -475,8 +484,9 @@ export class Sandbox {
475
484
  await this.refresh();
476
485
  if (this.state === "stopped" || this.state === "stopping")
477
486
  return end();
478
- if (this.state === "running") {
479
- const left = (Date.parse(this.info.expiresAt) - Date.now()) / 1000;
487
+ // No time limit: nothing to extend (an older server sends no endsAt).
488
+ if (this.state === "running" && this.info.endsAt !== null) {
489
+ const left = (Date.parse(this.info.endsAt ?? this.info.expiresAt) - Date.now()) / 1000;
480
490
  const need = Math.ceil(margin - left);
481
491
  if (need >= 1)
482
492
  await this.extend(Math.min(3600, need));
@@ -500,16 +510,34 @@ export class Sandbox {
500
510
  }
501
511
  /** Keeps this sandbox's whole machine (files, memory, running processes) as
502
512
  * a snapshot to start new sandboxes from. A running sandbox is paused for
503
- * the moment it takes, then woken; a paused one stays paused. */
513
+ * the moment it takes, then woken; a paused one stays paused. Past
514
+ * `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
515
+ * the error names the snapshot. */
504
516
  async snapshot(options = {}) {
505
517
  const { idempotencyKey, signal, timeoutMs, ...body } = options;
506
518
  if (body.mode !== undefined && body.mode !== "memory" && body.mode !== "disk")
507
519
  throw new TypeError("Snapshot mode must be memory or disk.");
508
520
  signal?.throwIfAborted();
509
- const until = performance.now() + Math.min(timeoutMs && timeoutMs > 0 ? timeoutMs : 60_000, 60_000);
521
+ // One minute, whatever `timeoutMs` said, was too short: pausing a busy
522
+ // 16 vCPU, 32 GiB sandbox writes its memory out first (4 October 2026).
523
+ const until = performance.now() + (timeoutMs && timeoutMs > 0 ? timeoutMs : SNAPSHOT_DEADLINE_MS);
524
+ let running = false;
525
+ /** The capture in progress, once the server has one. */
526
+ let capturing;
527
+ let late = false;
510
528
  const request = () => {
511
529
  signal?.throwIfAborted();
512
530
  const left = Math.ceil(until - performance.now());
531
+ if (left <= 0 && capturing) {
532
+ late = true;
533
+ throw new RuntimeError({
534
+ code: "snapshot_timeout",
535
+ status: 0,
536
+ message: `Snapshot ${capturing} was still capturing at the deadline.${running ? " The sandbox stays paused until it ends: waking it now would fail the capture." : ""}`,
537
+ hint: `Wait until runtime.snapshots.get("${capturing}") is ready${running ? ", then wake the sandbox" : ""}, or pass a longer timeoutMs.`,
538
+ details: { snapshotId: capturing, sourceSandboxId: this.id },
539
+ });
540
+ }
513
541
  if (left <= 0)
514
542
  throw new RuntimeError({
515
543
  code: "snapshot_timeout",
@@ -526,7 +554,7 @@ export class Sandbox {
526
554
  await this.waitFor("running", request());
527
555
  else if (this.state === "pausing")
528
556
  await this.waitFor("paused", request());
529
- const running = this.state === "running";
557
+ running = this.state === "running";
530
558
  // Check before entering cleanup: an abort during refresh must not pause or wake.
531
559
  const pauseOptions = running ? request() : undefined;
532
560
  let captured;
@@ -547,14 +575,8 @@ export class Sandbox {
547
575
  // Prefer: wait is bounded on the server. A capture still in progress
548
576
  // must keep its source paused, or the worker refuses it after we wake.
549
577
  while (snapshot.state === "capturing") {
550
- signal?.throwIfAborted();
551
- if (performance.now() >= until)
552
- throw new RuntimeError({
553
- code: "snapshot_timeout",
554
- status: 0,
555
- message: "Snapshot capture did not finish within one minute.",
556
- details: { snapshotId: snapshot.id },
557
- });
578
+ capturing = snapshot.id;
579
+ request();
558
580
  await new Promise((resolve) => setTimeout(resolve, 200));
559
581
  snapshot = await this.#t.json({
560
582
  method: "GET",
@@ -581,8 +603,13 @@ export class Sandbox {
581
603
  catch (error) {
582
604
  failed = true;
583
605
  primary = error;
606
+ // A request cut off by the deadline mid-capture is late all the same.
607
+ if (capturing && performance.now() >= until)
608
+ late = true;
584
609
  }
585
- if (running) {
610
+ // A capture still running needs its source paused: the worker fails one
611
+ // whose source woke. The timeout says so and leaves the wake to the caller.
612
+ if (running && !late) {
586
613
  try {
587
614
  await this.wake();
588
615
  }
@@ -617,11 +644,7 @@ export class Sandbox {
617
644
  path: `/v1/sandboxes/${enc(this.id)}:fork`,
618
645
  body,
619
646
  wait: 60,
620
- ...pick({
621
- ...(idempotencyKey ? { idempotencyKey } : {}),
622
- ...(signal ? { signal } : {}),
623
- ...(timeoutMs ? { timeoutMs } : {}),
624
- }),
647
+ ...requestOnly({ idempotencyKey, signal, timeoutMs }),
625
648
  });
626
649
  const sandboxes = reply.sandboxes.map((info) => new Sandbox(this.#t, info));
627
650
  return options.count === undefined ? sandboxes[0] : sandboxes;
@@ -636,7 +659,7 @@ export class Sandbox {
636
659
  path: `/v1/sandboxes/${enc(this.id)}:${verb}`,
637
660
  body,
638
661
  wait: options.wait === false ? 0 : 60,
639
- ...pick(options),
662
+ ...requestOnly(options),
640
663
  }));
641
664
  return this;
642
665
  }
@@ -660,13 +683,10 @@ class OutputCursor {
660
683
  this.cursor = cursor;
661
684
  }
662
685
  *pass(event) {
686
+ // As the status the API would have answered it, so a passing failure (a
687
+ // host restarting) is followed again rather than ending the read.
663
688
  if (event.type === "error")
664
- throw new RuntimeError({
665
- message: event.error.message,
666
- code: event.error.code,
667
- status: 0,
668
- ...(event.error.requestId ? { requestId: event.error.requestId } : {}),
669
- });
689
+ throw errorFor(event.error.status ?? 0, event);
670
690
  if (event.type === "truncated")
671
691
  this.cursor = Math.max(this.cursor, event.resumeAt);
672
692
  if (event.type === "stdout" || event.type === "stderr") {
@@ -697,11 +717,20 @@ function processBytes(encoded) {
697
717
  });
698
718
  }
699
719
  }
700
- /** A stream cut off by the network rather than refused by Runtime: worth
701
- * reconnecting to from the cursor. */
720
+ /** A stream cut off by the network, or refused by a failure that passes by
721
+ * itself (a busy API, a 429, a host restarting), rather than refused for good:
722
+ * worth reconnecting to from the cursor. */
702
723
  function cutOff(error) {
703
- return !(error instanceof RuntimeError) || error instanceof ConnectionError;
724
+ return (!(error instanceof RuntimeError) ||
725
+ error instanceof ConnectionError ||
726
+ ((error instanceof RateLimitError || error instanceof ServiceUnavailableError) &&
727
+ [429, 502, 503, 504].includes(error.status) &&
728
+ !DELIBERATE.has(error.code)));
704
729
  }
730
+ /** How long a follow keeps reconnecting while every attempt fails. A command
731
+ * runs for hours whether or not anyone reads it; a few minutes of a deploy, a
732
+ * network blip or a busy API must not end the read of it. */
733
+ const FOLLOW_OUTAGE_MS = 300_000;
705
734
  /** A streamed command keeps all its output unless the reader fell more than the
706
735
  * process's output buffer behind, which the server marks with a "truncated"
707
736
  * event. That event does not say which stream lost bytes, so both flags carry
@@ -709,6 +738,8 @@ function cutOff(error) {
709
738
  function truncation(dropped) {
710
739
  return { stdoutTruncated: dropped, stderrTruncated: dropped };
711
740
  }
741
+ /** A command's request options: its `timeoutMs` is the command's own limit
742
+ * in the guest, so the call is given a minute more for the answer. */
712
743
  function pick(options) {
713
744
  return {
714
745
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
@@ -716,6 +747,15 @@ function pick(options) {
716
747
  ...(options.timeoutMs ? { timeoutMs: options.timeoutMs + 60_000 } : {}),
717
748
  };
718
749
  }
750
+ /** Any other call's request options, as the caller gave them: `timeoutMs` is
751
+ * the call's whole deadline, and 0 turns it off. */
752
+ function requestOnly(options) {
753
+ return {
754
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
755
+ ...(options.signal ? { signal: options.signal } : {}),
756
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
757
+ };
758
+ }
719
759
  /** Sessions: a token your backend makes and hands to your own frontend, which
720
760
  * then runs commands, reads and writes files and reaches previews in this one
721
761
  * sandbox directly, without proxying through your servers. A session cannot
@@ -804,12 +844,19 @@ export class Processes {
804
844
  /** Output events from `cursor` until the process exits, across stream slices. */
805
845
  async *follow(processId, options = {}) {
806
846
  const read = new OutputCursor(options.cursor ?? 0);
807
- // Reconnections in a row that brought nothing: a stream that keeps
808
- // closing without output or an exit is given up, not followed forever.
847
+ // Reconnections in a row that closed cleanly with nothing: a stream that
848
+ // keeps closing without output or an exit is given up, not followed
849
+ // forever.
809
850
  let idle = 0;
851
+ // Since when every reconnection has failed (a cut, a busy API, a host
852
+ // restarting). The command runs on regardless, so these are waited out
853
+ // for FOLLOW_OUTAGE_MS, with backoff, before the follow gives up.
854
+ let failingSince;
855
+ let failures = 0;
810
856
  for (;;) {
811
857
  const before = read.cursor;
812
858
  let resumed = false;
859
+ let failure;
813
860
  try {
814
861
  for await (const event of this.t.events({
815
862
  method: "GET",
@@ -831,8 +878,28 @@ export class Processes {
831
878
  catch (error) {
832
879
  if (options.signal?.aborted || !cutOff(error))
833
880
  throw error;
881
+ failure = error;
882
+ }
883
+ if (resumed || read.cursor > before) {
884
+ idle = failures = 0;
885
+ failingSince = undefined;
886
+ continue;
887
+ }
888
+ if (failure === undefined)
889
+ idle++;
890
+ else {
891
+ failingSince ??= performance.now();
892
+ if (performance.now() - failingSince >= FOLLOW_OUTAGE_MS)
893
+ throw new ConnectionError({
894
+ message: `Lost the output stream of process ${processId} for ${FOLLOW_OUTAGE_MS / 60_000} minutes; the command may still be running.`,
895
+ code: "connection_error",
896
+ status: 0,
897
+ hint: `Follow it again: runtime sandbox logs ${this.sandboxId} ${processId} -f`,
898
+ details: { sandboxId: this.sandboxId, processId },
899
+ cause: failure,
900
+ });
901
+ await pause(Math.min(5000, 50 * 2 ** failures++), options.signal);
834
902
  }
835
- idle = resumed || read.cursor > before ? 0 : idle + 1;
836
903
  if (idle > 3)
837
904
  throw new ConnectionError({
838
905
  message: `The output stream of process ${processId} keeps closing before it ends.`,
package/dist/tar.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /** One entry's header: the ustar block, after the long-name and long-link
2
+ * entries it needs. A whole number of 512-byte blocks. */
1
3
  export declare function tarHeader(name: string, size: number, mode: number, type: "0" | "5" | "2", link?: string, mtime?: number): Uint8Array;
2
4
  export declare function packDirectory(root: string): Promise<Uint8Array>;
3
5
  /** Unpacks a gzipped tar that arrives as a stream into `target`, refusing any
package/dist/tar.js CHANGED
@@ -1,25 +1,35 @@
1
- import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
1
+ import { chmod, copyFile, link as hardLink, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { Readable } from "node:stream";
4
4
  import { createGunzip, gzipSync } from "node:zlib";
5
5
  import { RuntimeError } from "./errors.js";
6
- /* A small ustar writer and reader for directory uploads and downloads, so the
7
- SDK needs no dependency. Regular files, directories and symbolic links;
8
- names up to 255 bytes through the ustar prefix field. */
9
- export function tarHeader(name, size, mode, type, link = "", mtime = 0) {
10
- const block = new Uint8Array(512);
11
- const bytes = new TextEncoder().encode(name);
12
- let prefix = new Uint8Array();
13
- let short = bytes;
14
- if (bytes.length > 100) {
15
- const split = name.lastIndexOf("/", 155);
16
- if (split <= 0 || new TextEncoder().encode(name.slice(split + 1)).length > 100)
17
- throw new Error(`Path too long for a tar archive: ${name}`);
18
- prefix = new TextEncoder().encode(name.slice(0, split));
19
- short = new TextEncoder().encode(name.slice(split + 1));
6
+ /* A small tar writer and reader for directory uploads and downloads, so the
7
+ SDK needs no dependency. Regular files, directories, symbolic links and,
8
+ reading, hard links. A name that does not fit the ustar name and prefix
9
+ fields, or a link target longer than 100 bytes, travels in a GNU long-name
10
+ or long-link entry, which GNU tar, BusyBox tar and every SDK's reader
11
+ take; nothing is cut short. */
12
+ const encode = (value) => new TextEncoder().encode(value);
13
+ /** The ustar prefix and name of `name`, split at a slash so that each fits
14
+ * its field in bytes, or null when no split fits. */
15
+ function ustarName(name) {
16
+ const bytes = encode(name);
17
+ if (bytes.length <= 100)
18
+ return { prefix: new Uint8Array(), short: bytes };
19
+ for (let split = name.lastIndexOf("/"); split > 0; split = name.lastIndexOf("/", split - 1)) {
20
+ const prefix = encode(name.slice(0, split));
21
+ const short = encode(name.slice(split + 1));
22
+ if (short.length > 100)
23
+ return null;
24
+ if (prefix.length <= 155)
25
+ return { prefix, short };
20
26
  }
27
+ return null;
28
+ }
29
+ function headerBlock(short, prefix, size, mode, type, link, mtime) {
30
+ const block = new Uint8Array(512);
21
31
  const put = (offset, value, length) => {
22
- const data = typeof value === "string" ? new TextEncoder().encode(value) : value;
32
+ const data = typeof value === "string" ? encode(value) : value;
23
33
  block.set(data.subarray(0, length), offset);
24
34
  };
25
35
  const octal = (value, length) => value.toString(8).padStart(length - 1, "0") + "\0";
@@ -40,6 +50,29 @@ export function tarHeader(name, size, mode, type, link = "", mtime = 0) {
40
50
  put(148, sum.toString(8).padStart(6, "0") + "\0 ", 8);
41
51
  return block;
42
52
  }
53
+ /** A GNU long-name ("L") or long-link ("K") entry carrying `value`. */
54
+ function longEntry(type, value) {
55
+ const size = value.length + 1;
56
+ const out = new Uint8Array(512 + Math.ceil(size / 512) * 512);
57
+ out.set(headerBlock(encode("././@LongLink"), new Uint8Array(), size, 0o644, type, new Uint8Array(), 0));
58
+ out.set(value, 512);
59
+ return out;
60
+ }
61
+ /** One entry's header: the ustar block, after the long-name and long-link
62
+ * entries it needs. A whole number of 512-byte blocks. */
63
+ export function tarHeader(name, size, mode, type, link = "", mtime = 0) {
64
+ const parts = [];
65
+ let fitted = ustarName(name);
66
+ if (!fitted) {
67
+ parts.push(longEntry("L", encode(name)));
68
+ fitted = { prefix: new Uint8Array(), short: encode(name).subarray(0, 100) };
69
+ }
70
+ const target = encode(link);
71
+ if (target.length > 100)
72
+ parts.push(longEntry("K", target));
73
+ parts.push(headerBlock(fitted.short, fitted.prefix, size, mode, type, target, mtime));
74
+ return parts.length === 1 ? parts[0] : new Uint8Array(Buffer.concat(parts));
75
+ }
43
76
  export async function packDirectory(root) {
44
77
  const parts = [];
45
78
  const base = resolve(root);
@@ -352,8 +385,15 @@ async function unpackEntries(bytes, root, signal) {
352
385
  links.push({ destination, link, name });
353
386
  await skip();
354
387
  }
388
+ else if (type === "1") {
389
+ await hardLinkEntry(root, destination, link, name);
390
+ await skip();
391
+ }
355
392
  else if (type === "0" || type === "\0" || type === "7") {
356
393
  await mkdir(dirname(destination), { recursive: true });
394
+ // A name already unpacked may share its file with a hard link; writing
395
+ // through it would change the link's copy too.
396
+ await unlink(destination).catch(() => undefined);
357
397
  const file = await open(destination, "w");
358
398
  try {
359
399
  for await (const part of bytes.take(size)) {
@@ -378,6 +418,24 @@ async function unpackEntries(bytes, root, signal) {
378
418
  await skip();
379
419
  }
380
420
  }
421
+ /** A second name for a file the archive already carried: tar writes the
422
+ * first name as a file and every other as a hard link to it. The link names
423
+ * a plain file unpacked earlier in this archive, or the unpack fails; it
424
+ * never reaches outside, through a link or ahead. */
425
+ async function hardLinkEntry(root, destination, link, name) {
426
+ const named = link.replace(/^(\.\/)+/, "");
427
+ const source = resolve(root, named);
428
+ if (!named || isAbsolute(named) || source === root || !source.startsWith(root + sep))
429
+ throw new Error(`Refusing an archive hard link that leads outside the target: ${name} -> ${link}`);
430
+ await assertPlain(root, source, named);
431
+ if (!(await lstat(source).catch(() => undefined))?.isFile())
432
+ throw new Error(`Refusing an archive hard link to a file it does not carry: ${name} -> ${link}`);
433
+ if (source === destination)
434
+ return;
435
+ await mkdir(dirname(destination), { recursive: true });
436
+ await unlink(destination).catch(() => undefined);
437
+ await hardLink(source, destination).catch(() => copyFile(source, destination));
438
+ }
381
439
  /** Moves what was unpacked in `from` into `to`, merging with what is there
382
440
  * and replacing files of the same name, never through a link in `to`. */
383
441
  async function merge(from, to, root, signal) {
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.11.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;