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.
- package/BLAXEL.md +97 -32
- package/COMPATIBILITY.md +120 -5
- package/DAYTONA.md +34 -3
- package/E2B.md +34 -5
- package/README.md +36 -3
- package/README.src.md +35 -2
- package/VERCEL.md +52 -5
- package/compatibility-lock.json +71 -111
- package/dist/billing-cli.js +8 -4
- package/dist/blaxel/client.d.ts +11 -0
- package/dist/blaxel/client.js +28 -0
- package/dist/blaxel/errors.d.ts +65 -0
- package/dist/blaxel/errors.js +114 -0
- package/dist/blaxel/index.d.ts +10 -3
- package/dist/blaxel/index.js +23 -4
- package/dist/blaxel/preview.d.ts +29 -4
- package/dist/blaxel/preview.js +79 -5
- package/dist/blaxel/sandbox.d.ts +1 -1
- package/dist/blaxel/sandbox.js +2 -1
- package/dist/blaxel/session.js +4 -2
- package/dist/cli-extras.js +2 -1
- package/dist/cli-words.js +1 -0
- package/dist/cli.d.ts +17 -2
- package/dist/cli.js +291 -85
- package/dist/client.d.ts +1 -1
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +15 -0
- package/dist/daytona/client.d.ts +4 -0
- package/dist/daytona/client.js +8 -3
- package/dist/daytona/daytona.d.ts +4 -1
- package/dist/daytona/daytona.js +3 -2
- package/dist/e2b/sandbox.d.ts +7 -6
- package/dist/e2b/sandbox.js +9 -12
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/freestyle/index.d.ts +77 -5
- package/dist/freestyle/index.js +380 -48
- package/dist/identity.js +36 -18
- package/dist/index.d.ts +4 -3
- package/dist/machine.d.ts +39 -13
- package/dist/machine.js +72 -25
- package/dist/mcp.js +157 -20
- package/dist/modal/index.d.ts +15 -0
- package/dist/modal/index.js +35 -7
- package/dist/morph/index.d.ts +255 -0
- package/dist/morph/index.js +641 -0
- package/dist/network-cli.js +2 -2
- package/dist/product-names.d.ts +2 -2
- package/dist/product-names.js +2 -2
- package/dist/products/api-keys.d.ts +73 -0
- package/dist/products/api-keys.js +23 -0
- package/dist/products/apps.d.ts +12 -31
- package/dist/products/apps.js +8 -113
- package/dist/products/balancers.d.ts +87 -0
- package/dist/products/balancers.js +8 -0
- package/dist/products/builds.d.ts +30 -0
- package/dist/products/builds.js +6 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +2 -2
- package/dist/products/jobs.js +1 -1
- package/dist/products/limits.d.ts +16 -2
- package/dist/products/limits.js +2 -2
- package/dist/products/mcp.d.ts +5 -0
- package/dist/products/network-products.d.ts +21 -3
- package/dist/products/network-products.js +3 -1
- package/dist/products/observability.d.ts +7 -2
- package/dist/products/previews.d.ts +6 -3
- package/dist/products/previews.js +1 -1
- package/dist/products/resources.d.ts +54 -0
- package/dist/products/resources.js +122 -0
- package/dist/products/secrets.d.ts +22 -0
- package/dist/products/secrets.js +8 -0
- package/dist/products/volumes.d.ts +3 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +27 -2
- package/dist/snapshots.d.ts +4 -0
- package/dist/ssh.d.ts +35 -0
- package/dist/ssh.js +93 -1
- package/dist/transport.d.ts +14 -8
- package/dist/transport.js +70 -15
- package/dist/types.d.ts +33 -5
- package/dist/vercel/command.d.ts +48 -1
- package/dist/vercel/command.js +237 -6
- package/dist/vercel/sandbox-user.js +2 -1
- package/dist/vercel/sandbox.d.ts +17 -3
- package/dist/vercel/sandbox.js +46 -3
- package/dist/wallet.d.ts +4 -1
- package/dist/wallet.js +2 -1
- package/package.json +7 -14
- package/dist/cli/logs.test.d.ts +0 -1
- package/dist/cli/logs.test.js +0 -272
- package/dist/products/logs.test.d.ts +0 -1
- 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
|
|
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,
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
-
// (
|
|
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
|
|
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
|
|
633
|
-
// across
|
|
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.
|
|
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}
|
|
649
|
-
hint: `Check it and retry cleanup with: ${me} sandboxes
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1694
|
-
|
|
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
|
|
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" &&
|
|
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
|
-
|
|
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
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
--
|
|
2491
|
-
|
|
2492
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2499
|
-
browser
|
|
2500
|
-
|
|
2501
|
-
|
|
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"
|
|
2615
|
-
|
|
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
|
|
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
|
-
}
|
|
2638
|
-
|
|
2639
|
-
|
|
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
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2832
|
+
made.access === "read" ? "read only" : "full access",
|
|
2833
|
+
made.reach === "account" ? "account-wide" : undefined,
|
|
2834
|
+
made.access === "read"
|
|
2644
2835
|
? undefined
|
|
2645
|
-
:
|
|
2836
|
+
: made.dailyLimitMicros === null
|
|
2646
2837
|
? "no daily limit"
|
|
2647
|
-
: `daily limit
|
|
2648
|
-
"until revoked",
|
|
2838
|
+
: `daily limit ${usdOf(made.dailyLimitMicros)}`,
|
|
2839
|
+
made.expires ? `until ${made.expires}` : "until revoked",
|
|
2649
2840
|
].filter(Boolean);
|
|
2650
|
-
out.error(
|
|
2651
|
-
|
|
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
|
|
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
|
? {
|