@ornncompute/cli 1.0.137 → 1.0.138

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
@@ -210,9 +210,14 @@ node ID. Changing filters requires a new first-page request. Legacy raw owner
210
210
  cursors require the original `--before` value and filters.
211
211
  Metrics and logs use node-scoped staff gateway routes. Exported private keys
212
212
  include the final newline required by OpenSSH, both in files and with `--print`.
213
- `ornn admin nodes enroll` takes optional `--filter COMPARTMENT_IDS`
214
- (omit to run every compartment step); the retired `--mode` flag is gone. The
215
- gateway, not CLI metadata, authoritatively enforces the admin role.
213
+ `ornn admin enrollment-tokens create` exports the configurable run
214
+ `install_command` as a secret. Its file receipt separates `enrollment_run_id` from
215
+ `token_metadata.id`, the credential ID used for revocation. Read its exact
216
+ `created_at` with `enrollment-tokens list` before machine revocation; the run
217
+ timestamp is not the credential generation. `ornn admin nodes enroll` creates and SSH-launches
218
+ a configurable agent recovery run for the existing node. The former `--filter`
219
+ and `--verbose` compartment options are removed; inspect the saved run for step
220
+ progress. The gateway authoritatively enforces the admin role.
216
221
  `ornn admin nodes sanitize`, `sanitize-status`, and `sanitize-decide` drive one
217
222
  node's held sanitize job, sharing the capability with MCP
218
223
  `admin_node_sanitize_*` and Slack `node_sanitize_*`. `sanitize` needs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.137",
3
+ "version": "1.0.138",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,18 +1,11 @@
1
- import {
2
- enrollmentTokensPath,
3
- remoteRunPath,
4
- listEnrollmentLogs,
5
- } from "../vendor/api-client/orchestrator.js";
6
1
  import { createHash } from "node:crypto";
7
2
  import { lstat, readFile } from "node:fs/promises";
8
3
 
9
- import { CliApiError, resolveInstallerBaseUrl } from "./api-client.mjs";
10
- import { AtomicOutputExistsError, writeAtomicOutput } from "./atomic-output.mjs";
11
4
  import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
5
+ import { CliApiError } from "./api-client.mjs";
6
+ import { AtomicOutputExistsError, writeAtomicOutput } from "./atomic-output.mjs";
12
7
 
13
8
  const SECRET_OUTPUT = new Set(["tokens.create", "nodes.rotate-key"]);
14
- const ENROLLMENT_LOG_POLL_MS = 750;
15
- const ENROLLMENT_LOG_READ_TIMEOUT_MS = 5_000;
16
9
 
17
10
  const USAGE = {
18
11
  "tokens.create":
@@ -20,7 +13,7 @@ const USAGE = {
20
13
  "enrollment.token.revoke":
21
14
  "Usage: ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--idempotency-key KEY]",
22
15
  "nodes.enroll":
23
- "Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose]",
16
+ "Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID --identity-file PATH --token-ttl SECONDS --replace-existing-management",
24
17
  "nodes.export-key":
25
18
  "Usage: ornn admin nodes export-key <node-id> (--print | --output PATH) [--overwrite]",
26
19
  "nodes.rotate-key":
@@ -167,22 +160,12 @@ function parseNodeEnroll(argv) {
167
160
  const { options, positionals } = parseOptions(
168
161
  argv,
169
162
  {
170
- booleans: ["replace-existing-management", "verbose"],
163
+ booleans: ["replace-existing-management"],
171
164
  positionals: 1,
172
- values: [
173
- "facility",
174
- "filter",
175
- "host",
176
- "identity-file",
177
- "port",
178
- "supplier",
179
- "token-ttl",
180
- "username",
181
- ],
165
+ values: ["facility", "host", "identity-file", "port", "supplier", "token-ttl", "username"],
182
166
  },
183
167
  usage
184
168
  );
185
- const filter = options.filter?.trim();
186
169
  if (!options["replace-existing-management"]) {
187
170
  throw new AdminMutationError(`${usage}\n--replace-existing-management is required.`, {
188
171
  field: "replace-existing-management",
@@ -194,7 +177,6 @@ function parseNodeEnroll(argv) {
194
177
  host: required(options, "host", usage),
195
178
  organization_id: uuid(required(options, "supplier", usage), "supplier"),
196
179
  facility_id: uuid(required(options, "facility", usage), "facility"),
197
- ...(filter ? { filter } : {}),
198
180
  identity_file: required(options, "identity-file", usage),
199
181
  expires_in_seconds: boundedInteger(
200
182
  required(options, "token-ttl", usage),
@@ -205,7 +187,6 @@ function parseNodeEnroll(argv) {
205
187
  username: options.username?.trim() || "root",
206
188
  port: options.port ? boundedInteger(options.port, "port", 1, 65_535) : 22,
207
189
  replace_existing_management: true,
208
- verbose: options.verbose === true,
209
190
  },
210
191
  mutation: {
211
192
  safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
@@ -246,17 +227,18 @@ function text(value, fallback = "") {
246
227
 
247
228
  function safeTokenMetadata(payload) {
248
229
  return {
249
- id: text(payload?.id) || null,
230
+ id: text(payload?.credential_id) || null,
250
231
  organization_id: text(payload?.organization_id) || null,
251
- status: text(payload?.status, "active"),
252
- expires_at: text(payload?.expires_at) || null,
253
- created_at: text(payload?.created_at) || null,
232
+ status: "active",
233
+ expires_at: text(payload?.credential_expires_at) || null,
234
+ // The run's creation time is not the credential's generation identity.
235
+ created_at: null,
254
236
  };
255
237
  }
256
238
 
257
239
  function secretMaterial(capabilityId, payload) {
258
240
  if (capabilityId === "tokens.create") {
259
- return text(payload?.token) || text(payload?.plaintext);
241
+ return text(payload?.install_command);
260
242
  }
261
243
  const key = payload?.private_key;
262
244
  if (typeof key !== "string" || !key.length) return "";
@@ -302,7 +284,7 @@ async function exportSecret({ capabilityId, input, options, payload, stdout }) {
302
284
  }
303
285
  const metadata =
304
286
  capabilityId === "tokens.create"
305
- ? { token_metadata: safeTokenMetadata(payload) }
287
+ ? { enrollment_run_id: text(payload?.id) || null, token_metadata: safeTokenMetadata(payload) }
306
288
  : { node_key: keyMetadata(input, payload) };
307
289
  if (options.print) {
308
290
  stdout.write(material);
@@ -341,7 +323,7 @@ async function exportSecret({ capabilityId, input, options, payload, stdout }) {
341
323
  function reviewFor(capabilityId, input, prepared) {
342
324
  if (capabilityId === "tokens.create") {
343
325
  return {
344
- title: "Create enrollment token",
326
+ title: "Create enrollment command",
345
327
  fields: [
346
328
  { label: "Supplier", value: input.organization_id },
347
329
  { label: "Expires in", value: `${input.expires_in_seconds} seconds` },
@@ -374,7 +356,6 @@ function reviewFor(capabilityId, input, prepared) {
374
356
  { label: "SSH target", value: `${input.username}@${input.host}:${input.port}` },
375
357
  { label: "Supplier", value: input.organization_id },
376
358
  { label: "Facility", value: input.facility_id },
377
- ...(input.filter ? [{ label: "Compartment filter", value: input.filter }] : []),
378
359
  { label: "Token lifetime", value: `${input.expires_in_seconds} seconds` },
379
360
  ],
380
361
  consequence:
@@ -403,7 +384,6 @@ async function enrollmentIdentityInput(input) {
403
384
  }
404
385
  return {
405
386
  ...input,
406
- installer_base: resolveInstallerBaseUrl({ env: input.env }) ?? undefined,
407
387
  private_key: privateKey,
408
388
  };
409
389
  } catch (error) {
@@ -415,129 +395,6 @@ async function enrollmentIdentityInput(input) {
415
395
  }
416
396
  }
417
397
 
418
- function enrollmentRuns(payload) {
419
- if (!payload || typeof payload !== "object") return [];
420
- if (Array.isArray(payload.logs)) return payload.logs;
421
- return Array.isArray(payload.runs) ? payload.runs : [];
422
- }
423
-
424
- function redactEnrollmentText(value, secrets) {
425
- let text = typeof value === "string" ? value : String(value ?? "");
426
- for (const secret of secrets) {
427
- if (!secret) continue;
428
- text = text.replaceAll(secret, "[redacted]");
429
- text = text.replaceAll(encodeURIComponent(secret), "[redacted]");
430
- }
431
- return text;
432
- }
433
-
434
- function freshEnrollmentLogLines(payload, tokenId, secrets, seen) {
435
- const session = enrollmentRuns(payload).find((candidate) => candidate?.token_id === tokenId);
436
- if (!session || typeof session !== "object") return [];
437
- const compartments = Array.isArray(session.compartments) ? session.compartments : [];
438
- const total = Number.isInteger(session.progress_total)
439
- ? session.progress_total
440
- : compartments.length;
441
- const lines = [];
442
- for (const [index, compartment] of compartments.entries()) {
443
- if (!compartment || typeof compartment !== "object") continue;
444
- const id = String(compartment.id ?? index);
445
- const numericId = Number(id);
446
- const current = Number.isInteger(numericId) ? numericId + 1 : session.progress_index;
447
- const title =
448
- typeof compartment.title === "string" && compartment.title ? compartment.title : `Step ${id}`;
449
- const state = typeof compartment.state === "string" ? compartment.state : "";
450
- const stateKey = `${id}:state:${state}`;
451
- if (state && !seen.has(stateKey)) {
452
- seen.add(stateKey);
453
- lines.push(`[enroll ${current}/${total}] ${title}: ${state}`);
454
- }
455
- const output = Array.isArray(compartment.output) ? compartment.output : [];
456
- for (const [outputIndex, entry] of output.entries()) {
457
- const stream =
458
- entry && typeof entry === "object" && typeof entry.stream === "string"
459
- ? entry.stream
460
- : "stdout";
461
- const seq =
462
- entry && typeof entry === "object" && Number.isInteger(entry.seq) ? entry.seq : outputIndex;
463
- const key = `${id}:output:${seq}:${stream}`;
464
- if (seen.has(key)) continue;
465
- seen.add(key);
466
- const raw = entry && typeof entry === "object" ? entry.text : entry;
467
- const text = redactEnrollmentText(raw, secrets);
468
- if (text) lines.push(` ${stream === "stderr" ? "[stderr] " : ""}${text}`);
469
- }
470
- }
471
- return lines;
472
- }
473
-
474
- function enrollmentProgressTransport(fetch, runtime, privateKey) {
475
- let tokenId = "";
476
- const secrets = [privateKey];
477
- const seen = new Set();
478
- const request = (path, init = {}) =>
479
- fetch(
480
- path,
481
- init.signal === undefined && runtime.signal ? { ...init, signal: runtime.signal } : init
482
- );
483
-
484
- return async (path, init = {}) => {
485
- if (path === enrollmentTokensPath) {
486
- const minted = await request(path, init);
487
- if (minted && typeof minted === "object") {
488
- tokenId = typeof minted.id === "string" ? minted.id : "";
489
- const token =
490
- typeof minted.token === "string"
491
- ? minted.token
492
- : typeof minted.plaintext === "string"
493
- ? minted.plaintext
494
- : "";
495
- if (token) secrets.push(token);
496
- }
497
- return minted;
498
- }
499
- if (path !== remoteRunPath || !tokenId) return request(path, init);
500
-
501
- const remote = request(path, init).then(
502
- (value) => ({ ok: true, value }),
503
- (error) => ({ error, ok: false })
504
- );
505
- let stopped = false;
506
- let timer;
507
- let currentPoll = Promise.resolve();
508
- const poll = async () => {
509
- try {
510
- const payload = await listEnrollmentLogs(
511
- fetch,
512
- {},
513
- AbortSignal.timeout(ENROLLMENT_LOG_READ_TIMEOUT_MS)
514
- );
515
- const lines = freshEnrollmentLogLines(payload, tokenId, secrets, seen);
516
- for (const line of lines) runtime.stderr.write(`${line}\n`);
517
- } catch {
518
- // Progress is best-effort; the authoritative remote request still determines success.
519
- }
520
- };
521
- const schedule = () => {
522
- timer = setTimeout(() => {
523
- currentPoll = poll().finally(() => {
524
- if (!stopped) schedule();
525
- });
526
- }, ENROLLMENT_LOG_POLL_MS);
527
- };
528
-
529
- await poll();
530
- schedule();
531
- const result = await remote;
532
- stopped = true;
533
- clearTimeout(timer);
534
- await currentPoll;
535
- await poll();
536
- if (!result.ok) throw result.error;
537
- return result.value;
538
- };
539
- }
540
-
541
398
  function assertSecretOutputContext(input, runtime) {
542
399
  if (input.export?.print && runtime.formatProvided) {
543
400
  throw new AdminMutationError("--print cannot be combined with --format.", {
@@ -626,11 +483,7 @@ async function runEnrollmentMutation({ capability, execute, fetch, input, mutati
626
483
  capabilityId === "nodes.enroll"
627
484
  ? await enrollmentIdentityInput({ ...value, env: runtime.env })
628
485
  : value;
629
- const transport =
630
- capabilityId === "nodes.enroll" && executionInput.verbose
631
- ? enrollmentProgressTransport(fetch, runtime, executionInput.private_key)
632
- : undefined;
633
- return execute(executionInput, { confirmed: true, transport });
486
+ return execute(executionInput, { confirmed: true });
634
487
  },
635
488
  });
636
489