@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { nativeTermsInput, nativeTermFields, listingWriteControls } from "./native-listing.js";
|
|
3
|
+
import { defineCapability, preview } from "../capability.js";
|
|
4
|
+
import { consent, idempotencyHeaders, microUsd, microUsdLabel, positionals, query, seg, } from "./market-shared.js";
|
|
4
5
|
// Commerce market verbs: one capability per role-aware `/commerce` route.
|
|
5
6
|
// Commerce decides from the caller's session what each listing or
|
|
6
7
|
// reservation reveals; these verbs return its JSON unchanged.
|
|
@@ -18,39 +19,32 @@ function help(command, outcome, human, automation, related, order, acceptedRefer
|
|
|
18
19
|
order,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
const microUsd = z.coerce.number().int().positive();
|
|
22
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
23
|
export const listingsListInput = z.object({
|
|
30
24
|
$positionals: positionals,
|
|
31
|
-
kind: z.enum(["primary", "secondary"]).optional(),
|
|
32
|
-
state: z.enum(["available", "
|
|
25
|
+
kind: z.enum(["primary", "secondary", "on_demand"]).optional(),
|
|
26
|
+
state: z.enum(["draft", "available", "held", "sold", "delisted"]).optional(),
|
|
33
27
|
sourceReservationId: z.string().uuid().optional(),
|
|
34
28
|
sourceListingId: z.string().uuid().optional(),
|
|
35
|
-
limit:
|
|
29
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
36
30
|
cursor: z.string().min(1).optional(),
|
|
37
31
|
});
|
|
38
32
|
export const listingsListCapability = defineCapability({
|
|
39
33
|
id: "listings.list",
|
|
40
34
|
domain: "listings",
|
|
41
35
|
roles: ["user"],
|
|
42
|
-
description: "List
|
|
36
|
+
description: "List native capacity listings. Primary listings require organization approval. Returns capacityListingId, hardware, capacity and availability; read listings.terms separately for prices and durations.",
|
|
43
37
|
mutation: "read",
|
|
44
|
-
http: { method: "GET", path: "/commerce/listings" },
|
|
38
|
+
http: { method: "GET", path: "/commerce/v2/listings" },
|
|
45
39
|
input: listingsListInput,
|
|
46
40
|
cli: {
|
|
47
41
|
command: ["capacity-offers", "list"],
|
|
48
|
-
vnext: help(["capacity-offers", "list"], "List
|
|
42
|
+
vnext: help(["capacity-offers", "list"], "List capacity parents with bounded pagination; read their financial terms separately.", "ornn capacity-offers list [--kind primary|secondary|on_demand] [--source-reservation-id <id>] [--limit <1-100>] [--cursor <opaque>]", "ornn capacity-offers list --limit 50 --format json", ["capacity-offers show", "capacity-offers terms", "capacity-offers availability"], 20, ["opaque page cursor"]),
|
|
49
43
|
},
|
|
50
44
|
slack: false,
|
|
51
45
|
execute: async (ctx, input) => {
|
|
52
46
|
const parsed = listingsListInput.parse(input ?? {});
|
|
53
|
-
return ctx.fetch(`/commerce/listings${query({
|
|
47
|
+
return ctx.fetch(`/commerce/v2/listings${query({
|
|
54
48
|
kind: parsed.kind,
|
|
55
49
|
state: parsed.state,
|
|
56
50
|
sourceReservationId: parsed.sourceReservationId,
|
|
@@ -68,122 +62,145 @@ export const listingShowCapability = defineCapability({
|
|
|
68
62
|
id: "listings.show",
|
|
69
63
|
domain: "listings",
|
|
70
64
|
roles: ["user"],
|
|
71
|
-
description: "Show one
|
|
65
|
+
description: "Show one native capacity listing. Returns capacityListingId, hardware, capacity and availability; use listings.terms for financial choices.",
|
|
72
66
|
mutation: "read",
|
|
73
|
-
http: { method: "GET", path: "/commerce/listings/{listingId}" },
|
|
67
|
+
http: { method: "GET", path: "/commerce/v2/listings/{listingId}" },
|
|
74
68
|
input: listingShowInput,
|
|
75
69
|
cli: {
|
|
76
70
|
command: ["capacity-offers", "show"],
|
|
77
|
-
vnext: help(["capacity-offers", "show"], "Inspect one listing's hardware,
|
|
71
|
+
vnext: help(["capacity-offers", "show"], "Inspect one listing's hardware, capacity and availability.", "ornn capacity-offers show --listing-id <id>", "ornn capacity-offers show --listing-id <id> --format json", ["capacity-offers list", "capacity-offers terms", "capacity-offers availability"], 21, ["listing ID"]),
|
|
78
72
|
},
|
|
79
73
|
slack: false,
|
|
80
74
|
execute: async (ctx, input) => {
|
|
81
75
|
const parsed = listingShowInput.parse(input);
|
|
82
|
-
return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "GET" });
|
|
76
|
+
return ctx.fetch(`/commerce/v2/listings/${seg(parsed.listingId)}`, { method: "GET" });
|
|
83
77
|
},
|
|
84
78
|
});
|
|
85
|
-
export const
|
|
86
|
-
.
|
|
79
|
+
export const listingTermsCapability = defineCapability({
|
|
80
|
+
id: "listings.terms",
|
|
81
|
+
domain: "listings",
|
|
82
|
+
roles: ["user"],
|
|
83
|
+
description: "Read a capacity listing's advertised financial terms. Each term returns listingTermsId, capacityListingId, pricePerGpuHourMicroUsd, duration bounds, downPaymentCents and renewable. Use the parent's capacityListingId as listingId. Prices are gross integer micro-USD per GPU-hour, the price reservations.purchase expects.",
|
|
84
|
+
mutation: "read",
|
|
85
|
+
http: { method: "GET", path: "/commerce/v2/listings/{listingId}/terms" },
|
|
86
|
+
input: listingShowInput,
|
|
87
|
+
cli: {
|
|
88
|
+
command: ["capacity-offers", "terms"],
|
|
89
|
+
vnext: help(["capacity-offers", "terms"], "Read the advertised financial terms for a capacity listing.", "ornn capacity-offers terms --listing-id <id>", "ornn capacity-offers terms --listing-id <id> --format json", ["capacity-offers show", "offers post", "reservations buy"], 22, ["capacityListingId from capacity-offers list or show"]),
|
|
90
|
+
},
|
|
91
|
+
slack: false,
|
|
92
|
+
execute: async (ctx, input) => {
|
|
93
|
+
const parsed = listingShowInput.parse(input);
|
|
94
|
+
return ctx.fetch(`/commerce/v2/listings/${seg(parsed.listingId)}/terms`, { method: "GET" });
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
export const listingAvailabilityInput = z.object({
|
|
87
98
|
$positionals: positionals,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
99
|
+
listingId: z.string().uuid(),
|
|
100
|
+
from: z.iso
|
|
101
|
+
.datetime({ offset: true })
|
|
91
102
|
.optional()
|
|
92
|
-
.describe("
|
|
93
|
-
|
|
94
|
-
.
|
|
95
|
-
.uuid()
|
|
103
|
+
.describe("RFC 3339 start of the range to read. Defaults to now or the listing's start."),
|
|
104
|
+
to: z.iso
|
|
105
|
+
.datetime({ offset: true })
|
|
96
106
|
.optional()
|
|
97
|
-
.describe("
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
.describe("RFC 3339 end of the range to read. Defaults to the listing's end."),
|
|
108
|
+
});
|
|
109
|
+
export const listingAvailabilityCapability = defineCapability({
|
|
110
|
+
id: "listings.availability",
|
|
111
|
+
domain: "listings",
|
|
112
|
+
roles: ["user"],
|
|
113
|
+
description: "Read how much of a capacity listing is free to book over time: windows of startsAt, endsAt, gpus and nodes (the nodes with any GPU free) from now until the listing ends (for an open-ended listing, 30 days ahead for a Market listing and far enough for any term of a Reserve listing), optionally narrowed by from and to. Use the parent's capacityListingId as listingId.",
|
|
114
|
+
mutation: "read",
|
|
115
|
+
http: { method: "GET", path: "/v1/markets/listings/{listingId}/availability" },
|
|
116
|
+
input: listingAvailabilityInput,
|
|
117
|
+
cli: {
|
|
118
|
+
command: ["capacity-offers", "availability"],
|
|
119
|
+
vnext: help(["capacity-offers", "availability"], "Show how many of a listing's nodes are free over time.", "ornn capacity-offers availability --listing-id <id> [--from <time>] [--to <time>]", "ornn capacity-offers availability --listing-id <id> --format json", ["capacity-offers terms", "reservations buy", "offers post"], 23, ["capacityListingId from capacity-offers list or show"]),
|
|
120
|
+
},
|
|
121
|
+
slack: false,
|
|
122
|
+
execute: async (ctx, input) => {
|
|
123
|
+
const parsed = listingAvailabilityInput.parse(input);
|
|
124
|
+
return ctx.fetch(`/v1/markets/listings/${seg(parsed.listingId)}/availability${query({
|
|
125
|
+
from: parsed.from,
|
|
126
|
+
to: parsed.to,
|
|
127
|
+
})}`, { method: "GET" });
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
export const listingRelistInput = z.strictObject({
|
|
131
|
+
$positionals: positionals,
|
|
132
|
+
reservationId: z.string().uuid(),
|
|
133
|
+
startsAt: z.iso.datetime({ offset: true }),
|
|
134
|
+
endsAt: z.iso.datetime({ offset: true }),
|
|
135
|
+
nodeCount: z.coerce.number().int().positive(),
|
|
136
|
+
terms: nativeTermsInput,
|
|
105
137
|
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
106
138
|
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"],
|
|
139
|
+
yes: z.boolean().optional(),
|
|
111
140
|
});
|
|
112
141
|
export const listingRelistCapability = defineCapability({
|
|
113
142
|
id: "listings.relist",
|
|
114
143
|
domain: "listings",
|
|
115
144
|
roles: ["user"],
|
|
116
|
-
description: "Relist
|
|
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.",
|
|
145
|
+
description: "Relist whole nodes of a reservation's capacity using a native availability window, nodeCount and financial terms. Buyers run on nodes Ornn assigns when their rental starts. Requires confirm: true.",
|
|
118
146
|
mutation: "preview-confirm",
|
|
119
|
-
http: { method: "POST", path: "/commerce/
|
|
147
|
+
http: { method: "POST", path: "/commerce/reservations/{reservationId}/relist" },
|
|
120
148
|
input: listingRelistInput,
|
|
121
149
|
cli: {
|
|
122
150
|
command: ["capacity-offers", "relist"],
|
|
123
|
-
vnext: help(["capacity-offers", "relist"], "Relist
|
|
151
|
+
vnext: help(["capacity-offers", "relist"], "Relist reserved capacity with explicit financial terms.", "ornn capacity-offers relist --reservation-id UUID --node-count COUNT --starts-at TIME --ends-at TIME --terms JSON", "ornn capacity-offers relist --reservation-id UUID --node-count 1 --starts-at TIME --ends-at TIME --terms JSON --idempotency-key KEY --yes --format json", ["capacity-offers terms", "capacity-offers delist"], 24, ["reservation ID"]),
|
|
124
152
|
},
|
|
125
153
|
slack: false,
|
|
126
154
|
execute: async (ctx, input) => {
|
|
127
|
-
const
|
|
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
|
-
};
|
|
155
|
+
const { $positionals: _, reservationId, confirm: _confirm, yes: _yes, idempotencyKey, ...body } = listingRelistInput.parse(input);
|
|
137
156
|
const gate = await consent(ctx, "listings.relist", "Relist capacity", [
|
|
138
|
-
{
|
|
139
|
-
|
|
140
|
-
|
|
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);
|
|
157
|
+
{ label: "Reservation", value: reservationId },
|
|
158
|
+
{ label: "Capacity and financial terms", value: JSON.stringify(body) },
|
|
159
|
+
], { reservationId, ...body });
|
|
149
160
|
if (gate)
|
|
150
161
|
return gate;
|
|
151
|
-
return ctx.fetch(
|
|
162
|
+
return ctx.fetch(`/commerce/reservations/${seg(reservationId)}/relist`, {
|
|
152
163
|
method: "POST",
|
|
153
164
|
body,
|
|
154
|
-
headers: idempotencyHeaders(ctx,
|
|
165
|
+
headers: idempotencyHeaders(ctx, idempotencyKey),
|
|
155
166
|
});
|
|
156
167
|
},
|
|
157
168
|
});
|
|
158
|
-
export const listingUpdateInput = z
|
|
169
|
+
export const listingUpdateInput = z
|
|
170
|
+
.object({
|
|
159
171
|
$positionals: positionals,
|
|
160
172
|
listingId: z.string().uuid(),
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
listingTermsId: z.string().uuid(),
|
|
174
|
+
...nativeTermFields,
|
|
175
|
+
...listingWriteControls,
|
|
176
|
+
})
|
|
177
|
+
.strict();
|
|
164
178
|
export const listingUpdateCapability = defineCapability({
|
|
165
179
|
id: "listings.update",
|
|
166
180
|
domain: "listings",
|
|
167
|
-
roles: ["
|
|
168
|
-
description: "
|
|
181
|
+
roles: ["admin"],
|
|
182
|
+
description: "Staff only: edit one advertised native financial term with its complete financial fields. Reuse idempotencyKey on retries. Requires confirm: true.",
|
|
169
183
|
mutation: "preview-confirm",
|
|
170
|
-
http: { method: "PATCH", path: "/commerce/listings/{listingId}" },
|
|
184
|
+
http: { method: "PATCH", path: "/commerce/v2/listings/{listingId}/terms/{listingTermsId}" },
|
|
171
185
|
input: listingUpdateInput,
|
|
172
186
|
cli: {
|
|
173
|
-
command: ["capacity-offers", "update"],
|
|
174
|
-
vnext:
|
|
187
|
+
command: ["admin", "capacity-offers", "update"],
|
|
188
|
+
vnext: {
|
|
189
|
+
...help(["admin", "capacity-offers", "update"], "Edit one advertised financial term as staff.", "ornn admin capacity-offers update --listing-id UUID --listing-terms-id UUID --min-duration-hours HOURS --max-duration-hours HOURS --price-per-gpu-hour-micro-usd RATE --renewable true|false --idempotency-key KEY [--type reserved|spot|on-demand] [--down-payment-cents CENTS]", "Requires an eligible human terminal; reuse --idempotency-key on retries.", ["capacity-offers terms"], 25),
|
|
190
|
+
audience: "admin",
|
|
191
|
+
},
|
|
175
192
|
},
|
|
176
193
|
slack: false,
|
|
194
|
+
mcp: false,
|
|
177
195
|
execute: async (ctx, input) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "PATCH", body });
|
|
196
|
+
const { $positionals: _, listingId, listingTermsId, confirm: _confirm, idempotencyKey, ...body } = listingUpdateInput.parse(input);
|
|
197
|
+
if (!ctx.confirmed)
|
|
198
|
+
return preview("listings.update", { listingId, listingTermsId, ...body });
|
|
199
|
+
return ctx.fetch(`/commerce/v2/listings/${seg(listingId)}/terms/${seg(listingTermsId)}`, {
|
|
200
|
+
method: "PATCH",
|
|
201
|
+
body,
|
|
202
|
+
headers: idempotencyHeaders(ctx, idempotencyKey),
|
|
203
|
+
});
|
|
187
204
|
},
|
|
188
205
|
});
|
|
189
206
|
export const listingDelistInput = z.object({
|
|
@@ -197,11 +214,11 @@ export const listingDelistCapability = defineCapability({
|
|
|
197
214
|
roles: ["user"],
|
|
198
215
|
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
216
|
mutation: "preview-confirm",
|
|
200
|
-
http: { method: "
|
|
217
|
+
http: { method: "POST", path: "/commerce/v2/listings/{listingId}/delist" },
|
|
201
218
|
input: listingDelistInput,
|
|
202
219
|
cli: {
|
|
203
220
|
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"],
|
|
221
|
+
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"], 26, ["listing ID"]),
|
|
205
222
|
},
|
|
206
223
|
slack: false,
|
|
207
224
|
execute: async (ctx, input) => {
|
|
@@ -215,7 +232,7 @@ export const listingDelistCapability = defineCapability({
|
|
|
215
232
|
], { listingId: parsed.listingId });
|
|
216
233
|
if (gate)
|
|
217
234
|
return gate;
|
|
218
|
-
return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "
|
|
235
|
+
return ctx.fetch(`/commerce/v2/listings/${seg(parsed.listingId)}/delist`, { method: "POST" });
|
|
219
236
|
},
|
|
220
237
|
});
|
|
221
238
|
export const reservationsListInput = z.object({
|
|
@@ -245,7 +262,7 @@ export const reservationsListCapability = defineCapability({
|
|
|
245
262
|
input: reservationsListInput,
|
|
246
263
|
cli: {
|
|
247
264
|
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"],
|
|
265
|
+
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"], 27, ["opaque page cursor"]),
|
|
249
266
|
},
|
|
250
267
|
slack: false,
|
|
251
268
|
execute: async (ctx, input) => {
|
|
@@ -273,7 +290,7 @@ export const reservationsShowCapability = defineCapability({
|
|
|
273
290
|
input: reservationLookupInput,
|
|
274
291
|
cli: {
|
|
275
292
|
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"],
|
|
293
|
+
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"], 28, ["reservation ID"]),
|
|
277
294
|
},
|
|
278
295
|
slack: false,
|
|
279
296
|
execute: async (ctx, input) => {
|
|
@@ -291,7 +308,7 @@ export const reservationInvoicesCapability = defineCapability({
|
|
|
291
308
|
input: reservationLookupInput,
|
|
292
309
|
cli: {
|
|
293
310
|
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"],
|
|
311
|
+
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"], 29, ["reservation ID"]),
|
|
295
312
|
},
|
|
296
313
|
slack: false,
|
|
297
314
|
execute: async (ctx, input) => {
|
|
@@ -301,30 +318,88 @@ export const reservationInvoicesCapability = defineCapability({
|
|
|
301
318
|
});
|
|
302
319
|
},
|
|
303
320
|
});
|
|
304
|
-
export const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
321
|
+
export const reservationPurchaseInput = z.object({
|
|
322
|
+
$positionals: positionals,
|
|
323
|
+
listingId: z
|
|
324
|
+
.string()
|
|
325
|
+
.uuid()
|
|
326
|
+
.describe("The relisted (secondary) listing's capacityListingId, from listings.list."),
|
|
327
|
+
listingTermId: z
|
|
328
|
+
.string()
|
|
329
|
+
.uuid()
|
|
330
|
+
.describe("The listingTermsId of the listing's term, from listings.terms."),
|
|
331
|
+
nodeCount: z.coerce
|
|
332
|
+
.number()
|
|
333
|
+
.int()
|
|
334
|
+
.positive()
|
|
335
|
+
.optional()
|
|
336
|
+
.describe("Whole nodes to book for the window. Defaults to every node the listing sells."),
|
|
337
|
+
startsAt: z.iso
|
|
338
|
+
.datetime({ offset: true })
|
|
339
|
+
.describe("RFC 3339 start of the window, in whole seconds, inside the listing and not in the past."),
|
|
340
|
+
endsAt: z.iso
|
|
341
|
+
.datetime({ offset: true })
|
|
342
|
+
.describe("RFC 3339 end of the window, at least an hour after the start and within the term's duration range, or at the listing's end."),
|
|
343
|
+
pricePerGpuHourMicroUsd: microUsd.describe("The term's price from listings.terms, in micro-USD per GPU-hour. A changed price refuses the purchase."),
|
|
344
|
+
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
345
|
+
confirm: z.boolean().optional(),
|
|
346
|
+
});
|
|
347
|
+
export const reservationPurchaseCapability = defineCapability({
|
|
348
|
+
id: "reservations.purchase",
|
|
349
|
+
domain: "reservations",
|
|
308
350
|
roles: ["user"],
|
|
309
|
-
description: "
|
|
310
|
-
mutation: "
|
|
311
|
-
http: { method: "
|
|
312
|
-
input:
|
|
351
|
+
description: "Book a window of a relisted (secondary) listing on one of its terms at the term's price. The GPUs are held for you until the window starts; then it becomes your reservation, with the booking's ID. Nothing is charged when you book. Primary listings take offers instead (offers.post). Requires confirm: true.",
|
|
352
|
+
mutation: "preview-confirm",
|
|
353
|
+
http: { method: "POST", path: "/v1/markets/bookings" },
|
|
354
|
+
input: reservationPurchaseInput,
|
|
313
355
|
cli: {
|
|
314
|
-
command: ["
|
|
315
|
-
vnext: help(["
|
|
356
|
+
command: ["reservations", "buy"],
|
|
357
|
+
vnext: help(["reservations", "buy"], "Book a window of relisted capacity at its term's price.", "ornn reservations buy --listing-id <id> --listing-term-id <id> --starts-at <time> --ends-at <time> --price-per-gpu-hour-micro-usd <n> [--node-count <n>]", "ornn reservations buy --listing-id <id> --listing-term-id <id> --starts-at <time> --ends-at <time> --price-per-gpu-hour-micro-usd 4000000 --idempotency-key <key> --yes --format json", ["capacity-offers availability", "capacity-offers terms", "reservations show"], 30, ["listingTermsId from capacity-offers terms"]),
|
|
316
358
|
},
|
|
317
359
|
slack: false,
|
|
318
|
-
execute: async (ctx) =>
|
|
360
|
+
execute: async (ctx, input) => {
|
|
361
|
+
const parsed = reservationPurchaseInput.parse(input);
|
|
362
|
+
const body = {
|
|
363
|
+
legs: [
|
|
364
|
+
{
|
|
365
|
+
listingId: parsed.listingId,
|
|
366
|
+
listingTermId: parsed.listingTermId,
|
|
367
|
+
...(parsed.nodeCount !== undefined ? { nodes: parsed.nodeCount } : {}),
|
|
368
|
+
startsAt: parsed.startsAt,
|
|
369
|
+
endsAt: parsed.endsAt,
|
|
370
|
+
pricePerGpuHourMicroUsd: parsed.pricePerGpuHourMicroUsd,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
const gate = await consent(ctx, "reservations.purchase", "Book relisted window", [
|
|
375
|
+
{ label: "Listing term", value: parsed.listingTermId },
|
|
376
|
+
{
|
|
377
|
+
label: "Nodes",
|
|
378
|
+
value: parsed.nodeCount !== undefined ? String(parsed.nodeCount) : "Every node on the listing",
|
|
379
|
+
},
|
|
380
|
+
{ label: "Window", value: `${parsed.startsAt} to ${parsed.endsAt}` },
|
|
381
|
+
{ label: "Price", value: microUsdLabel(parsed.pricePerGpuHourMicroUsd) },
|
|
382
|
+
{ label: "Payment", value: "Nothing is charged when you book." },
|
|
383
|
+
], body);
|
|
384
|
+
if (gate)
|
|
385
|
+
return gate;
|
|
386
|
+
return ctx.fetch("/v1/markets/bookings", {
|
|
387
|
+
method: "POST",
|
|
388
|
+
body,
|
|
389
|
+
headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
|
|
390
|
+
});
|
|
391
|
+
},
|
|
319
392
|
});
|
|
320
393
|
export const commerceMarketCapabilities = [
|
|
321
394
|
listingsListCapability,
|
|
322
395
|
listingShowCapability,
|
|
396
|
+
listingTermsCapability,
|
|
397
|
+
listingAvailabilityCapability,
|
|
323
398
|
listingRelistCapability,
|
|
324
399
|
listingUpdateCapability,
|
|
325
400
|
listingDelistCapability,
|
|
326
401
|
reservationsListCapability,
|
|
327
402
|
reservationsShowCapability,
|
|
328
403
|
reservationInvoicesCapability,
|
|
329
|
-
|
|
404
|
+
reservationPurchaseCapability,
|
|
330
405
|
];
|
|
@@ -23,6 +23,7 @@ export const launchInput = z
|
|
|
23
23
|
resume: z.union([z.string().min(1), z.literal(true)]).optional(),
|
|
24
24
|
timeout: z.string().optional(),
|
|
25
25
|
username: z.string().optional(),
|
|
26
|
+
vms: z.string().optional(),
|
|
26
27
|
wait: z.boolean().optional(),
|
|
27
28
|
yes: z.boolean().optional(),
|
|
28
29
|
volume: z.string().optional(),
|
|
@@ -139,11 +140,12 @@ export const launchCapability = cliOnly({
|
|
|
139
140
|
category: "journey",
|
|
140
141
|
order: 2,
|
|
141
142
|
outcome: "Launch one reviewed VM, Bare Metal machine, Kubernetes cluster, or Slurm cluster from active reserved capacity, including a declared durable Bare Metal workspace.",
|
|
142
|
-
human: "ornn launch [<capacity-reservation>] --mode vm|bare-metal|kubernetes|slurm [--key <ssh-key>]; Bare Metal may add --network shared --volume <managed-volume> --mount-access read-only|read-write; Kubernetes/Slurm prompt for missing --network public|private and --nodes <membership-count>",
|
|
143
|
-
automation: "ornn launch <canonical-reservation-id> --mode vm|bare-metal|kubernetes|slurm --organization <id> --yes --idempotency-key <key> --format json; VM/Bare Metal/Slurm require --key <canonical-key-id>; Bare Metal may add --network shared --volume <canonical-volume-id> --mount-access read-only|read-write; Kubernetes/Slurm require --network public|private --nodes <membership-count> --machines <canonical-machine-ids>",
|
|
143
|
+
human: "ornn launch [<capacity-reservation>] --mode vm|bare-metal|kubernetes|slurm [--key <ssh-key>]; Bare Metal may add --network shared --volume <managed-volume> --mount-access read-only|read-write; Kubernetes/Slurm prompt for missing --network public|private and --nodes <membership-count>, and may add running VMs with --vms <vm-ids>",
|
|
144
|
+
automation: "ornn launch <canonical-reservation-id> --mode vm|bare-metal|kubernetes|slurm --organization <id> --yes --idempotency-key <key> --format json; VM/Bare Metal/Slurm require --key <canonical-key-id>; Bare Metal may add --network shared --volume <canonical-volume-id> --mount-access read-only|read-write; Kubernetes/Slurm require --network public|private --nodes <membership-count> --machines <canonical-machine-ids>, and may add --vms <canonical-vm-ids> (with --nodes 0 for a cluster of VMs only)",
|
|
144
145
|
acceptedReferences: [
|
|
145
146
|
"canonical capacity-reservation ID",
|
|
146
147
|
"canonical eligible machine IDs",
|
|
148
|
+
"canonical running VM IDs for cluster membership",
|
|
147
149
|
"invocation-local eligible reservation and machine choices",
|
|
148
150
|
"exact SSH-key and managed-volume references; organization network shared",
|
|
149
151
|
],
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability, preview } from "../capability.js";
|
|
3
|
+
export const creditGrantInput = z.object({
|
|
4
|
+
organizationId: z
|
|
5
|
+
.uuid()
|
|
6
|
+
.describe("Customer organization UUID. Resolve names or emails with organization_list; ask which organization if matches are ambiguous."),
|
|
7
|
+
adjustmentId: z
|
|
8
|
+
.uuid()
|
|
9
|
+
.describe("The credit-adjustment API's required grant UUID. Keep the same UUID when confirming this grant."),
|
|
10
|
+
amountCents: z
|
|
11
|
+
.number()
|
|
12
|
+
.int()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(100_000_000)
|
|
15
|
+
.describe("USD cents: 10000 grants $100.00."),
|
|
16
|
+
note: z
|
|
17
|
+
.string()
|
|
18
|
+
.trim()
|
|
19
|
+
.min(1)
|
|
20
|
+
.refine((value) => new TextEncoder().encode(value).length <= 500, "Keep the note under 500 UTF-8 bytes.")
|
|
21
|
+
.describe("Reason for the credit grant."),
|
|
22
|
+
confirm: z.boolean().optional(),
|
|
23
|
+
});
|
|
24
|
+
export const creditGrantCapability = defineCapability({
|
|
25
|
+
id: "credits.grant",
|
|
26
|
+
domain: "billing",
|
|
27
|
+
roles: ["admin"],
|
|
28
|
+
description: "Grant Ornn credits to a customer organization using the same operation as Internal Billing. " +
|
|
29
|
+
"Credits belong to the organization, not an individual person. Resolve the intended organization first. " +
|
|
30
|
+
"Preview first; confirm:true posts the same grant with its adjustmentId. The verified staff actor supplies audit identity.",
|
|
31
|
+
mutation: "preview-confirm",
|
|
32
|
+
http: { method: "POST", path: "/internal/v1/organizations/{organizationId}/credit-adjustments" },
|
|
33
|
+
cli: {
|
|
34
|
+
command: ["admin", "organizations", "grant-credit"],
|
|
35
|
+
vnext: {
|
|
36
|
+
audience: "admin",
|
|
37
|
+
category: "resource",
|
|
38
|
+
command: ["admin", "organizations", "grant-credit"],
|
|
39
|
+
outcome: "Grant Ornn credits to a customer organization.",
|
|
40
|
+
human: "ornn admin organizations grant-credit --organization UUID --amount-cents N --note TEXT --idempotency-key UUID [--yes]",
|
|
41
|
+
automation: "Requires an eligible human terminal.",
|
|
42
|
+
acceptedReferences: ["organization UUID from admin organizations list"],
|
|
43
|
+
review: "Review the organization, USD amount, note, and grant UUID before default-no consent.",
|
|
44
|
+
related: ["admin organizations list", "admin users list"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
slack: {
|
|
48
|
+
parameters: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
organizationId: {
|
|
52
|
+
type: "string",
|
|
53
|
+
format: "uuid",
|
|
54
|
+
description: "Exact customer organization UUID from organization_list.",
|
|
55
|
+
},
|
|
56
|
+
amountCents: {
|
|
57
|
+
type: "integer",
|
|
58
|
+
minimum: 1,
|
|
59
|
+
maximum: 100_000_000,
|
|
60
|
+
description: "USD cents: 10000 grants $100.00.",
|
|
61
|
+
},
|
|
62
|
+
note: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Reason for the credit grant, at most 500 UTF-8 bytes.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ["organizationId", "amountCents", "note"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
input: creditGrantInput,
|
|
71
|
+
execute: async (ctx, input) => {
|
|
72
|
+
const { organizationId, confirm: _confirm, ...grant } = creditGrantInput.parse(input);
|
|
73
|
+
const body = { ...grant, direction: "credit" };
|
|
74
|
+
if (!ctx.confirmed)
|
|
75
|
+
return preview("credits.grant", { organizationId, ...body });
|
|
76
|
+
return ctx.fetch(`/internal/v1/organizations/${encodeURIComponent(organizationId)}/credit-adjustments`, { method: "POST", body });
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -27,7 +27,8 @@ const nodesInput = z.object({
|
|
|
27
27
|
facility_id: z.string().optional(),
|
|
28
28
|
facilityId: z.string().optional(),
|
|
29
29
|
status: z.string().optional(),
|
|
30
|
-
|
|
30
|
+
reservation_id: z.string().optional(),
|
|
31
|
+
reservationId: z.string().optional(),
|
|
31
32
|
cursor: z.string().optional(),
|
|
32
33
|
max: z.number().int().positive().max(5000).optional(),
|
|
33
34
|
limit: z.number().int().positive().max(5000).optional(),
|
|
@@ -113,7 +114,7 @@ export const listNodesCapability = defineCapability({
|
|
|
113
114
|
id: "nodes.list",
|
|
114
115
|
domain: "fleet",
|
|
115
116
|
roles: ["user", "admin"],
|
|
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
|
|
117
|
+
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 reservation_id lists the nodes on one reservation (find a customer's reservations with reservation.list customerId). 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.",
|
|
117
118
|
mutation: "read",
|
|
118
119
|
http: { method: "GET", path: "/nodes" },
|
|
119
120
|
slack: {
|
|
@@ -128,9 +129,9 @@ export const listNodesCapability = defineCapability({
|
|
|
128
129
|
type: "string",
|
|
129
130
|
description: "Optional commerce facility UUID from facilities.list.",
|
|
130
131
|
},
|
|
131
|
-
|
|
132
|
+
reservation_id: {
|
|
132
133
|
type: "string",
|
|
133
|
-
description: "Optional
|
|
134
|
+
description: "Optional reservation UUID from reservation_list. Lists that reservation's nodes.",
|
|
134
135
|
},
|
|
135
136
|
status: {
|
|
136
137
|
type: "string",
|
|
@@ -162,7 +163,7 @@ export const listNodesCapability = defineCapability({
|
|
|
162
163
|
organization_id: parsed.organization_id ?? parsed.organizationId,
|
|
163
164
|
facility_id: parsed.facility_id ?? parsed.facilityId,
|
|
164
165
|
status: parsed.status,
|
|
165
|
-
|
|
166
|
+
reservation_id: parsed.reservation_id ?? parsed.reservationId,
|
|
166
167
|
...pagination,
|
|
167
168
|
}), { method: "GET" });
|
|
168
169
|
},
|