withruntime 0.8.2 → 0.8.4

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 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,8 +40,11 @@ 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 Names, hosts and placeholders, never values
44
- secrets rm <NAME> Delete a secret
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
45
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
@@ -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(`Recorded: switching from ${RIVAL_NAMES[provider]}. ${switchingLine(summary) ?? ""}`.trim(), summary);
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 line = switchingLine(c.switching, Object.hasOwn(SWITCH_FROM, from) ? SWITCH_FROM[from] : undefined);
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
- secret.name,
1760
- secret.hosts.join(","),
1761
- secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
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 list = await (await client(env)).secrets.list();
1769
- print(rows(list), list);
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
- if (!hosts.length)
1776
- throw usage("Give --host once for each host the value may go to, such as --host api.openai.com.");
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 (await client(env)).secrets.set(name, {
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 !== "-")
@@ -2405,6 +2536,257 @@ read = readSecret) {
2405
2536
  "registry",
2406
2537
  ]);
2407
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
+ }
2408
2790
  const VOLUME_HELP = `runtime volume <command>
2409
2791
 
2410
2792
  create --size-mib <N> [--name n] A persistent ext4 disk; prints its id
@@ -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", "Pass credentials as envVars or per command, never in the command text.");
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
- ["secrets", "Daytona secrets", "Pass credentials as envVars, never in the command text."],
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: Runtime does not export sandbox telemetry.",
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)
@@ -386,7 +386,7 @@ export class Sandbox {
386
386
  return this.resize();
387
387
  }
388
388
  getMetrics() {
389
- return refusal("Sandbox metrics", "Runtime reports CPU, memory and charges per sandbox in `npx withruntime usage` and runtime.usage().");
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", "Pass credentials as envVars or per command, never in the command text.");
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 shell <id>` or sandbox.withruntime.terminal().");
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();
@@ -24,4 +24,4 @@ export const waitForProcess = unsupportedExport("waitForProcess (a template read
24
24
  export const waitForFile = unsupportedExport("waitForFile (a template ready check)", TEMPLATES);
25
25
  export const waitForTimeout = unsupportedExport("waitForTimeout (a template ready check)", TEMPLATES);
26
26
  export const Volume = unsupportedExport("Volume", "Use runtime.volumes (Runtime volumes) and mount one with runtime: { create: { volumes: [{ volumeId, path }] } }.");
27
- export const Secret = unsupportedExport("Secret", "Pass secrets as envs at create or per command, never inside the command text.");
27
+ export const Secret = unsupportedExport("Secret", "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.");
package/dist/index.d.ts CHANGED
@@ -10,16 +10,18 @@ import { Sandbox as SandboxClass } from "./sandbox.js";
10
10
  import type { CreateSandbox, KeepAliveOptions } from "./types.js";
11
11
  import type { RequestOptions } from "./transport.js";
12
12
  export { Runtime, Sandboxes, FeedbackApi, SupportApi, type CreateOptions, type RuntimeOptions, type SupportReply, } from "./client.js";
13
- export { Files, Process, Processes, Terminal } from "./sandbox.js";
13
+ export { Files, Process, Processes, SandboxSessions, Terminal, type CreatedSandboxSession, type SandboxSession, } from "./sandbox.js";
14
14
  export { Tunnel, TunnelStream, type PortForward } from "./tunnel.js";
15
15
  export { Page } from "./page.js";
16
16
  export type { KeyLimits } from "./products/limits.js";
17
17
  export type { AuditEvent, AuditPage } from "./products/audit.js";
18
- export type { Secret, SecretRule, SetSecret } from "./products/secrets.js";
18
+ export type { JobSecret, Secret, SecretRule, SecretUse, SetSecret } from "./products/secrets.js";
19
+ export { SecretPartlyStoredError } from "./products/secrets.js";
19
20
  export type { McpCatalogEntry, McpGateway, McpServerRequest } from "./products/mcp.js";
20
21
  export type { FileEvent, FileEventType, WatchHandle, WatchNotice, WatchOptions, } from "./products/watch.js";
21
22
  export type { Mount, MountBucket } from "./products/mounts.js";
22
23
  export type { Volume, VolumeBackup, CreateVolume } from "./products/volumes.js";
24
+ export type { CreateJob, Job, JobCompute, JobDefinition, JobLogs, JobRun, JobRunState, JobSchedule, } from "./products/jobs.js";
23
25
  export type { CheckedDomain, DnsRecord, Domain, EgressAddress, TcpPort, Tunnel as PrivateTunnel, TunnelPeer, TunnelPeerCreated, SetUpstreamProxy, UpstreamProxy, } from "./products/network-products.js";
24
26
  export { verifyWebhook, WebhookVerificationError, type MetricPoint, type MetricRange, type OtelExport, type RuntimeEvent, type SandboxMetrics, type Webhook, type WebhookDelivery, type WebhookEventType, } from "./products/observability.js";
25
27
  export { Snapshots, type Snapshot, type SnapshotOptions } from "./snapshots.js";
package/dist/index.js CHANGED
@@ -2,9 +2,10 @@ import { defaultClient } from "./client.js";
2
2
  import { Sandbox as SandboxClass } from "./sandbox.js";
3
3
  import { identityToken } from "./identity.js";
4
4
  export { Runtime, Sandboxes, FeedbackApi, SupportApi, } from "./client.js";
5
- export { Files, Process, Processes, Terminal } from "./sandbox.js";
5
+ export { Files, Process, Processes, SandboxSessions, Terminal, } from "./sandbox.js";
6
6
  export { Tunnel, TunnelStream } from "./tunnel.js";
7
7
  export { Page } from "./page.js";
8
+ export { SecretPartlyStoredError } from "./products/secrets.js";
8
9
  export { verifyWebhook, WebhookVerificationError, } from "./products/observability.js";
9
10
  export { Snapshots } from "./snapshots.js";
10
11
  export { Transport, VERSION, apiOrigin } from "./transport.js";