@ornncompute/cli 1.0.35 → 1.0.62

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 (31) hide show
  1. package/README.md +92 -47
  2. package/package.json +2 -2
  3. package/src/admin-capacity-reservations.mjs +65 -153
  4. package/src/admin-command-handlers.mjs +2 -0
  5. package/src/admin-credits.mjs +57 -0
  6. package/src/admin-node-lifecycle.mjs +48 -7
  7. package/src/admin-shell.mjs +113 -27
  8. package/src/catalog-shell.mjs +13 -21
  9. package/src/evidence-commands.mjs +30 -12
  10. package/src/kubernetes-commands.mjs +58 -16
  11. package/src/launch-mode-handlers.mjs +2 -2
  12. package/src/storage-commands.mjs +9 -1
  13. package/src/vm-commands.mjs +3 -3
  14. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +55 -57
  15. package/vendor/capabilities/capabilities/admin-enrollment.js +3 -3
  16. package/vendor/capabilities/capabilities/admin.js +9 -7
  17. package/vendor/capabilities/capabilities/commerce-market.js +179 -104
  18. package/vendor/capabilities/capabilities/compute.js +4 -2
  19. package/vendor/capabilities/capabilities/credits.js +78 -0
  20. package/vendor/capabilities/capabilities/fleet.js +6 -5
  21. package/vendor/capabilities/capabilities/listings.js +58 -38
  22. package/vendor/capabilities/capabilities/market-shared.js +2 -0
  23. package/vendor/capabilities/capabilities/markets.js +254 -221
  24. package/vendor/capabilities/capabilities/native-listing.js +63 -0
  25. package/vendor/capabilities/capabilities/organization-invites.js +53 -0
  26. package/vendor/capabilities/capabilities/reservation-list.js +68 -0
  27. package/vendor/capabilities/capabilities/staff-shared.js +64 -0
  28. package/vendor/capabilities/capabilities/users.js +141 -31
  29. package/vendor/capabilities/cli-catalog.js +8 -2
  30. package/vendor/capabilities/cli-migrations.js +13 -10
  31. package/vendor/capabilities/software-catalog.js +52 -6
@@ -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)],
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
- import { defineCapability } from "../capability.js";
3
- import { createListingCapability } from "./listings.js";
2
+ import { defineCapability, preview } from "../capability.js";
3
+ import { nativeListingCreateInput } from "./native-listing.js";
4
+ import { idempotencyHeaders, microUsd } from "./market-shared.js";
4
5
  function adminHelp(command, outcome, human, automation, review) {
5
6
  return {
6
7
  audience: "admin",
@@ -13,81 +14,78 @@ function adminHelp(command, outcome, human, automation, review) {
13
14
  ...(review ? { review } : {}),
14
15
  };
15
16
  }
16
- const capacityOfferCreateInput = z.object({
17
- gpu_type: z.string().min(1),
18
- node_count: z.number().int().positive(),
19
- gpus_per_node: z.number().int().min(1).max(8),
20
- site_nickname: z.string().min(1),
21
- available_start_at: z.string().min(1),
22
- available_end_at: z.string().min(1),
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(),
27
- });
28
- export const adminCapacityOffersCreateCapability = {
29
- ...createListingCapability,
17
+ export const adminCapacityOffersCreateCapability = defineCapability({
18
+ id: "listing.create",
19
+ domain: "listings",
20
+ roles: ["admin"],
21
+ description: "Create native capacity with explicit organization/facility IDs, hardware, availability and advertised terms. Returns {listing, terms}. Requires an idempotencyKey reused on retries and confirm: true.",
22
+ mutation: "preview-confirm",
23
+ http: { method: "POST", path: "/commerce/v2/listings" },
24
+ input: nativeListingCreateInput,
25
+ slack: false,
30
26
  cli: {
31
- ...createListingCapability.cli,
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."),
27
+ command: ["admin", "capacity-offers", "create"],
28
+ vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --capacity-node-count COUNT --gpus-per-node COUNT --facility-id UUID --provisioning-type bare-metal|virtual-machine --state draft|available --terms JSON --idempotency-key KEY [--organization-id UUID] [--available-at TIME --available-until TIME] [--days-to-rfs DAYS]", "Requires an eligible human terminal; reuse --idempotency-key on retries.", "Review the native parent fields and each financial term before default-no consent."),
33
29
  },
34
- input: createListingCapability.input,
35
30
  execute: async (ctx, input) => {
36
- if (!ctx.cli)
37
- return createListingCapability.execute(ctx, createListingCapability.input.parse(input));
38
- const result = await createListingCapability.execute(ctx, capacityOfferCreateInput.parse(input));
39
- if (!ctx.confirmed || object(result).ok === false)
40
- return result;
41
- return { listing: result };
31
+ const { confirm: _confirm, idempotencyKey, ...body } = nativeListingCreateInput.parse(input);
32
+ if (!ctx.confirmed)
33
+ return preview("listing.create", body);
34
+ return ctx.fetch("/commerce/v2/listings", {
35
+ method: "POST",
36
+ body,
37
+ headers: idempotencyHeaders(ctx, idempotencyKey),
38
+ });
42
39
  },
43
- };
44
- const MICRO_USD = 1_000_000;
40
+ });
41
+ // Markets books whole-second windows; a fractional digit would shift or refuse one.
42
+ const wholeSecondInstant = z.iso
43
+ .datetime({ offset: true })
44
+ .refine((value) => !/\.\d*[1-9]/.test(value), "Use a time in whole seconds.");
45
45
  export const reservationCreateInput = z.object({
46
- organization_id: z.string().uuid(),
47
- listing_id: z.string().uuid(),
48
- start_at: z.string().min(1),
49
- end_at: z.string().min(1),
50
- price_per_gpu_hour: z
46
+ organizationId: z.string().uuid().describe("The customer organization the window is booked for."),
47
+ listingId: z
48
+ .string()
49
+ .uuid()
50
+ .describe("The listing's capacityListingId. Staff may book any available listing."),
51
+ listingTermId: z
51
52
  .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(),
53
+ .uuid()
54
+ .describe("The listingTermsId of a non-renewable Reserved term on that listing."),
55
+ nodes: z.number().int().positive().max(2_147_483_647).describe("Whole nodes to book."),
56
+ startsAt: wholeSecondInstant.describe("RFC 3339 start of the window in whole seconds, no more than five minutes ago."),
57
+ endsAt: wholeSecondInstant.describe("RFC 3339 end of the window in whole seconds, at least an hour after the start and within the term's duration range."),
58
+ pricePerGpuHourMicroUsd: microUsd.describe("The term's price in micro-USD per GPU-hour. A changed price refuses the booking."),
59
+ idempotencyKey: z
60
+ .string()
61
+ .trim()
62
+ .min(1)
63
+ .max(200)
64
+ .describe("Reuse this key when retrying the same booking."),
56
65
  });
57
- function object(value) {
58
- return value && typeof value === "object" && !Array.isArray(value)
59
- ? value
60
- : {};
61
- }
62
66
  export const adminReservationsCreateCapability = defineCapability({
63
67
  id: "reservations.create",
64
68
  domain: "reservations",
65
69
  roles: ["admin"],
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.",
70
+ description: "Book a window of a listing term for a customer organization (admin). Markets holds the GPUs until the window starts, then settles a Commerce reservation with the booking's ID. Returns {bookings}.",
67
71
  mutation: "preview-confirm",
68
- http: { method: "POST", path: "/commerce/reservations" },
72
+ http: { method: "POST", path: "/v1/markets/bookings" },
69
73
  cli: {
70
74
  command: ["admin", "reservations", "create"],
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."),
75
+ vnext: adminHelp(["admin", "reservations", "create"], "Book a listing term window for a customer organization.", "ornn admin reservations create --organization-id UUID --listing-id UUID --listing-term-id UUID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour-micro-usd N --idempotency-key KEY", "Requires an eligible human terminal. Reuse --idempotency-key when retrying the same booking.", "Customer organization, listing, term, nodes, window, price, and idempotency key are reviewed before default-no consent."),
72
76
  },
73
77
  slack: false,
74
78
  input: reservationCreateInput,
75
79
  execute: async (ctx, input) => {
76
- const parsed = reservationCreateInput.parse(input);
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 } : {}),
87
- };
80
+ const { organizationId, idempotencyKey, ...leg } = reservationCreateInput.parse(input);
81
+ const body = { organizationId, legs: [leg] };
88
82
  if (!ctx.confirmed)
89
83
  return { preview: body };
90
- return { reservation: await ctx.fetch("/commerce/reservations", { method: "POST", body }) };
84
+ return ctx.fetch("/v1/markets/bookings", {
85
+ method: "POST",
86
+ body,
87
+ headers: idempotencyHeaders(ctx, idempotencyKey),
88
+ });
91
89
  },
92
90
  });
93
91
  export const adminCapacityReservationCapabilities = [
@@ -109,7 +109,7 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
109
109
  roles: ["admin"],
110
110
  description: "Revoke one exact enrollment-token generation after reconciling its durable goal.",
111
111
  mutation: "preview-confirm",
112
- http: { method: "POST", path: "/enrollment/tokens/{id}/revoke" },
112
+ http: { method: "DELETE", path: "/enrollment-tokens/{id}" },
113
113
  cli: {
114
114
  command: ["admin", "enrollment-tokens", "revoke"],
115
115
  vnext: adminHelp(["admin", "enrollment-tokens", "revoke"], "Revoke one exact enrollment-token generation.", "ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--yes]", "ornn admin enrollment-tokens revoke <token-id> --expected-created-at TIME --yes --idempotency-key KEY --format json", ["canonical token UUID", "token created_at generation"], "Review the exact token generation and the loss of future enrollment access."),
@@ -136,8 +136,8 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
136
136
  if (!current.target.created_at) {
137
137
  throw codedError("token_identity_unavailable", `Enrollment token ${parsed.token_id} has no stable creation identity and cannot be revoked safely.`);
138
138
  }
139
- const revoked = (await ctx.fetch(`/enrollment/tokens/${encodeURIComponent(parsed.token_id)}/revoke`, {
140
- method: "POST",
139
+ const revoked = (await ctx.fetch(`/enrollment-tokens/${encodeURIComponent(parsed.token_id)}`, {
140
+ method: "DELETE",
141
141
  ...(parsed.idempotency_key
142
142
  ? { headers: { "Idempotency-Key": parsed.idempotency_key } }
143
143
  : {}),
@@ -3,10 +3,10 @@ import { defineCapability } from "../capability.js";
3
3
  import { listFacilitiesCapability, listNodesCapability, listSuppliersCapability } from "./fleet.js";
4
4
  import { logsLatestCapability, telemetryLatestCapability } from "./observability.js";
5
5
  import { tokensListCapability } from "./tokens.js";
6
- import { usersQueryCapability } from "./users.js";
7
- import { adminCapacityOffersCreateCapability } from "./admin-capacity-reservations.js";
6
+ import { organizationListCapability, usersListCapability } from "./users.js";
8
7
  export const ADMIN_CLI_ROOTS = [
9
8
  { name: "users" },
9
+ { name: "organizations" },
10
10
  { name: "suppliers" },
11
11
  { name: "facilities" },
12
12
  { name: "nodes" },
@@ -36,10 +36,11 @@ function withAdminBinding(capability, help) {
36
36
  },
37
37
  };
38
38
  }
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"));
39
+ export const adminUsersListCapability = withAdminBinding(usersListCapability, adminHelp(["admin", "users", "list"], "List users belonging to organizations in the Authorization directory.", "ornn admin users list [--query QUERY] [--organization UUID] [--limit 25] [--offset OFFSET] [--cursor CURSOR]", "ornn admin users list --query QUERY --limit 25 [--cursor CURSOR] --format json"));
40
+ export const adminOrganizationsListCapability = withAdminBinding(organizationListCapability, adminHelp(["admin", "organizations", "list"], "List organizations in the Authorization directory.", "ornn admin organizations list [--query QUERY] [--role consumer|supplier] [--approved true|false] [--ids UUID,...] [--limit 25] [--offset OFFSET] [--cursor CURSOR]", "ornn admin organizations list --query QUERY --limit 25 [--cursor CURSOR] --format json"));
40
41
  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
42
  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
+ 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] [--reservation UUID] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
43
44
  const nodeInput = z.object({ node_id: z.string().min(1) });
44
45
  export const adminNodeHealthCapability = defineCapability({
45
46
  id: "nodes.health",
@@ -64,7 +65,8 @@ export const adminNodeMetricsCapability = withAdminBinding(telemetryLatestCapabi
64
65
  export const adminNodeLogsCapability = withAdminBinding(logsLatestCapability, adminHelp(["admin", "nodes", "logs"], "Read one bounded node log page.", "ornn admin nodes logs <node-id> [--stream kernel|serial] [--count 200] [--before TIME] [--cursor CURSOR]", "ornn admin nodes logs <node-id> --stream kernel --count 200 [--before TIME] [--cursor CURSOR] --format json", ["canonical node ID"]));
65
66
  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
67
  export const adminCapabilities = [
67
- adminUsersSearchCapability,
68
+ adminUsersListCapability,
69
+ adminOrganizationsListCapability,
68
70
  adminSuppliersListCapability,
69
71
  adminFacilitiesListCapability,
70
72
  adminNodesListCapability,
@@ -74,14 +76,14 @@ export const adminCapabilities = [
74
76
  adminEnrollmentTokensListCapability,
75
77
  ];
76
78
  const replacements = {
77
- "listings.create": adminCapacityOffersCreateCapability,
78
79
  "facilities.list": adminFacilitiesListCapability,
79
80
  "logs.latest": adminNodeLogsCapability,
80
81
  "nodes.list": adminNodesListCapability,
81
82
  "supplier.list": adminSuppliersListCapability,
82
83
  "telemetry.latest": adminNodeMetricsCapability,
83
84
  "tokens.list": adminEnrollmentTokensListCapability,
84
- "users.query": adminUsersSearchCapability,
85
+ "users.list": adminUsersListCapability,
86
+ "organization.list": adminOrganizationsListCapability,
85
87
  };
86
88
  /** Applies canonical admin CLI bindings without changing MCP or Slack identities. */
87
89
  export function bindAdminCapabilities(capabilities) {