@ornncompute/cli 1.0.34 → 1.0.48

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.
@@ -156,6 +156,24 @@ function positiveInteger(value, name, maximum) {
156
156
  return parsed;
157
157
  }
158
158
 
159
+ function nonnegativeInteger(value, name) {
160
+ if (value === undefined) return undefined;
161
+ const parsed = Number(value);
162
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
163
+ throw new AdminUsageError(`--${name} must be a non-negative integer.`, {
164
+ field: name,
165
+ });
166
+ }
167
+ return parsed;
168
+ }
169
+
170
+ function booleanOption(value, name) {
171
+ if (value === undefined) return undefined;
172
+ if (value === "true") return true;
173
+ if (value === "false") return false;
174
+ throw new AdminUsageError(`--${name} must be true or false.`, { field: name });
175
+ }
176
+
159
177
  function parseAdminInput(capability, argv) {
160
178
  const command = vnextCommand(capability).join(" ");
161
179
  const help = capability.cli.vnext;
@@ -167,22 +185,32 @@ function parseAdminInput(capability, argv) {
167
185
  throw new AdminUsageError(usage);
168
186
  }
169
187
 
170
- const limit = positiveInteger(options.limit, "limit", MAX_PAGE_LIMIT) ?? DEFAULT_PAGE_LIMIT;
188
+ const offsetPagination = ["users.list", "organization.list"].includes(capability.id);
189
+ const limit = positiveInteger(
190
+ options.limit,
191
+ "limit",
192
+ offsetPagination ? 100 : MAX_PAGE_LIMIT
193
+ ) ?? (offsetPagination ? 25 : DEFAULT_PAGE_LIMIT);
171
194
  const input = {};
172
195
  if (nodeCommand) input.node_id = positionals[0];
173
196
  if (options.supplier) input.organization_id = options.supplier;
197
+ if (options.organization && ["users.list", "organization.invite"].includes(capability.id)) {
198
+ input.organization_id = options.organization;
199
+ }
174
200
  if (options.facility) input.facility_id = options.facility;
175
201
  if (options.status) input.status = options.status;
176
202
  if (options.email) input.email = options.email;
177
- if (options.organization) input.organization = options.organization;
178
- if (options.name) input.name = options.name;
203
+ if (options.query) input.q = options.query;
204
+ if (options.role) input.role = options.role;
205
+ const approved = booleanOption(options.approved, "approved");
206
+ if (approved !== undefined) input.approved = approved;
207
+ if (options.ids) input.ids = options.ids.split(",");
179
208
  if (options.span) input.span = options.span;
180
209
  if (options.start) input.start = options.start;
181
210
  if (options.end) input.end = options.end;
182
211
  if (options.before) input.before = options.before;
183
212
  if (options.stream) input.stream = options.stream;
184
213
  if (options.cursor && command === "admin nodes logs") input.cursor = options.cursor;
185
- if (command === "admin users search") input.max = 1000;
186
214
  const maxPoints = positiveInteger(options["max-points"], "max-points", 500);
187
215
  if (maxPoints !== undefined) input.max_points = maxPoints;
188
216
  const count = positiveInteger(options.count, "count", 500);
@@ -191,10 +219,20 @@ function parseAdminInput(capability, argv) {
191
219
  if (command === "admin nodes logs") return parseLogPage(input);
192
220
 
193
221
  const ownerPagination = ["facilities.list", "nodes.list"].includes(capability.id);
222
+ const explicitOffset = nonnegativeInteger(options.offset, "offset");
194
223
  if (ownerPagination) {
195
224
  input.limit = limit;
196
225
  if (options.cursor) input.cursor = options.cursor;
197
226
  }
227
+ if (offsetPagination) {
228
+ if (explicitOffset !== undefined && options.cursor) {
229
+ throw new AdminUsageError("--offset and --cursor cannot be used together.", {
230
+ field: "offset",
231
+ });
232
+ }
233
+ input.limit = limit;
234
+ input.offset = explicitOffset ?? decodeCursor(command, options.cursor);
235
+ }
198
236
 
199
237
  return {
200
238
  input,
@@ -202,6 +240,8 @@ function parseAdminInput(capability, argv) {
202
240
  cursor: options.cursor,
203
241
  limit,
204
242
  ownerPagination,
243
+ ownerOffsetPagination: offsetPagination,
244
+ offset: input.offset,
205
245
  },
206
246
  };
207
247
  }
@@ -281,7 +321,16 @@ function collectionKey(capability) {
281
321
  if (root === "nodes" && leaf === "logs") return "logs";
282
322
  if (!["list", "search"].includes(leaf)) return null;
283
323
  if (root === "enrollment-tokens") return "enrollment_tokens";
284
- return ["facilities", "nodes", "reservations", "suppliers", "users"].includes(root) ? root : null;
324
+ return [
325
+ "facilities",
326
+ "nodes",
327
+ "organizations",
328
+ "reservations",
329
+ "suppliers",
330
+ "users",
331
+ ].includes(root)
332
+ ? root
333
+ : null;
285
334
  }
286
335
 
287
336
  function isSensitiveAdminField(key) {
@@ -337,21 +386,36 @@ function encodeCursor(command, offset) {
337
386
 
338
387
  function decodeCursor(command, cursor) {
339
388
  if (!cursor) return 0;
389
+ let decoded;
340
390
  try {
341
- const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
342
- if (decoded.command === command && Number.isInteger(decoded.offset) && decoded.offset >= 0) {
343
- return decoded.offset;
344
- }
345
- if (decoded.command !== undefined || decoded.offset !== undefined) {
346
- throw new AdminUsageError("The pagination cursor belongs to a different admin command.", {
347
- code: "invalid_cursor",
348
- field: "cursor",
349
- });
350
- }
351
- } catch (error) {
352
- if (error instanceof AdminUsageError) throw error;
391
+ const bytes = Buffer.from(cursor, "base64url");
392
+ if (bytes.toString("base64url") !== cursor) throw new Error("invalid base64url");
393
+ decoded = JSON.parse(bytes.toString("utf8"));
394
+ } catch {
395
+ throw new AdminUsageError("The pagination cursor is invalid.", {
396
+ code: "invalid_cursor",
397
+ field: "cursor",
398
+ });
399
+ }
400
+ if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
401
+ throw new AdminUsageError("The pagination cursor is invalid.", {
402
+ code: "invalid_cursor",
403
+ field: "cursor",
404
+ });
405
+ }
406
+ if (decoded.command !== command) {
407
+ throw new AdminUsageError("The pagination cursor belongs to a different admin command.", {
408
+ code: "invalid_cursor",
409
+ field: "cursor",
410
+ });
411
+ }
412
+ if (!Number.isSafeInteger(decoded.offset) || decoded.offset < 0) {
413
+ throw new AdminUsageError("The pagination cursor has an invalid offset.", {
414
+ code: "invalid_cursor",
415
+ field: "cursor",
416
+ });
353
417
  }
354
- return 0;
418
+ return decoded.offset;
355
419
  }
356
420
 
357
421
  export function finalizeAdminResult(capability, payload, pagination) {
@@ -362,18 +426,27 @@ export function finalizeAdminResult(capability, payload, pagination) {
362
426
  const rows = collectionRows(payload, key);
363
427
  const offset = pagination.ownerPagination
364
428
  ? 0
365
- : pagination.logQuery
429
+ : pagination.logQuery || pagination.ownerOffsetPagination
366
430
  ? pagination.offset
367
431
  : decodeCursor(command, pagination.cursor);
368
432
  const selected = (
369
- pagination.ownerPagination ? rows : rows.slice(offset, offset + pagination.limit)
433
+ pagination.ownerPagination || pagination.ownerOffsetPagination
434
+ ? rows
435
+ : rows.slice(offset, offset + pagination.limit)
370
436
  ).map(sanitizeAdminValue);
437
+ const ownerNextOffset =
438
+ pagination.ownerOffsetPagination && payload && !Array.isArray(payload)
439
+ ? payload.next_offset
440
+ : undefined;
371
441
  const ownerNextCursor =
372
442
  payload && !Array.isArray(payload)
373
443
  ? (payload.next_cursor ?? payload.nextCursor ?? payload.page?.next_cursor ?? null)
374
444
  : null;
375
- let nextCursor =
376
- !pagination.ownerPagination && offset + selected.length < rows.length
445
+ let nextCursor = pagination.ownerOffsetPagination
446
+ ? Number.isSafeInteger(ownerNextOffset) && ownerNextOffset > offset
447
+ ? encodeCursor(command, ownerNextOffset)
448
+ : null
449
+ : !pagination.ownerPagination && offset + selected.length < rows.length
377
450
  ? encodeCursor(command, offset + selected.length)
378
451
  : (ownerNextCursor ?? null);
379
452
  if (pagination.ownerPagination && nextCursor !== null && nextCursor === pagination.cursor) {
@@ -438,12 +511,15 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
438
511
  if (result.page.next_cursor) {
439
512
  const target = command === "admin nodes logs" ? ` ${shellSingleQuote(input.node_id)}` : "";
440
513
  const filters = [
441
- ["supplier", input.organization_id],
514
+ ["supplier", command === "admin users list" ? undefined : input.organization_id],
515
+ ["organization", command === "admin users list" ? input.organization_id : undefined],
442
516
  ["facility", input.facility_id],
443
517
  ["status", input.status],
444
518
  ["email", input.email],
445
- ["organization", input.organization],
446
- ["name", input.name],
519
+ ["query", input.q],
520
+ ["role", input.role],
521
+ ["approved", input.approved],
522
+ ["ids", Array.isArray(input.ids) ? input.ids.join(",") : undefined],
447
523
  ["limit", input.limit],
448
524
  ]
449
525
  .filter(([, value]) => value !== undefined)
@@ -1,5 +1,3 @@
1
- import { HARDWARE_CATALOG_PATH } from "../vendor/capabilities/cli.js";
2
-
3
1
  import {
4
2
  CliApiError,
5
3
  cliRequest,
@@ -793,7 +791,7 @@ const CUSTOMER_FORBIDDEN_FIELDS = {
793
791
  };
794
792
  const SUPPLIER_OWNER_FIELDS = { organizationId: true, organization_id: true };
795
793
 
796
- // The caller's own organization on a listing it relisted is not supplier identity.
794
+ // A listing owner may receive its own organization ID.
797
795
  function assertCustomerResultSafe(value, ownOrganizationId) {
798
796
  if (Array.isArray(value)) {
799
797
  for (const item of value) assertCustomerResultSafe(item, ownOrganizationId);
@@ -802,6 +800,7 @@ function assertCustomerResultSafe(value, ownOrganizationId) {
802
800
  if (!value || typeof value !== "object") return;
803
801
  const ownedBySupplier = supplierOwnedRecord(value);
804
802
  for (const [key, child] of Object.entries(value)) {
803
+ if (key === "supplierId" && value.capacityListingId && child === ownOrganizationId) continue;
805
804
  if (
806
805
  CUSTOMER_FORBIDDEN_FIELDS[key] === true ||
807
806
  (ownedBySupplier &&
@@ -835,7 +834,12 @@ function parseCapabilityInput(capability, rawInput) {
835
834
 
836
835
  // Commerce's market routes and Markets serve every caller on one path and
837
836
  // decide from the session what it may do; a staff session is an admin there.
838
- const ROLE_AWARE_PREFIXES = ["/commerce/listings", "/commerce/reservations", "/commerce/fees", "/v1/markets"];
837
+ const ROLE_AWARE_PREFIXES = [
838
+ "/commerce/v2/listings",
839
+ "/commerce/reservations",
840
+ "/commerce/fees",
841
+ "/v1/markets",
842
+ ];
839
843
 
840
844
  function isRoleAwarePath(path) {
841
845
  const pathname = String(path).split("?")[0];
@@ -852,7 +856,9 @@ function adminFetch(io, path, init = {}) {
852
856
  ...(init.headers !== undefined ? { headers: init.headers } : {}),
853
857
  ...((init.signal ?? io.signal) !== undefined ? { signal: init.signal ?? io.signal } : {}),
854
858
  };
855
- return isRoleAwarePath(path) ? cliRequest(request) : operatorRequest(request);
859
+ return isRoleAwarePath(path) || (path === "/v1/staff/invites" && request.method === "POST")
860
+ ? cliRequest(request)
861
+ : operatorRequest(request);
856
862
  }
857
863
 
858
864
  async function customerCapabilityFetch(io, path, init = {}, sessionSnapshot) {
@@ -992,20 +998,6 @@ async function executeCapability(
992
998
  role,
993
999
  fetch: (path, init = {}) => {
994
1000
  if (executionRuntime?.transport) return executionRuntime.transport(path, init);
995
- if (
996
- audience === "admin" &&
997
- capability.id === "listings.create" &&
998
- path === HARDWARE_CATALOG_PATH
999
- ) {
1000
- return operatorRequest({
1001
- endpoint: "/catalog/hardware",
1002
- env: io.env,
1003
- fetchImpl: io.fetchImpl,
1004
- method: init.method ?? "GET",
1005
- ...(init.headers !== undefined ? { headers: init.headers } : {}),
1006
- ...(init.signal !== undefined ? { signal: init.signal } : {}),
1007
- });
1008
- }
1009
1001
  if (audience === "admin") {
1010
1002
  // Shared observability capabilities use their owner namespace;
1011
1003
  // the CLI reaches those same node-scoped routes through staff auth.
@@ -1173,9 +1165,12 @@ export async function runCatalogCli(argv, io, { version }) {
1173
1165
  eligibleHuman,
1174
1166
  machine,
1175
1167
  });
1176
- const role = await resolveAdminRole(io);
1177
1168
  const adminRequest = resolveAdminRequest(catalog, capabilities.ADMIN_CLI_ROOTS, request.argv);
1178
1169
  activeCommand = adminRequest.command;
1170
+ if (adminRequest.kind === "command" && !adminRequest.handler) {
1171
+ adminRequest.input = parseCapabilityInput(adminRequest.capability, adminRequest.input);
1172
+ }
1173
+ const role = await resolveAdminRole(io);
1179
1174
  if (adminRequest.kind === "root") {
1180
1175
  const data = adminRootData(catalog, capabilities.ADMIN_CLI_ROOTS, adminRequest.root);
1181
1176
  if (machine) writeMachine(stdout, format, resultEnvelope(adminRequest.command, data));
@@ -1040,7 +1040,7 @@ function metricSummary(metrics) {
1040
1040
  .join(", ");
1041
1041
  }
1042
1042
 
1043
- function evidenceLines(data) {
1043
+ function evidenceLines(data, printedLogs = new Map()) {
1044
1044
  const lines = [
1045
1045
  data.resource.kind === "machine"
1046
1046
  ? `Machine: ${data.resource.hostname ?? "selected machine"}`
@@ -1063,7 +1063,7 @@ function evidenceLines(data) {
1063
1063
  if (data.logs) {
1064
1064
  lines.push(`Logs (${data.logs.stream}): ${data.logs.state}`);
1065
1065
  for (const [index, source] of data.logs.sources.entries()) {
1066
- for (const entry of source.entries) {
1066
+ for (const entry of unprintedLogLines(source, printedLogs.get(index)).fresh) {
1067
1067
  lines.push(
1068
1068
  ` ${evidenceSourceLabel(data, index)} · ${entry.observed_at} · ${entry.severity} · ${entry.message}`
1069
1069
  );
@@ -1131,7 +1131,21 @@ function liveState(data) {
1131
1131
  };
1132
1132
  }
1133
1133
 
1134
- function writeHumanProgress(stdout, data) {
1134
+ // Follow re-reads the whole live window, so count each line's occurrences
1135
+ // rather than trusting timestamps: identical records still print once each,
1136
+ // and a late older line is still new.
1137
+ function unprintedLogLines(source, printed = new Map()) {
1138
+ const counts = new Map();
1139
+ const fresh = source.entries.filter((line) => {
1140
+ const key = `${line.observed_at} ${line.severity} ${line.message}`;
1141
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1142
+ return counts.get(key) > (printed.get(key) ?? 0);
1143
+ });
1144
+ for (const [key, count] of counts) counts.set(key, Math.max(count, printed.get(key) ?? 0));
1145
+ return { fresh, seen: counts };
1146
+ }
1147
+
1148
+ function writeHumanProgress(stdout, data, printedLogs) {
1135
1149
  if (data.metrics) {
1136
1150
  for (const [index, source] of data.metrics.series.entries()) {
1137
1151
  const point = source.points.at(-1);
@@ -1144,7 +1158,9 @@ function writeHumanProgress(stdout, data) {
1144
1158
  }
1145
1159
  if (data.logs) {
1146
1160
  for (const [index, source] of data.logs.sources.entries()) {
1147
- for (const line of source.entries) {
1161
+ const { fresh, seen } = unprintedLogLines(source, printedLogs.get(index));
1162
+ printedLogs.set(index, seen);
1163
+ for (const line of fresh) {
1148
1164
  stdout.write(
1149
1165
  `${evidenceSourceLabel(data, index)} ${line.observed_at} ${line.severity} ${line.message}\n`
1150
1166
  );
@@ -1153,14 +1169,14 @@ function writeHumanProgress(stdout, data) {
1153
1169
  }
1154
1170
  }
1155
1171
 
1156
- function streamResult(interaction, data, progress, exitCode, outcome, errors = []) {
1172
+ function streamResult(interaction, data, progress, printedLogs, exitCode, outcome, errors = []) {
1157
1173
  return interaction.result({
1158
1174
  data,
1159
1175
  errors: publicErrors(errors),
1160
1176
  exitCode,
1161
1177
  outcome,
1162
1178
  progress,
1163
- presentation: { lines: evidenceLines(data) },
1179
+ presentation: { lines: evidenceLines(data, printedLogs) },
1164
1180
  });
1165
1181
  }
1166
1182
 
@@ -1191,12 +1207,13 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1191
1207
  const deadline = timeoutMs === null ? Number.POSITIVE_INFINITY : startedAt + timeoutMs;
1192
1208
  let current = resourceBase(resource);
1193
1209
  const progress = [];
1210
+ const printedLogs = new Map();
1194
1211
  let iteration = 0;
1195
1212
  try {
1196
1213
  for (;;) {
1197
1214
  if (signal.aborted) {
1198
1215
  current = streamState(current, "interrupted");
1199
- return streamResult(interaction, current, progress, 130, {
1216
+ return streamResult(interaction, current, progress, printedLogs, 130, {
1200
1217
  class: "ended",
1201
1218
  phase: "interrupted",
1202
1219
  });
@@ -1204,7 +1221,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1204
1221
  const observedAt = nowMilliseconds(deps.now);
1205
1222
  if (observedAt >= deadline) {
1206
1223
  current = streamState(current, "ended");
1207
- return streamResult(interaction, current, progress, 124, {
1224
+ return streamResult(interaction, current, progress, printedLogs, 124, {
1208
1225
  class: "ended",
1209
1226
  phase: "observation_timeout",
1210
1227
  });
@@ -1230,7 +1247,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1230
1247
  { ...resourceBase(resource), metrics: collected.metrics, logs: collected.logs },
1231
1248
  "interrupted"
1232
1249
  );
1233
- return streamResult(interaction, current, progress, 130, {
1250
+ return streamResult(interaction, current, progress, printedLogs, 130, {
1234
1251
  class: "ended",
1235
1252
  phase: "interrupted",
1236
1253
  });
@@ -1248,6 +1265,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1248
1265
  interaction,
1249
1266
  current,
1250
1267
  progress,
1268
+ printedLogs,
1251
1269
  status.exitCode,
1252
1270
  status.outcome,
1253
1271
  collected.errors
@@ -1261,11 +1279,11 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1261
1279
  sequence: iteration,
1262
1280
  };
1263
1281
  if (interaction.machine) progress.push(progressEntry);
1264
- else writeHumanProgress(deps.stdout, current);
1282
+ else writeHumanProgress(deps.stdout, current, printedLogs);
1265
1283
 
1266
1284
  if (count !== null && iteration >= count) {
1267
1285
  current = streamState(current, "ended");
1268
- return streamResult(interaction, current, progress, 0, {
1286
+ return streamResult(interaction, current, progress, printedLogs, 0, {
1269
1287
  class: "ended",
1270
1288
  phase: "observation_complete",
1271
1289
  });
@@ -1275,7 +1293,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1275
1293
  const continued = await sleepUntil(Math.min(intervalMs, remaining), deps, signal);
1276
1294
  if (!continued) {
1277
1295
  current = streamState(current, "interrupted");
1278
- return streamResult(interaction, current, progress, 130, {
1296
+ return streamResult(interaction, current, progress, printedLogs, 130, {
1279
1297
  class: "ended",
1280
1298
  phase: "interrupted",
1281
1299
  });
@@ -330,6 +330,35 @@ function parsePositiveInteger(runtime, value, option) {
330
330
  return parsed;
331
331
  }
332
332
 
333
+ /** Running VMs that join as workers alongside the selected machines. */
334
+ function requestedVirtualMachineIds(runtime, input) {
335
+ const raw = text(input.vms);
336
+ const ids = raw
337
+ ? raw
338
+ .split(",")
339
+ .map((value) => value.trim())
340
+ .filter(Boolean)
341
+ : [];
342
+ if (ids.some((id) => !UUID.test(id))) {
343
+ runtime.usage("--vms takes canonical VM IDs.", {
344
+ code: "canonical_vm_id_required",
345
+ field: "vms",
346
+ });
347
+ }
348
+ if (new Set(ids).size !== ids.length) {
349
+ runtime.usage("Each selected VM must be unique.", {
350
+ code: "duplicate_vm_selection",
351
+ field: "vms",
352
+ });
353
+ }
354
+ return ids.sort();
355
+ }
356
+
357
+ function parseMembershipCount(runtime, value, allowZero) {
358
+ if (allowZero && Number(value) === 0) return 0;
359
+ return parsePositiveInteger(runtime, value, "--nodes");
360
+ }
361
+
333
362
  function parseTimeoutSeconds(runtime, raw, { required = false } = {}) {
334
363
  if (raw === undefined || raw === null || raw === "") {
335
364
  if (required)
@@ -1088,11 +1117,14 @@ export async function launchClusterMode(
1088
1117
  }
1089
1118
  const label = clusterModeLabel(accessMode);
1090
1119
  requireWaitShape(runtime, input);
1120
+ const virtualMachineIds = requestedVirtualMachineIds(runtime, input);
1091
1121
  const membershipValue = input.nodes ?? input.nodeCount;
1092
1122
  let membershipCount =
1093
1123
  membershipValue === undefined || membershipValue === null
1094
- ? null
1095
- : parsePositiveInteger(runtime, membershipValue, "--nodes");
1124
+ ? virtualMachineIds.length
1125
+ ? 0
1126
+ : null
1127
+ : parseMembershipCount(runtime, membershipValue, virtualMachineIds.length > 0);
1096
1128
  if (membershipCount === null && !runtime.eligibleHuman) {
1097
1129
  runtime.usage("--nodes is required outside an interactive terminal.", {
1098
1130
  code: "missing_input",
@@ -1145,7 +1177,7 @@ export async function launchClusterMode(
1145
1177
  membershipCount !== null &&
1146
1178
  requestedReferences.length === membershipCount &&
1147
1179
  requestedReferences.every((reference) => UUID.test(reference));
1148
- if (initialEligibility.length === 0 && !emptyProvisioningReplay) {
1180
+ if (initialEligibility.length === 0 && !emptyProvisioningReplay && membershipCount !== 0) {
1149
1181
  runtime.error(`No machines are currently eligible for ${label} launch.`, {
1150
1182
  code: "no_eligible_machines",
1151
1183
  });
@@ -1165,14 +1197,18 @@ export async function launchClusterMode(
1165
1197
  }
1166
1198
  const selected = emptyProvisioningReplay
1167
1199
  ? requestedReferences.map((id) => ({ id, hostname: id }))
1168
- : await selectLaunchMachines(
1169
- runtime,
1170
- input,
1171
- initialEligibility,
1172
- membershipCount,
1173
- initialExisting
1174
- );
1200
+ : membershipCount === 0
1201
+ ? []
1202
+ : await selectLaunchMachines(
1203
+ runtime,
1204
+ input,
1205
+ initialEligibility,
1206
+ membershipCount,
1207
+ initialExisting
1208
+ );
1175
1209
  const selectedIds = selected.map((machine) => machine.id).sort();
1210
+ // A VM member is listed under its VM id.
1211
+ const goalIds = [...selectedIds, ...virtualMachineIds].sort();
1176
1212
  const reviewedKey = record(keyPlan?.key);
1177
1213
  const requested = {
1178
1214
  reservation_id: reservationId,
@@ -1180,6 +1216,7 @@ export async function launchClusterMode(
1180
1216
  network_mode: networkMode,
1181
1217
  membership_count: membershipCount,
1182
1218
  machine_ids: selectedIds,
1219
+ ...(virtualMachineIds.length ? { virtual_machine_ids: virtualMachineIds } : {}),
1183
1220
  idempotency_key: idempotencyKey,
1184
1221
  organization,
1185
1222
  ...(keyPlan
@@ -1209,7 +1246,7 @@ export async function launchClusterMode(
1209
1246
  );
1210
1247
  }
1211
1248
  } else {
1212
- if (!sameMembers(currentIds, selectedIds)) {
1249
+ if (!sameMembers(currentIds, goalIds)) {
1213
1250
  runtime.error(
1214
1251
  `This reservation already has a ${label} cluster with a different membership goal.`,
1215
1252
  { code: "cluster_launch_goal_conflict" }
@@ -1231,7 +1268,7 @@ export async function launchClusterMode(
1231
1268
  signal: runtime.signal,
1232
1269
  sleep: runtime.sleep,
1233
1270
  },
1234
- goalIds: selectedIds,
1271
+ goalIds,
1235
1272
  input,
1236
1273
  requireAccess: true,
1237
1274
  })
@@ -1270,6 +1307,10 @@ export async function launchClusterMode(
1270
1307
  label: `Machine ${index + 1}`,
1271
1308
  value: `${machine.hostname} (${machine.id})`,
1272
1309
  })),
1310
+ ...virtualMachineIds.map((virtualMachineId, index) => ({
1311
+ label: `VM ${index + 1}`,
1312
+ value: `${virtualMachineId} (SSH keys are set aside while it is in the cluster)`,
1313
+ })),
1273
1314
  ],
1274
1315
  });
1275
1316
  if (!confirmed) {
@@ -1313,7 +1354,7 @@ export async function launchClusterMode(
1313
1354
  });
1314
1355
  }
1315
1356
  } else {
1316
- if (!sameMembers(currentIds, selectedIds)) {
1357
+ if (!sameMembers(currentIds, goalIds)) {
1317
1358
  runtime.error(`A different ${label} membership goal was accepted after review.`, {
1318
1359
  code: "cluster_launch_goal_conflict",
1319
1360
  });
@@ -1328,7 +1369,7 @@ export async function launchClusterMode(
1328
1369
  accepted: { replayed: true, requested },
1329
1370
  cluster: current,
1330
1371
  deps: { fetch: context.fetch, interaction: runtime },
1331
- goalIds: selectedIds,
1372
+ goalIds,
1332
1373
  input,
1333
1374
  requireAccess: true,
1334
1375
  })
@@ -1364,8 +1405,9 @@ export async function launchClusterMode(
1364
1405
  headers: { "Idempotency-Key": idempotencyKey },
1365
1406
  body: {
1366
1407
  network_mode: networkMode,
1367
- node_count: membershipCount,
1408
+ ...(membershipCount > 0 ? { node_count: membershipCount } : {}),
1368
1409
  node_ids: selectedIds,
1410
+ ...(virtualMachineIds.length ? { virtual_machine_ids: virtualMachineIds } : {}),
1369
1411
  ...(selectedKeyId ? { ssh_key_ids: [selectedKeyId] } : {}),
1370
1412
  },
1371
1413
  }
@@ -1402,7 +1444,7 @@ export async function launchClusterMode(
1402
1444
  sleep: runtime.sleep,
1403
1445
  stderr: runtime.stderr,
1404
1446
  },
1405
- goalIds: selectedIds,
1447
+ goalIds,
1406
1448
  input,
1407
1449
  requireAccess: true,
1408
1450
  });
@@ -32,10 +32,10 @@ const MODE_OPTIONS = new Map([
32
32
  "volume",
33
33
  ]),
34
34
  ],
35
- ["kubernetes", new Set([...COMMON_OPTIONS, "machine", "machines", "network", "nodes"])],
35
+ ["kubernetes", new Set([...COMMON_OPTIONS, "machine", "machines", "network", "nodes", "vms"])],
36
36
  [
37
37
  "slurm",
38
- new Set([...COMMON_OPTIONS, ...KEY_OPTIONS, "machine", "machines", "network", "nodes"]),
38
+ new Set([...COMMON_OPTIONS, ...KEY_OPTIONS, "machine", "machines", "network", "nodes", "vms"]),
39
39
  ],
40
40
  ]);
41
41
  const OPTION_FLAGS = {
@@ -646,6 +646,13 @@ async function deleteVolume(input, runtime) {
646
646
  }
647
647
  }
648
648
 
649
+ function shellQuote(value) {
650
+ const rendered = String(value);
651
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(rendered)
652
+ ? rendered
653
+ : `'${rendered.replaceAll("'", `'\\''`)}'`;
654
+ }
655
+
649
656
  async function fetchFileRows(runtime, drive, prefix) {
650
657
  const folder = prefix.includes("/") ? prefix.slice(0, prefix.lastIndexOf("/")) : "";
651
658
  const entries = [];
@@ -1121,8 +1128,9 @@ export function writeStorageResultHuman(stdout, capabilityId, execution) {
1121
1128
  }
1122
1129
  for (const file of files) stdout.write(`${file.path} ${file.size_bytes} bytes\n`);
1123
1130
  if (execution.page?.next_cursor) {
1131
+ const prefixFlag = data.prefix ? ` --prefix=${shellQuote(data.prefix)}` : "";
1124
1132
  stdout.write(
1125
- `Next: ornn volumes files list ${data.volume.id} --cursor ${execution.page.next_cursor} --format human\n`
1133
+ `Next: ornn volumes files list ${data.volume.id}${prefixFlag} --cursor ${execution.page.next_cursor} --format human\n`
1126
1134
  );
1127
1135
  }
1128
1136
  return;
@@ -1191,7 +1191,7 @@ export const vmLaunchModeHandler = {
1191
1191
  body: {
1192
1192
  access_mode: "virtual-machine",
1193
1193
  network_mode: "public",
1194
- image_id: null,
1194
+ disk_id: null,
1195
1195
  machine_count: 1,
1196
1196
  request_id: idempotencyKey,
1197
1197
  ssh_key_ids: [resourceId(key)],
@@ -2056,7 +2056,7 @@ async function launchDurableBareMetalWorkspace({
2056
2056
  method: "POST",
2057
2057
  body: {
2058
2058
  access_mode: "bare-metal",
2059
- image_id: null,
2059
+ disk_id: null,
2060
2060
  machine_count: 1,
2061
2061
  network_mode: "private",
2062
2062
  request_id: `${id}:machine`,
@@ -2456,7 +2456,7 @@ export const bareMetalLaunchModeHandler = {
2456
2456
  const launchBody = {
2457
2457
  access_mode: "bare-metal",
2458
2458
  network_mode: "public",
2459
- image_id: null,
2459
+ disk_id: null,
2460
2460
  machine_count: 1,
2461
2461
  request_id: idempotencyKey,
2462
2462
  ssh_key_ids: [resourceId(key)],