@ornncompute/cli 1.0.72 → 1.0.97

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.
Files changed (59) hide show
  1. package/README.md +55 -24
  2. package/package.json +4 -2
  3. package/src/admin-capacity-reservations.mjs +6 -13
  4. package/src/admin-command-handlers.mjs +2 -1
  5. package/src/admin-credits.mjs +1 -2
  6. package/src/admin-enrollment.mjs +22 -30
  7. package/src/admin-mutations.mjs +6 -56
  8. package/src/admin-node-diagnostics.mjs +3 -41
  9. package/src/admin-node-lifecycle.mjs +40 -160
  10. package/src/admin-node-sanitize.mjs +243 -0
  11. package/src/admin-shell.mjs +39 -32
  12. package/src/catalog-shell.mjs +6 -6
  13. package/src/evidence-commands.mjs +4 -6
  14. package/src/external-bucket-commands.mjs +76 -96
  15. package/src/interaction-runtime.mjs +5 -37
  16. package/src/kubernetes-commands.mjs +25 -183
  17. package/src/launch-mode-handlers.mjs +0 -1
  18. package/src/machine-reference.mjs +33 -56
  19. package/src/mount-commands.mjs +25 -131
  20. package/src/slurm-commands.mjs +13 -2
  21. package/src/storage-commands.mjs +2 -87
  22. package/src/vm-commands.mjs +48 -101
  23. package/vendor/api-client/commerce.js +743 -0
  24. package/vendor/capabilities/cli.js +7688 -7
  25. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +0 -101
  26. package/vendor/capabilities/capabilities/admin-enrollment.js +0 -321
  27. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +0 -88
  28. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +0 -235
  29. package/vendor/capabilities/capabilities/admin.js +0 -91
  30. package/vendor/capabilities/capabilities/cli.js +0 -156
  31. package/vendor/capabilities/capabilities/clusters.js +0 -248
  32. package/vendor/capabilities/capabilities/commerce-market.js +0 -405
  33. package/vendor/capabilities/capabilities/compute.js +0 -523
  34. package/vendor/capabilities/capabilities/credits.js +0 -78
  35. package/vendor/capabilities/capabilities/evidence.js +0 -145
  36. package/vendor/capabilities/capabilities/external-buckets.js +0 -191
  37. package/vendor/capabilities/capabilities/fleet.js +0 -306
  38. package/vendor/capabilities/capabilities/identity.js +0 -129
  39. package/vendor/capabilities/capabilities/listings.js +0 -398
  40. package/vendor/capabilities/capabilities/locality.js +0 -42
  41. package/vendor/capabilities/capabilities/market-shared.js +0 -46
  42. package/vendor/capabilities/capabilities/markets.js +0 -482
  43. package/vendor/capabilities/capabilities/mounts.js +0 -176
  44. package/vendor/capabilities/capabilities/native-listing.js +0 -99
  45. package/vendor/capabilities/capabilities/networks.js +0 -1264
  46. package/vendor/capabilities/capabilities/observability.js +0 -218
  47. package/vendor/capabilities/capabilities/organization-invites.js +0 -53
  48. package/vendor/capabilities/capabilities/reservation-list.js +0 -68
  49. package/vendor/capabilities/capabilities/staff-shared.js +0 -64
  50. package/vendor/capabilities/capabilities/tokens.js +0 -165
  51. package/vendor/capabilities/capabilities/users.js +0 -159
  52. package/vendor/capabilities/capabilities/volumes.js +0 -178
  53. package/vendor/capabilities/capability.js +0 -99
  54. package/vendor/capabilities/cli-catalog.js +0 -73
  55. package/vendor/capabilities/cli-migrations.js +0 -264
  56. package/vendor/capabilities/names.js +0 -6
  57. package/vendor/capabilities/software-catalog.js +0 -82
  58. package/vendor/capabilities/spec-catalog.js +0 -151
  59. package/vendor/capabilities/supplier-directory.js +0 -20
@@ -0,0 +1,243 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
5
+ import { parseSemanticArguments } from "./interaction-runtime.mjs";
6
+
7
+ const MANUAL_CLEANUP = "ACCEPT MANUAL CLEANUP";
8
+
9
+ function usageError(capability, detail, field) {
10
+ return new AdminMutationError(
11
+ [`Usage: ${capability.cli.vnext.human}`, detail].filter(Boolean).join("\n"),
12
+ { code: "usage_error", ...(field ? { field } : {}) }
13
+ );
14
+ }
15
+
16
+ function semanticArguments(argv, capability) {
17
+ const { $positionals, ...options } = parseSemanticArguments(argv);
18
+ if ($positionals.length !== 1 || !$positionals[0]) {
19
+ throw usageError(capability, "Pass exactly one canonical node ID.", "node-id");
20
+ }
21
+ return { nodeId: $positionals[0], options };
22
+ }
23
+
24
+ function rejectExtra(capability, extra) {
25
+ const [name] = Object.keys(extra);
26
+ if (name) throw usageError(capability, `Unknown option: --${name}`, name);
27
+ }
28
+
29
+ function validated(capability, input) {
30
+ const parsed = capability.input.safeParse(input);
31
+ if (!parsed.success) throw usageError(capability, parsed.error.message);
32
+ return parsed.data;
33
+ }
34
+
35
+ function evidenceSha256(capability, path) {
36
+ try {
37
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
38
+ } catch (error) {
39
+ throw usageError(
40
+ capability,
41
+ `Could not read --evidence-file: ${error instanceof Error ? error.message : String(error)}`,
42
+ "evidence-file"
43
+ );
44
+ }
45
+ }
46
+
47
+ function parseStart(argv, capability) {
48
+ const {
49
+ nodeId,
50
+ options: { operation, confirmation, jobId, batch, recoveryUuid, ...extra },
51
+ } = semanticArguments(argv, capability);
52
+ rejectExtra(capability, extra);
53
+ if (typeof operation !== "string") throw usageError(capability, "--operation is required.");
54
+ const input = validated(capability, {
55
+ node_id: nodeId,
56
+ job_id: jobId ?? randomUUID(),
57
+ operation,
58
+ ...(batch ? { maintenance_batch_id: batch } : {}),
59
+ ...(recoveryUuid ? { recovery_uuid: recoveryUuid } : {}),
60
+ confirmation,
61
+ });
62
+ return {
63
+ input,
64
+ mutation: { idempotencyKey: input.job_id, safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly },
65
+ };
66
+ }
67
+
68
+ function parseStatus(argv, capability) {
69
+ const {
70
+ nodeId,
71
+ options: { job, ...extra },
72
+ } = semanticArguments(argv, capability);
73
+ rejectExtra(capability, extra);
74
+ return { input: validated(capability, { node_id: nodeId, job_id: job }) };
75
+ }
76
+
77
+ function parseDecision(argv, capability) {
78
+ const {
79
+ nodeId,
80
+ options: {
81
+ job,
82
+ confirmation,
83
+ reservation,
84
+ releaseOperation,
85
+ reason,
86
+ evidenceFile,
87
+ tenantAccessRevoked,
88
+ dataDrivesSanitized,
89
+ acknowledgeRetainedOs,
90
+ ...extra
91
+ },
92
+ } = semanticArguments(argv, capability);
93
+ rejectExtra(capability, extra);
94
+ const cleanupOptions = {
95
+ reservation,
96
+ releaseOperation,
97
+ reason,
98
+ evidenceFile,
99
+ tenantAccessRevoked,
100
+ dataDrivesSanitized,
101
+ acknowledgeRetainedOs,
102
+ };
103
+ if (confirmation !== MANUAL_CLEANUP) {
104
+ const [supplied] =
105
+ Object.entries(cleanupOptions).find(([, value]) => value !== undefined) ?? [];
106
+ if (supplied) {
107
+ throw usageError(
108
+ capability,
109
+ `Manual cleanup options require --confirmation "${MANUAL_CLEANUP}".`
110
+ );
111
+ }
112
+ return {
113
+ input: validated(capability, { node_id: nodeId, job_id: job, confirmation }),
114
+ mutation: { safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly },
115
+ };
116
+ }
117
+ // Each attestation is the operator's own flag, never a default.
118
+ for (const [name, value] of Object.entries(cleanupOptions)) {
119
+ if (value === undefined || value === "") {
120
+ const flag = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
121
+ throw usageError(capability, `--${flag} is required for manual cleanup.`, flag);
122
+ }
123
+ }
124
+ const input = validated(capability, {
125
+ node_id: nodeId,
126
+ job_id: job,
127
+ confirmation,
128
+ manual_cleanup: {
129
+ reservation_id: reservation,
130
+ lifecycle_operation_id: releaseOperation,
131
+ reason,
132
+ evidence_sha256: evidenceSha256(capability, evidenceFile),
133
+ acknowledge_retained_os: true,
134
+ tenant_access_revoked: true,
135
+ data_drives_sanitized: true,
136
+ },
137
+ });
138
+ return { input, mutation: { safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly } };
139
+ }
140
+
141
+ const startConsequence = {
142
+ data_wipe: "Erases every local data drive; the OS drive is preserved.",
143
+ ram_roundtrip: "Boots the node through RAM and back without erasing drives.",
144
+ node_turnover: "Erases the whole node, including the OS drive, and restores a host image.",
145
+ host_restore: "Restores the host OS from its trusted image.",
146
+ };
147
+
148
+ function decisionReview(input) {
149
+ const fields = [
150
+ { label: "Node", value: input.node_id },
151
+ { label: "Sanitize job", value: input.job_id },
152
+ { label: "Decision", value: input.confirmation },
153
+ ];
154
+ if (input.confirmation === "APPROVE SANITIZATION") {
155
+ return { fields, consequence: "Starts the held destructive maintenance on this node." };
156
+ }
157
+ if (input.confirmation === "SKIP SANITIZATION") {
158
+ return {
159
+ fields,
160
+ consequence: "Skips an eligible first-use wipe; nothing is erased or certified.",
161
+ };
162
+ }
163
+ const cleanup = input.manual_cleanup;
164
+ return {
165
+ fields: [
166
+ ...fields,
167
+ { label: "Reservation", value: cleanup.reservation_id },
168
+ { label: "Release operation", value: cleanup.lifecycle_operation_id },
169
+ { label: "Reason", value: cleanup.reason },
170
+ { label: "Evidence SHA-256", value: cleanup.evidence_sha256 },
171
+ {
172
+ label: "You attest",
173
+ value:
174
+ "tenant access revoked; every local data drive sanitized; existing OS retained and not certified",
175
+ },
176
+ ],
177
+ consequence:
178
+ "Lets the held release finish without a full wipe. The boot disk and prior root exposure remain.",
179
+ };
180
+ }
181
+
182
+ async function runStart({ execute, input, mutation, runtime }) {
183
+ return runAdminMutation({
184
+ ...runtime,
185
+ ...mutation,
186
+ prepare: async () => ({
187
+ review: {
188
+ title: "Start node sanitization",
189
+ fields: [
190
+ { label: "Node", value: input.node_id },
191
+ { label: "Operation", value: input.operation ?? "data_wipe" },
192
+ ...(input.recovery_uuid ? [{ label: "Recovery UUID", value: input.recovery_uuid }] : []),
193
+ { label: "Sanitize job", value: input.job_id },
194
+ { label: "Confirmation", value: input.confirmation },
195
+ ],
196
+ consequence: `${startConsequence[input.operation ?? "data_wipe"]} The job waits for staff approval.`,
197
+ },
198
+ value: input,
199
+ }),
200
+ execute: (value) => execute(value, { confirmed: true }),
201
+ });
202
+ }
203
+
204
+ async function runStatus({ execute, input }) {
205
+ return {
206
+ data: await execute(input),
207
+ exitCode: 0,
208
+ outcome: { class: "ready", phase: "complete" },
209
+ };
210
+ }
211
+
212
+ async function runDecision({ execute, input, mutation, runtime }) {
213
+ return runAdminMutation({
214
+ ...runtime,
215
+ ...mutation,
216
+ prepare: async () => ({
217
+ review: { title: "Decide node sanitize job", ...decisionReview(input) },
218
+ value: input,
219
+ }),
220
+ execute: (value) => execute(value, { confirmed: true }),
221
+ });
222
+ }
223
+
224
+ export const adminNodeSanitizeHandlers = [
225
+ {
226
+ capabilityId: "node.sanitize.start",
227
+ parse: parseStart,
228
+ run: runStart,
229
+ safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
230
+ },
231
+ {
232
+ capabilityId: "node.sanitize.show",
233
+ parse: parseStatus,
234
+ run: runStatus,
235
+ safety: ADMIN_MUTATION_SAFETY.direct,
236
+ },
237
+ {
238
+ capabilityId: "node.sanitize.update",
239
+ parse: parseDecision,
240
+ run: runDecision,
241
+ safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
242
+ },
243
+ ];
@@ -185,13 +185,21 @@ function parseAdminInput(capability, argv) {
185
185
  throw new AdminUsageError(usage);
186
186
  }
187
187
 
188
- const offsetPagination = ["users.list", "organization.list"].includes(capability.id);
188
+ const offsetPagination = ["users.list", "organization.list", "supplier.list"].includes(
189
+ capability.id
190
+ );
191
+ const directoryPagination = ["users.list", "organization.list"].includes(capability.id);
189
192
  const reservationList = capability.id === "reservation.list";
190
- const limit = positiveInteger(
191
- options.limit,
192
- "limit",
193
- offsetPagination || reservationList ? 100 : MAX_PAGE_LIMIT
194
- ) ?? (offsetPagination ? 25 : DEFAULT_PAGE_LIMIT);
193
+ const maxLimit = directoryPagination
194
+ ? 100
195
+ : capability.id === "supplier.list"
196
+ ? 1000
197
+ : reservationList
198
+ ? 100
199
+ : MAX_PAGE_LIMIT;
200
+ const limit =
201
+ positiveInteger(options.limit, "limit", maxLimit) ??
202
+ (directoryPagination ? 25 : DEFAULT_PAGE_LIMIT);
195
203
  const input = {};
196
204
  if (nodeCommand) input.node_id = positionals[0];
197
205
  if (options.supplier) input.organization_id = options.supplier;
@@ -232,13 +240,8 @@ function parseAdminInput(capability, argv) {
232
240
  if (options.cursor) input.cursor = options.cursor;
233
241
  }
234
242
  if (offsetPagination) {
235
- if (explicitOffset !== undefined && options.cursor) {
236
- throw new AdminUsageError("--offset and --cursor cannot be used together.", {
237
- field: "offset",
238
- });
239
- }
240
243
  input.limit = limit;
241
- input.offset = explicitOffset ?? decodeCursor(command, options.cursor);
244
+ input.offset = explicitOffset ?? 0;
242
245
  }
243
246
 
244
247
  return {
@@ -328,14 +331,9 @@ function collectionKey(capability) {
328
331
  if (root === "nodes" && leaf === "logs") return "logs";
329
332
  if (!["list", "search"].includes(leaf)) return null;
330
333
  if (root === "enrollment-tokens") return "enrollment_tokens";
331
- return [
332
- "facilities",
333
- "nodes",
334
- "organizations",
335
- "reservations",
336
- "suppliers",
337
- "users",
338
- ].includes(root)
334
+ return ["facilities", "nodes", "organizations", "reservations", "suppliers", "users"].includes(
335
+ root
336
+ )
339
337
  ? root
340
338
  : null;
341
339
  }
@@ -445,14 +443,19 @@ export function finalizeAdminResult(capability, payload, pagination) {
445
443
  pagination.ownerOffsetPagination && payload && !Array.isArray(payload)
446
444
  ? payload.next_offset
447
445
  : undefined;
446
+ const nextOffset = pagination.ownerOffsetPagination
447
+ ? Number.isSafeInteger(ownerNextOffset) && ownerNextOffset > offset
448
+ ? ownerNextOffset
449
+ : selected.length === pagination.limit
450
+ ? offset + selected.length
451
+ : null
452
+ : undefined;
448
453
  const ownerNextCursor =
449
454
  payload && !Array.isArray(payload)
450
455
  ? (payload.next_cursor ?? payload.nextCursor ?? payload.page?.next_cursor ?? null)
451
456
  : null;
452
457
  let nextCursor = pagination.ownerOffsetPagination
453
- ? Number.isSafeInteger(ownerNextOffset) && ownerNextOffset > offset
454
- ? encodeCursor(command, ownerNextOffset)
455
- : null
458
+ ? null
456
459
  : !pagination.ownerPagination && offset + selected.length < rows.length
457
460
  ? encodeCursor(command, offset + selected.length)
458
461
  : (ownerNextCursor ?? null);
@@ -480,11 +483,9 @@ export function finalizeAdminResult(capability, payload, pagination) {
480
483
  }
481
484
  return {
482
485
  data: { [key]: selected },
483
- page: {
484
- limit: pagination.limit,
485
- returned: selected.length,
486
- next_cursor: nextCursor,
487
- },
486
+ page: pagination.ownerOffsetPagination
487
+ ? { limit: pagination.limit, returned: selected.length, next_offset: nextOffset }
488
+ : { limit: pagination.limit, returned: selected.length, next_cursor: nextCursor },
488
489
  };
489
490
  }
490
491
 
@@ -515,7 +516,15 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
515
516
  } else {
516
517
  for (const row of rows) stdout.write(`${JSON.stringify(row)}\n`);
517
518
  }
518
- if (result.page.next_cursor) {
519
+ const nextPage =
520
+ result.page.next_offset !== undefined
521
+ ? result.page.next_offset === null
522
+ ? null
523
+ : `--offset ${result.page.next_offset}`
524
+ : result.page.next_cursor
525
+ ? `--cursor ${shellSingleQuote(result.page.next_cursor)}`
526
+ : null;
527
+ if (nextPage) {
519
528
  const target = command === "admin nodes logs" ? ` ${shellSingleQuote(input.node_id)}` : "";
520
529
  const filters = [
521
530
  ["supplier", command === "admin users list" ? undefined : input.organization_id],
@@ -535,8 +544,6 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
535
544
  .filter(([, value]) => value !== undefined)
536
545
  .map(([name, value]) => ` --${name} ${shellSingleQuote(String(value))}`)
537
546
  .join("");
538
- stdout.write(
539
- `Next: ornn ${command}${target} --cursor ${shellSingleQuote(result.page.next_cursor)}${filters}\n`
540
- );
547
+ stdout.write(`Next: ornn ${command}${target} ${nextPage}${filters}\n`);
541
548
  }
542
549
  }
@@ -818,13 +818,13 @@ function parseCapabilityInput(capability, rawInput) {
818
818
  });
819
819
  }
820
820
 
821
- // Commerce's market routes and Markets serve every caller on one path and
822
- // decide from the session what it may do; a staff session is an admin there.
823
- const ROLE_AWARE_PREFIXES = ["/commerce/v2/listings", "/commerce/reservations", "/v1/markets"];
821
+ // Native Commerce and Markets routes authenticate the caller at the gateway.
822
+ // Commerce's /internal/v1 routes additionally require staff.
823
+ const DIRECT_GATEWAY_PREFIXES = ["/commerce", "/internal/v1", "/v1/markets"];
824
824
 
825
- function isRoleAwarePath(path) {
825
+ function isDirectGatewayPath(path) {
826
826
  const pathname = String(path).split("?")[0];
827
- return ROLE_AWARE_PREFIXES.some(
827
+ return DIRECT_GATEWAY_PREFIXES.some(
828
828
  (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
829
829
  );
830
830
  }
@@ -839,7 +839,7 @@ function adminFetch(io, path, init = {}) {
839
839
  ...(init.headers !== undefined ? { headers: init.headers } : {}),
840
840
  ...((init.signal ?? io.signal) !== undefined ? { signal: init.signal ?? io.signal } : {}),
841
841
  };
842
- return isRoleAwarePath(path) || (path === "/v1/staff/invites" && request.method === "POST")
842
+ return isDirectGatewayPath(path) || (path === "/v1/staff/invites" && request.method === "POST")
843
843
  ? cliRequest(request)
844
844
  : operatorRequest(request);
845
845
  }
@@ -264,15 +264,13 @@ function reachability(value) {
264
264
  if (explicit === true) return { state: "reachable", observed_at: null };
265
265
  if (explicit === false) return { state: "unreachable", observed_at: null };
266
266
  const candidate = record(source.reachability);
267
- const offGrid = record(source.off_grid_reachability);
268
- const state = stateToken(candidate.state ?? candidate.status ?? offGrid.state ?? offGrid.status);
269
- const phase = stateToken(candidate.phase ?? offGrid.phase);
270
- const observedAt =
271
- text(candidate.observed_at) ?? text(candidate.checked_at) ?? text(offGrid.last_checked_at);
267
+ const state = stateToken(candidate.state ?? candidate.status);
268
+ const phase = stateToken(candidate.phase);
269
+ const observedAt = text(candidate.observed_at) ?? text(candidate.checked_at);
272
270
  if (["reachable", "live", "online"].includes(state) || phase === "ssh_ready")
273
271
  return { state: "reachable", observed_at: observedAt };
274
272
  if (
275
- ["unreachable", "offline", "off_grid"].includes(state) ||
273
+ ["unreachable", "offline"].includes(state) ||
276
274
  ["ssh_unreachable", "unreachable"].includes(phase)
277
275
  )
278
276
  return { state: "unreachable", observed_at: observedAt };