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
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ Usage: runtime <product> <command> [options] runtime <command> [options]
24
24
 
25
25
  Start
26
26
  sandboxes run [--keep] [create options] -- <command>
27
- A fresh sandbox runs one command, then stops
27
+ A fresh sandbox runs one command, then is deleted
28
28
 
29
29
  Account
30
30
  login [--agent-name <name>] [--no-browser] [--wait]
@@ -88,6 +88,8 @@ Account
88
88
  Connect
89
89
  sandboxes ssh <id|name> [-- <command...>] Log in over SSH; \`runtime sandboxes ssh help\` for editors
90
90
  sandboxes ssh config [--install] Make \`ssh <id|name>.runtime\` work (VS Code, JetBrains)
91
+ sandboxes code <id|name> [--editor vscode|cursor|windsurf]
92
+ Open it in VS Code, Cursor or Windsurf over SSH
91
93
  sandboxes port-forward <id|name> <port|local:remote>...
92
94
  Forward local ports to ports in a sandbox
93
95
 
@@ -122,14 +124,17 @@ const SANDBOX_HELP = `runtime sandboxes <command>
122
124
 
123
125
  Every <id> below can also be the sandbox's name.
124
126
 
125
- run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
127
+ run [--keep] [create options] -- <command> A fresh sandbox runs one command, then is deleted
126
128
  ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
129
+ code <id|name> [--editor vscode|cursor|windsurf] [--path /workspace] [--print]
130
+ Open it in your editor over SSH, in one step
127
131
  port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
128
132
  create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
129
133
  [--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
130
134
  [--timeout <60-86400>] [--on-timeout pause|stop|delete] [--no-wait]
131
135
  [--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
132
136
  [--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
137
+ [--spread-from <id>]...
133
138
  [--idle-pause <seconds>] [--no-auto-wake] [--persistent] [--get-or-create]
134
139
  Create one; prints its id
135
140
  ls [--all] [--label k=v]... [--state running] List (live ones unless --all)
@@ -162,6 +167,7 @@ const SANDBOX_HELP = `runtime sandboxes <command>
162
167
  extend <id> <seconds> Move a time limit on (one with none needs nothing)
163
168
  update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
164
169
  [--persistent on|off] [--max-total-cost <usd>|none]
170
+ [--timeout <60-86400 from now>|0] [--on-timeout pause|stop|delete]
165
171
  Change its settings; persistent keeps it running until stopped
166
172
  snapshot <id> [--name n] [--retention days|credit] Keep its whole machine; prints the snapshot id
167
173
  mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
@@ -179,8 +185,9 @@ const SANDBOX_HELP = `runtime sandboxes <command>
179
185
  Print file changes as they happen, until Ctrl-C
180
186
  mcp catalog MCP servers a sandbox can run (licence, settings, hosts)
181
187
  mcp <id> start <server>... [--secret SERVER.SETTING=SECRET]... [--env SERVER.NAME=value]...
182
- [--option SERVER.NAME=value]... [--port 8765] [--replace]
188
+ [--option SERVER.NAME=value]... [--port 8765] [--replace] [--public]
183
189
  Start them; prints each URL and the header to send
190
+ (--public: URLs that never expire, paid only)
184
191
  mcp <id> [status] | mcp <id> stop Their state and fresh URLs; stop them
185
192
  preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address (--public: paid only)
186
193
  preview rotate <id> <port> Refuse every token given out for a private port so
@@ -254,7 +261,7 @@ commandAfter) {
254
261
  if (arg.startsWith("--") || (arg.startsWith("-") && arg.length === 2 && !/^-\d/.test(arg))) {
255
262
  const [typed, inline] = arg.replace(/^-+/, "").split(/=(.*)/s, 2);
256
263
  // A plural of an option is the option once per comma: --hosts a,b is
257
- // --host a --host b (13 October 2026: Marc's first try at a secret).
264
+ // --host a --host b (13 October 2026: a user's first try at a secret).
258
265
  const plural = !known.has(typed) &&
259
266
  typed.endsWith("s") &&
260
267
  LISTS.has(typed.slice(0, -1)) &&
@@ -428,6 +435,7 @@ const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
428
435
  const SANDBOX_VERBS = [
429
436
  "run",
430
437
  "ssh",
438
+ "code",
431
439
  "port-forward",
432
440
  "create",
433
441
  "ls",
@@ -585,12 +593,12 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
585
593
  out.error(`Warning: ${named(words.warning)}`);
586
594
  const [first, ...remaining] = words.argv;
587
595
  // Sandbox is one product of several, so its commands live under it
588
- // (AGENTS.md, 23 September 2026): `runtime sandboxes run`, `sandbox ssh` and
596
+ // (23 September 2026): `runtime sandboxes run`, `sandbox ssh` and
589
597
  // `sandbox port-forward`. The old top-level forms were removed, not aliased.
590
598
  if (first && SANDBOX_MOVED.includes(first))
591
599
  throw usage(`\`${me} ${first}\` is now \`${me} sandboxes ${first === "forward" ? "port-forward" : first}\`.`);
592
600
  const nested = (first === "sandbox" || first === "sandboxes" || first === "sbx") &&
593
- SANDBOX_MOVED.includes(remaining[0] ?? "");
601
+ [...SANDBOX_MOVED, "code"].includes(remaining[0] ?? "");
594
602
  const command = nested ? remaining[0] : first;
595
603
  const rest = nested ? remaining.slice(1) : remaining;
596
604
  // `runtime sandboxes create --help` is how an agent asks what a command takes:
@@ -627,7 +635,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
627
635
  : `Runtime Cloud ${VERSION}. ${start}
628
636
 
629
637
  ${me} sandboxes run -- python3 < invoice.py
630
- Run a local invoice script, then stop the sandbox
638
+ Run a local invoice script, then delete the sandbox
631
639
  ${me} sandboxes create A sandbox that stays; prints its id
632
640
  ${me} sandboxes exec <id> -- <command> Run in it
633
641
  ${me} help Everything else
@@ -649,24 +657,23 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
649
657
  const onCapacityWait = waiting(out);
650
658
  const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
651
659
  let cleanup;
652
- // SIGINT and normal completion share one stop. Keep an absolute deadline
653
- // across the stop retries and final state check, so cleanup cannot hang.
660
+ // SIGINT and normal completion share one delete, with an absolute
661
+ // deadline across its retries, so cleanup cannot hang. A delete, not a
662
+ // stop: a stopped sandbox keeps its disk, billed, and an account without
663
+ // credit keeps at most 10, so ten runs that only stopped refused the
664
+ // eleventh (no_credit_kept_limit, 0685). --keep is how to keep one.
654
665
  const stop = () => (cleanup ??= (async () => {
655
666
  const signal = AbortSignal.timeout(30_000);
656
667
  try {
657
- await sbx.stop({ wait: false, signal });
658
- if (sbx.state !== "stopped")
659
- await sbx.waitFor("stopped", { timeoutSeconds: 25, signal });
660
- if (sbx.state !== "stopped")
661
- throw new Error(`Sandbox is still ${sbx.state}.`);
668
+ await sbx.delete({ signal });
662
669
  return true;
663
670
  }
664
671
  catch (error) {
665
672
  out.error(describeError(new RuntimeError({
666
673
  code: "cleanup_failed",
667
674
  status: 0,
668
- message: `Could not confirm that sandbox ${sbx.id} stopped. ${error instanceof Error ? error.message : String(error)}`,
669
- hint: `Check it and retry cleanup with: ${me} sandboxes stop ${sbx.id}`,
675
+ message: `Could not confirm that sandbox ${sbx.id} was deleted. ${error instanceof Error ? error.message : String(error)}`,
676
+ hint: `Check it and retry cleanup with: ${me} sandboxes rm ${sbx.id}`,
670
677
  cause: error,
671
678
  }), out.json));
672
679
  return false;
@@ -1035,6 +1042,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
1035
1042
  product = "ssh";
1036
1043
  return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
1037
1044
  }
1045
+ if (nested && command === "code") {
1046
+ product = "ssh";
1047
+ return (await import("./ssh.js")).editorCommand(rest, env, out, () => client(env), me);
1048
+ }
1038
1049
  if (command === "port-forward" || command === "forward") {
1039
1050
  product = "ssh";
1040
1051
  return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
@@ -1852,6 +1863,8 @@ async function sandbox(argv, env, out) {
1852
1863
  "auto-wake",
1853
1864
  "persistent",
1854
1865
  "max-total-cost",
1866
+ "timeout",
1867
+ "on-timeout",
1855
1868
  ]);
1856
1869
  const sbx = await get(args.positional[0]);
1857
1870
  const onOff = (name) => {
@@ -1862,6 +1875,9 @@ async function sandbox(argv, env, out) {
1862
1875
  throw usage(`--${name} takes on or off.`);
1863
1876
  return value === "on";
1864
1877
  };
1878
+ const onTimeout = flag(args, "on-timeout")
1879
+ ? oneOf(args, "on-timeout", ["pause", "stop", "delete"])
1880
+ : undefined;
1865
1881
  const cost = flag(args, "max-total-cost");
1866
1882
  if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
1867
1883
  throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
@@ -1883,9 +1899,13 @@ async function sandbox(argv, env, out) {
1883
1899
  : {
1884
1900
  maxTotalCostMicros: cost === "none" ? null : usdMicros(args, "max-total-cost"),
1885
1901
  }),
1902
+ ...(integer(args, "timeout") !== undefined
1903
+ ? { timeoutSeconds: integer(args, "timeout") }
1904
+ : {}),
1905
+ ...(onTimeout === undefined ? {} : { onTimeout }),
1886
1906
  };
1887
1907
  if (!Object.keys(settings).length)
1888
- throw usage("Say what to change: --name, --label, --env, --unset-env, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
1908
+ throw usage("Say what to change: --name, --label, --env, --unset-env, --idle-pause, --auto-wake, --persistent, --max-total-cost, --timeout or --on-timeout.");
1889
1909
  await sbx.update(settings);
1890
1910
  print(describe(sbx.info), sbx.info);
1891
1911
  return 0;
@@ -2191,7 +2211,7 @@ function attachment(spec) {
2191
2211
  * A terminal: one line, not shown. */
2192
2212
  /** A pasted secret as the header it becomes: no line breaks (a token copied
2193
2213
  * from a terminal arrives wrapped), no bracketed-paste markers, no space at
2194
- * either end. 13 October 2026: Marc's pasted Claude token was refused as
2214
+ * either end. 13 October 2026: a user's pasted Claude token was refused as
2195
2215
  * "not visible ASCII". A header cannot hold a line break, so none is lost. */
2196
2216
  export function cleanSecret(raw) {
2197
2217
  return raw
@@ -2603,8 +2623,8 @@ Making, listing and revoking keys with a key needs one an owner or admin made.
2603
2623
  Every key is also at https://withruntime.com/account/keys.
2604
2624
  `;
2605
2625
  /* `runtime keys`: a key made from the terminal, by this machine's key with no
2606
- browser (Marc, 14 October 2026, agent swarms), or approved in the browser
2607
- (Marc, 23 September 2026; ARCHITECTURE.md section 10, "Keys from the
2626
+ browser (14 October 2026), or approved in the browser
2627
+ (23 September 2026; ARCHITECTURE.md section 10, "Keys from the
2608
2628
  terminal"). Only the options the keys page offers. */
2609
2629
  /* `runtime account`: the accounts this machine is connected to, and which one
2610
2630
  every command uses (ARCHITECTURE.md section 3.9). Each is its own saved
@@ -2787,7 +2807,7 @@ async function keys(argv, env, out) {
2787
2807
  dailyLimitMicros: limit === undefined ? null : dailyLimitMicros(limit),
2788
2808
  };
2789
2809
  /* A key on this machine makes the new one itself, with no browser: an
2790
- agent with no person, a swarm's coordinator (Marc, 14 October 2026). */
2810
+ agent with no person, a swarm's coordinator (14 October 2026). */
2791
2811
  const saved = has(args, "browser")
2792
2812
  ? null
2793
2813
  : env.RUNTIME_API_KEY || (await connectionStore(env).read())?.key || null;
@@ -3701,6 +3721,7 @@ const CREATE_VALUES = [
3701
3721
  "connect",
3702
3722
  "image",
3703
3723
  "snapshot",
3724
+ "spread-from",
3704
3725
  "volume",
3705
3726
  "env",
3706
3727
  ];
@@ -3728,6 +3749,18 @@ function createInput(args) {
3728
3749
  return {
3729
3750
  ...(flag(args, "image") ? { image: flag(args, "image") } : {}),
3730
3751
  ...(flag(args, "snapshot") ? { snapshot: flag(args, "snapshot") } : {}),
3752
+ ...(args.flags.has("spread-from")
3753
+ ? {
3754
+ spreadFrom: args.flags
3755
+ .get("spread-from")
3756
+ .flatMap((value) => value.split(","))
3757
+ .map((id) => {
3758
+ if (!id.trim())
3759
+ throw usage("--spread-from needs a non-empty value.");
3760
+ return id.trim();
3761
+ }),
3762
+ }
3763
+ : {}),
3731
3764
  ...(args.flags.has("volume") ? { volumes: args.flags.get("volume").map(attachment) } : {}),
3732
3765
  ...(has(args, "no-internet") || rules.length
3733
3766
  ? {
package/dist/client.d.ts CHANGED
@@ -6,7 +6,7 @@ import { Sandbox } from "./sandbox.js";
6
6
  import { Snapshots } from "./snapshots.js";
7
7
  import { Transport, type RequestOptions } from "./transport.js";
8
8
  import type { CreateSandbox, DeletedSandbox, FeedbackKind, SandboxInfo, Usage } from "./types.js";
9
- export type { CreateOptions } from "./machine.js";
9
+ export type { CreateOptions, LifecycleOptions } from "./machine.js";
10
10
  export type RuntimeOptions = {
11
11
  /** Default: the RUNTIME_API_KEY environment variable. */
12
12
  apiKey?: string;
@@ -40,5 +40,9 @@ export interface DaytonaConfig {
40
40
  export declare function pickKey(explicit?: string): string | undefined;
41
41
  /** The Runtime client for this configuration. */
42
42
  export declare function clientFor(config?: DaytonaConfig): Runtime;
43
+ /** Runtime runs in one US region: a Daytona target (the client's, or a
44
+ * create's own since @daytona/sdk 0.224) other than "us" is refused, and
45
+ * none or "" means that region. */
46
+ export declare function refuseTarget(target: string | undefined): void;
43
47
  /** Test hook: forget the cached clients. */
44
48
  export declare function resetClients(): void;
@@ -21,9 +21,7 @@ const clients = new Map();
21
21
  export function clientFor(config = {}) {
22
22
  if (config.jwtToken !== undefined || (config.organizationId !== undefined && !config.apiKey))
23
23
  throw new NotSupportedError("Daytona JWT sign-in (jwtToken, organizationId)", "Use a Runtime key: set RUNTIME_API_KEY, or run `npx withruntime login` once.");
24
- const target = config.target ?? env("DAYTONA_TARGET");
25
- if (target !== undefined && target !== "" && target !== "us")
26
- throw new NotSupportedError(`The Daytona target "${target}"`, 'Runtime runs in one US region; remove target or use "us".');
24
+ refuseTarget(config.target ?? env("DAYTONA_TARGET"));
27
25
  if (config.withruntime?.client)
28
26
  return config.withruntime.client;
29
27
  const key = pickKey(config.apiKey);
@@ -34,6 +32,13 @@ export function clientFor(config = {}) {
34
32
  }
35
33
  return client;
36
34
  }
35
+ /** Runtime runs in one US region: a Daytona target (the client's, or a
36
+ * create's own since @daytona/sdk 0.224) other than "us" is refused, and
37
+ * none or "" means that region. */
38
+ export function refuseTarget(target) {
39
+ if (target !== undefined && target !== "" && target !== "us")
40
+ throw new NotSupportedError(`The Daytona target "${target}"`, 'Runtime runs in one US region; remove target or use "us".');
41
+ }
37
42
  /** Test hook: forget the cached clients. */
38
43
  export function resetClients() {
39
44
  clients.clear();
@@ -62,6 +62,9 @@ export type CreateSandboxBaseParams = {
62
62
  spot?: boolean;
63
63
  linkedSandbox?: string;
64
64
  secrets?: Record<string, string>;
65
+ /** The region for this create, over the client's target: "us" (Runtime's
66
+ * one region) or none; any other is refused. */
67
+ target?: string;
65
68
  /** Runtime-only: an explicit client, or fields for this create. */
66
69
  withruntime?: WithRuntime;
67
70
  };
@@ -1,4 +1,4 @@
1
- import { clientFor, } from "./client.js";
1
+ import { clientFor, refuseTarget, } from "./client.js";
2
2
  import { DaytonaError, DaytonaInvalidArgumentError, DaytonaNotFoundError, DaytonaQueueTimeoutError, guard, NotSupportedError, } from "./errors.js";
3
3
  import { RuntimeError } from "../errors.js";
4
4
  import { LEASE_MAX_SECONDS } from "../compat/lease.js";
@@ -433,6 +433,7 @@ function refuseCreate(params) {
433
433
  gpuType !== "" &&
434
434
  (!Array.isArray(gpuType) || gpuType.length > 0)))
435
435
  throw new NotSupportedError("GPUs", "Runtime runs sandboxes on CPUs; remove gpu and gpuType from resources.");
436
+ refuseTarget(params.target);
436
437
  if (params.volumes?.some((mount) => mount.subpath))
437
438
  throw new NotSupportedError("Mounting part of a volume (subpath)", "Mount the whole volume.");
438
439
  if (params.kvm)
@@ -148,11 +148,12 @@ export declare class Sandbox {
148
148
  * deleted. */
149
149
  static kill(sandboxId: string, opts?: SandboxApiOpts): Promise<boolean>;
150
150
  kill(opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<boolean>;
151
- /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours, as E2B
152
- * does; the server keeps the deadline. Runtime cannot bring an end
153
- * sooner, so that is refused. A sandbox with no time limit (one on a pilot
154
- * account, kept running by the server) has no end to move, and nothing
155
- * changes. */
151
+ /** Sets the sandbox to end `timeoutMs` from now, sooner or later, up to 24
152
+ * hours, as E2B does: the server keeps the new time limit (an update's
153
+ * timeoutSeconds), and what the end does stays as created. Under a minute
154
+ * is a minute, Runtime's shortest limit. A sandbox with no time limit gets
155
+ * one; a persistent one (a pilot's, kept running by the server) has none to
156
+ * set, and nothing changes. */
156
157
  static setTimeout(sandboxId: string, timeoutMs: number, opts?: SandboxApiOpts): Promise<void>;
157
158
  setTimeout(timeoutMs: number, opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<void>;
158
159
  static getInfo(sandboxId: string, opts?: SandboxApiOpts): Promise<SandboxInfo>;
@@ -414,11 +414,12 @@ export class Sandbox {
414
414
  throw error;
415
415
  }
416
416
  }
417
- /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours, as E2B
418
- * does; the server keeps the deadline. Runtime cannot bring an end
419
- * sooner, so that is refused. A sandbox with no time limit (one on a pilot
420
- * account, kept running by the server) has no end to move, and nothing
421
- * changes. */
417
+ /** Sets the sandbox to end `timeoutMs` from now, sooner or later, up to 24
418
+ * hours, as E2B does: the server keeps the new time limit (an update's
419
+ * timeoutSeconds), and what the end does stays as created. Under a minute
420
+ * is a minute, Runtime's shortest limit. A sandbox with no time limit gets
421
+ * one; a persistent one (a pilot's, kept running by the server) has none to
422
+ * set, and nothing changes. */
422
423
  static async setTimeout(sandboxId, timeoutMs, opts = {}) {
423
424
  const client = clientFor(opts);
424
425
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
@@ -617,14 +618,10 @@ function secondsLater(runtime, timeoutMs) {
617
618
  return (Date.now() + timeoutMs - end) / 1000;
618
619
  }
619
620
  async function extendTo(runtime, timeoutMs, opts) {
620
- checkTimeout(timeoutMs);
621
- if (runtime.info.endsAt === null)
621
+ const seconds = timeoutSeconds(timeoutMs);
622
+ if (runtime.info.persistent === true)
622
623
  return;
623
- const later = secondsLater(runtime, timeoutMs);
624
- if (later < -1)
625
- throw new NotSupportedError("Ending a sandbox sooner than its time limit (a shorter setTimeout)", "Runtime cannot bring a time limit forward. Call kill() when the work is done.");
626
- if (later > 1)
627
- await guard("sandbox", () => runtime.extend(Math.ceil(later), request(opts)));
624
+ await guard("sandbox", () => runtime.update({ timeoutSeconds: seconds }, request(opts)));
628
625
  }
629
626
  async function pause(runtime, opts) {
630
627
  if (snapshotMemory("mode", opts.mode, opts.keepMemory) === false)
@@ -1,5 +1,5 @@
1
1
  import type { Runtime } from "../client.js";
2
- import type { Sandbox, Terminal } from "../sandbox.js";
2
+ import type { Sandbox } from "../sandbox.js";
3
3
  import type { Snapshot } from "../snapshots.js";
4
4
  import { CompatibilityError, type ClientOptions, type Refused, type RefusedAsync } from "../compat/core.js";
5
5
  export { CompatibilityError };
@@ -341,7 +341,30 @@ export interface PtyAttachOptions {
341
341
  session: PtySessionSelector;
342
342
  linuxUser?: string;
343
343
  }
344
+ export type PtyConnectionState = "connecting" | "connected" | "reconnecting" | "closed" | "exited";
345
+ export interface PtyReconnectOptions {
346
+ /** Total time to recover one dropped connection. Default: five minutes. */
347
+ timeoutMs?: number;
348
+ /** Initial backoff, with 30% jitter. Default: 400ms. */
349
+ initialDelayMs?: number;
350
+ /** Backoff ceiling before jitter. Default: 10 seconds. */
351
+ maxDelayMs?: number;
352
+ }
344
353
  export interface PtySessionEvents {
354
+ /** Reattach by itself after a dropped connection (Freestyle 0.2.18). */
355
+ reconnect?: boolean | PtyReconnectOptions;
356
+ /** Longest wait for each connection's terminal. Recovery waits at most 15
357
+ * seconds by default; a first connection, only when this is given. */
358
+ connectTimeoutMs?: number;
359
+ onStateChange?: (state: PtyConnectionState) => void;
360
+ /** Freestyle's exact replay boundaries. A Runtime terminal replays the
361
+ * session's output from its start on each connection without marking where
362
+ * the replay ends, as Freestyle's older guest agents do, so these never
363
+ * fire: the replay begins at the "connected" state. */
364
+ onReplayStart?: (info: {
365
+ exact: boolean;
366
+ }) => void;
367
+ onReplayEnd?: () => void;
345
368
  onData?: (data: Uint8Array) => void;
346
369
  onExit?: (exitCode: number) => void;
347
370
  onClose?: (info: {
@@ -353,25 +376,37 @@ export interface PtySessionEvents {
353
376
  }
354
377
  /** A session's number: FNV-1a of its process id, 31 bits, never 0. */
355
378
  export declare function sessionNumber(processId: string): number;
379
+ /** A stable handle across reattachments; input is never retried or buffered.
380
+ * Each connection is a Runtime terminal attached to the session's process. */
356
381
  export declare class PtySession {
357
382
  #private;
358
- private readonly terminal;
359
383
  private readonly sandbox;
360
384
  private readonly processId;
361
385
  readonly sessionId: number;
362
386
  readonly slug: string | undefined;
363
387
  readonly created: boolean;
364
- constructor(terminal: Terminal, sandbox: Sandbox, processId: string, sessionId: number, slug: string | undefined, created: boolean, events: PtySessionEvents);
365
- private readonly onError;
388
+ private readonly events;
389
+ /** Resolves once the first connection is up. */
390
+ readonly ready: Promise<void>;
391
+ constructor(sandbox: Sandbox, processId: string, sessionId: number, slug: string | undefined, created: boolean, events: PtySessionEvents, dimensions?: {
392
+ cols: number;
393
+ rows: number;
394
+ });
395
+ /** Retry now after a network change or a spent retry budget. Never opens a new shell. */
396
+ reconnect(): void;
397
+ /** Send input once. Throws while disconnected; uncertain keystrokes are never replayed. */
366
398
  write(data: Uint8Array | string): void;
399
+ /** Remembers the size and applies it again after a reattachment. */
367
400
  resize(options: {
368
401
  cols: number;
369
402
  rows: number;
370
403
  }): void;
371
404
  signal(sig: "sigint" | "sigkill"): void;
372
- /** Closes the connection; the shell keeps running and attach() finds it. */
405
+ /** Cancels recovery and closes the connection; the shell keeps running and
406
+ * attach() finds it. */
373
407
  detach(): void;
374
408
  get readyState(): number;
409
+ get state(): PtyConnectionState;
375
410
  }
376
411
  export declare class VmPty {
377
412
  private readonly get;
@@ -414,6 +449,43 @@ export declare class VmLinuxUserPty {
414
449
  exitCode?: undefined;
415
450
  }>;
416
451
  }
452
+ interface XtermDisposable {
453
+ dispose(): void;
454
+ }
455
+ interface XtermSequence {
456
+ prefix?: string;
457
+ intermediates?: string;
458
+ final: string;
459
+ }
460
+ /** The xterm.js surface connectXterm uses; xterm itself is not a dependency. */
461
+ export interface XtermTerminal {
462
+ cols: number;
463
+ rows: number;
464
+ write(data: string | Uint8Array, callback?: () => void): void;
465
+ onData(listener: (data: string) => void): XtermDisposable;
466
+ onResize(listener: (size: {
467
+ cols: number;
468
+ rows: number;
469
+ }) => void): XtermDisposable;
470
+ parser: {
471
+ registerCsiHandler(id: XtermSequence, handler: () => boolean): XtermDisposable;
472
+ registerDcsHandler(id: XtermSequence, handler: () => boolean): XtermDisposable;
473
+ };
474
+ }
475
+ export type ConnectXtermOptions = PtyOpenOptions & PtySessionEvents & {
476
+ pty: VmPty | VmLinuxUserPty;
477
+ terminal: XtermTerminal;
478
+ slug: string;
479
+ };
480
+ /** Freestyle 0.2.18's connectXterm: xterm.js on a named session that
481
+ * reattaches by itself unless `reconnect: false`. Each connection replays the
482
+ * session's output from its start, so the screen is reset first and the
483
+ * replay's terminal queries go unanswered until it has been written (150ms
484
+ * quiet, at most 2 seconds). dispose() detaches; the shell keeps running. */
485
+ export declare function connectXterm({ pty, terminal, ...options }: ConnectXtermOptions): Promise<{
486
+ session: PtySession;
487
+ dispose(): void;
488
+ }>;
417
489
  export declare class VmLinuxUser {
418
490
  private readonly vm;
419
491
  private readonly linuxUser;