@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.
- package/README.md +92 -47
- package/package.json +2 -2
- package/src/admin-capacity-reservations.mjs +65 -153
- package/src/admin-command-handlers.mjs +2 -0
- package/src/admin-credits.mjs +57 -0
- package/src/admin-node-lifecycle.mjs +48 -7
- package/src/admin-shell.mjs +113 -27
- package/src/catalog-shell.mjs +13 -21
- package/src/evidence-commands.mjs +30 -12
- package/src/kubernetes-commands.mjs +58 -16
- package/src/launch-mode-handlers.mjs +2 -2
- package/src/storage-commands.mjs +9 -1
- package/src/vm-commands.mjs +3 -3
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +55 -57
- package/vendor/capabilities/capabilities/admin-enrollment.js +3 -3
- package/vendor/capabilities/capabilities/admin.js +9 -7
- package/vendor/capabilities/capabilities/commerce-market.js +179 -104
- package/vendor/capabilities/capabilities/compute.js +4 -2
- package/vendor/capabilities/capabilities/credits.js +78 -0
- package/vendor/capabilities/capabilities/fleet.js +6 -5
- package/vendor/capabilities/capabilities/listings.js +58 -38
- package/vendor/capabilities/capabilities/market-shared.js +2 -0
- package/vendor/capabilities/capabilities/markets.js +254 -221
- package/vendor/capabilities/capabilities/native-listing.js +63 -0
- package/vendor/capabilities/capabilities/organization-invites.js +53 -0
- package/vendor/capabilities/capabilities/reservation-list.js +68 -0
- package/vendor/capabilities/capabilities/staff-shared.js +64 -0
- package/vendor/capabilities/capabilities/users.js +141 -31
- package/vendor/capabilities/cli-catalog.js +8 -2
- package/vendor/capabilities/cli-migrations.js +13 -10
- 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
|
-
?
|
|
1095
|
-
|
|
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
|
-
:
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 = {
|
package/src/storage-commands.mjs
CHANGED
|
@@ -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;
|
package/src/vm-commands.mjs
CHANGED
|
@@ -1191,7 +1191,7 @@ export const vmLaunchModeHandler = {
|
|
|
1191
1191
|
body: {
|
|
1192
1192
|
access_mode: "virtual-machine",
|
|
1193
1193
|
network_mode: "public",
|
|
1194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
32
|
-
vnext: adminHelp(["admin", "capacity-offers", "create"], "Create
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
.
|
|
53
|
-
.describe("
|
|
54
|
-
|
|
55
|
-
|
|
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: "
|
|
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: "/
|
|
72
|
+
http: { method: "POST", path: "/v1/markets/bookings" },
|
|
69
73
|
cli: {
|
|
70
74
|
command: ["admin", "reservations", "create"],
|
|
71
|
-
vnext: adminHelp(["admin", "reservations", "create"], "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
140
|
-
method: "
|
|
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 {
|
|
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
|
|
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] [--
|
|
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
|
-
|
|
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.
|
|
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) {
|