withruntime 0.8.4 → 0.10.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 (132) hide show
  1. package/BLAXEL.md +4 -4
  2. package/COMPATIBILITY.md +145 -0
  3. package/DAYTONA.md +13 -7
  4. package/E2B.md +34 -20
  5. package/README.md +22 -11
  6. package/README.src.md +22 -11
  7. package/VERCEL.md +13 -3
  8. package/compatibility-lock.json +357 -0
  9. package/dist/ai-harness/index.js +2 -2
  10. package/dist/api-defaults.d.ts +17 -0
  11. package/dist/api-defaults.js +22 -0
  12. package/dist/blaxel/errors.js +1 -1
  13. package/dist/blaxel/lookup.d.ts +3 -2
  14. package/dist/blaxel/lookup.js +3 -2
  15. package/dist/blaxel/pagination.d.ts +2 -2
  16. package/dist/blaxel/pagination.js +45 -17
  17. package/dist/blaxel/preview.d.ts +3 -2
  18. package/dist/blaxel/preview.js +16 -14
  19. package/dist/blaxel/process.d.ts +1 -1
  20. package/dist/blaxel/process.js +66 -11
  21. package/dist/blaxel/sandbox.d.ts +2 -0
  22. package/dist/blaxel/sandbox.js +2 -0
  23. package/dist/blaxel/snapshot.js +3 -5
  24. package/dist/cli-words.d.ts +27 -0
  25. package/dist/cli-words.js +135 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +351 -85
  28. package/dist/client.d.ts +33 -4
  29. package/dist/client.js +47 -12
  30. package/dist/cloudflare/identity.d.ts +7 -0
  31. package/dist/cloudflare/identity.js +13 -0
  32. package/dist/cloudflare/index.d.ts +303 -0
  33. package/dist/cloudflare/index.js +592 -0
  34. package/dist/cloudflare/worker.d.ts +419 -0
  35. package/dist/cloudflare/worker.js +447 -0
  36. package/dist/codesandbox/guest-service.d.ts +31 -0
  37. package/dist/codesandbox/guest-service.js +490 -0
  38. package/dist/codesandbox/index.d.ts +386 -0
  39. package/dist/codesandbox/index.js +1347 -0
  40. package/dist/compat/core.d.ts +40 -0
  41. package/dist/compat/core.js +238 -0
  42. package/dist/compat/files.d.ts +26 -0
  43. package/dist/compat/files.js +133 -0
  44. package/dist/compat/shell.d.ts +4 -0
  45. package/dist/compat/shell.js +165 -0
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +107 -38
  48. package/dist/daytona/context.js +2 -1
  49. package/dist/daytona/daytona.js +11 -4
  50. package/dist/daytona/filesystem.d.ts +17 -4
  51. package/dist/daytona/filesystem.js +121 -38
  52. package/dist/daytona/index.d.ts +1 -1
  53. package/dist/daytona/process.d.ts +1 -1
  54. package/dist/daytona/process.js +27 -4
  55. package/dist/daytona/sandbox.d.ts +1 -1
  56. package/dist/daytona/sandbox.js +16 -7
  57. package/dist/e2b/client.js +1 -1
  58. package/dist/e2b/code-interpreter.d.ts +4 -5
  59. package/dist/e2b/code-interpreter.js +54 -56
  60. package/dist/e2b/commands.d.ts +35 -12
  61. package/dist/e2b/commands.js +169 -73
  62. package/dist/e2b/errors.js +1 -1
  63. package/dist/e2b/filesystem.d.ts +8 -6
  64. package/dist/e2b/filesystem.js +127 -28
  65. package/dist/e2b/index.d.ts +1 -0
  66. package/dist/e2b/index.js +1 -0
  67. package/dist/e2b/pty.d.ts +24 -0
  68. package/dist/e2b/pty.js +80 -0
  69. package/dist/e2b/sandbox.d.ts +6 -4
  70. package/dist/e2b/sandbox.js +59 -25
  71. package/dist/errors.d.ts +8 -0
  72. package/dist/errors.js +15 -2
  73. package/dist/file-source.d.ts +16 -0
  74. package/dist/file-source.js +162 -0
  75. package/dist/freestyle/index.d.ts +309 -0
  76. package/dist/freestyle/index.js +343 -0
  77. package/dist/index.d.ts +3 -1
  78. package/dist/login.js +16 -10
  79. package/dist/mcp.js +2 -2
  80. package/dist/modal/index.d.ts +205 -0
  81. package/dist/modal/index.js +486 -0
  82. package/dist/network-cli.js +112 -44
  83. package/dist/openai-agents/index.js +18 -71
  84. package/dist/page.d.ts +1 -1
  85. package/dist/page.js +2 -2
  86. package/dist/products/audit.d.ts +3 -0
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/interpreter.d.ts +11 -4
  90. package/dist/products/interpreter.js +33 -9
  91. package/dist/products/jobs.js +3 -2
  92. package/dist/products/limits.d.ts +10 -0
  93. package/dist/products/observability.d.ts +44 -0
  94. package/dist/products/observability.js +6 -0
  95. package/dist/products/previews.d.ts +18 -4
  96. package/dist/products/previews.js +14 -5
  97. package/dist/products/tailscale.d.ts +57 -0
  98. package/dist/products/tailscale.js +17 -0
  99. package/dist/products/volumes.d.ts +42 -0
  100. package/dist/products/volumes.js +36 -0
  101. package/dist/products/watch.d.ts +25 -4
  102. package/dist/products/watch.js +132 -18
  103. package/dist/proxy.js +3 -1
  104. package/dist/runloop/index.d.ts +326 -0
  105. package/dist/runloop/index.js +535 -0
  106. package/dist/runloop/pagination.d.ts +46 -0
  107. package/dist/runloop/pagination.js +82 -0
  108. package/dist/runloop/snapshots.d.ts +52 -0
  109. package/dist/runloop/snapshots.js +244 -0
  110. package/dist/sandbox.d.ts +76 -16
  111. package/dist/sandbox.js +696 -135
  112. package/dist/snapshots.d.ts +20 -5
  113. package/dist/snapshots.js +11 -1
  114. package/dist/sprites/index.d.ts +251 -0
  115. package/dist/sprites/index.js +588 -0
  116. package/dist/tar.d.ts +10 -1
  117. package/dist/tar.js +311 -35
  118. package/dist/tools/index.js +2 -1
  119. package/dist/transport.d.ts +23 -8
  120. package/dist/transport.js +247 -127
  121. package/dist/types.d.ts +52 -3
  122. package/dist/vercel/command.d.ts +1 -1
  123. package/dist/vercel/command.js +15 -3
  124. package/dist/vercel/errors.d.ts +2 -2
  125. package/dist/vercel/errors.js +19 -2
  126. package/dist/vercel/filesystem.d.ts +21 -18
  127. package/dist/vercel/filesystem.js +95 -48
  128. package/dist/vercel/sandbox.d.ts +8 -8
  129. package/dist/vercel/sandbox.js +159 -64
  130. package/dist/vercel/snapshot.d.ts +3 -2
  131. package/dist/vercel/snapshot.js +9 -5
  132. package/package.json +46 -3
package/dist/cli.js CHANGED
@@ -1,15 +1,17 @@
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";
10
11
  import { NotFoundError, RuntimeError } from "./errors.js";
11
12
  import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
12
13
  import { billingCommand } from "./billing-cli.js";
14
+ import { cliRefusal, describeRecord, newestCpu, topicHelp } from "./cli-words.js";
13
15
  import { envFetch } from "./proxy.js";
14
16
  import { VERSION } from "./transport.js";
15
17
  const HELP = `Runtime Cloud CLI ${VERSION}
@@ -35,12 +37,16 @@ Account
35
37
  sso Single sign-on and SCIM directory sync (owner or
36
38
  admin keys); an owner changes it on the website
37
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
38
42
  limits Whether this key is read-only, and its daily spending limit
39
43
  secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
40
44
  [--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
41
45
  value is read from standard input, never an argument.
42
46
  --allow (paid): only these methods and paths get it
43
- secrets ls | get <NAME> Names, hosts and placeholders, never values
47
+ secrets set <NAME> --jobs [--host ...] A copy scheduled jobs put in their environment
48
+ (job create --secret NAME); with --host, both
49
+ secrets ls | get <NAME> Names, hosts, placeholders and jobs versions, never values
44
50
  secrets rotate <NAME> A new version of the jobs copy, from standard input
45
51
  secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
46
52
  never readable, by anyone
@@ -86,6 +92,7 @@ Products
86
92
  snapshot Saved sandboxes, disk and memory, to start or fork from
87
93
  image Custom images from a Dockerfile, any public or private image, or a package list
88
94
  volume Persistent disks to attach to sandboxes
95
+ job A command in a fresh sandbox, once or on a cron schedule, with its logs
89
96
  domain Your own hostname for a sandbox's port, with HTTPS
90
97
  port A public TCP port to a sandbox: databases, game servers
91
98
  address A dedicated outbound address, for allow-lists
@@ -103,7 +110,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
103
110
  run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
104
111
  ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
105
112
  port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
106
- create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
113
+ create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
107
114
  [--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
108
115
  [--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
109
116
  [--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
@@ -122,10 +129,15 @@ const SANDBOX_HELP = `runtime sandbox <command>
122
129
  cp <src> <dst> Copy files or directories; name a sandbox
123
130
  path as <id>:/path, e.g. cp ./app sbx:/workspace/app
124
131
  cat <id> <path> Print a file
125
- files <id> [path] [--depth 2] [--glob '**/*.py']
132
+ files <id> [path] [--depth 1] [--glob '**/*.py']
126
133
  stop <id> | pause <id> | wake <id> | restart <id>
134
+ rm <id> Delete it for good: stops it, deletes its disk and
135
+ paused memory, revokes its previews; snapshots stay
136
+ switch-image <id> <image> --keep-workspace Move it to another image, keeping /workspace (your home),
137
+ volumes, env and previews; processes restart, the rest of
138
+ its disk is lost (snapshot it first to keep everything)
127
139
  extend <id> <seconds> More time before the lease ends
128
- update <id> [--name n] [--label k=v]... [--idle-pause <seconds>] [--auto-wake on|off]
140
+ update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
129
141
  [--persistent on|off] [--max-total-cost <usd>|none]
130
142
  Change its settings; persistent keeps it running
131
143
  snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
@@ -133,6 +145,9 @@ const SANDBOX_HELP = `runtime sandbox <command>
133
145
  [--endpoint https://...] [--account-id id] [--read-only]
134
146
  Your bucket as a directory; the proxy signs, the sandbox never sees the key
135
147
  mounts <id> | unmount <id> <path> Its bucket mounts; unmount one
148
+ tailscale up <id> --auth-key-secret NAME [--hostname h] [--tag tag:x]...
149
+ Join your tailnet (paid); the key is a secret set with --jobs
150
+ tailscale status <id> | tailscale down <id> Its tailnet address; take it off the tailnet
136
151
  fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
137
152
  Copies of it as it is now, running; prints their ids
138
153
  run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
@@ -168,8 +183,8 @@ const SANDBOX_HELP = `runtime sandbox <command>
168
183
 
169
184
  Examples
170
185
  id=$(runtime sandbox create)
171
- runtime sandbox exec $id -- python3 -c 'print(6*7)'
172
- runtime sandbox cp ./project $id:/workspace/project
186
+ runtime sandbox cp ./invoice.py $id:/workspace/invoice.py
187
+ runtime sandbox exec $id -- python3 /workspace/invoice.py
173
188
  runtime sandbox stop $id
174
189
  `;
175
190
  /** A command's options. Reading one its parse() did not declare is a bug in
@@ -217,9 +232,13 @@ commandAfter) {
217
232
  throw commandAfter && !meantOption(name, known)
218
233
  ? usage(`Unknown option ${arg.split("=")[0]}. If it belongs to the command, put the command after --: ${commandAfter}`)
219
234
  : unknownOption(name, known);
220
- const value = inline ?? (booleans.includes(name) ? "true" : argv[++i]);
221
- if (value === undefined)
235
+ const boolean = booleans.includes(name);
236
+ const value = inline ?? (boolean ? "true" : argv[++i]);
237
+ if (value === undefined ||
238
+ (!boolean && inline === undefined && (value === "--" || /^--?[^\d]/.test(value))))
222
239
  throw usage(`--${name} needs a value.`);
240
+ if (boolean && value !== "true" && value !== "false")
241
+ throw usage(`--${name} takes true or false.`);
223
242
  flags.set(name, [...(flags.get(name) ?? []), value]);
224
243
  }
225
244
  else
@@ -251,7 +270,10 @@ const integer = (args, name) => {
251
270
  return undefined;
252
271
  if (!/^\d+$/.test(value))
253
272
  throw usage(`--${name} must be a whole number.`);
254
- return Number(value);
273
+ const parsed = Number(value);
274
+ if (!Number.isSafeInteger(parsed))
275
+ throw usage(`--${name} is too large to represent exactly.`);
276
+ return parsed;
255
277
  };
256
278
  /** A flag that takes one of a few words, checked before anything is sent. */
257
279
  function oneOf(args, name, choices) {
@@ -385,6 +407,9 @@ const SANDBOX_VERBS = [
385
407
  "ls",
386
408
  "get",
387
409
  "stop",
410
+ "rm",
411
+ "delete",
412
+ "switch-image",
388
413
  "pause",
389
414
  "wake",
390
415
  "restart",
@@ -392,6 +417,7 @@ const SANDBOX_VERBS = [
392
417
  "mount",
393
418
  "mounts",
394
419
  "unmount",
420
+ "tailscale",
395
421
  "fork",
396
422
  "extend",
397
423
  "update",
@@ -484,6 +510,12 @@ function describe(info) {
484
510
  ["funding", info.funding],
485
511
  ["region", info.region],
486
512
  ["expires", info.expiresAt ?? "-"],
513
+ ["at lease end", info.onLeaseEnd ?? "-"],
514
+ // Why a sandbox is no longer running (troubleshooting.md, "A sandbox
515
+ // stopped on its own"); `get` left both out until 30 September 2026.
516
+ ...(info.pausedAt ? [["paused at", info.pausedAt]] : []),
517
+ ...(info.endedAt ? [["ended", info.endedAt]] : []),
518
+ ...(info.stopReason ? [["stop reason", info.stopReason]] : []),
487
519
  ["auto wake", info.autoWake === false ? "off" : "on"],
488
520
  [
489
521
  "idle pause",
@@ -523,6 +555,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
523
555
  // `runtime sandbox create --help` is how an agent asks what a command takes:
524
556
  // answer with the product's help, never by running the command.
525
557
  const cut = remaining.indexOf("--");
558
+ const asked = (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h") ||
559
+ (remaining.length === 1 && remaining[0] === "help");
560
+ // A command without a help of its own answers `help` with its lines of the
561
+ // top-level help, and never takes the word as its input.
562
+ if (first && first !== "help" && asked && !nested && !OWN_HELP.includes(first)) {
563
+ const lines = topicHelp(named(HELP), first);
564
+ if (lines) {
565
+ out.write(out.json ? JSON.stringify({ usage: lines }) : lines);
566
+ return 0;
567
+ }
568
+ }
526
569
  if (first &&
527
570
  (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h"))
528
571
  return run(nested && command === "ssh"
@@ -542,8 +585,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
542
585
  ? JSON.stringify({ connected, usage: HELP })
543
586
  : `Runtime Cloud ${VERSION}. ${start}
544
587
 
545
- ${me} sandbox run -- python3 -c 'print(6*7)'
546
- A fresh sandbox runs one command, then stops
588
+ ${me} sandbox run -- python3 < invoice.py
589
+ Run a local invoice script, then stop the sandbox
547
590
  ${me} sandbox create A sandbox that stays; prints its id
548
591
  ${me} sandbox exec <id> -- <command> Run in it
549
592
  ${me} help Everything else
@@ -555,10 +598,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
555
598
  return 0;
556
599
  }
557
600
  if (command === "run") {
558
- const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 -c 'print(6*7)'`);
601
+ const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 < invoice.py`);
559
602
  const commandLine = args.rest ?? args.positional;
560
603
  if (!commandLine.length)
561
- throw usage(`Give the command after --: ${me} sandbox run -- python3 -c 'print(6*7)'`);
604
+ throw usage(`Give the command after --: ${me} sandbox run -- python3 < invoice.py`);
562
605
  const runtime = await client(env);
563
606
  const timeout = integer(args, "timeout");
564
607
  const { timeoutSeconds: _lease, ...input } = createInput(args);
@@ -655,6 +698,40 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
655
698
  return 0;
656
699
  }
657
700
  if (command === "usage") {
701
+ if (rest[0] === "export") {
702
+ const args = parse(rest.slice(1), [], ["since", "until"]);
703
+ if (args.positional.length || args.rest)
704
+ throw usage(`Use ${me} usage export --since <UTC time> --until <UTC time>.`);
705
+ const since = need(flag(args, "since"), "--since <UTC time>, such as 2026-09-01T00:00:00Z");
706
+ const until = need(flag(args, "until"), "--until <UTC time>, such as 2026-10-01T00:00:00Z");
707
+ const rt = await client(env);
708
+ const cursors = new Set();
709
+ let cursor;
710
+ for (;;) {
711
+ const page = await rt.usageExport({
712
+ since,
713
+ until,
714
+ ...(cursor ? { cursor } : {}),
715
+ limit: 500,
716
+ });
717
+ if (page.nextCursor !== null && cursors.has(page.nextCursor))
718
+ throw new RuntimeError({
719
+ code: "usage_export_incomplete",
720
+ status: 0,
721
+ message: "The usage export repeated a page cursor and could not finish.",
722
+ hint: "Run the export again; do not treat the partial output as complete.",
723
+ });
724
+ if (out.json)
725
+ out.write(JSON.stringify(page));
726
+ else if (page.csv)
727
+ out.write(page.csv);
728
+ await out.drain?.();
729
+ if (page.nextCursor === null)
730
+ return 0;
731
+ cursors.add(page.nextCursor);
732
+ cursor = page.nextCursor;
733
+ }
734
+ }
658
735
  const csv = rest.includes("--csv");
659
736
  const unknown = rest.find((arg) => arg !== "--csv");
660
737
  if (unknown)
@@ -671,7 +748,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
671
748
  }
672
749
  if (command === "limits") {
673
750
  const l = await (await client(env)).limits.get();
674
- const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
751
+ // As `usage` shows money: $0.0014 of spend read $0.00 until 30 September 2026.
752
+ const usd = (micros) => dollars(micros);
675
753
  const access = l.access === "read"
676
754
  ? "read only: reads every product, cannot start, change or spend"
677
755
  : l.access === "full"
@@ -687,6 +765,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
687
765
  ],
688
766
  ["used, last 24 hours", usd(l.daily.usedMicros)],
689
767
  ...(l.daily.remainingMicros === null ? [] : [["left", usd(l.daily.remainingMicros)]]),
768
+ ...(l.trial ? [["free trial", trialLeft(l.trial)]] : []),
690
769
  ]), l);
691
770
  return 0;
692
771
  }
@@ -703,7 +782,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
703
782
  const page = await rt.events.list({
704
783
  ...(sandbox ? { resourceId: await sandboxIdOf(rt, sandbox) } : {}),
705
784
  ...(flag(args, "type") ? { type: flag(args, "type") } : {}),
706
- ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
785
+ ...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
707
786
  });
708
787
  print(page.data.length
709
788
  ? table(page.data.map((e) => {
@@ -748,7 +827,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
748
827
  throw usage(`Unknown option --${name}.`);
749
828
  const page = await (await client(env)).audit.list({
750
829
  ...(flag(args, "action") ? { action: flag(args, "action") } : {}),
751
- ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
830
+ ...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
752
831
  ...(flag(args, "before") ? { before: flag(args, "before") } : {}),
753
832
  });
754
833
  print(page.events.length
@@ -774,17 +853,26 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
774
853
  const origins = connectionOrigins(env);
775
854
  const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
776
855
  return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
777
- open: (input) => openWalletAccount(input, { authUrl: origins.auth }),
778
- claim: (code) => claimWalletAccount(code, { authUrl: origins.auth }),
779
- save: (key, orgId) => connectionStore(env).save({
780
- version: 1,
781
- apiOrigin: origins.api,
782
- authOrigin: origins.auth,
783
- key,
784
- connectionId: "pasted",
785
- orgId,
786
- agentName: "Wallet agent",
787
- }),
856
+ open: (input) => {
857
+ checkBrowserOrigins(origins);
858
+ return openWalletAccount(input, { authUrl: origins.auth });
859
+ },
860
+ claim: (code) => {
861
+ checkBrowserOrigins(origins);
862
+ return claimWalletAccount(code, { authUrl: origins.auth });
863
+ },
864
+ save: (key, orgId) => {
865
+ checkBrowserOrigins(origins);
866
+ return connectionStore(env).save({
867
+ version: 1,
868
+ apiOrigin: origins.api,
869
+ authOrigin: origins.auth,
870
+ key,
871
+ connectionId: "pasted",
872
+ orgId,
873
+ agentName: "Wallet agent",
874
+ });
875
+ },
788
876
  });
789
877
  }
790
878
  if (command === "referrals") {
@@ -975,11 +1063,15 @@ export function usageCsv(u) {
975
1063
  .join(","));
976
1064
  return [head.join(","), ...rows].join("\n");
977
1065
  }
1066
+ /** The free trial's time left, as `runtime usage` and `runtime limits` say it. */
1067
+ function trialLeft(trial) {
1068
+ const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
1069
+ return `${hours(trial.availableMs)} of ${hours(trial.totalMs)} hours left${trial.reservedMs > 0 ? `, ${hours(trial.reservedMs)} held by running sandboxes` : ""}`;
1070
+ }
978
1071
  function usageSummary(u) {
979
1072
  const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
980
1073
  ? BigInt(value)
981
1074
  : 0n;
982
- const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
983
1075
  const takenBack = micros(u.takenBack);
984
1076
  const rows = [
985
1077
  ["available", dollars(micros(u.available))],
@@ -996,10 +1088,7 @@ function usageSummary(u) {
996
1088
  if (micros(u.expired) > 0n)
997
1089
  rows.push(["expired or taken back", dollars(micros(u.expired))]);
998
1090
  if (u.trial)
999
- rows.push([
1000
- "free trial",
1001
- `${hours(u.trial.availableMs)} of ${hours(u.trial.totalMs)} hours left${u.trial.reservedMs > 0 ? `, ${hours(u.trial.reservedMs)} held by running sandboxes` : ""}`,
1002
- ]);
1091
+ rows.push(["free trial", trialLeft(u.trial)]);
1003
1092
  if (u.outbound) {
1004
1093
  const gib = (bytes) => `${(bytes / 1_073_741_824).toLocaleString("en-US", { maximumFractionDigits: 1 })} GiB`;
1005
1094
  const left = Math.max(0, u.outbound.allowanceBytes - u.outbound.freeBytes);
@@ -1357,8 +1446,12 @@ async function sandbox(argv, env, out) {
1357
1446
  ...(args.flags.has("state")
1358
1447
  ? { state: args.flags.get("state") }
1359
1448
  : {}),
1449
+ limit: 100,
1360
1450
  });
1361
- const all = await page.toArray(1000);
1451
+ // Every page, oldest first, so the newest are last on the screen. Until
1452
+ // 30 September 2026 this stopped at 1,000 and silently left out the
1453
+ // newest sandboxes of a busy account.
1454
+ const all = await page.toArray(Infinity);
1362
1455
  // Stopped sandboxes are hidden by default; say so, or an agent that
1363
1456
  // stopped everything reads "No sandboxes" as "never had any".
1364
1457
  const hidden = out.json || args.flags.size
@@ -1392,6 +1485,23 @@ async function sandbox(argv, env, out) {
1392
1485
  print(describe(sbx.info), sbx.info);
1393
1486
  return 0;
1394
1487
  }
1488
+ case "switch-image": {
1489
+ const args = parse(rest, ["keep-workspace"], []);
1490
+ const [id, image] = args.positional;
1491
+ const sbx = await use(need(id, `a sandbox, e.g. ${me} sandbox switch-image <id> app:v2 --keep-workspace`));
1492
+ if (!args.flags.has("keep-workspace"))
1493
+ throw usage(`A switch keeps only /workspace (your home), volumes, env and previews; processes restart and everything else on its disk is lost. Add --keep-workspace to say so, or snapshot it first (${me} sandbox snapshot ${sbx.id}) to keep everything.`);
1494
+ await sbx.switchImage(need(image, "an image, e.g. app:v2"), { keep: "workspace" });
1495
+ print(describe(sbx.info), sbx.info);
1496
+ return 0;
1497
+ }
1498
+ case "rm":
1499
+ case "delete": {
1500
+ const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandbox rm <id>`));
1501
+ const gone = await sbx.delete();
1502
+ print(`${gone.id} deleted`, gone);
1503
+ return 0;
1504
+ }
1395
1505
  case "stop":
1396
1506
  case "pause":
1397
1507
  case "wake":
@@ -1445,12 +1555,43 @@ async function sandbox(argv, env, out) {
1445
1555
  print(`${gone.path} unmounted`, gone);
1446
1556
  return 0;
1447
1557
  }
1558
+ case "tailscale": {
1559
+ const action = rest[0];
1560
+ if (action === "up") {
1561
+ const args = parse(rest.slice(1), [], ["auth-key-secret", "hostname", "tag"]);
1562
+ const sbx = await use(args.positional[0]);
1563
+ const joined = await sbx.tailscale.up({
1564
+ authKeySecret: need(flag(args, "auth-key-secret"), "--auth-key-secret, the name of a secret set with --jobs that holds your Tailscale auth key"),
1565
+ ...(flag(args, "hostname") ? { hostname: flag(args, "hostname") } : {}),
1566
+ ...(args.flags.has("tag") ? { tags: args.flags.get("tag") } : {}),
1567
+ });
1568
+ print(`${joined.hostname} ${joined.addresses.join(" ")}${joined.proxy ? ` (reach the tailnet through ${joined.proxy})` : ""}`, joined);
1569
+ return 0;
1570
+ }
1571
+ if (action === "status") {
1572
+ const sbx = await use(rest[1]);
1573
+ const now = await sbx.tailscale.status();
1574
+ print(now.tailnet
1575
+ ? `${now.tailnet.hostname} ${now.running ? now.addresses.join(" ") : (now.state ?? "not running")}`
1576
+ : "Not on a tailnet.", now);
1577
+ return 0;
1578
+ }
1579
+ if (action === "down") {
1580
+ const sbx = await use(rest[1]);
1581
+ const gone = await sbx.tailscale.down();
1582
+ print(gone.left ? "Left the tailnet." : "Not on a tailnet.", gone);
1583
+ return 0;
1584
+ }
1585
+ throw usage("runtime sandbox tailscale up|status|down <id>");
1586
+ }
1448
1587
  case "snapshot": {
1449
1588
  const args = parse(rest, [], ["name", "retention"]);
1450
1589
  const sbx = await get(args.positional[0]);
1451
1590
  const snapshot = await sbx.snapshot({
1452
1591
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
1453
- ...(integer(args, "retention") ? { retentionDays: integer(args, "retention") } : {}),
1592
+ ...(integer(args, "retention") !== undefined
1593
+ ? { retentionDays: integer(args, "retention") }
1594
+ : {}),
1454
1595
  });
1455
1596
  print(snapshot.id, snapshot);
1456
1597
  return 0;
@@ -1472,7 +1613,16 @@ async function sandbox(argv, env, out) {
1472
1613
  return 0;
1473
1614
  }
1474
1615
  case "update": {
1475
- const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
1616
+ const args = parse(rest, [], [
1617
+ "name",
1618
+ "label",
1619
+ "env",
1620
+ "unset-env",
1621
+ "idle-pause",
1622
+ "auto-wake",
1623
+ "persistent",
1624
+ "max-total-cost",
1625
+ ]);
1476
1626
  const sbx = await get(args.positional[0]);
1477
1627
  const onOff = (name) => {
1478
1628
  const value = flag(args, name);
@@ -1485,9 +1635,14 @@ async function sandbox(argv, env, out) {
1485
1635
  const cost = flag(args, "max-total-cost");
1486
1636
  if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
1487
1637
  throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
1638
+ const env = {
1639
+ ...(args.flags.has("env") ? pairs(args, "env") : {}),
1640
+ ...Object.fromEntries((args.flags.get("unset-env") ?? []).map((name) => [name, null])),
1641
+ };
1488
1642
  const settings = {
1489
1643
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
1490
1644
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
1645
+ ...(Object.keys(env).length ? { env } : {}),
1491
1646
  ...(integer(args, "idle-pause") !== undefined
1492
1647
  ? { idlePauseSeconds: integer(args, "idle-pause") }
1493
1648
  : {}),
@@ -1496,11 +1651,11 @@ async function sandbox(argv, env, out) {
1496
1651
  ...(cost === undefined
1497
1652
  ? {}
1498
1653
  : {
1499
- maxTotalCostMicros: cost === "none" ? null : Math.round(Number(cost) * 1e6),
1654
+ maxTotalCostMicros: cost === "none" ? null : usdMicros(args, "max-total-cost"),
1500
1655
  }),
1501
1656
  };
1502
1657
  if (!Object.keys(settings).length)
1503
- throw usage("Say what to change: --name, --label, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
1658
+ throw usage("Say what to change: --name, --label, --env, --unset-env, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
1504
1659
  await sbx.update(settings);
1505
1660
  print(describe(sbx.info), sbx.info);
1506
1661
  return 0;
@@ -1576,8 +1731,21 @@ async function sandbox(argv, env, out) {
1576
1731
  const args = parse(rest, [], ["signal"]);
1577
1732
  const sbx = await use(args.positional[0]);
1578
1733
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1734
+ // A process that has ended has nothing to signal; "Signalled" said it
1735
+ // had been until 30 September 2026.
1736
+ if (proc.info.state === "exited" ||
1737
+ proc.info.state === "killed" ||
1738
+ proc.info.state === "timed_out") {
1739
+ print(`${proc.id} had already ${proc.info.state === "exited" ? "exited" : "ended"}${proc.info.exitCode == null ? "" : ` (exit code ${proc.info.exitCode})`}; nothing to signal.`, {
1740
+ processId: proc.id,
1741
+ state: proc.info.state,
1742
+ exitCode: proc.info.exitCode ?? null,
1743
+ signalled: false,
1744
+ });
1745
+ return 0;
1746
+ }
1579
1747
  await proc.kill((flag(args, "signal") ?? "SIGTERM"));
1580
- print(`Signalled ${proc.id}.`, { processId: proc.id });
1748
+ print(`Signalled ${proc.id}.`, { processId: proc.id, signalled: true });
1581
1749
  return 0;
1582
1750
  }
1583
1751
  case "shell": {
@@ -1596,7 +1764,7 @@ async function sandbox(argv, env, out) {
1596
1764
  const args = parse(rest, [], ["depth", "glob"]);
1597
1765
  const sbx = await use(args.positional[0]);
1598
1766
  const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
1599
- ...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
1767
+ ...(integer(args, "depth") !== undefined ? { depth: integer(args, "depth") } : {}),
1600
1768
  ...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
1601
1769
  });
1602
1770
  print(entries
@@ -1639,7 +1807,7 @@ async function sandbox(argv, env, out) {
1639
1807
  const sbx = await use(args.positional[0]);
1640
1808
  const preview = await sbx.previews.create(port, {
1641
1809
  ...(has(args, "public") ? { visibility: "public" } : {}),
1642
- ...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
1810
+ ...(integer(args, "ttl") !== undefined ? { ttlSeconds: integer(args, "ttl") } : {}),
1643
1811
  });
1644
1812
  // The link and how to use it, nothing more: the API's `hint` (which
1645
1813
  // carries a referral line) stays in --json, not in the command's words.
@@ -1971,7 +2139,7 @@ async function runCode(sbx, argv, out) {
1971
2139
  const execution = await sbx.interpreter.run(code, {
1972
2140
  language,
1973
2141
  ...(flag(args, "context") ? { context: flag(args, "context") } : {}),
1974
- ...(timeout ? { timeoutMs: timeout * 1000 } : {}),
2142
+ ...(timeout !== undefined ? { timeoutMs: timeout * 1000 } : {}),
1975
2143
  ...(out.json
1976
2144
  ? {}
1977
2145
  : {
@@ -2020,8 +2188,8 @@ async function desktop(sbx, argv, out) {
2020
2188
  case "start": {
2021
2189
  const args = parse(rest, [], ["width", "height"]);
2022
2190
  const started = await d.start({
2023
- ...(integer(args, "width") ? { width: integer(args, "width") } : {}),
2024
- ...(integer(args, "height") ? { height: integer(args, "height") } : {}),
2191
+ ...(integer(args, "width") !== undefined ? { width: integer(args, "width") } : {}),
2192
+ ...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
2025
2193
  });
2026
2194
  print(`Desktop ${started.size} on ${started.display}. Watch it: ${started.streamUrl}`, started);
2027
2195
  return 0;
@@ -2256,7 +2424,7 @@ async function snapshot(argv, env, out) {
2256
2424
  ? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
2257
2425
  : {}),
2258
2426
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
2259
- })).toArray(1000);
2427
+ })).toArray(Infinity);
2260
2428
  print(all.length
2261
2429
  ? table([
2262
2430
  ["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
@@ -2360,9 +2528,13 @@ read = readSecret) {
2360
2528
  if (sources.filter((s) => s !== undefined).length !== 1)
2361
2529
  throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
2362
2530
  const build = {
2363
- ...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
2364
- ...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
2365
- ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
2531
+ ...(integer(args, "max-image-mib") !== undefined
2532
+ ? { maxImageMiB: integer(args, "max-image-mib") }
2533
+ : {}),
2534
+ ...(integer(args, "disk-mib") !== undefined ? { diskMiB: integer(args, "disk-mib") } : {}),
2535
+ ...(integer(args, "timeout") !== undefined
2536
+ ? { timeoutSeconds: integer(args, "timeout") }
2537
+ : {}),
2366
2538
  };
2367
2539
  // -t app, -t app:v2 -t app:latest: one name, its tags.
2368
2540
  let name = flag(args, "name");
@@ -2379,9 +2551,11 @@ read = readSecret) {
2379
2551
  }
2380
2552
  const start = {
2381
2553
  ...(flag(args, "start") ? { command: flag(args, "start") } : {}),
2382
- ...(integer(args, "ready-port") ? { readyPort: integer(args, "ready-port") } : {}),
2554
+ ...(integer(args, "ready-port") !== undefined
2555
+ ? { readyPort: integer(args, "ready-port") }
2556
+ : {}),
2383
2557
  ...(flag(args, "ready-command") ? { readyCommand: flag(args, "ready-command") } : {}),
2384
- ...(integer(args, "ready-timeout")
2558
+ ...(integer(args, "ready-timeout") !== undefined
2385
2559
  ? { readyTimeoutSeconds: integer(args, "ready-timeout") }
2386
2560
  : {}),
2387
2561
  };
@@ -2426,7 +2600,7 @@ read = readSecret) {
2426
2600
  case "list":
2427
2601
  case "versions": {
2428
2602
  const name = verb === "versions" ? need(rest[0], "a name") : undefined;
2429
- const all = await (await api.list(name ? { name } : {})).toArray(1000);
2603
+ const all = await (await api.list(name ? { name } : {})).toArray(Infinity);
2430
2604
  print(all.length
2431
2605
  ? table([
2432
2606
  ["ID", "NAME", "VERSION", "TAGS", "STATE", "MIB", "CREATED"],
@@ -2447,7 +2621,7 @@ read = readSecret) {
2447
2621
  }
2448
2622
  case "get": {
2449
2623
  const found = await api.resolve(need(rest[0], "an image"));
2450
- print(JSON.stringify(found, null, 2), found);
2624
+ print(describeRecord(found, table), found);
2451
2625
  return 0;
2452
2626
  }
2453
2627
  case "logs": {
@@ -2645,23 +2819,29 @@ async function jobCommand(argv, env, out) {
2645
2819
  ? { cron, ...(flag(args, "timezone") ? { timezone: flag(args, "timezone") } : {}) }
2646
2820
  : { at: atTime(at) },
2647
2821
  command: cwd ? { argv, cwd } : argv,
2648
- ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
2822
+ ...(integer(args, "timeout") !== undefined
2823
+ ? { timeoutSeconds: integer(args, "timeout") }
2824
+ : {}),
2649
2825
  compute: {
2650
- ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
2651
- ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
2652
- ...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
2826
+ ...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
2827
+ ...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
2828
+ ...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
2653
2829
  ...(flag(args, "cpu")
2654
2830
  ? { cpuMode: oneOf(args, "cpu", ["shared", "reserved"]) }
2655
2831
  : {}),
2656
- ...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
2832
+ ...(integer(args, "cpu-floor") !== undefined
2833
+ ? { cpuFloorMillis: integer(args, "cpu-floor") }
2834
+ : {}),
2657
2835
  ...(flag(args, "region") ? { region: flag(args, "region") } : {}),
2658
- ...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
2836
+ ...(usdMicros(args, "max-cost") !== undefined
2837
+ ? { maxCostMicros: usdMicros(args, "max-cost") }
2838
+ : {}),
2659
2839
  },
2660
2840
  ...(attempts !== undefined
2661
2841
  ? { retry: { maxAttempts: attempts, backoffSeconds: backoff ?? 30 } }
2662
2842
  : {}),
2663
2843
  ...(secrets ? { secrets } : {}),
2664
- ...(usdMicros(args, "max-total-cost")
2844
+ ...(usdMicros(args, "max-total-cost") !== undefined
2665
2845
  ? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
2666
2846
  : {}),
2667
2847
  });
@@ -2672,7 +2852,7 @@ async function jobCommand(argv, env, out) {
2672
2852
  }
2673
2853
  case "ls":
2674
2854
  case "list": {
2675
- const all = await (await api.list({ limit: 100 })).toArray(1000);
2855
+ const all = await (await api.list({ limit: 100 })).toArray(Infinity);
2676
2856
  print(all.length
2677
2857
  ? table([
2678
2858
  ["ID", "NAME", "STATE", "SCHEDULE", "NEXT RUN", "BLOCKED"],
@@ -2681,7 +2861,8 @@ async function jobCommand(argv, env, out) {
2681
2861
  j.name,
2682
2862
  j.state,
2683
2863
  scheduleText(j),
2684
- when(j.nextRunAt),
2864
+ // A canceled or finished job keeps its last nextRunAt, but runs no more.
2865
+ j.state === "active" || j.state === "paused" ? when(j.nextRunAt) : "-",
2685
2866
  j.blockedReason ?? "-",
2686
2867
  ]),
2687
2868
  ])
@@ -2695,7 +2876,7 @@ async function jobCommand(argv, env, out) {
2695
2876
  }
2696
2877
  case "runs": {
2697
2878
  const jobId = need(rest[0], "a job id");
2698
- const all = await (await api.runs(jobId, { limit: 100 })).toArray(1000);
2879
+ const all = await (await api.runs(jobId, { limit: 100 })).toArray(Infinity);
2699
2880
  print(all.length
2700
2881
  ? table([
2701
2882
  ["RUN", "ATTEMPT", "STATE", "EXIT", "SCHEDULED", "STARTED", "ENDED", "REASON"],
@@ -2724,7 +2905,7 @@ async function jobCommand(argv, env, out) {
2724
2905
  if (!(error instanceof NotFoundError))
2725
2906
  throw error;
2726
2907
  // A job's id: its latest run.
2727
- const runs = await (await api.runs(id, { limit: 100 })).toArray(1000);
2908
+ const runs = await (await api.runs(id, { limit: 100 })).toArray(Infinity);
2728
2909
  const latest = runs.at(-1);
2729
2910
  if (!latest) {
2730
2911
  const job = await api.get(id);
@@ -2789,9 +2970,13 @@ async function jobCommand(argv, env, out) {
2789
2970
  }
2790
2971
  const VOLUME_HELP = `runtime volume <command>
2791
2972
 
2792
- create --size-mib <N> [--name n] A persistent ext4 disk; prints its id
2973
+ create --size-mib <N> [--name n] [--shared] A persistent disk; prints its id
2793
2974
  ls Your volumes
2794
2975
  get <id> One volume, its backups and where it is attached
2976
+ resize <id> --size-mib <N> Grow a detached volume on its current host
2977
+ attach <id> <sandboxId> --path /data Mount a shared disk in a running sandbox
2978
+ detach <id> <attachmentId> Flush and unmount its shared attachment
2979
+ attachment <id> <attachmentId> Read the attachment's state and error
2795
2980
  rm <id> Delete it and everything on it (its backups stay)
2796
2981
  backup <id> [--retention-days N] [--no-wait]
2797
2982
  Back it up now, off its host; prints the backup id
@@ -2802,6 +2987,7 @@ const VOLUME_HELP = `runtime volume <command>
2802
2987
  Daily backups on or off, and how long each is kept
2803
2988
  Attach one: runtime sandbox create --volume <id>:/data
2804
2989
  Volumes are backed up off their host daily unless you turn it off.
2990
+ Shared commands work where shared disks are enabled.
2805
2991
  `;
2806
2992
  async function volume(argv, env, out) {
2807
2993
  const [verb, ...rest] = argv;
@@ -2813,17 +2999,18 @@ async function volume(argv, env, out) {
2813
2999
  const api = (await client(env)).volumes;
2814
3000
  switch (verb) {
2815
3001
  case "create": {
2816
- const args = parse(rest, [], ["size-mib", "name"]);
3002
+ const args = parse(rest, ["shared"], ["size-mib", "name"]);
2817
3003
  const created = await api.create({
2818
3004
  sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
2819
3005
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
3006
+ ...(has(args, "shared") ? { shared: true } : {}),
2820
3007
  });
2821
3008
  print(created.id, created);
2822
3009
  return 0;
2823
3010
  }
2824
3011
  case "ls":
2825
3012
  case "list": {
2826
- const all = await (await api.list()).toArray(1000);
3013
+ const all = await (await api.list()).toArray(Infinity);
2827
3014
  print(all.length
2828
3015
  ? table([
2829
3016
  ["ID", "NAME", "STATE", "MIB", "BACKED UP", "ATTACHED"],
@@ -2841,9 +3028,43 @@ async function volume(argv, env, out) {
2841
3028
  }
2842
3029
  case "get": {
2843
3030
  const found = await api.get(need(rest[0], "a volume id"));
2844
- print(JSON.stringify(found, null, 2), found);
3031
+ print(describeRecord(found, table), found);
2845
3032
  return 0;
2846
3033
  }
3034
+ case "resize": {
3035
+ const args = parse(rest, [], ["size-mib"]);
3036
+ const id = need(args.positional[0], "a volume id");
3037
+ if (args.positional.length !== 1 || args.rest)
3038
+ throw usage(`Use ${me} volume resize <id> --size-mib <N>.`);
3039
+ const sizeMiB = integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>"));
3040
+ const resized = await api.resize(id, { sizeMiB });
3041
+ print(describeRecord(resized, table), resized);
3042
+ return resized.resize?.state === "failed" ? 1 : 0;
3043
+ }
3044
+ case "attach": {
3045
+ const args = parse(rest, [], ["path"]);
3046
+ const id = need(args.positional[0], "a volume id");
3047
+ const sandboxId = need(args.positional[1], "a sandbox id");
3048
+ const path = need(flag(args, "path"), "--path /data");
3049
+ if (args.positional.length !== 2 || args.rest)
3050
+ throw usage(`Use ${me} volume attach <id> <sandboxId> --path /data.`);
3051
+ const attachment = await api.attach(id, { sandboxId, path });
3052
+ print(describeRecord(attachment, table), attachment);
3053
+ return attachment.error ? 1 : 0;
3054
+ }
3055
+ case "detach":
3056
+ case "attachment": {
3057
+ const args = parse(rest, [], []);
3058
+ const id = need(args.positional[0], "a volume id");
3059
+ const attachmentId = need(args.positional[1], "an attachment id");
3060
+ if (args.positional.length !== 2 || args.rest)
3061
+ throw usage(`Use ${me} volume ${verb} <id> <attachmentId>.`);
3062
+ const attachment = verb === "detach"
3063
+ ? await api.detach(id, attachmentId)
3064
+ : await api.getAttachment(id, attachmentId);
3065
+ print(describeRecord(attachment, table), attachment);
3066
+ return verb === "detach" && attachment.error ? 1 : 0;
3067
+ }
2847
3068
  case "rm":
2848
3069
  case "delete": {
2849
3070
  const found = await api.delete(need(rest[0], "a volume id"));
@@ -2862,7 +3083,7 @@ async function volume(argv, env, out) {
2862
3083
  }
2863
3084
  case "backups": {
2864
3085
  const volumeId = rest[0];
2865
- const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(1000);
3086
+ const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(Infinity);
2866
3087
  print(all.length
2867
3088
  ? table([
2868
3089
  ["ID", "VOLUME", "STATE", "TRIGGER", "MIB", "STORED", "TAKEN", "EXPIRES"],
@@ -2915,6 +3136,10 @@ async function volume(argv, env, out) {
2915
3136
  "create",
2916
3137
  "ls",
2917
3138
  "get",
3139
+ "resize",
3140
+ "attach",
3141
+ "detach",
3142
+ "attachment",
2918
3143
  "rm",
2919
3144
  "backup",
2920
3145
  "backups",
@@ -2966,6 +3191,7 @@ const CREATE_VALUES = [
2966
3191
  "image",
2967
3192
  "snapshot",
2968
3193
  "volume",
3194
+ "env",
2969
3195
  ];
2970
3196
  /** Dollars from the command line as integer microdollars: 25, 2.50 or 0.000001. */
2971
3197
  function usdMicros(args, name) {
@@ -2974,7 +3200,13 @@ function usdMicros(args, name) {
2974
3200
  return undefined;
2975
3201
  if (!/^\d+(\.\d{1,6})?$/.test(value))
2976
3202
  throw usage(`--${name} takes dollars, such as 25 or 2.50.`);
2977
- return Math.round(Number(value) * 1e6);
3203
+ // ARCHITECTURE.md section 10: integer microdollars throughout, with no
3204
+ // floating-point money path or rounding drift.
3205
+ const [whole, fraction = ""] = value.split(".");
3206
+ const micros = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
3207
+ if (micros > BigInt(Number.MAX_SAFE_INTEGER))
3208
+ throw usage(`--${name} is too large to represent exactly in microdollars.`);
3209
+ return Number(micros);
2978
3210
  }
2979
3211
  /** A create body from the command line's flags. */
2980
3212
  function createInput(args) {
@@ -2993,17 +3225,24 @@ function createInput(args) {
2993
3225
  : {}),
2994
3226
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
2995
3227
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
2996
- ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
2997
- ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
2998
- ...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
3228
+ ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
3229
+ ...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
3230
+ ...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
3231
+ ...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
2999
3232
  ...(flag(args, "cpu") ? { cpu: oneOf(args, "cpu", ["shared", "reserved"]) } : {}),
3000
- ...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
3233
+ ...(integer(args, "cpu-floor") !== undefined
3234
+ ? { cpuFloorMillis: integer(args, "cpu-floor") }
3235
+ : {}),
3001
3236
  ...(flag(args, "region") ? { region: flag(args, "region") } : {}),
3002
- ...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
3003
- ...(usdMicros(args, "max-total-cost")
3237
+ ...(usdMicros(args, "max-cost") !== undefined
3238
+ ? { maxCostMicros: usdMicros(args, "max-cost") }
3239
+ : {}),
3240
+ ...(usdMicros(args, "max-total-cost") !== undefined
3004
3241
  ? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
3005
3242
  : {}),
3006
- ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
3243
+ ...(integer(args, "timeout") !== undefined
3244
+ ? { timeoutSeconds: integer(args, "timeout") }
3245
+ : {}),
3007
3246
  ...(flag(args, "on-timeout")
3008
3247
  ? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
3009
3248
  : {}),
@@ -3033,7 +3272,7 @@ ownInterrupt = true) {
3033
3272
  ...(stdin ? { stdin } : {}),
3034
3273
  ...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
3035
3274
  ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
3036
- ...(timeout ? { timeoutMs: timeout * 1000 } : {}),
3275
+ ...(timeout !== undefined ? { timeoutMs: timeout * 1000 } : {}),
3037
3276
  };
3038
3277
  // Ctrl-C stops the command in the sandbox too, and says so; without this
3039
3278
  // the CLI exited and the command ran on unseen.
@@ -3071,7 +3310,11 @@ ownInterrupt = true) {
3071
3310
  const lost = extras.lostOutput(result, sbx.id);
3072
3311
  if (lost && !out.json)
3073
3312
  out.error(lost);
3074
- const code = result.timedOut ? 124 : Math.min(255, Math.max(0, result.exitCode ?? 1));
3313
+ /* A command a signal ended comes back as minus the signal (-9 after the
3314
+ out-of-memory killer's SIGKILL); a shell says 128 + the signal. Clamped
3315
+ to 0 until 30 September 2026, a killed command exited 0. */
3316
+ const exit = result.exitCode ?? 1;
3317
+ const code = result.timedOut ? 124 : Math.min(255, exit < 0 ? 128 - exit : exit);
3075
3318
  return lost && code === 0 ? 1 : code;
3076
3319
  }
3077
3320
  async function shell(sbx, command) {
@@ -3102,6 +3345,10 @@ function defaultOut(argv) {
3102
3345
  return {
3103
3346
  json,
3104
3347
  write: (text) => process.stdout.write(text.endsWith("\n") ? text : `${text}\n`),
3348
+ drain: async () => {
3349
+ if (process.stdout.writableNeedDrain)
3350
+ await once(process.stdout, "drain");
3351
+ },
3105
3352
  error: (text) => process.stderr.write(`${text}\n`),
3106
3353
  };
3107
3354
  }
@@ -3119,7 +3366,9 @@ export function describeError(error, json) {
3119
3366
  details: error.details,
3120
3367
  },
3121
3368
  });
3122
- return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
3369
+ // In the command line's words: the option, not the request's field.
3370
+ const { message, hint } = cliRefusal(error);
3371
+ return `Error [${error.code}]: ${message}${hint ? `\nHint: ${hint}` : ""}${error.requestId
3123
3372
  ? // A refusal of what was sent is the caller's to fix; only a failure
3124
3373
  // of ours is worth a report.
3125
3374
  error.status >= 400 && error.status < 500
@@ -3157,21 +3406,38 @@ function spark(values) {
3157
3406
  }
3158
3407
  const mib = (bytes) => `${Math.round(bytes / 1_048_576).toLocaleString("en-US")} MiB`;
3159
3408
  function metricsText(m) {
3409
+ const waiting = (value) => value == null ? "not measured" : `${value}%`;
3410
+ const waitingHistory = (field) => m.points.some((point) => point[field] != null)
3411
+ ? spark(m.points.map((point) => point[field] ?? null))
3412
+ : "not measured";
3160
3413
  const cpu = m.points.map((p) => p.cpuPercent);
3161
3414
  const memory = m.points.map((p) => p.memoryBytes);
3162
3415
  const peak = Math.max(0, ...m.points.map((p) => p.cpuPeakPercent ?? p.cpuPercent ?? 0));
3163
- const latest = m.latest;
3416
+ // `latest` is the newest reading of the last 15 minutes whatever the state,
3417
+ // so a sandbox paused a minute ago still has one from before it slept. Only a
3418
+ // running sandbox has a now.
3419
+ const latest = m.state === "running" ? m.latest : null;
3420
+ const still = m.state === "running" ? null : `none: it is ${m.state}`;
3421
+ const cpuNow = newestCpu(m);
3164
3422
  return [
3165
3423
  table([
3166
3424
  ["state", m.state],
3167
3425
  [
3168
3426
  "cpu now",
3169
- latest?.cpuPercent == null
3170
- ? "no reading in the last 15 minutes"
3171
- : `${latest.cpuPercent}% of ${m.vcpu} vCPU (${latest.cpuCores} cores)`,
3427
+ still ??
3428
+ (cpuNow
3429
+ ? `${cpuNow.percent}% of ${m.vcpu} vCPU${cpuNow.cores == null ? "" : ` (${cpuNow.cores} cores)`}${cpuNow.ago ? `, ${cpuNow.ago}` : ""}`
3430
+ : "no reading in this range"),
3431
+ ],
3432
+ [
3433
+ "memory now",
3434
+ still ?? (latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"),
3172
3435
  ],
3173
- ["memory now", latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"],
3436
+ ["cpu wait now", still ?? waiting(latest?.cpuWaitPercent)],
3437
+ ["memory stalls now", still ?? waiting(latest?.memoryStallPercent)],
3174
3438
  [`cpu, ${m.range}`, m.points.length ? `${spark(cpu)} peak ${peak}%` : "no readings"],
3439
+ [`cpu wait, ${m.range}`, waitingHistory("cpuWaitPercent")],
3440
+ [`memory stalls, ${m.range}`, waitingHistory("memoryStallPercent")],
3175
3441
  [
3176
3442
  `memory, ${m.range}`,
3177
3443
  m.points.length
@@ -3266,7 +3532,7 @@ async function webhooksCommand(argv, env, out) {
3266
3532
  }
3267
3533
  case "deliveries": {
3268
3534
  const page = await hooks.deliveries(id(), {
3269
- ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
3535
+ ...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
3270
3536
  });
3271
3537
  print(page.data.length
3272
3538
  ? table(page.data.map((d) => [