@ornncompute/cli 1.0.48 → 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 CHANGED
@@ -59,10 +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 | terms | relist | delist
63
- reservations list | show | invoices
64
- spot books | book | trades | bid | orders | cancel
65
- 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
66
65
  offers list | post | accept | cancel
67
66
  machines list | show | wait | switch | reboot | reset | metrics | logs
68
67
  clusters list | show | wait | eligible-machines | add-node | remove-node | teardown | metrics | logs
@@ -85,31 +84,41 @@ Market commands call one gateway route each and print that route's JSON.
85
84
  (`/commerce/v2/listings*`). List/show return parent fields with `capacityListingId`;
86
85
  pass it as `--listing-id`. `terms` separately returns advertised financial choices
87
86
  with `listingTermsId`, price, duration bounds, down payment and renewable flag.
88
- List accepts limits of 1–100 and the owner cursor unchanged. Relisting uses
89
- `POST /commerce/reservations/{id}/relist` with `nodeCount`, dates, optional node
90
- selection and a `terms` array. Delisting uses the native parent’s `/delist` POST. `reservations` reads Commerce
91
- reservations, schedule and spot alike. `spot`, `schedules`, and `offers` call
92
- Markets (`/v1/markets`): place 8-GPU spot bids (one full node each), buy a
93
- secondary window at its asking price (`schedules buy`), post offers on a
94
- primary listing's schedule, and accept or cancel offers on primaries you own.
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`.
95
103
  Prices are integer micro-USD per GPU-hour (`1000000` is $1.00). Every write
96
104
  reviews its terms and needs default-no consent, or `--yes` with
97
- `--format json`; writes accept `--idempotency-key`. Payment methods and
98
- 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
99
107
  capacity; internal reservation lookups remain necessary for access and
100
108
  ownership.
101
109
 
102
110
  Only admins can create primary listings or reservations through
103
111
  `ornn admin capacity-offers create` and `ornn admin reservations create`.
104
- They post to the role-aware `POST /commerce/v2/listings` and
105
- `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
106
114
  the admin role. Both commands retain explicit commercial-term validation and
107
- default-no human confirmation. Reservation creation takes `--customer UUID`,
108
- `--listing-term UUID`, `--nodes COUNT`, `--gpus COUNT`, `--starts-at TIME`, and
109
- `--transaction UUID`, plus optional `--ends-at TIME` and `--vm-disk UUID`.
110
- Prices and down payments come from the selected listing term. Omit `--ends-at`
111
- for hourly terms, and reuse the same transaction UUID and payload on retries.
112
- Creation requires an eligible human terminal. Native capacity creation takes
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
113
122
  `--gpu-slug`, `--capacity-node-count`, `--gpus-per-node`, `--facility-id`,
114
123
  `--provisioning-type`, `--state`, `--terms` (a JSON array) and `--idempotency-key`,
115
124
  plus optional `--organization-id`. Supply availability dates or `--days-to-rfs`.
@@ -146,6 +155,15 @@ on `POST /v1/staff/invites`. The result includes the sign-in invitation path and
146
155
  `email_sent`. If delivery is uncertain, the invitation remains valid; check
147
156
  delivery or share the sign-in link before retrying. Use `--format json` for automation.
148
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
+
149
167
  `ornn admin nodes --help` lists the authorized node commands. Node health
150
168
  returns the node detail object; metrics preserve the owner `tier` and `windows`;
151
169
  logs return `data.logs` and an opaque continuation cursor in `page.next_cursor`.
@@ -280,33 +298,33 @@ error record. Progress is additive and contains no secret or supplier identity.
280
298
  vNext is a clean cutover. Retired forms are never aliases: each fails before I/O
281
299
  with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
282
300
 
283
- | Retired form | Replacement |
301
+ | Retired form | Replacement |
284
302
  | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
285
- | `ornn account`, `ornn whoami` | `ornn context` |
286
- | `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves | matching `ornn capacity-offers` leaf |
287
- | `ornn listings create` | `ornn admin capacity-offers create` |
288
- | `ornn buy` | Use the Ornn web app. |
289
- | `ornn bid create`, `exchange create` | Use the Ornn web app. |
290
- | `ornn bid` or `exchange` list, show, update, withdraw | Use the Ornn web app. |
291
- | `ornn gpu` or `gpus` list, show | Use the Ornn web app. |
292
- | `ornn gpu` or `gpus` checkout, `reservations checkout` | Use the Ornn web app. |
293
- | `ornn nodes launch` | `ornn launch` |
294
- | `ornn nodes` list, show, wait, switch, reboot, hard-reset | matching `ornn machines` leaf; reset needs erase flag |
295
- | `ornn ssh`, `nodes ssh-command` | `ornn connect` |
296
- | `ornn metrics`, `telemetry`, `logs` | `ornn check`, `machines metrics`, or `machines logs` |
297
- | `ornn clusters create`, `kubernetes launch`, `slurm launch` | `ornn launch --mode kubernetes` or `ornn launch --mode slurm` |
298
- | Kubernetes or Slurm status, credentials, SSH, teardown | `ornn clusters show`, `connect`, or `clusters teardown` |
299
- | `ornn network *`, `networks *` | `ornn private-networks <verb>` |
300
- | `ornn access *`, `keys` | exact `ornn machines` or `ornn ssh-keys` leaf printed |
301
- | `ornn storage volumes *` | `ornn volumes <verb>` |
302
- | `ornn storage files *` | `ornn volumes files <verb>` |
303
- | `ornn storage buckets *` | `ornn external-buckets <verb>` |
304
- | Storage targets, deploy, status, detach, filesystem | exact `ornn volume-mounts` or `ornn filesystems` leaf printed |
305
- | `ornn billing` | Use the Ornn web app. |
306
- | customer-root `facilities`, `tokens`, `users`, `node`, or staff `reservations *` | exact `ornn admin ...` command printed by the error |
307
- | `ornn api`, `commerce`, `orchestrator`, `cache purge` | `ornn resources`; no generic replacement exists |
308
- | `--json` | `--format json` |
309
- | `--interactive` | `--guided` |
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` |
310
328
 
311
329
  Changed behavior is intentional: waits are deadline-bound, consent is semantic
312
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.48",
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
  }
@@ -5,7 +5,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-
5
5
 
6
6
  const USAGE = {
7
7
  "reservations.create":
8
- "Usage: ornn admin reservations create --customer ID --listing-term ID --nodes COUNT --gpus COUNT --starts-at TIME --transaction ID [--ends-at TIME] [--vm-disk ID] [--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]",
9
9
  };
10
10
 
11
11
  function parseOptions(argv, { values, positionals }, usage) {
@@ -67,40 +67,20 @@ function uuid(options, name, usage) {
67
67
  return value.toLowerCase();
68
68
  }
69
69
 
70
+ // Markets books windows in whole seconds.
70
71
  function timestamp(options, name, usage) {
71
72
  const value = required(options, name, usage);
72
73
  const parsed = Date.parse(value);
73
- if (!Number.isFinite(parsed)) {
74
- throw new AdminMutationError(`--${name} must be an ISO timestamp.`, { field: name });
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,
77
+ });
75
78
  }
76
- return new Date(parsed).toISOString();
77
- }
78
-
79
- // Preserve the microseconds Commerce uses to match a reservation to its accepted term.
80
- function reservationTimestamp(options, name, usage) {
81
- const value = required(options, name, usage);
82
- const match = value.match(
83
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.(\d{1,6}))?(?:Z|[+-]\d{2}:\d{2})$/
84
- );
85
- if (!match) {
86
- throw new AdminMutationError(
87
- `--${name} must be an ISO timestamp with a timezone and at most 6 fractional digits.`,
88
- { field: name }
89
- );
90
- }
91
- const normalized = timestamp(options, name, usage);
92
- return match[1]?.length > 3 ? `${normalized.slice(0, 19)}.${match[1]}Z` : normalized;
93
- }
94
-
95
- function timestampMicroseconds(value) {
96
- const fraction = value.match(/\.(\d+)Z$/)?.[1] ?? "";
97
- return BigInt(Date.parse(value)) * 1000n + BigInt(fraction.padEnd(6, "0").slice(3));
98
- }
99
-
100
- function assertWindow(start, end) {
101
- if (timestampMicroseconds(start) >= timestampMicroseconds(end)) {
102
- 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 });
103
82
  }
83
+ return new Date(parsed).toISOString();
104
84
  }
105
85
 
106
86
  function parseCapacityOffer(argv) {
@@ -123,36 +103,37 @@ function parseReservationCreate(argv) {
123
103
  {
124
104
  positionals: 0,
125
105
  values: [
126
- "customer",
127
- "listing-term",
106
+ "organization-id",
107
+ "listing-id",
108
+ "listing-term-id",
128
109
  "nodes",
129
- "gpus",
130
110
  "starts-at",
131
111
  "ends-at",
132
- "transaction",
133
- "vm-disk",
112
+ "price-per-gpu-hour-micro-usd",
134
113
  "idempotency-key",
135
114
  ],
136
115
  },
137
116
  usage
138
117
  );
139
- const startsAt = reservationTimestamp(options, "starts-at", usage);
140
- const endsAt =
141
- options["ends-at"] === undefined ? null : reservationTimestamp(options, "ends-at", usage);
142
- if (endsAt !== null) assertWindow(startsAt, endsAt);
118
+ const startsAt = timestamp(options, "starts-at", usage);
119
+ const endsAt = timestamp(options, "ends-at", usage);
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);
143
124
  return {
144
125
  input: {
145
- customerId: uuid(options, "customer", usage),
146
- listingTermId: uuid(options, "listing-term", usage),
147
- reservedNodeCount: positiveInteger(options, "nodes", usage),
148
- reservedGpuCount: positiveInteger(options, "gpus", usage),
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),
149
130
  startsAt,
150
131
  endsAt,
151
- transactionId: uuid(options, "transaction", usage),
152
- ...(options["vm-disk"] !== undefined ? { vmDiskId: uuid(options, "vm-disk", usage) } : {}),
132
+ pricePerGpuHourMicroUsd: positiveInteger(options, "price-per-gpu-hour-micro-usd", usage),
133
+ idempotencyKey,
153
134
  },
154
135
  mutation: {
155
- idempotencyKey: options["idempotency-key"],
136
+ idempotencyKey,
156
137
  safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
157
138
  yes: options.yes === true,
158
139
  },
@@ -174,18 +155,19 @@ function capacityOfferReview(body, editing) {
174
155
 
175
156
  function reservationCreateReview(input) {
176
157
  return {
177
- title: "Create capacity reservation",
158
+ title: "Book reservation for customer",
178
159
  fields: [
179
- { label: "Customer", value: input.customerId },
160
+ { label: "Customer organization", value: input.organizationId },
161
+ { label: "Listing", value: input.listingId },
180
162
  { label: "Listing term", value: input.listingTermId },
181
- { label: "Reserved nodes", value: input.reservedNodeCount },
182
- { label: "Reserved GPUs", value: input.reservedGpuCount },
163
+ { label: "Nodes", value: input.nodes },
183
164
  { label: "Starts at", value: input.startsAt },
184
- { label: "Ends at", value: input.endsAt ?? "Open-ended" },
185
- { label: "Transaction", value: input.transactionId },
186
- ...(input.vmDiskId ? [{ label: "VM disk", value: input.vmDiskId }] : []),
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 },
187
168
  ],
188
- consequence: "Reserves the reviewed quantities and dates under the selected listing term.",
169
+ consequence:
170
+ "Holds the reviewed nodes for the window; the customer's reservation starts when it opens.",
189
171
  };
190
172
  }
191
173
 
@@ -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
+ ];
@@ -186,10 +186,11 @@ function parseAdminInput(capability, argv) {
186
186
  }
187
187
 
188
188
  const offsetPagination = ["users.list", "organization.list"].includes(capability.id);
189
+ const reservationList = capability.id === "reservation.list";
189
190
  const limit = positiveInteger(
190
191
  options.limit,
191
192
  "limit",
192
- offsetPagination ? 100 : MAX_PAGE_LIMIT
193
+ offsetPagination || reservationList ? 100 : MAX_PAGE_LIMIT
193
194
  ) ?? (offsetPagination ? 25 : DEFAULT_PAGE_LIMIT);
194
195
  const input = {};
195
196
  if (nodeCommand) input.node_id = positionals[0];
@@ -200,6 +201,10 @@ function parseAdminInput(capability, argv) {
200
201
  if (options.facility) input.facility_id = options.facility;
201
202
  if (options.status) input.status = options.status;
202
203
  if (options.email) input.email = options.email;
204
+ if (options.reservation) input.reservation_id = options.reservation;
205
+ if (options.customer) input.customerId = options.customer;
206
+ if (options.listing) input.listingId = options.listing;
207
+ if (options["listing-term"]) input.listingTermId = options["listing-term"];
203
208
  if (options.query) input.q = options.query;
204
209
  if (options.role) input.role = options.role;
205
210
  const approved = booleanOption(options.approved, "approved");
@@ -218,7 +223,9 @@ function parseAdminInput(capability, argv) {
218
223
 
219
224
  if (command === "admin nodes logs") return parseLogPage(input);
220
225
 
221
- const ownerPagination = ["facilities.list", "nodes.list"].includes(capability.id);
226
+ const ownerPagination = ["facilities.list", "nodes.list", "reservation.list"].includes(
227
+ capability.id
228
+ );
222
229
  const explicitOffset = nonnegativeInteger(options.offset, "offset");
223
230
  if (ownerPagination) {
224
231
  input.limit = limit;
@@ -515,7 +522,10 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
515
522
  ["organization", command === "admin users list" ? input.organization_id : undefined],
516
523
  ["facility", input.facility_id],
517
524
  ["status", input.status],
518
- ["email", input.email],
525
+ ["reservation", input.reservation_id],
526
+ ["customer", input.customerId],
527
+ ["listing", input.listingId],
528
+ ["listing-term", input.listingTermId],
519
529
  ["query", input.q],
520
530
  ["role", input.role],
521
531
  ["approved", input.approved],
@@ -834,16 +834,13 @@ function parseCapabilityInput(capability, rawInput) {
834
834
 
835
835
  // Commerce's market routes and Markets serve every caller on one path and
836
836
  // decide from the session what it may do; a staff session is an admin there.
837
- const ROLE_AWARE_PREFIXES = [
838
- "/commerce/v2/listings",
839
- "/commerce/reservations",
840
- "/commerce/fees",
841
- "/v1/markets",
842
- ];
837
+ const ROLE_AWARE_PREFIXES = ["/commerce/v2/listings", "/commerce/reservations", "/v1/markets"];
843
838
 
844
839
  function isRoleAwarePath(path) {
845
840
  const pathname = String(path).split("?")[0];
846
- return ROLE_AWARE_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
841
+ return ROLE_AWARE_PREFIXES.some(
842
+ (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
843
+ );
847
844
  }
848
845
 
849
846
  function adminFetch(io, path, init = {}) {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { defineCapability, preview } from "../capability.js";
3
3
  import { nativeListingCreateInput } from "./native-listing.js";
4
- import { idempotencyHeaders } from "./market-shared.js";
4
+ import { idempotencyHeaders, microUsd } from "./market-shared.js";
5
5
  function adminHelp(command, outcome, human, automation, review) {
6
6
  return {
7
7
  audience: "admin",
@@ -38,37 +38,54 @@ export const adminCapacityOffersCreateCapability = defineCapability({
38
38
  });
39
39
  },
40
40
  });
41
+ // Markets books whole-second windows; a fractional digit would shift or refuse one.
42
+ const wholeSecondInstant = z.iso
43
+ .datetime({ offset: true })
44
+ .refine((value) => !/\.\d*[1-9]/.test(value), "Use a time in whole seconds.");
41
45
  export const reservationCreateInput = z.object({
42
- customerId: z.string().uuid(),
43
- listingTermId: z.string().uuid(),
44
- reservedNodeCount: z.number().int().positive().max(2_147_483_647),
45
- reservedGpuCount: z.number().int().positive().max(2_147_483_647),
46
- startsAt: z.iso.datetime({ offset: true }),
47
- endsAt: z.iso.datetime({ offset: true }).nullable().optional(),
48
- transactionId: z
46
+ organizationId: z.string().uuid().describe("The customer organization the window is booked for."),
47
+ listingId: z
49
48
  .string()
50
49
  .uuid()
51
- .describe("Reuse this transaction ID when retrying the same reservation."),
52
- vmDiskId: z.string().uuid().nullable().optional(),
50
+ .describe("The listing's capacityListingId. Staff may book any available listing."),
51
+ listingTermId: z
52
+ .string()
53
+ .uuid()
54
+ .describe("The listingTermsId of a non-renewable Reserved term on that listing."),
55
+ nodes: z.number().int().positive().max(2_147_483_647).describe("Whole nodes to book."),
56
+ startsAt: wholeSecondInstant.describe("RFC 3339 start of the window in whole seconds, no more than five minutes ago."),
57
+ endsAt: wholeSecondInstant.describe("RFC 3339 end of the window in whole seconds, at least an hour after the start and within the term's duration range."),
58
+ pricePerGpuHourMicroUsd: microUsd.describe("The term's price in micro-USD per GPU-hour. A changed price refuses the booking."),
59
+ idempotencyKey: z
60
+ .string()
61
+ .trim()
62
+ .min(1)
63
+ .max(200)
64
+ .describe("Reuse this key when retrying the same booking."),
53
65
  });
54
66
  export const adminReservationsCreateCapability = defineCapability({
55
67
  id: "reservations.create",
56
68
  domain: "reservations",
57
69
  roles: ["admin"],
58
- description: "Create one reservation from a listing term, customer, quantities, dates, and a stable transaction ID (admin).",
70
+ description: "Book a window of a listing term for a customer organization (admin). Markets holds the GPUs until the window starts, then settles a Commerce reservation with the booking's ID. Returns {bookings}.",
59
71
  mutation: "preview-confirm",
60
- http: { method: "POST", path: "/commerce/reservations" },
72
+ http: { method: "POST", path: "/v1/markets/bookings" },
61
73
  cli: {
62
74
  command: ["admin", "reservations", "create"],
63
- vnext: adminHelp(["admin", "reservations", "create"], "Create one reservation using the selected listing term and quantities.", "ornn admin reservations create --customer ID --listing-term ID --nodes COUNT --gpus COUNT --starts-at TIME --transaction ID [--ends-at TIME] [--vm-disk ID]", "Requires an eligible human terminal. Reuse --transaction when retrying the same reservation.", "Customer, listing term, reserved quantities, dates, VM disk, and transaction ID are reviewed before default-no consent."),
75
+ vnext: adminHelp(["admin", "reservations", "create"], "Book a listing term window for a customer organization.", "ornn admin reservations create --organization-id UUID --listing-id UUID --listing-term-id UUID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour-micro-usd N --idempotency-key KEY", "Requires an eligible human terminal. Reuse --idempotency-key when retrying the same booking.", "Customer organization, listing, term, nodes, window, price, and idempotency key are reviewed before default-no consent."),
64
76
  },
65
77
  slack: false,
66
78
  input: reservationCreateInput,
67
79
  execute: async (ctx, input) => {
68
- const body = reservationCreateInput.parse(input);
80
+ const { organizationId, idempotencyKey, ...leg } = reservationCreateInput.parse(input);
81
+ const body = { organizationId, legs: [leg] };
69
82
  if (!ctx.confirmed)
70
83
  return { preview: body };
71
- return { reservation: await ctx.fetch("/commerce/reservations", { method: "POST", body }) };
84
+ return ctx.fetch("/v1/markets/bookings", {
85
+ method: "POST",
86
+ body,
87
+ headers: idempotencyHeaders(ctx, idempotencyKey),
88
+ });
72
89
  },
73
90
  });
74
91
  export const adminCapacityReservationCapabilities = [
@@ -109,7 +109,7 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
109
109
  roles: ["admin"],
110
110
  description: "Revoke one exact enrollment-token generation after reconciling its durable goal.",
111
111
  mutation: "preview-confirm",
112
- http: { method: "POST", path: "/enrollment/tokens/{id}/revoke" },
112
+ http: { method: "DELETE", path: "/enrollment-tokens/{id}" },
113
113
  cli: {
114
114
  command: ["admin", "enrollment-tokens", "revoke"],
115
115
  vnext: adminHelp(["admin", "enrollment-tokens", "revoke"], "Revoke one exact enrollment-token generation.", "ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--yes]", "ornn admin enrollment-tokens revoke <token-id> --expected-created-at TIME --yes --idempotency-key KEY --format json", ["canonical token UUID", "token created_at generation"], "Review the exact token generation and the loss of future enrollment access."),
@@ -136,8 +136,8 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
136
136
  if (!current.target.created_at) {
137
137
  throw codedError("token_identity_unavailable", `Enrollment token ${parsed.token_id} has no stable creation identity and cannot be revoked safely.`);
138
138
  }
139
- const revoked = (await ctx.fetch(`/enrollment/tokens/${encodeURIComponent(parsed.token_id)}/revoke`, {
140
- method: "POST",
139
+ const revoked = (await ctx.fetch(`/enrollment-tokens/${encodeURIComponent(parsed.token_id)}`, {
140
+ method: "DELETE",
141
141
  ...(parsed.idempotency_key
142
142
  ? { headers: { "Idempotency-Key": parsed.idempotency_key } }
143
143
  : {}),
@@ -40,7 +40,7 @@ export const adminUsersListCapability = withAdminBinding(usersListCapability, ad
40
40
  export const adminOrganizationsListCapability = withAdminBinding(organizationListCapability, adminHelp(["admin", "organizations", "list"], "List organizations in the Authorization directory.", "ornn admin organizations list [--query QUERY] [--role consumer|supplier] [--approved true|false] [--ids UUID,...] [--limit 25] [--offset OFFSET] [--cursor CURSOR]", "ornn admin organizations list --query QUERY --limit 25 [--cursor CURSOR] --format json"));
41
41
  export const adminSuppliersListCapability = withAdminBinding(listSuppliersCapability, adminHelp(["admin", "suppliers", "list"], "List supplier organizations visible to staff.", "ornn admin suppliers list [--limit 50] [--cursor CURSOR]", "ornn admin suppliers list --limit 50 --format json"));
42
42
  export const adminFacilitiesListCapability = withAdminBinding(listFacilitiesCapability, adminHelp(["admin", "facilities", "list"], "List facilities visible to staff.", "ornn admin facilities list [--supplier UUID] [--limit 50] [--cursor CURSOR]", "ornn admin facilities list --limit 50 --format json"));
43
- export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--supplier UUID] [--facility FACILITY] [--status STATUS] [--email EMAIL] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
43
+ export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--supplier UUID] [--facility FACILITY] [--status STATUS] [--reservation UUID] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
44
44
  const nodeInput = z.object({ node_id: z.string().min(1) });
45
45
  export const adminNodeHealthCapability = defineCapability({
46
46
  id: "nodes.health",