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
@@ -18,9 +18,23 @@ export type KeyLimits = {
18
18
  /** Machine time the account can still run without credit, the same
19
19
  * figures as `usage.get()`'s `trial`, or null when it has none. */
20
20
  trial: Usage["trial"];
21
+ /** Every limit the account is held to, with what it uses of each now: the
22
+ * numbers a create past one is refused `quota_exceeded` by (its
23
+ * `details.limit` names the row). Null when they can't be read. */
24
+ account: AccountLimit[] | null;
21
25
  };
22
- /** `runtime.limits.get()`: whether this key is read-only, and what its agent
23
- * may still spend today. An owner sets the limit at
26
+ /** One of the account's limits, as https://withruntime.com/account/limits shows it. */
27
+ export type AccountLimit = {
28
+ /** `<kind>.<dimension>`: sandbox.count, sandbox.vcpu, sandbox.memory_mib, volume.count… */
29
+ limit: string;
30
+ label: string;
31
+ /** What `max` and `used` count: things, or MiB. */
32
+ unit: "count" | "mib";
33
+ max: number;
34
+ used: number;
35
+ };
36
+ /** `runtime.limits.get()`: whether this key is read-only, what its agent
37
+ * may still spend today, and the account's limits with what it uses of each. An owner sets the limit at
24
38
  * https://withruntime.com/account/keys; a key can read it, never change it.
25
39
  * Past it, a create, wake, extension or renewal fails with
26
40
  * `spending_limit_reached` (HTTP 402). */
@@ -1,5 +1,5 @@
1
- /** `runtime.limits.get()`: whether this key is read-only, and what its agent
2
- * may still spend today. An owner sets the limit at
1
+ /** `runtime.limits.get()`: whether this key is read-only, what its agent
2
+ * may still spend today, and the account's limits with what it uses of each. An owner sets the limit at
3
3
  * https://withruntime.com/account/keys; a key can read it, never change it.
4
4
  * Past it, a create, wake, extension or renewal fails with
5
5
  * `spending_limit_reached` (HTTP 402). */
@@ -38,6 +38,8 @@ export type McpServerRequest = {
38
38
  export type McpGateway = {
39
39
  running: boolean;
40
40
  port: number | null;
41
+ /** The URLs are a public preview: no link token, no expiry. */
42
+ public?: boolean;
41
43
  /** Send it as `Authorization: Bearer <token>`. */
42
44
  token?: string | null;
43
45
  headers?: Record<string, string>;
@@ -65,6 +67,9 @@ export declare function sandboxMcp(t: Transport, sandbox: Sandbox): {
65
67
  start: (servers: McpServerRequest[], input?: {
66
68
  port?: number;
67
69
  replace?: boolean;
70
+ /** URLs that never expire, guarded by the header alone, which is kept
71
+ * across starts until `stop()`. Paid accounts only. */
72
+ public?: boolean;
68
73
  }, options?: RequestOptions) => Promise<McpGateway>;
69
74
  /** Their state, and fresh URLs. */
70
75
  get: (options?: RequestOptions) => Promise<McpGateway>;
@@ -8,7 +8,10 @@ export type DnsRecord = {
8
8
  export type Domain = {
9
9
  id: string;
10
10
  hostname: string;
11
- sandboxId: string;
11
+ /** The sandbox it serves; null for an App's. */
12
+ sandboxId: string | null;
13
+ /** The App it serves, through every deploy and rollback; null for a sandbox's. */
14
+ appId?: string | null;
12
15
  port: number;
13
16
  /** pending until the TXT record proves the name is yours, then active. */
14
17
  state: "pending" | "active" | "released";
@@ -20,6 +23,13 @@ export type Domain = {
20
23
  verifiedAt: string | null;
21
24
  checkedAt: string | null;
22
25
  checkError: string | null;
26
+ /** An App's domain's certificate, issued before visitors arrive: point the
27
+ * name at the App once `ready` is true. Null for a sandbox's. */
28
+ certificate?: {
29
+ ready: boolean;
30
+ expiresAt: string | null;
31
+ issuers: string[];
32
+ } | null;
23
33
  createdAt: string;
24
34
  };
25
35
  export type CheckedDomain = Domain & {
@@ -97,12 +107,20 @@ export type TunnelPeerCreated = {
97
107
  /** `runtime.domains`: serve a sandbox's port at your own hostname, with HTTPS. */
98
108
  export declare function domains(t: Transport): {
99
109
  /** Claims the hostname and answers the DNS records to set. Calling it again
100
- * with another sandbox or port points the name there. */
110
+ * with another sandbox or port points the name there. With `appId`, an App
111
+ * serves it: add the `_acme-challenge` CNAME too, and point the name at
112
+ * the App once `certificate.ready` is true. */
101
113
  add: (input: {
102
114
  hostname: string;
115
+ } & ({
103
116
  sandboxId: string;
104
117
  port: number;
105
- }, options?: RequestOptions) => Promise<CheckedDomain>;
118
+ appId?: never;
119
+ } | {
120
+ appId: string;
121
+ sandboxId?: never;
122
+ port?: never;
123
+ }), options?: RequestOptions) => Promise<CheckedDomain>;
106
124
  /** Checks the TXT record now; the name goes live when it matches. */
107
125
  verify: (hostname: string, options?: RequestOptions) => Promise<CheckedDomain>;
108
126
  get: (hostname: string, options?: RequestOptions) => Promise<Domain>;
@@ -5,7 +5,9 @@ const enc = encodeURIComponent;
5
5
  export function domains(t) {
6
6
  return {
7
7
  /** Claims the hostname and answers the DNS records to set. Calling it again
8
- * with another sandbox or port points the name there. */
8
+ * with another sandbox or port points the name there. With `appId`, an App
9
+ * serves it: add the `_acme-challenge` CNAME too, and point the name at
10
+ * the App once `certificate.ready` is true. */
9
11
  add: (input, options) => t.json({ method: "POST", path: "/v1/domains", body: input, ...options }),
10
12
  /** Checks the TXT record now; the name goes live when it matches. */
11
13
  verify: (hostname, options) => t.json({
@@ -9,9 +9,12 @@ export type Preview = {
9
9
  /** `https://<port>-<sandbox>.<preview domain>/`. A private preview also
10
10
  * needs the token. */
11
11
  url: string;
12
- /** Private previews: a fresh signed token. Send it as the
13
- * `x-runtime-preview-token` header. */
12
+ /** Private previews: a token signed for this answer. Send it as the
13
+ * `x-runtime-preview-token` header. Every create and read signs its own;
14
+ * one signed before keeps working until its own expiry or a rotate. */
14
15
  token: string | null;
16
+ /** When this answer's token stops working. Not stored on the preview: a
17
+ * later read signs another token and leaves this one as it is. */
15
18
  tokenExpiresAt: string | null;
16
19
  /** Private previews: a link that carries the token once, for a browser. */
17
20
  urlWithToken: string | null;
@@ -48,7 +51,7 @@ export declare function sandboxPreviews(t: Transport, sandbox: Sandbox): {
48
51
  /** Shares `port`, or changes its visibility and embedOrigins if it is
49
52
  * shared already. */
50
53
  create: (port: number, input?: CreatePreview, options?: RequestOptions) => Promise<Preview>;
51
- /** Every shared port, each private one with a fresh token. */
54
+ /** Every shared port, each private one with a fresh token that lasts a day; tokens signed before keep their own expiry. */
52
55
  list: (options?: RequestOptions) => Promise<Preview[]>;
53
56
  /** One preview, with a fresh private token. `expiresAt` bounds its expiry
54
57
  * to an absolute ISO deadline; with no TTL, that deadline is used exactly. */
@@ -10,7 +10,7 @@ export function sandboxPreviews(t, sandbox) {
10
10
  /** Shares `port`, or changes its visibility and embedOrigins if it is
11
11
  * shared already. */
12
12
  create: (port, input = {}, options) => t.json({ method: "POST", path: base(), body: { port, ...input }, ...options }),
13
- /** Every shared port, each private one with a fresh token. */
13
+ /** Every shared port, each private one with a fresh token that lasts a day; tokens signed before keep their own expiry. */
14
14
  list: async (options) => (await t.json({ method: "GET", path: base(), ...options })).data,
15
15
  /** One preview, with a fresh private token. `expiresAt` bounds its expiry
16
16
  * to an absolute ISO deadline; with no TTL, that deadline is used exactly. */
@@ -37,6 +37,10 @@ export type Snapshot = {
37
37
  durableAt: string | null;
38
38
  storedBytes: number | null;
39
39
  };
40
+ /** An opaque tag for the server it is kept on, like `pl_3f9a0c1b2d4e5f60`.
41
+ * The same tag as a sandbox of yours means the same server. Null before it
42
+ * is placed. Absent from older servers. */
43
+ placement?: string | null;
40
44
  error: string | null;
41
45
  createdAt: string;
42
46
  readyAt: string | null;
package/dist/ssh.d.ts CHANGED
@@ -16,6 +16,41 @@ export declare function sshKey(env: NodeJS.ProcessEnv): Promise<{
16
16
  export declare function resolveSandbox(runtime: Runtime, target: string): Promise<Sandbox>;
17
17
  export declare function sshConfig(env: NodeJS.ProcessEnv): Promise<string>;
18
18
  export declare function sshCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
19
+ /** The editors `runtime sandboxes code` opens: each one's command line program
20
+ * and the URL scheme its app registers. All three are VS Code underneath and
21
+ * name an SSH remote the same way, `ssh-remote+<host>`. */
22
+ export declare const EDITORS: {
23
+ readonly vscode: {
24
+ readonly name: "VS Code";
25
+ readonly bin: "code";
26
+ readonly scheme: "vscode";
27
+ };
28
+ readonly cursor: {
29
+ readonly name: "Cursor";
30
+ readonly bin: "cursor";
31
+ readonly scheme: "cursor";
32
+ };
33
+ readonly windsurf: {
34
+ readonly name: "Windsurf";
35
+ readonly bin: "windsurf";
36
+ readonly scheme: "windsurf";
37
+ };
38
+ };
39
+ export type EditorName = keyof typeof EDITORS;
40
+ /** What opens a sandbox's folder in an editor over the `*.runtime` ssh host:
41
+ * the editor's own program and its arguments, or, without the program on the
42
+ * PATH, the link its app answers. */
43
+ export declare function editorLaunch(editor: EditorName, sandboxId: string, path: string): {
44
+ host: string;
45
+ bin: "code" | "cursor" | "windsurf";
46
+ args: string[];
47
+ link: string;
48
+ };
49
+ /** `runtime sandboxes code <id|name> [--editor cursor] [--path /workspace] [--print]`:
50
+ * a sandbox opened in VS Code, Cursor or Windsurf in one step. It makes sure
51
+ * the sandbox has an SSH server and this machine has the `*.runtime` ssh
52
+ * entry, then hands the editor the SSH remote. */
53
+ export declare function editorCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
19
54
  /** `runtime sandboxes port-forward <id> 5432 8080:3000 ...` */
20
55
  export declare function portForwardCommand(argv: string[], out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
21
56
  export {};
package/dist/ssh.js CHANGED
@@ -11,6 +11,9 @@ ${me} sandboxes ssh config [--install] The ssh config for \`ssh <id|nam
11
11
  VS Code Remote-SSH and JetBrains Gateway; --install adds
12
12
  it to ~/.ssh/config
13
13
  ${me} sandboxes ssh proxy <id|name>[.runtime] ProxyCommand mode: SSH on stdin and stdout
14
+ ${me} sandboxes code <id|name> [--editor vscode|cursor|windsurf] [--path /workspace] [--print]
15
+ Open it in your editor over SSH; installs the ssh config
16
+ above once. --print gives the link instead
14
17
  ${me} sandboxes port-forward <id|name> <port|local:remote>... [--address 127.0.0.1]
15
18
  Forward local ports to ports in the sandbox, until Ctrl-C
16
19
 
@@ -98,7 +101,7 @@ Host *.runtime
98
101
  async function installConfig(env) {
99
102
  const file = join(configDirectory(env), "ssh", "config");
100
103
  await writeFile(file, await sshConfig(env), { mode: 0o600 });
101
- const sshDir = join(homedir(), ".ssh");
104
+ const sshDir = join(env.HOME || homedir(), ".ssh");
102
105
  await mkdir(sshDir, { recursive: true, mode: 0o700 });
103
106
  const main = join(sshDir, "config");
104
107
  const current = await readFile(main, "utf8").catch(() => "");
@@ -236,6 +239,95 @@ function usageError(message, me) {
236
239
  hint: `Run \`${me} sandboxes ssh help\`.`,
237
240
  });
238
241
  }
242
+ /** The editors `runtime sandboxes code` opens: each one's command line program
243
+ * and the URL scheme its app registers. All three are VS Code underneath and
244
+ * name an SSH remote the same way, `ssh-remote+<host>`. */
245
+ export const EDITORS = {
246
+ vscode: { name: "VS Code", bin: "code", scheme: "vscode" },
247
+ cursor: { name: "Cursor", bin: "cursor", scheme: "cursor" },
248
+ windsurf: { name: "Windsurf", bin: "windsurf", scheme: "windsurf" },
249
+ };
250
+ /** What opens a sandbox's folder in an editor over the `*.runtime` ssh host:
251
+ * the editor's own program and its arguments, or, without the program on the
252
+ * PATH, the link its app answers. */
253
+ export function editorLaunch(editor, sandboxId, path) {
254
+ const { bin, scheme } = EDITORS[editor];
255
+ const remote = `ssh-remote+${sandboxId}.runtime`;
256
+ return {
257
+ host: `${sandboxId}.runtime`,
258
+ bin,
259
+ args: ["--remote", remote, path],
260
+ link: `${scheme}://vscode-remote/${remote}${path}`,
261
+ };
262
+ }
263
+ /** Runs a program, answering whether it started and exited 0; a program
264
+ * missing from the PATH is false, not an error. */
265
+ function launched(bin, args, env) {
266
+ return new Promise((resolve) => {
267
+ const child = spawn(bin, args, {
268
+ env,
269
+ stdio: "ignore",
270
+ // `code` and `cursor` are .cmd scripts on Windows.
271
+ shell: process.platform === "win32",
272
+ });
273
+ child.on("error", () => resolve(false));
274
+ child.on("exit", (code) => resolve(code === 0));
275
+ });
276
+ }
277
+ /** `runtime sandboxes code <id|name> [--editor cursor] [--path /workspace] [--print]`:
278
+ * a sandbox opened in VS Code, Cursor or Windsurf in one step. It makes sure
279
+ * the sandbox has an SSH server and this machine has the `*.runtime` ssh
280
+ * entry, then hands the editor the SSH remote. */
281
+ export async function editorCommand(argv, env, out, connect, me) {
282
+ const valueOf = (flag) => {
283
+ const at = argv.indexOf(flag);
284
+ return at === -1 ? undefined : argv[at + 1];
285
+ };
286
+ const editor = (valueOf("--editor") ?? "vscode").toLowerCase().replace(/^code$/, "vscode");
287
+ const path = valueOf("--path") ?? "/workspace";
288
+ const valued = new Set(["--editor", "--path"]);
289
+ const [target] = argv.filter((word, i) => !word.startsWith("--") && !valued.has(argv[i - 1] ?? ""));
290
+ if (!target || target === "help") {
291
+ out.write(SSH_HELP(me));
292
+ return target ? 0 : 64;
293
+ }
294
+ if (!(editor in EDITORS))
295
+ throw new RuntimeError({
296
+ message: `${editor} is not an editor this opens.`,
297
+ code: "usage",
298
+ status: 0,
299
+ hint: `Use --editor ${Object.keys(EDITORS).join(", --editor ")}; JetBrains Gateway connects to <id>.runtime after \`${me} sandboxes ssh config --install\`.`,
300
+ });
301
+ if (!path.startsWith("/"))
302
+ throw usageError(`--path is a folder in the sandbox, from /: ${path}`, me);
303
+ const sbx = await resolveSandbox(await connect(), target);
304
+ await ensureSshd(sbx, out);
305
+ await installConfig(env);
306
+ const how = editorLaunch(editor, sbx.id, path);
307
+ const { name } = EDITORS[editor];
308
+ const print = argv.includes("--print");
309
+ // Without the editor's program on the PATH (VS Code's `code` is opt-in on a
310
+ // Mac), the desktop opens the editor's link.
311
+ const [opener, ...openerArgs] = process.platform === "darwin"
312
+ ? ["open", how.link]
313
+ : process.platform === "win32"
314
+ ? ["cmd", "/c", "start", '""', how.link]
315
+ : ["xdg-open", how.link];
316
+ const opened = print
317
+ ? false
318
+ : (await launched(how.bin, how.args, env))
319
+ ? "program"
320
+ : (await launched(opener, openerArgs, env))
321
+ ? "link"
322
+ : false;
323
+ if (out.json)
324
+ out.write(JSON.stringify({ sandboxId: sbx.id, editor, host: how.host, path, link: how.link, opened }));
325
+ else if (opened)
326
+ out.write(`Opening ${path} of ${sbx.id} in ${name} over SSH (host ${how.host}). It needs ${name === "VS Code" ? "the Remote - SSH extension" : "its Remote - SSH extension"}; the first connection installs the editor's server in the sandbox.`);
327
+ else
328
+ out.write(`${print ? "" : `Could not start ${how.bin} or open a link here. `}Open this link on this machine:\n${how.link}\nor run: ${how.bin} ${how.args.join(" ")}`);
329
+ return opened || print ? 0 : 1;
330
+ }
239
331
  /** `runtime sandboxes port-forward <id> 5432 8080:3000 ...` */
240
332
  export async function portForwardCommand(argv, out, connect, me) {
241
333
  const at = argv.indexOf("--address");
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.14.0";
2
+ export declare const VERSION = "0.16.0";
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
@@ -54,14 +54,17 @@ export type Call = RequestOptions & {
54
54
  /** How long to keep retrying, with the same key and input, a refusal that
55
55
  * clears when a sandbox stops or a host frees room (WAITS_FOR_ROOM): a
56
56
  * full slots without credit, a full quota, a full region. Then the refusal is thrown as
57
- * it came. Only a create sets it; 0 or absent fails at once. */
57
+ * it came. A create, wake or restart sets it; 0 or absent fails at once. */
58
58
  waitForCapacityMs?: number;
59
59
  /** Called each time a call waits for room, with the refusal it is waiting
60
60
  * out and how long it will sleep before sending the call again. */
61
61
  onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
62
- /** Internal: when the call's own deadline ends (performance.now()), set
63
- * once the call is prepared; absent when it has none. */
64
- deadlineAt?: number;
62
+ /** Internal: when the call's own deadline ends now (performance.now()),
63
+ * set once the call is prepared; absent when it has none. */
64
+ deadlineAt?: () => number;
65
+ /** Internal: moves the call's deadline on by a wait for room, so a wait
66
+ * for capacity never spends the caller's own `timeoutMs`. */
67
+ extendDeadline?: (ms: number) => void;
65
68
  };
66
69
  /** How long one attempt waits for its answer to start. The API starts every
67
70
  * answer within 120 s: Caddy answers 503 itself past that, and a longer call
@@ -69,8 +72,8 @@ export type Call = RequestOptions & {
69
72
  * with no answer by then is talking to a server that is gone, such as a
70
73
  * controller that died with the call in flight while its address moved to
71
74
  * the new leader: a client that only waits sends nothing the new leader could
72
- * refuse, so it waited out the whole deadline and failed (the leader drill on
73
- * vin-5, 4 October 2026). It is sent again under the same key. */
75
+ * refuse, so it waited out the whole deadline and failed (a controller
76
+ * failover drill, 4 October 2026). It is sent again under the same key. */
74
77
  export declare const ANSWER_START_MS = 125000;
75
78
  export declare function apiOrigin(value: string): string;
76
79
  export declare function encodeQuery(query: Query | undefined): string;
@@ -90,7 +93,7 @@ export declare const DEFAULT_MAX_CONNECTIONS = 48;
90
93
  /** AbortSignal.timeout in whole milliseconds, rounded up, never under 1.
91
94
  * Node throws ERR_OUT_OF_RANGE on a fraction (Bun accepts one), so a
92
95
  * customer's timeoutMs of 1500.5 would be a RangeError instead of a request;
93
- * the live promise probe broke this way on 9 October 2026 (d3cd4bfbd). Every
96
+ * the live promise probe broke this way on 9 October 2026. Every
94
97
  * deadline the SDK makes from a customer's or a computed value goes through
95
98
  * this. */
96
99
  export declare function timeoutSignal(ms: number): AbortSignal;
package/dist/transport.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
3
  import { warningText } from "./product-renames.js";
4
- export const VERSION = "0.14.0";
4
+ export const VERSION = "0.16.0";
5
5
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
6
6
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
7
7
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -48,8 +48,8 @@ const NOTHING_RAN_MS = 300_000;
48
48
  * with no answer by then is talking to a server that is gone, such as a
49
49
  * controller that died with the call in flight while its address moved to
50
50
  * the new leader: a client that only waits sends nothing the new leader could
51
- * refuse, so it waited out the whole deadline and failed (the leader drill on
52
- * vin-5, 4 October 2026). It is sent again under the same key. */
51
+ * refuse, so it waited out the whole deadline and failed (a controller
52
+ * failover drill, 4 October 2026). It is sent again under the same key. */
53
53
  export const ANSWER_START_MS = 125_000;
54
54
  export function apiOrigin(value) {
55
55
  const url = new URL(value);
@@ -179,12 +179,36 @@ const KEPT_FOR_CALLS = 8;
179
179
  /** AbortSignal.timeout in whole milliseconds, rounded up, never under 1.
180
180
  * Node throws ERR_OUT_OF_RANGE on a fraction (Bun accepts one), so a
181
181
  * customer's timeoutMs of 1500.5 would be a RangeError instead of a request;
182
- * the live promise probe broke this way on 9 October 2026 (d3cd4bfbd). Every
182
+ * the live promise probe broke this way on 9 October 2026. Every
183
183
  * deadline the SDK makes from a customer's or a computed value goes through
184
184
  * this. */
185
185
  export function timeoutSignal(ms) {
186
186
  return AbortSignal.timeout(Math.max(1, Math.ceil(ms)));
187
187
  }
188
+ /** A deadline `ms` from now that `extend` moves later; it aborts with a
189
+ * TimeoutError, as AbortSignal.timeout does. */
190
+ function movableDeadline(ms) {
191
+ const controller = new AbortController();
192
+ let at = performance.now() + ms;
193
+ let timer;
194
+ const arm = () => {
195
+ timer = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), Math.max(1, Math.ceil(at - performance.now())));
196
+ // Never keeps a process alive by itself, as AbortSignal.timeout does not.
197
+ timer.unref?.();
198
+ };
199
+ arm();
200
+ return {
201
+ signal: controller.signal,
202
+ at: () => at,
203
+ extend: (by) => {
204
+ if (controller.signal.aborted || by <= 0)
205
+ return;
206
+ at += by;
207
+ clearTimeout(timer);
208
+ arm();
209
+ },
210
+ };
211
+ }
188
212
  export class Transport {
189
213
  baseUrl;
190
214
  #apiKey;
@@ -263,22 +287,29 @@ export class Transport {
263
287
  * signal through every retry and the complete response body. */
264
288
  #prepare(call) {
265
289
  const timeoutMs = call.timeoutMs ?? this.#timeoutMs;
266
- const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
267
- const deadline = timeoutMs === 0 ? undefined : timeoutSignal(timeoutMs + roomMs);
290
+ // The deadline is the caller's timeoutMs plus only the time actually
291
+ // spent waiting for room (WAITS_FOR_ROOM), which moves it on as it waits:
292
+ // a wake given timeoutMs 20 on a server that never answers still ends at
293
+ // 20 ms, not two minutes later (lifecycle-deadlines.test.ts).
294
+ const deadline = timeoutMs === 0 ? undefined : movableDeadline(timeoutMs);
268
295
  const signal = deadline
269
296
  ? call.signal
270
- ? AbortSignal.any([call.signal, deadline])
271
- : deadline
297
+ ? AbortSignal.any([call.signal, deadline.signal])
298
+ : deadline.signal
272
299
  : call.signal;
273
- return {
300
+ const prepared = {
274
301
  ...call,
275
302
  timeoutMs: 0,
276
- ...(timeoutMs === 0 ? {} : { deadlineAt: performance.now() + timeoutMs + roomMs }),
277
303
  ...(signal ? { signal } : {}),
278
304
  ...(call.method !== "GET"
279
305
  ? { idempotencyKey: call.idempotencyKey ?? crypto.randomUUID() }
280
306
  : {}),
281
307
  };
308
+ if (deadline) {
309
+ prepared.deadlineAt = deadline.at;
310
+ prepared.extendDeadline = deadline.extend;
311
+ }
312
+ return prepared;
282
313
  }
283
314
  #connectionError(call, cause) {
284
315
  const cancelled = call.signal?.aborted === true;
@@ -343,7 +374,9 @@ export class Transport {
343
374
  const retry = call.retry ?? true;
344
375
  let attempt = 0;
345
376
  // An answer that ran nothing is sent again until the call's deadline.
346
- const idleUntil = call.deadlineAt ?? performance.now() + NOTHING_RAN_MS;
377
+ // Read at each use: a wait for room moves the call's deadline on.
378
+ const idleFallback = performance.now() + NOTHING_RAN_MS;
379
+ const idleUntil = () => call.deadlineAt?.() ?? idleFallback;
347
380
  let idleAttempt = 0;
348
381
  // The last try never left, and was sent again at once.
349
382
  let resentAtOnce = false;
@@ -434,6 +467,7 @@ export class Transport {
434
467
  inCallback = true;
435
468
  call.onCapacityWait?.(error, pause);
436
469
  inCallback = false;
470
+ call.extendDeadline?.(pause);
437
471
  await this.#retryAfter(error, pause, signal, gate);
438
472
  continue;
439
473
  }
@@ -448,7 +482,7 @@ export class Transport {
448
482
  const header = Number(response.headers.get("retry-after"));
449
483
  const base = error.retryAfterMs ?? (Number.isFinite(header) && header > 0 ? header * 1000 : 250);
450
484
  const pause = Math.min(30_000, base * 2 ** Math.min(idleAttempt++, 3)) * (0.9 + Math.random() * 0.2);
451
- if (pause >= idleUntil - performance.now())
485
+ if (pause >= idleUntil() - performance.now())
452
486
  throw error;
453
487
  await this.#retryAfter(error, pause, signal, gate);
454
488
  continue;
package/dist/types.d.ts CHANGED
@@ -62,6 +62,11 @@ export type SandboxInfo = {
62
62
  /** The names of its own environment variables (`env`), on a create, an
63
63
  * update and a read of one sandbox. Values are never shown. */
64
64
  envNames?: string[];
65
+ /** On an update with labels by a key that may read secrets, when one of
66
+ * the account's egress secrets is scoped: true once every server running its sandboxes holds the change,
67
+ * so the next request uses the new scope; false while one has not
68
+ * confirmed (it takes it within 15 seconds). */
69
+ secretsEnforced?: boolean;
65
70
  /** On a create from an image with a start command: whether it started
66
71
  * (and was still running half a second later), became ready (readyMs),
67
72
  * timed out, or exited first (exitCode, and stderr's last lines). The create's answer
@@ -78,6 +83,11 @@ export type SandboxInfo = {
78
83
  };
79
84
  /** Present and true when getOrCreate answered a sandbox that already held the name. */
80
85
  reused?: boolean;
86
+ /** An opaque tag for the server it is on, like `pl_3f9a0c1b2d4e5f60`. Your
87
+ * sandboxes and snapshots with the same tag are on the same server. It is
88
+ * never the server's name, and other accounts see unrelated tags. Null
89
+ * before it is placed. Absent from older servers. */
90
+ placement?: string | null;
81
91
  [key: string]: unknown;
82
92
  };
83
93
  export type CreateSandbox = {
@@ -142,6 +152,13 @@ export type CreateSandbox = {
142
152
  image?: string;
143
153
  /** A ready snapshot's id: the sandbox starts as a copy of it, with its shape. Not with image. */
144
154
  snapshot?: string;
155
+ /** One to 16 ids of your own sandboxes or snapshots. The new sandbox starts
156
+ * on a server none of them is on, and stays apart if its start is moved.
157
+ * With no other server that has room, the create answers 409 `no_capacity`
158
+ * and nothing is made or charged. A copy of a snapshot starts only where that
159
+ * snapshot is kept, so a `snapshot` with `spreadFrom` naming its server also
160
+ * answers `no_capacity`. */
161
+ spreadFrom?: string[];
145
162
  /** Up to four volumes: read-write ("rw", one sandbox at a time) or a
146
163
  * read-only "snapshot" copy. */
147
164
  volumes?: {
@@ -430,6 +447,13 @@ export type SandboxSettings = {
430
447
  persistent?: boolean;
431
448
  /** Lifetime cap in microdollars; null removes it. */
432
449
  maxTotalCostMicros?: number | null;
450
+ /** A new time limit counted from now, 60 to 86400 seconds, sooner or later
451
+ * than the one it had; 0 takes the limit away. A paused sandbox counts it
452
+ * from its next wake. Not for a persistent sandbox. */
453
+ timeoutSeconds?: number;
454
+ /** What the end of its time limit does: pause, stop (keep the disk) or
455
+ * delete. */
456
+ onTimeout?: "pause" | "stop" | "delete";
433
457
  };
434
458
  /** A deleted sandbox, as `delete()` answers it the first time and every time
435
459
  * after. It reads nowhere else afterwards. */
@@ -1,3 +1,4 @@
1
+ import type { Readable } from "node:stream";
1
2
  import type { Process } from "../sandbox.js";
2
3
  /** One piece of a command's output, as Vercel's `logs()` yields it. */
3
4
  export type LogOutputLine = {
@@ -21,6 +22,8 @@ type Init = {
21
22
  sandboxName: string;
22
23
  process?: Process;
23
24
  lines?: LogOutputLine[];
25
+ /** Each line as it arrives, for an attached command's stdout/stderr. */
26
+ onLine?: (line: LogOutputLine) => void;
24
27
  };
25
28
  /** A command in a sandbox, as Vercel's `Command`. A detached one runs on:
26
29
  * `wait()` for its end, `logs()` for its output as it happens. `cmdId` is
@@ -30,6 +33,8 @@ export declare class Command {
30
33
  exitCode: number | null;
31
34
  durationMs?: number;
32
35
  protected readonly init: Init;
36
+ /** Set when the command was started with a `Readable` for stdin. */
37
+ stdinPipe: StdinPipe | null;
33
38
  constructor(init: Init);
34
39
  get cmdId(): string;
35
40
  get cwd(): string;
@@ -41,7 +46,8 @@ export declare class Command {
41
46
  }): AsyncGenerator<LogOutputLine, void, void> & Disposable & {
42
47
  close(): void;
43
48
  };
44
- /** Waits for the command to end. */
49
+ /** Waits for the command to end. With a `Readable` piped to its stdin,
50
+ * rejects if piping fails, and stops piping once the command exits. */
45
51
  wait(params?: {
46
52
  signal?: AbortSignal;
47
53
  }): Promise<CommandFinished>;
@@ -60,11 +66,52 @@ export declare class Command {
60
66
  kill(signal?: Signal, opts?: {
61
67
  abortSignal?: AbortSignal;
62
68
  }): Promise<void>;
69
+ /** Writes to the stdin of a command started with `stdin: true` and
70
+ * `detached: true`. Writes reach the process in call order and resolve once
71
+ * it has accepted the data; Runtime's input offsets make every resend exact,
72
+ * so no byte is written twice. If a write fails, part of it may have been
73
+ * written, and later writes are refused; closeStdin still works. */
74
+ writeStdin(data: string | Uint8Array, opts?: {
75
+ abortSignal?: AbortSignal;
76
+ }): Promise<void>;
77
+ /** Closes the command's stdin, so it reads EOF once it has consumed what
78
+ * was written. */
79
+ closeStdin(opts?: {
80
+ abortSignal?: AbortSignal;
81
+ }): Promise<void>;
63
82
  }
83
+ /** A `Readable` being written to a command's stdin. */
84
+ export interface StdinPipe {
85
+ /** Resolves once the stream has ended and stdin is closed, or the command
86
+ * stops accepting input; rejects if a write fails or the stream errors. */
87
+ done: Promise<void>;
88
+ /** Stops reading the stream, for when the command has exited. */
89
+ stop(): void;
90
+ }
91
+ /** Writes a stream to a command's stdin in order, closing stdin when it
92
+ * ends. What is read while a write is in flight goes in the next write, and
93
+ * reading pauses while too much waits. The stream is never ended or
94
+ * destroyed, and every listener is removed once piping stops. */
95
+ export declare function pipeStdin(command: Command, stream: Readable, signal?: AbortSignal): StdinPipe;
96
+ /** Waits for a command to finish, rejecting early if piping its stdin fails,
97
+ * and stops piping once it has exited. Aborting `signal` stops waiting but
98
+ * leaves the pipe running. */
99
+ export declare function waitWithStdinPipe<T>(finished: Promise<T>, pipe: StdinPipe | null, opts?: {
100
+ signal?: AbortSignal;
101
+ onPipeError?: () => void;
102
+ }): Promise<T>;
64
103
  /** A command that has ended; `exitCode` is set. */
65
104
  export declare class CommandFinished extends Command {
66
105
  exitCode: number;
67
106
  constructor(init: Init, exitCode: number, durationMs?: number);
68
107
  wait(): Promise<CommandFinished>;
108
+ /** Always throws: the command has already exited. */
109
+ writeStdin(_data: string | Uint8Array, _opts?: {
110
+ abortSignal?: AbortSignal;
111
+ }): Promise<never>;
112
+ /** Always throws: the command has already exited. */
113
+ closeStdin(_opts?: {
114
+ abortSignal?: AbortSignal;
115
+ }): Promise<never>;
69
116
  }
70
117
  export {};