withruntime 0.12.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/BLAXEL.md +97 -32
  2. package/COMPATIBILITY.md +120 -5
  3. package/DAYTONA.md +34 -3
  4. package/E2B.md +34 -5
  5. package/README.md +36 -3
  6. package/README.src.md +35 -2
  7. package/VERCEL.md +52 -5
  8. package/compatibility-lock.json +71 -111
  9. package/dist/billing-cli.js +8 -4
  10. package/dist/blaxel/client.d.ts +11 -0
  11. package/dist/blaxel/client.js +28 -0
  12. package/dist/blaxel/errors.d.ts +65 -0
  13. package/dist/blaxel/errors.js +114 -0
  14. package/dist/blaxel/index.d.ts +10 -3
  15. package/dist/blaxel/index.js +23 -4
  16. package/dist/blaxel/preview.d.ts +29 -4
  17. package/dist/blaxel/preview.js +79 -5
  18. package/dist/blaxel/sandbox.d.ts +1 -1
  19. package/dist/blaxel/sandbox.js +2 -1
  20. package/dist/blaxel/session.js +4 -2
  21. package/dist/cli-extras.js +2 -1
  22. package/dist/cli-words.js +1 -0
  23. package/dist/cli.d.ts +17 -2
  24. package/dist/cli.js +291 -85
  25. package/dist/client.d.ts +1 -1
  26. package/dist/credentials.d.ts +3 -0
  27. package/dist/credentials.js +15 -0
  28. package/dist/daytona/client.d.ts +4 -0
  29. package/dist/daytona/client.js +8 -3
  30. package/dist/daytona/daytona.d.ts +4 -1
  31. package/dist/daytona/daytona.js +3 -2
  32. package/dist/e2b/sandbox.d.ts +7 -6
  33. package/dist/e2b/sandbox.js +9 -12
  34. package/dist/errors.d.ts +1 -1
  35. package/dist/errors.js +1 -1
  36. package/dist/freestyle/index.d.ts +77 -5
  37. package/dist/freestyle/index.js +380 -48
  38. package/dist/identity.js +36 -18
  39. package/dist/index.d.ts +4 -3
  40. package/dist/machine.d.ts +39 -13
  41. package/dist/machine.js +72 -25
  42. package/dist/mcp.js +157 -20
  43. package/dist/modal/index.d.ts +15 -0
  44. package/dist/modal/index.js +35 -7
  45. package/dist/morph/index.d.ts +255 -0
  46. package/dist/morph/index.js +641 -0
  47. package/dist/network-cli.js +2 -2
  48. package/dist/product-names.d.ts +2 -2
  49. package/dist/product-names.js +2 -2
  50. package/dist/products/api-keys.d.ts +73 -0
  51. package/dist/products/api-keys.js +23 -0
  52. package/dist/products/apps.d.ts +12 -31
  53. package/dist/products/apps.js +8 -113
  54. package/dist/products/balancers.d.ts +87 -0
  55. package/dist/products/balancers.js +8 -0
  56. package/dist/products/builds.d.ts +30 -0
  57. package/dist/products/builds.js +6 -0
  58. package/dist/products/index.d.ts +2 -0
  59. package/dist/products/index.js +2 -0
  60. package/dist/products/jobs.d.ts +2 -2
  61. package/dist/products/jobs.js +1 -1
  62. package/dist/products/limits.d.ts +16 -2
  63. package/dist/products/limits.js +2 -2
  64. package/dist/products/mcp.d.ts +5 -0
  65. package/dist/products/network-products.d.ts +21 -3
  66. package/dist/products/network-products.js +3 -1
  67. package/dist/products/observability.d.ts +7 -2
  68. package/dist/products/previews.d.ts +6 -3
  69. package/dist/products/previews.js +1 -1
  70. package/dist/products/resources.d.ts +54 -0
  71. package/dist/products/resources.js +122 -0
  72. package/dist/products/secrets.d.ts +22 -0
  73. package/dist/products/secrets.js +8 -0
  74. package/dist/products/volumes.d.ts +3 -0
  75. package/dist/sandbox.d.ts +4 -0
  76. package/dist/sandbox.js +27 -2
  77. package/dist/snapshots.d.ts +4 -0
  78. package/dist/ssh.d.ts +35 -0
  79. package/dist/ssh.js +93 -1
  80. package/dist/transport.d.ts +14 -8
  81. package/dist/transport.js +70 -15
  82. package/dist/types.d.ts +33 -5
  83. package/dist/vercel/command.d.ts +48 -1
  84. package/dist/vercel/command.js +237 -6
  85. package/dist/vercel/sandbox-user.js +2 -1
  86. package/dist/vercel/sandbox.d.ts +17 -3
  87. package/dist/vercel/sandbox.js +46 -3
  88. package/dist/wallet.d.ts +4 -1
  89. package/dist/wallet.js +2 -1
  90. package/package.json +7 -14
  91. package/dist/cli/logs.test.d.ts +0 -1
  92. package/dist/cli/logs.test.js +0 -272
  93. package/dist/products/logs.test.d.ts +0 -1
  94. package/dist/products/logs.test.js +0 -343
package/dist/cli.js CHANGED
@@ -24,12 +24,14 @@ Usage: runtime <product> <command> [options] runtime <command> [options]
24
24
 
25
25
  Start
26
26
  sandboxes run [--keep] [create options] -- <command>
27
- A fresh sandbox runs one command, then stops
27
+ A fresh sandbox runs one command, then is deleted
28
28
 
29
29
  Account
30
30
  login [--agent-name <name>] [--no-browser] [--wait]
31
31
  Connect this machine (no key to copy)
32
32
  login --with-key Or paste a key from withruntime.com/account/keys
33
+ billing topup <usd> --new-account --accept-terms [--wait]
34
+ No person? Open an account paid in USDC
33
35
  logout Revoke this machine's connection
34
36
  whoami The organization and agent you act as
35
37
  account [ls] The accounts this machine is connected to
@@ -40,16 +42,22 @@ Account
40
42
  The account's audit log (owner or admin keys)
41
43
  sso Single sign-on and SCIM directory sync (owner or
42
44
  admin keys); an owner changes it on the website
43
- usage [--csv] Balance, included usage and charges; --csv, each resource
45
+ usage [--csv] Balance, free usage and charges; --csv, each resource
44
46
  usage export --since <UTC time> --until <UTC time>
45
47
  Every settlement in the range as CSV; --json, each page
46
48
  limits Whether this key is read-only, and its daily spending limit
47
49
  secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
48
50
  [--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
49
- value is read from standard input, never an argument.
50
- --allow (paid): only these methods and paths get it
51
+ [--only-label key=value[,k=v]]... value is read from standard input, never an argument.
52
+ [--only-sandbox <id>]... --allow (paid): only these methods and paths get it.
53
+ [--all-sandboxes] --only-label: only sandboxes with those labels (each
54
+ flag one selector, any may match); --only-sandbox:
55
+ only that sandbox. A new value keeps the scope;
56
+ --all-sandboxes gives it to every sandbox again
51
57
  secrets set <NAME> --jobs [--host ...] A copy scheduled jobs put in their environment
52
58
  (job create --secret NAME); with --host, both
59
+ secrets scope <NAME> --only-label k=v... | --only-sandbox <id>... | --all-sandboxes
60
+ Change which sandboxes get it; no value needed
53
61
  secrets ls | get <NAME> Names, hosts, placeholders and jobs versions, never values
54
62
  secrets rotate <NAME> A new version of the jobs copy, from standard input
55
63
  secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
@@ -80,6 +88,8 @@ Account
80
88
  Connect
81
89
  sandboxes ssh <id|name> [-- <command...>] Log in over SSH; \`runtime sandboxes ssh help\` for editors
82
90
  sandboxes ssh config [--install] Make \`ssh <id|name>.runtime\` work (VS Code, JetBrains)
91
+ sandboxes code <id|name> [--editor vscode|cursor|windsurf]
92
+ Open it in VS Code, Cursor or Windsurf over SSH
83
93
  sandboxes port-forward <id|name> <port|local:remote>...
84
94
  Forward local ports to ports in a sandbox
85
95
 
@@ -114,14 +124,17 @@ const SANDBOX_HELP = `runtime sandboxes <command>
114
124
 
115
125
  Every <id> below can also be the sandbox's name.
116
126
 
117
- run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
127
+ run [--keep] [create options] -- <command> A fresh sandbox runs one command, then is deleted
118
128
  ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
129
+ code <id|name> [--editor vscode|cursor|windsurf] [--path /workspace] [--print]
130
+ Open it in your editor over SSH, in one step
119
131
  port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
120
132
  create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
121
133
  [--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
122
134
  [--timeout <60-86400>] [--on-timeout pause|stop|delete] [--no-wait]
123
135
  [--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
124
136
  [--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
137
+ [--spread-from <id>]...
125
138
  [--idle-pause <seconds>] [--no-auto-wake] [--persistent] [--get-or-create]
126
139
  Create one; prints its id
127
140
  ls [--all] [--label k=v]... [--state running] List (live ones unless --all)
@@ -143,6 +156,8 @@ const SANDBOX_HELP = `runtime sandboxes <command>
143
156
  stop --label k=v [--label k=v]... Stop every live sandbox with all those labels
144
157
  rm <id> Delete it for good: stops it, deletes its disk and
145
158
  paused memory, revokes its previews; snapshots stay
159
+ rm --label k=v [--label k=v]... Delete for good every sandbox with all those labels,
160
+ stopped ones too
146
161
  switch-image <id> <image> --keep-workspace Move it to another image, keeping /workspace (your home),
147
162
  volumes, env and previews; processes restart, the rest of
148
163
  its disk is lost (snapshot it first to keep everything)
@@ -152,6 +167,7 @@ const SANDBOX_HELP = `runtime sandboxes <command>
152
167
  extend <id> <seconds> Move a time limit on (one with none needs nothing)
153
168
  update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
154
169
  [--persistent on|off] [--max-total-cost <usd>|none]
170
+ [--timeout <60-86400 from now>|0] [--on-timeout pause|stop|delete]
155
171
  Change its settings; persistent keeps it running until stopped
156
172
  snapshot <id> [--name n] [--retention days|credit] Keep its whole machine; prints the snapshot id
157
173
  mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
@@ -169,8 +185,9 @@ const SANDBOX_HELP = `runtime sandboxes <command>
169
185
  Print file changes as they happen, until Ctrl-C
170
186
  mcp catalog MCP servers a sandbox can run (licence, settings, hosts)
171
187
  mcp <id> start <server>... [--secret SERVER.SETTING=SECRET]... [--env SERVER.NAME=value]...
172
- [--option SERVER.NAME=value]... [--port 8765] [--replace]
188
+ [--option SERVER.NAME=value]... [--port 8765] [--replace] [--public]
173
189
  Start them; prints each URL and the header to send
190
+ (--public: URLs that never expire, paid only)
174
191
  mcp <id> [status] | mcp <id> stop Their state and fresh URLs; stop them
175
192
  preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address (--public: paid only)
176
193
  preview rotate <id> <port> Refuse every token given out for a private port so
@@ -242,7 +259,14 @@ commandAfter) {
242
259
  break;
243
260
  }
244
261
  if (arg.startsWith("--") || (arg.startsWith("-") && arg.length === 2 && !/^-\d/.test(arg))) {
245
- const [name, inline] = arg.replace(/^-+/, "").split(/=(.*)/s, 2);
262
+ const [typed, inline] = arg.replace(/^-+/, "").split(/=(.*)/s, 2);
263
+ // A plural of an option is the option once per comma: --hosts a,b is
264
+ // --host a --host b (13 October 2026: a user's first try at a secret).
265
+ const plural = !known.has(typed) &&
266
+ typed.endsWith("s") &&
267
+ LISTS.has(typed.slice(0, -1)) &&
268
+ known.has(typed.slice(0, -1));
269
+ const name = plural ? typed.slice(0, -1) : typed;
246
270
  if (!known.has(name))
247
271
  throw commandAfter && !meantOption(name, known)
248
272
  ? usage(`Unknown option ${arg.split("=")[0]}. If it belongs to the command, put the command after --: ${commandAfter}`)
@@ -254,13 +278,16 @@ commandAfter) {
254
278
  throw usage(`--${name} needs a value.`);
255
279
  if (boolean && value !== "true" && value !== "false")
256
280
  throw usage(`--${name} takes true or false.`);
257
- flags.set(name, [...(flags.get(name) ?? []), value]);
281
+ const values = plural ? value.split(",").filter(Boolean) : [value];
282
+ flags.set(name, [...(flags.get(name) ?? []), ...values]);
258
283
  }
259
284
  else
260
285
  positional.push(arg);
261
286
  }
262
287
  return { positional, flags, rest };
263
288
  }
289
+ /** Options given once per item, which a plural takes as a comma list. */
290
+ const LISTS = new Set(["host", "allow", "deny", "connect"]);
264
291
  /** The declared option a mistyped one most likely meant. --memory-mib for
265
292
  * --memory, --timeout-seconds for --timeout: the SDK's spelling of an option is
266
293
  * the CLI's with its unit added. */
@@ -408,6 +435,7 @@ const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
408
435
  const SANDBOX_VERBS = [
409
436
  "run",
410
437
  "ssh",
438
+ "code",
411
439
  "port-forward",
412
440
  "create",
413
441
  "ls",
@@ -565,12 +593,12 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
565
593
  out.error(`Warning: ${named(words.warning)}`);
566
594
  const [first, ...remaining] = words.argv;
567
595
  // Sandbox is one product of several, so its commands live under it
568
- // (AGENTS.md, 23 September 2026): `runtime sandboxes run`, `sandbox ssh` and
596
+ // (23 September 2026): `runtime sandboxes run`, `sandbox ssh` and
569
597
  // `sandbox port-forward`. The old top-level forms were removed, not aliased.
570
598
  if (first && SANDBOX_MOVED.includes(first))
571
599
  throw usage(`\`${me} ${first}\` is now \`${me} sandboxes ${first === "forward" ? "port-forward" : first}\`.`);
572
600
  const nested = (first === "sandbox" || first === "sandboxes" || first === "sbx") &&
573
- SANDBOX_MOVED.includes(remaining[0] ?? "");
601
+ [...SANDBOX_MOVED, "code"].includes(remaining[0] ?? "");
574
602
  const command = nested ? remaining[0] : first;
575
603
  const rest = nested ? remaining.slice(1) : remaining;
576
604
  // `runtime sandboxes create --help` is how an agent asks what a command takes:
@@ -607,7 +635,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
607
635
  : `Runtime Cloud ${VERSION}. ${start}
608
636
 
609
637
  ${me} sandboxes run -- python3 < invoice.py
610
- Run a local invoice script, then stop the sandbox
638
+ Run a local invoice script, then delete the sandbox
611
639
  ${me} sandboxes create A sandbox that stays; prints its id
612
640
  ${me} sandboxes exec <id> -- <command> Run in it
613
641
  ${me} help Everything else
@@ -629,24 +657,23 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
629
657
  const onCapacityWait = waiting(out);
630
658
  const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
631
659
  let cleanup;
632
- // SIGINT and normal completion share one stop. Keep an absolute deadline
633
- // across the stop retries and final state check, so cleanup cannot hang.
660
+ // SIGINT and normal completion share one delete, with an absolute
661
+ // deadline across its retries, so cleanup cannot hang. A delete, not a
662
+ // stop: a stopped sandbox keeps its disk, billed, and an account without
663
+ // credit keeps at most 10, so ten runs that only stopped refused the
664
+ // eleventh (no_credit_kept_limit, 0685). --keep is how to keep one.
634
665
  const stop = () => (cleanup ??= (async () => {
635
666
  const signal = AbortSignal.timeout(30_000);
636
667
  try {
637
- await sbx.stop({ wait: false, signal });
638
- if (sbx.state !== "stopped")
639
- await sbx.waitFor("stopped", { timeoutSeconds: 25, signal });
640
- if (sbx.state !== "stopped")
641
- throw new Error(`Sandbox is still ${sbx.state}.`);
668
+ await sbx.delete({ signal });
642
669
  return true;
643
670
  }
644
671
  catch (error) {
645
672
  out.error(describeError(new RuntimeError({
646
673
  code: "cleanup_failed",
647
674
  status: 0,
648
- message: `Could not confirm that sandbox ${sbx.id} stopped. ${error instanceof Error ? error.message : String(error)}`,
649
- hint: `Check it and retry cleanup with: ${me} sandboxes stop ${sbx.id}`,
675
+ message: `Could not confirm that sandbox ${sbx.id} was deleted. ${error instanceof Error ? error.message : String(error)}`,
676
+ hint: `Check it and retry cleanup with: ${me} sandboxes rm ${sbx.id}`,
650
677
  cause: error,
651
678
  }), out.json));
652
679
  return false;
@@ -706,11 +733,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
706
733
  ...(spend.pilot ? [pilotFunding(spend.pilot)] : []),
707
734
  ...(trialLeft > 0
708
735
  ? [
709
- `included usage, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left without credit`,
736
+ `free usage, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left without credit`,
710
737
  ]
711
738
  : []),
712
739
  ...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
713
- ].join("; ") || "no included usage or credit left: runtime billing";
740
+ ].join("; ") || "no free usage or credit left: runtime billing";
714
741
  print(table([
715
742
  ["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
716
743
  ...(who.role ? [["role", who.role]] : []),
@@ -1015,6 +1042,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
1015
1042
  product = "ssh";
1016
1043
  return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
1017
1044
  }
1045
+ if (nested && command === "code") {
1046
+ product = "ssh";
1047
+ return (await import("./ssh.js")).editorCommand(rest, env, out, () => client(env), me);
1048
+ }
1018
1049
  if (command === "port-forward" || command === "forward") {
1019
1050
  product = "ssh";
1020
1051
  return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
@@ -1049,7 +1080,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
1049
1080
  }
1050
1081
  throw unknown("command", command, COMMANDS);
1051
1082
  }
1052
- /** `runtime usage` for a person: the balance, the included usage and what each kind
1083
+ /** `runtime usage` for a person: the balance, the free usage and what each kind
1053
1084
  * of resource was charged, in dollars. --json keeps every figure, and each
1054
1085
  * resource with its rates and CPU time. */
1055
1086
  /** One row per resource, for a spreadsheet: when it ran, how big it was, how
@@ -1104,15 +1135,15 @@ export function usageCsv(u) {
1104
1135
  /** Milliseconds as hours, to a tenth: "8,731.2". */
1105
1136
  const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
1106
1137
  /** The machine time left without credit, as `runtime usage` and `runtime
1107
- * limits` say it: this month's included usage, as hours at 2 vCPU and 4 GiB,
1138
+ * limits` say it: this month's free usage, as hours at 2 vCPU and 4 GiB,
1108
1139
  * or for an account given free hours before 5 October 2026, those hours too. */
1109
1140
  function trialLeft(trial) {
1110
1141
  const renews = trial.renewsAt ? `, renews ${trial.renewsAt.slice(0, 10)}` : "";
1111
1142
  return trial.totalMs > 0
1112
- ? `${hours(trial.availableMs)} hours left (${hours(trial.totalMs)} free hours given before 5 October 2026, and the included usage${renews})`
1143
+ ? `${hours(trial.availableMs)} hours left (${hours(trial.totalMs)} free hours given before 5 October 2026, and the free usage${renews})`
1113
1144
  : `${hours(trial.availableMs)} hours at 2 vCPU and 4 GiB left this month${renews}`;
1114
1145
  }
1115
- /** This month's included usage, one row per product, as `runtime usage`
1146
+ /** This month's free usage, one row per product, as `runtime usage`
1116
1147
  * says it. */
1117
1148
  function allowanceRows(allowances) {
1118
1149
  const amount = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
@@ -1598,6 +1629,28 @@ async function sandbox(argv, env, out) {
1598
1629
  const use = async (id) => SANDBOX_UUID.test(id ?? "")
1599
1630
  ? new SandboxHandle(runtime.transport, { id })
1600
1631
  : get(id);
1632
+ /* `stop --label` and `rm --label`: every sandbox with all the labels, each
1633
+ named with what happened to it; exits 1 if any failed. */
1634
+ const byLabel = (words) => words.some((word) => /^--label(=|$)/.test(word));
1635
+ const everyLabelled = async (act, words) => {
1636
+ const args = parse(words, [], ["label"]);
1637
+ if (args.positional.length)
1638
+ throw usage("Give a sandbox id or --label, not both.");
1639
+ const labels = pairs(args, "label");
1640
+ const { done, failed } = act === "stop"
1641
+ ? await runtime.sandboxes.stopAll({ labels }).then((r) => ({ ...r, done: r.stopped }))
1642
+ : await runtime.sandboxes.deleteAll({ labels }).then((r) => ({ ...r, done: r.deleted }));
1643
+ const past = act === "stop" ? "stopped" : "deleted";
1644
+ const said = (error) => (error instanceof Error ? error.message : String(error));
1645
+ print([
1646
+ done.length || failed.length
1647
+ ? `${done.length} ${past}${failed.length ? `, ${failed.length} failed` : ""}.`
1648
+ : `No ${act === "stop" ? "live " : ""}sandbox has those labels.`,
1649
+ ...done.map((id) => `${id} ${past}`),
1650
+ ...failed.map((f) => `${f.id} failed: ${said(f.error)}`),
1651
+ ].join("\n"), { [past]: done, failed: failed.map((f) => ({ id: f.id, error: said(f.error) })) });
1652
+ return failed.length ? 1 : 0;
1653
+ };
1601
1654
  switch (verb) {
1602
1655
  case "create": {
1603
1656
  const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
@@ -1681,6 +1734,8 @@ async function sandbox(argv, env, out) {
1681
1734
  }
1682
1735
  case "rm":
1683
1736
  case "delete": {
1737
+ if (byLabel(rest))
1738
+ return everyLabelled("delete", rest);
1684
1739
  const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandboxes rm <id>`));
1685
1740
  const gone = await sbx.delete();
1686
1741
  print(`${gone.id} deleted`, gone);
@@ -1690,23 +1745,8 @@ async function sandbox(argv, env, out) {
1690
1745
  case "pause":
1691
1746
  case "wake":
1692
1747
  case "restart": {
1693
- if (verb === "stop" && rest.some((word) => /^--label(=|$)/.test(word))) {
1694
- const args = parse(rest, [], ["label"]);
1695
- if (args.positional.length)
1696
- throw usage("Give a sandbox id or --label, not both.");
1697
- const { stopped, failed } = await runtime.sandboxes.stopAll({
1698
- labels: pairs(args, "label"),
1699
- });
1700
- const said = (error) => (error instanceof Error ? error.message : String(error));
1701
- print([
1702
- stopped.length || failed.length
1703
- ? `${stopped.length} stopped${failed.length ? `, ${failed.length} failed` : ""}.`
1704
- : "No live sandbox has those labels.",
1705
- ...stopped.map((id) => `${id} stopped`),
1706
- ...failed.map((f) => `${f.id} failed: ${said(f.error)}`),
1707
- ].join("\n"), { stopped, failed: failed.map((f) => ({ id: f.id, error: said(f.error) })) });
1708
- return failed.length ? 1 : 0;
1709
- }
1748
+ if (verb === "stop" && byLabel(rest))
1749
+ return everyLabelled("stop", rest);
1710
1750
  const sbx = await get(rest[0]);
1711
1751
  await sbx[verb]();
1712
1752
  print(`${sbx.id} ${sbx.state}`, sbx.info);
@@ -1823,6 +1863,8 @@ async function sandbox(argv, env, out) {
1823
1863
  "auto-wake",
1824
1864
  "persistent",
1825
1865
  "max-total-cost",
1866
+ "timeout",
1867
+ "on-timeout",
1826
1868
  ]);
1827
1869
  const sbx = await get(args.positional[0]);
1828
1870
  const onOff = (name) => {
@@ -1833,6 +1875,9 @@ async function sandbox(argv, env, out) {
1833
1875
  throw usage(`--${name} takes on or off.`);
1834
1876
  return value === "on";
1835
1877
  };
1878
+ const onTimeout = flag(args, "on-timeout")
1879
+ ? oneOf(args, "on-timeout", ["pause", "stop", "delete"])
1880
+ : undefined;
1836
1881
  const cost = flag(args, "max-total-cost");
1837
1882
  if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
1838
1883
  throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
@@ -1854,9 +1899,13 @@ async function sandbox(argv, env, out) {
1854
1899
  : {
1855
1900
  maxTotalCostMicros: cost === "none" ? null : usdMicros(args, "max-total-cost"),
1856
1901
  }),
1902
+ ...(integer(args, "timeout") !== undefined
1903
+ ? { timeoutSeconds: integer(args, "timeout") }
1904
+ : {}),
1905
+ ...(onTimeout === undefined ? {} : { onTimeout }),
1857
1906
  };
1858
1907
  if (!Object.keys(settings).length)
1859
- throw usage("Say what to change: --name, --label, --env, --unset-env, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
1908
+ throw usage("Say what to change: --name, --label, --env, --unset-env, --idle-pause, --auto-wake, --persistent, --max-total-cost, --timeout or --on-timeout.");
1860
1909
  await sbx.update(settings);
1861
1910
  print(describe(sbx.info), sbx.info);
1862
1911
  return 0;
@@ -2160,15 +2209,24 @@ function attachment(spec) {
2160
2209
  /** A secret's value, from standard input only, so it never sits in shell
2161
2210
  * history or a process list. Piped: everything, less one trailing newline.
2162
2211
  * A terminal: one line, not shown. */
2212
+ /** A pasted secret as the header it becomes: no line breaks (a token copied
2213
+ * from a terminal arrives wrapped), no bracketed-paste markers, no space at
2214
+ * either end. 13 October 2026: a user's pasted Claude token was refused as
2215
+ * "not visible ASCII". A header cannot hold a line break, so none is lost. */
2216
+ export function cleanSecret(raw) {
2217
+ return raw
2218
+ .replaceAll("\u001b[200~", "")
2219
+ .replaceAll("\u001b[201~", "")
2220
+ .replace(/[\r\n]/g, "")
2221
+ .trim();
2222
+ }
2163
2223
  async function readSecret() {
2164
2224
  const stdin = process.stdin;
2165
2225
  if (!stdin.isTTY) {
2166
2226
  const chunks = [];
2167
2227
  for await (const chunk of stdin)
2168
2228
  chunks.push(chunk);
2169
- return Buffer.concat(chunks)
2170
- .toString("utf8")
2171
- .replace(/\r?\n$/, "");
2229
+ return cleanSecret(Buffer.concat(chunks).toString("utf8"));
2172
2230
  }
2173
2231
  process.stderr.write("Value (not shown), then Enter: ");
2174
2232
  stdin.setRawMode(true);
@@ -2181,7 +2239,7 @@ async function readSecret() {
2181
2239
  if (char === "\r" || char === "\n") {
2182
2240
  stdin.off("data", onData);
2183
2241
  process.stderr.write("\n");
2184
- return done(value);
2242
+ return done(cleanSecret(value));
2185
2243
  }
2186
2244
  if (char === "\u0003") {
2187
2245
  stdin.off("data", onData);
@@ -2224,6 +2282,48 @@ export function parseSecretRules(values) {
2224
2282
  }
2225
2283
  return [...byMethods.values()];
2226
2284
  }
2285
+ /** `--only-label role=hub,tier=x --only-sandbox <id>`: a sandbox whose labels
2286
+ * hold every pair of any one --only-label gets the secret, and so does each
2287
+ * --only-sandbox. `--all-sandboxes`: every sandbox again. Nothing: undefined,
2288
+ * so a replacement keeps the scope it has. */
2289
+ export function parseSecretScope(labels, sandboxes, all) {
2290
+ if (all && (labels?.length || sandboxes?.length))
2291
+ throw usage("--all-sandboxes gives the secret to every sandbox; leave out --only-label and --only-sandbox.");
2292
+ if (all)
2293
+ return {};
2294
+ if (!labels?.length && !sandboxes?.length)
2295
+ return undefined;
2296
+ const selectors = (labels ?? []).map((text) => {
2297
+ const selector = {};
2298
+ for (const pair of text.split(",").filter(Boolean)) {
2299
+ const at = pair.indexOf("=");
2300
+ if (at < 1)
2301
+ throw usage(`--only-label takes key=value pairs, such as role=hub; got "${text}".`);
2302
+ selector[pair.slice(0, at).trim()] = pair.slice(at + 1).trim();
2303
+ }
2304
+ if (!Object.keys(selector).length)
2305
+ throw usage(`--only-label takes key=value pairs, such as role=hub; got "${text}".`);
2306
+ return selector;
2307
+ });
2308
+ for (const id of sandboxes ?? [])
2309
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id))
2310
+ throw usage(`--only-sandbox takes a sandbox id; "${id}" is not one.`);
2311
+ return {
2312
+ ...(selectors.length ? { labels: selectors } : {}),
2313
+ ...(sandboxes?.length
2314
+ ? { sandboxes: [...new Set(sandboxes.map((id) => id.toLowerCase()))] }
2315
+ : {}),
2316
+ };
2317
+ }
2318
+ /** Who gets a secret, in a few words. */
2319
+ export const scopeText = (only) => only && (only.labels?.length || only.sandboxes?.length)
2320
+ ? [
2321
+ ...(only.labels ?? []).map((selector) => Object.entries(selector)
2322
+ .map(([key, value]) => `${key}=${value}`)
2323
+ .join(",")),
2324
+ ...(only.sandboxes ?? []),
2325
+ ].join(" | ")
2326
+ : "all";
2227
2327
  const ruleText = (rules) => rules?.length
2228
2328
  ? rules
2229
2329
  .map((rule) => `${rule.methods ? `${rule.methods.join(",")} ` : ""}${rule.paths.join(" ")}`)
@@ -2233,17 +2333,18 @@ export async function secretsCommand(argv, env, out,
2233
2333
  /** Tests give the value here; the CLI reads standard input. */
2234
2334
  read = readSecret) {
2235
2335
  const [verb, ...rest] = argv;
2236
- const args = parse(rest, ["jobs"], ["host", "header", "format", "allow", "value", "version"]);
2336
+ const args = parse(rest, ["jobs", "all-sandboxes"], ["host", "header", "format", "allow", "value", "version", "only-label", "only-sandbox"]);
2237
2337
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2238
2338
  const rows = (list) => list.length
2239
2339
  ? table([
2240
- ["name", "hosts", "header", "placeholder", "allow", "jobs"],
2340
+ ["name", "hosts", "header", "placeholder", "allow", "sandboxes", "jobs"],
2241
2341
  ...list.map(({ name, sandboxes: secret, jobs }) => [
2242
2342
  name,
2243
2343
  secret ? secret.hosts.join(",") : "-",
2244
2344
  secret?.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
2245
2345
  secret ? secret.placeholder : "-",
2246
2346
  secret ? ruleText(secret.rules) : "-",
2347
+ secret ? scopeText(secret.only) : "-",
2247
2348
  jobs ? `v${jobs.version}` : "-",
2248
2349
  ]),
2249
2350
  ])
@@ -2266,7 +2367,13 @@ read = readSecret) {
2266
2367
  const name = need(args.positional[0], "the secret's name, such as OPENAI_API_KEY");
2267
2368
  const hosts = args.flags.get("host") ?? [];
2268
2369
  const jobs = verb === "rotate" || has(args, "jobs");
2269
- if (verb === "rotate" && (hosts.length || args.flags.has("header") || args.flags.has("allow")))
2370
+ if (verb === "rotate" &&
2371
+ (hosts.length ||
2372
+ args.flags.has("header") ||
2373
+ args.flags.has("allow") ||
2374
+ args.flags.has("only-label") ||
2375
+ args.flags.has("only-sandbox") ||
2376
+ args.flags.has("all-sandboxes")))
2270
2377
  throw usage(`rotate replaces the copy for jobs. ${jobsOnly}; to change the sandboxes' copy, set it again with --host.`);
2271
2378
  if (!hosts.length && !jobs)
2272
2379
  throw usage("Give --host once for each host the value may go to, such as --host api.openai.com, and/or --jobs to let scheduled jobs use it.");
@@ -2290,16 +2397,20 @@ read = readSecret) {
2290
2397
  const format = flag(args, "format");
2291
2398
  const allow = args.flags.get("allow");
2292
2399
  const rules = allow ? parseSecretRules(allow) : undefined;
2400
+ const only = parseSecretScope(args.flags.get("only-label"), args.flags.get("only-sandbox"), has(args, "all-sandboxes"));
2293
2401
  const saved = await runtime.secrets.set(name, {
2294
2402
  value,
2295
2403
  hosts,
2296
2404
  ...(header ? { header } : {}),
2297
2405
  ...(format ? { format } : {}),
2298
2406
  ...(rules ? { rules } : {}),
2407
+ ...(only ? { only } : {}),
2299
2408
  ...(jobs ? { jobs: true } : {}),
2300
2409
  });
2301
2410
  print([
2302
- `Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
2411
+ saved.only
2412
+ ? `Stored ${saved.name}. Only these sandboxes get it and see ${saved.name}=${saved.placeholder}: ${scopeText(saved.only)}.`
2413
+ : `Stored ${saved.name}. Every sandbox sees ${saved.name}=${saved.placeholder}.`,
2303
2414
  saved.header
2304
2415
  ? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
2305
2416
  : `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the Authorization header or another credential header (x-api-key, x-goog-api-key); in the URL it stays a placeholder.`,
@@ -2315,6 +2426,15 @@ read = readSecret) {
2315
2426
  ].join("\n"), saved);
2316
2427
  return 0;
2317
2428
  }
2429
+ if (verb === "scope") {
2430
+ const name = need(args.positional[0], "the secret's name");
2431
+ const only = parseSecretScope(args.flags.get("only-label"), args.flags.get("only-sandbox"), has(args, "all-sandboxes"));
2432
+ if (!only)
2433
+ throw usage("Say who gets it: --only-label key=value (once per selector), --only-sandbox <id>, or --all-sandboxes.");
2434
+ const saved = await (await client(env)).secrets.scope(name, only);
2435
+ print(`${saved.name}: ${saved.only ? `only ${scopeText(saved.only)}` : "every sandbox"}.${saved.enforced ? "" : " A host is catching up; running sandboxes have it within a minute."}`, saved);
2436
+ return 0;
2437
+ }
2318
2438
  if (verb === "reveal") {
2319
2439
  const name = need(args.positional[0], "the secret's name");
2320
2440
  const version = flag(args, "version");
@@ -2330,7 +2450,7 @@ read = readSecret) {
2330
2450
  print(`Deleted ${name}${removed.from.length > 1 ? " for sandboxes and jobs" : removed.from[0] === "jobs" ? " for jobs" : ""}.`, removed);
2331
2451
  return 0;
2332
2452
  }
2333
- throw usage("secrets takes set, ls, get, rotate, reveal or rm. Run `runtime help`.");
2453
+ throw usage("secrets takes set, scope, ls, get, rotate, reveal or rm. Run `runtime help`.");
2334
2454
  }
2335
2455
  async function readInput(source) {
2336
2456
  if (source !== "-")
@@ -2482,23 +2602,30 @@ async function browser(sbx, argv, out) {
2482
2602
  }
2483
2603
  const KEYS_HELP = `runtime keys <command>
2484
2604
 
2485
- create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
2486
- A new API key, for a CI runner or any place with no browser. Prints a link
2487
- and a code; an account owner approves the key in the browser, seeing its
2488
- name, access and limit. The key is printed once, alone on stdout, and
2489
- lasts until revoked. --json prints it as JSON, still alone on stdout.
2490
- --account-wide makes a key that sees and uses every sandbox and other
2491
- resource in the account, whichever key made it; only an owner or admin
2492
- can approve one. Without it, a key sees only what it makes.
2605
+ create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide] [--browser]
2606
+ A new API key. With a key saved on this machine (or RUNTIME_API_KEY), that
2607
+ key makes it at once, with no browser: a key per worker in an agent swarm.
2608
+ It never has more than the key that made it, and when that key has a daily
2609
+ limit, the keys it made share it, so each needs --daily-limit. Without a
2610
+ saved key, or with --browser, it prints a link and a code and an account
2611
+ owner approves the key in the browser. The key is printed once, alone on
2612
+ stdout, and lasts until revoked. --json prints it as JSON, still alone on
2613
+ stdout. --account-wide makes a key that sees and uses every sandbox and
2614
+ other resource in the account; without it, a key sees only what it makes.
2615
+ ls This key and the keys it made, with what each spent today
2616
+ usage <keyId> What one of them has spent and holds
2617
+ revoke <keyId> Revoke one: a worker's key when the worker is done
2493
2618
 
2494
- runtime keys create --name ci --daily-limit 25 | gh secret set RUNTIME_API_KEY
2619
+ runtime keys create --name worker-1 --daily-limit 5
2620
+ runtime keys create --name ci --daily-limit 25 --browser | gh secret set RUNTIME_API_KEY
2495
2621
 
2496
- List, limit and revoke keys at https://withruntime.com/account/keys.
2622
+ Making, listing and revoking keys with a key needs one an owner or admin made.
2623
+ Every key is also at https://withruntime.com/account/keys.
2497
2624
  `;
2498
- /* `runtime keys create`: a key made from the terminal and approved in the
2499
- browser. Marc, 23 September 2026; ARCHITECTURE.md section 10, "Keys from the
2500
- terminal". Only the options the keys page offers. No key can list, create or
2501
- revoke keys through the API, so `ls` and `revoke` send you to the page. */
2625
+ /* `runtime keys`: a key made from the terminal, by this machine's key with no
2626
+ browser (14 October 2026), or approved in the browser
2627
+ (23 September 2026; ARCHITECTURE.md section 10, "Keys from the
2628
+ terminal"). Only the options the keys page offers. */
2502
2629
  /* `runtime account`: the accounts this machine is connected to, and which one
2503
2630
  every command uses (ARCHITECTURE.md section 3.9). Each is its own saved
2504
2631
  connection with its own key; the SDK itself stays key-based. */
@@ -2604,19 +2731,63 @@ async function nameSaved(env) {
2604
2731
  await store.save({ ...again, orgName: who.orgName });
2605
2732
  }
2606
2733
  }
2734
+ /** Dollars from integer microdollars, for a person to read: "$5.00". */
2735
+ const usdOf = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
2607
2736
  async function keys(argv, env, out) {
2608
2737
  product = "keys";
2609
2738
  const [verb, ...rest] = argv;
2739
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2610
2740
  if (!verb || verb === "help" || verb === "--help" || verb === "-h") {
2611
2741
  out.write(out.json ? JSON.stringify({ usage: named(KEYS_HELP) }) : named(KEYS_HELP));
2612
2742
  return 0;
2613
2743
  }
2614
- if (verb === "ls" || verb === "list" || verb === "revoke")
2615
- throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
2744
+ if (verb === "ls" || verb === "list") {
2745
+ const args = parse(rest, [], []);
2746
+ if (args.flags.size || args.positional.length)
2747
+ throw usage(`Usage: ${named("runtime keys ls")}`);
2748
+ const all = await (await client(env)).keys.list();
2749
+ print(all.length
2750
+ ? table([
2751
+ ["KEY", "NAME", "ACCESS", "REACH", "DAILY LIMIT", "LAST 24H", "MADE BY", "STATE"],
2752
+ ...all.map((k) => [
2753
+ k.keyId,
2754
+ k.name,
2755
+ k.access,
2756
+ k.reach,
2757
+ k.dailyLimitMicros === null ? "none" : usdOf(k.dailyLimitMicros),
2758
+ usdOf(k.dailyUsedMicros),
2759
+ k.madeBy ?? "website",
2760
+ k.revokedAt ? "revoked" : "live",
2761
+ ]),
2762
+ ])
2763
+ : "No keys.", all);
2764
+ return 0;
2765
+ }
2766
+ if (verb === "revoke" || verb === "rm" || verb === "usage") {
2767
+ const args = parse(rest, [], []);
2768
+ const id = args.positional[0];
2769
+ if (!id || args.positional.length > 1 || args.flags.size)
2770
+ throw usage(`Usage: ${named(`runtime keys ${verb} <keyId>`)}`);
2771
+ const api = (await client(env)).keys;
2772
+ if (verb === "usage") {
2773
+ const used = await api.usage(id);
2774
+ print([
2775
+ `${used.name} (${used.keyId})${used.revokedAt ? ", revoked" : ""}`,
2776
+ ` last 24 hours ${usdOf(used.dailyUsedMicros)}${used.dailyLimitMicros === null ? "" : ` of ${usdOf(used.dailyLimitMicros)}`}`,
2777
+ ` spent ${usdOf(used.spentMicros)}`,
2778
+ ` held now ${usdOf(used.heldMicros)}`,
2779
+ ` sandboxes ${used.runningSandboxes} running, ${used.resources} kept in all`,
2780
+ ].join("\n"), used);
2781
+ return 0;
2782
+ }
2783
+ const revoked = await api.revoke(id);
2784
+ print(`Revoked ${revoked.keyId}. It stops working within ten seconds.`, revoked);
2785
+ return 0;
2786
+ }
2616
2787
  if (verb !== "create")
2617
- throw unknown("keys command", verb, ["create"]);
2618
- const args = parse(rest, ["read-only", "account-wide"], ["name", "daily-limit"]);
2619
- const known = new Set(["name", "read-only", "daily-limit", "account-wide"]);
2788
+ throw unknown("keys command", verb, ["create", "ls", "usage", "revoke"]);
2789
+ const args = parse(rest, ["read-only", "account-wide", "browser"], ["name", "daily-limit"]);
2790
+ const known = new Set(["name", "read-only", "daily-limit", "account-wide", "browser"]);
2620
2791
  for (const name of args.flags.keys())
2621
2792
  if (!known.has(name))
2622
2793
  throw usage(`Unknown option --${name}.`);
@@ -2629,26 +2800,48 @@ async function keys(argv, env, out) {
2629
2800
  if (readOnly && has(args, "account-wide"))
2630
2801
  throw usage("A read-only key already sees the whole account, so it takes no --account-wide.");
2631
2802
  const { createKey, dailyLimitMicros } = await import("./keys.js");
2632
- const created = await createKey({
2803
+ const request = {
2633
2804
  name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
2634
2805
  access: readOnly ? "read" : "full",
2635
2806
  ...(has(args, "account-wide") ? { reach: "account" } : {}),
2636
2807
  dailyLimitMicros: limit === undefined ? null : dailyLimitMicros(limit),
2637
- }, env, { notify: out.error });
2638
- const { confirm, ...key } = created;
2639
- out.write(out.json ? JSON.stringify(key) : key.key);
2808
+ };
2809
+ /* A key on this machine makes the new one itself, with no browser: an
2810
+ agent with no person, a swarm's coordinator (14 October 2026). */
2811
+ const saved = has(args, "browser")
2812
+ ? null
2813
+ : env.RUNTIME_API_KEY || (await connectionStore(env).read())?.key || null;
2814
+ let key;
2815
+ if (saved) {
2816
+ key = await new Runtime({
2817
+ apiKey: saved,
2818
+ baseUrl: env.RUNTIME_API_URL,
2819
+ maxRetries: 4,
2820
+ }).keys.create({
2821
+ name: request.name,
2822
+ access: request.access,
2823
+ reach: request.reach ?? "agent",
2824
+ dailyLimitMicros: request.dailyLimitMicros,
2825
+ });
2826
+ }
2827
+ else
2828
+ key = await createKey(request, env, { notify: out.error });
2829
+ const { confirm, ...made } = key;
2830
+ out.write(out.json ? JSON.stringify(made) : made.key);
2640
2831
  const terms = [
2641
- key.access === "read" ? "read only" : "full access",
2642
- key.reach === "account" ? "account-wide" : undefined,
2643
- key.access === "read"
2832
+ made.access === "read" ? "read only" : "full access",
2833
+ made.reach === "account" ? "account-wide" : undefined,
2834
+ made.access === "read"
2644
2835
  ? undefined
2645
- : key.dailyLimitMicros === null
2836
+ : made.dailyLimitMicros === null
2646
2837
  ? "no daily limit"
2647
- : `daily limit $${(Number(BigInt(key.dailyLimitMicros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
2648
- "until revoked",
2838
+ : `daily limit ${usdOf(made.dailyLimitMicros)}`,
2839
+ made.expires ? `until ${made.expires}` : "until revoked",
2649
2840
  ].filter(Boolean);
2650
- out.error(`Created key "${key.name}" (${terms.join(", ")}). Store it now: it is not shown again, and Runtime keeps only a hash of it.\nIn CI, give it to your jobs as RUNTIME_API_KEY from the secret store.`);
2651
- await confirm();
2841
+ out.error(saved
2842
+ ? `Created key "${made.name}" (${terms.join(", ")}), made by this machine's key. Store it now: it is not shown again, and Runtime keeps only a hash of it.\nGive it to one worker as RUNTIME_API_KEY; revoke it with ${named("runtime keys revoke")} ${made.keyId} when that worker is done.`
2843
+ : `Created key "${made.name}" (${terms.join(", ")}). Store it now: it is not shown again, and Runtime keeps only a hash of it.\nIn CI, give it to your jobs as RUNTIME_API_KEY from the secret store.`);
2844
+ await confirm?.();
2652
2845
  return 0;
2653
2846
  }
2654
2847
  const SNAPSHOT_HELP = `runtime snapshots <command>
@@ -2976,7 +3169,7 @@ read = readSecret) {
2976
3169
  const JOB_HELP = `runtime jobs <command>
2977
3170
 
2978
3171
  A job runs a command in a fresh sandbox, once or on a schedule. Each run spends
2979
- the included usage first, then credit at the sandbox rates.
3172
+ the free usage first, then credit at the sandbox rates.
2980
3173
 
2981
3174
  create <name> (--cron "<min hour day month weekday>" [--timezone Europe/Berlin] | --at <time|now>)
2982
3175
  [--vcpu 2] [--memory 4096] [--disk 4096] [--cpu shared|reserved] [--cpu-floor <thousandths>]
@@ -3528,6 +3721,7 @@ const CREATE_VALUES = [
3528
3721
  "connect",
3529
3722
  "image",
3530
3723
  "snapshot",
3724
+ "spread-from",
3531
3725
  "volume",
3532
3726
  "env",
3533
3727
  ];
@@ -3555,6 +3749,18 @@ function createInput(args) {
3555
3749
  return {
3556
3750
  ...(flag(args, "image") ? { image: flag(args, "image") } : {}),
3557
3751
  ...(flag(args, "snapshot") ? { snapshot: flag(args, "snapshot") } : {}),
3752
+ ...(args.flags.has("spread-from")
3753
+ ? {
3754
+ spreadFrom: args.flags
3755
+ .get("spread-from")
3756
+ .flatMap((value) => value.split(","))
3757
+ .map((id) => {
3758
+ if (!id.trim())
3759
+ throw usage("--spread-from needs a non-empty value.");
3760
+ return id.trim();
3761
+ }),
3762
+ }
3763
+ : {}),
3558
3764
  ...(args.flags.has("volume") ? { volumes: args.flags.get("volume").map(attachment) } : {}),
3559
3765
  ...(has(args, "no-internet") || rules.length
3560
3766
  ? {