@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
package/README.md CHANGED
@@ -59,11 +59,9 @@ ornn resources
59
59
  Advanced customer resources are registered under one noun and one verb:
60
60
 
61
61
  ```text
62
- capacity-offers list | show | relist | update | delist
63
- reservations list | show | invoices
64
- fees show
65
- spot books | book | trades | bid | orders | cancel
66
- schedules list | show | buy
62
+ capacity-offers list | show | terms | availability | relist | delist
63
+ reservations list | show | invoices | buy
64
+ market books | book | trades | bid | orders | cancel
67
65
  offers list | post | accept | cancel
68
66
  machines list | show | wait | switch | reboot | reset | metrics | logs
69
67
  clusters list | show | wait | eligible-machines | add-node | remove-node | teardown | metrics | logs
@@ -82,34 +80,60 @@ filesystems deploy | show | wait | delete
82
80
  `ssh-keys list` uses backend cursors; see [pagination](../../docs/cli.md#advanced-customer-resources).
83
81
 
84
82
  Market commands call one gateway route each and print that route's JSON.
85
- `capacity-offers` reads Commerce listings (`/commerce/listings`) and relists a
86
- schedule reservation you hold, or a primary listing you own, as a secondary
87
- listing with an asking price, its only price: buyers buy its windows at that
88
- price with no acceptance. `reservations` reads Commerce
89
- reservations, schedule and spot alike. `spot`, `schedules`, and `offers` call
90
- Markets (`/v1/markets`): place 1-8 GPU spot bids (one node each), buy a
91
- secondary window at its asking price (`schedules buy`), post offers on a
92
- primary listing's schedule, and accept or cancel offers on primaries you own.
83
+ `capacity-offers list|show|terms` reads native Commerce capacity
84
+ (`/commerce/v2/listings*`). List/show return parent fields with `capacityListingId`;
85
+ pass it as `--listing-id`. `terms` separately returns advertised financial choices
86
+ with `listingTermsId`, price, duration bounds, down payment and renewable flag.
87
+ List accepts limits of 1–100 and the owner cursor unchanged. `availability`
88
+ reads Markets `/v1/markets/listings/{id}/availability` (optional `--from`/`--to`)
89
+ as windows of free GPUs and the whole machines those GPUs make
90
+ ([Markets routes](../../services/markets/README.md#routes)). Relisting uses
91
+ `POST /commerce/reservations/{id}/relist` with `nodeCount`, dates, and a
92
+ `terms` array. The body does not take node IDs. Delisting uses the native parent’s `/delist` POST. `reservations` reads Commerce
93
+ reservations, schedule and spot alike; `reservations buy` books one window of a
94
+ relisted (secondary) listing term with Markets `POST /v1/markets/bookings`
95
+ (`{legs: [...]}`), at the term price `terms` shows; it becomes a reservation
96
+ when the window starts. `market` and `offers` call Markets (`/v1/markets`):
97
+ buy `--nodes` at market (Markets also takes GPU counts and limit bids; the
98
+ CLI does not send them yet), end a fill with
99
+ `market end` (Commerce `POST /commerce/reservations/{id}/stop`), post an offer
100
+ on a whole primary listing for one of its terms (`--listing-term-id`, optional
101
+ `--duration-seconds`), list the offers your organization made or received, and
102
+ accept or cancel offers by `--offer-id`.
93
103
  Prices are integer micro-USD per GPU-hour (`1000000` is $1.00). Every write
94
104
  reviews its terms and needs default-no consent, or `--yes` with
95
- `--format json`; writes accept `--idempotency-key`. Payment methods and
96
- invoices are managed in the web app. Launch consumes already allocated
105
+ `--format json`; writes accept `--idempotency-key`. Customer payment methods and
106
+ billing stay in the web app. Launch consumes already allocated
97
107
  capacity; internal reservation lookups remain necessary for access and
98
108
  ownership.
99
109
 
100
110
  Only admins can create primary listings or reservations through
101
111
  `ornn admin capacity-offers create` and `ornn admin reservations create`.
102
- They post to the role-aware `POST /commerce/listings` and
103
- `POST /commerce/reservations` with the admin's own session; Commerce enforces
112
+ They post to the role-aware `POST /commerce/v2/listings` and Markets
113
+ `POST /v1/markets/bookings` with the admin's own session; the owners enforce
104
114
  the admin role. Both commands retain explicit commercial-term validation and
105
- default-no human confirmation. A new primary listing reaches the market once
106
- staff attach its nodes.
115
+ default-no human confirmation. Reservation creation books a window for a
116
+ customer and takes `--organization-id UUID`, `--listing-id UUID`,
117
+ `--listing-term-id UUID`, `--nodes COUNT`, `--starts-at TIME`, `--ends-at TIME`,
118
+ `--price-per-gpu-hour-micro-usd N`, and `--idempotency-key KEY`. The window is
119
+ whole seconds, the term is non-renewable Reserved, and the price is the term's.
120
+ Reuse the key and payload on retries. Creation requires an eligible human
121
+ terminal. Native capacity creation takes
122
+ `--gpu-slug`, `--capacity-node-count`, `--gpus-per-node`, `--facility-id`,
123
+ `--provisioning-type`, `--state`, `--terms` (a JSON array) and `--idempotency-key`,
124
+ plus optional `--organization-id`. Supply availability dates or `--days-to-rfs`.
125
+ Reuse the key and the same body on retries. Prices are integer micro-USD inside
126
+ terms, with explicit duration bounds, nullable deposit and renewable flag.
127
+ `ornn admin capacity-offers update` edits a selected term using `--listing-id`,
128
+ `--listing-terms-id`, the complete term fields and `--idempotency-key`. Financial
129
+ editing remains staff-only. The old customer `capacity-offers update` is removed.
107
130
 
108
131
  All staff discovery and execution starts at `ornn admin`. A fresh staff preflight
109
- runs before the catalog is shown. The seven roots are:
132
+ runs before the catalog is shown. The eight roots are:
110
133
 
111
134
  ```text
112
135
  ornn admin users
136
+ ornn admin organizations
113
137
  ornn admin suppliers
114
138
  ornn admin facilities
115
139
  ornn admin nodes
@@ -119,6 +143,27 @@ ornn admin reservations
119
143
  ```
120
144
 
121
145
  Every `ornn admin` command, read or mutation, requires an admin session.
146
+ `ornn admin users list` and `ornn admin organizations list` search the
147
+ Authorization directory by person name, email, or company name, with bounded `--limit` and
148
+ `--offset` pagination. Users can be restricted by `--organization`; organization
149
+ results can be filtered by `--role`, `--approved`, or `--ids`.
150
+
151
+ `ornn admin organizations invite --organization UUID --email EMAIL` emails an
152
+ invitation immediately using the selected organization UUID from the directory.
153
+ It shares its schema and execution with MCP and Slack and uses the staff session
154
+ on `POST /v1/staff/invites`. The result includes the sign-in invitation path and
155
+ `email_sent`. If delivery is uncertain, the invitation remains valid; check
156
+ delivery or share the sign-in link before retrying. Use `--format json` for automation.
157
+
158
+ `ornn admin organizations grant-credit --organization UUID --amount-cents 10000
159
+ --note "Customer goodwill credit" --idempotency-key UUID` grants $100.00 in Ornn
160
+ credits to the organization. Find the organization with the directory commands
161
+ above. Like admin reservation creation, it requires a human terminal and
162
+ reviews the organization, USD amount, note, and UUID before default-no consent.
163
+ The key supplies the existing API's required `adjustmentId`; repeating it with
164
+ the same details returns the same grant. The admin's session supplies audit
165
+ identity. See the [credit-adjustment contract](../../services/commerce/README.md#staff-credit-adjustments).
166
+
122
167
  `ornn admin nodes --help` lists the authorized node commands. Node health
123
168
  returns the node detail object; metrics preserve the owner `tier` and `windows`;
124
169
  logs return `data.logs` and an opaque continuation cursor in `page.next_cursor`.
@@ -253,33 +298,33 @@ error record. Progress is additive and contains no secret or supplier identity.
253
298
  vNext is a clean cutover. Retired forms are never aliases: each fails before I/O
254
299
  with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
255
300
 
256
- | Retired form | Replacement |
257
- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
258
- | `ornn account`, `ornn whoami` | `ornn context` |
259
- | `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves | matching `ornn capacity-offers` leaf |
260
- | `ornn listings create` | `ornn admin capacity-offers create` |
261
- | `ornn buy` | Use the Ornn web app. |
262
- | `ornn bid create`, `exchange create` | Use the Ornn web app. |
263
- | `ornn bid` or `exchange` list, show, update, withdraw | Use the Ornn web app. |
264
- | `ornn gpu` or `gpus` list, show | Use the Ornn web app. |
265
- | `ornn gpu` or `gpus` checkout, `reservations checkout` | Use the Ornn web app. |
266
- | `ornn nodes launch` | `ornn launch` |
267
- | `ornn nodes` list, show, wait, switch, reboot, hard-reset | matching `ornn machines` leaf; reset needs erase flag |
268
- | `ornn ssh`, `nodes ssh-command` | `ornn connect` |
269
- | `ornn metrics`, `telemetry`, `logs` | `ornn check`, `machines metrics`, or `machines logs` |
270
- | `ornn clusters create`, `kubernetes launch`, `slurm launch` | `ornn launch --mode kubernetes` or `ornn launch --mode slurm` |
271
- | Kubernetes or Slurm status, credentials, SSH, teardown | `ornn clusters show`, `connect`, or `clusters teardown` |
272
- | `ornn network *`, `networks *` | `ornn private-networks <verb>` |
273
- | `ornn access *`, `keys` | exact `ornn machines` or `ornn ssh-keys` leaf printed |
274
- | `ornn storage volumes *` | `ornn volumes <verb>` |
275
- | `ornn storage files *` | `ornn volumes files <verb>` |
276
- | `ornn storage buckets *` | `ornn external-buckets <verb>` |
277
- | Storage targets, deploy, status, detach, filesystem | exact `ornn volume-mounts` or `ornn filesystems` leaf printed |
278
- | `ornn billing` | Use the Ornn web app. |
279
- | customer-root `facilities`, `tokens`, `users`, `node`, or staff `reservations *` | exact `ornn admin ...` command printed by the error |
280
- | `ornn api`, `commerce`, `orchestrator`, `cache purge` | `ornn resources`; no generic replacement exists |
281
- | `--json` | `--format json` |
282
- | `--interactive` | `--guided` |
301
+ | Retired form | Replacement |
302
+ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
303
+ | `ornn account`, `ornn whoami` | `ornn context` |
304
+ | `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves | matching `ornn capacity-offers` leaf |
305
+ | `ornn listings create` | `ornn admin capacity-offers create` |
306
+ | `ornn buy` | Use the Ornn web app. |
307
+ | `ornn bid create`, `exchange create` | Use the Ornn web app. |
308
+ | `ornn bid` or `exchange` list, show, update, withdraw | Use the Ornn web app. |
309
+ | `ornn gpu` or `gpus` list, show | Use the Ornn web app. |
310
+ | `ornn gpu` or `gpus` checkout, `reservations checkout` | Use the Ornn web app. |
311
+ | `ornn nodes launch` | `ornn launch` |
312
+ | `ornn nodes` list, show, wait, switch, reboot, hard-reset | matching `ornn machines` leaf; reset needs erase flag |
313
+ | `ornn ssh`, `nodes ssh-command` | `ornn connect` |
314
+ | `ornn metrics`, `telemetry`, `logs` | `ornn check`, `machines metrics`, or `machines logs` |
315
+ | `ornn clusters create`, `kubernetes launch`, `slurm launch` | `ornn launch --mode kubernetes` or `ornn launch --mode slurm` |
316
+ | Kubernetes or Slurm status, credentials, SSH, teardown | `ornn clusters show`, `connect`, or `clusters teardown` |
317
+ | `ornn network *`, `networks *` | `ornn private-networks <verb>` |
318
+ | `ornn access *`, `keys` | exact `ornn machines` or `ornn ssh-keys` leaf printed |
319
+ | `ornn storage volumes *` | `ornn volumes <verb>` |
320
+ | `ornn storage files *` | `ornn volumes files <verb>` |
321
+ | `ornn storage buckets *` | `ornn external-buckets <verb>` |
322
+ | Storage targets, deploy, status, detach, filesystem | exact `ornn volume-mounts` or `ornn filesystems` leaf printed |
323
+ | `ornn billing` | Use the Ornn web app. |
324
+ | customer-root `facilities`, `tokens`, `users`, `node`, or staff `reservations *` | exact `ornn admin ...` command printed by the error |
325
+ | `ornn api`, `commerce`, `orchestrator`, `cache purge` | `ornn resources`; no generic replacement exists |
326
+ | `--json` | `--format json` |
327
+ | `--interactive` | `--guided` |
283
328
 
284
329
  Changed behavior is intentional: waits are deadline-bound, consent is semantic
285
330
  and default-no, pagination is bounded and cursor-based, machine output is always
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.35",
3
+ "version": "1.0.62",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "picocolors": "^1.1.1",
45
- "zod": "^4.6.5",
45
+ "zod": "^4.6.7",
46
46
  "update-notifier": "7.3.1"
47
47
  }
48
48
  }
@@ -1,17 +1,12 @@
1
1
  import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
2
- import { gpuLabeler } from "./gpu-catalog.mjs";
2
+ import { parseSemanticArguments } from "./interaction-runtime.mjs";
3
3
 
4
4
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5
- const DECIMAL = /^\d+(?:\.\d{1,6})?$/;
6
5
 
7
6
  const USAGE = {
8
- "listings.create":
9
- "Usage: ornn admin capacity-offers create --gpu GPU --nodes COUNT --gpus-per-node COUNT --site SITE --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--supplier-price-per-gpu-hour USD] [--down-payment-cents CENTS] [--access-mode bare-metal|virtual-machine] [--yes]",
10
7
  "reservations.create":
11
- "Usage: ornn admin reservations create --organization ID --capacity-offer ID --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--down-payment-cents CENTS] [--yes]",
8
+ "Usage: ornn admin reservations create --organization-id ID --listing-id ID --listing-term-id ID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour-micro-usd N --idempotency-key KEY [--yes]",
12
9
  };
13
- const ACCESS_MODES = new Set(["bare-metal", "virtual-machine"]);
14
- const STAFF_HARDWARE_CATALOG = "/catalog/hardware";
15
10
 
16
11
  function parseOptions(argv, { values, positionals }, usage) {
17
12
  const options = {};
@@ -72,97 +67,31 @@ function uuid(options, name, usage) {
72
67
  return value.toLowerCase();
73
68
  }
74
69
 
70
+ // Markets books windows in whole seconds.
75
71
  function timestamp(options, name, usage) {
76
72
  const value = required(options, name, usage);
77
73
  const parsed = Date.parse(value);
78
- if (!Number.isFinite(parsed)) {
79
- throw new AdminMutationError(`--${name} must be an ISO timestamp.`, { field: name });
80
- }
81
- return new Date(parsed).toISOString();
82
- }
83
-
84
- function price(options, usage, name = "price-per-gpu-hour") {
85
- const value = required(options, name, usage);
86
- if (!DECIMAL.test(value)) {
87
- throw new AdminMutationError(
88
- `--${name} must be zero or a positive decimal with at most 6 decimal places.`,
89
- { field: name }
90
- );
91
- }
92
- return value;
93
- }
94
-
95
- function optionalCents(options) {
96
- const value = options["down-payment-cents"]?.trim();
97
- if (value === undefined) return undefined;
98
- const parsed = Number(value);
99
- if (!Number.isSafeInteger(parsed) || parsed < 0) {
100
- throw new AdminMutationError("--down-payment-cents must be a whole number of cents.", {
101
- field: "down-payment-cents",
74
+ if (!Number.isFinite(parsed) || !/(?:Z|[+-]\d{2}:\d{2})$/i.test(value)) {
75
+ throw new AdminMutationError(`--${name} must be an ISO timestamp with a timezone.`, {
76
+ field: name,
102
77
  });
103
78
  }
104
- return parsed;
105
- }
106
-
107
- function assertWindow(start, end) {
108
- if (Date.parse(start) >= Date.parse(end)) {
109
- throw new AdminMutationError("--ends-at must be later than --starts-at.", { field: "ends-at" });
79
+ // Date.parse drops sub-millisecond digits, so check the text itself.
80
+ if (/\.\d*[1-9]/.test(value)) {
81
+ throw new AdminMutationError(`--${name} must be in whole seconds.`, { field: name });
110
82
  }
83
+ return new Date(parsed).toISOString();
111
84
  }
112
85
 
113
86
  function parseCapacityOffer(argv) {
114
- const usage = USAGE["listings.create"];
115
- const { options } = parseOptions(
116
- argv,
117
- {
118
- positionals: 0,
119
- values: [
120
- "gpu",
121
- "nodes",
122
- "gpus-per-node",
123
- "site",
124
- "starts-at",
125
- "ends-at",
126
- "price-per-gpu-hour",
127
- "supplier-price-per-gpu-hour",
128
- "down-payment-cents",
129
- "access-mode",
130
- "idempotency-key",
131
- ],
132
- },
133
- usage
134
- );
135
- const startsAt = timestamp(options, "starts-at", usage);
136
- const endsAt = timestamp(options, "ends-at", usage);
137
- assertWindow(startsAt, endsAt);
138
- const accessMode = options["access-mode"]?.trim();
139
- if (accessMode !== undefined && !ACCESS_MODES.has(accessMode)) {
140
- throw new AdminMutationError("--access-mode must be bare-metal or virtual-machine.", {
141
- field: "access-mode",
142
- });
143
- }
144
- const downPaymentCents = optionalCents(options);
87
+ const { $positionals, yes, ...input } = parseSemanticArguments(argv);
88
+ if ($positionals.length) throw new AdminMutationError("Use named options for listing writes.");
145
89
  return {
146
- input: {
147
- gpu_type: required(options, "gpu", usage),
148
- node_count: positiveInteger(options, "nodes", usage),
149
- gpus_per_node: positiveInteger(options, "gpus-per-node", usage),
150
- site_nickname: required(options, "site", usage),
151
- available_start_at: startsAt,
152
- available_end_at: endsAt,
153
- buy_now_price_per_gpu_hour: Number(price(options, usage)),
154
- ...(options["supplier-price-per-gpu-hour"] !== undefined
155
- ? {
156
- cost_per_gpu_hour: Number(price(options, usage, "supplier-price-per-gpu-hour")),
157
- }
158
- : {}),
159
- ...(downPaymentCents !== undefined ? { down_payment_cents: downPaymentCents } : {}),
160
- ...(accessMode ? { access_mode: accessMode } : {}),
161
- },
90
+ input,
162
91
  mutation: {
163
- idempotencyKey: options["idempotency-key"],
92
+ idempotencyKey: input.idempotencyKey,
164
93
  safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
165
- yes: options.yes === true,
94
+ yes: yes === true,
166
95
  },
167
96
  };
168
97
  }
@@ -174,12 +103,13 @@ function parseReservationCreate(argv) {
174
103
  {
175
104
  positionals: 0,
176
105
  values: [
177
- "organization",
178
- "capacity-offer",
106
+ "organization-id",
107
+ "listing-id",
108
+ "listing-term-id",
109
+ "nodes",
179
110
  "starts-at",
180
111
  "ends-at",
181
- "price-per-gpu-hour",
182
- "down-payment-cents",
112
+ "price-per-gpu-hour-micro-usd",
183
113
  "idempotency-key",
184
114
  ],
185
115
  },
@@ -187,94 +117,70 @@ function parseReservationCreate(argv) {
187
117
  );
188
118
  const startsAt = timestamp(options, "starts-at", usage);
189
119
  const endsAt = timestamp(options, "ends-at", usage);
190
- assertWindow(startsAt, endsAt);
191
- const downPaymentCents = optionalCents(options);
120
+ if (Date.parse(endsAt) <= Date.parse(startsAt)) {
121
+ throw new AdminMutationError("--ends-at must be later than --starts-at.", { field: "ends-at" });
122
+ }
123
+ const idempotencyKey = required(options, "idempotency-key", usage);
192
124
  return {
193
125
  input: {
194
- organization_id: uuid(options, "organization", usage),
195
- listing_id: uuid(options, "capacity-offer", usage),
196
- start_at: startsAt,
197
- end_at: endsAt,
198
- price_per_gpu_hour: price(options, usage),
199
- ...(downPaymentCents !== undefined ? { down_payment_cents: downPaymentCents } : {}),
126
+ organizationId: uuid(options, "organization-id", usage),
127
+ listingId: uuid(options, "listing-id", usage),
128
+ listingTermId: uuid(options, "listing-term-id", usage),
129
+ nodes: positiveInteger(options, "nodes", usage),
130
+ startsAt,
131
+ endsAt,
132
+ pricePerGpuHourMicroUsd: positiveInteger(options, "price-per-gpu-hour-micro-usd", usage),
133
+ idempotencyKey,
200
134
  },
201
135
  mutation: {
202
- idempotencyKey: options["idempotency-key"],
136
+ idempotencyKey,
203
137
  safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
204
138
  yes: options.yes === true,
205
139
  },
206
140
  };
207
141
  }
208
142
 
209
- function previewPayload(value) {
210
- if (value?.ok === false) {
211
- throw new AdminMutationError("The capacity offer contains an unknown hardware catalog term.", {
212
- code: value.error ?? "unknown_catalog_term",
213
- });
214
- }
215
- if (!value?.payload || typeof value.payload !== "object") {
216
- throw new AdminMutationError("Capacity-offer normalization returned no effective terms.", {
217
- code: "admin_review_incomplete",
218
- });
219
- }
220
- return value.payload;
221
- }
222
-
223
- function usd(microUsd) {
224
- return typeof microUsd === "number" ? (microUsd / 1_000_000).toFixed(6) : undefined;
225
- }
226
-
227
- function capacityOfferReview(payload, input, gpuLabel) {
228
- const listing = payload.listing ?? {};
143
+ function capacityOfferReview(body, editing) {
229
144
  return {
230
- title: "Create capacity offer",
231
- fields: [
232
- { label: "GPU", value: gpuLabel(payload.gpu_type ?? listing.gpuSlug) },
233
- { label: "Nodes to attach", value: payload.node_count ?? input.node_count },
234
- { label: "GPUs per node", value: listing.gpusPerNode ?? input.gpus_per_node },
235
- { label: "Site", value: payload.site_nickname ?? input.site_nickname },
236
- { label: "Access mode", value: listing.accessMode },
237
- { label: "Starts at", value: listing.startsAt ?? input.available_start_at },
238
- { label: "Ends at", value: listing.endsAt ?? input.available_end_at },
239
- { label: "Price per GPU-hour (USD)", value: input.buy_now_price_per_gpu_hour },
240
- {
241
- label: "Supplier price per GPU-hour (USD)",
242
- value: usd(listing.supplierPricePerGpuHourMicroUsd),
243
- },
244
- { label: "Spread per GPU-hour (USD)", value: usd(listing.spreadPerGpuHourMicroUsd) },
245
- { label: "Down payment (cents)", value: listing.downPaymentCents },
246
- ],
247
- consequence:
248
- "Publishes a primary listing with these terms; attach its nodes before it reaches the market.",
145
+ title: editing ? "Edit listing term" : "Create capacity offer",
146
+ fields: Object.entries(body).map(([label, value]) => ({
147
+ label,
148
+ value: typeof value === "object" ? JSON.stringify(value) : value,
149
+ })),
150
+ consequence: editing
151
+ ? "Updates the selected advertised financial term."
152
+ : "Creates native capacity and its advertised financial terms.",
249
153
  };
250
154
  }
251
155
 
252
156
  function reservationCreateReview(input) {
253
157
  return {
254
- title: "Create capacity reservation",
158
+ title: "Book reservation for customer",
255
159
  fields: [
256
- { label: "Organization", value: input.organization_id },
257
- { label: "Capacity offer", value: input.listing_id },
258
- { label: "Starts at", value: input.start_at },
259
- { label: "Ends at", value: input.end_at },
260
- { label: "Price per GPU-hour (USD)", value: input.price_per_gpu_hour },
261
- ...(input.down_payment_cents !== undefined
262
- ? [{ label: "Down payment (cents)", value: input.down_payment_cents }]
263
- : []),
160
+ { label: "Customer organization", value: input.organizationId },
161
+ { label: "Listing", value: input.listingId },
162
+ { label: "Listing term", value: input.listingTermId },
163
+ { label: "Nodes", value: input.nodes },
164
+ { label: "Starts at", value: input.startsAt },
165
+ { label: "Ends at", value: input.endsAt },
166
+ { label: "Price per GPU-hour (micro-USD)", value: input.pricePerGpuHourMicroUsd },
167
+ { label: "Idempotency key", value: input.idempotencyKey },
264
168
  ],
265
169
  consequence:
266
- "Creates a schedule reservation on the listing's nodes with the reviewed dates and price.",
170
+ "Holds the reviewed nodes for the window; the customer's reservation starts when it opens.",
267
171
  };
268
172
  }
269
173
 
270
- async function runCapacityOffer({ execute, fetch, input, mutation, runtime }) {
174
+ async function runCapacityOffer({ capability, execute, input, mutation, runtime }) {
271
175
  return runAdminMutation({
272
176
  ...runtime,
273
177
  ...mutation,
274
178
  prepare: async () => {
275
- const payload = previewPayload(await execute(input));
276
- const gpuLabel = await gpuLabeler(fetch, STAFF_HARDWARE_CATALOG);
277
- return { review: capacityOfferReview(payload, input, gpuLabel), value: input };
179
+ const { payload } = await execute(input);
180
+ return {
181
+ review: capacityOfferReview(payload, capability.id === "listings.update"),
182
+ value: input,
183
+ };
278
184
  },
279
185
  execute: (value) => execute(value, { confirmed: true }),
280
186
  });
@@ -291,7 +197,13 @@ async function runReservationCreate({ execute, input, mutation, runtime }) {
291
197
 
292
198
  export const adminCapacityReservationHandlers = [
293
199
  {
294
- capabilityId: "listings.create",
200
+ capabilityId: "listing.create",
201
+ parse: parseCapacityOffer,
202
+ run: runCapacityOffer,
203
+ safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
204
+ },
205
+ {
206
+ capabilityId: "listings.update",
295
207
  parse: parseCapacityOffer,
296
208
  run: runCapacityOffer,
297
209
  safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
@@ -1,4 +1,5 @@
1
1
  import { adminCapacityReservationHandlers } from "./admin-capacity-reservations.mjs";
2
+ import { adminCreditHandlers } from "./admin-credits.mjs";
2
3
  import { adminEnrollmentHandlers } from "./admin-enrollment.mjs";
3
4
  import { adminNodeDiagnosticsHandlers } from "./admin-node-diagnostics.mjs";
4
5
  import { adminNodeLifecycleHandlers } from "./admin-node-lifecycle.mjs";
@@ -6,6 +7,7 @@ import { ADMIN_MUTATION_SAFETY, assertAdminMutationContext } from "./admin-mutat
6
7
 
7
8
  const handlers = [
8
9
  ...adminCapacityReservationHandlers,
10
+ ...adminCreditHandlers,
9
11
  ...adminEnrollmentHandlers,
10
12
  ...adminNodeDiagnosticsHandlers,
11
13
  ...adminNodeLifecycleHandlers,
@@ -0,0 +1,57 @@
1
+ import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
2
+ import { parseSemanticArguments } from "./interaction-runtime.mjs";
3
+
4
+ function parseGrant(argv, capability) {
5
+ const { $positionals, yes, organization, amountCents, note, idempotencyKey, ...extra } =
6
+ parseSemanticArguments(argv);
7
+ if ($positionals.length || Object.keys(extra).length) {
8
+ throw new AdminMutationError(`Usage: ${capability.cli.vnext.human}`);
9
+ }
10
+ const parsed = capability.input.safeParse({
11
+ organizationId: organization,
12
+ adjustmentId: idempotencyKey,
13
+ amountCents: amountCents === undefined ? undefined : Number(amountCents),
14
+ note,
15
+ });
16
+ if (!parsed.success) {
17
+ throw new AdminMutationError(`Usage: ${capability.cli.vnext.human}\n${parsed.error.message}`);
18
+ }
19
+ return {
20
+ input: parsed.data,
21
+ mutation: {
22
+ idempotencyKey: parsed.data.adjustmentId,
23
+ safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
24
+ yes: yes === true,
25
+ },
26
+ };
27
+ }
28
+
29
+ async function runGrant({ execute, input, mutation, runtime }) {
30
+ return runAdminMutation({
31
+ ...runtime,
32
+ ...mutation,
33
+ prepare: async () => ({
34
+ review: {
35
+ title: "Grant Ornn credits",
36
+ fields: [
37
+ { label: "Customer organization", value: input.organizationId },
38
+ { label: "Amount (USD)", value: `$${(input.amountCents / 100).toFixed(2)}` },
39
+ { label: "Note", value: input.note },
40
+ { label: "Grant UUID", value: input.adjustmentId },
41
+ ],
42
+ consequence: "Adds spendable Ornn credits to the selected organization.",
43
+ },
44
+ value: input,
45
+ }),
46
+ execute: (value) => execute(value, { confirmed: true }),
47
+ });
48
+ }
49
+
50
+ export const adminCreditHandlers = [
51
+ {
52
+ capabilityId: "credits.grant",
53
+ parse: parseGrant,
54
+ run: runGrant,
55
+ safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
56
+ },
57
+ ];