withruntime 0.8.1 → 0.8.3
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 +44 -42
- package/DAYTONA.md +16 -16
- package/E2B.md +1 -1
- package/README.md +30 -5
- package/README.src.md +194 -0
- package/VERCEL.md +1 -1
- package/dist/blaxel/index.d.ts +1 -1
- package/dist/blaxel/index.js +2 -2
- package/dist/blaxel/sandbox.d.ts +8 -2
- package/dist/blaxel/sandbox.js +16 -4
- package/dist/blaxel/session.d.ts +31 -0
- package/dist/blaxel/session.js +134 -0
- package/dist/blaxel/unsupported.d.ts +2 -4
- package/dist/blaxel/unsupported.js +2 -4
- package/dist/browser-stub.d.ts +1 -0
- package/dist/browser-stub.js +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +414 -25
- package/dist/daytona/daytona.js +7 -3
- package/dist/daytona/sandbox.js +3 -3
- package/dist/e2b/unsupported.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +4 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +213 -0
- package/dist/products/jobs.js +186 -0
- package/dist/products/secrets.d.ts +81 -9
- package/dist/products/secrets.js +214 -19
- package/dist/sandbox.d.ts +63 -1
- package/dist/sandbox.js +120 -10
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/types.d.ts +11 -6
- package/dist/vercel/sandbox.js +1 -1
- package/package.json +7 -1
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { connectionOrigins, connectionStore, resolveCredential } from "./credent
|
|
|
7
7
|
import { me, named } from "./cli-name.js";
|
|
8
8
|
import { Runtime } from "./client.js";
|
|
9
9
|
import { Sandbox as SandboxHandle } from "./sandbox.js";
|
|
10
|
-
import { RuntimeError } from "./errors.js";
|
|
10
|
+
import { NotFoundError, RuntimeError } from "./errors.js";
|
|
11
11
|
import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
|
|
12
12
|
import { billingCommand } from "./billing-cli.js";
|
|
13
13
|
import { envFetch } from "./proxy.js";
|
|
@@ -40,9 +40,12 @@ Account
|
|
|
40
40
|
[--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
|
|
41
41
|
value is read from standard input, never an argument.
|
|
42
42
|
--allow (paid): only these methods and paths get it
|
|
43
|
-
secrets ls
|
|
44
|
-
secrets
|
|
45
|
-
|
|
43
|
+
secrets ls | get <NAME> Names, hosts and placeholders, never values
|
|
44
|
+
secrets rotate <NAME> A new version of the jobs copy, from standard input
|
|
45
|
+
secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
|
|
46
|
+
never readable, by anyone
|
|
47
|
+
secrets rm <NAME> Delete a secret, every copy
|
|
48
|
+
keys create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
|
|
46
49
|
A new API key for CI, approved by an owner in the browser
|
|
47
50
|
referrals Your referral link: you both get up to $500
|
|
48
51
|
compare --from <provider> [--days 30] What your usage would cost at e2b, daytona, vercel,
|
|
@@ -146,6 +149,11 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
146
149
|
far; prints the new token
|
|
147
150
|
previews <id> Every shared port
|
|
148
151
|
unshare <id> <port> Stop sharing a port
|
|
152
|
+
session create <id> [--origin <url>]... [--ttl <s>] [--name <n>]
|
|
153
|
+
A token a web page uses for this sandbox's commands,
|
|
154
|
+
files and previews; printed once
|
|
155
|
+
session ls <id> | session revoke <id> <session-id>
|
|
156
|
+
Its sessions; end one now
|
|
149
157
|
network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
150
158
|
Show its network rules, or replace them
|
|
151
159
|
identity-token --audience <aud> [--lifetime <seconds>]
|
|
@@ -328,6 +336,7 @@ const COMMANDS = [
|
|
|
328
336
|
"snapshot",
|
|
329
337
|
"image",
|
|
330
338
|
"volume",
|
|
339
|
+
"job",
|
|
331
340
|
"domain",
|
|
332
341
|
"port",
|
|
333
342
|
"address",
|
|
@@ -350,6 +359,8 @@ const OWN_HELP = [
|
|
|
350
359
|
"images",
|
|
351
360
|
"volume",
|
|
352
361
|
"volumes",
|
|
362
|
+
"job",
|
|
363
|
+
"jobs",
|
|
353
364
|
"keys",
|
|
354
365
|
"webhooks",
|
|
355
366
|
"webhook",
|
|
@@ -397,6 +408,7 @@ const SANDBOX_VERBS = [
|
|
|
397
408
|
"preview",
|
|
398
409
|
"previews",
|
|
399
410
|
"unshare",
|
|
411
|
+
"session",
|
|
400
412
|
"network",
|
|
401
413
|
"desktop",
|
|
402
414
|
"metrics",
|
|
@@ -899,6 +911,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
899
911
|
product = "volume";
|
|
900
912
|
return volume(rest, env, out);
|
|
901
913
|
}
|
|
914
|
+
if (command === "job" || command === "jobs") {
|
|
915
|
+
product = "job";
|
|
916
|
+
return jobCommand(rest, env, out);
|
|
917
|
+
}
|
|
902
918
|
const networkProduct = { domains: "domain", ports: "port", addresses: "address", tunnels: "tunnel" }[command] ??
|
|
903
919
|
command;
|
|
904
920
|
if (NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
@@ -1089,6 +1105,38 @@ const RIVAL_NAMES = {
|
|
|
1089
1105
|
fly: "Fly",
|
|
1090
1106
|
blaxel: "Blaxel",
|
|
1091
1107
|
};
|
|
1108
|
+
/** The rivals whose SDK runs on Runtime unchanged but for one import, and that
|
|
1109
|
+
* import in JavaScript and Python. Held to the guides' own lines by
|
|
1110
|
+
* `tests/cli-drop-in.test.ts`. */
|
|
1111
|
+
const DROP_IN = {
|
|
1112
|
+
e2b: {
|
|
1113
|
+
js: 'import { Sandbox } from "withruntime/e2b"; // was: from "e2b"',
|
|
1114
|
+
py: "from withruntime.e2b import Sandbox # was: from e2b import Sandbox",
|
|
1115
|
+
},
|
|
1116
|
+
daytona: {
|
|
1117
|
+
js: 'import { Daytona } from "withruntime/daytona"; // was: from "@daytona/sdk"',
|
|
1118
|
+
py: "from withruntime.daytona import Daytona # was: from daytona import Daytona",
|
|
1119
|
+
},
|
|
1120
|
+
vercel: {
|
|
1121
|
+
js: 'import { Sandbox } from "withruntime/vercel"; // was: from "@vercel/sandbox"',
|
|
1122
|
+
py: "from withruntime.vercel import sandbox # was: from vercel import sandbox",
|
|
1123
|
+
},
|
|
1124
|
+
blaxel: {
|
|
1125
|
+
js: 'import { SandboxInstance } from "withruntime/blaxel"; // was: from "@blaxel/core"',
|
|
1126
|
+
py: "from withruntime.blaxel import SandboxInstance # was: from blaxel.core import SandboxInstance",
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
/** How to move a rival's code across, when one import does it. */
|
|
1130
|
+
export function dropInLines(provider) {
|
|
1131
|
+
const drop = provider && DROP_IN[provider];
|
|
1132
|
+
if (!drop)
|
|
1133
|
+
return [];
|
|
1134
|
+
return [
|
|
1135
|
+
`Your ${RIVAL_NAMES[provider]} code runs on Runtime with one import changed:`,
|
|
1136
|
+
` ${drop.js}`,
|
|
1137
|
+
` ${drop.py}`,
|
|
1138
|
+
];
|
|
1139
|
+
}
|
|
1092
1140
|
/** A date as the CLI writes one: 23 September 2026. */
|
|
1093
1141
|
const longDate = (iso) => new Date(iso).toLocaleDateString("en-GB", {
|
|
1094
1142
|
day: "numeric",
|
|
@@ -1175,7 +1223,10 @@ async function switching(command, argv, env, out) {
|
|
|
1175
1223
|
if (!provider || from === "fly-machines")
|
|
1176
1224
|
throw unknown("provider", from, Object.keys(RIVAL_NAMES));
|
|
1177
1225
|
const summary = await api.record({ provider });
|
|
1178
|
-
print(
|
|
1226
|
+
print([
|
|
1227
|
+
`Recorded: switching from ${RIVAL_NAMES[provider]}. ${switchingLine(summary) ?? ""}`.trim(),
|
|
1228
|
+
...dropInLines(provider),
|
|
1229
|
+
].join("\n"), summary);
|
|
1179
1230
|
return 0;
|
|
1180
1231
|
}
|
|
1181
1232
|
if (!from)
|
|
@@ -1217,9 +1268,13 @@ async function switching(command, argv, env, out) {
|
|
|
1217
1268
|
`The example: ${figures}.`,
|
|
1218
1269
|
];
|
|
1219
1270
|
lines.push(`${c.rival.name}'s published rates, checked ${longDate(`${c.rival.checked}T12:00:00Z`)}: ${c.rival.rates.join("; ")}. Compute only, with the disk where a rival bills it while running: storage at rest, network, plan fees and free allowances are left out.`);
|
|
1220
|
-
const
|
|
1271
|
+
const rival = Object.hasOwn(SWITCH_FROM, from) ? SWITCH_FROM[from] : undefined;
|
|
1272
|
+
const line = switchingLine(c.switching, rival);
|
|
1221
1273
|
if (line)
|
|
1222
1274
|
lines.push("", line);
|
|
1275
|
+
const drop = dropInLines(rival);
|
|
1276
|
+
if (drop.length)
|
|
1277
|
+
lines.push("", ...drop);
|
|
1223
1278
|
print(lines.join("\n"), c);
|
|
1224
1279
|
return 0;
|
|
1225
1280
|
}
|
|
@@ -1603,6 +1658,43 @@ async function sandbox(argv, env, out) {
|
|
|
1603
1658
|
: "No shared ports. Share one: runtime sandbox preview <id> 3000", list);
|
|
1604
1659
|
return 0;
|
|
1605
1660
|
}
|
|
1661
|
+
case "session": {
|
|
1662
|
+
const verb = rest[0];
|
|
1663
|
+
if (verb === "ls") {
|
|
1664
|
+
const list = await (await use(rest[1])).sessions.list();
|
|
1665
|
+
print(list.length
|
|
1666
|
+
? table([
|
|
1667
|
+
["ID", "STATE", "EXPIRES", "ORIGINS"],
|
|
1668
|
+
...list.map((one) => [
|
|
1669
|
+
one.id,
|
|
1670
|
+
one.state,
|
|
1671
|
+
one.expiresAt,
|
|
1672
|
+
one.origins.join(",") || "-",
|
|
1673
|
+
]),
|
|
1674
|
+
])
|
|
1675
|
+
: "No sessions. Make one: runtime sandbox session create <id> --origin https://app.example.com", list);
|
|
1676
|
+
return 0;
|
|
1677
|
+
}
|
|
1678
|
+
if (verb === "revoke") {
|
|
1679
|
+
const sessionId = need(rest[2], "a session id: runtime sandbox session revoke <id> <session-id>");
|
|
1680
|
+
const ended = await (await use(rest[1])).sessions.revoke(sessionId);
|
|
1681
|
+
print(`Session ${ended.id} ended.`, ended);
|
|
1682
|
+
return 0;
|
|
1683
|
+
}
|
|
1684
|
+
if (verb !== "create")
|
|
1685
|
+
throw usage("runtime sandbox session create <id> [--origin <url>]..., session ls <id>, or session revoke <id> <session-id>");
|
|
1686
|
+
const args = parse(rest.slice(1), [], ["origin", "ttl", "name"]);
|
|
1687
|
+
const origins = args.flags.get("origin") ?? [];
|
|
1688
|
+
const ttl = integer(args, "ttl");
|
|
1689
|
+
const name = flag(args, "name");
|
|
1690
|
+
const made = await (await use(args.positional[0])).sessions.create({
|
|
1691
|
+
...(ttl === undefined ? {} : { ttlSeconds: ttl }),
|
|
1692
|
+
...(origins.length ? { origins } : {}),
|
|
1693
|
+
...(name === undefined ? {} : { name }),
|
|
1694
|
+
});
|
|
1695
|
+
print(`${made.token}\nFor sandbox ${made.sandboxId} until ${made.expiresAt}${made.origins.length ? `, from ${made.origins.join(", ")}` : ", from no browser page"}. Shown once: hand it to your page (Sandbox.fromSession), never log it.`, made);
|
|
1696
|
+
return 0;
|
|
1697
|
+
}
|
|
1606
1698
|
case "unshare": {
|
|
1607
1699
|
const port = portOf(rest[1], "runtime sandbox unshare <id> 3000");
|
|
1608
1700
|
const sbx = await use(rest[0]);
|
|
@@ -1750,45 +1842,70 @@ export async function secretsCommand(argv, env, out,
|
|
|
1750
1842
|
/** Tests give the value here; the CLI reads standard input. */
|
|
1751
1843
|
read = readSecret) {
|
|
1752
1844
|
const [verb, ...rest] = argv;
|
|
1753
|
-
const args = parse(rest, [], ["host", "header", "format", "allow", "value"]);
|
|
1845
|
+
const args = parse(rest, ["jobs"], ["host", "header", "format", "allow", "value", "version"]);
|
|
1754
1846
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1755
1847
|
const rows = (list) => list.length
|
|
1756
1848
|
? table([
|
|
1757
|
-
["name", "hosts", "header", "placeholder", "allow"],
|
|
1758
|
-
...list.map((secret) => [
|
|
1759
|
-
|
|
1760
|
-
secret.hosts.join(","),
|
|
1761
|
-
secret
|
|
1762
|
-
secret.placeholder,
|
|
1763
|
-
ruleText(secret.rules),
|
|
1849
|
+
["name", "hosts", "header", "placeholder", "allow", "jobs"],
|
|
1850
|
+
...list.map(({ name, sandboxes: secret, jobs }) => [
|
|
1851
|
+
name,
|
|
1852
|
+
secret ? secret.hosts.join(",") : "-",
|
|
1853
|
+
secret?.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
|
|
1854
|
+
secret ? secret.placeholder : "-",
|
|
1855
|
+
secret ? ruleText(secret.rules) : "-",
|
|
1856
|
+
jobs ? `v${jobs.version}` : "-",
|
|
1764
1857
|
]),
|
|
1765
1858
|
])
|
|
1766
1859
|
: "No secrets.";
|
|
1767
1860
|
if (verb === "ls" || verb === "list" || verb === undefined) {
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1861
|
+
const runtime = await client(env);
|
|
1862
|
+
const all = await runtime.secrets.all();
|
|
1863
|
+
// --json keeps the sandboxes' list it always printed; the jobs copies
|
|
1864
|
+
// are in `runtime secrets get <NAME> --json`.
|
|
1865
|
+
print(rows(all), all.flatMap((s) => (s.sandboxes ? [s.sandboxes] : [])));
|
|
1866
|
+
return 0;
|
|
1867
|
+
}
|
|
1868
|
+
const jobsOnly = "Only a secret's copy for jobs has versions and can be read back";
|
|
1869
|
+
if (verb === "get") {
|
|
1870
|
+
const found = await (await client(env)).secrets.get(need(args.positional[0], "the secret's name"));
|
|
1871
|
+
print(rows([found]), found);
|
|
1770
1872
|
return 0;
|
|
1771
1873
|
}
|
|
1772
|
-
if (verb === "set") {
|
|
1874
|
+
if (verb === "set" || verb === "rotate") {
|
|
1773
1875
|
const name = need(args.positional[0], "the secret's name, such as OPENAI_API_KEY");
|
|
1774
1876
|
const hosts = args.flags.get("host") ?? [];
|
|
1775
|
-
|
|
1776
|
-
|
|
1877
|
+
const jobs = verb === "rotate" || has(args, "jobs");
|
|
1878
|
+
if (verb === "rotate" && (hosts.length || args.flags.has("header") || args.flags.has("allow")))
|
|
1879
|
+
throw usage(`rotate replaces the copy for jobs. ${jobsOnly}; to change the sandboxes' copy, set it again with --host.`);
|
|
1880
|
+
if (!hosts.length && !jobs)
|
|
1881
|
+
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.");
|
|
1777
1882
|
if (args.flags.has("value"))
|
|
1778
1883
|
throw usage('The value is read from standard input, never an argument: printf %s "$KEY" | runtime secrets set NAME --host ...');
|
|
1779
1884
|
const value = await read();
|
|
1780
1885
|
if (!value)
|
|
1781
1886
|
throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
|
|
1887
|
+
const runtime = await client(env);
|
|
1888
|
+
if (verb === "rotate") {
|
|
1889
|
+
const rotated = await runtime.secrets.rotate(name, value);
|
|
1890
|
+
print(`Rotated ${name} for jobs to version ${rotated.version}. Runs bound to the old version are refused it.`, rotated);
|
|
1891
|
+
return 0;
|
|
1892
|
+
}
|
|
1893
|
+
if (!hosts.length) {
|
|
1894
|
+
const saved = await runtime.secrets.set(name, { value, jobs: true });
|
|
1895
|
+
print(`Stored ${name} for jobs, version ${saved.jobs.version}. Bind it: ${me} job create ... --secret ${name}`, saved);
|
|
1896
|
+
return 0;
|
|
1897
|
+
}
|
|
1782
1898
|
const header = flag(args, "header");
|
|
1783
1899
|
const format = flag(args, "format");
|
|
1784
1900
|
const allow = args.flags.get("allow");
|
|
1785
1901
|
const rules = allow ? parseSecretRules(allow) : undefined;
|
|
1786
|
-
const saved = await
|
|
1902
|
+
const saved = await runtime.secrets.set(name, {
|
|
1787
1903
|
value,
|
|
1788
1904
|
hosts,
|
|
1789
1905
|
...(header ? { header } : {}),
|
|
1790
1906
|
...(format ? { format } : {}),
|
|
1791
1907
|
...(rules ? { rules } : {}),
|
|
1908
|
+
...(jobs ? { jobs: true } : {}),
|
|
1792
1909
|
});
|
|
1793
1910
|
print([
|
|
1794
1911
|
`Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
|
|
@@ -1799,16 +1916,30 @@ read = readSecret) {
|
|
|
1799
1916
|
...(saved.enforced
|
|
1800
1917
|
? []
|
|
1801
1918
|
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
1919
|
+
...(saved.jobs
|
|
1920
|
+
? [
|
|
1921
|
+
`Also stored for jobs, version ${saved.jobs.version}: --secret ${saved.name} on ${me} job create.`,
|
|
1922
|
+
]
|
|
1923
|
+
: []),
|
|
1802
1924
|
].join("\n"), saved);
|
|
1803
1925
|
return 0;
|
|
1804
1926
|
}
|
|
1927
|
+
if (verb === "reveal") {
|
|
1928
|
+
const name = need(args.positional[0], "the secret's name");
|
|
1929
|
+
const version = flag(args, "version");
|
|
1930
|
+
if (version !== undefined && !/^\d+$/.test(version))
|
|
1931
|
+
throw usage("--version takes a number.");
|
|
1932
|
+
const shown = await (await client(env)).secrets.reveal(name, version === undefined ? {} : { version: Number(version) });
|
|
1933
|
+
print(shown.value, shown);
|
|
1934
|
+
return 0;
|
|
1935
|
+
}
|
|
1805
1936
|
if (verb === "rm" || verb === "delete") {
|
|
1806
1937
|
const name = need(args.positional[0], "the secret's name");
|
|
1807
1938
|
const removed = await (await client(env)).secrets.delete(name);
|
|
1808
|
-
print(`Deleted ${name}.`, removed);
|
|
1939
|
+
print(`Deleted ${name}${removed.from.length > 1 ? " for sandboxes and jobs" : removed.from[0] === "jobs" ? " for jobs" : ""}.`, removed);
|
|
1809
1940
|
return 0;
|
|
1810
1941
|
}
|
|
1811
|
-
throw usage("secrets takes set, ls or rm. Run `runtime help`.");
|
|
1942
|
+
throw usage("secrets takes set, ls, get, rotate, reveal or rm. Run `runtime help`.");
|
|
1812
1943
|
}
|
|
1813
1944
|
async function readInput(source) {
|
|
1814
1945
|
if (source !== "-")
|
|
@@ -1930,11 +2061,14 @@ async function desktop(sbx, argv, out) {
|
|
|
1930
2061
|
}
|
|
1931
2062
|
const KEYS_HELP = `runtime keys <command>
|
|
1932
2063
|
|
|
1933
|
-
create [--name <name>] [--read-only] [--daily-limit <usd>]
|
|
2064
|
+
create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
|
|
1934
2065
|
A new API key, for a CI runner or any place with no browser. Prints a link
|
|
1935
2066
|
and a code; an account owner approves the key in the browser, seeing its
|
|
1936
2067
|
name, access and limit. The key is printed once, alone on stdout, and
|
|
1937
2068
|
lasts until revoked. --json prints it as JSON, still alone on stdout.
|
|
2069
|
+
--account-wide makes a key that sees and uses every sandbox and other
|
|
2070
|
+
resource in the account, whichever key made it; only an owner or admin
|
|
2071
|
+
can approve one. Without it, a key sees only what it makes.
|
|
1938
2072
|
|
|
1939
2073
|
runtime keys create --name ci --daily-limit 25 | gh secret set RUNTIME_API_KEY
|
|
1940
2074
|
|
|
@@ -2060,8 +2194,8 @@ async function keys(argv, env, out) {
|
|
|
2060
2194
|
throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
|
|
2061
2195
|
if (verb !== "create")
|
|
2062
2196
|
throw unknown("keys command", verb, ["create"]);
|
|
2063
|
-
const args = parse(rest, ["read-only"], ["name", "daily-limit"]);
|
|
2064
|
-
const known = new Set(["name", "read-only", "daily-limit"]);
|
|
2197
|
+
const args = parse(rest, ["read-only", "account-wide"], ["name", "daily-limit"]);
|
|
2198
|
+
const known = new Set(["name", "read-only", "daily-limit", "account-wide"]);
|
|
2065
2199
|
for (const name of args.flags.keys())
|
|
2066
2200
|
if (!known.has(name))
|
|
2067
2201
|
throw usage(`Unknown option --${name}.`);
|
|
@@ -2071,16 +2205,20 @@ async function keys(argv, env, out) {
|
|
|
2071
2205
|
const limit = flag(args, "daily-limit");
|
|
2072
2206
|
if (readOnly && limit !== undefined)
|
|
2073
2207
|
throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
|
|
2208
|
+
if (readOnly && has(args, "account-wide"))
|
|
2209
|
+
throw usage("A read-only key already sees the whole account, so it takes no --account-wide.");
|
|
2074
2210
|
const { createKey, dailyLimitMicros } = await import("./keys.js");
|
|
2075
2211
|
const created = await createKey({
|
|
2076
2212
|
name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
|
|
2077
2213
|
access: readOnly ? "read" : "full",
|
|
2214
|
+
...(has(args, "account-wide") ? { reach: "account" } : {}),
|
|
2078
2215
|
dailyLimitMicros: limit === undefined ? null : dailyLimitMicros(limit),
|
|
2079
2216
|
}, env, { notify: out.error });
|
|
2080
2217
|
const { confirm, ...key } = created;
|
|
2081
2218
|
out.write(out.json ? JSON.stringify(key) : key.key);
|
|
2082
2219
|
const terms = [
|
|
2083
2220
|
key.access === "read" ? "read only" : "full access",
|
|
2221
|
+
key.reach === "account" ? "account-wide" : undefined,
|
|
2084
2222
|
key.access === "read"
|
|
2085
2223
|
? undefined
|
|
2086
2224
|
: key.dailyLimitMicros === null
|
|
@@ -2398,6 +2536,257 @@ read = readSecret) {
|
|
|
2398
2536
|
"registry",
|
|
2399
2537
|
]);
|
|
2400
2538
|
}
|
|
2539
|
+
const JOB_HELP = `runtime job <command>
|
|
2540
|
+
|
|
2541
|
+
A job runs a command in a fresh sandbox, once or on a schedule. Each run is a
|
|
2542
|
+
paid sandbox billed at the sandbox rates; the trial's hours do not fund jobs.
|
|
2543
|
+
|
|
2544
|
+
create <name> (--cron "<min hour day month weekday>" [--timezone Europe/Berlin] | --at <time|now>)
|
|
2545
|
+
[--vcpu 2] [--memory 4096] [--disk 4096] [--cpu shared|reserved] [--cpu-floor <thousandths>]
|
|
2546
|
+
[--timeout 1800] [--attempts 1] [--backoff 30] [--secret NAME[=ENV_NAME]]...
|
|
2547
|
+
[--cwd /workspace/dir] [--max-cost <usd>] [--max-total-cost <usd>] [--region r] -- <command...>
|
|
2548
|
+
Schedule it; prints its id. Nothing runs until its time.
|
|
2549
|
+
--at takes an ISO time (2026-10-01T03:00:00Z) or now.
|
|
2550
|
+
The command runs as given, no shell: -- bash -lc '...'
|
|
2551
|
+
--attempts: tries per occurrence when it exits non-zero, 1 to 5.
|
|
2552
|
+
--secret: a secret stored with --jobs, in the run's
|
|
2553
|
+
environment as ENV_NAME (NAME by default)
|
|
2554
|
+
ls Your jobs: state, schedule and next run
|
|
2555
|
+
get <id> One job, in full
|
|
2556
|
+
runs <id> Its runs: state, exit code, when
|
|
2557
|
+
logs <runId|jobId> [-f] A run's output (a job id: its latest run); -f follows it
|
|
2558
|
+
pause <id> | resume <id> Stop or restart scheduling; a run already going finishes
|
|
2559
|
+
cancel <id> End it for good and stop a run in progress
|
|
2560
|
+
|
|
2561
|
+
Examples
|
|
2562
|
+
runtime job create nightly --cron "0 3 * * *" --timezone Europe/Berlin -- python3 /workspace/report.py
|
|
2563
|
+
runtime job create once --at now -- bash -lc 'echo hello from a job'
|
|
2564
|
+
runtime job logs <jobId> -f
|
|
2565
|
+
`;
|
|
2566
|
+
const when = (ms) => (ms == null ? "-" : new Date(ms).toISOString());
|
|
2567
|
+
function scheduleText(job) {
|
|
2568
|
+
const s = job.definition.schedule;
|
|
2569
|
+
return s.kind === "once" ? `once at ${when(s.at)}` : `${s.expression} ${s.timezone ?? "UTC"}`;
|
|
2570
|
+
}
|
|
2571
|
+
/** Seconds from the command line, `--at`: an ISO time, Unix milliseconds or
|
|
2572
|
+
* `now`. A time well in the past is refused: the job would run at once, and
|
|
2573
|
+
* that is what `now` says. */
|
|
2574
|
+
function atTime(value, now = Date.now()) {
|
|
2575
|
+
if (value === "now")
|
|
2576
|
+
return now;
|
|
2577
|
+
const at = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
|
|
2578
|
+
if (!Number.isSafeInteger(at))
|
|
2579
|
+
throw usage(`--at takes an ISO time such as 2026-10-01T03:00:00Z, or now; got ${value}.`);
|
|
2580
|
+
if (at < now - 60_000)
|
|
2581
|
+
throw usage(`--at ${value} is in the past. To run it at once, say --at now.`);
|
|
2582
|
+
return at;
|
|
2583
|
+
}
|
|
2584
|
+
async function jobCommand(argv, env, out) {
|
|
2585
|
+
const [verb, ...rest] = argv;
|
|
2586
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
2587
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
2588
|
+
print(named(JOB_HELP), { usage: named(JOB_HELP) });
|
|
2589
|
+
return 0;
|
|
2590
|
+
}
|
|
2591
|
+
const runtime = await client(env);
|
|
2592
|
+
const api = runtime.jobs;
|
|
2593
|
+
switch (verb) {
|
|
2594
|
+
case "create": {
|
|
2595
|
+
const args = parse(rest, [], [
|
|
2596
|
+
"cron",
|
|
2597
|
+
"timezone",
|
|
2598
|
+
"at",
|
|
2599
|
+
"vcpu",
|
|
2600
|
+
"memory",
|
|
2601
|
+
"disk",
|
|
2602
|
+
"cpu",
|
|
2603
|
+
"cpu-floor",
|
|
2604
|
+
"timeout",
|
|
2605
|
+
"attempts",
|
|
2606
|
+
"backoff",
|
|
2607
|
+
"secret",
|
|
2608
|
+
"cwd",
|
|
2609
|
+
"max-cost",
|
|
2610
|
+
"max-total-cost",
|
|
2611
|
+
"region",
|
|
2612
|
+
], `${me} job create <name> --cron "0 3 * * *" -- python3 -c '...'`);
|
|
2613
|
+
const name = need(args.positional[0], "the job's name, such as nightly-report");
|
|
2614
|
+
if (args.positional.length > 1)
|
|
2615
|
+
throw usage(`Put the command after --: ${me} job create ${name} ... -- ${args.positional.slice(1).join(" ")}`);
|
|
2616
|
+
const argv = args.rest ?? [];
|
|
2617
|
+
if (!argv.length)
|
|
2618
|
+
throw usage(`Give the command after --: ${me} job create ${name} --at now -- echo hello`);
|
|
2619
|
+
const cron = flag(args, "cron");
|
|
2620
|
+
const at = flag(args, "at");
|
|
2621
|
+
if ((cron === undefined) === (at === undefined))
|
|
2622
|
+
throw usage('Give --cron "<five fields>" for a schedule, or --at <time|now> for one run.');
|
|
2623
|
+
if (at !== undefined && flag(args, "timezone") !== undefined)
|
|
2624
|
+
throw usage("--timezone goes with --cron; --at takes a time with its own offset.");
|
|
2625
|
+
const attempts = integer(args, "attempts");
|
|
2626
|
+
const backoff = integer(args, "backoff");
|
|
2627
|
+
if (backoff !== undefined && attempts === undefined)
|
|
2628
|
+
throw usage("--backoff goes with --attempts: the seconds between tries.");
|
|
2629
|
+
const bindings = args.flags.get("secret") ?? [];
|
|
2630
|
+
let secrets;
|
|
2631
|
+
if (bindings.length) {
|
|
2632
|
+
const known = await runtime.secrets.all();
|
|
2633
|
+
secrets = bindings.map((binding) => {
|
|
2634
|
+
const [secret, env = secret] = binding.split("=", 2);
|
|
2635
|
+
const found = known.find((s) => s.name === secret)?.jobs;
|
|
2636
|
+
if (!found)
|
|
2637
|
+
throw usage(`${secret} has no copy for jobs. Store one: printf %s "$VALUE" | ${me} secrets set ${secret} --jobs`);
|
|
2638
|
+
return { name: env, secretId: found.id };
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
const cwd = flag(args, "cwd");
|
|
2642
|
+
const job = await api.create({
|
|
2643
|
+
name,
|
|
2644
|
+
schedule: cron !== undefined
|
|
2645
|
+
? { cron, ...(flag(args, "timezone") ? { timezone: flag(args, "timezone") } : {}) }
|
|
2646
|
+
: { at: atTime(at) },
|
|
2647
|
+
command: cwd ? { argv, cwd } : argv,
|
|
2648
|
+
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
2649
|
+
compute: {
|
|
2650
|
+
...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
|
|
2651
|
+
...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
|
|
2652
|
+
...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
|
|
2653
|
+
...(flag(args, "cpu")
|
|
2654
|
+
? { cpuMode: oneOf(args, "cpu", ["shared", "reserved"]) }
|
|
2655
|
+
: {}),
|
|
2656
|
+
...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
|
|
2657
|
+
...(flag(args, "region") ? { region: flag(args, "region") } : {}),
|
|
2658
|
+
...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
|
|
2659
|
+
},
|
|
2660
|
+
...(attempts !== undefined
|
|
2661
|
+
? { retry: { maxAttempts: attempts, backoffSeconds: backoff ?? 30 } }
|
|
2662
|
+
: {}),
|
|
2663
|
+
...(secrets ? { secrets } : {}),
|
|
2664
|
+
...(usdMicros(args, "max-total-cost")
|
|
2665
|
+
? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
|
|
2666
|
+
: {}),
|
|
2667
|
+
});
|
|
2668
|
+
if (!out.json)
|
|
2669
|
+
out.error(`Scheduled ${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}. Follow it: ${me} job logs ${job.id} -f`);
|
|
2670
|
+
print(job.id, job);
|
|
2671
|
+
return 0;
|
|
2672
|
+
}
|
|
2673
|
+
case "ls":
|
|
2674
|
+
case "list": {
|
|
2675
|
+
const all = await (await api.list({ limit: 100 })).toArray(1000);
|
|
2676
|
+
print(all.length
|
|
2677
|
+
? table([
|
|
2678
|
+
["ID", "NAME", "STATE", "SCHEDULE", "NEXT RUN", "BLOCKED"],
|
|
2679
|
+
...all.map((j) => [
|
|
2680
|
+
j.id,
|
|
2681
|
+
j.name,
|
|
2682
|
+
j.state,
|
|
2683
|
+
scheduleText(j),
|
|
2684
|
+
when(j.nextRunAt),
|
|
2685
|
+
j.blockedReason ?? "-",
|
|
2686
|
+
]),
|
|
2687
|
+
])
|
|
2688
|
+
: `No jobs. Schedule one: ${me} job create hello --at now -- echo hello`, all);
|
|
2689
|
+
return 0;
|
|
2690
|
+
}
|
|
2691
|
+
case "get": {
|
|
2692
|
+
const found = await api.get(need(rest[0], "a job id"));
|
|
2693
|
+
print(JSON.stringify(found, null, 2), found);
|
|
2694
|
+
return 0;
|
|
2695
|
+
}
|
|
2696
|
+
case "runs": {
|
|
2697
|
+
const jobId = need(rest[0], "a job id");
|
|
2698
|
+
const all = await (await api.runs(jobId, { limit: 100 })).toArray(1000);
|
|
2699
|
+
print(all.length
|
|
2700
|
+
? table([
|
|
2701
|
+
["RUN", "ATTEMPT", "STATE", "EXIT", "SCHEDULED", "STARTED", "ENDED", "REASON"],
|
|
2702
|
+
...all.map((r) => [
|
|
2703
|
+
r.id,
|
|
2704
|
+
String(r.attempt),
|
|
2705
|
+
r.state,
|
|
2706
|
+
r.exitCode === null ? "-" : String(r.exitCode),
|
|
2707
|
+
when(r.scheduledFor),
|
|
2708
|
+
when(r.startedAt),
|
|
2709
|
+
when(r.endedAt),
|
|
2710
|
+
r.reason ?? "-",
|
|
2711
|
+
]),
|
|
2712
|
+
])
|
|
2713
|
+
: "No runs yet.", all);
|
|
2714
|
+
return 0;
|
|
2715
|
+
}
|
|
2716
|
+
case "logs": {
|
|
2717
|
+
const args = parse(rest, ["f", "follow"], []);
|
|
2718
|
+
const id = need(args.positional[0], "a run id or a job id");
|
|
2719
|
+
let runId = id;
|
|
2720
|
+
try {
|
|
2721
|
+
await api.run(id);
|
|
2722
|
+
}
|
|
2723
|
+
catch (error) {
|
|
2724
|
+
if (!(error instanceof NotFoundError))
|
|
2725
|
+
throw error;
|
|
2726
|
+
// A job's id: its latest run.
|
|
2727
|
+
const runs = await (await api.runs(id, { limit: 100 })).toArray(1000);
|
|
2728
|
+
const latest = runs.at(-1);
|
|
2729
|
+
if (!latest) {
|
|
2730
|
+
const job = await api.get(id);
|
|
2731
|
+
print(`${job.name} has not run yet. Next run ${when(job.nextRunAt)}.`, {
|
|
2732
|
+
chunks: [],
|
|
2733
|
+
nextCursor: 0,
|
|
2734
|
+
truncated: false,
|
|
2735
|
+
complete: false,
|
|
2736
|
+
});
|
|
2737
|
+
return 0;
|
|
2738
|
+
}
|
|
2739
|
+
runId = latest.id;
|
|
2740
|
+
}
|
|
2741
|
+
const follow = has(args, "f") || has(args, "follow");
|
|
2742
|
+
let cursor = 0;
|
|
2743
|
+
for (;;) {
|
|
2744
|
+
const page = await api.logs(runId, { cursor, limitBytes: 65536 });
|
|
2745
|
+
if (out.json)
|
|
2746
|
+
out.write(JSON.stringify(page));
|
|
2747
|
+
else
|
|
2748
|
+
for (const chunk of page.chunks)
|
|
2749
|
+
(chunk.stream === "stderr" ? process.stderr : process.stdout).write(chunk.text);
|
|
2750
|
+
cursor = page.nextCursor;
|
|
2751
|
+
if (page.complete) {
|
|
2752
|
+
if (!out.json) {
|
|
2753
|
+
const run = await api.run(runId);
|
|
2754
|
+
if (page.truncated)
|
|
2755
|
+
process.stderr.write("[the run printed more than it keeps; this is its last part]\n");
|
|
2756
|
+
process.stderr.write(`[${run.state}${run.exitCode === null ? "" : `, exit code ${run.exitCode}`}${run.reason ? `, ${run.reason}` : ""}]\n`);
|
|
2757
|
+
}
|
|
2758
|
+
return 0;
|
|
2759
|
+
}
|
|
2760
|
+
if (page.chunks.length)
|
|
2761
|
+
continue;
|
|
2762
|
+
if (!follow) {
|
|
2763
|
+
if (!out.json)
|
|
2764
|
+
process.stderr.write(`[${runId} is still going; follow it: ${me} job logs ${runId} -f]\n`);
|
|
2765
|
+
return 0;
|
|
2766
|
+
}
|
|
2767
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
case "pause":
|
|
2771
|
+
case "resume":
|
|
2772
|
+
case "cancel": {
|
|
2773
|
+
const id = need(rest[0], "a job id");
|
|
2774
|
+
const job = await api[verb](id);
|
|
2775
|
+
print(`${job.name} ${job.state}.`, job);
|
|
2776
|
+
return 0;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
throw unknown("job command", verb, [
|
|
2780
|
+
"create",
|
|
2781
|
+
"ls",
|
|
2782
|
+
"get",
|
|
2783
|
+
"runs",
|
|
2784
|
+
"logs",
|
|
2785
|
+
"pause",
|
|
2786
|
+
"resume",
|
|
2787
|
+
"cancel",
|
|
2788
|
+
]);
|
|
2789
|
+
}
|
|
2401
2790
|
const VOLUME_HELP = `runtime volume <command>
|
|
2402
2791
|
|
|
2403
2792
|
create --size-mib <N> [--name n] A persistent ext4 disk; prints its id
|
package/dist/daytona/daytona.js
CHANGED
|
@@ -170,7 +170,7 @@ export class Daytona {
|
|
|
170
170
|
this.#create = config.withruntime?.create ?? {};
|
|
171
171
|
this.snapshot = new SnapshotService(this.#client);
|
|
172
172
|
this.volume = new VolumeService(this.#client);
|
|
173
|
-
this.secret = unsupportedService("secrets", "
|
|
173
|
+
this.secret = unsupportedService("secrets", "Use a Runtime secret: `npx withruntime secrets set NAME --host api.example.com`. The sandbox sees a placeholder, and the egress proxy adds the value on HTTPS to that host.");
|
|
174
174
|
this.warmPool = unsupportedService("warm pools", "Runtime starts sandboxes from warm templates already; nothing to configure.");
|
|
175
175
|
}
|
|
176
176
|
async [Symbol.asyncDispose]() { }
|
|
@@ -370,11 +370,15 @@ function refuseCreate(params) {
|
|
|
370
370
|
"Linked sandboxes (linkedSandbox)",
|
|
371
371
|
"Run both programs in one sandbox, or connect them through a preview address.",
|
|
372
372
|
],
|
|
373
|
-
[
|
|
373
|
+
[
|
|
374
|
+
"secrets",
|
|
375
|
+
"Daytona secrets",
|
|
376
|
+
"Use a Runtime secret: `npx withruntime secrets set NAME --host api.example.com`. The sandbox sees a placeholder, and the egress proxy adds the value on HTTPS to that host.",
|
|
377
|
+
],
|
|
374
378
|
[
|
|
375
379
|
"otelEndpointOverride",
|
|
376
380
|
"Sending sandbox telemetry elsewhere (otelEndpointOverride)",
|
|
377
|
-
"Remove it:
|
|
381
|
+
"Remove it: runtime.otel.create() exports every sandbox's events and CPU and memory metrics over OTLP/HTTP.",
|
|
378
382
|
],
|
|
379
383
|
];
|
|
380
384
|
for (const [field, feature, alternative] of refusals)
|
package/dist/daytona/sandbox.js
CHANGED
|
@@ -386,7 +386,7 @@ export class Sandbox {
|
|
|
386
386
|
return this.resize();
|
|
387
387
|
}
|
|
388
388
|
getMetrics() {
|
|
389
|
-
return refusal("Sandbox metrics", "
|
|
389
|
+
return refusal("Sandbox metrics", 'Use `await sandbox.withruntime.metrics({ range: "1h" })` for its CPU and memory over time; runtime.otel exports them to your own tools.');
|
|
390
390
|
}
|
|
391
391
|
getMetricsLatest() {
|
|
392
392
|
return this.getMetrics();
|
|
@@ -395,7 +395,7 @@ export class Sandbox {
|
|
|
395
395
|
throw new NotSupportedError("Daytona's language servers", "Start one in a session: process.executeSessionCommand(id, { command: 'pyright-langserver --stdio', runAsync: true }).");
|
|
396
396
|
}
|
|
397
397
|
updateSecrets() {
|
|
398
|
-
return refusal("Daytona secrets", "
|
|
398
|
+
return refusal("Daytona secrets", "Use a Runtime secret: `npx withruntime secrets set NAME --host api.example.com`. The sandbox sees a placeholder, and the egress proxy adds the value on HTTPS to that host.");
|
|
399
399
|
}
|
|
400
400
|
getSignedPreviewUrl() {
|
|
401
401
|
return refusal("Signed preview URLs", "Use getPreviewLink(port) and send its token as the x-runtime-preview-token header, or create the sandbox with public: true.");
|
|
@@ -413,7 +413,7 @@ export class Sandbox {
|
|
|
413
413
|
return refusal("Signed download URLs", "Use sandbox.fs.downloadFile(path).");
|
|
414
414
|
}
|
|
415
415
|
createSshAccess() {
|
|
416
|
-
return refusal("SSH access", "Use `npx withruntime sandbox
|
|
416
|
+
return refusal("SSH access", "Use `npx withruntime sandbox ssh <id>` (also VS Code and JetBrains) or sandbox.withruntime.terminal().");
|
|
417
417
|
}
|
|
418
418
|
revokeSshAccess() {
|
|
419
419
|
return this.createSshAccess();
|