@ornncompute/cli 1.0.97 → 1.0.107

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
@@ -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.107",
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.
@@ -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 } : {}