@ornncompute/cli 1.0.25 → 1.0.34

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 (35) hide show
  1. package/README.md +76 -23
  2. package/package.json +1 -1
  3. package/src/admin-capacity-reservations.mjs +68 -43
  4. package/src/admin-enrollment.mjs +14 -13
  5. package/src/admin-node-lifecycle.mjs +10 -13
  6. package/src/admin-shell.mjs +146 -12
  7. package/src/catalog-shell.mjs +58 -45
  8. package/src/device-auth.mjs +1 -1
  9. package/src/evidence-commands.mjs +25 -11
  10. package/src/gpu-catalog.mjs +21 -0
  11. package/src/identity-file.mjs +28 -0
  12. package/src/kubernetes-commands.mjs +12 -6
  13. package/src/machine-reference.mjs +10 -2
  14. package/src/mount-commands.mjs +4 -2
  15. package/src/slurm-commands.mjs +11 -0
  16. package/src/storage-commands.mjs +27 -7
  17. package/src/vm-commands.mjs +42 -7
  18. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
  19. package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
  20. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
  21. package/vendor/capabilities/capabilities/admin.js +7 -7
  22. package/vendor/capabilities/capabilities/commerce-market.js +330 -0
  23. package/vendor/capabilities/capabilities/fleet.js +57 -21
  24. package/vendor/capabilities/capabilities/listings.js +137 -146
  25. package/vendor/capabilities/capabilities/market-shared.js +44 -0
  26. package/vendor/capabilities/capabilities/markets.js +449 -0
  27. package/vendor/capabilities/capabilities/networks.js +7 -0
  28. package/vendor/capabilities/capabilities/tokens.js +12 -9
  29. package/vendor/capabilities/capability.js +2 -1
  30. package/vendor/capabilities/cli-catalog.js +4 -2
  31. package/vendor/capabilities/cli-migrations.js +16 -43
  32. package/vendor/capabilities/cli.js +1 -1
  33. package/vendor/capabilities/spec-catalog.js +26 -3
  34. package/vendor/capabilities/supplier-directory.js +20 -0
  35. package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
package/README.md CHANGED
@@ -8,11 +8,10 @@ For the customer install and usage guide, see [`docs/cli.md`](../../docs/cli.md)
8
8
  The shared capability catalog in `packages/capabilities` is the authority for
9
9
  command identity, role visibility, input schemas, help, and resource discovery.
10
10
  The CLI imports `@ornn/capabilities/cli`; packaging includes only that entrypoint's
11
- dependencies, excluding staff bidding and CRM handlers.
11
+ dependencies, excluding staff commerce and CRM handlers.
12
12
 
13
13
  For GCS imports, start with the [source-bucket permissions and version-specific CLI guide](https://docs.ornn.com/cli#gcs-imports-and-storage-transfer-service).
14
- It documents Ornn's Storage Transfer service agent, the required `gcloud` IAM
15
- grants, and the current CLI 1.x GCS verification limitation. Scoped
14
+ It documents the current CLI 1.x GCS verification limitation. Scoped
16
15
  `ornn help external-buckets connect`, `verify`, and `import` help links to the
17
16
  same guide.
18
17
 
@@ -26,8 +25,11 @@ ornn login
26
25
  ornn context
27
26
  ```
28
27
 
29
- `ornn login` uses the browser device flow. Login links must use HTTP(S) on the configured authentication origin. Use `--no-browser` when the CLI must
30
- print the URL instead of opening it. Session and endpoint configuration remains
28
+ `ornn login` uses the browser device flow. A fresh npm install defaults to
29
+ `https://compute.ornn.com` for browser authorization and `https://api.ornn.com`
30
+ for API requests. Use `ORNN_AUTH_BASE_URL` and `ORNN_API_BASE_URL` for explicit
31
+ local or staging endpoints. Login links must use HTTP(S) on the configured
32
+ authentication origin. Use `--no-browser` when the CLI must print the URL instead of opening it. Session and endpoint configuration remains
31
33
  in `~/.config/ornn`; the gateway re-derives organization and role scope from the
32
34
  bearer on every request.
33
35
 
@@ -57,7 +59,12 @@ ornn resources
57
59
  Advanced customer resources are registered under one noun and one verb:
58
60
 
59
61
  ```text
60
- capacity-offers list | forecast | show
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
67
+ offers list | post | accept | cancel
61
68
  machines list | show | wait | switch | reboot | reset | metrics | logs
62
69
  clusters list | show | wait | eligible-machines | add-node | remove-node | teardown | metrics | logs
63
70
  ssh-keys list | add | delete
@@ -72,22 +79,38 @@ external-buckets list | show | connect | update-credentials | verify | import |
72
79
  filesystems deploy | show | wait | delete
73
80
  ```
74
81
 
75
- Listing discovery is read-only: list, inspect, and forecast capacity, including
76
- advertised prices. Purchases, bids, payments, resale, billing, and commercial
77
- reservation management use the web app. Launch consumes already allocated
78
- capacity; internal reservation lookups remain necessary for access and ownership.
79
-
80
- Only admins can create listings or reservations through
82
+ `ssh-keys list` uses backend cursors; see [pagination](../../docs/cli.md#advanced-customer-resources).
83
+
84
+ 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.
93
+ Prices are integer micro-USD per GPU-hour (`1000000` is $1.00). Every write
94
+ 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
97
+ capacity; internal reservation lookups remain necessary for access and
98
+ ownership.
99
+
100
+ Only admins can create primary listings or reservations through
81
101
  `ornn admin capacity-offers create` and `ornn admin reservations create`.
82
- The CLI and gateway both enforce the admin role before mutation. Both commands
83
- retain explicit commercial-term validation and default-no human confirmation.
102
+ They post to the role-aware `POST /commerce/listings` and
103
+ `POST /commerce/reservations` with the admin's own session; Commerce enforces
104
+ 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.
84
107
 
85
108
  All staff discovery and execution starts at `ornn admin`. A fresh staff preflight
86
109
  runs before the catalog is shown. The seven roots are:
87
110
 
88
111
  ```text
89
112
  ornn admin users
90
- ornn admin operators
113
+ ornn admin suppliers
91
114
  ornn admin facilities
92
115
  ornn admin nodes
93
116
  ornn admin enrollment-tokens
@@ -96,9 +119,22 @@ ornn admin reservations
96
119
  ```
97
120
 
98
121
  Every `ornn admin` command, read or mutation, requires an admin session.
122
+ `ornn admin nodes --help` lists the authorized node commands. Node health
123
+ returns the node detail object; metrics preserve the owner `tier` and `windows`;
124
+ logs return `data.logs` and an opaque continuation cursor in `page.next_cursor`.
125
+ The cursor preserves the node, stream, count, time window, and owner position;
126
+ pass it unchanged with the same node ID. The human `Next` command includes that
127
+ node ID. Changing filters requires a new first-page request. Legacy raw owner
128
+ cursors require the original `--before` value and filters.
129
+ Metrics and logs use node-scoped staff gateway routes. Exported private keys
130
+ include the final newline required by OpenSSH, both in files and with `--print`.
99
131
  `ornn admin nodes enroll` takes optional `--filter COMPARTMENT_IDS`
100
132
  (omit to run every compartment step); the retired `--mode` flag is gone. The
101
133
  gateway, not CLI metadata, authoritatively enforces the admin role.
134
+ Suppliers are Authorization supplier organizations: `ornn admin suppliers list`
135
+ returns their `id` and `name`, and `--supplier UUID` on `facilities list`,
136
+ `nodes list`, `enrollment-tokens create`, and `nodes enroll` takes that
137
+ organization ID.
102
138
 
103
139
  Token revocation looks up the token by ID, including tokens outside the newest
104
140
  500-row list. Creation-time checks and safe retries still apply.
@@ -171,7 +207,9 @@ advertised, including JSON and NDJSON. The CLI resolves them through the
171
207
  bounded owned-machine list and sends only the canonical machine ID to the
172
208
  machine owner route. Bare `connect` discovers VM, Bare Metal,
173
209
  Kubernetes, and Slurm targets before making one safe-fact choice across the
174
- combined set. `connect --print` renders a native direct-access command; raw
210
+ combined set. An unresolved target fails with
211
+ `connect_target_not_found` before any private-network or VPN setup.
212
+ `connect --print` renders a native direct-access command; raw
175
213
  secret `--print` rejects every explicit `--format`. Direct human `connect`
176
214
  announces the native command and explains that Ctrl-C ends only local SSH while
177
215
  the remote machine keeps running before it starts the session. Human metrics
@@ -220,11 +258,11 @@ with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
220
258
  | `ornn account`, `ornn whoami` | `ornn context` |
221
259
  | `ornn catalog search`, `gpu-types`, `availability *`, `listing *`, `listings` read leaves | matching `ornn capacity-offers` leaf |
222
260
  | `ornn listings create` | `ornn admin capacity-offers create` |
223
- | `ornn buy` | Use the Ornn web app. |
224
- | `ornn bid create`, `exchange create` | Use the Ornn web app. |
225
- | `ornn bid` or `exchange` list, show, update, withdraw | Use the Ornn web app. |
226
- | `ornn gpu` or `gpus` list, show | Use the Ornn web app. |
227
- | `ornn gpu` or `gpus` checkout, `reservations checkout` | Use the Ornn web app. |
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. |
228
266
  | `ornn nodes launch` | `ornn launch` |
229
267
  | `ornn nodes` list, show, wait, switch, reboot, hard-reset | matching `ornn machines` leaf; reset needs erase flag |
230
268
  | `ornn ssh`, `nodes ssh-command` | `ornn connect` |
@@ -237,8 +275,8 @@ with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
237
275
  | `ornn storage files *` | `ornn volumes files <verb>` |
238
276
  | `ornn storage buckets *` | `ornn external-buckets <verb>` |
239
277
  | Storage targets, deploy, status, detach, filesystem | exact `ornn volume-mounts` or `ornn filesystems` leaf printed |
240
- | `ornn billing` | Use the Ornn web app. |
241
- | customer-root `operator(s)`, `facilities`, `tokens`, `users`, `node`, or staff `reservations *` | exact `ornn admin ...` command printed by the error |
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 |
242
280
  | `ornn api`, `commerce`, `orchestrator`, `cache purge` | `ornn resources`; no generic replacement exists |
243
281
  | `--json` | `--format json` |
244
282
  | `--interactive` | `--guided` |
@@ -323,3 +361,18 @@ Ornn CLI 1.0.22 and later request storage lists one page at a time.
323
361
  `page.next_cursor` to `--cursor` for the next page; null means the end.
324
362
  Refresh from the first page after changes. Older CLI versions receive an
325
363
  upgrade error for storage lists. File-list pagination is unchanged.
364
+
365
+ ### Reliable retries and discovery
366
+
367
+ CLI 1.0.28 forwards staff facility/node `--cursor` and `--limit` to the owner
368
+ and preserves filters in the printed next-page command. A repeated owner cursor
369
+ fails explicitly. Volume mount discovery uses the authenticated
370
+ `/v1/orchestrator/storage-targets` route.
371
+
372
+ File uploads include a SHA-256 request fingerprint. Reusing `--idempotency-key`
373
+ with the same volume, path, bytes, and content type reuses the upload; a completed
374
+ replay returns its original upload ID without sending bytes again. A changed
375
+ request conflicts, and an expired incomplete request requires a new key.
376
+ `ornn volumes refresh <volume>` reconciles recorded usage against stored files.
377
+ The upload replay contract requires the matching gateway/orchestrator release;
378
+ see [the service contract](../../services/orchestrator/README.md#file-upload-replay-and-usage).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.25",
3
+ "version": "1.0.34",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,14 +1,17 @@
1
1
  import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
2
+ import { gpuLabeler } from "./gpu-catalog.mjs";
2
3
 
3
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;
4
5
  const DECIMAL = /^\d+(?:\.\d{1,6})?$/;
5
6
 
6
7
  const USAGE = {
7
8
  "listings.create":
8
- "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 [--yes]",
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]",
9
10
  "reservations.create":
10
- "Usage: ornn admin reservations create --organization ID --capacity-offer ID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--yes]",
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]",
11
12
  };
13
+ const ACCESS_MODES = new Set(["bare-metal", "virtual-machine"]);
14
+ const STAFF_HARDWARE_CATALOG = "/catalog/hardware";
12
15
 
13
16
  function parseOptions(argv, { values, positionals }, usage) {
14
17
  const options = {};
@@ -78,17 +81,29 @@ function timestamp(options, name, usage) {
78
81
  return new Date(parsed).toISOString();
79
82
  }
80
83
 
81
- function price(options, usage) {
82
- const value = required(options, "price-per-gpu-hour", usage);
84
+ function price(options, usage, name = "price-per-gpu-hour") {
85
+ const value = required(options, name, usage);
83
86
  if (!DECIMAL.test(value)) {
84
87
  throw new AdminMutationError(
85
- "--price-per-gpu-hour must be zero or a positive decimal with at most 6 decimal places.",
86
- { field: "price-per-gpu-hour" }
88
+ `--${name} must be zero or a positive decimal with at most 6 decimal places.`,
89
+ { field: name }
87
90
  );
88
91
  }
89
92
  return value;
90
93
  }
91
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",
102
+ });
103
+ }
104
+ return parsed;
105
+ }
106
+
92
107
  function assertWindow(start, end) {
93
108
  if (Date.parse(start) >= Date.parse(end)) {
94
109
  throw new AdminMutationError("--ends-at must be later than --starts-at.", { field: "ends-at" });
@@ -109,6 +124,9 @@ function parseCapacityOffer(argv) {
109
124
  "starts-at",
110
125
  "ends-at",
111
126
  "price-per-gpu-hour",
127
+ "supplier-price-per-gpu-hour",
128
+ "down-payment-cents",
129
+ "access-mode",
112
130
  "idempotency-key",
113
131
  ],
114
132
  },
@@ -117,6 +135,13 @@ function parseCapacityOffer(argv) {
117
135
  const startsAt = timestamp(options, "starts-at", usage);
118
136
  const endsAt = timestamp(options, "ends-at", usage);
119
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);
120
145
  return {
121
146
  input: {
122
147
  gpu_type: required(options, "gpu", usage),
@@ -126,6 +151,13 @@ function parseCapacityOffer(argv) {
126
151
  available_start_at: startsAt,
127
152
  available_end_at: endsAt,
128
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 } : {}),
129
161
  },
130
162
  mutation: {
131
163
  idempotencyKey: options["idempotency-key"],
@@ -144,10 +176,10 @@ function parseReservationCreate(argv) {
144
176
  values: [
145
177
  "organization",
146
178
  "capacity-offer",
147
- "nodes",
148
179
  "starts-at",
149
180
  "ends-at",
150
181
  "price-per-gpu-hour",
182
+ "down-payment-cents",
151
183
  "idempotency-key",
152
184
  ],
153
185
  },
@@ -156,14 +188,15 @@ function parseReservationCreate(argv) {
156
188
  const startsAt = timestamp(options, "starts-at", usage);
157
189
  const endsAt = timestamp(options, "ends-at", usage);
158
190
  assertWindow(startsAt, endsAt);
191
+ const downPaymentCents = optionalCents(options);
159
192
  return {
160
193
  input: {
161
194
  organization_id: uuid(options, "organization", usage),
162
195
  listing_id: uuid(options, "capacity-offer", usage),
163
- node_count: positiveInteger(options, "nodes", usage),
164
196
  start_at: startsAt,
165
197
  end_at: endsAt,
166
198
  price_per_gpu_hour: price(options, usage),
199
+ ...(downPaymentCents !== undefined ? { down_payment_cents: downPaymentCents } : {}),
167
200
  },
168
201
  mutation: {
169
202
  idempotencyKey: options["idempotency-key"],
@@ -187,44 +220,32 @@ function previewPayload(value) {
187
220
  return value.payload;
188
221
  }
189
222
 
190
- function capacityOfferReview(payload, input) {
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 ?? {};
191
229
  return {
192
230
  title: "Create capacity offer",
193
231
  fields: [
194
- { label: "GPU", value: payload.gpu_type ?? input.gpu_type },
195
- {
196
- label: "Nodes",
197
- value:
198
- payload.capacityNodes ?? payload.capacity_nodes ?? payload.node_count ?? input.node_count,
199
- },
200
- {
201
- label: "GPUs per node",
202
- value: payload.gpusPerNode ?? payload.gpus_per_node ?? input.gpus_per_node,
203
- },
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 },
204
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 },
205
240
  {
206
- label: "Starts at",
207
- value:
208
- payload.start_at ??
209
- payload.startAt ??
210
- payload.available_start_at ??
211
- input.available_start_at,
212
- },
213
- {
214
- label: "Ends at",
215
- value:
216
- payload.end_at ?? payload.endAt ?? payload.available_end_at ?? input.available_end_at,
217
- },
218
- {
219
- label: "Price per GPU-hour (USD)",
220
- value:
221
- payload.buy_now_rate_per_gpu_hour ??
222
- payload.buyNowRatePerGpuHour ??
223
- payload.buy_now_price_per_gpu_hour ??
224
- input.buy_now_price_per_gpu_hour,
241
+ label: "Supplier price per GPU-hour (USD)",
242
+ value: usd(listing.supplierPricePerGpuHourMicroUsd),
225
243
  },
244
+ { label: "Spread per GPU-hour (USD)", value: usd(listing.spreadPerGpuHourMicroUsd) },
245
+ { label: "Down payment (cents)", value: listing.downPaymentCents },
226
246
  ],
227
- consequence: "Publishes new commercial capacity terms.",
247
+ consequence:
248
+ "Publishes a primary listing with these terms; attach its nodes before it reaches the market.",
228
249
  };
229
250
  }
230
251
 
@@ -234,22 +255,26 @@ function reservationCreateReview(input) {
234
255
  fields: [
235
256
  { label: "Organization", value: input.organization_id },
236
257
  { label: "Capacity offer", value: input.listing_id },
237
- { label: "Nodes", value: input.node_count },
238
258
  { label: "Starts at", value: input.start_at },
239
259
  { label: "Ends at", value: input.end_at },
240
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
+ : []),
241
264
  ],
242
- consequence: "Creates a reservation with the reviewed quantity, dates, and price.",
265
+ consequence:
266
+ "Creates a schedule reservation on the listing's nodes with the reviewed dates and price.",
243
267
  };
244
268
  }
245
269
 
246
- async function runCapacityOffer({ execute, input, mutation, runtime }) {
270
+ async function runCapacityOffer({ execute, fetch, input, mutation, runtime }) {
247
271
  return runAdminMutation({
248
272
  ...runtime,
249
273
  ...mutation,
250
274
  prepare: async () => {
251
275
  const payload = previewPayload(await execute(input));
252
- return { review: capacityOfferReview(payload, input), value: input };
276
+ const gpuLabel = await gpuLabeler(fetch, STAFF_HARDWARE_CATALOG);
277
+ return { review: capacityOfferReview(payload, input, gpuLabel), value: input };
253
278
  },
254
279
  execute: (value) => execute(value, { confirmed: true }),
255
280
  });
@@ -11,11 +11,11 @@ const ENROLLMENT_LOG_READ_TIMEOUT_MS = 5_000;
11
11
 
12
12
  const USAGE = {
13
13
  "tokens.create":
14
- "Usage: ornn admin enrollment-tokens create --operator UUID --expires-in SECONDS (--print | --output PATH) [--overwrite] [--yes]",
14
+ "Usage: ornn admin enrollment-tokens create --supplier UUID --expires-in SECONDS (--print | --output PATH) [--overwrite] [--yes]",
15
15
  "tokens.revoke":
16
16
  "Usage: ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--yes] [--idempotency-key KEY]",
17
17
  "nodes.enroll":
18
- "Usage: ornn admin nodes enroll <node-id> --host HOST --operator UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose] [--yes]",
18
+ "Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose] [--yes]",
19
19
  "nodes.export-key":
20
20
  "Usage: ornn admin nodes export-key <node-id> (--print | --output PATH) [--overwrite]",
21
21
  "nodes.rotate-key":
@@ -110,13 +110,13 @@ function parseTokenCreate(argv) {
110
110
  {
111
111
  booleans: ["overwrite", "print", "yes"],
112
112
  positionals: 0,
113
- values: ["operator", "expires-in", "output"],
113
+ values: ["supplier", "expires-in", "output"],
114
114
  },
115
115
  usage
116
116
  );
117
117
  return {
118
118
  input: {
119
- operator_id: uuid(required(options, "operator", usage), "operator"),
119
+ organization_id: uuid(required(options, "supplier", usage), "supplier"),
120
120
  expires_in_seconds: boundedInteger(
121
121
  required(options, "expires-in", usage),
122
122
  "expires-in",
@@ -169,8 +169,8 @@ function parseNodeEnroll(argv) {
169
169
  "filter",
170
170
  "host",
171
171
  "identity-file",
172
- "operator",
173
172
  "port",
173
+ "supplier",
174
174
  "token-ttl",
175
175
  "username",
176
176
  ],
@@ -190,7 +190,7 @@ function parseNodeEnroll(argv) {
190
190
  input: {
191
191
  node_id: uuid(positionals[0], "node-id"),
192
192
  host: required(options, "host", usage),
193
- operator_id: uuid(required(options, "operator", usage), "operator"),
193
+ organization_id: uuid(required(options, "supplier", usage), "supplier"),
194
194
  facility_id: uuid(required(options, "facility", usage), "facility"),
195
195
  ...(filter ? { filter } : {}),
196
196
  identity_file: required(options, "identity-file", usage),
@@ -247,7 +247,7 @@ function text(value, fallback = "") {
247
247
  function safeTokenMetadata(payload) {
248
248
  return {
249
249
  id: text(payload?.id) || null,
250
- operator_id: text(payload?.operator_id) || null,
250
+ organization_id: text(payload?.organization_id) || null,
251
251
  status: text(payload?.status, "active"),
252
252
  expires_at: text(payload?.expires_at) || null,
253
253
  created_at: text(payload?.created_at) || null,
@@ -258,9 +258,10 @@ function secretMaterial(capabilityId, payload) {
258
258
  if (capabilityId === "tokens.create") {
259
259
  return text(payload?.token) || text(payload?.plaintext);
260
260
  }
261
- return typeof payload?.private_key === "string" && payload.private_key.length
262
- ? payload.private_key
263
- : "";
261
+ const key = payload?.private_key;
262
+ if (typeof key !== "string" || !key.length) return "";
263
+ // OpenSSH rejects otherwise valid private keys without a terminating newline.
264
+ return key.endsWith("\n") ? key : `${key}\n`;
264
265
  }
265
266
 
266
267
  function keyMetadata(input, payload) {
@@ -342,7 +343,7 @@ function reviewFor(capabilityId, input, prepared) {
342
343
  return {
343
344
  title: "Create enrollment token",
344
345
  fields: [
345
- { label: "Operator", value: input.operator_id },
346
+ { label: "Supplier", value: input.organization_id },
346
347
  { label: "Expires in", value: `${input.expires_in_seconds} seconds` },
347
348
  {
348
349
  label: "Secret destination",
@@ -359,7 +360,7 @@ function reviewFor(capabilityId, input, prepared) {
359
360
  fields: [
360
361
  { label: "Token", value: input.token_id },
361
362
  { label: "Created", value: target?.created_at ?? input.expected_created_at ?? "absent" },
362
- { label: "Operator", value: target?.operator_id ?? "unknown" },
363
+ { label: "Supplier", value: target?.organization_id ?? "unknown" },
363
364
  { label: "Expires", value: target?.expires_at ?? "unknown" },
364
365
  ],
365
366
  consequence: "Prevents this exact token generation from enrolling another node.",
@@ -371,7 +372,7 @@ function reviewFor(capabilityId, input, prepared) {
371
372
  fields: [
372
373
  { label: "Node", value: input.node_id },
373
374
  { label: "SSH target", value: `${input.username}@${input.host}:${input.port}` },
374
- { label: "Operator", value: input.operator_id },
375
+ { label: "Supplier", value: input.organization_id },
375
376
  { label: "Facility", value: input.facility_id },
376
377
  ...(input.filter
377
378
  ? [{ label: "Compartment filter", value: input.filter }]
@@ -215,7 +215,7 @@ function nodeSnapshot(node) {
215
215
  node_id: nodeId(node),
216
216
  hostname: text(node?.hostname ?? node?.k8s_node_name),
217
217
  status: text(node?.status),
218
- operator_id: text(node?.operator_id ?? node?.operatorId),
218
+ organization_id: text(node?.organization_id),
219
219
  facility_id: text(node?.facility_id ?? node?.facilityId),
220
220
  reservation_id: reservationId(node),
221
221
  off_grid: node?.off_grid === true,
@@ -358,22 +358,19 @@ async function resolveLiveMachine(fetch, node) {
358
358
 
359
359
  function reservationSnapshot(reservation, requestedId) {
360
360
  return {
361
- reservation_id:
362
- text(reservation?.reservationId ?? reservation?.reservation_id ?? reservation?.id) ||
363
- requestedId,
364
- organization_id: text(
365
- reservation?.organizationId ?? reservation?.organization_id ?? reservation?.tenant_id
366
- ),
367
- listing_id: text(reservation?.listingId ?? reservation?.listing_id),
361
+ reservation_id: text(reservation?.reservationId) || requestedId,
362
+ organization_id: text(reservation?.organizationId),
363
+ listing_id: text(reservation?.listingId),
364
+ kind: text(reservation?.kind),
368
365
  status: text(reservation?.status),
369
- node_count: Number(reservation?.nodeCount ?? reservation?.node_count ?? 0) || 0,
370
- start_at: text(reservation?.startAt ?? reservation?.start_at),
371
- end_at: text(reservation?.endAt ?? reservation?.end_at),
366
+ node_count: Number(reservation?.nodeCount ?? 0) || 0,
367
+ starts_at: text(reservation?.startsAt),
368
+ ends_at: text(reservation?.endsAt),
372
369
  };
373
370
  }
374
371
 
375
372
  async function readReservation(fetch, id) {
376
- const reservation = await fetch(`/internal/v1/reservations/${encodeURIComponent(id)}`, {
373
+ const reservation = await fetch(`/commerce/reservations/${encodeURIComponent(id)}`, {
377
374
  method: "GET",
378
375
  });
379
376
  return reservationSnapshot(reservation, id);
@@ -395,7 +392,7 @@ function commonReviewFields(node) {
395
392
  const snapshot = nodeSnapshot(node);
396
393
  return [
397
394
  { label: "Node", value: `${snapshot.hostname || "unnamed"} (${snapshot.node_id})` },
398
- { label: "Operator", value: label(snapshot.operator_id) },
395
+ { label: "Supplier", value: label(snapshot.organization_id) },
399
396
  { label: "Facility", value: label(snapshot.facility_id) },
400
397
  { label: "Capacity reservation", value: capacityLabel(node) },
401
398
  ];