@ornncompute/cli 1.0.34 → 1.0.48
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 +45 -21
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +69 -139
- package/src/admin-node-lifecycle.mjs +48 -7
- package/src/admin-shell.mjs +101 -25
- package/src/catalog-shell.mjs +15 -20
- 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 +36 -55
- package/vendor/capabilities/capabilities/admin.js +8 -6
- package/vendor/capabilities/capabilities/commerce-market.js +76 -101
- package/vendor/capabilities/capabilities/compute.js +4 -2
- package/vendor/capabilities/capabilities/listings.js +58 -38
- package/vendor/capabilities/capabilities/markets.js +7 -7
- package/vendor/capabilities/capabilities/native-listing.js +63 -0
- package/vendor/capabilities/capabilities/organization-invites.js +53 -0
- package/vendor/capabilities/capabilities/users.js +141 -31
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +1 -2
- package/vendor/capabilities/software-catalog.js +52 -6
|
@@ -46,22 +46,16 @@ export const listingCreateInput = z.object({
|
|
|
46
46
|
.describe("End of the availability window. YYYY-MM-DD or RFC3339."),
|
|
47
47
|
available_from: z.string().optional().describe("Alias for available_start_at."),
|
|
48
48
|
available_to: z.string().optional().describe("Alias for available_end_at."),
|
|
49
|
-
buy_now_price_per_gpu_hour: z
|
|
50
|
-
.number()
|
|
51
|
-
.optional()
|
|
52
|
-
.describe("Price buyers pay, USD per GPU-hour. The spread over cost_per_gpu_hour is Ornn's."),
|
|
53
|
-
cost_per_gpu_hour: z
|
|
54
|
-
.number()
|
|
55
|
-
.optional()
|
|
56
|
-
.describe("Supplier price, USD per GPU-hour. Defaults to the buy-now price (spread 0)."),
|
|
49
|
+
buy_now_price_per_gpu_hour: z.number().optional().describe("Price buyers pay, USD per GPU-hour."),
|
|
57
50
|
deposit_percent: z.number().optional(),
|
|
58
51
|
down_payment_cents: z.number().int().nonnegative().optional(),
|
|
59
52
|
access_mode: z.enum(["bare-metal", "virtual-machine"]).optional(),
|
|
60
|
-
state: z.enum(["available", "
|
|
53
|
+
state: z.enum(["available", "draft"]).optional(),
|
|
61
54
|
price_omitted: z.boolean().optional(),
|
|
62
55
|
announce: z.boolean().optional(),
|
|
63
56
|
text: z.string().optional(),
|
|
64
57
|
confirm: z.boolean().optional(),
|
|
58
|
+
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
65
59
|
});
|
|
66
60
|
const catalogTermsInput = z.object({ kind: z.enum(["gpu", "cpu", "node", "link"]).optional() });
|
|
67
61
|
const slackListingCreateParams = {
|
|
@@ -80,7 +74,6 @@ const slackListingCreateParams = {
|
|
|
80
74
|
available_start_at: { type: "string" },
|
|
81
75
|
available_end_at: { type: "string" },
|
|
82
76
|
buy_now_price_per_gpu_hour: { type: "number" },
|
|
83
|
-
cost_per_gpu_hour: { type: "number" },
|
|
84
77
|
deposit_percent: { type: "number" },
|
|
85
78
|
price_omitted: { type: "boolean" },
|
|
86
79
|
announce: { type: "boolean" },
|
|
@@ -166,6 +159,23 @@ export function primaryDownPaymentCents(terms) {
|
|
|
166
159
|
const totalCents = (terms.pricePerGpuHourMicroUsd * terms.nodeCount * terms.gpusPerNode * hours) / 10_000;
|
|
167
160
|
return Math.max(0, Math.round((totalCents * terms.percent) / 100));
|
|
168
161
|
}
|
|
162
|
+
/** Shared native creation body for Slack's preview and human-confirmed write. */
|
|
163
|
+
export function primaryListingBody(input, pricePerGpuHourMicroUsd, downPaymentCents) {
|
|
164
|
+
return {
|
|
165
|
+
...input,
|
|
166
|
+
kind: "primary",
|
|
167
|
+
terms: [
|
|
168
|
+
{
|
|
169
|
+
type: "reserved",
|
|
170
|
+
minDurationHours: 1 / 3600,
|
|
171
|
+
maxDurationHours: (Date.parse(input.availableUntil) - Date.parse(input.availableAt)) / 3600000,
|
|
172
|
+
renewable: false,
|
|
173
|
+
pricePerGpuHourMicroUsd,
|
|
174
|
+
downPaymentCents,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
169
179
|
/** Commerce parses listing windows as RFC3339 timestamps, not bare dates. */
|
|
170
180
|
function rfc3339(value) {
|
|
171
181
|
const trimmed = value.trim();
|
|
@@ -193,13 +203,13 @@ const LISTING_CREATE_INPUT_ONLY_KEYS = [
|
|
|
193
203
|
"site",
|
|
194
204
|
"kind",
|
|
195
205
|
"confirm",
|
|
206
|
+
"idempotencyKey",
|
|
196
207
|
"organizationId",
|
|
197
208
|
"organization_id",
|
|
198
209
|
"site_supplier",
|
|
199
210
|
"facilityId",
|
|
200
211
|
"facility_id",
|
|
201
212
|
"buy_now_price_per_gpu_hour",
|
|
202
|
-
"cost_per_gpu_hour",
|
|
203
213
|
"deposit_percent",
|
|
204
214
|
"down_payment_cents",
|
|
205
215
|
"access_mode",
|
|
@@ -208,12 +218,13 @@ const LISTING_CREATE_INPUT_ONLY_KEYS = [
|
|
|
208
218
|
];
|
|
209
219
|
export const createListingCapability = defineCapability({
|
|
210
220
|
id: "listings.create",
|
|
221
|
+
mcp: false,
|
|
211
222
|
domain: "listings",
|
|
212
223
|
roles: ["admin"],
|
|
213
224
|
description: "Create a primary marketplace listing (admin). gpu_type, cpu, node_model, and fabric_type must exact-match an orchestrator catalog display name, canonical, or alias; gpu_type becomes the listing's GPU slug. " +
|
|
214
|
-
"
|
|
225
|
+
"buy_now_price_per_gpu_hour is the customer price; deposit_percent is the upfront payment as a percentage of the full availability window. Nodes are attached afterwards. Slack never POSTs until a human replies confirm. MCP and CLI use native listing.create instead.",
|
|
215
226
|
mutation: "preview-confirm",
|
|
216
|
-
http: { method: "POST", path: "/commerce/listings" },
|
|
227
|
+
http: { method: "POST", path: "/commerce/v2/listings" },
|
|
217
228
|
slack: { parameters: slackListingCreateParams },
|
|
218
229
|
input: listingCreateInput,
|
|
219
230
|
execute: async (ctx, input) => {
|
|
@@ -244,14 +255,21 @@ export const createListingCapability = defineCapability({
|
|
|
244
255
|
const seenCursors = new Set();
|
|
245
256
|
let cursor;
|
|
246
257
|
do {
|
|
247
|
-
const response = (await ctx.fetch(queryPath("/provisioning/facilities", { cursor }), {
|
|
258
|
+
const response = (await ctx.fetch(queryPath("/provisioning/facilities", { cursor }), {
|
|
259
|
+
method: "GET",
|
|
260
|
+
}));
|
|
248
261
|
if (!Array.isArray(response.facilities))
|
|
249
262
|
throw new Error("invalid facility catalog");
|
|
250
|
-
matches.push(...response.facilities.filter((facility) => (!organizationId ||
|
|
263
|
+
matches.push(...response.facilities.filter((facility) => (!organizationId ||
|
|
264
|
+
(facility.organizationId ?? facility.organization_id) === organizationId) &&
|
|
251
265
|
(facilityId
|
|
252
266
|
? (facility.facilityId ?? facility.id) === facilityId
|
|
253
|
-
: !facilityQuery ||
|
|
254
|
-
|
|
267
|
+
: !facilityQuery ||
|
|
268
|
+
[
|
|
269
|
+
facility.facilityId ?? facility.id,
|
|
270
|
+
facility.slug,
|
|
271
|
+
facility.displayName ?? facility.display_name,
|
|
272
|
+
].some((value) => value?.toLowerCase() === facilityQuery))));
|
|
255
273
|
cursor = response.nextCursor ?? undefined;
|
|
256
274
|
if (cursor) {
|
|
257
275
|
if (seenCursors.has(cursor) || seenCursors.size >= 49) {
|
|
@@ -274,12 +292,12 @@ export const createListingCapability = defineCapability({
|
|
|
274
292
|
const startsAt = rfc3339(parsed.available_start_at ??
|
|
275
293
|
parsed.available_from ??
|
|
276
294
|
parsed.starts_after ??
|
|
277
|
-
new Date().toISOString());
|
|
295
|
+
new Date(Math.floor(Date.now() / 1000) * 1000).toISOString());
|
|
278
296
|
const endsAt = rfc3339(parsed.available_end_at ??
|
|
279
297
|
parsed.available_to ??
|
|
280
298
|
parsed.ends_before ??
|
|
281
299
|
parsed.ends_after ??
|
|
282
|
-
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString());
|
|
300
|
+
new Date(Math.floor(Date.now() / 1000) * 1000 + 30 * 24 * 60 * 60 * 1000).toISOString());
|
|
283
301
|
const nodeCount = parsed.node_count ?? parsed.min_nodes ?? 1;
|
|
284
302
|
const gpusPerNode = parsed.gpus_per_node ?? 8;
|
|
285
303
|
const boundPayload = bound.payload;
|
|
@@ -288,12 +306,7 @@ export const createListingCapability = defineCapability({
|
|
|
288
306
|
? resolveCatalogTerm(hardware.terms, "gpu", normalized.gpu_type).canonical
|
|
289
307
|
: null;
|
|
290
308
|
const buyerPrice = parsed.buy_now_price_per_gpu_hour;
|
|
291
|
-
const
|
|
292
|
-
const supplierMicro = supplierPrice != null ? microUsd(supplierPrice) : null;
|
|
293
|
-
const buyerMicro = buyerPrice != null ? microUsd(buyerPrice) : supplierMicro;
|
|
294
|
-
if (supplierMicro != null && buyerMicro != null && buyerMicro < supplierMicro) {
|
|
295
|
-
return { ok: false, error: "price_below_supplier_price" };
|
|
296
|
-
}
|
|
309
|
+
const buyerMicro = buyerPrice != null ? microUsd(buyerPrice) : null;
|
|
297
310
|
const downPaymentCents = parsed.down_payment_cents ??
|
|
298
311
|
(parsed.deposit_percent != null && buyerMicro != null
|
|
299
312
|
? primaryDownPaymentCents({
|
|
@@ -305,26 +318,23 @@ export const createListingCapability = defineCapability({
|
|
|
305
318
|
percent: parsed.deposit_percent,
|
|
306
319
|
})
|
|
307
320
|
: 0);
|
|
308
|
-
const listing = organizationId && facilityId && gpuSlug &&
|
|
309
|
-
? {
|
|
321
|
+
const listing = organizationId && facilityId && gpuSlug && buyerMicro != null
|
|
322
|
+
? primaryListingBody({
|
|
310
323
|
organizationId,
|
|
311
324
|
facilityId,
|
|
312
325
|
gpuSlug,
|
|
313
326
|
gpusPerNode,
|
|
314
|
-
|
|
315
|
-
startsAt,
|
|
316
|
-
endsAt,
|
|
327
|
+
provisioningType: parsed.access_mode ?? "virtual-machine",
|
|
328
|
+
availableAt: startsAt,
|
|
329
|
+
availableUntil: endsAt,
|
|
317
330
|
...(typeof draft.cpu === "string" ? { cpu: draft.cpu } : {}),
|
|
318
331
|
...(typeof draft.ram === "string" ? { ram: draft.ram } : {}),
|
|
319
332
|
...(typeof draft.storage === "string" ? { storage: draft.storage } : {}),
|
|
320
333
|
...(typeof draft.fabric_type === "string" ? { fabricType: draft.fabric_type } : {}),
|
|
321
334
|
...(typeof draft.internet === "string" ? { internet: draft.internet } : {}),
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
downPaymentCents,
|
|
326
|
-
...(parsed.state ? { state: parsed.state } : {}),
|
|
327
|
-
}
|
|
335
|
+
capacityNodeCount: nodeCount,
|
|
336
|
+
state: parsed.state ?? "available",
|
|
337
|
+
}, buyerMicro, downPaymentCents)
|
|
328
338
|
: null;
|
|
329
339
|
if (!ctx.confirmed) {
|
|
330
340
|
return preview("create_listing", {
|
|
@@ -337,10 +347,20 @@ export const createListingCapability = defineCapability({
|
|
|
337
347
|
if (!listing) {
|
|
338
348
|
return {
|
|
339
349
|
ok: false,
|
|
340
|
-
error: !organizationId || !facilityId
|
|
350
|
+
error: !organizationId || !facilityId
|
|
351
|
+
? "facility_required"
|
|
352
|
+
: gpuSlug
|
|
353
|
+
? "price_required"
|
|
354
|
+
: "gpu_required",
|
|
341
355
|
};
|
|
342
356
|
}
|
|
343
|
-
|
|
357
|
+
if (!parsed.idempotencyKey)
|
|
358
|
+
return { ok: false, error: "idempotency_key_required" };
|
|
359
|
+
return ctx.fetch("/commerce/v2/listings", {
|
|
360
|
+
method: "POST",
|
|
361
|
+
body: listing,
|
|
362
|
+
headers: { "Idempotency-Key": parsed.idempotencyKey },
|
|
363
|
+
});
|
|
344
364
|
},
|
|
345
365
|
});
|
|
346
366
|
export const listingCapabilities = [
|
|
@@ -84,9 +84,9 @@ export const spotBidInput = z
|
|
|
84
84
|
gpus: z.coerce
|
|
85
85
|
.number()
|
|
86
86
|
.int()
|
|
87
|
-
.min(
|
|
87
|
+
.min(8)
|
|
88
88
|
.max(8)
|
|
89
|
-
.describe("GPUs on one node
|
|
89
|
+
.describe("Exactly 8 GPUs on one full node. To buy N nodes, place N bids."),
|
|
90
90
|
orderType: z
|
|
91
91
|
.enum(["limit", "market"])
|
|
92
92
|
.default("limit")
|
|
@@ -105,14 +105,14 @@ export const spotBidCapability = defineCapability({
|
|
|
105
105
|
id: "spot.bid",
|
|
106
106
|
domain: "markets",
|
|
107
107
|
roles: ["user"],
|
|
108
|
-
description: "Place a spot bid for
|
|
109
|
-
"prepaid hourly from Ornn credits first and then the saved card
|
|
108
|
+
description: "Place a spot bid for a full node with 8 GPUs. A filled bid becomes a running spot reservation, " +
|
|
109
|
+
"prepaid hourly from Ornn credits first and then the saved card. Book prices are what you pay; a limit is the most you pay per GPU-hour. Requires confirm: true.",
|
|
110
110
|
mutation: "preview-confirm",
|
|
111
111
|
http: { method: "POST", path: `${BASE}/spot/orders` },
|
|
112
112
|
input: spotBidInput,
|
|
113
113
|
cli: {
|
|
114
114
|
command: ["spot", "bid"],
|
|
115
|
-
vnext: help(["spot", "bid"], "Place one spot bid for
|
|
115
|
+
vnext: help(["spot", "bid"], "Place one spot bid for a full node with 8 GPUs.", "ornn spot bid --gpu-slug <slug> --gpus 8 [--order-type limit|market] [--price-per-gpu-hour-micro-usd <n>]", "ornn spot bid --gpu-slug <slug> --gpus 8 --price-per-gpu-hour-micro-usd 2500000 --yes --format json", ["spot orders", "spot cancel"], 33, ["catalog GPU slug"]),
|
|
116
116
|
},
|
|
117
117
|
slack: false,
|
|
118
118
|
execute: async (ctx, input) => {
|
|
@@ -266,7 +266,7 @@ export const schedulePurchaseCapability = defineCapability({
|
|
|
266
266
|
id: "schedules.purchase",
|
|
267
267
|
domain: "markets",
|
|
268
268
|
roles: ["user"],
|
|
269
|
-
description: "Buy a window of a secondary (relisted) listing at its asking price, for every node on it. There is no offer: the window is yours once it is paid. You pay the asking price
|
|
269
|
+
description: "Buy a window of a secondary (relisted) listing at its asking price, for every node on it. There is no offer: the window is yours once it is paid. You pay the asking price up front, from Ornn credits and then your saved card; $10,000 or more is invoiced by wire. Requires confirm: true.",
|
|
270
270
|
mutation: "preview-confirm",
|
|
271
271
|
http: { method: "POST", path: `${BASE}/schedules/purchases` },
|
|
272
272
|
input: schedulePurchaseInput,
|
|
@@ -293,7 +293,7 @@ export const schedulePurchaseCapability = defineCapability({
|
|
|
293
293
|
{ label: "Asking price", value: microUsdLabel(parsed.askPricePerGpuHourMicroUsd) },
|
|
294
294
|
{
|
|
295
295
|
label: "Payment",
|
|
296
|
-
value: "Up front
|
|
296
|
+
value: "Up front: Ornn credits, then your saved card.",
|
|
297
297
|
},
|
|
298
298
|
], body);
|
|
299
299
|
if (gate)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { flag } from "./market-shared.js";
|
|
3
|
+
export const nativeTermFields = {
|
|
4
|
+
type: z.enum(["reserved", "spot", "on-demand"]).nullable().optional(),
|
|
5
|
+
minDurationHours: z.coerce.number().nonnegative(),
|
|
6
|
+
maxDurationHours: z.coerce.number().nonnegative(),
|
|
7
|
+
pricePerGpuHourMicroUsd: z.coerce.number().int().positive(),
|
|
8
|
+
downPaymentCents: z.coerce.number().int().nonnegative().nullable().optional(),
|
|
9
|
+
renewable: flag,
|
|
10
|
+
};
|
|
11
|
+
export const nativeTermInput = z.object(nativeTermFields).strict();
|
|
12
|
+
export const nativeTermsInput = z.preprocess((value) => {
|
|
13
|
+
if (typeof value !== "string")
|
|
14
|
+
return value;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
}, z.array(nativeTermInput));
|
|
22
|
+
export const listingWriteControls = {
|
|
23
|
+
confirm: z.boolean().optional(),
|
|
24
|
+
idempotencyKey: z.string().min(1).max(200),
|
|
25
|
+
};
|
|
26
|
+
export const nativeListingCreateInput = z
|
|
27
|
+
.object({
|
|
28
|
+
organizationId: z.string().uuid().nullable().optional(),
|
|
29
|
+
kind: z.enum(["primary", "secondary"]).default("primary"),
|
|
30
|
+
gpuSlug: z.string().min(1),
|
|
31
|
+
gpusPerNode: z.coerce.number().int().positive(),
|
|
32
|
+
capacityNodeCount: z.coerce.number().int().positive(),
|
|
33
|
+
facilityId: z.string().uuid(),
|
|
34
|
+
provisioningType: z.enum(["bare-metal", "virtual-machine"]),
|
|
35
|
+
state: z.enum(["draft", "available"]),
|
|
36
|
+
availableAt: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
37
|
+
availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
38
|
+
daysToRfs: z.coerce.number().int().nonnegative().optional(),
|
|
39
|
+
cpu: z.string().optional(),
|
|
40
|
+
ram: z.string().optional(),
|
|
41
|
+
storage: z.string().optional(),
|
|
42
|
+
fabricType: z.string().optional(),
|
|
43
|
+
internet: z.string().optional(),
|
|
44
|
+
terms: nativeTermsInput,
|
|
45
|
+
...listingWriteControls,
|
|
46
|
+
})
|
|
47
|
+
.strict();
|
|
48
|
+
export const nativeListingUpdateFields = {
|
|
49
|
+
state: z.enum(["draft", "available", "delisted"]).optional(),
|
|
50
|
+
nodeCount: z.coerce.number().int().positive().optional(),
|
|
51
|
+
gpuSlug: z.string().min(1).optional(),
|
|
52
|
+
gpusPerNode: z.coerce.number().int().positive().optional(),
|
|
53
|
+
facilityId: z.string().uuid().optional(),
|
|
54
|
+
provisioningType: z.enum(["bare-metal", "virtual-machine"]).optional(),
|
|
55
|
+
availableAt: z.iso.datetime({ offset: true }).optional(),
|
|
56
|
+
availableUntil: z.iso.datetime({ offset: true }).optional(),
|
|
57
|
+
daysToRfs: z.coerce.number().int().nonnegative().optional(),
|
|
58
|
+
cpu: z.string().optional(),
|
|
59
|
+
ram: z.string().optional(),
|
|
60
|
+
storage: z.string().optional(),
|
|
61
|
+
fabricType: z.string().optional(),
|
|
62
|
+
internet: z.string().optional(),
|
|
63
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
export const organizationInviteInput = z.object({
|
|
4
|
+
organization_id: z.uuid(),
|
|
5
|
+
email: z.string().trim().toLowerCase().pipe(z.email()),
|
|
6
|
+
});
|
|
7
|
+
export const organizationInviteCapability = defineCapability({
|
|
8
|
+
id: "organization.invite",
|
|
9
|
+
domain: "users",
|
|
10
|
+
roles: ["admin"],
|
|
11
|
+
description: "Email an invitation to an existing organization. Resolve its UUID with organization_list first; ask the operator to choose if matches are ambiguous. The recipient may already belong to another organization. Send directly when the admin requests the invitation.",
|
|
12
|
+
mutation: "immediate",
|
|
13
|
+
http: { method: "POST", path: "/v1/staff/invites" },
|
|
14
|
+
cli: {
|
|
15
|
+
command: ["admin", "organizations", "invite"],
|
|
16
|
+
vnext: {
|
|
17
|
+
audience: "admin",
|
|
18
|
+
category: "resource",
|
|
19
|
+
command: ["admin", "organizations", "invite"],
|
|
20
|
+
outcome: "Email an invitation to an existing organization.",
|
|
21
|
+
human: "ornn admin organizations invite --organization UUID --email EMAIL",
|
|
22
|
+
automation: "ornn admin organizations invite --organization UUID --email EMAIL --format json",
|
|
23
|
+
acceptedReferences: ["organization UUID from admin organizations list"],
|
|
24
|
+
related: ["admin organizations list"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
slack: {
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
organization_id: {
|
|
32
|
+
type: "string",
|
|
33
|
+
format: "uuid",
|
|
34
|
+
description: "Exact organization UUID from organization_list.",
|
|
35
|
+
},
|
|
36
|
+
email: {
|
|
37
|
+
type: "string",
|
|
38
|
+
format: "email",
|
|
39
|
+
description: "Address to receive the invitation.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["organization_id", "email"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
input: organizationInviteInput,
|
|
46
|
+
execute: async (ctx, input) => {
|
|
47
|
+
const parsed = organizationInviteInput.parse(input);
|
|
48
|
+
return ctx.fetch("/v1/staff/invites", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: { ...parsed, send_email: true },
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -1,49 +1,159 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineCapability } from "../capability.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const directoryLimit = z.number().int().min(1).max(100).default(25);
|
|
4
|
+
const directoryOffset = z.number().int().nonnegative().optional();
|
|
5
|
+
export const organizationListInput = z.object({
|
|
6
|
+
q: z.string().trim().optional().describe("Company name, contact/member name, or email."),
|
|
7
|
+
role: z.enum(["consumer", "supplier"]).optional(),
|
|
8
|
+
approved: z.boolean().optional(),
|
|
9
|
+
ids: z.array(z.uuid()).min(1).max(100).optional(),
|
|
10
|
+
limit: directoryLimit,
|
|
11
|
+
offset: directoryOffset,
|
|
12
|
+
});
|
|
13
|
+
export const usersListInput = z.object({
|
|
14
|
+
q: z.string().trim().optional().describe("Person name, email, or company name."),
|
|
15
|
+
organization_id: z.uuid().optional(),
|
|
16
|
+
limit: directoryLimit,
|
|
17
|
+
offset: directoryOffset,
|
|
18
|
+
});
|
|
19
|
+
function directoryPath(input) {
|
|
20
|
+
const params = [
|
|
21
|
+
`limit=${input.limit}`,
|
|
22
|
+
`offset=${input.offset}`,
|
|
23
|
+
"sort=name",
|
|
24
|
+
"direction=asc",
|
|
25
|
+
];
|
|
26
|
+
if (input.q)
|
|
27
|
+
params.push(`q=${encodeURIComponent(input.q)}`);
|
|
28
|
+
if (input.role)
|
|
29
|
+
params.push(`role=${encodeURIComponent(input.role)}`);
|
|
30
|
+
if (input.approved !== undefined) {
|
|
31
|
+
params.push(`approved=${input.approved}`);
|
|
8
32
|
}
|
|
9
|
-
|
|
33
|
+
if (input.ids)
|
|
34
|
+
params.push(`ids=${encodeURIComponent(input.ids.join(","))}`);
|
|
35
|
+
return `/v1/organizations?${params.join("&")}`;
|
|
36
|
+
}
|
|
37
|
+
function directoryMembers(organization) {
|
|
38
|
+
return organization.members ?? [];
|
|
39
|
+
}
|
|
40
|
+
function organizationMatchesQuery(organization, query) {
|
|
41
|
+
return (organization.name.toLowerCase().includes(query) ||
|
|
42
|
+
organization.id.toLowerCase() === query ||
|
|
43
|
+
[
|
|
44
|
+
organization.company_profile?.company_name,
|
|
45
|
+
organization.company_profile?.contact_name,
|
|
46
|
+
organization.company_profile?.contact_email,
|
|
47
|
+
organization.company_profile?.work_email,
|
|
48
|
+
].some((value) => value?.toLowerCase().includes(query)));
|
|
10
49
|
}
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
50
|
+
export const organizationListCapability = defineCapability({
|
|
51
|
+
id: "organization.list",
|
|
52
|
+
domain: "users",
|
|
53
|
+
roles: ["admin"],
|
|
54
|
+
description: "List organizations from the Authorization directory. Search by company name, contact/member name, or email; filter by role, approval, or UUIDs. Use the organization id as organizationId when recording downtime, and ask the user to choose if matches are ambiguous.",
|
|
55
|
+
mutation: "read",
|
|
56
|
+
http: { method: "GET", path: "/v1/organizations" },
|
|
57
|
+
slack: {
|
|
58
|
+
parameters: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
q: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Company name, contact/member name, or email.",
|
|
64
|
+
},
|
|
65
|
+
role: { type: "string", enum: ["consumer", "supplier"] },
|
|
66
|
+
approved: { type: "boolean" },
|
|
67
|
+
ids: {
|
|
68
|
+
type: "array",
|
|
69
|
+
items: { type: "string", format: "uuid" },
|
|
70
|
+
minItems: 1,
|
|
71
|
+
maxItems: 100,
|
|
72
|
+
},
|
|
73
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
|
|
74
|
+
offset: { type: "integer", minimum: 0 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
input: organizationListInput,
|
|
79
|
+
execute: async (ctx, input) => {
|
|
80
|
+
const parsed = organizationListInput.parse(input);
|
|
81
|
+
const limit = parsed.limit;
|
|
82
|
+
const offset = parsed.offset ?? 0;
|
|
83
|
+
const rows = (await ctx.fetch(directoryPath({ ...parsed, limit, offset }), { method: "GET" }));
|
|
84
|
+
return {
|
|
85
|
+
organizations: rows.map((organization) => ({
|
|
86
|
+
id: organization.id,
|
|
87
|
+
name: organization.name,
|
|
88
|
+
role: organization.role,
|
|
89
|
+
approved: organization.approved,
|
|
90
|
+
members: directoryMembers(organization).map((member) => ({
|
|
91
|
+
user_id: member.user_id,
|
|
92
|
+
email: member.email,
|
|
93
|
+
name: member.name ?? null,
|
|
94
|
+
})),
|
|
95
|
+
})),
|
|
96
|
+
...(rows.length === limit ? { next_offset: offset + rows.length } : {}),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
18
99
|
});
|
|
19
|
-
export const
|
|
20
|
-
id: "users.
|
|
100
|
+
export const usersListCapability = defineCapability({
|
|
101
|
+
id: "users.list",
|
|
21
102
|
domain: "users",
|
|
22
103
|
roles: ["admin"],
|
|
23
|
-
description: "
|
|
104
|
+
description: "List users who belong to an organization, from the Authorization directory. Search by person name, email, or company name; use organization_id to restrict results to one organization.",
|
|
24
105
|
mutation: "read",
|
|
25
|
-
http: { method: "GET", path: "/
|
|
106
|
+
http: { method: "GET", path: "/v1/organizations" },
|
|
26
107
|
slack: {
|
|
27
108
|
parameters: {
|
|
28
109
|
type: "object",
|
|
29
110
|
properties: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
111
|
+
q: { type: "string", description: "Person name, email, or company name." },
|
|
112
|
+
organization_id: { type: "string", format: "uuid" },
|
|
113
|
+
limit: {
|
|
114
|
+
type: "integer",
|
|
115
|
+
minimum: 1,
|
|
116
|
+
maximum: 100,
|
|
117
|
+
default: 25,
|
|
118
|
+
description: "Maximum number of organizations to search.",
|
|
119
|
+
},
|
|
120
|
+
offset: { type: "integer", minimum: 0 },
|
|
34
121
|
},
|
|
35
122
|
},
|
|
36
123
|
},
|
|
37
|
-
input:
|
|
124
|
+
input: usersListInput,
|
|
38
125
|
execute: async (ctx, input) => {
|
|
39
|
-
const parsed =
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
126
|
+
const parsed = usersListInput.parse(input);
|
|
127
|
+
const limit = parsed.limit;
|
|
128
|
+
const offset = parsed.offset ?? 0;
|
|
129
|
+
const rows = (await ctx.fetch(directoryPath({
|
|
130
|
+
limit,
|
|
131
|
+
offset,
|
|
132
|
+
q: parsed.q,
|
|
133
|
+
ids: parsed.organization_id ? [parsed.organization_id] : undefined,
|
|
134
|
+
}), { method: "GET" }));
|
|
135
|
+
const q = parsed.q?.toLowerCase();
|
|
136
|
+
const users = rows.flatMap((organization) => {
|
|
137
|
+
const members = directoryMembers(organization);
|
|
138
|
+
const matchesOrganization = q ? organizationMatchesQuery(organization, q) : false;
|
|
139
|
+
return members
|
|
140
|
+
.filter((member) => !q ||
|
|
141
|
+
matchesOrganization ||
|
|
142
|
+
(member.name ?? "").toLowerCase().includes(q) || member.email.toLowerCase().includes(q))
|
|
143
|
+
.map((member) => ({
|
|
144
|
+
user_id: member.user_id,
|
|
145
|
+
email: member.email,
|
|
146
|
+
name: member.name ?? null,
|
|
147
|
+
organization_id: organization.id,
|
|
148
|
+
organization_name: organization.name,
|
|
149
|
+
organization_role: organization.role,
|
|
150
|
+
approved: organization.approved,
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
users,
|
|
155
|
+
...(rows.length === limit ? { next_offset: offset + rows.length } : {}),
|
|
156
|
+
};
|
|
47
157
|
},
|
|
48
158
|
});
|
|
49
|
-
export const userCapabilities = [
|
|
159
|
+
export const userCapabilities = [organizationListCapability, usersListCapability];
|
|
@@ -16,6 +16,7 @@ import { mountCapabilities } from "./capabilities/mounts.js";
|
|
|
16
16
|
import { identityCapabilities } from "./capabilities/identity.js";
|
|
17
17
|
import { listingCapabilities } from "./capabilities/listings.js";
|
|
18
18
|
import { observabilityCapabilities } from "./capabilities/observability.js";
|
|
19
|
+
import { organizationInviteCapability } from "./capabilities/organization-invites.js";
|
|
19
20
|
import { networkCapabilities } from "./capabilities/networks.js";
|
|
20
21
|
import { volumeCapabilities } from "./capabilities/volumes.js";
|
|
21
22
|
import { userCapabilities } from "./capabilities/users.js";
|
|
@@ -41,7 +42,7 @@ export const catalog = [
|
|
|
41
42
|
...bindAdminEnrollmentCapabilities(bindAdminCapabilities([
|
|
42
43
|
...cliShellCapabilities,
|
|
43
44
|
...identityCapabilities,
|
|
44
|
-
...listingCapabilities,
|
|
45
|
+
...listingCapabilities.filter((capability) => capability.id !== "listings.create"),
|
|
45
46
|
...commerceMarketCapabilities,
|
|
46
47
|
...marketCapabilities,
|
|
47
48
|
...computeCapabilities,
|
|
@@ -60,8 +61,9 @@ export const catalog = [
|
|
|
60
61
|
...adminNodeLifecycleCapabilities,
|
|
61
62
|
])),
|
|
62
63
|
...adminCapabilities.filter((capability) => capability.id === "nodes.health"),
|
|
63
|
-
...adminCapacityReservationCapabilities
|
|
64
|
+
...adminCapacityReservationCapabilities,
|
|
64
65
|
...adminNodeDiagnosticCapabilities,
|
|
65
66
|
...adminEnrollmentCapabilities,
|
|
67
|
+
organizationInviteCapability,
|
|
66
68
|
]),
|
|
67
69
|
];
|
|
@@ -231,7 +231,6 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
231
231
|
{ legacy: ["tokens", "list"], replacement: "ornn admin enrollment-tokens list" },
|
|
232
232
|
{ legacy: ["tokens", "create"], replacement: "ornn admin enrollment-tokens create" },
|
|
233
233
|
{ legacy: ["tokens", "revoke"], replacement: "ornn admin enrollment-tokens revoke" },
|
|
234
|
-
{ legacy: ["users", "query"], replacement: "ornn admin users search" },
|
|
235
234
|
{ legacy: ["node", "enroll"], replacement: "ornn admin nodes enroll" },
|
|
236
235
|
{ legacy: ["node", "list"], replacement: "ornn admin nodes list" },
|
|
237
236
|
{ legacy: ["node", "health"], replacement: "ornn admin nodes health" },
|
|
@@ -247,7 +246,7 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
247
246
|
{ legacy: ["operators"], replacement: "ornn admin suppliers list" },
|
|
248
247
|
{ legacy: ["facilities"], replacement: "ornn admin facilities list" },
|
|
249
248
|
{ legacy: ["tokens"], replacement: "ornn admin enrollment-tokens list" },
|
|
250
|
-
{ legacy: ["users"], replacement: "ornn admin users
|
|
249
|
+
{ legacy: ["users"], replacement: "ornn admin users list" },
|
|
251
250
|
{ legacy: ["node"], replacement: "ornn admin nodes list" },
|
|
252
251
|
{ legacy: ["cache", "purge"], replacement: "ornn resources" },
|
|
253
252
|
{ legacy: ["api"], replacement: "ornn resources" },
|