withruntime 0.16.0 → 0.17.1
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/dist/cli.js +101 -34
- package/dist/identity.js +45 -22
- package/dist/index.d.ts +1 -1
- package/dist/machine.d.ts +4 -1
- package/dist/machine.js +3 -1
- package/dist/products/api-keys.d.ts +32 -4
- package/dist/products/apps.d.ts +10 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +2 -0
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -124,12 +124,12 @@ const SANDBOX_HELP = `runtime sandboxes <command>
|
|
|
124
124
|
|
|
125
125
|
Every <id> below can also be the sandbox's name.
|
|
126
126
|
|
|
127
|
-
run [--keep] [create options] -- <command>
|
|
127
|
+
run [--keep] [--root] [create options] -- <command> A fresh sandbox runs one command, then is deleted
|
|
128
128
|
ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
|
|
129
129
|
code <id|name> [--editor vscode|cursor|windsurf] [--path /workspace] [--print]
|
|
130
130
|
Open it in your editor over SSH, in one step
|
|
131
131
|
port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
|
|
132
|
-
create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
132
|
+
create [--name n] [--label k=v]... [--env K[=V]]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
133
133
|
[--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
|
|
134
134
|
[--timeout <60-86400>] [--on-timeout pause|stop|delete] [--no-wait]
|
|
135
135
|
[--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
@@ -139,15 +139,16 @@ const SANDBOX_HELP = `runtime sandboxes <command>
|
|
|
139
139
|
Create one; prints its id
|
|
140
140
|
ls [--all] [--label k=v]... [--state running] List (live ones unless --all)
|
|
141
141
|
get <id> One sandbox, in full
|
|
142
|
-
exec <id> [--cwd /workspace] [--env K=V]... [--timeout <s>] -- <command...>
|
|
143
|
-
Run and stream output; exits with its code
|
|
144
|
-
|
|
142
|
+
exec <id> [--cwd /workspace] [--env K[=V]]... [--timeout <s>] [--root] -- <command...>
|
|
143
|
+
Run and stream output; exits with its code.
|
|
144
|
+
--root runs it as root: no sudo needed
|
|
145
|
+
spawn <id> [--pty] [--root] -- <command...> Start in the background; prints a process id
|
|
145
146
|
ps <id> Running and recent processes
|
|
146
147
|
logs <id> <processId> [-f] [--raw] A process's output so far; -f follows it to the end;
|
|
147
148
|
on a terminal control characters show as \\uXXXX
|
|
148
149
|
unless --raw
|
|
149
150
|
kill <id> <processId> [--signal SIGTERM]
|
|
150
|
-
shell <id> [--command "bash -l"]
|
|
151
|
+
shell <id> [--command "bash -l"] [--root] An interactive terminal
|
|
151
152
|
cp <src> <dst> Copy files or directories; name a sandbox
|
|
152
153
|
path as <id>:/path, e.g. cp ./app sbx:/workspace/app
|
|
153
154
|
cat <id> <path> Print a file
|
|
@@ -161,14 +162,17 @@ const SANDBOX_HELP = `runtime sandboxes <command>
|
|
|
161
162
|
switch-image <id> <image> --keep-workspace Move it to another image, keeping /workspace (your home),
|
|
162
163
|
volumes, env and previews; processes restart, the rest of
|
|
163
164
|
its disk is lost (snapshot it first to keep everything)
|
|
164
|
-
resize <id> [--vcpu 2] [--memory 4096] --restart
|
|
165
|
-
Change its size by a restart: the whole disk stays
|
|
166
|
-
programs stop (snapshot it first
|
|
165
|
+
resize <id> [--vcpu 2] [--memory 4096] [--disk 20480] --restart
|
|
166
|
+
Change its size by a restart: the whole disk stays and
|
|
167
|
+
--disk only grows it; programs stop (snapshot it first
|
|
168
|
+
to keep memory)
|
|
167
169
|
extend <id> <seconds> Move a time limit on (one with none needs nothing)
|
|
168
|
-
update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
|
|
170
|
+
update <id> [--name n] [--label k=v]... [--env K[=V]]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
|
|
169
171
|
[--persistent on|off] [--max-total-cost <usd>|none]
|
|
170
172
|
[--timeout <60-86400 from now>|0] [--on-timeout pause|stop|delete]
|
|
171
|
-
Change its settings; persistent keeps it running until stopped
|
|
173
|
+
Change its settings; persistent keeps it running until stopped;
|
|
174
|
+
--env K alone sends K's value from your own environment,
|
|
175
|
+
so it is in no command line or shell history
|
|
172
176
|
snapshot <id> [--name n] [--retention days|credit] Keep its whole machine; prints the snapshot id
|
|
173
177
|
mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
|
|
174
178
|
[--endpoint https://...] [--account-id id] [--read-only]
|
|
@@ -332,6 +336,20 @@ const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).ma
|
|
|
332
336
|
throw usage(`--${name} takes KEY=VALUE.`);
|
|
333
337
|
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
334
338
|
}));
|
|
339
|
+
/** --env KEY=VALUE, or --env KEY alone to take KEY's value from the CLI's own
|
|
340
|
+
* environment, as docker run -e does: the value stays out of the process's
|
|
341
|
+
* arguments and the shell's history. */
|
|
342
|
+
const envPairs = (args, from) => Object.fromEntries((args.flags.get("env") ?? []).map((pair) => {
|
|
343
|
+
const at = pair.indexOf("=");
|
|
344
|
+
if (at > 0)
|
|
345
|
+
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
346
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(pair))
|
|
347
|
+
throw usage(`--env takes KEY=VALUE, or KEY alone to send KEY's value from your environment.`);
|
|
348
|
+
const value = from[pair];
|
|
349
|
+
if (value === undefined)
|
|
350
|
+
throw usage(`--env ${pair} sends ${pair} from your environment, and it is not set there.`);
|
|
351
|
+
return [pair, value];
|
|
352
|
+
}));
|
|
335
353
|
/** What the commands in cli-extras.ts use of this file. */
|
|
336
354
|
const kit = {
|
|
337
355
|
parse: (argv, booleans, values) => parse(argv, booleans, values),
|
|
@@ -647,13 +665,13 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
647
665
|
return 0;
|
|
648
666
|
}
|
|
649
667
|
if (command === "run") {
|
|
650
|
-
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandboxes run -- python3 < invoice.py`);
|
|
668
|
+
const args = parse(rest, [...CREATE_FLAGS, "keep", "root"], [...CREATE_VALUES, "cwd", "env"], `${me} sandboxes run -- python3 < invoice.py`);
|
|
651
669
|
const commandLine = args.rest ?? args.positional;
|
|
652
670
|
if (!commandLine.length)
|
|
653
671
|
throw usage(`Give the command after --: ${me} sandboxes run -- python3 < invoice.py`);
|
|
654
672
|
const runtime = await client(env);
|
|
655
673
|
const timeout = integer(args, "timeout");
|
|
656
|
-
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
674
|
+
const { timeoutSeconds: _lease, ...input } = createInput(args, env);
|
|
657
675
|
const onCapacityWait = waiting(out);
|
|
658
676
|
const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
|
|
659
677
|
let cleanup;
|
|
@@ -684,7 +702,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
684
702
|
process.once("SIGINT", interrupted);
|
|
685
703
|
let code = 1;
|
|
686
704
|
try {
|
|
687
|
-
code = await execute(sbx, commandLine, args, out, timeout, has(args, "keep"));
|
|
705
|
+
code = await execute(sbx, commandLine, args, out, timeout, env, has(args, "keep"));
|
|
688
706
|
}
|
|
689
707
|
finally {
|
|
690
708
|
try {
|
|
@@ -1654,7 +1672,7 @@ async function sandbox(argv, env, out) {
|
|
|
1654
1672
|
switch (verb) {
|
|
1655
1673
|
case "create": {
|
|
1656
1674
|
const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
|
|
1657
|
-
const input = createInput(args);
|
|
1675
|
+
const input = createInput(args, env);
|
|
1658
1676
|
const onCapacityWait = waiting(out);
|
|
1659
1677
|
const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
|
|
1660
1678
|
print(sbx.id, sbx.info);
|
|
@@ -1718,13 +1736,14 @@ async function sandbox(argv, env, out) {
|
|
|
1718
1736
|
return 0;
|
|
1719
1737
|
}
|
|
1720
1738
|
case "resize": {
|
|
1721
|
-
const args = parse(rest, ["restart"], ["vcpu", "memory"]);
|
|
1739
|
+
const args = parse(rest, ["restart"], ["vcpu", "memory", "disk"]);
|
|
1722
1740
|
const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me} sandboxes resize <id> --memory 4096 --restart`));
|
|
1723
1741
|
const size = {
|
|
1724
1742
|
...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
|
|
1725
1743
|
...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
|
|
1744
|
+
...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
|
|
1726
1745
|
};
|
|
1727
|
-
if (size.vcpu === undefined && size.memoryMiB === undefined)
|
|
1746
|
+
if (size.vcpu === undefined && size.memoryMiB === undefined && size.diskMiB === undefined)
|
|
1728
1747
|
throw usage(`Name a new size, e.g. ${me} sandboxes resize ${sbx.id} --memory 4096 --restart`);
|
|
1729
1748
|
if (!args.flags.has("restart"))
|
|
1730
1749
|
throw usage(`A resize restarts it: its whole disk stays and its programs stop. Add --restart to say so, or snapshot it first (${me} sandboxes snapshot ${sbx.id}) to keep its memory too.`);
|
|
@@ -1881,14 +1900,14 @@ async function sandbox(argv, env, out) {
|
|
|
1881
1900
|
const cost = flag(args, "max-total-cost");
|
|
1882
1901
|
if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
|
|
1883
1902
|
throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
|
|
1884
|
-
const
|
|
1885
|
-
...(args.flags.has("env") ?
|
|
1903
|
+
const variables = {
|
|
1904
|
+
...(args.flags.has("env") ? envPairs(args, env) : {}),
|
|
1886
1905
|
...Object.fromEntries((args.flags.get("unset-env") ?? []).map((name) => [name, null])),
|
|
1887
1906
|
};
|
|
1888
1907
|
const settings = {
|
|
1889
1908
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1890
1909
|
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
1891
|
-
...(Object.keys(
|
|
1910
|
+
...(Object.keys(variables).length ? { env: variables } : {}),
|
|
1892
1911
|
...(integer(args, "idle-pause") !== undefined
|
|
1893
1912
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
1894
1913
|
: {}),
|
|
@@ -1920,23 +1939,24 @@ async function sandbox(argv, env, out) {
|
|
|
1920
1939
|
return 0;
|
|
1921
1940
|
}
|
|
1922
1941
|
case "exec": {
|
|
1923
|
-
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandboxes exec <id> -- ls -la`);
|
|
1942
|
+
const args = parse(rest, ["root"], ["cwd", "env", "timeout"], `${me} sandboxes exec <id> -- ls -la`);
|
|
1924
1943
|
const command = args.rest ?? args.positional.slice(1);
|
|
1925
1944
|
if (!command.length)
|
|
1926
1945
|
throw usage("Give the command after --: runtime sandboxes exec <id> -- ls -la");
|
|
1927
1946
|
const sbx = await use(args.positional[0]);
|
|
1928
|
-
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
1947
|
+
return execute(sbx, command, args, out, integer(args, "timeout"), env);
|
|
1929
1948
|
}
|
|
1930
1949
|
case "spawn": {
|
|
1931
|
-
const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandboxes spawn <id> -- npm run dev`);
|
|
1950
|
+
const args = parse(rest, ["pty", "root"], ["cwd", "env"], `${me} sandboxes spawn <id> -- npm run dev`);
|
|
1932
1951
|
const command = args.rest ?? [];
|
|
1933
1952
|
if (!command.length)
|
|
1934
1953
|
throw usage("Give the command after --: runtime sandboxes spawn <id> -- npm run dev");
|
|
1935
1954
|
const sbx = await use(args.positional[0]);
|
|
1936
1955
|
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
1937
1956
|
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
1957
|
+
...(has(args, "root") ? { user: "root" } : {}),
|
|
1938
1958
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
1939
|
-
...(args.flags.has("env") ? { env:
|
|
1959
|
+
...(args.flags.has("env") ? { env: envPairs(args, env) } : {}),
|
|
1940
1960
|
});
|
|
1941
1961
|
print(proc.id, proc.info);
|
|
1942
1962
|
return 0;
|
|
@@ -1946,12 +1966,13 @@ async function sandbox(argv, env, out) {
|
|
|
1946
1966
|
const list = await sbx.processes.list();
|
|
1947
1967
|
print(list.length
|
|
1948
1968
|
? table([
|
|
1949
|
-
["PROCESS", "STATE", "EXIT", "COMMAND"],
|
|
1969
|
+
["PROCESS", "STATE", "EXIT", "USER", "COMMAND"],
|
|
1950
1970
|
// The command line is the guest's report: escaped, as a log is.
|
|
1951
1971
|
...list.map((p) => [
|
|
1952
1972
|
terminalSafe(String(p.id)),
|
|
1953
1973
|
terminalSafe(String(p.state)),
|
|
1954
1974
|
`${typeof p.exitCode === "number" ? p.exitCode : "-"}${p.outOfMemory ? " (out of memory)" : ""}`,
|
|
1975
|
+
p.user === "root" ? "root" : "sandbox",
|
|
1955
1976
|
terminalSafe(String(p.command ?? "")),
|
|
1956
1977
|
]),
|
|
1957
1978
|
])
|
|
@@ -2019,9 +2040,9 @@ async function sandbox(argv, env, out) {
|
|
|
2019
2040
|
return 0;
|
|
2020
2041
|
}
|
|
2021
2042
|
case "shell": {
|
|
2022
|
-
const args = parse(rest, [], ["command"]);
|
|
2043
|
+
const args = parse(rest, ["root"], ["command"]);
|
|
2023
2044
|
const sbx = await use(args.positional[0]);
|
|
2024
|
-
return shell(sbx, flag(args, "command"));
|
|
2045
|
+
return shell(sbx, flag(args, "command"), has(args, "root"));
|
|
2025
2046
|
}
|
|
2026
2047
|
case "cat": {
|
|
2027
2048
|
const sbx = await use(rest[0]);
|
|
@@ -2612,12 +2633,20 @@ const KEYS_HELP = `runtime keys <command>
|
|
|
2612
2633
|
stdout, and lasts until revoked. --json prints it as JSON, still alone on
|
|
2613
2634
|
stdout. --account-wide makes a key that sees and uses every sandbox and
|
|
2614
2635
|
other resource in the account; without it, a key sees only what it makes.
|
|
2636
|
+
create --sandbox <id,...> --actions <action,...> [--name <name>] [--daily-limit <usd>]
|
|
2637
|
+
A key limited to those sandboxes (up to 20, by id) and those actions on
|
|
2638
|
+
them, made by the saved key: get reads them; create starts, wakes and
|
|
2639
|
+
restarts them, and needs --daily-limit when the making key has one; stop
|
|
2640
|
+
stops and pauses them; exec runs commands; read_file and write_file reach
|
|
2641
|
+
their files. It is served nothing else: no list, no other sandbox, no
|
|
2642
|
+
account route, no MCP.
|
|
2615
2643
|
ls This key and the keys it made, with what each spent today
|
|
2616
2644
|
usage <keyId> What one of them has spent and holds
|
|
2617
2645
|
revoke <keyId> Revoke one: a worker's key when the worker is done
|
|
2618
2646
|
|
|
2619
2647
|
runtime keys create --name worker-1 --daily-limit 5
|
|
2620
2648
|
runtime keys create --name ci --daily-limit 25 --browser | gh secret set RUNTIME_API_KEY
|
|
2649
|
+
runtime keys create --name watcher --sandbox sbx_id --actions get,create --daily-limit 2
|
|
2621
2650
|
|
|
2622
2651
|
Making, listing and revoking keys with a key needs one an owner or admin made.
|
|
2623
2652
|
Every key is also at https://withruntime.com/account/keys.
|
|
@@ -2786,8 +2815,16 @@ async function keys(argv, env, out) {
|
|
|
2786
2815
|
}
|
|
2787
2816
|
if (verb !== "create")
|
|
2788
2817
|
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([
|
|
2818
|
+
const args = parse(rest, ["read-only", "account-wide", "browser"], ["name", "daily-limit", "sandbox", "actions"]);
|
|
2819
|
+
const known = new Set([
|
|
2820
|
+
"name",
|
|
2821
|
+
"read-only",
|
|
2822
|
+
"daily-limit",
|
|
2823
|
+
"account-wide",
|
|
2824
|
+
"browser",
|
|
2825
|
+
"sandbox",
|
|
2826
|
+
"actions",
|
|
2827
|
+
]);
|
|
2791
2828
|
for (const name of args.flags.keys())
|
|
2792
2829
|
if (!known.has(name))
|
|
2793
2830
|
throw usage(`Unknown option --${name}.`);
|
|
@@ -2795,6 +2832,8 @@ async function keys(argv, env, out) {
|
|
|
2795
2832
|
throw usage(`Unexpected ${args.positional[0] ?? "--"}.`);
|
|
2796
2833
|
const readOnly = has(args, "read-only");
|
|
2797
2834
|
const limit = flag(args, "daily-limit");
|
|
2835
|
+
if (flag(args, "sandbox") !== undefined || flag(args, "actions") !== undefined)
|
|
2836
|
+
return limitedKey(args, env, out, limit === undefined ? null : limit);
|
|
2798
2837
|
if (readOnly && limit !== undefined)
|
|
2799
2838
|
throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
|
|
2800
2839
|
if (readOnly && has(args, "account-wide"))
|
|
@@ -2844,6 +2883,32 @@ async function keys(argv, env, out) {
|
|
|
2844
2883
|
await confirm?.();
|
|
2845
2884
|
return 0;
|
|
2846
2885
|
}
|
|
2886
|
+
/** `runtime keys create --sandbox ... --actions ...`: a key limited to
|
|
2887
|
+
* sandboxes, made by the saved key (never in the browser: the keys page
|
|
2888
|
+
* makes none). */
|
|
2889
|
+
async function limitedKey(args, env, out, limit) {
|
|
2890
|
+
for (const refused of ["read-only", "account-wide", "browser"])
|
|
2891
|
+
if (has(args, refused))
|
|
2892
|
+
throw usage(`A key limited to sandboxes reaches only them and is made by this machine's key, so it takes no --${refused}.`);
|
|
2893
|
+
const list = (name) => (flag(args, name) ?? "")
|
|
2894
|
+
.split(",")
|
|
2895
|
+
.map((each) => each.trim())
|
|
2896
|
+
.filter(Boolean);
|
|
2897
|
+
const sandboxes = list("sandbox");
|
|
2898
|
+
const actions = list("actions");
|
|
2899
|
+
if (!sandboxes.length || !actions.length)
|
|
2900
|
+
throw usage(`Usage: ${named("runtime keys create --sandbox <id,...> --actions <get,create,stop,exec,read_file,write_file> [--daily-limit <usd>]")}`);
|
|
2901
|
+
const { dailyLimitMicros } = await import("./keys.js");
|
|
2902
|
+
const made = await (await client(env)).keys.create({
|
|
2903
|
+
name: flag(args, "name") ?? "Sandbox key",
|
|
2904
|
+
sandboxes,
|
|
2905
|
+
actions: actions,
|
|
2906
|
+
dailyLimitMicros: limit === null ? null : dailyLimitMicros(limit),
|
|
2907
|
+
});
|
|
2908
|
+
out.write(out.json ? JSON.stringify(made) : made.key);
|
|
2909
|
+
out.error(`Created key "${made.name}", limited to ${made.sandboxes?.length === 1 ? "sandbox" : "sandboxes"} ${(made.sandboxes ?? []).join(", ")} for ${(made.actions ?? []).join(", ")}${made.dailyLimitMicros === null ? "" : `, daily limit ${usdOf(made.dailyLimitMicros)}`}, made by this machine's key. Store it now: it is not shown again.\nIt calls those sandboxes by id and nothing else; revoke it with ${named("runtime keys revoke")} ${made.keyId}.`);
|
|
2910
|
+
return 0;
|
|
2911
|
+
}
|
|
2847
2912
|
const SNAPSHOT_HELP = `runtime snapshots <command>
|
|
2848
2913
|
|
|
2849
2914
|
ls [--sandbox <id>] [--name n] Your snapshots
|
|
@@ -3741,7 +3806,7 @@ function usdMicros(args, name) {
|
|
|
3741
3806
|
return Number(micros);
|
|
3742
3807
|
}
|
|
3743
3808
|
/** A create body from the command line's flags. */
|
|
3744
|
-
function createInput(args) {
|
|
3809
|
+
function createInput(args, env) {
|
|
3745
3810
|
for (const name of ["image", "snapshot", "volume", "name", "region", "connect"])
|
|
3746
3811
|
if (args.flags.get(name)?.some((value) => !value.trim()))
|
|
3747
3812
|
throw usage(`--${name} needs a non-empty value.`);
|
|
@@ -3772,7 +3837,7 @@ function createInput(args) {
|
|
|
3772
3837
|
: {}),
|
|
3773
3838
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
3774
3839
|
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
3775
|
-
...(args.flags.has("env") ? { env:
|
|
3840
|
+
...(args.flags.has("env") ? { env: envPairs(args, env) } : {}),
|
|
3776
3841
|
...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
|
|
3777
3842
|
...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
|
|
3778
3843
|
...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
|
|
@@ -3809,7 +3874,7 @@ function createInput(args) {
|
|
|
3809
3874
|
}
|
|
3810
3875
|
/** Runs a command in a sandbox, streaming its output, and answers its exit code
|
|
3811
3876
|
* (124 on a timeout), so a script's `set -e` behaves. */
|
|
3812
|
-
async function execute(sbx, command, args, out, timeout,
|
|
3877
|
+
async function execute(sbx, command, args, out, timeout, env,
|
|
3813
3878
|
/** `run` stops its whole sandbox on Ctrl-C, so it handles the signal itself. */
|
|
3814
3879
|
ownInterrupt = true) {
|
|
3815
3880
|
const target = command.length === 1 ? command[0] : command;
|
|
@@ -3818,7 +3883,8 @@ ownInterrupt = true) {
|
|
|
3818
3883
|
const options = {
|
|
3819
3884
|
...(stdin ? { stdin } : {}),
|
|
3820
3885
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
3821
|
-
...(args.flags.has("env") ? { env:
|
|
3886
|
+
...(args.flags.has("env") ? { env: envPairs(args, env) } : {}),
|
|
3887
|
+
...(has(args, "root") ? { user: "root" } : {}),
|
|
3822
3888
|
...(timeout !== undefined ? { timeoutMs: timeout * 1000 } : {}),
|
|
3823
3889
|
};
|
|
3824
3890
|
// Ctrl-C stops the command in the sandbox too, and says so; without this
|
|
@@ -3867,12 +3933,13 @@ ownInterrupt = true) {
|
|
|
3867
3933
|
const code = result.timedOut ? 124 : Math.min(255, exit < 0 ? 128 - exit : exit);
|
|
3868
3934
|
return lost && code === 0 ? 1 : code;
|
|
3869
3935
|
}
|
|
3870
|
-
async function shell(sbx, command) {
|
|
3936
|
+
async function shell(sbx, command, root = false) {
|
|
3871
3937
|
const stdin = process.stdin;
|
|
3872
3938
|
const size = () => ({ cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 });
|
|
3873
3939
|
const terminal = await sbx.terminal({
|
|
3874
3940
|
...size(),
|
|
3875
3941
|
...(command ? { command } : {}),
|
|
3942
|
+
...(root ? { user: "root" } : {}),
|
|
3876
3943
|
onData: (data) => process.stdout.write(data),
|
|
3877
3944
|
});
|
|
3878
3945
|
if (stdin.isTTY)
|
package/dist/identity.js
CHANGED
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
2
|
import { sandboxMarker } from "./transport.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
async function requestCredentials() {
|
|
4
|
+
const url = process.env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
5
|
+
const token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
6
|
+
if (url && token)
|
|
7
|
+
return { url, token };
|
|
8
|
+
// A process started before the guest wrote its environment still finds it.
|
|
9
|
+
return fileCredentials();
|
|
10
|
+
}
|
|
6
11
|
async function fileCredentials() {
|
|
7
12
|
try {
|
|
8
13
|
// Loaded here, so the SDK still loads where there is no file system.
|
|
9
14
|
const { readFile } = await import("node:fs/promises");
|
|
10
15
|
const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
|
|
11
|
-
|
|
16
|
+
const url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
17
|
+
const token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
18
|
+
return url && token ? { url, token } : null;
|
|
12
19
|
}
|
|
13
20
|
catch {
|
|
14
21
|
return null;
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/** The sandbox's own listener (packages/cloud/src/linux/egress.ts). */
|
|
25
|
+
const ENVIRONMENT_URL = "http://runtime.internal/v1/environment";
|
|
26
|
+
/** The sandbox's request token as its host holds it now. The one in
|
|
27
|
+
* process.env was current when the process started: it changes with every
|
|
28
|
+
* renewal of the sandbox's lease (about every 30 minutes for one that keeps
|
|
29
|
+
* running), and a server restored from a snapshot (an App's copies, a fork)
|
|
30
|
+
* holds the statement of the sandbox it was saved from. The host's listener
|
|
31
|
+
* answers this sandbox's own; guest-net.py's file is a few seconds behind it. */
|
|
32
|
+
async function currentCredentials(signal) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(ENVIRONMENT_URL, {
|
|
35
|
+
signal: signal ?? AbortSignal.timeout(5_000),
|
|
36
|
+
});
|
|
37
|
+
if (response.ok) {
|
|
38
|
+
const env = (await response.json());
|
|
39
|
+
const url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
40
|
+
const token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
41
|
+
if (typeof url === "string" && url && typeof token === "string" && token)
|
|
42
|
+
return { url, token };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not in a sandbox, or a host that predates the listener's environment.
|
|
25
47
|
}
|
|
26
|
-
return
|
|
48
|
+
return fileCredentials();
|
|
27
49
|
}
|
|
28
50
|
/**
|
|
29
51
|
* Inside a Runtime sandbox: a short-lived OIDC token, signed by Runtime,
|
|
@@ -44,24 +66,25 @@ export async function identityToken(input) {
|
|
|
44
66
|
status: 0,
|
|
45
67
|
message: "Identity tokens are issued only inside a Runtime sandbox (RUNTIME_ID_TOKEN_REQUEST_TOKEN is not set).",
|
|
46
68
|
});
|
|
47
|
-
const ask = (
|
|
48
|
-
const url = new URL(
|
|
69
|
+
const ask = (credentials) => {
|
|
70
|
+
const url = new URL(credentials.url);
|
|
49
71
|
url.searchParams.set("audience", input.audience);
|
|
50
72
|
if (input.lifetimeSeconds !== undefined)
|
|
51
73
|
url.searchParams.set("lifetimeSeconds", String(input.lifetimeSeconds));
|
|
52
74
|
return fetch(url, {
|
|
53
|
-
headers: { authorization: `Bearer ${
|
|
75
|
+
headers: { authorization: `Bearer ${credentials.token}` },
|
|
54
76
|
signal: input.signal ?? AbortSignal.timeout(10_000),
|
|
55
77
|
});
|
|
56
78
|
};
|
|
57
79
|
let response = await ask(found);
|
|
58
|
-
// Refused for a request token that
|
|
59
|
-
//
|
|
60
|
-
// own, 16 October 2026, from its start until it was restarted).
|
|
80
|
+
// Refused for a request token that is no longer this sandbox's current one:
|
|
81
|
+
// ask once more with the one its host holds now.
|
|
61
82
|
if (response.status === 401 || response.status === 403) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
83
|
+
const current = await currentCredentials(input.signal);
|
|
84
|
+
if (current && current.token !== found.token) {
|
|
85
|
+
await response.body?.cancel();
|
|
86
|
+
response = await ask(current);
|
|
87
|
+
}
|
|
65
88
|
}
|
|
66
89
|
const body = (await response.json().catch(() => ({})));
|
|
67
90
|
if (!response.ok || !body.token)
|
package/dist/index.d.ts
CHANGED
|
@@ -74,5 +74,5 @@ export type { RuntimeOptions as ClientOptions };
|
|
|
74
74
|
export { identityToken, type IdentityToken } from "./identity.js";
|
|
75
75
|
export { openWalletAccount, claimWalletAccount, type OpenedWalletAccount, type WalletClaim, type WalletOptions, } from "./wallet.js";
|
|
76
76
|
export type { Topup } from "./products/billing.js";
|
|
77
|
-
export type { NewKey, MadeKey, ListedKey, KeyUsage } from "./products/api-keys.js";
|
|
77
|
+
export type { NewKey, LimitedKey, LimitedKeyAction, MadeKey, ListedKey, KeyUsage, } from "./products/api-keys.js";
|
|
78
78
|
export type { SsoConnection, SsoStatus } from "./products/sso.js";
|
package/dist/machine.d.ts
CHANGED
|
@@ -95,9 +95,12 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
|
|
|
95
95
|
* while a snapshot or fork of it is being taken. `restart` is optional:
|
|
96
96
|
* true is the only value, and it is sent either way, so an API from before
|
|
97
97
|
* it was optional accepts the call too. */
|
|
98
|
-
resize(
|
|
98
|
+
resize(
|
|
99
|
+
/** diskMiB only grows; the filesystem fills it at the restart. */
|
|
100
|
+
size: {
|
|
99
101
|
vcpu?: number;
|
|
100
102
|
memoryMiB?: number;
|
|
103
|
+
diskMiB?: number;
|
|
101
104
|
}, options?: RequestOptions & {
|
|
102
105
|
restart?: true;
|
|
103
106
|
}): Promise<this>;
|
package/dist/machine.js
CHANGED
|
@@ -175,7 +175,9 @@ export class Machine {
|
|
|
175
175
|
* while a snapshot or fork of it is being taken. `restart` is optional:
|
|
176
176
|
* true is the only value, and it is sent either way, so an API from before
|
|
177
177
|
* it was optional accepts the call too. */
|
|
178
|
-
async resize(
|
|
178
|
+
async resize(
|
|
179
|
+
/** diskMiB only grows; the filesystem fills it at the restart. */
|
|
180
|
+
size, options = {}) {
|
|
179
181
|
const { restart = true, ...rest } = options;
|
|
180
182
|
this.#keep(await this.#t.json({
|
|
181
183
|
method: "POST",
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { RequestOptions, Transport } from "../transport.js";
|
|
2
|
-
|
|
2
|
+
/** What a key limited to sandboxes may do to them. get reads them; create
|
|
3
|
+
* starts, wakes and restarts them (never makes one), and is the one that
|
|
4
|
+
* spends; stop stops and pauses them; exec runs commands and processes;
|
|
5
|
+
* read_file and write_file reach their files. */
|
|
6
|
+
export type LimitedKeyAction = "get" | "create" | "stop" | "exec" | "read_file" | "write_file";
|
|
7
|
+
export type NewKey = FullOrReadKey | LimitedKey;
|
|
8
|
+
type FullOrReadKey = {
|
|
3
9
|
/** The key's name, and its agent's: up to 80 characters. */
|
|
4
10
|
name: string;
|
|
5
11
|
/** full (the default): every action. read: every read, nothing else. */
|
|
@@ -11,6 +17,19 @@ export type NewKey = {
|
|
|
11
17
|
* when the making key has a limit: the limits of the keys a key made add
|
|
12
18
|
* up to at most its own. */
|
|
13
19
|
dailyLimitMicros?: number | null;
|
|
20
|
+
sandboxes?: never;
|
|
21
|
+
actions?: never;
|
|
22
|
+
};
|
|
23
|
+
/** A key limited to these sandboxes and these actions on them. */
|
|
24
|
+
export type LimitedKey = {
|
|
25
|
+
name: string;
|
|
26
|
+
/** 1 to 20 sandbox ids this key acts on, outside every compartment. */
|
|
27
|
+
sandboxes: string[];
|
|
28
|
+
actions: LimitedKeyAction[];
|
|
29
|
+
/** Only with create, the one action that spends. */
|
|
30
|
+
dailyLimitMicros?: number | null;
|
|
31
|
+
access?: never;
|
|
32
|
+
reach?: never;
|
|
14
33
|
};
|
|
15
34
|
export type MadeKey = {
|
|
16
35
|
/** The key itself. Shown once: store it now. */
|
|
@@ -19,9 +38,15 @@ export type MadeKey = {
|
|
|
19
38
|
agentId: string;
|
|
20
39
|
orgId: string;
|
|
21
40
|
name: string;
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
/** selected: a key limited to sandboxes. */
|
|
42
|
+
access: "full" | "read" | "selected";
|
|
43
|
+
/** sandboxes: only the sandboxes it names. */
|
|
44
|
+
reach: "agent" | "account" | "sandboxes";
|
|
24
45
|
dailyLimitMicros: number | null;
|
|
46
|
+
/** The sandboxes it is limited to, or null. */
|
|
47
|
+
sandboxes: string[] | null;
|
|
48
|
+
/** Its actions, when it has named ones, or null. */
|
|
49
|
+
actions: string[] | null;
|
|
25
50
|
/** The making key's expiry, or null for until revoked. */
|
|
26
51
|
expires: string | null;
|
|
27
52
|
/** The id of the key that made it. */
|
|
@@ -32,8 +57,10 @@ export type ListedKey = {
|
|
|
32
57
|
agentId: string;
|
|
33
58
|
name: string;
|
|
34
59
|
access: "full" | "read" | "selected";
|
|
35
|
-
reach: "agent" | "account";
|
|
60
|
+
reach: "agent" | "account" | "sandboxes";
|
|
36
61
|
dailyLimitMicros: number | null;
|
|
62
|
+
sandboxes: string[] | null;
|
|
63
|
+
actions: string[] | null;
|
|
37
64
|
/** Committed in the last 24 hours: settled plus held, as the limit counts. */
|
|
38
65
|
dailyUsedMicros: number;
|
|
39
66
|
/** The key that made it, or null for one made on the website. */
|
|
@@ -71,3 +98,4 @@ export declare function keys(t: Transport): {
|
|
|
71
98
|
/** What one key spent: itself, or a key it made. */
|
|
72
99
|
usage: (keyId: string, options?: RequestOptions) => Promise<KeyUsage>;
|
|
73
100
|
};
|
|
101
|
+
export {};
|
package/dist/products/apps.d.ts
CHANGED
|
@@ -90,6 +90,12 @@ export type DeployApp = ({
|
|
|
90
90
|
image?: never;
|
|
91
91
|
}) & {
|
|
92
92
|
env?: Record<string, string>;
|
|
93
|
+
/** Carry the last deploy's environment, still sealed. With env, that
|
|
94
|
+
* environment with env's names set: change one value without sending the rest
|
|
95
|
+
* (needs a key that may reveal the App's secrets, secrets_reveal; else 403).
|
|
96
|
+
* Only for a build a push to the production branch made, from the repository the
|
|
97
|
+
* environment came with; otherwise refused with keep_env_refused (409). */
|
|
98
|
+
keepEnv?: boolean;
|
|
93
99
|
port?: number;
|
|
94
100
|
healthPath?: string;
|
|
95
101
|
/** Return live, failed or superseded, including its URL/version or failure details. */
|
|
@@ -100,6 +106,10 @@ export type AppVersion = {
|
|
|
100
106
|
live: boolean;
|
|
101
107
|
previous: boolean;
|
|
102
108
|
saved: boolean;
|
|
109
|
+
/** Its deploy's id, for GET /v1/apps/{id}/deploys/{deploy}. */
|
|
110
|
+
deploy: string;
|
|
111
|
+
/** The build it was deployed from, or null for an image. */
|
|
112
|
+
build: string | null;
|
|
103
113
|
};
|
|
104
114
|
export type AppCopy = {
|
|
105
115
|
id: string;
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export declare class Sandbox extends Machine<SandboxInfo, SandboxSettings, Delet
|
|
|
73
73
|
rows?: number;
|
|
74
74
|
command?: string;
|
|
75
75
|
cwd?: string;
|
|
76
|
+
/** "root" opens the shell (or runs `command`) as root. */
|
|
77
|
+
user?: "sandbox" | "root";
|
|
76
78
|
/** Attach to a process started with a pty instead of a new shell. */
|
|
77
79
|
processId?: string;
|
|
78
80
|
onData?: (data: Uint8Array) => void;
|
package/dist/sandbox.js
CHANGED
|
@@ -79,6 +79,7 @@ function commandBody(command, options) {
|
|
|
79
79
|
...(typeof command === "string" ? { command } : { argv: [...command] }),
|
|
80
80
|
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
81
81
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
82
|
+
...(options.user === undefined ? {} : { user: options.user }),
|
|
82
83
|
...(options.stdin === undefined
|
|
83
84
|
? {}
|
|
84
85
|
: typeof options.stdin === "string"
|
|
@@ -314,6 +315,7 @@ export class Sandbox extends Machine {
|
|
|
314
315
|
rows: options.rows,
|
|
315
316
|
command: options.command,
|
|
316
317
|
cwd: options.cwd,
|
|
318
|
+
user: options.user,
|
|
317
319
|
processId: options.processId,
|
|
318
320
|
});
|
|
319
321
|
return Terminal.open(socket, options.onData);
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.17.1";
|
|
3
3
|
export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
4
4
|
/** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
5
5
|
* own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
|
package/dist/transport.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
|
|
2
2
|
import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
|
|
3
3
|
import { warningText } from "./product-renames.js";
|
|
4
|
-
export const VERSION = "0.
|
|
4
|
+
export const VERSION = "0.17.1";
|
|
5
5
|
export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
6
6
|
/** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
7
7
|
* own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
|
package/dist/types.d.ts
CHANGED
|
@@ -200,6 +200,10 @@ export type CommandResult = {
|
|
|
200
200
|
};
|
|
201
201
|
export type ExecOptions = {
|
|
202
202
|
cwd?: string;
|
|
203
|
+
/** Who runs it: "sandbox" (the default, uid 1000, HOME=/workspace) or
|
|
204
|
+
* "root", through the sandbox's passwordless sudo with the same
|
|
205
|
+
* environment and PATH and HOME=/root: no need to prefix it with sudo. */
|
|
206
|
+
user?: "sandbox" | "root";
|
|
203
207
|
/** Merged over the sandbox's environment. Put secrets here, never in the command. */
|
|
204
208
|
env?: Record<string, string>;
|
|
205
209
|
/** Given to standard input, then closed. */
|
|
@@ -220,6 +224,8 @@ export type ProcessInfo = {
|
|
|
220
224
|
state: "running" | "exited" | "killed" | "timed_out" | "unknown";
|
|
221
225
|
exitCode: number | null;
|
|
222
226
|
command: string;
|
|
227
|
+
/** Who it runs as; absent from APIs before 17 October 2026. */
|
|
228
|
+
user?: "sandbox" | "root";
|
|
223
229
|
cwd: string;
|
|
224
230
|
pty: boolean;
|
|
225
231
|
stdinOpen: boolean;
|