withruntime 0.9.0 → 0.11.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 (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
+ import { once } from "node:events";
3
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
5
  import { dirname, join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
- import { connectionOrigins, connectionStore, resolveCredential } from "./credentials.js";
7
+ import { checkBrowserOrigins, connectionOrigins, connectionStore, resolveCredential, } from "./credentials.js";
7
8
  import { me, named } from "./cli-name.js";
8
9
  import { Runtime } from "./client.js";
9
10
  import { Sandbox as SandboxHandle } from "./sandbox.js";
@@ -36,6 +37,8 @@ Account
36
37
  sso Single sign-on and SCIM directory sync (owner or
37
38
  admin keys); an owner changes it on the website
38
39
  usage [--csv] Balance, trial time and charges; --csv, each resource
40
+ usage export --since <UTC time> --until <UTC time>
41
+ Every settlement in the range as CSV; --json, each page
39
42
  limits Whether this key is read-only, and its daily spending limit
40
43
  secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
41
44
  [--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
@@ -85,7 +88,7 @@ Observability
85
88
  Events and metrics to an OpenTelemetry endpoint
86
89
 
87
90
  Products
88
- sandbox Linux microVMs: exec, shell, files, code, previews, network, desktop
91
+ sandbox Linux microVMs: exec, shell, files, code, previews, network, desktop, browser
89
92
  snapshot Saved sandboxes, disk and memory, to start or fork from
90
93
  image Custom images from a Dockerfile, any public or private image, or a package list
91
94
  volume Persistent disks to attach to sandboxes
@@ -94,7 +97,7 @@ Products
94
97
  port A public TCP port to a sandbox: databases, game servers
95
98
  address A dedicated outbound address, for allow-lists
96
99
  tunnel A WireGuard tunnel from your own network into your sandboxes
97
- network Account-wide network settings: send outbound traffic through your own proxy
100
+ network Account-wide network settings: your own proxy, sandboxes reaching each other by name
98
101
  Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
99
102
 
100
103
  Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
@@ -128,11 +131,15 @@ const SANDBOX_HELP = `runtime sandbox <command>
128
131
  cat <id> <path> Print a file
129
132
  files <id> [path] [--depth 1] [--glob '**/*.py']
130
133
  stop <id> | pause <id> | wake <id> | restart <id>
134
+ stop --label k=v [--label k=v]... Stop every live sandbox with all those labels
131
135
  rm <id> Delete it for good: stops it, deletes its disk and
132
136
  paused memory, revokes its previews; snapshots stay
133
137
  switch-image <id> <image> --keep-workspace Move it to another image, keeping /workspace (your home),
134
138
  volumes, env and previews; processes restart, the rest of
135
139
  its disk is lost (snapshot it first to keep everything)
140
+ resize <id> [--vcpu 2] [--memory 4096] --restart
141
+ Change its size by a restart: the whole disk stays,
142
+ programs stop (snapshot it first to keep memory)
136
143
  extend <id> <seconds> More time before the lease ends
137
144
  update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
138
145
  [--persistent on|off] [--max-total-cost <usd>|none]
@@ -177,11 +184,13 @@ const SANDBOX_HELP = `runtime sandbox <command>
177
184
  | type <text> | press <keys> | windows
178
185
  desktop <id> record start [--fps 10] [--max-seconds 1800] [--max-mib 512] | record stop <recId>
179
186
  | record ls | record fetch <recId> [file.mp4] | record rm <recId>
187
+ browser <id> start [--headed] [--width 1280] [--height 800] | get | stop
188
+ Chromium for CDP agents: prints its cdpUrl
180
189
 
181
190
  Examples
182
191
  id=$(runtime sandbox create)
183
- runtime sandbox exec $id -- python3 -c 'print(6*7)'
184
- runtime sandbox cp ./project $id:/workspace/project
192
+ runtime sandbox cp ./invoice.py $id:/workspace/invoice.py
193
+ runtime sandbox exec $id -- python3 /workspace/invoice.py
185
194
  runtime sandbox stop $id
186
195
  `;
187
196
  /** A command's options. Reading one its parse() did not declare is a bug in
@@ -229,9 +238,13 @@ commandAfter) {
229
238
  throw commandAfter && !meantOption(name, known)
230
239
  ? usage(`Unknown option ${arg.split("=")[0]}. If it belongs to the command, put the command after --: ${commandAfter}`)
231
240
  : unknownOption(name, known);
232
- const value = inline ?? (booleans.includes(name) ? "true" : argv[++i]);
233
- if (value === undefined)
241
+ const boolean = booleans.includes(name);
242
+ const value = inline ?? (boolean ? "true" : argv[++i]);
243
+ if (value === undefined ||
244
+ (!boolean && inline === undefined && (value === "--" || /^--?[^\d]/.test(value))))
234
245
  throw usage(`--${name} needs a value.`);
246
+ if (boolean && value !== "true" && value !== "false")
247
+ throw usage(`--${name} takes true or false.`);
235
248
  flags.set(name, [...(flags.get(name) ?? []), value]);
236
249
  }
237
250
  else
@@ -263,7 +276,10 @@ const integer = (args, name) => {
263
276
  return undefined;
264
277
  if (!/^\d+$/.test(value))
265
278
  throw usage(`--${name} must be a whole number.`);
266
- return Number(value);
279
+ const parsed = Number(value);
280
+ if (!Number.isSafeInteger(parsed))
281
+ throw usage(`--${name} is too large to represent exactly.`);
282
+ return parsed;
267
283
  };
268
284
  /** A flag that takes one of a few words, checked before anything is sent. */
269
285
  function oneOf(args, name, choices) {
@@ -400,6 +416,7 @@ const SANDBOX_VERBS = [
400
416
  "rm",
401
417
  "delete",
402
418
  "switch-image",
419
+ "resize",
403
420
  "pause",
404
421
  "wake",
405
422
  "restart",
@@ -427,6 +444,7 @@ const SANDBOX_VERBS = [
427
444
  "session",
428
445
  "network",
429
446
  "desktop",
447
+ "browser",
430
448
  "metrics",
431
449
  "watch",
432
450
  "mcp",
@@ -575,8 +593,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
575
593
  ? JSON.stringify({ connected, usage: HELP })
576
594
  : `Runtime Cloud ${VERSION}. ${start}
577
595
 
578
- ${me} sandbox run -- python3 -c 'print(6*7)'
579
- A fresh sandbox runs one command, then stops
596
+ ${me} sandbox run -- python3 < invoice.py
597
+ Run a local invoice script, then stop the sandbox
580
598
  ${me} sandbox create A sandbox that stays; prints its id
581
599
  ${me} sandbox exec <id> -- <command> Run in it
582
600
  ${me} help Everything else
@@ -588,10 +606,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
588
606
  return 0;
589
607
  }
590
608
  if (command === "run") {
591
- const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 -c 'print(6*7)'`);
609
+ const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 < invoice.py`);
592
610
  const commandLine = args.rest ?? args.positional;
593
611
  if (!commandLine.length)
594
- throw usage(`Give the command after --: ${me} sandbox run -- python3 -c 'print(6*7)'`);
612
+ throw usage(`Give the command after --: ${me} sandbox run -- python3 < invoice.py`);
595
613
  const runtime = await client(env);
596
614
  const timeout = integer(args, "timeout");
597
615
  const { timeoutSeconds: _lease, ...input } = createInput(args);
@@ -688,6 +706,40 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
688
706
  return 0;
689
707
  }
690
708
  if (command === "usage") {
709
+ if (rest[0] === "export") {
710
+ const args = parse(rest.slice(1), [], ["since", "until"]);
711
+ if (args.positional.length || args.rest)
712
+ throw usage(`Use ${me} usage export --since <UTC time> --until <UTC time>.`);
713
+ const since = need(flag(args, "since"), "--since <UTC time>, such as 2026-09-01T00:00:00Z");
714
+ const until = need(flag(args, "until"), "--until <UTC time>, such as 2026-10-01T00:00:00Z");
715
+ const rt = await client(env);
716
+ const cursors = new Set();
717
+ let cursor;
718
+ for (;;) {
719
+ const page = await rt.usageExport({
720
+ since,
721
+ until,
722
+ ...(cursor ? { cursor } : {}),
723
+ limit: 500,
724
+ });
725
+ if (page.nextCursor !== null && cursors.has(page.nextCursor))
726
+ throw new RuntimeError({
727
+ code: "usage_export_incomplete",
728
+ status: 0,
729
+ message: "The usage export repeated a page cursor and could not finish.",
730
+ hint: "Run the export again; do not treat the partial output as complete.",
731
+ });
732
+ if (out.json)
733
+ out.write(JSON.stringify(page));
734
+ else if (page.csv)
735
+ out.write(page.csv);
736
+ await out.drain?.();
737
+ if (page.nextCursor === null)
738
+ return 0;
739
+ cursors.add(page.nextCursor);
740
+ cursor = page.nextCursor;
741
+ }
742
+ }
691
743
  const csv = rest.includes("--csv");
692
744
  const unknown = rest.find((arg) => arg !== "--csv");
693
745
  if (unknown)
@@ -738,7 +790,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
738
790
  const page = await rt.events.list({
739
791
  ...(sandbox ? { resourceId: await sandboxIdOf(rt, sandbox) } : {}),
740
792
  ...(flag(args, "type") ? { type: flag(args, "type") } : {}),
741
- ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
793
+ ...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
742
794
  });
743
795
  print(page.data.length
744
796
  ? table(page.data.map((e) => {
@@ -783,7 +835,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
783
835
  throw usage(`Unknown option --${name}.`);
784
836
  const page = await (await client(env)).audit.list({
785
837
  ...(flag(args, "action") ? { action: flag(args, "action") } : {}),
786
- ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
838
+ ...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
787
839
  ...(flag(args, "before") ? { before: flag(args, "before") } : {}),
788
840
  });
789
841
  print(page.events.length
@@ -809,17 +861,26 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
809
861
  const origins = connectionOrigins(env);
810
862
  const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
811
863
  return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
812
- open: (input) => openWalletAccount(input, { authUrl: origins.auth }),
813
- claim: (code) => claimWalletAccount(code, { authUrl: origins.auth }),
814
- save: (key, orgId) => connectionStore(env).save({
815
- version: 1,
816
- apiOrigin: origins.api,
817
- authOrigin: origins.auth,
818
- key,
819
- connectionId: "pasted",
820
- orgId,
821
- agentName: "Wallet agent",
822
- }),
864
+ open: (input) => {
865
+ checkBrowserOrigins(origins);
866
+ return openWalletAccount(input, { authUrl: origins.auth });
867
+ },
868
+ claim: (code) => {
869
+ checkBrowserOrigins(origins);
870
+ return claimWalletAccount(code, { authUrl: origins.auth });
871
+ },
872
+ save: (key, orgId) => {
873
+ checkBrowserOrigins(origins);
874
+ return connectionStore(env).save({
875
+ version: 1,
876
+ apiOrigin: origins.api,
877
+ authOrigin: origins.auth,
878
+ key,
879
+ connectionId: "pasted",
880
+ orgId,
881
+ agentName: "Wallet agent",
882
+ });
883
+ },
823
884
  });
824
885
  }
825
886
  if (command === "referrals") {
@@ -1442,6 +1503,21 @@ async function sandbox(argv, env, out) {
1442
1503
  print(describe(sbx.info), sbx.info);
1443
1504
  return 0;
1444
1505
  }
1506
+ case "resize": {
1507
+ const args = parse(rest, ["restart"], ["vcpu", "memory"]);
1508
+ const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me} sandbox resize <id> --memory 4096 --restart`));
1509
+ const size = {
1510
+ ...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
1511
+ ...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
1512
+ };
1513
+ if (size.vcpu === undefined && size.memoryMiB === undefined)
1514
+ throw usage(`Name a new size, e.g. ${me} sandbox resize ${sbx.id} --memory 4096 --restart`);
1515
+ if (!args.flags.has("restart"))
1516
+ throw usage(`A resize restarts it: its whole disk stays and its programs stop. Add --restart to say so, or snapshot it first (${me} sandbox snapshot ${sbx.id}) to keep its memory too.`);
1517
+ await sbx.resize(size, { restart: true });
1518
+ print(describe(sbx.info), sbx.info);
1519
+ return 0;
1520
+ }
1445
1521
  case "rm":
1446
1522
  case "delete": {
1447
1523
  const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandbox rm <id>`));
@@ -1453,6 +1529,23 @@ async function sandbox(argv, env, out) {
1453
1529
  case "pause":
1454
1530
  case "wake":
1455
1531
  case "restart": {
1532
+ if (verb === "stop" && rest.some((word) => /^--label(=|$)/.test(word))) {
1533
+ const args = parse(rest, [], ["label"]);
1534
+ if (args.positional.length)
1535
+ throw usage("Give a sandbox id or --label, not both.");
1536
+ const { stopped, failed } = await runtime.sandboxes.stopAll({
1537
+ labels: pairs(args, "label"),
1538
+ });
1539
+ const said = (error) => (error instanceof Error ? error.message : String(error));
1540
+ print([
1541
+ stopped.length || failed.length
1542
+ ? `${stopped.length} stopped${failed.length ? `, ${failed.length} failed` : ""}.`
1543
+ : "No live sandbox has those labels.",
1544
+ ...stopped.map((id) => `${id} stopped`),
1545
+ ...failed.map((f) => `${f.id} failed: ${said(f.error)}`),
1546
+ ].join("\n"), { stopped, failed: failed.map((f) => ({ id: f.id, error: said(f.error) })) });
1547
+ return failed.length ? 1 : 0;
1548
+ }
1456
1549
  const sbx = await get(rest[0]);
1457
1550
  await sbx[verb]();
1458
1551
  print(`${sbx.id} ${sbx.state}`, sbx.info);
@@ -1536,7 +1629,9 @@ async function sandbox(argv, env, out) {
1536
1629
  const sbx = await get(args.positional[0]);
1537
1630
  const snapshot = await sbx.snapshot({
1538
1631
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
1539
- ...(integer(args, "retention") ? { retentionDays: integer(args, "retention") } : {}),
1632
+ ...(integer(args, "retention") !== undefined
1633
+ ? { retentionDays: integer(args, "retention") }
1634
+ : {}),
1540
1635
  });
1541
1636
  print(snapshot.id, snapshot);
1542
1637
  return 0;
@@ -1596,7 +1691,7 @@ async function sandbox(argv, env, out) {
1596
1691
  ...(cost === undefined
1597
1692
  ? {}
1598
1693
  : {
1599
- maxTotalCostMicros: cost === "none" ? null : Math.round(Number(cost) * 1e6),
1694
+ maxTotalCostMicros: cost === "none" ? null : usdMicros(args, "max-total-cost"),
1600
1695
  }),
1601
1696
  };
1602
1697
  if (!Object.keys(settings).length)
@@ -1709,7 +1804,7 @@ async function sandbox(argv, env, out) {
1709
1804
  const args = parse(rest, [], ["depth", "glob"]);
1710
1805
  const sbx = await use(args.positional[0]);
1711
1806
  const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
1712
- ...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
1807
+ ...(integer(args, "depth") !== undefined ? { depth: integer(args, "depth") } : {}),
1713
1808
  ...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
1714
1809
  });
1715
1810
  print(entries
@@ -1752,7 +1847,7 @@ async function sandbox(argv, env, out) {
1752
1847
  const sbx = await use(args.positional[0]);
1753
1848
  const preview = await sbx.previews.create(port, {
1754
1849
  ...(has(args, "public") ? { visibility: "public" } : {}),
1755
- ...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
1850
+ ...(integer(args, "ttl") !== undefined ? { ttlSeconds: integer(args, "ttl") } : {}),
1756
1851
  });
1757
1852
  // The link and how to use it, nothing more: the API's `hint` (which
1758
1853
  // carries a referral line) stays in --json, not in the command's words.
@@ -1838,6 +1933,8 @@ async function sandbox(argv, env, out) {
1838
1933
  }
1839
1934
  case "desktop":
1840
1935
  return desktop(await use(rest[0]), rest.slice(1), out);
1936
+ case "browser":
1937
+ return browser(await use(rest[0]), rest.slice(1), out);
1841
1938
  case "watch":
1842
1939
  return (await import("./cli-extras.js")).watchCommand(await use(rest[0]), rest.slice(1), out, kit);
1843
1940
  case "mcp":
@@ -2084,7 +2181,7 @@ async function runCode(sbx, argv, out) {
2084
2181
  const execution = await sbx.interpreter.run(code, {
2085
2182
  language,
2086
2183
  ...(flag(args, "context") ? { context: flag(args, "context") } : {}),
2087
- ...(timeout ? { timeoutMs: timeout * 1000 } : {}),
2184
+ ...(timeout !== undefined ? { timeoutMs: timeout * 1000 } : {}),
2088
2185
  ...(out.json
2089
2186
  ? {}
2090
2187
  : {
@@ -2133,8 +2230,8 @@ async function desktop(sbx, argv, out) {
2133
2230
  case "start": {
2134
2231
  const args = parse(rest, [], ["width", "height"]);
2135
2232
  const started = await d.start({
2136
- ...(integer(args, "width") ? { width: integer(args, "width") } : {}),
2137
- ...(integer(args, "height") ? { height: integer(args, "height") } : {}),
2233
+ ...(integer(args, "width") !== undefined ? { width: integer(args, "width") } : {}),
2234
+ ...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
2138
2235
  });
2139
2236
  print(`Desktop ${started.size} on ${started.display}. Watch it: ${started.streamUrl}`, started);
2140
2237
  return 0;
@@ -2172,6 +2269,35 @@ async function desktop(sbx, argv, out) {
2172
2269
  }
2173
2270
  throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
2174
2271
  }
2272
+ async function browser(sbx, argv, out) {
2273
+ const [action, ...rest] = argv;
2274
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2275
+ switch (action) {
2276
+ case "start": {
2277
+ const args = parse(rest, ["headed"], ["width", "height"]);
2278
+ const started = await sbx.browser.start({
2279
+ ...(args.flags.has("headed") ? { headless: false } : {}),
2280
+ ...(integer(args, "width") !== undefined ? { width: integer(args, "width") } : {}),
2281
+ ...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
2282
+ });
2283
+ print(`${started.version}${started.headless ? "" : `, on the desktop: ${started.streamUrl}`}\n` +
2284
+ `CDP: ${started.cdpUrl}\n(private; it expires ${started.expiresAt ?? "never"})`, started);
2285
+ return 0;
2286
+ }
2287
+ case "get":
2288
+ case undefined: {
2289
+ const state = await sbx.browser.get();
2290
+ print(state.running ? `${state.version}\nCDP: ${state.cdpUrl}` : "No browser running.", state);
2291
+ return 0;
2292
+ }
2293
+ case "stop": {
2294
+ const stopped = await sbx.browser.stop();
2295
+ print(stopped.stopped ? "Browser stopped." : "No browser was running.", stopped);
2296
+ return 0;
2297
+ }
2298
+ }
2299
+ throw usage(`${me} sandbox browser <id> start [--headed]|get|stop`);
2300
+ }
2175
2301
  const KEYS_HELP = `runtime keys <command>
2176
2302
 
2177
2303
  create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
@@ -2473,9 +2599,13 @@ read = readSecret) {
2473
2599
  if (sources.filter((s) => s !== undefined).length !== 1)
2474
2600
  throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
2475
2601
  const build = {
2476
- ...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
2477
- ...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
2478
- ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
2602
+ ...(integer(args, "max-image-mib") !== undefined
2603
+ ? { maxImageMiB: integer(args, "max-image-mib") }
2604
+ : {}),
2605
+ ...(integer(args, "disk-mib") !== undefined ? { diskMiB: integer(args, "disk-mib") } : {}),
2606
+ ...(integer(args, "timeout") !== undefined
2607
+ ? { timeoutSeconds: integer(args, "timeout") }
2608
+ : {}),
2479
2609
  };
2480
2610
  // -t app, -t app:v2 -t app:latest: one name, its tags.
2481
2611
  let name = flag(args, "name");
@@ -2492,9 +2622,11 @@ read = readSecret) {
2492
2622
  }
2493
2623
  const start = {
2494
2624
  ...(flag(args, "start") ? { command: flag(args, "start") } : {}),
2495
- ...(integer(args, "ready-port") ? { readyPort: integer(args, "ready-port") } : {}),
2625
+ ...(integer(args, "ready-port") !== undefined
2626
+ ? { readyPort: integer(args, "ready-port") }
2627
+ : {}),
2496
2628
  ...(flag(args, "ready-command") ? { readyCommand: flag(args, "ready-command") } : {}),
2497
- ...(integer(args, "ready-timeout")
2629
+ ...(integer(args, "ready-timeout") !== undefined
2498
2630
  ? { readyTimeoutSeconds: integer(args, "ready-timeout") }
2499
2631
  : {}),
2500
2632
  };
@@ -2758,23 +2890,29 @@ async function jobCommand(argv, env, out) {
2758
2890
  ? { cron, ...(flag(args, "timezone") ? { timezone: flag(args, "timezone") } : {}) }
2759
2891
  : { at: atTime(at) },
2760
2892
  command: cwd ? { argv, cwd } : argv,
2761
- ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
2893
+ ...(integer(args, "timeout") !== undefined
2894
+ ? { timeoutSeconds: integer(args, "timeout") }
2895
+ : {}),
2762
2896
  compute: {
2763
- ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
2764
- ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
2765
- ...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
2897
+ ...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
2898
+ ...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
2899
+ ...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
2766
2900
  ...(flag(args, "cpu")
2767
2901
  ? { cpuMode: oneOf(args, "cpu", ["shared", "reserved"]) }
2768
2902
  : {}),
2769
- ...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
2903
+ ...(integer(args, "cpu-floor") !== undefined
2904
+ ? { cpuFloorMillis: integer(args, "cpu-floor") }
2905
+ : {}),
2770
2906
  ...(flag(args, "region") ? { region: flag(args, "region") } : {}),
2771
- ...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
2907
+ ...(usdMicros(args, "max-cost") !== undefined
2908
+ ? { maxCostMicros: usdMicros(args, "max-cost") }
2909
+ : {}),
2772
2910
  },
2773
2911
  ...(attempts !== undefined
2774
2912
  ? { retry: { maxAttempts: attempts, backoffSeconds: backoff ?? 30 } }
2775
2913
  : {}),
2776
2914
  ...(secrets ? { secrets } : {}),
2777
- ...(usdMicros(args, "max-total-cost")
2915
+ ...(usdMicros(args, "max-total-cost") !== undefined
2778
2916
  ? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
2779
2917
  : {}),
2780
2918
  });
@@ -2903,9 +3041,13 @@ async function jobCommand(argv, env, out) {
2903
3041
  }
2904
3042
  const VOLUME_HELP = `runtime volume <command>
2905
3043
 
2906
- create --size-mib <N> [--name n] A persistent ext4 disk; prints its id
3044
+ create --size-mib <N> [--name n] [--shared] A persistent disk; prints its id
2907
3045
  ls Your volumes
2908
3046
  get <id> One volume, its backups and where it is attached
3047
+ resize <id> --size-mib <N> Grow a detached volume on its current host
3048
+ attach <id> <sandboxId> --path /data Mount a shared disk in a running sandbox
3049
+ detach <id> <attachmentId> Flush and unmount its shared attachment
3050
+ attachment <id> <attachmentId> Read the attachment's state and error
2909
3051
  rm <id> Delete it and everything on it (its backups stay)
2910
3052
  backup <id> [--retention-days N] [--no-wait]
2911
3053
  Back it up now, off its host; prints the backup id
@@ -2916,6 +3058,7 @@ const VOLUME_HELP = `runtime volume <command>
2916
3058
  Daily backups on or off, and how long each is kept
2917
3059
  Attach one: runtime sandbox create --volume <id>:/data
2918
3060
  Volumes are backed up off their host daily unless you turn it off.
3061
+ Shared commands work where shared disks are enabled.
2919
3062
  `;
2920
3063
  async function volume(argv, env, out) {
2921
3064
  const [verb, ...rest] = argv;
@@ -2927,10 +3070,11 @@ async function volume(argv, env, out) {
2927
3070
  const api = (await client(env)).volumes;
2928
3071
  switch (verb) {
2929
3072
  case "create": {
2930
- const args = parse(rest, [], ["size-mib", "name"]);
3073
+ const args = parse(rest, ["shared"], ["size-mib", "name"]);
2931
3074
  const created = await api.create({
2932
3075
  sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
2933
3076
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
3077
+ ...(has(args, "shared") ? { shared: true } : {}),
2934
3078
  });
2935
3079
  print(created.id, created);
2936
3080
  return 0;
@@ -2958,6 +3102,40 @@ async function volume(argv, env, out) {
2958
3102
  print(describeRecord(found, table), found);
2959
3103
  return 0;
2960
3104
  }
3105
+ case "resize": {
3106
+ const args = parse(rest, [], ["size-mib"]);
3107
+ const id = need(args.positional[0], "a volume id");
3108
+ if (args.positional.length !== 1 || args.rest)
3109
+ throw usage(`Use ${me} volume resize <id> --size-mib <N>.`);
3110
+ const sizeMiB = integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>"));
3111
+ const resized = await api.resize(id, { sizeMiB });
3112
+ print(describeRecord(resized, table), resized);
3113
+ return resized.resize?.state === "failed" ? 1 : 0;
3114
+ }
3115
+ case "attach": {
3116
+ const args = parse(rest, [], ["path"]);
3117
+ const id = need(args.positional[0], "a volume id");
3118
+ const sandboxId = need(args.positional[1], "a sandbox id");
3119
+ const path = need(flag(args, "path"), "--path /data");
3120
+ if (args.positional.length !== 2 || args.rest)
3121
+ throw usage(`Use ${me} volume attach <id> <sandboxId> --path /data.`);
3122
+ const attachment = await api.attach(id, { sandboxId, path });
3123
+ print(describeRecord(attachment, table), attachment);
3124
+ return attachment.error ? 1 : 0;
3125
+ }
3126
+ case "detach":
3127
+ case "attachment": {
3128
+ const args = parse(rest, [], []);
3129
+ const id = need(args.positional[0], "a volume id");
3130
+ const attachmentId = need(args.positional[1], "an attachment id");
3131
+ if (args.positional.length !== 2 || args.rest)
3132
+ throw usage(`Use ${me} volume ${verb} <id> <attachmentId>.`);
3133
+ const attachment = verb === "detach"
3134
+ ? await api.detach(id, attachmentId)
3135
+ : await api.getAttachment(id, attachmentId);
3136
+ print(describeRecord(attachment, table), attachment);
3137
+ return verb === "detach" && attachment.error ? 1 : 0;
3138
+ }
2961
3139
  case "rm":
2962
3140
  case "delete": {
2963
3141
  const found = await api.delete(need(rest[0], "a volume id"));
@@ -3029,6 +3207,10 @@ async function volume(argv, env, out) {
3029
3207
  "create",
3030
3208
  "ls",
3031
3209
  "get",
3210
+ "resize",
3211
+ "attach",
3212
+ "detach",
3213
+ "attachment",
3032
3214
  "rm",
3033
3215
  "backup",
3034
3216
  "backups",
@@ -3089,7 +3271,13 @@ function usdMicros(args, name) {
3089
3271
  return undefined;
3090
3272
  if (!/^\d+(\.\d{1,6})?$/.test(value))
3091
3273
  throw usage(`--${name} takes dollars, such as 25 or 2.50.`);
3092
- return Math.round(Number(value) * 1e6);
3274
+ // ARCHITECTURE.md section 10: integer microdollars throughout, with no
3275
+ // floating-point money path or rounding drift.
3276
+ const [whole, fraction = ""] = value.split(".");
3277
+ const micros = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
3278
+ if (micros > BigInt(Number.MAX_SAFE_INTEGER))
3279
+ throw usage(`--${name} is too large to represent exactly in microdollars.`);
3280
+ return Number(micros);
3093
3281
  }
3094
3282
  /** A create body from the command line's flags. */
3095
3283
  function createInput(args) {
@@ -3109,17 +3297,23 @@ function createInput(args) {
3109
3297
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
3110
3298
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
3111
3299
  ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
3112
- ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
3113
- ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
3114
- ...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
3300
+ ...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
3301
+ ...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
3302
+ ...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
3115
3303
  ...(flag(args, "cpu") ? { cpu: oneOf(args, "cpu", ["shared", "reserved"]) } : {}),
3116
- ...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
3304
+ ...(integer(args, "cpu-floor") !== undefined
3305
+ ? { cpuFloorMillis: integer(args, "cpu-floor") }
3306
+ : {}),
3117
3307
  ...(flag(args, "region") ? { region: flag(args, "region") } : {}),
3118
- ...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
3119
- ...(usdMicros(args, "max-total-cost")
3308
+ ...(usdMicros(args, "max-cost") !== undefined
3309
+ ? { maxCostMicros: usdMicros(args, "max-cost") }
3310
+ : {}),
3311
+ ...(usdMicros(args, "max-total-cost") !== undefined
3120
3312
  ? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
3121
3313
  : {}),
3122
- ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
3314
+ ...(integer(args, "timeout") !== undefined
3315
+ ? { timeoutSeconds: integer(args, "timeout") }
3316
+ : {}),
3123
3317
  ...(flag(args, "on-timeout")
3124
3318
  ? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
3125
3319
  : {}),
@@ -3149,7 +3343,7 @@ ownInterrupt = true) {
3149
3343
  ...(stdin ? { stdin } : {}),
3150
3344
  ...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
3151
3345
  ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
3152
- ...(timeout ? { timeoutMs: timeout * 1000 } : {}),
3346
+ ...(timeout !== undefined ? { timeoutMs: timeout * 1000 } : {}),
3153
3347
  };
3154
3348
  // Ctrl-C stops the command in the sandbox too, and says so; without this
3155
3349
  // the CLI exited and the command ran on unseen.
@@ -3222,6 +3416,10 @@ function defaultOut(argv) {
3222
3416
  return {
3223
3417
  json,
3224
3418
  write: (text) => process.stdout.write(text.endsWith("\n") ? text : `${text}\n`),
3419
+ drain: async () => {
3420
+ if (process.stdout.writableNeedDrain)
3421
+ await once(process.stdout, "drain");
3422
+ },
3225
3423
  error: (text) => process.stderr.write(`${text}\n`),
3226
3424
  };
3227
3425
  }
@@ -3279,6 +3477,10 @@ function spark(values) {
3279
3477
  }
3280
3478
  const mib = (bytes) => `${Math.round(bytes / 1_048_576).toLocaleString("en-US")} MiB`;
3281
3479
  function metricsText(m) {
3480
+ const waiting = (value) => value == null ? "not measured" : `${value}%`;
3481
+ const waitingHistory = (field) => m.points.some((point) => point[field] != null)
3482
+ ? spark(m.points.map((point) => point[field] ?? null))
3483
+ : "not measured";
3282
3484
  const cpu = m.points.map((p) => p.cpuPercent);
3283
3485
  const memory = m.points.map((p) => p.memoryBytes);
3284
3486
  const peak = Math.max(0, ...m.points.map((p) => p.cpuPeakPercent ?? p.cpuPercent ?? 0));
@@ -3302,7 +3504,11 @@ function metricsText(m) {
3302
3504
  "memory now",
3303
3505
  still ?? (latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"),
3304
3506
  ],
3507
+ ["cpu wait now", still ?? waiting(latest?.cpuWaitPercent)],
3508
+ ["memory stalls now", still ?? waiting(latest?.memoryStallPercent)],
3305
3509
  [`cpu, ${m.range}`, m.points.length ? `${spark(cpu)} peak ${peak}%` : "no readings"],
3510
+ [`cpu wait, ${m.range}`, waitingHistory("cpuWaitPercent")],
3511
+ [`memory stalls, ${m.range}`, waitingHistory("memoryStallPercent")],
3306
3512
  [
3307
3513
  `memory, ${m.range}`,
3308
3514
  m.points.length
@@ -3397,7 +3603,7 @@ async function webhooksCommand(argv, env, out) {
3397
3603
  }
3398
3604
  case "deliveries": {
3399
3605
  const page = await hooks.deliveries(id(), {
3400
- ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
3606
+ ...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
3401
3607
  });
3402
3608
  print(page.data.length
3403
3609
  ? table(page.data.map((d) => [