@ornncompute/cli 1.0.25 → 1.0.34

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 (35) hide show
  1. package/README.md +76 -23
  2. package/package.json +1 -1
  3. package/src/admin-capacity-reservations.mjs +68 -43
  4. package/src/admin-enrollment.mjs +14 -13
  5. package/src/admin-node-lifecycle.mjs +10 -13
  6. package/src/admin-shell.mjs +146 -12
  7. package/src/catalog-shell.mjs +58 -45
  8. package/src/device-auth.mjs +1 -1
  9. package/src/evidence-commands.mjs +25 -11
  10. package/src/gpu-catalog.mjs +21 -0
  11. package/src/identity-file.mjs +28 -0
  12. package/src/kubernetes-commands.mjs +12 -6
  13. package/src/machine-reference.mjs +10 -2
  14. package/src/mount-commands.mjs +4 -2
  15. package/src/slurm-commands.mjs +11 -0
  16. package/src/storage-commands.mjs +27 -7
  17. package/src/vm-commands.mjs +42 -7
  18. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
  19. package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
  20. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
  21. package/vendor/capabilities/capabilities/admin.js +7 -7
  22. package/vendor/capabilities/capabilities/commerce-market.js +330 -0
  23. package/vendor/capabilities/capabilities/fleet.js +57 -21
  24. package/vendor/capabilities/capabilities/listings.js +137 -146
  25. package/vendor/capabilities/capabilities/market-shared.js +44 -0
  26. package/vendor/capabilities/capabilities/markets.js +449 -0
  27. package/vendor/capabilities/capabilities/networks.js +7 -0
  28. package/vendor/capabilities/capabilities/tokens.js +12 -9
  29. package/vendor/capabilities/capability.js +2 -1
  30. package/vendor/capabilities/cli-catalog.js +4 -2
  31. package/vendor/capabilities/cli-migrations.js +16 -43
  32. package/vendor/capabilities/cli.js +1 -1
  33. package/vendor/capabilities/spec-catalog.js +26 -3
  34. package/vendor/capabilities/supplier-directory.js +20 -0
  35. package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
@@ -351,7 +351,7 @@ async function reservationMachineCount(runtime, reservationId, signal) {
351
351
  let count = 0;
352
352
  try {
353
353
  const rows = candidateRows(
354
- await runtime.fetch("/v1/cli/storage-targets", {
354
+ await runtime.fetch("/v1/orchestrator/storage-targets", {
355
355
  method: "GET",
356
356
  ...(signal ? { signal } : {}),
357
357
  })
@@ -595,7 +595,9 @@ async function candidates(input, runtime) {
595
595
  "ornn volume-mounts candidates [--limit <1-500>] [--cursor <cursor>]",
596
596
  runtime.interaction
597
597
  );
598
- const rows = candidateRows(await runtime.fetch("/v1/cli/storage-targets", { method: "GET" }));
598
+ const rows = candidateRows(
599
+ await runtime.fetch("/v1/orchestrator/storage-targets", { method: "GET" })
600
+ );
599
601
  const offset = cursorOffset(input.cursor, runtime.interaction);
600
602
  const limit = pageLimit(input.limit, runtime.interaction);
601
603
  if (offset > rows.length) {
@@ -1,6 +1,7 @@
1
1
  import { spawn as spawnProcessDefault } from "node:child_process";
2
2
  import { homedir } from "node:os";
3
3
 
4
+ import { identityFilePermissionProblem } from "./identity-file.mjs";
4
5
  import {
5
6
  clusterModeConnectCandidates,
6
7
  launchClusterMode,
@@ -203,6 +204,16 @@ export function createSlurmConnectModeHandler({ spawnProcess = spawnProcessDefau
203
204
  ssh,
204
205
  };
205
206
  if (runtime.eligibleHuman && !runtime.machine && state.input.print !== true) {
207
+ const identityFile = text(state.input.identityFile);
208
+ const keyProblem = await identityFilePermissionProblem(
209
+ identityFile ? expandHome(identityFile) : null
210
+ );
211
+ if (keyProblem) {
212
+ runtime.usage(keyProblem, {
213
+ code: "identity_file_permissions",
214
+ field: "identityFile",
215
+ });
216
+ }
206
217
  const launched = await spawnSsh(login, spawnProcess);
207
218
  if (launched.code === 130) {
208
219
  return {
@@ -288,12 +288,17 @@ function safeStorageError(error, fallback, { action } = {}) {
288
288
  ? "service_unavailable"
289
289
  : "storage_operation_failed");
290
290
  const unavailable = code === "service_unavailable";
291
- return new StorageCommandError(SAFE_ERROR_MESSAGES[code] ?? fallback, {
292
- action,
293
- code,
294
- exitCode: unavailable ? 6 : 1,
295
- retryable: unavailable,
296
- });
291
+ const ownerMessage = detailOf(error)?.message;
292
+ return new StorageCommandError(
293
+ SAFE_ERROR_MESSAGES[code] ??
294
+ (typeof ownerMessage === "string" && ownerMessage.trim() ? ownerMessage : fallback),
295
+ {
296
+ action,
297
+ code,
298
+ exitCode: unavailable ? 6 : 1,
299
+ retryable: unavailable,
300
+ }
301
+ );
297
302
  }
298
303
 
299
304
  export function idempotencyHeaders(input) {
@@ -828,12 +833,15 @@ async function uploadFile(input, runtime) {
828
833
  title: "Upload volume file",
829
834
  });
830
835
  if (declined) return declined;
836
+ const requestHash = createHash("sha256");
837
+ for await (const chunk of createReadStream(localFile)) requestHash.update(chunk);
838
+ const sha256 = requestHash.digest("hex");
831
839
  let session;
832
840
  try {
833
841
  session = await runtime.fetch(
834
842
  computeEndpoint(`${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/file-upload-url`),
835
843
  {
836
- body: { content_type: contentType, path: remotePath, size_bytes: fileInfo.size },
844
+ body: { content_type: contentType, path: remotePath, size_bytes: fileInfo.size, sha256 },
837
845
  headers: idempotencyHeaders(input),
838
846
  method: "POST",
839
847
  }
@@ -843,6 +851,18 @@ async function uploadFile(input, runtime) {
843
851
  }
844
852
  const uploadId =
845
853
  typeof session?.upload_id === "string" ? safeIdentifier(session.upload_id) : null;
854
+ if (uploadId && session.state === "complete") {
855
+ return runtime.interaction.result({
856
+ data: {
857
+ file: safeFile({ path: remotePath, size_bytes: fileInfo.size }),
858
+ integrity: { sha256 },
859
+ upload_id: uploadId,
860
+ volume: safeVolume(drive),
861
+ },
862
+ outcome: { class: "ready", phase: "uploaded" },
863
+ resume: { kind: "volume-upload", ref: uploadId },
864
+ });
865
+ }
846
866
  if (!uploadId || typeof session?.upload_url !== "string") {
847
867
  if (uploadId) await cancelUpload(runtime, String(drive.id), uploadId);
848
868
  throw new StorageCommandError("Ornn returned an invalid upload session.", {
@@ -3,13 +3,14 @@ import { createHash } from "node:crypto";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { isIP } from "node:net";
6
+ import { identityFilePermissionProblem } from "./identity-file.mjs";
6
7
  import { isRecoverableWaitFailure } from "./interaction-runtime.mjs";
7
8
  import { resolvedMountPath } from "./mount-commands.mjs";
8
9
  import {
9
10
  isReferenceMiss,
10
11
  listActiveDirectReservations,
11
12
  listOwnedActiveMachines,
12
- reservationChoiceLabel as reservationLabel,
13
+ reservationLabeler,
13
14
  resolveMachineReference,
14
15
  } from "./machine-reference.mjs";
15
16
 
@@ -513,6 +514,7 @@ async function resolveReservation(context, input, interaction, expectedMode = nu
513
514
  }
514
515
  let ref = resourceId(candidates[0]);
515
516
  if (candidates.length > 1) {
517
+ const reservationLabel = await reservationLabeler(context.fetch);
516
518
  ref = await interaction.ask({
517
519
  label: "Capacity reservation",
518
520
  choices: candidates.map((reservation) => ({
@@ -534,9 +536,21 @@ async function resolveReservation(context, input, interaction, expectedMode = nu
534
536
  }
535
537
 
536
538
  async function fetchOrganizationKeys(context) {
537
- return activeKeys(
538
- keysFromPayload(await context.fetch("/v1/workflows/compute/nodes/organizations/me/ssh-keys"))
539
- );
539
+ const keys = [];
540
+ const seen = new Set();
541
+ let cursor = null;
542
+ do {
543
+ const query = new URLSearchParams({ status: "active", limit: "500" });
544
+ if (cursor) query.set("cursor", cursor);
545
+ const page = await context.fetch(
546
+ `/v1/workflows/compute/nodes/organizations/me/ssh-keys?${query}`
547
+ );
548
+ keys.push(...keysFromPayload(page));
549
+ cursor = page.next_cursor ?? null;
550
+ if (cursor && seen.has(cursor)) throw new Error("SSH key pagination did not advance.");
551
+ if (cursor) seen.add(cursor);
552
+ } while (cursor);
553
+ return activeKeys(keys);
540
554
  }
541
555
 
542
556
  export function keyInputShapePreflight(input, interaction) {
@@ -1071,6 +1085,7 @@ export const vmLaunchModeHandler = {
1071
1085
  const reviewedRelationshipSnapshot = JSON.stringify(reviewedRelationships);
1072
1086
  const waitByDefault = interaction.eligibleHuman && !interaction.machine;
1073
1087
  const shouldWait = input.noWait ? false : input.wait ? true : waitByDefault;
1088
+ const reservationLabel = await reservationLabeler(context.fetch);
1074
1089
  const reviewAccepted = await interaction.review({
1075
1090
  title: "VM launch review",
1076
1091
  terms: [
@@ -1828,6 +1843,7 @@ async function launchDurableBareMetalWorkspace({
1828
1843
  : input.wait
1829
1844
  ? true
1830
1845
  : interaction.eligibleHuman && !interaction.machine;
1846
+ const reservationLabel = await reservationLabeler(context.fetch);
1831
1847
  const accepted = await interaction.review({
1832
1848
  title: "Durable Bare Metal workspace review",
1833
1849
  terms: [
@@ -2333,6 +2349,7 @@ export const bareMetalLaunchModeHandler = {
2333
2349
  ? true
2334
2350
  : interaction.eligibleHuman && !interaction.machine;
2335
2351
  const allocation = reservationAllocationContext(reservation);
2352
+ const reservationLabel = await reservationLabeler(context.fetch);
2336
2353
  const reviewAccepted = await interaction.review({
2337
2354
  title: "Bare Metal launch review",
2338
2355
  terms: [
@@ -2857,6 +2874,15 @@ function createDirectMachineConnectModeHandler({
2857
2874
  ssh_command: command,
2858
2875
  };
2859
2876
  if (interaction.eligibleHuman && !interaction.machine && input.print !== true) {
2877
+ const keyProblem = await identityFilePermissionProblem(
2878
+ invocation.raw || !text(input.identityFile) ? null : expandPath(input.identityFile)
2879
+ );
2880
+ if (keyProblem) {
2881
+ interaction.usage(keyProblem, {
2882
+ code: "identity_file_permissions",
2883
+ field: "identityFile",
2884
+ });
2885
+ }
2860
2886
  interaction.stderr?.write(
2861
2887
  `Opening SSH to ${displayName}.\nRun: ${command}\nWaiting for SSH to exit. Press Ctrl-C to stop only local SSH; the remote machine keeps running.\n`
2862
2888
  );
@@ -3355,6 +3381,7 @@ async function guidedSwitchReservation(context, interaction) {
3355
3381
  }
3356
3382
  let reference = resourceId(reservations[0]);
3357
3383
  if (reservations.length > 1) {
3384
+ const reservationLabel = await reservationLabeler(context.fetch);
3358
3385
  reference = await interaction.ask({
3359
3386
  label: "Capacity reservation",
3360
3387
  choices: reservations.map((reservation) => ({
@@ -3527,6 +3554,7 @@ async function switchMachine(input, context, interaction) {
3527
3554
  reservationId,
3528
3555
  interaction
3529
3556
  );
3557
+ const reservationLabel = await reservationLabeler(context.fetch);
3530
3558
  if (
3531
3559
  reservationLabel(currentReservation) !== reservationLabel(reservation) ||
3532
3560
  directMachineSnapshot(currentMachines, currentRelationships, currentKeys) !== snapshot
@@ -3988,10 +4016,17 @@ function reservationPosition(input, interaction) {
3988
4016
  export async function executeSshKeyCommand(command, input, context, interaction) {
3989
4017
  if (command === "list") {
3990
4018
  positional(input, 0, "ornn ssh-keys list [--limit <1-500>] [--cursor <cursor>]", interaction);
3991
- const keys = fetchOrganizationKeys(context).then((rows) =>
3992
- rows.sort((left, right) => String(resourceId(left)).localeCompare(String(resourceId(right))))
4019
+ const limit = input.limit ?? 50;
4020
+ const query = new URLSearchParams({ status: "active", limit: String(limit) });
4021
+ if (text(input.cursor)) query.set("cursor", input.cursor);
4022
+ const payload = await context.fetch(
4023
+ `/v1/workflows/compute/nodes/organizations/me/ssh-keys?${query}`
3993
4024
  );
3994
- const page = cursorPage(await keys, input, interaction);
4025
+ const items = activeKeys(keysFromPayload(payload));
4026
+ const page = {
4027
+ items,
4028
+ page: { limit, returned: items.length, next_cursor: payload.next_cursor ?? null },
4029
+ };
3995
4030
  return interaction.result({
3996
4031
  data: { ssh_keys: page.items.map(projectKey) },
3997
4032
  page: page.page,
@@ -16,17 +16,20 @@ function adminHelp(command, outcome, human, automation, review) {
16
16
  const capacityOfferCreateInput = z.object({
17
17
  gpu_type: z.string().min(1),
18
18
  node_count: z.number().int().positive(),
19
- gpus_per_node: z.number().int().positive(),
19
+ gpus_per_node: z.number().int().min(1).max(8),
20
20
  site_nickname: z.string().min(1),
21
21
  available_start_at: z.string().min(1),
22
22
  available_end_at: z.string().min(1),
23
- buy_now_price_per_gpu_hour: z.number().nonnegative(),
23
+ buy_now_price_per_gpu_hour: z.number().positive(),
24
+ cost_per_gpu_hour: z.number().positive().optional(),
25
+ down_payment_cents: z.number().int().nonnegative().optional(),
26
+ access_mode: z.enum(["bare-metal", "virtual-machine"]).optional(),
24
27
  });
25
28
  export const adminCapacityOffersCreateCapability = {
26
29
  ...createListingCapability,
27
30
  cli: {
28
31
  ...createListingCapability.cli,
29
- vnext: adminHelp(["admin", "capacity-offers", "create"], "Create one capacity offer from explicit commercial terms.", "ornn admin capacity-offers create --gpu GPU --nodes COUNT --gpus-per-node COUNT --site SITE --starts-at TIME --ends-at TIME --price-per-gpu-hour USD", "Not supported in machine or redirected execution because offer creation has no durable replay identity.", "GPU, capacity, site, availability window, and price are normalized before default-no consent."),
32
+ vnext: adminHelp(["admin", "capacity-offers", "create"], "Create one primary listing from explicit commercial terms.", "ornn admin capacity-offers create --gpu GPU --nodes COUNT --gpus-per-node COUNT --site SITE --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--supplier-price-per-gpu-hour USD] [--down-payment-cents CENTS] [--access-mode bare-metal|virtual-machine]", "Not supported in machine or redirected execution because offer creation has no durable replay identity.", "GPU, site, availability window, supplier price, spread, and down payment are normalized before default-no consent."),
30
33
  },
31
34
  input: createListingCapability.input,
32
35
  execute: async (ctx, input) => {
@@ -35,89 +38,56 @@ export const adminCapacityOffersCreateCapability = {
35
38
  const result = await createListingCapability.execute(ctx, capacityOfferCreateInput.parse(input));
36
39
  if (!ctx.confirmed || object(result).ok === false)
37
40
  return result;
38
- return { capacity_offer: normalizeCapacityOffer(result) };
41
+ return { listing: result };
39
42
  },
40
43
  };
41
- const reservationCreateInput = z.object({
44
+ const MICRO_USD = 1_000_000;
45
+ export const reservationCreateInput = z.object({
42
46
  organization_id: z.string().uuid(),
43
47
  listing_id: z.string().uuid(),
44
- node_count: z.number().int().positive(),
45
48
  start_at: z.string().min(1),
46
49
  end_at: z.string().min(1),
47
- price_per_gpu_hour: z.string().regex(/^\d+(?:\.\d{1,6})?$/),
50
+ price_per_gpu_hour: z
51
+ .string()
52
+ .regex(/^\d+(?:\.\d{1,6})?$/)
53
+ .describe("USD per GPU-hour, for example 2.50."),
54
+ down_payment_cents: z.number().int().nonnegative().optional(),
55
+ notes: z.string().max(2000).optional(),
48
56
  });
49
57
  function object(value) {
50
58
  return value && typeof value === "object" && !Array.isArray(value)
51
59
  ? value
52
60
  : {};
53
61
  }
54
- function field(value, ...keys) {
55
- for (const key of keys) {
56
- if (value[key] !== undefined)
57
- return value[key];
58
- }
59
- return undefined;
60
- }
61
- function normalizeCapacityOffer(value) {
62
- const row = object(object(value).capacity_offer ?? object(value).listing ?? value);
63
- return {
64
- capacity_offer_id: field(row, "capacity_offer_id", "listing_id", "listingId", "id"),
65
- gpu: field(row, "gpu", "gpu_type", "gpuType"),
66
- node_count: field(row, "node_count", "nodeCount", "capacity_nodes", "capacityNodes"),
67
- gpus_per_node: field(row, "gpus_per_node", "gpusPerNode"),
68
- site: field(row, "site", "site_nickname", "siteNickname", "facility"),
69
- starts_at: field(row, "starts_at", "start_at", "startAt", "available_start_at"),
70
- ends_at: field(row, "ends_at", "end_at", "endAt", "available_end_at"),
71
- price_per_gpu_hour: field(row, "price_per_gpu_hour", "buy_now_price_per_gpu_hour", "buyNowRatePerGpuHour"),
72
- status: field(row, "status", "state"),
73
- };
74
- }
75
- function normalizeReservation(value) {
76
- const row = object(value);
77
- return {
78
- reservation_id: field(row, "reservation_id", "reservationId", "id"),
79
- listing_id: field(row, "listing_id", "listingId"),
80
- organization_id: field(row, "organization_id", "organizationId", "tenant_id", "tenantId"),
81
- node_count: field(row, "node_count", "nodeCount"),
82
- start_at: field(row, "start_at", "startAt"),
83
- end_at: field(row, "end_at", "endAt"),
84
- status: field(row, "status"),
85
- price_per_gpu_hour: field(row, "price_per_gpu_hour", "pricePerGpuHour", "pricePerGpuHr"),
86
- };
87
- }
88
62
  export const adminReservationsCreateCapability = defineCapability({
89
63
  id: "reservations.create",
90
64
  domain: "reservations",
91
65
  roles: ["admin"],
92
- description: "Create one capacity reservation from explicit, reviewed commercial terms.",
66
+ description: "Create one schedule reservation on a listing from explicit, reviewed commercial terms (admin). The reservation covers the listing's nodes for the window.",
93
67
  mutation: "preview-confirm",
94
- http: { method: "POST", path: "/internal/commerce-reservations" },
68
+ http: { method: "POST", path: "/commerce/reservations" },
95
69
  cli: {
96
70
  command: ["admin", "reservations", "create"],
97
- vnext: adminHelp(["admin", "reservations", "create"], "Create one capacity reservation from explicit commercial terms.", "ornn admin reservations create --organization ID --capacity-offer ID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour USD", "Not supported in machine or redirected execution because reservation creation has no durable replay identity.", "Organization, capacity offer, quantity, dates, and price are normalized before default-no consent."),
71
+ vnext: adminHelp(["admin", "reservations", "create"], "Create one schedule reservation from explicit commercial terms.", "ornn admin reservations create --organization ID --capacity-offer ID --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--down-payment-cents CENTS]", "Not supported in machine or redirected execution because reservation creation has no durable replay identity.", "Organization, listing, dates, price, and down payment are normalized before default-no consent."),
98
72
  },
99
- mcp: false,
100
73
  slack: false,
101
74
  input: reservationCreateInput,
102
75
  execute: async (ctx, input) => {
103
76
  const parsed = reservationCreateInput.parse(input);
104
- if (!ctx.confirmed)
105
- return { preview: parsed };
106
- const payload = await ctx.fetch("/internal/commerce-reservations", {
107
- method: "POST",
108
- body: {
109
- tenant_id: parsed.organization_id,
110
- listing_id: parsed.listing_id,
111
- node_count: parsed.node_count,
112
- start_at: parsed.start_at,
113
- end_at: parsed.end_at,
114
- price_per_gpu_hr: parsed.price_per_gpu_hour,
115
- },
116
- });
117
- const body = object(payload);
118
- return {
119
- reservation: normalizeReservation(body.reservation ?? body),
77
+ const body = {
78
+ listingId: parsed.listing_id,
79
+ organizationId: parsed.organization_id,
80
+ startsAt: parsed.start_at,
81
+ endsAt: parsed.end_at,
82
+ pricePerGpuHourMicroUsd: Math.round(Number(parsed.price_per_gpu_hour) * MICRO_USD),
83
+ ...(parsed.down_payment_cents !== undefined
84
+ ? { downPaymentCents: parsed.down_payment_cents }
85
+ : {}),
86
+ ...(parsed.notes ? { notes: parsed.notes } : {}),
120
87
  };
88
+ if (!ctx.confirmed)
89
+ return { preview: body };
90
+ return { reservation: await ctx.fetch("/commerce/reservations", { method: "POST", body }) };
121
91
  },
122
92
  });
123
93
  export const adminCapacityReservationCapabilities = [
@@ -44,7 +44,7 @@ export const adminEnrollmentTokensListCapability = {
44
44
  };
45
45
  },
46
46
  };
47
- export const adminEnrollmentTokenCreateCapability = withVnextBinding(tokensCreateCapability, adminHelp(["admin", "enrollment-tokens", "create"], "Create one expiring enrollment token through an explicit secret export.", "ornn admin enrollment-tokens create --operator UUID --expires-in SECONDS (--print | --output PATH) [--overwrite] [--yes]", "Unsupported: token creation requires an eligible human terminal.", ["canonical operator UUID"], "Review the operator, expiry, and secret destination before minting the token."));
47
+ export const adminEnrollmentTokenCreateCapability = withVnextBinding(tokensCreateCapability, adminHelp(["admin", "enrollment-tokens", "create"], "Create one expiring enrollment token through an explicit secret export.", "ornn admin enrollment-tokens create --supplier UUID --expires-in SECONDS (--print | --output PATH) [--overwrite] [--yes]", "Unsupported: token creation requires an eligible human terminal.", ["canonical supplier organization UUID"], "Review the supplier, expiry, and secret destination before minting the token."));
48
48
  const tokenRevokeInput = z.object({
49
49
  token_id: UUID,
50
50
  expected_created_at: nonEmpty.optional(),
@@ -70,7 +70,7 @@ function tokenMetadata(value) {
70
70
  return null;
71
71
  return {
72
72
  id: row.id,
73
- operator_id: typeof row.operator_id === "string" ? row.operator_id : null,
73
+ organization_id: typeof row.organization_id === "string" ? row.organization_id : null,
74
74
  status: typeof row.status === "string" ? row.status : "unknown",
75
75
  expires_at: typeof row.expires_at === "string" ? row.expires_at : null,
76
76
  created_at: typeof row.created_at === "string" ? row.created_at : null,
@@ -145,7 +145,7 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
145
145
  return {
146
146
  id: parsed.token_id,
147
147
  created_at: current.target.created_at,
148
- operator_id: current.target.operator_id,
148
+ organization_id: current.target.organization_id,
149
149
  expires_at: current.target.expires_at,
150
150
  status: "revoked",
151
151
  reconciled: false,
@@ -156,7 +156,7 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
156
156
  const nodeEnrollInput = z.object({
157
157
  node_id: UUID,
158
158
  host: nonEmpty,
159
- operator_id: UUID,
159
+ organization_id: UUID,
160
160
  facility_id: UUID,
161
161
  filter: z.string().optional(),
162
162
  identity_file: nonEmpty,
@@ -185,12 +185,12 @@ export const adminNodeEnrollCapability = defineCapability({
185
185
  id: "nodes.enroll",
186
186
  domain: "fleet",
187
187
  roles: ["admin"],
188
- description: "Enroll one explicit node over SSH with an explicit operator and facility assignment.",
188
+ description: "Enroll one explicit node over SSH with an explicit supplier and facility assignment.",
189
189
  mutation: "preview-confirm",
190
190
  http: { method: "POST", path: "/internal/remote" },
191
191
  cli: {
192
192
  command: ["admin", "nodes", "enroll"],
193
- vnext: adminHelp(["admin", "nodes", "enroll"], "Install Ornn management on one explicitly assigned node.", "ornn admin nodes enroll <node-id> --host HOST --operator UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose] [--yes]", "Unsupported: remote enrollment requires an eligible human terminal.", ["canonical node UUID", "canonical operator UUID", "canonical facility UUID"], "Review node identity, SSH target, assignment, compartment filter, and replacement of existing management."),
193
+ vnext: adminHelp(["admin", "nodes", "enroll"], "Install Ornn management on one explicitly assigned node.", "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] [--yes]", "Unsupported: remote enrollment requires an eligible human terminal.", ["canonical node UUID", "canonical supplier organization UUID", "canonical facility UUID"], "Review node identity, SSH target, assignment, compartment filter, and replacement of existing management."),
194
194
  },
195
195
  mcp: false,
196
196
  slack: false,
@@ -201,7 +201,7 @@ export const adminNodeEnrollCapability = defineCapability({
201
201
  return {
202
202
  node_id: parsed.node_id,
203
203
  host: parsed.host,
204
- operator_id: parsed.operator_id,
204
+ organization_id: parsed.organization_id,
205
205
  facility_id: parsed.facility_id,
206
206
  filter: parsed.filter,
207
207
  expires_in_seconds: parsed.expires_in_seconds,
@@ -214,7 +214,7 @@ export const adminNodeEnrollCapability = defineCapability({
214
214
  const minted = (await ctx.fetch("/enrollment-tokens", {
215
215
  method: "POST",
216
216
  body: {
217
- operator_id: parsed.operator_id,
217
+ organization_id: parsed.organization_id,
218
218
  expires_in_seconds: parsed.expires_in_seconds,
219
219
  },
220
220
  }));
@@ -248,7 +248,7 @@ export const adminNodeEnrollCapability = defineCapability({
248
248
  return {
249
249
  node_id: parsed.node_id,
250
250
  host: parsed.host,
251
- operator_id: parsed.operator_id,
251
+ organization_id: parsed.organization_id,
252
252
  facility_id: parsed.facility_id,
253
253
  filter: parsed.filter,
254
254
  management: "replaced",
@@ -83,7 +83,7 @@ export const adminNodeRebootCapability = defineCapability({
83
83
  http: { method: "POST", path: "/nodes/{id}/reboot" },
84
84
  cli: {
85
85
  command: ["admin", "nodes", "reboot"],
86
- vnext: adminLifecycleHelp(["admin", "nodes", "reboot"], "Reboot one exact live machine while preserving local data and access configuration.", "ornn admin nodes reboot <node-ref> [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: reboot requires an eligible human terminal.", "Node and live-machine identities, operator, facility, capacity assignment, and preserved access."),
86
+ vnext: adminLifecycleHelp(["admin", "nodes", "reboot"], "Reboot one exact live machine while preserving local data and access configuration.", "ornn admin nodes reboot <node-ref> [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: reboot requires an eligible human terminal.", "Node and live-machine identities, supplier, facility, capacity assignment, and preserved access."),
87
87
  },
88
88
  mcp: false,
89
89
  slack: false,
@@ -99,7 +99,7 @@ export const adminNodeHardResetCapability = defineCapability({
99
99
  http: { method: "POST", path: "/nodes/{id}/reboot" },
100
100
  cli: {
101
101
  command: ["admin", "nodes", "hard-reset"],
102
- vnext: adminLifecycleHelp(["admin", "nodes", "hard-reset"], "Erase local tenant data and hard reset one exact live machine.", "ornn admin nodes hard-reset <node-ref> --erase-local-data [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: hard reset requires an eligible human terminal.", "Node and live-machine identities, operator, facility, capacity assignment, erased local data, and restored keys."),
102
+ vnext: adminLifecycleHelp(["admin", "nodes", "hard-reset"], "Erase local tenant data and hard reset one exact live machine.", "ornn admin nodes hard-reset <node-ref> --erase-local-data [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: hard reset requires an eligible human terminal.", "Node and live-machine identities, supplier, facility, capacity assignment, erased local data, and restored keys."),
103
103
  },
104
104
  mcp: false,
105
105
  slack: false,
@@ -115,7 +115,7 @@ export const adminNodeSuspendManagementCapability = defineCapability({
115
115
  http: { method: "POST", path: "/internal/nodes/{id}/off-grid" },
116
116
  cli: {
117
117
  command: ["admin", "nodes", "suspend-management"],
118
- vnext: adminLifecycleHelp(["admin", "nodes", "suspend-management"], "Remove Ornn management and managed access from one powered-on node.", "ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--yes] [--no-wait]", "ornn admin nodes suspend-management <node-id> --yes --idempotency-key KEY --format json", "Node, operator, facility, capacity assignment, management removal, and managed-access loss."),
118
+ vnext: adminLifecycleHelp(["admin", "nodes", "suspend-management"], "Remove Ornn management and managed access from one powered-on node.", "ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--yes] [--no-wait]", "ornn admin nodes suspend-management <node-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, capacity assignment, management removal, and managed-access loss."),
119
119
  },
120
120
  mcp: false,
121
121
  slack: false,
@@ -145,7 +145,7 @@ export const adminNodeResumeManagementCapability = defineCapability({
145
145
  http: { method: "POST", path: "/internal/nodes/{id}/on-grid" },
146
146
  cli: {
147
147
  command: ["admin", "nodes", "resume-management"],
148
- vnext: adminLifecycleHelp(["admin", "nodes", "resume-management"], "Restore Ornn management and managed access on one off-grid node.", "ornn admin nodes resume-management <node-ref> [--yes] [--no-wait]", "ornn admin nodes resume-management <node-id> --yes --idempotency-key KEY --format json", "Node, operator, facility, capacity assignment, remote re-enrollment, and restored managed access."),
148
+ vnext: adminLifecycleHelp(["admin", "nodes", "resume-management"], "Restore Ornn management and managed access on one off-grid node.", "ornn admin nodes resume-management <node-ref> [--yes] [--no-wait]", "ornn admin nodes resume-management <node-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, capacity assignment, remote re-enrollment, and restored managed access."),
149
149
  },
150
150
  mcp: false,
151
151
  slack: false,
@@ -173,7 +173,7 @@ export const adminNodeTerminateCapability = defineCapability({
173
173
  http: { method: "POST", path: "/internal/nodes/{id}/terminate" },
174
174
  cli: {
175
175
  command: ["admin", "nodes", "terminate"],
176
- vnext: adminLifecycleHelp(["admin", "nodes", "terminate"], "End managed access and permanently delete one exact node record.", "ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--yes]", "ornn admin nodes terminate <node-id> --delete-node-record --yes --idempotency-key KEY --format json", "Node, operator, facility, reservation/capacity, host cleanup, node-record deletion, and access loss."),
176
+ vnext: adminLifecycleHelp(["admin", "nodes", "terminate"], "End managed access and permanently delete one exact node record.", "ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--yes]", "ornn admin nodes terminate <node-id> --delete-node-record --yes --idempotency-key KEY --format json", "Node, supplier, facility, reservation/capacity, host cleanup, node-record deletion, and access loss."),
177
177
  },
178
178
  mcp: false,
179
179
  slack: false,
@@ -204,7 +204,7 @@ export const adminNodeAssignCapability = defineCapability({
204
204
  http: { method: "POST", path: "/internal/nodes/{id}/assign" },
205
205
  cli: {
206
206
  command: ["admin", "nodes", "assign"],
207
- vnext: adminLifecycleHelp(["admin", "nodes", "assign"], "Assign one exact node to one exact capacity reservation.", "ornn admin nodes assign <node-ref> --reservation <reservation-id> [--yes] [--no-wait]", "ornn admin nodes assign <node-id> --reservation <reservation-id> --yes --idempotency-key KEY --format json", "Node, operator, facility, previous and requested capacity assignments, and tenant access change."),
207
+ vnext: adminLifecycleHelp(["admin", "nodes", "assign"], "Assign one exact node to one exact capacity reservation.", "ornn admin nodes assign <node-ref> --reservation <reservation-id> [--yes] [--no-wait]", "ornn admin nodes assign <node-id> --reservation <reservation-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, previous and requested capacity assignments, and tenant access change."),
208
208
  },
209
209
  mcp: false,
210
210
  slack: false,
@@ -1,13 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { defineCapability } from "../capability.js";
3
- import { listFacilitiesCapability, listNodesCapability, listOperatorsCapability } from "./fleet.js";
3
+ import { listFacilitiesCapability, listNodesCapability, listSuppliersCapability } from "./fleet.js";
4
4
  import { logsLatestCapability, telemetryLatestCapability } from "./observability.js";
5
5
  import { tokensListCapability } from "./tokens.js";
6
6
  import { usersQueryCapability } from "./users.js";
7
7
  import { adminCapacityOffersCreateCapability } from "./admin-capacity-reservations.js";
8
8
  export const ADMIN_CLI_ROOTS = [
9
9
  { name: "users" },
10
- { name: "operators" },
10
+ { name: "suppliers" },
11
11
  { name: "facilities" },
12
12
  { name: "nodes" },
13
13
  { name: "enrollment-tokens" },
@@ -37,9 +37,9 @@ function withAdminBinding(capability, help) {
37
37
  };
38
38
  }
39
39
  export const adminUsersSearchCapability = withAdminBinding(usersQueryCapability, adminHelp(["admin", "users", "search"], "Search the staff-authorized user directory.", "ornn admin users search [--name NAME] [--email EMAIL] [--organization NAME] [--limit 50] [--cursor CURSOR]", "ornn admin users search --email EMAIL --limit 50 [--cursor CURSOR] --format json"));
40
- export const adminOperatorsListCapability = withAdminBinding(listOperatorsCapability, adminHelp(["admin", "operators", "list"], "List operators visible to staff.", "ornn admin operators list [--limit 50] [--cursor CURSOR]", "ornn admin operators list --limit 50 --format json"));
41
- export const adminFacilitiesListCapability = withAdminBinding(listFacilitiesCapability, adminHelp(["admin", "facilities", "list"], "List facilities visible to staff.", "ornn admin facilities list [--operator OPERATOR] [--limit 50] [--cursor CURSOR]", "ornn admin facilities list --limit 50 --format json"));
42
- export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--operator OPERATOR] [--facility FACILITY] [--status STATUS] [--email EMAIL] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
40
+ export const adminSuppliersListCapability = withAdminBinding(listSuppliersCapability, adminHelp(["admin", "suppliers", "list"], "List supplier organizations visible to staff.", "ornn admin suppliers list [--limit 50] [--cursor CURSOR]", "ornn admin suppliers list --limit 50 --format json"));
41
+ export const adminFacilitiesListCapability = withAdminBinding(listFacilitiesCapability, adminHelp(["admin", "facilities", "list"], "List facilities visible to staff.", "ornn admin facilities list [--supplier UUID] [--limit 50] [--cursor CURSOR]", "ornn admin facilities list --limit 50 --format json"));
42
+ export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--supplier UUID] [--facility FACILITY] [--status STATUS] [--email EMAIL] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
43
43
  const nodeInput = z.object({ node_id: z.string().min(1) });
44
44
  export const adminNodeHealthCapability = defineCapability({
45
45
  id: "nodes.health",
@@ -65,7 +65,7 @@ export const adminNodeLogsCapability = withAdminBinding(logsLatestCapability, ad
65
65
  export const adminEnrollmentTokensListCapability = withAdminBinding(tokensListCapability, adminHelp(["admin", "enrollment-tokens", "list"], "List enrollment-token metadata without token bytes.", "ornn admin enrollment-tokens list [--limit 50] [--cursor CURSOR]", "ornn admin enrollment-tokens list --limit 50 --format json"));
66
66
  export const adminCapabilities = [
67
67
  adminUsersSearchCapability,
68
- adminOperatorsListCapability,
68
+ adminSuppliersListCapability,
69
69
  adminFacilitiesListCapability,
70
70
  adminNodesListCapability,
71
71
  adminNodeHealthCapability,
@@ -78,7 +78,7 @@ const replacements = {
78
78
  "facilities.list": adminFacilitiesListCapability,
79
79
  "logs.latest": adminNodeLogsCapability,
80
80
  "nodes.list": adminNodesListCapability,
81
- "operator.list": adminOperatorsListCapability,
81
+ "supplier.list": adminSuppliersListCapability,
82
82
  "telemetry.latest": adminNodeMetricsCapability,
83
83
  "tokens.list": adminEnrollmentTokensListCapability,
84
84
  "users.query": adminUsersSearchCapability,