@ornncompute/cli 1.0.35 → 1.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +92 -47
  2. package/package.json +2 -2
  3. package/src/admin-capacity-reservations.mjs +65 -153
  4. package/src/admin-command-handlers.mjs +2 -0
  5. package/src/admin-credits.mjs +57 -0
  6. package/src/admin-node-lifecycle.mjs +48 -7
  7. package/src/admin-shell.mjs +113 -27
  8. package/src/catalog-shell.mjs +13 -21
  9. package/src/evidence-commands.mjs +30 -12
  10. package/src/kubernetes-commands.mjs +58 -16
  11. package/src/launch-mode-handlers.mjs +2 -2
  12. package/src/storage-commands.mjs +9 -1
  13. package/src/vm-commands.mjs +3 -3
  14. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +55 -57
  15. package/vendor/capabilities/capabilities/admin-enrollment.js +3 -3
  16. package/vendor/capabilities/capabilities/admin.js +9 -7
  17. package/vendor/capabilities/capabilities/commerce-market.js +179 -104
  18. package/vendor/capabilities/capabilities/compute.js +4 -2
  19. package/vendor/capabilities/capabilities/credits.js +78 -0
  20. package/vendor/capabilities/capabilities/fleet.js +6 -5
  21. package/vendor/capabilities/capabilities/listings.js +58 -38
  22. package/vendor/capabilities/capabilities/market-shared.js +2 -0
  23. package/vendor/capabilities/capabilities/markets.js +254 -221
  24. package/vendor/capabilities/capabilities/native-listing.js +63 -0
  25. package/vendor/capabilities/capabilities/organization-invites.js +53 -0
  26. package/vendor/capabilities/capabilities/reservation-list.js +68 -0
  27. package/vendor/capabilities/capabilities/staff-shared.js +64 -0
  28. package/vendor/capabilities/capabilities/users.js +141 -31
  29. package/vendor/capabilities/cli-catalog.js +8 -2
  30. package/vendor/capabilities/cli-migrations.js +13 -10
  31. package/vendor/capabilities/software-catalog.js +52 -6
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
- import { defineCapability } from "../capability.js";
3
- import { consent, flag, idempotencyHeaders, microUsdLabel, positionals, query, seg, } from "./market-shared.js";
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", "internal", "delisted"]).optional(),
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: pageLimit,
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 Commerce listings. Primary listings are shown to approved organizations; secondary listings (relists) to every signed-in organization. Prices are micro-USD per GPU-hour.",
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 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"]),
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 Commerce listing: specs, window, node count and its buyer price.",
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, 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"]),
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 listingRelistInput = z
86
- .object({
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
- sourceReservationId: z
89
- .string()
90
- .uuid()
99
+ listingId: z.string().uuid(),
100
+ from: z.iso
101
+ .datetime({ offset: true })
91
102
  .optional()
92
- .describe("A confirmed or active schedule reservation you hold."),
93
- sourceListingId: z
94
- .string()
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("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."),
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 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.",
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/listings" },
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 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"]),
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 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
- };
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
- 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);
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("/commerce/listings", {
162
+ return ctx.fetch(`/commerce/reservations/${seg(reservationId)}/relist`, {
152
163
  method: "POST",
153
164
  body,
154
- headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
165
+ headers: idempotencyHeaders(ctx, idempotencyKey),
155
166
  });
156
167
  },
157
168
  });
158
- export const listingUpdateInput = z.object({
169
+ export const listingUpdateInput = z
170
+ .object({
159
171
  $positionals: positionals,
160
172
  listingId: z.string().uuid(),
161
- askPricePerGpuHourMicroUsd: microUsd,
162
- confirm: z.boolean().optional(),
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: ["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.",
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: 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"]),
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 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 });
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: "DELETE", path: "/commerce/listings/{listingId}" },
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"], 25, ["listing ID"]),
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: "DELETE" });
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"], 26, ["opaque page cursor"]),
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"], 27, ["reservation ID"]),
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"], 28, ["reservation ID"]),
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 feesShowInput = z.object({ $positionals: positionals });
305
- export const feesShowCapability = defineCapability({
306
- id: "fees.show",
307
- domain: "billing",
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: "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,
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: ["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),
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) => ctx.fetch("/commerce/fees", { method: "GET" }),
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
- feesShowCapability,
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
- email: z.string().optional(),
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 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.",
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
- email: {
132
+ reservation_id: {
132
133
  type: "string",
133
- description: "Optional. List nodes for this user's organizations.",
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
- email: parsed.email,
166
+ reservation_id: parsed.reservation_id ?? parsed.reservationId,
166
167
  ...pagination,
167
168
  }), { method: "GET" });
168
169
  },