@ornncompute/cli 1.0.72 → 1.0.75

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 (34) hide show
  1. package/README.md +24 -20
  2. package/package.json +1 -1
  3. package/src/admin-capacity-reservations.mjs +2 -12
  4. package/src/admin-command-handlers.mjs +0 -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 +2 -36
  9. package/src/admin-node-lifecycle.mjs +25 -43
  10. package/src/admin-shell.mjs +39 -32
  11. package/src/external-bucket-commands.mjs +5 -73
  12. package/src/interaction-runtime.mjs +2 -37
  13. package/src/kubernetes-commands.mjs +9 -100
  14. package/src/launch-mode-handlers.mjs +0 -1
  15. package/src/mount-commands.mjs +25 -131
  16. package/src/storage-commands.mjs +2 -87
  17. package/src/vm-commands.mjs +9 -42
  18. package/vendor/capabilities/capabilities/admin-enrollment.js +18 -17
  19. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +3 -101
  20. package/vendor/capabilities/capabilities/admin.js +3 -3
  21. package/vendor/capabilities/capabilities/clusters.js +3 -3
  22. package/vendor/capabilities/capabilities/commerce-market.js +9 -9
  23. package/vendor/capabilities/capabilities/compute.js +7 -7
  24. package/vendor/capabilities/capabilities/credits.js +7 -2
  25. package/vendor/capabilities/capabilities/external-buckets.js +4 -4
  26. package/vendor/capabilities/capabilities/fleet.js +109 -29
  27. package/vendor/capabilities/capabilities/market-shared.js +1 -1
  28. package/vendor/capabilities/capabilities/markets.js +12 -12
  29. package/vendor/capabilities/capabilities/mounts.js +4 -4
  30. package/vendor/capabilities/capabilities/networks.js +5 -5
  31. package/vendor/capabilities/capabilities/observability.js +49 -14
  32. package/vendor/capabilities/capabilities/tokens.js +5 -9
  33. package/vendor/capabilities/capabilities/users.js +8 -10
  34. package/vendor/capabilities/capabilities/volumes.js +5 -5
@@ -9,9 +9,9 @@ const POLL_INTERVAL_MS = 1_000;
9
9
  const COMMAND_BY_CAPABILITY = Object.freeze({
10
10
  "nodes.reboot": "reboot",
11
11
  "nodes.hard_reset": "hard-reset",
12
- "nodes.suspend_management": "suspend-management",
13
- "nodes.resume_management": "resume-management",
14
- "nodes.terminate": "terminate",
12
+ "node.off-grid": "suspend-management",
13
+ "node.on-grid": "resume-management",
14
+ "node.terminate": "terminate",
15
15
  "nodes.assign": "assign",
16
16
  });
17
17
 
@@ -22,17 +22,17 @@ const CAPABILITY_BY_COMMAND = Object.freeze(
22
22
  );
23
23
 
24
24
  const USAGE = Object.freeze({
25
- reboot: "Usage: ornn admin nodes reboot <node-ref> [--yes] [--no-wait] [--timeout SECONDS]",
25
+ reboot: "Usage: ornn admin nodes reboot <node-ref> [--no-wait] [--timeout SECONDS]",
26
26
  "hard-reset":
27
- "Usage: ornn admin nodes hard-reset <node-ref> --erase-local-data [--yes] [--no-wait] [--timeout SECONDS]",
27
+ "Usage: ornn admin nodes hard-reset <node-ref> --erase-local-data [--no-wait] [--timeout SECONDS]",
28
28
  "suspend-management":
29
- "Usage: ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--yes] [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
29
+ "Usage: ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
30
30
  "resume-management":
31
- "Usage: ornn admin nodes resume-management <node-ref> [--yes] [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
31
+ "Usage: ornn admin nodes resume-management <node-ref> [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
32
32
  terminate:
33
- "Usage: ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--yes] [--idempotency-key KEY]",
33
+ "Usage: ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--idempotency-key KEY]",
34
34
  assign:
35
- "Usage: ornn admin nodes assign <node-ref> --reservation RESERVATION [--yes] [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
35
+ "Usage: ornn admin nodes assign <node-ref> --reservation RESERVATION [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
36
36
  });
37
37
 
38
38
  const BOOLEAN_OPTIONS = new Set([
@@ -41,16 +41,15 @@ const BOOLEAN_OPTIONS = new Set([
41
41
  "no-wait",
42
42
  "skip-cleanup",
43
43
  "wait",
44
- "yes",
45
44
  ]);
46
45
  const VALUE_OPTIONS = new Set(["idempotency-key", "reason", "reservation", "timeout"]);
47
46
  const ALLOWED_OPTIONS = Object.freeze({
48
- reboot: new Set(["yes", "wait", "no-wait", "timeout"]),
49
- "hard-reset": new Set(["erase-local-data", "yes", "wait", "no-wait", "timeout"]),
50
- "suspend-management": new Set(["reason", "yes", "idempotency-key", "wait", "no-wait", "timeout"]),
51
- "resume-management": new Set(["yes", "idempotency-key", "wait", "no-wait", "timeout"]),
52
- terminate: new Set(["delete-node-record", "skip-cleanup", "reason", "yes", "idempotency-key"]),
53
- assign: new Set(["reservation", "yes", "idempotency-key", "wait", "no-wait", "timeout"]),
47
+ reboot: new Set(["wait", "no-wait", "timeout"]),
48
+ "hard-reset": new Set(["erase-local-data", "wait", "no-wait", "timeout"]),
49
+ "suspend-management": new Set(["reason", "idempotency-key", "wait", "no-wait", "timeout"]),
50
+ "resume-management": new Set(["idempotency-key", "wait", "no-wait", "timeout"]),
51
+ terminate: new Set(["delete-node-record", "skip-cleanup", "reason", "idempotency-key"]),
52
+ assign: new Set(["reservation", "idempotency-key", "wait", "no-wait", "timeout"]),
54
53
  });
55
54
 
56
55
  function mutationError(message, { code = "usage_error", field } = {}) {
@@ -135,22 +134,16 @@ export function parseAdminNodeLifecycleInput(command, argv) {
135
134
  });
136
135
  }
137
136
  if (name === "hard-reset" && options["erase-local-data"] !== true) {
138
- throw mutationError(
139
- `${usage}\nHard reset requires --erase-local-data; --yes cannot imply data loss.`,
140
- {
141
- code: "erase_local_data_required",
142
- field: "erase-local-data",
143
- }
144
- );
137
+ throw mutationError(`${usage}\nHard reset requires --erase-local-data.`, {
138
+ code: "erase_local_data_required",
139
+ field: "erase-local-data",
140
+ });
145
141
  }
146
142
  if (name === "terminate" && options["delete-node-record"] !== true) {
147
- throw mutationError(
148
- `${usage}\nTermination requires --delete-node-record; --yes cannot imply deletion.`,
149
- {
150
- code: "delete_node_record_required",
151
- field: "delete-node-record",
152
- }
153
- );
143
+ throw mutationError(`${usage}\nTermination requires --delete-node-record.`, {
144
+ code: "delete_node_record_required",
145
+ field: "delete-node-record",
146
+ });
154
147
  }
155
148
  if (name === "assign" && !options.reservation) {
156
149
  throw mutationError(`${usage}\nAssignment requires --reservation.`, {
@@ -170,12 +163,11 @@ export function parseAdminNodeLifecycleInput(command, argv) {
170
163
  const idempotencyKey = options["idempotency-key"]?.trim();
171
164
  const safety =
172
165
  name === "reboot" || name === "hard-reset"
173
- ? ADMIN_MUTATION_SAFETY.eligibleHumanOnly
166
+ ? ADMIN_MUTATION_SAFETY.direct
174
167
  : ADMIN_MUTATION_SAFETY.reconcile;
175
168
  return {
176
169
  input: {
177
170
  node_ref: nodeRef,
178
- ...(options.yes ? { yes: true } : {}),
179
171
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
180
172
  ...(options.reason ? { reason: options.reason } : {}),
181
173
  ...(options.wait ? { wait: true } : options["no-wait"] ? { wait: false } : {}),
@@ -192,7 +184,6 @@ export function parseAdminNodeLifecycleInput(command, argv) {
192
184
  mutation: {
193
185
  idempotencyKey,
194
186
  safety,
195
- yes: options.yes === true,
196
187
  },
197
188
  };
198
189
  }
@@ -900,15 +891,6 @@ export async function runAdminNodeLifecycle({
900
891
  },
901
892
  });
902
893
 
903
- if (result.status === "declined") {
904
- return {
905
- ...result,
906
- action: null,
907
- resume: null,
908
- errors: [],
909
- };
910
- }
911
-
912
894
  const accepted = {
913
895
  operation: command,
914
896
  reconciled: result.reconciled === true || lateReconciled,
@@ -939,7 +921,7 @@ export const adminNodeLifecycleHandlers = Object.keys(COMMAND_BY_CAPABILITY).map
939
921
  safety:
940
922
  COMMAND_BY_CAPABILITY[capabilityId] === "reboot" ||
941
923
  COMMAND_BY_CAPABILITY[capabilityId] === "hard-reset"
942
- ? ADMIN_MUTATION_SAFETY.eligibleHumanOnly
924
+ ? ADMIN_MUTATION_SAFETY.direct
943
925
  : ADMIN_MUTATION_SAFETY.reconcile,
944
926
  run: runAdminNodeLifecycle,
945
927
  })
@@ -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
  }
@@ -402,7 +402,7 @@ function connectionEquivalent(drive, intent) {
402
402
  );
403
403
  }
404
404
 
405
- function requireMutationPreflight(input, runtime, { consent = false } = {}) {
405
+ function requireMutationPreflight(input, runtime) {
406
406
  const noninteractive = runtime.interaction.machine || runtime.interaction.redirected;
407
407
  if (noninteractive && !String(input.idempotencyKey ?? "").trim()) {
408
408
  usage(
@@ -410,23 +410,6 @@ function requireMutationPreflight(input, runtime, { consent = false } = {}) {
410
410
  "idempotency-key"
411
411
  );
412
412
  }
413
- if (noninteractive && consent && input.yes !== true) {
414
- usage(
415
- "Consent is required. Review the exact intent and supply --yes.",
416
- "yes",
417
- "consent_required"
418
- );
419
- }
420
- }
421
-
422
- async function reviewed(runtime, options) {
423
- const accepted = await runtime.interaction.review(options);
424
- if (accepted) return null;
425
- return runtime.interaction.result({
426
- data: null,
427
- exitCode: 1,
428
- outcome: { class: "ended", phase: "declined" },
429
- });
430
413
  }
431
414
 
432
415
  const SAFE_EXTERNAL_ERRORS = {
@@ -493,28 +476,11 @@ async function showBucket(input, runtime) {
493
476
 
494
477
  async function connectBucket(input, runtime) {
495
478
  const intent = await connectionIntent(input);
496
- requireMutationPreflight(input, runtime, { consent: true });
479
+ requireMutationPreflight(input, runtime);
497
480
  const fingerprint =
498
481
  intent.accessKeyId && intent.secretAccessKey
499
482
  ? credentialFingerprint(intent.accessKeyId, intent.secretAccessKey)
500
483
  : null;
501
- const declined = await reviewed(runtime, {
502
- title: "Connect external bucket",
503
- terms: [
504
- { label: "Kind", value: providerName(intent.kind) },
505
- { label: "Name", value: intent.name },
506
- { label: "Bucket", value: intent.bucket },
507
- { label: "Prefix", value: intent.prefix ?? "(bucket root)" },
508
- { label: "Access", value: intent.readOnly ? "read-only" : "read-write" },
509
- {
510
- label: "Credentials",
511
- value: fingerprint ? `fingerprint ${fingerprint.slice(0, 12)}` : "ambient GCS access",
512
- },
513
- { label: "Verification", value: "separate command required" },
514
- ],
515
- prompt: "Connect this external bucket?",
516
- });
517
- if (declined) return declined;
518
484
 
519
485
  const rows = await fetchDrives(runtime);
520
486
  const sameName = bucketRows(rows).filter(
@@ -644,7 +610,7 @@ async function updateCredentials(input, runtime) {
644
610
  );
645
611
  const accessKeyId = requiredText(input.accessKeyId, "--access-key-id", "access-key-id");
646
612
  const secret = await readCredentialFile(input.secretAccessKeyFile);
647
- requireMutationPreflight(input, runtime, { consent: true });
613
+ requireMutationPreflight(input, runtime);
648
614
  const fingerprint = credentialFingerprint(accessKeyId, secret);
649
615
  const source = resolveExternalBucket(await fetchDrives(runtime), reference);
650
616
  const kind = externalKind(source);
@@ -655,17 +621,6 @@ async function updateCredentials(input, runtime) {
655
621
  );
656
622
  }
657
623
  const id = safeIdentifier(source.id);
658
- const declined = await reviewed(runtime, {
659
- title: "Update external bucket credentials",
660
- terms: [
661
- { label: "External bucket", value: `${safeName(source.name)} (${id})` },
662
- { label: "Kind", value: providerName(kind) },
663
- { label: "Credential fingerprint", value: fingerprint.slice(0, 12) },
664
- { label: "Verification", value: "must be run again" },
665
- ],
666
- prompt: "Replace these credentials?",
667
- });
668
- if (declined) return declined;
669
624
  let updated;
670
625
  try {
671
626
  updated = await runtime.fetch(
@@ -698,20 +653,9 @@ async function updateCredentials(input, runtime) {
698
653
 
699
654
  async function disconnectBucket(input, runtime) {
700
655
  const [reference] = positions(input, 1, "ornn external-buckets disconnect <external-bucket>");
701
- requireMutationPreflight(input, runtime, { consent: true });
656
+ requireMutationPreflight(input, runtime);
702
657
  const source = resolveExternalBucket(await fetchDrives(runtime), reference);
703
658
  const safe = safeExternalBucket(source);
704
- const declined = await reviewed(runtime, {
705
- title: "Disconnect external bucket",
706
- terms: [
707
- { label: "External bucket", value: `${safe.name} (${safe.id})` },
708
- { label: "Kind", value: providerName(safe.kind) },
709
- { label: "Source writeback", value: "disabled" },
710
- { label: "Consequence", value: "source access is removed after all mounts are detached" },
711
- ],
712
- prompt: "Disconnect this external bucket?",
713
- });
714
- if (declined) return declined;
715
659
  const attachedReservations = activeMountReservations(source);
716
660
  if (attachedReservations.length > 0) {
717
661
  throw new StorageCommandError(
@@ -1159,7 +1103,7 @@ async function importBucket(input, runtime) {
1159
1103
  usage("Noninteractive --wait requires --timeout <duration>.", "timeout");
1160
1104
  }
1161
1105
  const timeoutMs = parseTimeout(input.timeout);
1162
- requireMutationPreflight(input, runtime, { consent: true });
1106
+ requireMutationPreflight(input, runtime);
1163
1107
 
1164
1108
  const source = resolveExternalBucket(await fetchDrives(runtime), reference);
1165
1109
  const bucket = safeExternalBucket(source);
@@ -1229,18 +1173,6 @@ async function importBucket(input, runtime) {
1229
1173
  : null;
1230
1174
  const replayed = attachment !== null;
1231
1175
  if (!attachment) {
1232
- const declined = await reviewed(runtime, {
1233
- title: "Import external bucket",
1234
- terms: [
1235
- { label: "Source", value: `${providerName(bucket.kind)} ${bucket.location.bucket}` },
1236
- { label: "Working volume", value: `${bucket.name} (${bucket.id})` },
1237
- { label: "Reservation", value: reservationId },
1238
- { label: "Isolation", value: "one Ornn-managed working copy" },
1239
- { label: "Source writeback", value: "disabled" },
1240
- ],
1241
- prompt: "Create this managed working copy?",
1242
- });
1243
- if (declined) return declined;
1244
1176
  try {
1245
1177
  const payload = await runtime.fetch(
1246
1178
  computeEndpoint(
@@ -18,7 +18,6 @@ const BOOLEAN_OPTIONS = new Set([
18
18
  "read-write",
19
19
  "recursive",
20
20
  "wait",
21
- "yes",
22
21
  ]);
23
22
 
24
23
  const OPTIONAL_VALUE_OPTIONS = new Set(["resume"]);
@@ -230,42 +229,8 @@ export function createInteractionRuntime({
230
229
  return answer;
231
230
  }
232
231
 
233
- async function review({ prompt = "Continue?", requireLive = false, terms, title }) {
234
- if (machine) {
235
- if (!requireLive && options.yes === true) return true;
236
- usage(
237
- requireLive
238
- ? "This operation is supported only in an eligible interactive terminal."
239
- : "Consent is required. Review the terms and supply --yes to continue.",
240
- {
241
- code: requireLive ? "unsupported_in_machine_mode" : "consent_required",
242
- ...(!requireLive ? { field: "yes" } : {}),
243
- }
244
- );
245
- }
246
- if (requireLive && !eligibleHuman) {
247
- usage("This operation is supported only in an eligible interactive terminal.", {
248
- code: "unsupported_in_machine_mode",
249
- });
250
- }
251
- stderr.write(`${title}\n`);
252
- for (const term of terms) stderr.write(` ${term.label}: ${term.value}\n`);
253
-
254
- if (!requireLive && options.yes === true) return true;
255
- if (!eligibleHuman) {
256
- usage("Consent is required. Review the terms and supply --yes to continue.", {
257
- code: "consent_required",
258
- field: "yes",
259
- });
260
- }
261
- let answer;
262
- try {
263
- answer = await readline().question(`${prompt} [y/N] `);
264
- } catch (caught) {
265
- if (caught?.code === "ERR_USE_AFTER_CLOSE") return false;
266
- throw caught;
267
- }
268
- return /^(y|yes)$/i.test(String(answer ?? "").trim());
232
+ async function review() {
233
+ return true;
269
234
  }
270
235
 
271
236
  function webUrl(path) {
@@ -914,12 +914,6 @@ function clusterMachineLaunchPreflight(runtime, input, membershipCount) {
914
914
  });
915
915
  }
916
916
  if (runtime.eligibleHuman) return;
917
- if (input.yes !== true) {
918
- runtime.usage("--yes is required for automation after reviewing the exact intent.", {
919
- code: "consent_required",
920
- field: "yes",
921
- });
922
- }
923
917
  const references = requestedMachineReferences(input);
924
918
  if (membershipCount === null || references.length !== membershipCount) {
925
919
  runtime.usage("Automation requires --machines with exactly the --nodes membership count.", {
@@ -1257,47 +1251,13 @@ export async function launchClusterMode(
1257
1251
  accessMode
1258
1252
  );
1259
1253
  if (eligibilitySignature(reviewEligibility) !== eligibilitySignature(initialEligibility)) {
1260
- runtime.error(`${label} eligibility changed before review; no cluster was created.`, {
1254
+ runtime.error(`${label} eligibility changed before launch; no cluster was created.`, {
1261
1255
  code: "cluster_eligibility_changed",
1262
1256
  });
1263
1257
  }
1264
- const confirmed = await runtime.review({
1265
- title: `Launch ${label} cluster`,
1266
- terms: [
1267
- { label: "Organization", value: `${organization.name} (${organization.id})` },
1268
- { label: "Capacity reservation", value: reservationId },
1269
- { label: "Access mode", value: label },
1270
- { label: "Network", value: networkMode },
1271
- ...(keyPlan
1272
- ? [
1273
- {
1274
- label: "SSH key",
1275
- value: `${text(reviewedKey.label) ?? "SSH key"} (${text(reviewedKey.fingerprint) ?? text(reviewedKey.id) ?? "new key"})`,
1276
- },
1277
- ]
1278
- : []),
1279
- { label: "Cluster membership", value: String(membershipCount) },
1280
- ...selected.map((machine, index) => ({
1281
- label: `Machine ${index + 1}`,
1282
- value: `${machine.hostname} (${machine.id})`,
1283
- })),
1284
- ...virtualMachineIds.map((virtualMachineId, index) => ({
1285
- label: `VM ${index + 1}`,
1286
- value: `${virtualMachineId} (SSH keys are set aside while it is in the cluster)`,
1287
- })),
1288
- ],
1289
- });
1290
- if (!confirmed) {
1291
- return runtime.result({
1292
- data: { created: false, requested },
1293
- exitCode: 1,
1294
- outcome: { class: "ended", phase: "declined" },
1295
- presentation: { lines: ["No cluster was created."] },
1296
- });
1297
- }
1298
1258
  const currentOrganization = await selectedOrganization(context, runtime);
1299
1259
  if (currentOrganization.id !== organization.id) {
1300
- runtime.error("The selected organization changed after review; no cluster was created.", {
1260
+ runtime.error("The selected organization changed before launch; no cluster was created.", {
1301
1261
  code: "organization_changed",
1302
1262
  exitCode: 2,
1303
1263
  });
@@ -1312,20 +1272,20 @@ export async function launchClusterMode(
1312
1272
  if (racedExisting) {
1313
1273
  const current = await fetchClusterDetail(context.fetch, runtime, racedExisting);
1314
1274
  if (current.network_mode && current.network_mode !== networkMode) {
1315
- runtime.error(`A different ${label} network goal was accepted after review.`, {
1275
+ runtime.error(`A different ${label} network goal was accepted before launch.`, {
1316
1276
  code: "cluster_launch_goal_conflict",
1317
1277
  });
1318
1278
  }
1319
1279
  const currentIds = memberIds(current);
1320
1280
  if (currentIds.length === 0) {
1321
1281
  if (current.lifecycle.phase !== "provisioning") {
1322
- runtime.error(`A non-recoverable empty ${label} cluster appeared after review.`, {
1282
+ runtime.error(`A non-recoverable empty ${label} cluster appeared before launch.`, {
1323
1283
  code: "cluster_launch_goal_conflict",
1324
1284
  });
1325
1285
  }
1326
1286
  } else {
1327
1287
  if (!sameMembers(currentIds, goalIds)) {
1328
- runtime.error(`A different ${label} membership goal was accepted after review.`, {
1288
+ runtime.error(`A different ${label} membership goal was accepted before launch.`, {
1329
1289
  code: "cluster_launch_goal_conflict",
1330
1290
  });
1331
1291
  }
@@ -1618,21 +1578,6 @@ async function addNode(input, deps) {
1618
1578
  if (!machine)
1619
1579
  deps.interaction.usage("Choose one exact eligible machine.", { code: "machine_not_eligible" });
1620
1580
  const goalIds = [...initialMembers, machine.id].sort();
1621
- const confirmed = await deps.interaction.review({
1622
- title: `Add ${clusterModeLabel(prepared.cluster.access_mode)} cluster node`,
1623
- terms: [
1624
- { label: "Cluster", value: prepared.cluster.id },
1625
- { label: "Machine", value: `${machine.hostname} (${machine.id})` },
1626
- { label: "Membership goal", value: String(goalIds.length) },
1627
- ],
1628
- });
1629
- if (!confirmed)
1630
- return deps.interaction.result({
1631
- data: { changed: false, cluster: prepared.cluster },
1632
- exitCode: 1,
1633
- outcome: { class: "ended", phase: "declined" },
1634
- presentation: { lines: ["Cluster membership was not changed."] },
1635
- });
1636
1581
  const current = await fetchClusterDetail(deps.fetch, deps.interaction, prepared.cluster);
1637
1582
  const currentIds = memberIds(current);
1638
1583
  if (sameMembers(currentIds, goalIds)) {
@@ -1643,7 +1588,7 @@ async function addNode(input, deps) {
1643
1588
  });
1644
1589
  }
1645
1590
  if (!sameMembers(currentIds, initialMembers)) {
1646
- deps.interaction.error("Cluster membership changed after review; no node was added.", {
1591
+ deps.interaction.error("Cluster membership changed before adding the node.", {
1647
1592
  code: "cluster_membership_changed",
1648
1593
  });
1649
1594
  }
@@ -1654,7 +1599,7 @@ async function addNode(input, deps) {
1654
1599
  current.access_mode
1655
1600
  );
1656
1601
  if (eligibilitySignature(currentEligibility) !== eligibilitySignature(initialEligibility)) {
1657
- deps.interaction.error("Eligible machines changed after review; no node was added.", {
1602
+ deps.interaction.error("Eligible machines changed before adding the node.", {
1658
1603
  code: "cluster_eligibility_changed",
1659
1604
  });
1660
1605
  }
@@ -1736,22 +1681,6 @@ async function removeNode(input, deps) {
1736
1681
  code: "cluster_member_not_found",
1737
1682
  });
1738
1683
  const goalIds = initialMembers.filter((id) => id !== machine.id);
1739
- const confirmed = await deps.interaction.review({
1740
- title: `Remove ${clusterModeLabel(prepared.cluster.access_mode)} cluster node`,
1741
- terms: [
1742
- { label: "Cluster", value: prepared.cluster.id },
1743
- { label: "Machine", value: `${machine.hostname} (${machine.id})` },
1744
- { label: "Membership goal", value: String(goalIds.length) },
1745
- { label: "Consequence", value: "Workloads and native access on this member stop." },
1746
- ],
1747
- });
1748
- if (!confirmed)
1749
- return deps.interaction.result({
1750
- data: { changed: false, cluster: prepared.cluster },
1751
- exitCode: 1,
1752
- outcome: { class: "ended", phase: "declined" },
1753
- presentation: { lines: ["Cluster membership was not changed."] },
1754
- });
1755
1684
  const current = await fetchClusterDetail(deps.fetch, deps.interaction, prepared.cluster);
1756
1685
  const currentIds = memberIds(current);
1757
1686
  if (sameMembers(currentIds, goalIds)) {
@@ -1762,7 +1691,7 @@ async function removeNode(input, deps) {
1762
1691
  });
1763
1692
  }
1764
1693
  if (!sameMembers(currentIds, initialMembers)) {
1765
- deps.interaction.error("Cluster membership changed after review; no node was removed.", {
1694
+ deps.interaction.error("Cluster membership changed before removing the node.", {
1766
1695
  code: "cluster_membership_changed",
1767
1696
  });
1768
1697
  }
@@ -1836,29 +1765,9 @@ async function teardownCluster(input, deps) {
1836
1765
  });
1837
1766
  }
1838
1767
  const reviewedMembers = memberIds(reviewed);
1839
- const confirmed = await deps.interaction.review({
1840
- title: `Tear down ${clusterModeLabel(reviewed.access_mode)} cluster`,
1841
- terms: [
1842
- { label: "Cluster", value: reviewed.id },
1843
- { label: "Members", value: String(reviewedMembers.length) },
1844
- {
1845
- label: "Native access",
1846
- value: `${clusterNativeAccess(reviewed.access_mode)} access ends.`,
1847
- },
1848
- { label: "Capacity reservation", value: `${reviewed.reservation_id} remains active.` },
1849
- { label: "Charges", value: "Capacity charges continue after cluster teardown." },
1850
- ],
1851
- });
1852
- if (!confirmed)
1853
- return deps.interaction.result({
1854
- data: { changed: false, cluster: reviewed },
1855
- exitCode: 1,
1856
- outcome: { class: "ready", phase: "unchanged" },
1857
- presentation: { lines: ["The cluster remains active. Capacity charges continue."] },
1858
- });
1859
1768
  const current = await fetchClusterDetail(deps.fetch, deps.interaction, reviewed);
1860
1769
  if (!sameMembers(memberIds(current), reviewedMembers)) {
1861
- deps.interaction.error("Cluster membership changed after review; teardown was not submitted.", {
1770
+ deps.interaction.error("Cluster membership changed before teardown was submitted.", {
1862
1771
  code: "cluster_membership_changed",
1863
1772
  });
1864
1773
  }
@@ -15,7 +15,6 @@ const COMMON_OPTIONS = [
15
15
  "reservation",
16
16
  "timeout",
17
17
  "wait",
18
- "yes",
19
18
  ];
20
19
  const KEY_OPTIONS = ["key", "keyId", "label", "publicKey", "publicKeyFile"];
21
20
  const MODE_OPTIONS = new Map([