@ornncompute/cli 1.0.48 → 1.0.63

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.
@@ -112,7 +112,7 @@ export const adminNodeSuspendManagementCapability = defineCapability({
112
112
  roles: ["admin"],
113
113
  description: "Suspend Ornn management while leaving the exact host powered on.",
114
114
  mutation: "preview-confirm",
115
- http: { method: "POST", path: "/internal/nodes/{id}/off-grid" },
115
+ http: { method: "POST", path: "/nodes/{id}/off-grid" },
116
116
  cli: {
117
117
  command: ["admin", "nodes", "suspend-management"],
118
118
  vnext: adminLifecycleHelp(["admin", "nodes", "suspend-management"], "Remove Ornn management and managed access from one powered-on node.", "ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--yes] [--no-wait]", "ornn admin nodes suspend-management <node-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, capacity assignment, management removal, and managed-access loss."),
@@ -129,7 +129,7 @@ export const adminNodeSuspendManagementCapability = defineCapability({
129
129
  };
130
130
  if (!ctx.confirmed)
131
131
  return preview("nodes.suspend_management", { node_id: id, ...body });
132
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/off-grid`, {
132
+ return ctx.fetch(`/nodes/${encodeURIComponent(id)}/off-grid`, {
133
133
  method: "POST",
134
134
  body,
135
135
  headers: requestHeaders(parsed.idempotency_key),
@@ -142,7 +142,7 @@ export const adminNodeResumeManagementCapability = defineCapability({
142
142
  roles: ["admin"],
143
143
  description: "Resume Ornn management for one exact off-grid node.",
144
144
  mutation: "preview-confirm",
145
- http: { method: "POST", path: "/internal/nodes/{id}/on-grid" },
145
+ http: { method: "POST", path: "/nodes/{id}/on-grid" },
146
146
  cli: {
147
147
  command: ["admin", "nodes", "resume-management"],
148
148
  vnext: adminLifecycleHelp(["admin", "nodes", "resume-management"], "Restore Ornn management and managed access on one off-grid node.", "ornn admin nodes resume-management <node-ref> [--yes] [--no-wait]", "ornn admin nodes resume-management <node-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, capacity assignment, remote re-enrollment, and restored managed access."),
@@ -155,7 +155,7 @@ export const adminNodeResumeManagementCapability = defineCapability({
155
155
  const id = nodeId(parsed);
156
156
  if (!ctx.confirmed)
157
157
  return preview("nodes.resume_management", { node_id: id });
158
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/on-grid`, {
158
+ return ctx.fetch(`/nodes/${encodeURIComponent(id)}/on-grid`, {
159
159
  method: "POST",
160
160
  body: {
161
161
  ...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
@@ -170,7 +170,7 @@ export const adminNodeTerminateCapability = defineCapability({
170
170
  roles: ["admin"],
171
171
  description: "Delete one exact node record with explicit cleanup scope and consequence review.",
172
172
  mutation: "preview-confirm",
173
- http: { method: "POST", path: "/internal/nodes/{id}/terminate" },
173
+ http: { method: "POST", path: "/nodes/{id}/terminate" },
174
174
  cli: {
175
175
  command: ["admin", "nodes", "terminate"],
176
176
  vnext: adminLifecycleHelp(["admin", "nodes", "terminate"], "End managed access and permanently delete one exact node record.", "ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--yes]", "ornn admin nodes terminate <node-id> --delete-node-record --yes --idempotency-key KEY --format json", "Node, supplier, facility, reservation/capacity, host cleanup, node-record deletion, and access loss."),
@@ -188,7 +188,7 @@ export const adminNodeTerminateCapability = defineCapability({
188
188
  };
189
189
  if (!ctx.confirmed)
190
190
  return preview("nodes.terminate", { node_id: id, ...body });
191
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/terminate`, {
191
+ return ctx.fetch(`/nodes/${encodeURIComponent(id)}/terminate`, {
192
192
  method: "POST",
193
193
  body,
194
194
  headers: requestHeaders(parsed.idempotency_key),
@@ -201,7 +201,7 @@ export const adminNodeAssignCapability = defineCapability({
201
201
  roles: ["admin"],
202
202
  description: "Assign one exact node to one exact capacity reservation after consequence review.",
203
203
  mutation: "preview-confirm",
204
- http: { method: "POST", path: "/internal/nodes/{id}/assign" },
204
+ http: { method: "POST", path: "/nodes/{id}/assign" },
205
205
  cli: {
206
206
  command: ["admin", "nodes", "assign"],
207
207
  vnext: adminLifecycleHelp(["admin", "nodes", "assign"], "Assign one exact node to one exact capacity reservation.", "ornn admin nodes assign <node-ref> --reservation <reservation-id> [--yes] [--no-wait]", "ornn admin nodes assign <node-id> --reservation <reservation-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, previous and requested capacity assignments, and tenant access change."),
@@ -218,7 +218,7 @@ export const adminNodeAssignCapability = defineCapability({
218
218
  };
219
219
  if (!ctx.confirmed)
220
220
  return preview("nodes.assign", { node_id: id, ...body });
221
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/assign`, {
221
+ return ctx.fetch(`/nodes/${encodeURIComponent(id)}/assign`, {
222
222
  method: "POST",
223
223
  body,
224
224
  headers: requestHeaders(parsed.idempotency_key),
@@ -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",
@@ -48,7 +48,7 @@ export const adminNodeHealthCapability = defineCapability({
48
48
  roles: ["admin"],
49
49
  description: "Read lifecycle and readiness facts for one enrolled node.",
50
50
  mutation: "read",
51
- http: { method: "GET", path: "/internal/nodes/{id}" },
51
+ http: { method: "GET", path: "/nodes/{id}" },
52
52
  cli: {
53
53
  command: ["admin", "nodes", "health"],
54
54
  vnext: adminHelp(["admin", "nodes", "health"], "Inspect lifecycle and readiness facts for one node.", "ornn admin nodes health <node-id>", "ornn admin nodes health <node-id> --format json", ["canonical node ID"]),
@@ -58,7 +58,7 @@ export const adminNodeHealthCapability = defineCapability({
58
58
  input: nodeInput,
59
59
  execute: async (ctx, input) => {
60
60
  const parsed = nodeInput.parse(input);
61
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(parsed.node_id)}`, { method: "GET" });
61
+ return ctx.fetch(`/nodes/${encodeURIComponent(parsed.node_id)}`, { method: "GET" });
62
62
  },
63
63
  });
64
64
  export const adminNodeMetricsCapability = withAdminBinding(telemetryLatestCapability, adminHelp(["admin", "nodes", "metrics"], "Read a bounded telemetry window for one node.", "ornn admin nodes metrics <node-id> [--span 15m] [--start TIME --end TIME] [--max-points 240]", "ornn admin nodes metrics <node-id> --span 15m [--start TIME --end TIME] --max-points 240 --format json", ["canonical node ID"]));
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { nativeTermsInput, nativeTermFields, listingWriteControls } from "./native-listing.js";
3
3
  import { defineCapability, preview } from "../capability.js";
4
- import { consent, idempotencyHeaders, positionals, query, seg } from "./market-shared.js";
4
+ import { consent, idempotencyHeaders, microUsd, microUsdLabel, positionals, query, seg, } from "./market-shared.js";
5
5
  // Commerce market verbs: one capability per role-aware `/commerce` route.
6
6
  // Commerce decides from the caller's session what each listing or
7
7
  // reservation reveals; these verbs return its JSON unchanged.
@@ -20,12 +20,6 @@ function help(command, outcome, human, automation, related, order, acceptedRefer
20
20
  };
21
21
  }
22
22
  const pageLimit = z.coerce.number().int().min(1).max(200).optional();
23
- const nodeIdList = z.preprocess((value) => typeof value === "string"
24
- ? value
25
- .split(",")
26
- .map((part) => part.trim())
27
- .filter(Boolean)
28
- : value, z.array(z.string().uuid()).min(1));
29
23
  export const listingsListInput = z.object({
30
24
  $positionals: positionals,
31
25
  kind: z.enum(["primary", "secondary", "on_demand"]).optional(),
@@ -45,7 +39,7 @@ export const listingsListCapability = defineCapability({
45
39
  input: listingsListInput,
46
40
  cli: {
47
41
  command: ["capacity-offers", "list"],
48
- vnext: help(["capacity-offers", "list"], "List capacity parents with bounded pagination; read their financial terms separately.", "ornn capacity-offers list [--kind primary|secondary|on_demand] [--source-reservation-id <id>] [--limit <1-100>] [--cursor <opaque>]", "ornn capacity-offers list --limit 50 --format json", ["capacity-offers show", "capacity-offers terms", "schedules list"], 20, ["opaque page cursor"]),
42
+ vnext: help(["capacity-offers", "list"], "List capacity parents with bounded pagination; read their financial terms separately.", "ornn capacity-offers list [--kind primary|secondary|on_demand] [--source-reservation-id <id>] [--limit <1-100>] [--cursor <opaque>]", "ornn capacity-offers list --limit 50 --format json", ["capacity-offers show", "capacity-offers terms", "capacity-offers availability"], 20, ["opaque page cursor"]),
49
43
  },
50
44
  slack: false,
51
45
  execute: async (ctx, input) => {
@@ -74,7 +68,7 @@ export const listingShowCapability = defineCapability({
74
68
  input: listingShowInput,
75
69
  cli: {
76
70
  command: ["capacity-offers", "show"],
77
- vnext: help(["capacity-offers", "show"], "Inspect one listing's hardware, capacity and availability.", "ornn capacity-offers show --listing-id <id>", "ornn capacity-offers show --listing-id <id> --format json", ["capacity-offers list", "capacity-offers terms", "schedules show"], 21, ["listing ID"]),
71
+ vnext: help(["capacity-offers", "show"], "Inspect one listing's hardware, capacity and availability.", "ornn capacity-offers show --listing-id <id>", "ornn capacity-offers show --listing-id <id> --format json", ["capacity-offers list", "capacity-offers terms", "capacity-offers availability"], 21, ["listing ID"]),
78
72
  },
79
73
  slack: false,
80
74
  execute: async (ctx, input) => {
@@ -86,13 +80,13 @@ export const listingTermsCapability = defineCapability({
86
80
  id: "listings.terms",
87
81
  domain: "listings",
88
82
  roles: ["user"],
89
- description: "Read a capacity listing's advertised financial terms. Each term returns listingTermsId, capacityListingId, pricePerGpuHourMicroUsd, duration bounds, downPaymentCents and renewable. Use the parent's capacityListingId as listingId. Prices are integer micro-USD per GPU-hour.",
83
+ description: "Read a capacity listing's advertised financial terms. Each term returns listingTermsId, capacityListingId, pricePerGpuHourMicroUsd, duration bounds, downPaymentCents and renewable. Use the parent's capacityListingId as listingId. Prices are gross integer micro-USD per GPU-hour, the price reservations.purchase expects.",
90
84
  mutation: "read",
91
85
  http: { method: "GET", path: "/commerce/v2/listings/{listingId}/terms" },
92
86
  input: listingShowInput,
93
87
  cli: {
94
88
  command: ["capacity-offers", "terms"],
95
- vnext: help(["capacity-offers", "terms"], "Read the advertised financial terms for a capacity listing.", "ornn capacity-offers terms --listing-id <id>", "ornn capacity-offers terms --listing-id <id> --format json", ["capacity-offers show", "offers post"], 22, ["capacityListingId from capacity-offers list or show"]),
89
+ vnext: help(["capacity-offers", "terms"], "Read the advertised financial terms for a capacity listing.", "ornn capacity-offers terms --listing-id <id>", "ornn capacity-offers terms --listing-id <id> --format json", ["capacity-offers show", "offers post", "reservations buy"], 22, ["capacityListingId from capacity-offers list or show"]),
96
90
  },
97
91
  slack: false,
98
92
  execute: async (ctx, input) => {
@@ -100,33 +94,65 @@ export const listingTermsCapability = defineCapability({
100
94
  return ctx.fetch(`/commerce/v2/listings/${seg(parsed.listingId)}/terms`, { method: "GET" });
101
95
  },
102
96
  });
103
- export const listingRelistInput = z.object({
97
+ export const listingAvailabilityInput = z.object({
98
+ $positionals: positionals,
99
+ listingId: z.string().uuid(),
100
+ from: z.iso
101
+ .datetime({ offset: true })
102
+ .optional()
103
+ .describe("RFC 3339 start of the range to read. Defaults to now or the listing's start."),
104
+ to: z.iso
105
+ .datetime({ offset: true })
106
+ .optional()
107
+ .describe("RFC 3339 end of the range to read. Defaults to the listing's end."),
108
+ });
109
+ export const listingAvailabilityCapability = defineCapability({
110
+ id: "listings.availability",
111
+ domain: "listings",
112
+ roles: ["user"],
113
+ description: "Read how much of a capacity listing is free to book over time: windows of startsAt, endsAt, gpus and nodes (the nodes with any GPU free) from now until the listing ends (for an open-ended listing, 30 days ahead for a Market listing and far enough for any term of a Reserve listing), optionally narrowed by from and to. Use the parent's capacityListingId as listingId.",
114
+ mutation: "read",
115
+ http: { method: "GET", path: "/v1/markets/listings/{listingId}/availability" },
116
+ input: listingAvailabilityInput,
117
+ cli: {
118
+ command: ["capacity-offers", "availability"],
119
+ vnext: help(["capacity-offers", "availability"], "Show how many of a listing's nodes are free over time.", "ornn capacity-offers availability --listing-id <id> [--from <time>] [--to <time>]", "ornn capacity-offers availability --listing-id <id> --format json", ["capacity-offers terms", "reservations buy", "offers post"], 23, ["capacityListingId from capacity-offers list or show"]),
120
+ },
121
+ slack: false,
122
+ execute: async (ctx, input) => {
123
+ const parsed = listingAvailabilityInput.parse(input);
124
+ return ctx.fetch(`/v1/markets/listings/${seg(parsed.listingId)}/availability${query({
125
+ from: parsed.from,
126
+ to: parsed.to,
127
+ })}`, { method: "GET" });
128
+ },
129
+ });
130
+ export const listingRelistInput = z.strictObject({
104
131
  $positionals: positionals,
105
132
  reservationId: z.string().uuid(),
106
133
  startsAt: z.iso.datetime({ offset: true }),
107
134
  endsAt: z.iso.datetime({ offset: true }),
108
135
  nodeCount: z.coerce.number().int().positive(),
109
- nodeIds: nodeIdList.optional(),
110
- reservedNodeIds: nodeIdList.optional(),
111
136
  terms: nativeTermsInput,
112
137
  idempotencyKey: z.string().min(1).max(200).optional(),
113
138
  confirm: z.boolean().optional(),
139
+ yes: z.boolean().optional(),
114
140
  });
115
141
  export const listingRelistCapability = defineCapability({
116
142
  id: "listings.relist",
117
143
  domain: "listings",
118
144
  roles: ["user"],
119
- description: "Relist delivered capacity from a reservation using a native availability window, nodeCount and financial terms. Supply nodeIds or reservedNodeIds only when selecting specific nodes. Requires confirm: true.",
145
+ description: "Relist whole nodes of a reservation's capacity using a native availability window, nodeCount and financial terms. Buyers run on nodes Ornn assigns when their rental starts. Requires confirm: true.",
120
146
  mutation: "preview-confirm",
121
147
  http: { method: "POST", path: "/commerce/reservations/{reservationId}/relist" },
122
148
  input: listingRelistInput,
123
149
  cli: {
124
150
  command: ["capacity-offers", "relist"],
125
- vnext: help(["capacity-offers", "relist"], "Relist reserved capacity with explicit financial terms.", "ornn capacity-offers relist --reservation-id UUID --node-count COUNT --starts-at TIME --ends-at TIME --terms JSON [--node-ids UUID,...] [--reserved-node-ids UUID,...]", "ornn capacity-offers relist --reservation-id UUID --node-count 1 --starts-at TIME --ends-at TIME --terms JSON --idempotency-key KEY --yes --format json", ["capacity-offers terms", "capacity-offers delist"], 23, ["reservation ID"]),
151
+ vnext: help(["capacity-offers", "relist"], "Relist reserved capacity with explicit financial terms.", "ornn capacity-offers relist --reservation-id UUID --node-count COUNT --starts-at TIME --ends-at TIME --terms JSON", "ornn capacity-offers relist --reservation-id UUID --node-count 1 --starts-at TIME --ends-at TIME --terms JSON --idempotency-key KEY --yes --format json", ["capacity-offers terms", "capacity-offers delist"], 24, ["reservation ID"]),
126
152
  },
127
153
  slack: false,
128
154
  execute: async (ctx, input) => {
129
- const { $positionals: _, reservationId, confirm: _confirm, idempotencyKey, ...body } = listingRelistInput.parse(input);
155
+ const { $positionals: _, reservationId, confirm: _confirm, yes: _yes, idempotencyKey, ...body } = listingRelistInput.parse(input);
130
156
  const gate = await consent(ctx, "listings.relist", "Relist capacity", [
131
157
  { label: "Reservation", value: reservationId },
132
158
  { label: "Capacity and financial terms", value: JSON.stringify(body) },
@@ -160,7 +186,7 @@ export const listingUpdateCapability = defineCapability({
160
186
  cli: {
161
187
  command: ["admin", "capacity-offers", "update"],
162
188
  vnext: {
163
- ...help(["admin", "capacity-offers", "update"], "Edit one advertised financial term as staff.", "ornn admin capacity-offers update --listing-id UUID --listing-terms-id UUID --min-duration-hours HOURS --max-duration-hours HOURS --price-per-gpu-hour-micro-usd RATE --renewable true|false --idempotency-key KEY [--type reserved|spot|on-demand] [--down-payment-cents CENTS]", "Requires an eligible human terminal; reuse --idempotency-key on retries.", ["capacity-offers terms"], 24),
189
+ ...help(["admin", "capacity-offers", "update"], "Edit one advertised financial term as staff.", "ornn admin capacity-offers update --listing-id UUID --listing-terms-id UUID --min-duration-hours HOURS --max-duration-hours HOURS --price-per-gpu-hour-micro-usd RATE --renewable true|false --idempotency-key KEY [--type reserved|spot|on-demand] [--down-payment-cents CENTS]", "Requires an eligible human terminal; reuse --idempotency-key on retries.", ["capacity-offers terms"], 25),
164
190
  audience: "admin",
165
191
  },
166
192
  },
@@ -192,7 +218,7 @@ export const listingDelistCapability = defineCapability({
192
218
  input: listingDelistInput,
193
219
  cli: {
194
220
  command: ["capacity-offers", "delist"],
195
- vnext: help(["capacity-offers", "delist"], "Delist a listing you relisted.", "ornn capacity-offers delist --listing-id <id>", "ornn capacity-offers delist --listing-id <id> --yes --format json", ["capacity-offers relist"], 25, ["listing ID"]),
221
+ vnext: help(["capacity-offers", "delist"], "Delist a listing you relisted.", "ornn capacity-offers delist --listing-id <id>", "ornn capacity-offers delist --listing-id <id> --yes --format json", ["capacity-offers relist"], 26, ["listing ID"]),
196
222
  },
197
223
  slack: false,
198
224
  execute: async (ctx, input) => {
@@ -236,7 +262,7 @@ export const reservationsListCapability = defineCapability({
236
262
  input: reservationsListInput,
237
263
  cli: {
238
264
  command: ["reservations", "list"],
239
- vnext: help(["reservations", "list"], "List your organization's schedule and spot reservations.", "ornn reservations list [--kind schedule|spot] [--status <status>] [--listing-id <id>] [--limit <1-200>] [--cursor <opaque>]", "ornn reservations list --format json", ["reservations show", "capacity-offers relist"], 26, ["opaque page cursor"]),
265
+ vnext: help(["reservations", "list"], "List your organization's schedule and spot reservations.", "ornn reservations list [--kind schedule|spot] [--status <status>] [--listing-id <id>] [--limit <1-200>] [--cursor <opaque>]", "ornn reservations list --format json", ["reservations show", "capacity-offers relist"], 27, ["opaque page cursor"]),
240
266
  },
241
267
  slack: false,
242
268
  execute: async (ctx, input) => {
@@ -264,7 +290,7 @@ export const reservationsShowCapability = defineCapability({
264
290
  input: reservationLookupInput,
265
291
  cli: {
266
292
  command: ["reservations", "show"],
267
- vnext: help(["reservations", "show"], "Inspect one reservation.", "ornn reservations show --reservation-id <id>", "ornn reservations show --reservation-id <id> --format json", ["reservations list", "reservations invoices"], 27, ["reservation ID"]),
293
+ vnext: help(["reservations", "show"], "Inspect one reservation.", "ornn reservations show --reservation-id <id>", "ornn reservations show --reservation-id <id> --format json", ["reservations list", "reservations invoices"], 28, ["reservation ID"]),
268
294
  },
269
295
  slack: false,
270
296
  execute: async (ctx, input) => {
@@ -282,7 +308,7 @@ export const reservationInvoicesCapability = defineCapability({
282
308
  input: reservationLookupInput,
283
309
  cli: {
284
310
  command: ["reservations", "invoices"],
285
- vnext: help(["reservations", "invoices"], "List one reservation's invoices.", "ornn reservations invoices --reservation-id <id>", "ornn reservations invoices --reservation-id <id> --format json", ["reservations show"], 28, ["reservation ID"]),
311
+ vnext: help(["reservations", "invoices"], "List one reservation's invoices.", "ornn reservations invoices --reservation-id <id>", "ornn reservations invoices --reservation-id <id> --format json", ["reservations show"], 29, ["reservation ID"]),
286
312
  },
287
313
  slack: false,
288
314
  execute: async (ctx, input) => {
@@ -292,14 +318,88 @@ export const reservationInvoicesCapability = defineCapability({
292
318
  });
293
319
  },
294
320
  });
321
+ export const reservationPurchaseInput = z.object({
322
+ $positionals: positionals,
323
+ listingId: z
324
+ .string()
325
+ .uuid()
326
+ .describe("The relisted (secondary) listing's capacityListingId, from listings.list."),
327
+ listingTermId: z
328
+ .string()
329
+ .uuid()
330
+ .describe("The listingTermsId of the listing's term, from listings.terms."),
331
+ nodeCount: z.coerce
332
+ .number()
333
+ .int()
334
+ .positive()
335
+ .optional()
336
+ .describe("Whole nodes to book for the window. Defaults to every node the listing sells."),
337
+ startsAt: z.iso
338
+ .datetime({ offset: true })
339
+ .describe("RFC 3339 start of the window, in whole seconds, inside the listing and not in the past."),
340
+ endsAt: z.iso
341
+ .datetime({ offset: true })
342
+ .describe("RFC 3339 end of the window, at least an hour after the start and within the term's duration range, or at the listing's end."),
343
+ pricePerGpuHourMicroUsd: microUsd.describe("The term's price from listings.terms, in micro-USD per GPU-hour. A changed price refuses the purchase."),
344
+ idempotencyKey: z.string().min(1).max(200).optional(),
345
+ confirm: z.boolean().optional(),
346
+ });
347
+ export const reservationPurchaseCapability = defineCapability({
348
+ id: "reservations.purchase",
349
+ domain: "reservations",
350
+ roles: ["user"],
351
+ description: "Book a window of a relisted (secondary) listing on one of its terms at the term's price. The GPUs are held for you until the window starts; then it becomes your reservation, with the booking's ID. Nothing is charged when you book. Primary listings take offers instead (offers.post). Requires confirm: true.",
352
+ mutation: "preview-confirm",
353
+ http: { method: "POST", path: "/v1/markets/bookings" },
354
+ input: reservationPurchaseInput,
355
+ cli: {
356
+ command: ["reservations", "buy"],
357
+ vnext: help(["reservations", "buy"], "Book a window of relisted capacity at its term's price.", "ornn reservations buy --listing-id <id> --listing-term-id <id> --starts-at <time> --ends-at <time> --price-per-gpu-hour-micro-usd <n> [--node-count <n>]", "ornn reservations buy --listing-id <id> --listing-term-id <id> --starts-at <time> --ends-at <time> --price-per-gpu-hour-micro-usd 4000000 --idempotency-key <key> --yes --format json", ["capacity-offers availability", "capacity-offers terms", "reservations show"], 30, ["listingTermsId from capacity-offers terms"]),
358
+ },
359
+ slack: false,
360
+ execute: async (ctx, input) => {
361
+ const parsed = reservationPurchaseInput.parse(input);
362
+ const body = {
363
+ legs: [
364
+ {
365
+ listingId: parsed.listingId,
366
+ listingTermId: parsed.listingTermId,
367
+ ...(parsed.nodeCount !== undefined ? { nodes: parsed.nodeCount } : {}),
368
+ startsAt: parsed.startsAt,
369
+ endsAt: parsed.endsAt,
370
+ pricePerGpuHourMicroUsd: parsed.pricePerGpuHourMicroUsd,
371
+ },
372
+ ],
373
+ };
374
+ const gate = await consent(ctx, "reservations.purchase", "Book relisted window", [
375
+ { label: "Listing term", value: parsed.listingTermId },
376
+ {
377
+ label: "Nodes",
378
+ value: parsed.nodeCount !== undefined ? String(parsed.nodeCount) : "Every node on the listing",
379
+ },
380
+ { label: "Window", value: `${parsed.startsAt} to ${parsed.endsAt}` },
381
+ { label: "Price", value: microUsdLabel(parsed.pricePerGpuHourMicroUsd) },
382
+ { label: "Payment", value: "Nothing is charged when you book." },
383
+ ], body);
384
+ if (gate)
385
+ return gate;
386
+ return ctx.fetch("/v1/markets/bookings", {
387
+ method: "POST",
388
+ body,
389
+ headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
390
+ });
391
+ },
392
+ });
295
393
  export const commerceMarketCapabilities = [
296
394
  listingsListCapability,
297
395
  listingShowCapability,
298
396
  listingTermsCapability,
397
+ listingAvailabilityCapability,
299
398
  listingRelistCapability,
300
399
  listingUpdateCapability,
301
400
  listingDelistCapability,
302
401
  reservationsListCapability,
303
402
  reservationsShowCapability,
304
403
  reservationInvoicesCapability,
404
+ reservationPurchaseCapability,
305
405
  ];
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { defineCapability, preview } from "../capability.js";
3
+ export const creditGrantInput = z.object({
4
+ organizationId: z
5
+ .uuid()
6
+ .describe("Customer organization UUID. Resolve names or emails with organization_list; ask which organization if matches are ambiguous."),
7
+ adjustmentId: z
8
+ .uuid()
9
+ .describe("The credit-adjustment API's required grant UUID. Keep the same UUID when confirming this grant."),
10
+ amountCents: z
11
+ .number()
12
+ .int()
13
+ .min(1)
14
+ .max(100_000_000)
15
+ .describe("USD cents: 10000 grants $100.00."),
16
+ note: z
17
+ .string()
18
+ .trim()
19
+ .min(1)
20
+ .refine((value) => new TextEncoder().encode(value).length <= 500, "Keep the note under 500 UTF-8 bytes.")
21
+ .describe("Reason for the credit grant."),
22
+ confirm: z.boolean().optional(),
23
+ });
24
+ export const creditGrantCapability = defineCapability({
25
+ id: "credits.grant",
26
+ domain: "billing",
27
+ roles: ["admin"],
28
+ description: "Grant Ornn credits to a customer organization using the same operation as Internal Billing. " +
29
+ "Credits belong to the organization, not an individual person. Resolve the intended organization first. " +
30
+ "Preview first; confirm:true posts the same grant with its adjustmentId. The verified staff actor supplies audit identity.",
31
+ mutation: "preview-confirm",
32
+ http: { method: "POST", path: "/internal/v1/organizations/{organizationId}/credit-adjustments" },
33
+ cli: {
34
+ command: ["admin", "organizations", "grant-credit"],
35
+ vnext: {
36
+ audience: "admin",
37
+ category: "resource",
38
+ command: ["admin", "organizations", "grant-credit"],
39
+ outcome: "Grant Ornn credits to a customer organization.",
40
+ human: "ornn admin organizations grant-credit --organization UUID --amount-cents N --note TEXT --idempotency-key UUID [--yes]",
41
+ automation: "Requires an eligible human terminal.",
42
+ acceptedReferences: ["organization UUID from admin organizations list"],
43
+ review: "Review the organization, USD amount, note, and grant UUID before default-no consent.",
44
+ related: ["admin organizations list", "admin users list"],
45
+ },
46
+ },
47
+ slack: {
48
+ parameters: {
49
+ type: "object",
50
+ properties: {
51
+ organizationId: {
52
+ type: "string",
53
+ format: "uuid",
54
+ description: "Exact customer organization UUID from organization_list.",
55
+ },
56
+ amountCents: {
57
+ type: "integer",
58
+ minimum: 1,
59
+ maximum: 100_000_000,
60
+ description: "USD cents: 10000 grants $100.00.",
61
+ },
62
+ note: {
63
+ type: "string",
64
+ description: "Reason for the credit grant, at most 500 UTF-8 bytes.",
65
+ },
66
+ },
67
+ required: ["organizationId", "amountCents", "note"],
68
+ },
69
+ },
70
+ input: creditGrantInput,
71
+ execute: async (ctx, input) => {
72
+ const { organizationId, confirm: _confirm, ...grant } = creditGrantInput.parse(input);
73
+ const body = { ...grant, direction: "credit" };
74
+ if (!ctx.confirmed)
75
+ return preview("credits.grant", { organizationId, ...body });
76
+ return ctx.fetch(`/internal/v1/organizations/${encodeURIComponent(organizationId)}/credit-adjustments`, { method: "POST", body });
77
+ },
78
+ });
@@ -27,7 +27,8 @@ const nodesInput = z.object({
27
27
  facility_id: z.string().optional(),
28
28
  facilityId: z.string().optional(),
29
29
  status: z.string().optional(),
30
- email: z.string().optional(),
30
+ reservation_id: z.string().optional(),
31
+ reservationId: z.string().optional(),
31
32
  cursor: z.string().optional(),
32
33
  max: z.number().int().positive().max(5000).optional(),
33
34
  limit: z.number().int().positive().max(5000).optional(),
@@ -113,7 +114,7 @@ export const listNodesCapability = defineCapability({
113
114
  id: "nodes.list",
114
115
  domain: "fleet",
115
116
  roles: ["user", "admin"],
116
- description: "List nodes. Users see their own reserved machines. Admins see enrolled, ghost, and off-grid nodes. Call with no filters for the whole fleet. Optional email lists nodes for that user's organizations. Optional organization_id (supplier), facility_id, and status (ghost, off-grid, bare-metal) narrow the same list. Paginate large fleets with cursor + max: pass the response's next_cursor back as cursor to fetch the next page; next_cursor is omitted once the page is not full. Never invent nodes. Never print private keys.",
117
+ description: "List nodes. Users see their own reserved machines. Admins see enrolled, ghost, and off-grid nodes. Call with no filters for the whole fleet. Optional reservation_id lists the nodes on one reservation (find a customer's reservations with reservation.list customerId). Optional organization_id (supplier), facility_id, and status (ghost, off-grid, bare-metal) narrow the same list. Paginate large fleets with cursor + max: pass the response's next_cursor back as cursor to fetch the next page; next_cursor is omitted once the page is not full. Never invent nodes. Never print private keys.",
117
118
  mutation: "read",
118
119
  http: { method: "GET", path: "/nodes" },
119
120
  slack: {
@@ -128,9 +129,9 @@ export const listNodesCapability = defineCapability({
128
129
  type: "string",
129
130
  description: "Optional commerce facility UUID from facilities.list.",
130
131
  },
131
- email: {
132
+ reservation_id: {
132
133
  type: "string",
133
- description: "Optional. List nodes for this user's organizations.",
134
+ description: "Optional reservation UUID from reservation_list. Lists that reservation's nodes.",
134
135
  },
135
136
  status: {
136
137
  type: "string",
@@ -162,7 +163,7 @@ export const listNodesCapability = defineCapability({
162
163
  organization_id: parsed.organization_id ?? parsed.organizationId,
163
164
  facility_id: parsed.facility_id ?? parsed.facilityId,
164
165
  status: parsed.status,
165
- email: parsed.email,
166
+ reservation_id: parsed.reservation_id ?? parsed.reservationId,
166
167
  ...pagination,
167
168
  }), { method: "GET" });
168
169
  },
@@ -173,7 +174,7 @@ export const nodeOffGridCapability = defineCapability({
173
174
  roles: ["admin"],
174
175
  description: "Admin-only. Take a node off-grid (remove Ornn agent management, leave the host powered on). CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
175
176
  mutation: "preview-confirm",
176
- http: { method: "POST", path: "/internal/nodes/{id}/off-grid" },
177
+ http: { method: "POST", path: "/nodes/{id}/off-grid" },
177
178
  slack: {
178
179
  parameters: {
179
180
  type: "object",
@@ -192,7 +193,7 @@ export const nodeOffGridCapability = defineCapability({
192
193
  if (!ctx.confirmed) {
193
194
  return preview("node.off-grid", { node_id: id, ...body });
194
195
  }
195
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/off-grid`, {
196
+ return ctx.fetch(`/nodes/${encodeURIComponent(id)}/off-grid`, {
196
197
  method: "POST",
197
198
  body,
198
199
  });
@@ -204,7 +205,7 @@ export const nodeOnGridCapability = defineCapability({
204
205
  roles: ["admin"],
205
206
  description: "Admin-only. Re-enroll an off-grid node: orchestrator SSHs with the stored admin key and runs the installer with restore_node_id. No caller key. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
206
207
  mutation: "preview-confirm",
207
- http: { method: "POST", path: "/internal/nodes/{id}/on-grid" },
208
+ http: { method: "POST", path: "/nodes/{id}/on-grid" },
208
209
  slack: {
209
210
  parameters: {
210
211
  type: "object",
@@ -221,7 +222,7 @@ export const nodeOnGridCapability = defineCapability({
221
222
  if (!ctx.confirmed) {
222
223
  return preview("node.on-grid", { node_id: id });
223
224
  }
224
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/on-grid`, {
225
+ return ctx.fetch(`/nodes/${encodeURIComponent(id)}/on-grid`, {
225
226
  method: "POST",
226
227
  body: {},
227
228
  });
@@ -233,7 +234,7 @@ export const nodeExecCapability = defineCapability({
233
234
  roles: ["admin"],
234
235
  description: "Admin-only. Run one unrestricted command on an enrolled node over orchestrator SSH with the stored admin key. Always pass timeout_seconds. Not an interactive shell. On NVIDIA hosts use nvidia-smi; on AMD Instinct use amd-smi. Runs on the tool call — no confirm, no y/N.",
235
236
  mutation: "read",
236
- http: { method: "POST", path: "/internal/remote" },
237
+ http: { method: "POST", path: "/remote" },
237
238
  slack: {
238
239
  parameters: {
239
240
  type: "object",
@@ -253,7 +254,7 @@ export const nodeExecCapability = defineCapability({
253
254
  const parsed = nodeExecInput.parse(input);
254
255
  const id = nodeIdOf(parsed);
255
256
  const timeoutSeconds = parsed.timeout_seconds ?? parsed.timeoutSeconds;
256
- return ctx.fetch("/internal/remote", {
257
+ return ctx.fetch("/remote", {
257
258
  method: "POST",
258
259
  body: { node_id: id, command: parsed.command, timeout_seconds: timeoutSeconds },
259
260
  });
@@ -265,7 +266,7 @@ export const nodeTerminateCapability = defineCapability({
265
266
  roles: ["admin"],
266
267
  description: "Admin-only. Terminate a node record (best-effort clean + delete). Off-grid nodes need force: true. There is no deenroll verb. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
267
268
  mutation: "preview-confirm",
268
- http: { method: "POST", path: "/internal/nodes/{id}/terminate" },
269
+ http: { method: "POST", path: "/nodes/{id}/terminate" },
269
270
  slack: {
270
271
  parameters: {
271
272
  type: "object",
@@ -288,7 +289,7 @@ export const nodeTerminateCapability = defineCapability({
288
289
  if (!ctx.confirmed) {
289
290
  return preview("node.terminate", { node_id: id, ...body });
290
291
  }
291
- return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/terminate`, {
292
+ return ctx.fetch(`/nodes/${encodeURIComponent(id)}/terminate`, {
292
293
  method: "POST",
293
294
  body,
294
295
  });
@@ -267,7 +267,7 @@ export const createListingCapability = defineCapability({
267
267
  : !facilityQuery ||
268
268
  [
269
269
  facility.facilityId ?? facility.id,
270
- facility.slug,
270
+ facility.region,
271
271
  facility.displayName ?? facility.display_name,
272
272
  ].some((value) => value?.toLowerCase() === facilityQuery))));
273
273
  cursor = response.nextCursor ?? undefined;
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
  import { preview, } from "../capability.js";
3
3
  /** CLI semantic arguments arrive as strings; MCP and Slack send JSON values. */
4
4
  export const positionals = z.array(z.string()).max(0).default([]);
5
+ /** Integer micro-USD per GPU-hour (1000000 = $1.00). */
6
+ export const microUsd = z.coerce.number().int().positive();
5
7
  export const flag = z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean());
6
8
  export function seg(value) {
7
9
  return encodeURIComponent(value);