withruntime 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/BLAXEL.md +97 -32
  2. package/COMPATIBILITY.md +120 -5
  3. package/DAYTONA.md +34 -3
  4. package/E2B.md +34 -5
  5. package/README.md +33 -0
  6. package/README.src.md +33 -0
  7. package/VERCEL.md +52 -5
  8. package/compatibility-lock.json +71 -111
  9. package/dist/blaxel/client.d.ts +11 -0
  10. package/dist/blaxel/client.js +28 -0
  11. package/dist/blaxel/errors.d.ts +65 -0
  12. package/dist/blaxel/errors.js +114 -0
  13. package/dist/blaxel/index.d.ts +10 -3
  14. package/dist/blaxel/index.js +23 -4
  15. package/dist/blaxel/preview.d.ts +29 -4
  16. package/dist/blaxel/preview.js +79 -5
  17. package/dist/blaxel/sandbox.js +1 -0
  18. package/dist/blaxel/session.js +4 -2
  19. package/dist/cli-extras.js +2 -1
  20. package/dist/cli-words.js +1 -0
  21. package/dist/cli.d.ts +1 -1
  22. package/dist/cli.js +54 -21
  23. package/dist/client.d.ts +1 -1
  24. package/dist/daytona/client.d.ts +4 -0
  25. package/dist/daytona/client.js +8 -3
  26. package/dist/daytona/daytona.d.ts +3 -0
  27. package/dist/daytona/daytona.js +2 -1
  28. package/dist/e2b/sandbox.d.ts +6 -5
  29. package/dist/e2b/sandbox.js +9 -12
  30. package/dist/freestyle/index.d.ts +77 -5
  31. package/dist/freestyle/index.js +380 -48
  32. package/dist/identity.js +36 -18
  33. package/dist/index.d.ts +2 -2
  34. package/dist/machine.d.ts +24 -12
  35. package/dist/machine.js +46 -14
  36. package/dist/mcp.js +16 -3
  37. package/dist/modal/index.d.ts +15 -0
  38. package/dist/modal/index.js +35 -7
  39. package/dist/morph/index.d.ts +255 -0
  40. package/dist/morph/index.js +641 -0
  41. package/dist/network-cli.js +2 -2
  42. package/dist/product-names.d.ts +2 -2
  43. package/dist/product-names.js +2 -2
  44. package/dist/products/limits.d.ts +16 -2
  45. package/dist/products/limits.js +2 -2
  46. package/dist/products/mcp.d.ts +5 -0
  47. package/dist/products/network-products.d.ts +21 -3
  48. package/dist/products/network-products.js +3 -1
  49. package/dist/products/previews.d.ts +6 -3
  50. package/dist/products/previews.js +1 -1
  51. package/dist/snapshots.d.ts +4 -0
  52. package/dist/ssh.d.ts +35 -0
  53. package/dist/ssh.js +93 -1
  54. package/dist/transport.d.ts +11 -8
  55. package/dist/transport.js +46 -12
  56. package/dist/types.d.ts +24 -0
  57. package/dist/vercel/command.d.ts +48 -1
  58. package/dist/vercel/command.js +237 -6
  59. package/dist/vercel/sandbox-user.js +2 -1
  60. package/dist/vercel/sandbox.d.ts +16 -2
  61. package/dist/vercel/sandbox.js +45 -2
  62. package/package.json +6 -1
@@ -31,23 +31,27 @@ export class FreestyleTransportError extends TypeError {
31
31
  return { name: this.name, message: this.message, errors: this.errors };
32
32
  }
33
33
  }
34
+ /** A Runtime error as Freestyle's client throws it. */
35
+ function translate(error) {
36
+ if (error instanceof ConnectionError)
37
+ return new FreestyleTransportError(error, [
38
+ { name: error.name, message: error.message, code: error.code },
39
+ ]);
40
+ if (!(error instanceof RuntimeError))
41
+ return error;
42
+ const translated = new FreestyleApiError(error.status, {
43
+ code: error.code.toUpperCase(),
44
+ message: error.message,
45
+ });
46
+ Object.defineProperty(translated, "cause", { value: error });
47
+ return translated;
48
+ }
34
49
  async function api(call) {
35
50
  try {
36
51
  return await call();
37
52
  }
38
53
  catch (error) {
39
- if (error instanceof ConnectionError)
40
- throw new FreestyleTransportError(error, [
41
- { name: error.name, message: error.message, code: error.code },
42
- ]);
43
- if (!(error instanceof RuntimeError))
44
- throw error;
45
- const translated = new FreestyleApiError(error.status, {
46
- code: error.code.toUpperCase(),
47
- message: error.message,
48
- });
49
- Object.defineProperty(translated, "cause", { value: error });
50
- throw translated;
54
+ throw translate(error);
51
55
  }
52
56
  }
53
57
  const notFound = (what) => new FreestyleApiError(404, { code: "NOT_FOUND", message: `${what} not found` });
@@ -555,63 +559,295 @@ const dimension = (value, fallback, name) => {
555
559
  throw new TypeError(`${name} must be a whole number from 1 to 1000`);
556
560
  return value;
557
561
  };
558
- const PTY_OPEN = [
559
- "exec",
560
- "cols",
561
- "rows",
562
- "linuxUser",
563
- "slug",
562
+ const PTY_EVENTS = [
563
+ "reconnect",
564
+ "connectTimeoutMs",
565
+ "onStateChange",
566
+ "onReplayStart",
567
+ "onReplayEnd",
564
568
  "onData",
565
569
  "onExit",
566
570
  "onClose",
567
571
  "onError",
568
572
  ];
573
+ const PTY_OPEN = ["exec", "cols", "rows", "linuxUser", "slug", ...PTY_EVENTS];
574
+ /** Freestyle 0.2.18 checks its timers before anything is opened. */
575
+ function connectionOptions(options) {
576
+ const { reconnect } = options;
577
+ if (reconnect !== undefined && reconnect !== null && typeof reconnect !== "boolean") {
578
+ if (typeof reconnect !== "object")
579
+ throw new TypeError("reconnect must be true, false or reconnect options");
580
+ only("Freestyle pty reconnect", reconnect, ["timeoutMs", "initialDelayMs", "maxDelayMs"]);
581
+ }
582
+ const timers = {
583
+ connectTimeoutMs: options.connectTimeoutMs,
584
+ ...(typeof reconnect === "object" && reconnect ? reconnect : {}),
585
+ };
586
+ for (const [name, value] of Object.entries(timers))
587
+ if (value !== undefined &&
588
+ (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 2_147_483_647))
589
+ throw new RangeError(`${name} must be a positive finite timer duration`);
590
+ }
591
+ const LOST = { wasClean: false, code: 1006, reason: "Connection lost" };
592
+ /** A 4xx other than 409 and 429 will not get better by retrying. */
593
+ const fatal = (error) => error instanceof RuntimeError &&
594
+ error.status >= 400 &&
595
+ error.status < 500 &&
596
+ error.status !== 409 &&
597
+ error.status !== 429;
598
+ /** A stable handle across reattachments; input is never retried or buffered.
599
+ * Each connection is a Runtime terminal attached to the session's process. */
569
600
  export class PtySession {
570
- terminal;
571
601
  sandbox;
572
602
  processId;
573
603
  sessionId;
574
604
  slug;
575
605
  created;
576
- #readyState = 1;
577
- constructor(terminal, sandbox, processId, sessionId, slug, created, events) {
578
- this.terminal = terminal;
606
+ events;
607
+ #state = "connecting";
608
+ #readyState = 0;
609
+ #terminal;
610
+ #generation = 0;
611
+ #attempt = 0;
612
+ #deadline = 0;
613
+ #retry;
614
+ #stopped = false;
615
+ #exited = false;
616
+ #fatal = false;
617
+ #established = false;
618
+ #closeReported = false;
619
+ #dimensions;
620
+ #removeWake;
621
+ #resolveReady;
622
+ #rejectReady;
623
+ /** Resolves once the first connection is up. */
624
+ ready;
625
+ constructor(sandbox, processId, sessionId, slug, created, events, dimensions) {
579
626
  this.sandbox = sandbox;
580
627
  this.processId = processId;
581
628
  this.sessionId = sessionId;
582
629
  this.slug = slug;
583
630
  this.created = created;
584
- void terminal.exited.then((exitCode) => {
585
- this.#readyState = 3;
586
- if (exitCode !== null)
587
- events.onExit?.(exitCode);
588
- events.onClose?.({ wasClean: true, code: 1000, reason: "" });
631
+ this.events = events;
632
+ this.#dimensions = dimensions;
633
+ this.ready = new Promise((resolve, reject) => {
634
+ this.#resolveReady = resolve;
635
+ this.#rejectReady = reject;
636
+ });
637
+ void this.ready.catch(() => { });
638
+ if (events.reconnect && typeof window !== "undefined" && typeof document !== "undefined") {
639
+ // A browser back online or back in view retries a dropped session now.
640
+ const wake = () => {
641
+ if (document.visibilityState !== "hidden" && this.#state === "closed")
642
+ this.reconnect();
643
+ };
644
+ window.addEventListener("online", wake);
645
+ window.addEventListener("pageshow", wake);
646
+ document.addEventListener("visibilitychange", wake);
647
+ this.#removeWake = () => {
648
+ window.removeEventListener("online", wake);
649
+ window.removeEventListener("pageshow", wake);
650
+ document.removeEventListener("visibilitychange", wake);
651
+ };
652
+ }
653
+ void this.#dial();
654
+ }
655
+ #setState(state) {
656
+ if (this.#state === state)
657
+ return;
658
+ this.#state = state;
659
+ this.events.onStateChange?.(state);
660
+ }
661
+ async #dial() {
662
+ const generation = ++this.#generation;
663
+ const current = () => generation === this.#generation && !this.#stopped;
664
+ const limit = this.#established
665
+ ? Math.min(this.events.connectTimeoutMs ?? 15_000, Math.max(1, this.#deadline - Date.now()))
666
+ : this.events.connectTimeoutMs;
667
+ try {
668
+ // Before a reattachment, as Freestyle's preflight: a session that is
669
+ // gone, or credentials that no longer work, end it instead of retrying.
670
+ if (this.#established)
671
+ await this.sandbox.processes.get(this.processId);
672
+ if (!current())
673
+ return;
674
+ const opening = this.sandbox.terminal({
675
+ processId: this.processId,
676
+ onData: (data) => {
677
+ if (current())
678
+ this.events.onData?.(data);
679
+ },
680
+ });
681
+ let timer;
682
+ const terminal = await (limit === undefined
683
+ ? opening
684
+ : Promise.race([
685
+ opening,
686
+ new Promise((_, reject) => {
687
+ timer = setTimeout(() => {
688
+ void opening.then((late) => late.close()).catch(() => { });
689
+ reject(new Error("PTY handshake timed out"));
690
+ }, limit);
691
+ }),
692
+ ]).finally(() => clearTimeout(timer)));
693
+ if (!current()) {
694
+ terminal.close();
695
+ return;
696
+ }
697
+ this.#bind(terminal, generation);
698
+ }
699
+ catch (error) {
700
+ if (!current())
701
+ return;
702
+ if (!this.#established)
703
+ return this.#finish(translate(error));
704
+ if (fatal(error)) {
705
+ this.#fatal = true;
706
+ this.#finish(translate(error));
707
+ }
708
+ else
709
+ this.#scheduleRetry(translate(error));
710
+ }
711
+ }
712
+ #bind(terminal, generation) {
713
+ this.#terminal = terminal;
714
+ this.#readyState = 1;
715
+ const recovering = this.#established;
716
+ this.#established = true;
717
+ this.#attempt = 0;
718
+ this.#deadline = 0;
719
+ this.#closeReported = false;
720
+ this.#resolveReady();
721
+ this.#setState("connected");
722
+ if (recovering && this.#dimensions)
723
+ terminal.resize(this.#dimensions.cols, this.#dimensions.rows);
724
+ void terminal.exited.then((exitCode) => this.#ended(generation, exitCode));
725
+ }
726
+ /** The connection ended: with the shell's exit code, or without one when the
727
+ * shell was killed by a signal or the connection dropped. */
728
+ #ended(generation, exitCode) {
729
+ const current = () => generation === this.#generation && !this.#stopped;
730
+ if (!current())
731
+ return;
732
+ this.#readyState = 3;
733
+ if (exitCode !== null)
734
+ return this.#exit(exitCode);
735
+ this.sandbox.processes.get(this.processId).then((process) => {
736
+ if (!current())
737
+ return;
738
+ if (process.state === "running")
739
+ this.#lost();
740
+ else
741
+ this.#exit(process.exitCode);
742
+ }, (error) => {
743
+ if (current())
744
+ this.#lost(translate(error));
589
745
  });
590
- this.onError = events.onError;
591
746
  }
592
- onError;
747
+ #retire() {
748
+ ++this.#generation;
749
+ clearTimeout(this.#retry);
750
+ if (this.#readyState === 1)
751
+ this.#readyState = 2;
752
+ this.#terminal?.close();
753
+ this.#terminal = undefined;
754
+ }
755
+ #exit(exitCode) {
756
+ this.#exited = true;
757
+ this.#retire();
758
+ this.#readyState = 3;
759
+ this.#setState("exited");
760
+ this.#removeWake?.();
761
+ if (exitCode !== null)
762
+ this.events.onExit?.(exitCode);
763
+ this.#report({ wasClean: true, code: 1000, reason: "" });
764
+ }
765
+ #lost(error) {
766
+ this.#retire();
767
+ this.#readyState = 3;
768
+ if (this.#stopped)
769
+ return;
770
+ if (!this.events.reconnect || this.#exited || this.#fatal)
771
+ return this.#finish(error);
772
+ this.#scheduleRetry(error);
773
+ }
774
+ #scheduleRetry(error) {
775
+ const options = typeof this.events.reconnect === "object" ? this.events.reconnect : null;
776
+ this.#deadline ||= Date.now() + (options?.timeoutMs ?? 300_000);
777
+ if (Date.now() >= this.#deadline)
778
+ return this.#finish(error ?? new Error("PTY reconnect timed out"));
779
+ this.#setState("reconnecting");
780
+ const base = Math.min((options?.initialDelayMs ?? 400) * 2 ** this.#attempt++, options?.maxDelayMs ?? 10_000);
781
+ const delay = Math.min(Math.round(base * (0.7 + Math.random() * 0.6)), Math.max(0, this.#deadline - Date.now()));
782
+ this.#retry = setTimeout(() => void this.#dial(), delay);
783
+ }
784
+ #finish(error, info = LOST) {
785
+ clearTimeout(this.#retry);
786
+ if (!this.#established)
787
+ this.#rejectReady(error ?? new Error("PTY connection failed"));
788
+ this.#setState(this.#exited ? "exited" : "closed");
789
+ if (this.#fatal || this.#exited || !this.#established)
790
+ this.#removeWake?.();
791
+ this.#report(info, error);
792
+ }
793
+ #report(info, error) {
794
+ if (this.#closeReported)
795
+ return;
796
+ this.#closeReported = true;
797
+ if (error)
798
+ this.events.onError?.(error);
799
+ this.events.onClose?.(info);
800
+ }
801
+ /** Retry now after a network change or a spent retry budget. Never opens a new shell. */
802
+ reconnect() {
803
+ if (this.#stopped || this.#exited || this.#fatal || !this.#established)
804
+ return;
805
+ this.#retire();
806
+ this.#attempt = 0;
807
+ this.#deadline = 0;
808
+ this.#closeReported = false;
809
+ this.#scheduleRetry();
810
+ }
811
+ /** Send input once. Throws while disconnected; uncertain keystrokes are never replayed. */
593
812
  write(data) {
594
- this.terminal.write(data);
813
+ if (this.#state !== "connected" || !this.#terminal)
814
+ throw new Error("PTY is not connected");
815
+ this.#terminal.write(data);
595
816
  }
817
+ /** Remembers the size and applies it again after a reattachment. */
596
818
  resize(options) {
597
- this.terminal.resize(dimension(options.cols, 80, "cols"), dimension(options.rows, 24, "rows"));
819
+ const cols = dimension(options.cols, 80, "cols");
820
+ const rows = dimension(options.rows, 24, "rows");
821
+ this.#dimensions = { cols, rows };
822
+ if (this.#state === "connected")
823
+ this.#terminal?.resize(cols, rows);
598
824
  }
599
825
  signal(sig) {
600
826
  if (sig !== "sigint" && sig !== "sigkill")
601
827
  throw new TypeError('signal must be "sigint" or "sigkill"');
828
+ if (this.#state !== "connected")
829
+ throw new Error("PTY is not connected");
602
830
  void (async () => {
603
831
  const process = await this.sandbox.processes.get(this.processId);
604
832
  await process.kill(sig === "sigint" ? "SIGINT" : "SIGKILL");
605
- })().catch((error) => this.onError?.(error));
833
+ })().catch((error) => this.events.onError?.(translate(error)));
606
834
  }
607
- /** Closes the connection; the shell keeps running and attach() finds it. */
835
+ /** Cancels recovery and closes the connection; the shell keeps running and
836
+ * attach() finds it. */
608
837
  detach() {
609
- this.#readyState = 2;
610
- this.terminal.close();
838
+ if (this.#stopped)
839
+ return;
840
+ this.#stopped = true;
841
+ this.#retire();
842
+ this.#removeWake?.();
843
+ this.#finish(undefined, { wasClean: true, code: 1000, reason: "Detached" });
611
844
  }
612
845
  get readyState() {
613
846
  return this.#readyState;
614
847
  }
848
+ get state() {
849
+ return this.#state;
850
+ }
615
851
  }
616
852
  export class VmPty {
617
853
  get;
@@ -633,12 +869,14 @@ export class VmPty {
633
869
  throw notFound(`PTY session ${selector} of ${linuxUser}`);
634
870
  return found;
635
871
  }
636
- async connect(sandbox, found, created, events) {
637
- const terminal = await sandbox.terminal({ processId: found.processId, onData: events.onData });
638
- return new PtySession(terminal, sandbox, found.processId, found.info.sessionId, found.info.slug ?? undefined, created, events);
872
+ async connect(sandbox, found, created, events, dimensions) {
873
+ const session = new PtySession(sandbox, found.processId, found.info.sessionId, found.info.slug ?? undefined, created, events, dimensions);
874
+ await session.ready;
875
+ return session;
639
876
  }
640
877
  async open(options = {}) {
641
878
  only("Freestyle pty open", options, PTY_OPEN);
879
+ connectionOptions(options);
642
880
  const cols = dimension(options.cols, 80, "cols");
643
881
  const rows = dimension(options.rows, 24, "rows");
644
882
  const user = options.linuxUser === undefined ? "" : validateLinuxUser(options.linuxUser);
@@ -654,7 +892,7 @@ export class VmPty {
654
892
  const existing = (await this.sessions(sandbox)).find((s) => s.info.slug === options.slug);
655
893
  // Get-or-create: a running session with the slug is handed back, exec unrun.
656
894
  if (existing)
657
- return this.connect(sandbox, existing, false, options);
895
+ return this.connect(sandbox, existing, false, options, { cols, rows });
658
896
  }
659
897
  const shell = options.exec === undefined
660
898
  ? user
@@ -667,18 +905,12 @@ export class VmPty {
667
905
  const started = session(process.toJSON());
668
906
  if (!started)
669
907
  throw new Error("The started PTY session has no session line");
670
- return this.connect(sandbox, started, true, options);
908
+ return this.connect(sandbox, started, true, options, { cols, rows });
671
909
  });
672
910
  }
673
911
  async attach(options) {
674
- only("Freestyle pty attach", options, [
675
- "session",
676
- "linuxUser",
677
- "onData",
678
- "onExit",
679
- "onClose",
680
- "onError",
681
- ]);
912
+ only("Freestyle pty attach", options, ["session", "linuxUser", ...PTY_EVENTS]);
913
+ connectionOptions(options);
682
914
  if (options.linuxUser !== undefined)
683
915
  validateLinuxUser(options.linuxUser);
684
916
  return api(async () => {
@@ -732,6 +964,106 @@ export class VmLinuxUserPty {
732
964
  return this.pty.close(session, { linuxUser: this.linuxUser });
733
965
  }
734
966
  }
967
+ /** Terminal queries a replay may hold (device attributes, status and mode
968
+ * reports, window ops): answering them again would type the answers. */
969
+ const XTERM_QUERIES = [
970
+ { final: "c" },
971
+ { prefix: ">", final: "c" },
972
+ { final: "n" },
973
+ { prefix: "?", final: "n" },
974
+ { final: "t" },
975
+ { intermediates: "$", final: "p" },
976
+ { prefix: "?", intermediates: "$", final: "p" },
977
+ ];
978
+ /** Freestyle 0.2.18's connectXterm: xterm.js on a named session that
979
+ * reattaches by itself unless `reconnect: false`. Each connection replays the
980
+ * session's output from its start, so the screen is reset first and the
981
+ * replay's terminal queries go unanswered until it has been written (150ms
982
+ * quiet, at most 2 seconds). dispose() detaches; the shell keeps running. */
983
+ export async function connectXterm({ pty, terminal, ...options }) {
984
+ only("Freestyle connectXterm", options, PTY_OPEN);
985
+ let muted = [];
986
+ let quiet;
987
+ let cap;
988
+ let replay = 0;
989
+ let disposed = false;
990
+ const unmute = () => {
991
+ clearTimeout(quiet);
992
+ clearTimeout(cap);
993
+ for (const handler of muted)
994
+ handler.dispose();
995
+ muted = [];
996
+ };
997
+ const endReplay = () => {
998
+ clearTimeout(quiet);
999
+ clearTimeout(cap);
1000
+ const current = replay;
1001
+ // Unmute once xterm has parsed everything written before this point.
1002
+ terminal.write("", () => {
1003
+ if (current === replay)
1004
+ unmute();
1005
+ });
1006
+ };
1007
+ const beginReplay = () => {
1008
+ ++replay;
1009
+ unmute();
1010
+ terminal.write("\x1bc");
1011
+ muted = XTERM_QUERIES.map((id) => terminal.parser.registerCsiHandler(id, () => true));
1012
+ muted.push(terminal.parser.registerDcsHandler({ intermediates: "$", final: "q" }, () => true));
1013
+ quiet = setTimeout(endReplay, 150);
1014
+ cap = setTimeout(endReplay, 2_000);
1015
+ };
1016
+ const stopReplay = () => {
1017
+ ++replay;
1018
+ unmute();
1019
+ };
1020
+ try {
1021
+ const session = await pty.open({
1022
+ ...options,
1023
+ reconnect: options.reconnect ?? true,
1024
+ cols: options.cols ?? terminal.cols,
1025
+ rows: options.rows ?? terminal.rows,
1026
+ onStateChange(state) {
1027
+ if (state === "connected")
1028
+ beginReplay();
1029
+ if (state === "closed" || state === "exited")
1030
+ endReplay();
1031
+ options.onStateChange?.(state);
1032
+ },
1033
+ onData(bytes) {
1034
+ if (disposed)
1035
+ return;
1036
+ if (muted.length) {
1037
+ clearTimeout(quiet);
1038
+ quiet = setTimeout(endReplay, 150);
1039
+ }
1040
+ terminal.write(bytes);
1041
+ options.onData?.(bytes);
1042
+ },
1043
+ });
1044
+ const input = terminal.onData((data) => {
1045
+ if (session.state === "connected")
1046
+ session.write(data);
1047
+ });
1048
+ const resize = terminal.onResize((size) => session.resize(size));
1049
+ return {
1050
+ session,
1051
+ dispose() {
1052
+ if (disposed)
1053
+ return;
1054
+ disposed = true;
1055
+ input.dispose();
1056
+ resize.dispose();
1057
+ session.detach();
1058
+ stopReplay();
1059
+ },
1060
+ };
1061
+ }
1062
+ catch (error) {
1063
+ stopReplay();
1064
+ throw error;
1065
+ }
1066
+ }
735
1067
  export class VmLinuxUser {
736
1068
  vm;
737
1069
  linuxUser;
package/dist/identity.js CHANGED
@@ -1,20 +1,27 @@
1
1
  import { RuntimeError } from "./errors.js";
2
2
  import { sandboxMarker } from "./transport.js";
3
+ /** The guest's environment file, which holds the sandbox's current request
4
+ * token: the guest writes it again when the token changes (a wake on another
5
+ * server, a restore), while a process keeps the environment it started with. */
6
+ async function fileCredentials() {
7
+ try {
8
+ // Loaded here, so the SDK still loads where there is no file system.
9
+ const { readFile } = await import("node:fs/promises");
10
+ const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
11
+ return { url: env.RUNTIME_ID_TOKEN_REQUEST_URL, token: env.RUNTIME_ID_TOKEN_REQUEST_TOKEN };
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
3
17
  async function requestCredentials() {
4
18
  let url = process.env.RUNTIME_ID_TOKEN_REQUEST_URL;
5
19
  let token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
6
20
  if (!url || !token) {
7
21
  // A process started before the guest wrote its environment still finds it.
8
- try {
9
- // Loaded here, so the SDK still loads where there is no file system.
10
- const { readFile } = await import("node:fs/promises");
11
- const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
12
- url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
13
- token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
14
- }
15
- catch {
16
- return null;
17
- }
22
+ const file = await fileCredentials();
23
+ url = file?.url;
24
+ token = file?.token;
18
25
  }
19
26
  return url && token ? { url, token } : null;
20
27
  }
@@ -37,14 +44,25 @@ export async function identityToken(input) {
37
44
  status: 0,
38
45
  message: "Identity tokens are issued only inside a Runtime sandbox (RUNTIME_ID_TOKEN_REQUEST_TOKEN is not set).",
39
46
  });
40
- const url = new URL(found.url);
41
- url.searchParams.set("audience", input.audience);
42
- if (input.lifetimeSeconds !== undefined)
43
- url.searchParams.set("lifetimeSeconds", String(input.lifetimeSeconds));
44
- const response = await fetch(url, {
45
- headers: { authorization: `Bearer ${found.token}` },
46
- signal: input.signal ?? AbortSignal.timeout(10_000),
47
- });
47
+ const ask = (from) => {
48
+ const url = new URL(from.url);
49
+ url.searchParams.set("audience", input.audience);
50
+ if (input.lifetimeSeconds !== undefined)
51
+ url.searchParams.set("lifetimeSeconds", String(input.lifetimeSeconds));
52
+ return fetch(url, {
53
+ headers: { authorization: `Bearer ${from.token}` },
54
+ signal: input.signal ?? AbortSignal.timeout(10_000),
55
+ });
56
+ };
57
+ let response = await ask(found);
58
+ // Refused for a request token that has since changed: a long-running
59
+ // process asks again with the current one from the guest's file (the hub's
60
+ // own, 16 October 2026, from its start until it was restarted).
61
+ if (response.status === 401 || response.status === 403) {
62
+ const file = await fileCredentials();
63
+ if (file?.url && file.token && file.token !== found.token)
64
+ response = await ask({ url: file.url, token: file.token });
65
+ }
48
66
  const body = (await response.json().catch(() => ({})));
49
67
  if (!response.ok || !body.token)
50
68
  throw new RuntimeError({
package/dist/index.d.ts CHANGED
@@ -9,11 +9,11 @@ import { type CreateOptions, type RuntimeOptions } from "./client.js";
9
9
  import { Sandbox as SandboxClass } from "./sandbox.js";
10
10
  import type { CreateSandbox, KeepAliveOptions } from "./types.js";
11
11
  import type { RequestOptions } from "./transport.js";
12
- export { Runtime, Sandboxes, FeedbackApi, SupportApi, type CreateOptions, type RuntimeOptions, type SupportReply, } from "./client.js";
12
+ export { Runtime, Sandboxes, FeedbackApi, SupportApi, type CreateOptions, type LifecycleOptions, type RuntimeOptions, type SupportReply, } from "./client.js";
13
13
  export { Files, Process, Processes, SandboxSessions, Terminal, type CreatedSandboxSession, type SandboxSession, } from "./sandbox.js";
14
14
  export { Tunnel, TunnelStream, type PortForward } from "./tunnel.js";
15
15
  export { Page } from "./page.js";
16
- export type { KeyLimits } from "./products/limits.js";
16
+ export type { AccountLimit, KeyLimits } from "./products/limits.js";
17
17
  export type { AuditEvent, AuditPage } from "./products/audit.js";
18
18
  export type { UsageExportQuery, UsageExportRow, UsageExportPage, } from "./products/observability.js";
19
19
  export type { JobSecret, Secret, SecretRule, SecretScope, SecretUse, SetSecret, } from "./products/secrets.js";
package/dist/machine.d.ts CHANGED
@@ -57,18 +57,21 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
57
57
  * answers the same. */
58
58
  delete(options?: RequestOptions): Promise<Deleted>;
59
59
  /** Carries on a paused sandbox; for one with a time limit,
60
- * `timeoutSeconds` is its new one. */
61
- wake(options?: RequestOptions & {
62
- wait?: boolean;
60
+ * `timeoutSeconds` is its new one. A wake stays on its server; when that
61
+ * server is full for now (`no_capacity`) it waits for room as `create`
62
+ * does, up to `waitForCapacityMs` (the client's, two minutes by default). */
63
+ wake(options?: LifecycleOptions & {
63
64
  timeoutSeconds?: number;
64
65
  }): Promise<this>;
65
- /** More time before its time limit ends (at most an hour ahead of now). A
66
+ /** More time before its time limit ends (at most 24 hours ahead of now). A
66
67
  * sandbox with no time limit needs none: the call answers at once and
67
- * changes nothing. */
68
+ * changes nothing. To set the end sooner, or give one with no limit a limit,
69
+ * `update({ timeoutSeconds })`. */
68
70
  extend(seconds: number, options?: RequestOptions): Promise<this>;
69
- /** Changes its name, labels, automatic wake, idle pause or persistence;
70
- * fields left out stay as they are. `persistent: true` keeps it running
71
- * while credit lasts (paid only). */
71
+ /** Changes its name, labels, automatic wake, idle pause, time limit, end
72
+ * action or persistence; fields left out stay as they are. `persistent:
73
+ * true` keeps it running while credit lasts (paid only). `timeoutSeconds`
74
+ * ends it that long from now, sooner or later than before (0: no limit). */
72
75
  update(settings: Settings, options?: RequestOptions): Promise<this>;
73
76
  /** Moves it to another image (id, name, name:tag or name@version), keeping
74
77
  * its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
@@ -116,10 +119,10 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
116
119
  * `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
117
120
  * the error names the snapshot. */
118
121
  snapshot(options?: SnapshotOptions & RequestOptions): Promise<Snapshot>;
119
- /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
120
- restart(options?: RequestOptions & {
121
- wait?: boolean;
122
- }): Promise<this>;
122
+ /** Starts a stopped persistent sandbox again from its disk (memory is not
123
+ * kept), waiting for room on a full server as `wake` does. Its image's
124
+ * start command runs again, reported in `info.start`. */
125
+ restart(options?: LifecycleOptions): Promise<this>;
123
126
  /** The end of an `await using` block. A sandbox this client created is
124
127
  * deleted, as E2B's block kills it, unless it was created with
125
128
  * `keep: true`; one it found with `get` or `getOrCreate` is stopped, which
@@ -137,6 +140,15 @@ export type CreateOptions = RequestOptions & {
137
140
  * next try: to tell a person why nothing has happened yet. */
138
141
  onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
139
142
  };
143
+ /** How `wake` and `restart` wait when the sandbox's server is full for now. */
144
+ export type LifecycleOptions = RequestOptions & {
145
+ wait?: boolean;
146
+ /** How long to keep retrying `no_capacity` and the like. Default: the
147
+ * client's, two minutes; 0 fails at once. */
148
+ waitForCapacityMs?: number;
149
+ /** Called before each wait, with the refusal and the milliseconds until the next try. */
150
+ onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
151
+ };
140
152
  /** A machine product's collection: `runtime.<product>`. */
141
153
  export declare abstract class Machines<M extends Machine<Info, object, Deleted>, Info extends MachineInfo, Create extends {
142
154
  name?: string;