@ornncompute/cli 1.0.97 → 1.0.109

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/README.md CHANGED
@@ -110,7 +110,7 @@ reservations, schedule and spot alike; `reservations buy` books one window of a
110
110
  relisted (secondary) listing term with Markets `POST /v1/markets/bookings`
111
111
  (`{legs: [...]}`), at the term price `terms` shows; it becomes a reservation
112
112
  when the window starts. `market` and `offers` call Markets (`/v1/markets`):
113
- buy `--nodes` at market (Markets also takes GPU counts and limit bids; the
113
+ buy `--nodes` at market (Markets also takes limit bids; the
114
114
  CLI does not send them yet), end a fill with
115
115
  `market end` (Commerce `DELETE /commerce/reservations/{id}`), post an offer
116
116
  on a whole primary listing for one of its terms (`--listing-term-id`, optional
@@ -457,3 +457,9 @@ request conflicts, and an expired incomplete request requires a new key.
457
457
  `ornn volumes refresh <volume>` reconciles recorded usage against stored files.
458
458
  The upload replay contract requires the matching gateway/orchestrator release;
459
459
  see [the service contract](../../services/orchestrator/README.md#file-upload-replay-and-usage).
460
+
461
+ Staff can use `ornn admin ssh <node-id>` for certificate access on explicitly
462
+ opted-in nodes. It requires an interactive staff login, creates a temporary
463
+ local key, obtains a 15-minute certificate through the gateway and launches
464
+ OpenSSH with normal host-key verification. Temporary files are removed when
465
+ the command exits. Shared and per-node key access remain available.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.97",
3
+ "version": "1.0.109",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ import { adminSshHandlers } from "./admin-ssh.mjs";
1
2
  import { adminCapacityReservationHandlers } from "./admin-capacity-reservations.mjs";
2
3
  import { adminCreditHandlers } from "./admin-credits.mjs";
3
4
  import { adminEnrollmentHandlers } from "./admin-enrollment.mjs";
@@ -7,6 +8,7 @@ import { adminNodeSanitizeHandlers } from "./admin-node-sanitize.mjs";
7
8
  import { ADMIN_MUTATION_SAFETY, assertAdminMutationContext } from "./admin-mutations.mjs";
8
9
 
9
10
  const handlers = [
11
+ ...adminSshHandlers,
10
12
  ...adminCapacityReservationHandlers,
11
13
  ...adminCreditHandlers,
12
14
  ...adminEnrollmentHandlers,
@@ -0,0 +1,113 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { constants, tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { ADMIN_MUTATION_SAFETY } from "./admin-mutations.mjs";
6
+ import { forwardSignals } from "./signals.mjs";
7
+
8
+ function run(program, args, signal) {
9
+ signal.throwIfAborted();
10
+ return new Promise((resolve, reject) => {
11
+ const child = spawn(program, args, { stdio: "inherit", signal });
12
+ child.once("error", (error) => {
13
+ // A cancelled key generator must finish exiting before we remove its files.
14
+ if (error.name !== "AbortError") reject(error);
15
+ });
16
+ child.once("close", (code, killed) =>
17
+ killed ? reject(new Error("SSH was interrupted.")) : resolve(code ?? 1)
18
+ );
19
+ });
20
+ }
21
+
22
+ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProcess = run } = {}) {
23
+ // Read the existing node route; issuance returns only its own certificate.
24
+ const node = await fetch(`/nodes/${encodeURIComponent(input.node_id)}`, { method: "GET" });
25
+ const host = node.ip;
26
+ const port = node.port ?? 22;
27
+ if (
28
+ typeof host !== "string" ||
29
+ !/^[a-zA-Z0-9.:_-]+$/.test(host) ||
30
+ host.startsWith("-") ||
31
+ !Number.isInteger(port) ||
32
+ port < 1 ||
33
+ port > 65535
34
+ ) {
35
+ throw new Error("The node does not have a valid SSH address.");
36
+ }
37
+ const controller = new AbortController();
38
+ const signal = runtime.signal
39
+ ? AbortSignal.any([runtime.signal, controller.signal])
40
+ : controller.signal;
41
+ let interrupted;
42
+ let directory;
43
+ const removeSignals = forwardSignals(process, null, (received) => {
44
+ interrupted ??= received;
45
+ controller.abort();
46
+ });
47
+ try {
48
+ directory = await mkdtemp(join(tmpdir(), "ornn-ssh-"));
49
+ const key = join(directory, "identity");
50
+ const generated = await runProcess(
51
+ "ssh-keygen",
52
+ ["-q", "-t", "ed25519", "-N", "", "-C", "ornn-staff", "-f", key],
53
+ signal
54
+ );
55
+ if (generated !== 0) throw new Error("Could not generate the temporary SSH key.");
56
+ const public_key = (await readFile(`${key}.pub`, "utf8")).trim();
57
+ const issued = await execute(
58
+ { node_id: input.node_id, public_key },
59
+ { transport: (path, init) => fetch(path, { ...init, signal }) }
60
+ );
61
+ signal.throwIfAborted();
62
+ if (
63
+ typeof issued.certificate !== "string" ||
64
+ !issued.certificate.includes("-cert-v01@openssh.com ")
65
+ )
66
+ throw new Error("The issuer returned an invalid SSH certificate.");
67
+ await writeFile(`${key}-cert.pub`, `${issued.certificate}\n`, { mode: 0o600 });
68
+ const exitCode = await runProcess(
69
+ "ssh",
70
+ [
71
+ "-o",
72
+ "IdentitiesOnly=yes",
73
+ "-o",
74
+ `CertificateFile=${key}-cert.pub`,
75
+ "-i",
76
+ key,
77
+ "-p",
78
+ String(port),
79
+ "-l",
80
+ "admin",
81
+ host,
82
+ ],
83
+ signal
84
+ );
85
+ signal.throwIfAborted();
86
+ return { suppressResult: true, exitCode };
87
+ } catch (error) {
88
+ if (!interrupted) throw error;
89
+ return { suppressResult: true, exitCode: 128 + constants.signals[interrupted] };
90
+ } finally {
91
+ try {
92
+ if (directory) await rm(directory, { recursive: true, force: true });
93
+ } finally {
94
+ removeSignals();
95
+ }
96
+ }
97
+ }
98
+
99
+ export const adminSshHandlers = [
100
+ {
101
+ capabilityId: "node.ssh",
102
+ safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
103
+ parse(argv) {
104
+ if (
105
+ argv.length !== 1 ||
106
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(argv[0])
107
+ )
108
+ throw new Error("Usage: ornn admin ssh <node-id>");
109
+ return { input: { node_id: argv[0] } };
110
+ },
111
+ run: openAdminSsh,
112
+ },
113
+ ];
@@ -251,7 +251,10 @@ export function resolveInstallerBaseUrl({ env = process.env } = {}) {
251
251
  }
252
252
 
253
253
  function operatorProxyEndpoint(endpoint) {
254
- return `/v1/staff${normalizeComputePath(endpoint)}`;
254
+ // Benchmark functions use their owner namespace; retain the existing staff
255
+ // envelope, including its admin-role and impersonation checks.
256
+ const path = normalizeComputePath(endpoint).replace(/^\/v1\/benchmark(?=\/|$)/, "");
257
+ return `/v1/staff${path}`;
255
258
  }
256
259
 
257
260
  // Staff path: CLI bearer → gateway /v1/orchestrator/ → the owning service.
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+
1
3
  import {
2
4
  CliApiError,
3
5
  cliRequest,
@@ -78,6 +80,9 @@ const MACHINE_SCHEMA = "ornn.cli/v1";
78
80
  const UPDATE_CHANNEL = "stable";
79
81
  const FORMATS = { human: true, json: true, ndjson: true };
80
82
  const SESSION_CAPABILITY_IDS = new Set(["identity.context", "identity.login", "identity.logout"]);
83
+ // Cloud Shell's welcome prints this file from the installed package.
84
+ const LOGO_URL = new URL("./ornn-logo.txt", import.meta.url);
85
+ const LOGO_COLUMNS = 72;
81
86
  export const CLI_EXIT_STATUS = Object.freeze({
82
87
  success: 0,
83
88
  failure: 1,
@@ -584,6 +589,9 @@ function writeRootHuman(stdout, catalog) {
584
589
  }
585
590
  stdout.write("\n");
586
591
  };
592
+ if (stdout.isTTY && stdout.columns >= LOGO_COLUMNS) {
593
+ stdout.write(`\n${readFileSync(LOGO_URL, "utf8")}\n`);
594
+ }
587
595
  stdout.write("Ornn CLI\n\n");
588
596
  stdout.write("Usage:\n ornn <command> [options]\n\n");
589
597
  stdout.write("Available commands:\n\n");
@@ -0,0 +1,8 @@
1
+ ▄██████▀▀▀▀▀▀▀▀ ▄▄█▀▀▀█▄▄
2
+ ▀▀▀▀▀▀ ▄▄▄▄▄▄▄ ▄█▀ ▀█▄
3
+ ████████ ▀▀▀▀▀▀ ██▀ ▀██ ▄▄▄ ▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄▄
4
+ ▄▄▄▄▄▄▄ ██ ██▄ ████▀▀▀ ███▄▀▀▀██▄ ██▄▀▀▀▀██
5
+ ▀▀▀▀▀▀▀▀ ██ ██▀ ███ ███ ███ ██▀ ██
6
+ ████████ ██▄ ▄██ ██▄ ███ ███ ██ ██
7
+ ▀▀▀▀▀▀▀ ▀█▄ ▄██ ███ ███ ███ ██ ██
8
+ ▄▄▄▄▄▄▄███████▀ ▀██▄▄▄██▀▀ ███ ███ ███ ██▄ ███
@@ -216,6 +216,8 @@ function getReservationUsage(request, input, signal) {
216
216
  const query = new URLSearchParams({ from: input.from, to: input.to, bucket: input.bucket });
217
217
  if (input.organizationId)
218
218
  query.set("organizationId", input.organizationId);
219
+ if (input.groupBy)
220
+ query.set("groupBy", input.groupBy);
219
221
  return request(`${BASE2}/usage?${query}`, {
220
222
  method: "GET",
221
223
  ...signal ? { signal } : {}