@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.
- package/README.md +76 -23
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +68 -43
- package/src/admin-enrollment.mjs +14 -13
- package/src/admin-node-lifecycle.mjs +10 -13
- package/src/admin-shell.mjs +146 -12
- package/src/catalog-shell.mjs +58 -45
- package/src/device-auth.mjs +1 -1
- package/src/evidence-commands.mjs +25 -11
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/kubernetes-commands.mjs +12 -6
- package/src/machine-reference.mjs +10 -2
- package/src/mount-commands.mjs +4 -2
- package/src/slurm-commands.mjs +11 -0
- package/src/storage-commands.mjs +27 -7
- package/src/vm-commands.mjs +42 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
- package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
- package/vendor/capabilities/capabilities/admin.js +7 -7
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/fleet.js +57 -21
- package/vendor/capabilities/capabilities/listings.js +137 -146
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/networks.js +7 -0
- package/vendor/capabilities/capabilities/tokens.js +12 -9
- package/vendor/capabilities/capability.js +2 -1
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +16 -43
- package/vendor/capabilities/cli.js +1 -1
- package/vendor/capabilities/spec-catalog.js +26 -3
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
import { consent, flag, idempotencyHeaders, microUsdLabel, positionals, query, seg, } from "./market-shared.js";
|
|
4
|
+
// Commerce market verbs: one capability per role-aware `/commerce` route.
|
|
5
|
+
// Commerce decides from the caller's session what each listing or
|
|
6
|
+
// reservation reveals; these verbs return its JSON unchanged.
|
|
7
|
+
function help(command, outcome, human, automation, related, order, acceptedReferences = []) {
|
|
8
|
+
return {
|
|
9
|
+
arguments: "semantic",
|
|
10
|
+
audience: "customer",
|
|
11
|
+
category: "resource",
|
|
12
|
+
command,
|
|
13
|
+
outcome,
|
|
14
|
+
human,
|
|
15
|
+
automation,
|
|
16
|
+
acceptedReferences,
|
|
17
|
+
related,
|
|
18
|
+
order,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const microUsd = z.coerce.number().int().positive();
|
|
22
|
+
const pageLimit = z.coerce.number().int().min(1).max(200).optional();
|
|
23
|
+
const nodeIdList = z.preprocess((value) => typeof value === "string"
|
|
24
|
+
? value
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((part) => part.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
: value, z.array(z.string().uuid()).min(1));
|
|
29
|
+
export const listingsListInput = z.object({
|
|
30
|
+
$positionals: positionals,
|
|
31
|
+
kind: z.enum(["primary", "secondary"]).optional(),
|
|
32
|
+
state: z.enum(["available", "internal", "delisted"]).optional(),
|
|
33
|
+
sourceReservationId: z.string().uuid().optional(),
|
|
34
|
+
sourceListingId: z.string().uuid().optional(),
|
|
35
|
+
limit: pageLimit,
|
|
36
|
+
cursor: z.string().min(1).optional(),
|
|
37
|
+
});
|
|
38
|
+
export const listingsListCapability = defineCapability({
|
|
39
|
+
id: "listings.list",
|
|
40
|
+
domain: "listings",
|
|
41
|
+
roles: ["user"],
|
|
42
|
+
description: "List Commerce listings. Primary listings are shown to approved organizations; secondary listings (relists) to every signed-in organization. Prices are micro-USD per GPU-hour.",
|
|
43
|
+
mutation: "read",
|
|
44
|
+
http: { method: "GET", path: "/commerce/listings" },
|
|
45
|
+
input: listingsListInput,
|
|
46
|
+
cli: {
|
|
47
|
+
command: ["capacity-offers", "list"],
|
|
48
|
+
vnext: help(["capacity-offers", "list"], "List primary and secondary listings with bounded pagination and no supplier provenance.", "ornn capacity-offers list [--kind primary|secondary] [--source-reservation-id <id>] [--limit <1-200>] [--cursor <opaque>]", "ornn capacity-offers list --limit 50 --format json", ["capacity-offers show", "schedules list"], 20, ["opaque page cursor"]),
|
|
49
|
+
},
|
|
50
|
+
slack: false,
|
|
51
|
+
execute: async (ctx, input) => {
|
|
52
|
+
const parsed = listingsListInput.parse(input ?? {});
|
|
53
|
+
return ctx.fetch(`/commerce/listings${query({
|
|
54
|
+
kind: parsed.kind,
|
|
55
|
+
state: parsed.state,
|
|
56
|
+
sourceReservationId: parsed.sourceReservationId,
|
|
57
|
+
sourceListingId: parsed.sourceListingId,
|
|
58
|
+
limit: parsed.limit,
|
|
59
|
+
cursor: parsed.cursor,
|
|
60
|
+
})}`, { method: "GET" });
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
export const listingShowInput = z.object({
|
|
64
|
+
$positionals: positionals,
|
|
65
|
+
listingId: z.string().uuid(),
|
|
66
|
+
});
|
|
67
|
+
export const listingShowCapability = defineCapability({
|
|
68
|
+
id: "listings.show",
|
|
69
|
+
domain: "listings",
|
|
70
|
+
roles: ["user"],
|
|
71
|
+
description: "Show one Commerce listing: specs, window, node count and its buyer price.",
|
|
72
|
+
mutation: "read",
|
|
73
|
+
http: { method: "GET", path: "/commerce/listings/{listingId}" },
|
|
74
|
+
input: listingShowInput,
|
|
75
|
+
cli: {
|
|
76
|
+
command: ["capacity-offers", "show"],
|
|
77
|
+
vnext: help(["capacity-offers", "show"], "Inspect one listing's hardware, facility, window and price.", "ornn capacity-offers show --listing-id <id>", "ornn capacity-offers show --listing-id <id> --format json", ["capacity-offers list", "schedules show"], 21, ["listing ID"]),
|
|
78
|
+
},
|
|
79
|
+
slack: false,
|
|
80
|
+
execute: async (ctx, input) => {
|
|
81
|
+
const parsed = listingShowInput.parse(input);
|
|
82
|
+
return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "GET" });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
export const listingRelistInput = z
|
|
86
|
+
.object({
|
|
87
|
+
$positionals: positionals,
|
|
88
|
+
sourceReservationId: z
|
|
89
|
+
.string()
|
|
90
|
+
.uuid()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("A confirmed or active schedule reservation you hold."),
|
|
93
|
+
sourceListingId: z
|
|
94
|
+
.string()
|
|
95
|
+
.uuid()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe("A primary listing you own (suppliers relist their own listings)."),
|
|
98
|
+
nodeIds: nodeIdList.describe("The nodes to relist: a subset of the source's nodes (comma-separated on the CLI)."),
|
|
99
|
+
startsAt: z.string().min(1).describe("RFC 3339 start, inside the source's window."),
|
|
100
|
+
endsAt: z.string().min(1).describe("RFC 3339 end, inside the source's window."),
|
|
101
|
+
askPricePerGpuHourMicroUsd: microUsd.describe("Asking price in micro-USD per GPU-hour, the listing's only price: buyers buy any open window at it, and idle time sells as spot at it."),
|
|
102
|
+
sellAtMarketFirst: flag
|
|
103
|
+
.default(false)
|
|
104
|
+
.describe("Sell each node's GPUs into resting spot bids before asking at the asking price."),
|
|
105
|
+
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
106
|
+
confirm: z.boolean().optional(),
|
|
107
|
+
})
|
|
108
|
+
.refine((relist) => (relist.sourceReservationId === undefined) !== (relist.sourceListingId === undefined), {
|
|
109
|
+
message: "Give exactly one of sourceReservationId or sourceListingId.",
|
|
110
|
+
path: ["sourceReservationId"],
|
|
111
|
+
});
|
|
112
|
+
export const listingRelistCapability = defineCapability({
|
|
113
|
+
id: "listings.relist",
|
|
114
|
+
domain: "listings",
|
|
115
|
+
roles: ["user"],
|
|
116
|
+
description: "Relist a time slice and/or node subset of a schedule reservation you hold (or a primary listing you own) as a secondary listing with an asking price. " +
|
|
117
|
+
"Buyers buy any open window of it at the asking price, and its unsold time goes on the spot book. Fails with node_window_sold if those nodes are already sold for part of the window, or relist_overlaps if another live relist of those nodes overlaps it. Requires confirm: true.",
|
|
118
|
+
mutation: "preview-confirm",
|
|
119
|
+
http: { method: "POST", path: "/commerce/listings" },
|
|
120
|
+
input: listingRelistInput,
|
|
121
|
+
cli: {
|
|
122
|
+
command: ["capacity-offers", "relist"],
|
|
123
|
+
vnext: help(["capacity-offers", "relist"], "Relist part of a reservation or primary listing you hold.", "ornn capacity-offers relist --source-reservation-id <id> --node-ids <id,...> --starts-at <time> --ends-at <time> --ask-price-per-gpu-hour-micro-usd <n> [--sell-at-market-first true]", "ornn capacity-offers relist --source-reservation-id <id> --node-ids <id> --starts-at <time> --ends-at <time> --ask-price-per-gpu-hour-micro-usd 2500000 --yes --format json", ["capacity-offers update", "capacity-offers delist", "reservations show"], 23, ["reservation ID", "listing ID", "node ID"]),
|
|
124
|
+
},
|
|
125
|
+
slack: false,
|
|
126
|
+
execute: async (ctx, input) => {
|
|
127
|
+
const parsed = listingRelistInput.parse(input);
|
|
128
|
+
const body = {
|
|
129
|
+
...(parsed.sourceReservationId ? { sourceReservationId: parsed.sourceReservationId } : {}),
|
|
130
|
+
...(parsed.sourceListingId ? { sourceListingId: parsed.sourceListingId } : {}),
|
|
131
|
+
nodeIds: parsed.nodeIds,
|
|
132
|
+
startsAt: parsed.startsAt,
|
|
133
|
+
endsAt: parsed.endsAt,
|
|
134
|
+
askPricePerGpuHourMicroUsd: parsed.askPricePerGpuHourMicroUsd,
|
|
135
|
+
sellAtMarketFirst: parsed.sellAtMarketFirst,
|
|
136
|
+
};
|
|
137
|
+
const gate = await consent(ctx, "listings.relist", "Relist capacity", [
|
|
138
|
+
{
|
|
139
|
+
label: "Source",
|
|
140
|
+
value: parsed.sourceReservationId
|
|
141
|
+
? `reservation ${parsed.sourceReservationId}`
|
|
142
|
+
: `listing ${parsed.sourceListingId}`,
|
|
143
|
+
},
|
|
144
|
+
{ label: "Nodes", value: parsed.nodeIds.join(", ") },
|
|
145
|
+
{ label: "Window", value: `${parsed.startsAt} to ${parsed.endsAt}` },
|
|
146
|
+
{ label: "Asking price", value: microUsdLabel(parsed.askPricePerGpuHourMicroUsd) },
|
|
147
|
+
{ label: "Sell at market first", value: parsed.sellAtMarketFirst ? "yes" : "no" },
|
|
148
|
+
], body);
|
|
149
|
+
if (gate)
|
|
150
|
+
return gate;
|
|
151
|
+
return ctx.fetch("/commerce/listings", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body,
|
|
154
|
+
headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
export const listingUpdateInput = z.object({
|
|
159
|
+
$positionals: positionals,
|
|
160
|
+
listingId: z.string().uuid(),
|
|
161
|
+
askPricePerGpuHourMicroUsd: microUsd,
|
|
162
|
+
confirm: z.boolean().optional(),
|
|
163
|
+
});
|
|
164
|
+
export const listingUpdateCapability = defineCapability({
|
|
165
|
+
id: "listings.update",
|
|
166
|
+
domain: "listings",
|
|
167
|
+
roles: ["user"],
|
|
168
|
+
description: "Change the asking price of a secondary listing you own, its only price. Windows bought later use the new price, and the listing's spot asks are re-sent at it. Requires confirm: true.",
|
|
169
|
+
mutation: "preview-confirm",
|
|
170
|
+
http: { method: "PATCH", path: "/commerce/listings/{listingId}" },
|
|
171
|
+
input: listingUpdateInput,
|
|
172
|
+
cli: {
|
|
173
|
+
command: ["capacity-offers", "update"],
|
|
174
|
+
vnext: help(["capacity-offers", "update"], "Change the asking price of a listing you relisted.", "ornn capacity-offers update --listing-id <id> --ask-price-per-gpu-hour-micro-usd <n>", "ornn capacity-offers update --listing-id <id> --ask-price-per-gpu-hour-micro-usd 2400000 --yes --format json", ["capacity-offers relist", "capacity-offers delist"], 24, ["listing ID"]),
|
|
175
|
+
},
|
|
176
|
+
slack: false,
|
|
177
|
+
execute: async (ctx, input) => {
|
|
178
|
+
const parsed = listingUpdateInput.parse(input);
|
|
179
|
+
const body = { askPricePerGpuHourMicroUsd: parsed.askPricePerGpuHourMicroUsd };
|
|
180
|
+
const gate = await consent(ctx, "listings.update", "Update listing asking price", [
|
|
181
|
+
{ label: "Listing", value: parsed.listingId },
|
|
182
|
+
{ label: "Asking price", value: microUsdLabel(body.askPricePerGpuHourMicroUsd) },
|
|
183
|
+
], { listingId: parsed.listingId, ...body });
|
|
184
|
+
if (gate)
|
|
185
|
+
return gate;
|
|
186
|
+
return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "PATCH", body });
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
export const listingDelistInput = z.object({
|
|
190
|
+
$positionals: positionals,
|
|
191
|
+
listingId: z.string().uuid(),
|
|
192
|
+
confirm: z.boolean().optional(),
|
|
193
|
+
});
|
|
194
|
+
export const listingDelistCapability = defineCapability({
|
|
195
|
+
id: "listings.delist",
|
|
196
|
+
domain: "listings",
|
|
197
|
+
roles: ["user"],
|
|
198
|
+
description: "Delist a listing you own. Its unsold time leaves the market, its spot asks are cancelled and running spot reservations on it end. Windows already bought stay sold. Requires confirm: true.",
|
|
199
|
+
mutation: "preview-confirm",
|
|
200
|
+
http: { method: "DELETE", path: "/commerce/listings/{listingId}" },
|
|
201
|
+
input: listingDelistInput,
|
|
202
|
+
cli: {
|
|
203
|
+
command: ["capacity-offers", "delist"],
|
|
204
|
+
vnext: help(["capacity-offers", "delist"], "Delist a listing you relisted.", "ornn capacity-offers delist --listing-id <id>", "ornn capacity-offers delist --listing-id <id> --yes --format json", ["capacity-offers relist"], 25, ["listing ID"]),
|
|
205
|
+
},
|
|
206
|
+
slack: false,
|
|
207
|
+
execute: async (ctx, input) => {
|
|
208
|
+
const parsed = listingDelistInput.parse(input);
|
|
209
|
+
const gate = await consent(ctx, "listings.delist", "Delist listing", [
|
|
210
|
+
{ label: "Listing", value: parsed.listingId },
|
|
211
|
+
{
|
|
212
|
+
label: "Consequence",
|
|
213
|
+
value: "Unsold time and spot asks leave the market; running spot reservations on it end.",
|
|
214
|
+
},
|
|
215
|
+
], { listingId: parsed.listingId });
|
|
216
|
+
if (gate)
|
|
217
|
+
return gate;
|
|
218
|
+
return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "DELETE" });
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
export const reservationsListInput = z.object({
|
|
222
|
+
$positionals: positionals,
|
|
223
|
+
kind: z.enum(["schedule", "spot"]).optional(),
|
|
224
|
+
status: z
|
|
225
|
+
.enum([
|
|
226
|
+
"awaiting_down_payment",
|
|
227
|
+
"awaiting_payment",
|
|
228
|
+
"confirmed",
|
|
229
|
+
"active",
|
|
230
|
+
"completed",
|
|
231
|
+
"cancelled",
|
|
232
|
+
])
|
|
233
|
+
.optional(),
|
|
234
|
+
listingId: z.string().uuid().optional().describe("Trades on a listing you own."),
|
|
235
|
+
limit: pageLimit,
|
|
236
|
+
cursor: z.string().min(1).optional(),
|
|
237
|
+
});
|
|
238
|
+
export const reservationsListCapability = defineCapability({
|
|
239
|
+
id: "reservations.list",
|
|
240
|
+
domain: "reservations",
|
|
241
|
+
roles: ["user"],
|
|
242
|
+
description: "List this organization's reservations, schedule and spot alike (a running spot reservation has no end time). With listingId, list the trades on a listing you own.",
|
|
243
|
+
mutation: "read",
|
|
244
|
+
http: { method: "GET", path: "/commerce/reservations" },
|
|
245
|
+
input: reservationsListInput,
|
|
246
|
+
cli: {
|
|
247
|
+
command: ["reservations", "list"],
|
|
248
|
+
vnext: help(["reservations", "list"], "List your organization's schedule and spot reservations.", "ornn reservations list [--kind schedule|spot] [--status <status>] [--listing-id <id>] [--limit <1-200>] [--cursor <opaque>]", "ornn reservations list --format json", ["reservations show", "capacity-offers relist"], 26, ["opaque page cursor"]),
|
|
249
|
+
},
|
|
250
|
+
slack: false,
|
|
251
|
+
execute: async (ctx, input) => {
|
|
252
|
+
const parsed = reservationsListInput.parse(input ?? {});
|
|
253
|
+
return ctx.fetch(`/commerce/reservations${query({
|
|
254
|
+
kind: parsed.kind,
|
|
255
|
+
status: parsed.status,
|
|
256
|
+
listingId: parsed.listingId,
|
|
257
|
+
limit: parsed.limit,
|
|
258
|
+
cursor: parsed.cursor,
|
|
259
|
+
})}`, { method: "GET" });
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
export const reservationLookupInput = z.object({
|
|
263
|
+
$positionals: positionals,
|
|
264
|
+
reservationId: z.string().uuid(),
|
|
265
|
+
});
|
|
266
|
+
export const reservationsShowCapability = defineCapability({
|
|
267
|
+
id: "reservations.show",
|
|
268
|
+
domain: "reservations",
|
|
269
|
+
roles: ["user"],
|
|
270
|
+
description: "Show one reservation: kind, status, window, price and node count.",
|
|
271
|
+
mutation: "read",
|
|
272
|
+
http: { method: "GET", path: "/commerce/reservations/{reservationId}" },
|
|
273
|
+
input: reservationLookupInput,
|
|
274
|
+
cli: {
|
|
275
|
+
command: ["reservations", "show"],
|
|
276
|
+
vnext: help(["reservations", "show"], "Inspect one reservation.", "ornn reservations show --reservation-id <id>", "ornn reservations show --reservation-id <id> --format json", ["reservations list", "reservations invoices"], 27, ["reservation ID"]),
|
|
277
|
+
},
|
|
278
|
+
slack: false,
|
|
279
|
+
execute: async (ctx, input) => {
|
|
280
|
+
const parsed = reservationLookupInput.parse(input);
|
|
281
|
+
return ctx.fetch(`/commerce/reservations/${seg(parsed.reservationId)}`, { method: "GET" });
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
export const reservationInvoicesCapability = defineCapability({
|
|
285
|
+
id: "reservations.invoices",
|
|
286
|
+
domain: "billing",
|
|
287
|
+
roles: ["user"],
|
|
288
|
+
description: "List a reservation's invoices: the down payment and each billing period, with status and amount in cents.",
|
|
289
|
+
mutation: "read",
|
|
290
|
+
http: { method: "GET", path: "/commerce/reservations/{reservationId}/invoices" },
|
|
291
|
+
input: reservationLookupInput,
|
|
292
|
+
cli: {
|
|
293
|
+
command: ["reservations", "invoices"],
|
|
294
|
+
vnext: help(["reservations", "invoices"], "List one reservation's invoices.", "ornn reservations invoices --reservation-id <id>", "ornn reservations invoices --reservation-id <id> --format json", ["reservations show"], 28, ["reservation ID"]),
|
|
295
|
+
},
|
|
296
|
+
slack: false,
|
|
297
|
+
execute: async (ctx, input) => {
|
|
298
|
+
const parsed = reservationLookupInput.parse(input);
|
|
299
|
+
return ctx.fetch(`/commerce/reservations/${seg(parsed.reservationId)}/invoices`, {
|
|
300
|
+
method: "GET",
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
export const feesShowInput = z.object({ $positionals: positionals });
|
|
305
|
+
export const feesShowCapability = defineCapability({
|
|
306
|
+
id: "fees.show",
|
|
307
|
+
domain: "billing",
|
|
308
|
+
roles: ["user"],
|
|
309
|
+
description: "Show the transaction fee on spot and secondary trades, in parts per million (1000000 = 100%). The side that takes a resting order pays it; primary schedules carry no transaction fee.",
|
|
310
|
+
mutation: "read",
|
|
311
|
+
http: { method: "GET", path: "/commerce/fees" },
|
|
312
|
+
input: feesShowInput,
|
|
313
|
+
cli: {
|
|
314
|
+
command: ["fees", "show"],
|
|
315
|
+
vnext: help(["fees", "show"], "Show the transaction fee on spot and secondary trades.", "ornn fees show", "ornn fees show --format json", ["spot bid", "offers post"], 29),
|
|
316
|
+
},
|
|
317
|
+
slack: false,
|
|
318
|
+
execute: async (ctx) => ctx.fetch("/commerce/fees", { method: "GET" }),
|
|
319
|
+
});
|
|
320
|
+
export const commerceMarketCapabilities = [
|
|
321
|
+
listingsListCapability,
|
|
322
|
+
listingShowCapability,
|
|
323
|
+
listingRelistCapability,
|
|
324
|
+
listingUpdateCapability,
|
|
325
|
+
listingDelistCapability,
|
|
326
|
+
reservationsListCapability,
|
|
327
|
+
reservationsShowCapability,
|
|
328
|
+
reservationInvoicesCapability,
|
|
329
|
+
feesShowCapability,
|
|
330
|
+
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { listSupplierOrganizations, listSupplierOrganizationPage } from "../supplier-directory.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { defineCapability, preview } from "../capability.js";
|
|
3
4
|
function queryPath(path, params) {
|
|
@@ -9,14 +10,20 @@ function queryPath(path, params) {
|
|
|
9
10
|
return parts.length ? `${path}?${parts.join("&")}` : path;
|
|
10
11
|
}
|
|
11
12
|
const emptyInput = z.object({});
|
|
13
|
+
const suppliersInput = z.object({
|
|
14
|
+
q: z.string().optional(),
|
|
15
|
+
offset: z.number().int().min(0).optional(),
|
|
16
|
+
limit: z.number().int().min(1).max(1000).optional(),
|
|
17
|
+
});
|
|
12
18
|
const facilitiesInput = z.object({
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
organization_id: z.string().optional(),
|
|
20
|
+
organizationId: z.string().optional(),
|
|
21
|
+
cursor: z.string().optional(),
|
|
22
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
15
23
|
});
|
|
16
24
|
const nodesInput = z.object({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
operator: z.string().optional(),
|
|
25
|
+
organization_id: z.string().optional(),
|
|
26
|
+
organizationId: z.string().optional(),
|
|
20
27
|
facility_id: z.string().optional(),
|
|
21
28
|
facilityId: z.string().optional(),
|
|
22
29
|
status: z.string().optional(),
|
|
@@ -42,29 +49,53 @@ const nodeExecInput = z.object({
|
|
|
42
49
|
function nodeIdOf(input) {
|
|
43
50
|
return (input.node_id || input.nodeId || "").trim();
|
|
44
51
|
}
|
|
45
|
-
export const
|
|
46
|
-
id: "
|
|
52
|
+
export const listSuppliersCapability = defineCapability({
|
|
53
|
+
id: "supplier.list",
|
|
47
54
|
domain: "fleet",
|
|
48
55
|
roles: ["admin"],
|
|
49
|
-
description: "List
|
|
56
|
+
description: "List supplier organizations (id, name). Resolve supplier names to organization_id with this before nodes.list or facilities.list.",
|
|
50
57
|
mutation: "read",
|
|
51
|
-
http: { method: "GET", path: "/
|
|
52
|
-
slack: {
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
http: { method: "GET", path: "/v1/organizations" },
|
|
59
|
+
slack: {
|
|
60
|
+
parameters: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
q: { type: "string", description: "Search supplier names." },
|
|
64
|
+
offset: { type: "integer", description: "Use next_offset to fetch another page." },
|
|
65
|
+
limit: { type: "integer", description: "Page size (Slack maximum 10)." },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
input: suppliersInput,
|
|
70
|
+
execute: async (ctx, input) => {
|
|
71
|
+
const parsed = suppliersInput.parse(input);
|
|
72
|
+
const paged = parsed.limit !== undefined || parsed.offset !== undefined || parsed.q !== undefined;
|
|
73
|
+
const limit = parsed.limit ?? 25;
|
|
74
|
+
const offset = parsed.offset ?? 0;
|
|
75
|
+
const rows = paged
|
|
76
|
+
? await listSupplierOrganizationPage(ctx.fetch, { ...parsed, limit, offset })
|
|
77
|
+
: await listSupplierOrganizations(ctx.fetch);
|
|
78
|
+
return {
|
|
79
|
+
suppliers: rows.map((org) => ({ id: org.id, name: org.name })),
|
|
80
|
+
...(paged && rows.length === limit ? { next_offset: offset + rows.length } : {}),
|
|
81
|
+
};
|
|
82
|
+
},
|
|
55
83
|
});
|
|
56
84
|
export const listFacilitiesCapability = defineCapability({
|
|
57
85
|
id: "facilities.list",
|
|
58
86
|
domain: "fleet",
|
|
59
87
|
roles: ["admin"],
|
|
60
|
-
description: "List facilities (id, display name, region,
|
|
88
|
+
description: "List facilities (id, display name, region, supplier organization id). Optional organization_id filter. Resolve facility names with this before nodes.list.",
|
|
61
89
|
mutation: "read",
|
|
62
90
|
http: { method: "GET", path: "/provisioning/facilities" },
|
|
63
91
|
slack: {
|
|
64
92
|
parameters: {
|
|
65
93
|
type: "object",
|
|
66
94
|
properties: {
|
|
67
|
-
|
|
95
|
+
organization_id: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Optional supplier organization UUID from supplier.list.",
|
|
98
|
+
},
|
|
68
99
|
},
|
|
69
100
|
},
|
|
70
101
|
},
|
|
@@ -72,7 +103,9 @@ export const listFacilitiesCapability = defineCapability({
|
|
|
72
103
|
execute: async (ctx, input) => {
|
|
73
104
|
const parsed = facilitiesInput.parse(input);
|
|
74
105
|
return ctx.fetch(queryPath("/provisioning/facilities", {
|
|
75
|
-
|
|
106
|
+
organizationId: parsed.organization_id ?? parsed.organizationId,
|
|
107
|
+
cursor: parsed.cursor,
|
|
108
|
+
limit: parsed.limit === undefined ? undefined : String(parsed.limit),
|
|
76
109
|
}), { method: "GET" });
|
|
77
110
|
},
|
|
78
111
|
});
|
|
@@ -80,22 +113,25 @@ export const listNodesCapability = defineCapability({
|
|
|
80
113
|
id: "nodes.list",
|
|
81
114
|
domain: "fleet",
|
|
82
115
|
roles: ["user", "admin"],
|
|
83
|
-
description: "List nodes. Users see their own reserved machines. Admins see enrolled, ghost, and off-grid nodes. Call with no filters for the whole fleet. Optional email lists nodes for that user's organizations. Optional
|
|
116
|
+
description: "List nodes. Users see their own reserved machines. Admins see enrolled, ghost, and off-grid nodes. Call with no filters for the whole fleet. Optional email lists nodes for that user's organizations. Optional organization_id (supplier), facility_id, and status (ghost, off-grid, bare-metal) narrow the same list. Paginate large fleets with cursor + max: pass the response's next_cursor back as cursor to fetch the next page; next_cursor is omitted once the page is not full. Never invent nodes. Never print private keys.",
|
|
84
117
|
mutation: "read",
|
|
85
118
|
http: { method: "GET", path: "/nodes" },
|
|
86
119
|
slack: {
|
|
87
120
|
parameters: {
|
|
88
121
|
type: "object",
|
|
89
122
|
properties: {
|
|
90
|
-
|
|
123
|
+
organization_id: {
|
|
91
124
|
type: "string",
|
|
92
|
-
description: "Optional
|
|
125
|
+
description: "Optional supplier organization UUID from supplier.list.",
|
|
93
126
|
},
|
|
94
127
|
facility_id: {
|
|
95
128
|
type: "string",
|
|
96
129
|
description: "Optional commerce facility UUID from facilities.list.",
|
|
97
130
|
},
|
|
98
|
-
email: {
|
|
131
|
+
email: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Optional. List nodes for this user's organizations.",
|
|
134
|
+
},
|
|
99
135
|
status: {
|
|
100
136
|
type: "string",
|
|
101
137
|
enum: ["ghost", "off-grid", "bare-metal"],
|
|
@@ -123,7 +159,7 @@ export const listNodesCapability = defineCapability({
|
|
|
123
159
|
return ctx.fetch(queryPath("/nodes", pagination), { method: "GET" });
|
|
124
160
|
}
|
|
125
161
|
return ctx.fetch(queryPath("/nodes", {
|
|
126
|
-
|
|
162
|
+
organization_id: parsed.organization_id ?? parsed.organizationId,
|
|
127
163
|
facility_id: parsed.facility_id ?? parsed.facilityId,
|
|
128
164
|
status: parsed.status,
|
|
129
165
|
email: parsed.email,
|
|
@@ -259,7 +295,7 @@ export const nodeTerminateCapability = defineCapability({
|
|
|
259
295
|
},
|
|
260
296
|
});
|
|
261
297
|
export const fleetCapabilities = [
|
|
262
|
-
|
|
298
|
+
listSuppliersCapability,
|
|
263
299
|
listFacilitiesCapability,
|
|
264
300
|
listNodesCapability,
|
|
265
301
|
nodeOffGridCapability,
|