@ornncompute/cli 0.2.8 → 0.2.10
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/package.json +1 -1
- package/vendor/capabilities/capabilities/listings.d.ts +5 -0
- package/vendor/capabilities/capabilities/listings.js +167 -5
- package/vendor/capabilities/capabilities/staff-access.d.ts +3 -1
- package/vendor/capabilities/capabilities/staff-access.js +100 -28
- package/vendor/capabilities/capabilities/staff-commerce.d.ts +91 -14
- package/vendor/capabilities/capabilities/staff-commerce.js +466 -67
- package/vendor/capabilities/capabilities/staff-fleet.d.ts +33 -34
- package/vendor/capabilities/capabilities/staff-fleet.js +143 -70
- package/vendor/capabilities/capabilities/staff-shared.d.ts +29 -0
- package/vendor/capabilities/capabilities/staff-shared.js +64 -0
- package/vendor/capabilities/index.d.ts +2 -2
- package/vendor/capabilities/index.js +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineStaffMcpCapability, preview } from "../capability.js";
|
|
3
|
-
import { mapBody, seg } from "./staff-shared.js";
|
|
3
|
+
import { errorTextOf, mapBody, requireUuid, resolveVersion, seg, statusOf } from "./staff-shared.js";
|
|
4
4
|
// Staff commerce verbs: reservations, bids, forward inventory, and the tenant
|
|
5
5
|
// credit profile. MCP-only — see `defineStaffMcpCapability`.
|
|
6
6
|
export const reservationCancelInput = z.object({
|
|
@@ -28,8 +28,6 @@ export const reservationCancelCapability = defineStaffMcpCapability({
|
|
|
28
28
|
export const reservationTransferInput = z.object({
|
|
29
29
|
reservationId: z.string(),
|
|
30
30
|
targetTenantId: z.string(),
|
|
31
|
-
targetAuthUserId: z.string().optional(),
|
|
32
|
-
nodeId: z.string().optional(),
|
|
33
31
|
reservedStrategy: z.enum(["reject", "park"]),
|
|
34
32
|
confirmReservedTransfer: z.boolean().optional(),
|
|
35
33
|
notes: z.string().max(2000).optional(),
|
|
@@ -39,35 +37,50 @@ export const reservationTransferCapability = defineStaffMcpCapability({
|
|
|
39
37
|
id: "reservation.transfer",
|
|
40
38
|
domain: "reservations",
|
|
41
39
|
roles: ["admin"],
|
|
42
|
-
description: "Internal staff only: transfer a reservation to a different tenant
|
|
40
|
+
description: "Internal staff only: transfer a reservation to a different tenant. Mirrors the internal node-overlay transfer action. " +
|
|
43
41
|
"reservedStrategy has no default and must be chosen explicitly: \"reject\" (transfer only if the reservation isn't " +
|
|
44
42
|
"currently provisioned/assigned) or \"park\" (clean up old access and park it for the target tenant, for a live handoff). " +
|
|
45
43
|
"The dashboard always computes this from the reservation's live state before sending it rather than relying on the " +
|
|
46
44
|
"backend's own default of \"reject\", because a silent \"reject\" default on a reservation the backend doesn't detect as " +
|
|
47
|
-
"actively provisioned/assigned would transfer it without ever asking for
|
|
45
|
+
"actively provisioned/assigned would transfer it without ever asking for confirmReservedTransfer.",
|
|
48
46
|
mutation: "preview-confirm",
|
|
49
47
|
http: { method: "POST", path: "/internal/reservations/{reservationId}/transfer" },
|
|
50
48
|
input: reservationTransferInput,
|
|
51
49
|
execute: async (ctx, input) => {
|
|
52
50
|
const parsed = reservationTransferInput.parse(input);
|
|
51
|
+
// camelCase: commerce's transferReservationRequest has no snake aliases,
|
|
52
|
+
// so the previous all-snake body bound nothing but `notes` and every
|
|
53
|
+
// transfer failed with "targetOrganizationId is required".
|
|
53
54
|
const body = {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
...mapBody(parsed, {
|
|
58
|
-
targetAuthUserId: "target_auth_user_id",
|
|
59
|
-
nodeId: "node_id",
|
|
60
|
-
notes: "notes",
|
|
61
|
-
}),
|
|
55
|
+
targetOrganizationId: requireUuid(parsed.targetTenantId, "targetTenantId"),
|
|
56
|
+
reservedStrategy: parsed.reservedStrategy,
|
|
57
|
+
confirmReservedTransfer: parsed.confirmReservedTransfer ?? false,
|
|
58
|
+
...mapBody(parsed, { notes: "notes" }),
|
|
62
59
|
};
|
|
63
60
|
if (!ctx.confirmed)
|
|
64
61
|
return preview("reservation.transfer", body);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
try {
|
|
63
|
+
return await ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}/transfer`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw inTenantVocabulary(error);
|
|
70
|
+
}
|
|
69
71
|
},
|
|
70
72
|
});
|
|
73
|
+
/**
|
|
74
|
+
* Commerce names the transfer target `targetOrganizationId`; this tool's
|
|
75
|
+
* parameter is `targetTenantId`. Passing the wire name through tells the
|
|
76
|
+
* caller to go fix a parameter that does not exist, so translate it back.
|
|
77
|
+
*/
|
|
78
|
+
function inTenantVocabulary(error) {
|
|
79
|
+
const text = errorTextOf(error);
|
|
80
|
+
if (!text.includes("targetOrganizationId"))
|
|
81
|
+
return error;
|
|
82
|
+
return new Error(text.replaceAll("targetOrganizationId", "targetTenantId"));
|
|
83
|
+
}
|
|
71
84
|
export const reservationMachinesListInput = z.object({ reservationId: z.string() });
|
|
72
85
|
export const reservationMachinesListCapability = defineStaffMcpCapability({
|
|
73
86
|
id: "reservation.machines.list",
|
|
@@ -86,32 +99,55 @@ export const reservationMachinesListCapability = defineStaffMcpCapability({
|
|
|
86
99
|
});
|
|
87
100
|
export const reservationSshKeyAddInput = z.object({
|
|
88
101
|
reservationId: z.string(),
|
|
102
|
+
tenantId: z.string(),
|
|
89
103
|
userId: z.string(),
|
|
90
|
-
|
|
104
|
+
sshKeyId: z.string().optional(),
|
|
105
|
+
keyId: z.string().optional(),
|
|
106
|
+
publicKey: z.string().min(1).optional(),
|
|
91
107
|
label: z.string().optional(),
|
|
92
|
-
nodeId: z.string().optional(),
|
|
93
108
|
confirm: z.boolean().optional(),
|
|
94
109
|
});
|
|
95
110
|
export const reservationSshKeyAddCapability = defineStaffMcpCapability({
|
|
96
111
|
id: "reservation.ssh-key.add",
|
|
97
112
|
domain: "reservations",
|
|
98
113
|
roles: ["admin"],
|
|
99
|
-
description: "Internal staff only:
|
|
114
|
+
description: "Internal staff only: attach an SSH key to a reservation for a tenant user. " +
|
|
115
|
+
"Pass sshKeyId to attach a key the organization already has, or publicKey (with label) to register a new " +
|
|
116
|
+
"organization key and attach it. Attaching does not itself push the key to live machines; use ssh_key_push for that. " +
|
|
117
|
+
"Requires confirm: true.",
|
|
100
118
|
mutation: "preview-confirm",
|
|
101
119
|
http: { method: "POST", path: "/internal/v1/reservations/{reservationId}/ssh-keys" },
|
|
102
120
|
input: reservationSshKeyAddInput,
|
|
103
121
|
execute: async (ctx, input) => {
|
|
104
122
|
const parsed = reservationSshKeyAddInput.parse(input);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
const keyId = parsed.sshKeyId ?? parsed.keyId;
|
|
124
|
+
if (!keyId && !parsed.publicKey) {
|
|
125
|
+
throw new Error("Pass sshKeyId to attach an existing organization key, or publicKey to register a new one.");
|
|
126
|
+
}
|
|
127
|
+
if (!ctx.confirmed) {
|
|
128
|
+
return preview("reservation.ssh-key.add", {
|
|
129
|
+
reservationId: parsed.reservationId,
|
|
130
|
+
userId: parsed.userId,
|
|
131
|
+
...(keyId ? { sshKeyId: keyId } : { publicKey: parsed.publicKey, label: parsed.label }),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Commerce attaches an existing authorization key by id; it reads only
|
|
135
|
+
// sshKeyId/userId. The old body also sent public_key, label and node_id,
|
|
136
|
+
// which bound nothing, so a publicKey-only call always failed.
|
|
137
|
+
let sshKeyId = keyId;
|
|
138
|
+
if (!sshKeyId) {
|
|
139
|
+
const registered = (await ctx.fetch(`/v1/organizations/${seg(parsed.tenantId)}/ssh-keys`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
body: { public_key: parsed.publicKey, ...mapBody(parsed, { label: "label" }) },
|
|
142
|
+
}));
|
|
143
|
+
if (!registered.id) {
|
|
144
|
+
throw new Error("Authorization did not return an SSH key id.");
|
|
145
|
+
}
|
|
146
|
+
sshKeyId = registered.id;
|
|
147
|
+
}
|
|
112
148
|
return ctx.fetch(`/internal/v1/reservations/${seg(parsed.reservationId)}/ssh-keys`, {
|
|
113
149
|
method: "POST",
|
|
114
|
-
body,
|
|
150
|
+
body: { sshKeyId, userId: parsed.userId },
|
|
115
151
|
});
|
|
116
152
|
},
|
|
117
153
|
});
|
|
@@ -119,69 +155,181 @@ export const reservationSshKeyRemoveInput = z.object({
|
|
|
119
155
|
reservationId: z.string(),
|
|
120
156
|
keyId: z.string(),
|
|
121
157
|
userId: z.string(),
|
|
122
|
-
nodeId: z.string().optional(),
|
|
123
158
|
confirm: z.boolean().optional(),
|
|
124
159
|
});
|
|
125
160
|
export const reservationSshKeyRemoveCapability = defineStaffMcpCapability({
|
|
126
161
|
id: "reservation.ssh-key.remove",
|
|
127
162
|
domain: "reservations",
|
|
128
163
|
roles: ["admin"],
|
|
129
|
-
description: "Internal staff only:
|
|
164
|
+
description: "Internal staff only: detach an SSH key from a reservation for a tenant user. " +
|
|
165
|
+
"Detaching does not itself remove the key from live machines; use ssh_key_push to resync them. Requires confirm: true.",
|
|
130
166
|
mutation: "preview-confirm",
|
|
131
167
|
http: { method: "DELETE", path: "/internal/v1/reservations/{reservationId}/ssh-keys/{keyId}" },
|
|
132
168
|
input: reservationSshKeyRemoveInput,
|
|
133
169
|
execute: async (ctx, input) => {
|
|
134
170
|
const parsed = reservationSshKeyRemoveInput.parse(input);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
171
|
+
if (!ctx.confirmed) {
|
|
172
|
+
return preview("reservation.ssh-key.remove", {
|
|
173
|
+
reservationId: parsed.reservationId,
|
|
174
|
+
keyId: parsed.keyId,
|
|
175
|
+
userId: parsed.userId,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// The handler reads userId from the query string, not the body. Sent as a
|
|
179
|
+
// body it was dropped and the detach ran unscoped across every user.
|
|
180
|
+
const result = await ctx.fetch(`/internal/v1/reservations/${seg(parsed.reservationId)}/ssh-keys/${seg(parsed.keyId)}?userId=${seg(parsed.userId)}`, { method: "DELETE" });
|
|
181
|
+
return (result ?? {
|
|
182
|
+
removed: true,
|
|
183
|
+
reservationId: parsed.reservationId,
|
|
184
|
+
keyId: parsed.keyId,
|
|
185
|
+
userId: parsed.userId,
|
|
186
|
+
});
|
|
142
187
|
},
|
|
143
188
|
});
|
|
144
189
|
export const reservationPublishInput = z.object({
|
|
145
|
-
reservationId: z.string(),
|
|
190
|
+
reservationId: z.string().optional(),
|
|
191
|
+
reservation_id: z.string().optional(),
|
|
192
|
+
version: z.number().int().positive().optional(),
|
|
193
|
+
awaitDownPayment: z.boolean().optional(),
|
|
194
|
+
await_down_payment: z.boolean().optional(),
|
|
146
195
|
confirm: z.boolean().optional(),
|
|
147
196
|
});
|
|
148
197
|
export const reservationPublishCapability = defineStaffMcpCapability({
|
|
149
198
|
id: "reservation.publish",
|
|
150
199
|
domain: "reservations",
|
|
151
200
|
roles: ["admin"],
|
|
152
|
-
description: "Internal staff only: publish a commerce draft into a live Commerce reservation. Requires confirm: true.",
|
|
201
|
+
description: "Internal staff only: publish a commerce draft or uncommitted bid reservation into a live Commerce reservation. Requires confirm: true.",
|
|
153
202
|
mutation: "preview-confirm",
|
|
154
203
|
http: { method: "POST", path: "/internal/reservations/{reservationId}/publish" },
|
|
155
204
|
input: reservationPublishInput,
|
|
156
205
|
execute: async (ctx, input) => {
|
|
157
206
|
const parsed = reservationPublishInput.parse(input);
|
|
207
|
+
const reservationId = parsed.reservationId || parsed.reservation_id;
|
|
208
|
+
if (!reservationId) {
|
|
209
|
+
throw new Error("reservationId is required");
|
|
210
|
+
}
|
|
211
|
+
const version = await resolveVersion(ctx, `/internal/reservations/${seg(reservationId)}`, parsed.version);
|
|
212
|
+
const awaitDownPayment = parsed.awaitDownPayment ?? parsed.await_down_payment;
|
|
213
|
+
const body = {
|
|
214
|
+
...(version !== undefined ? { version } : {}),
|
|
215
|
+
...(awaitDownPayment !== undefined ? { awaitDownPayment, await_down_payment: awaitDownPayment } : {}),
|
|
216
|
+
};
|
|
158
217
|
if (!ctx.confirmed) {
|
|
159
|
-
return preview("reservation.publish", { reservationId
|
|
218
|
+
return preview("reservation.publish", { reservationId, ...body });
|
|
160
219
|
}
|
|
161
|
-
return ctx.fetch(`/internal/reservations/${seg(
|
|
220
|
+
return ctx.fetch(`/internal/reservations/${seg(reservationId)}/publish`, {
|
|
162
221
|
method: "POST",
|
|
222
|
+
...(Object.keys(body).length > 0 ? { body } : {}),
|
|
163
223
|
});
|
|
164
224
|
},
|
|
165
225
|
});
|
|
226
|
+
export const reservationShowInput = z.object({ reservationId: z.string() });
|
|
227
|
+
// Accepting a bid inserts its fill reservation as 'uncommitted' (commerce's
|
|
228
|
+
// InsertBidFillReservation): it holds no capacity and carries no invoice until
|
|
229
|
+
// it is published, and buyer-facing reads deliberately filter it out. Staff had
|
|
230
|
+
// no read of their own, so the reservationId that bid.accept returns could not
|
|
231
|
+
// be looked up anywhere at all. These two verbs are the admin-side read, which
|
|
232
|
+
// does surface uncommitted rows.
|
|
233
|
+
export const reservationShowCapability = defineStaffMcpCapability({
|
|
234
|
+
id: "reservation.show",
|
|
235
|
+
domain: "reservations",
|
|
236
|
+
roles: ["reviewer", "admin"],
|
|
237
|
+
description: "Internal staff only: show one commerce reservation, including draft and uncommitted rows that " +
|
|
238
|
+
"customer-facing reads hide (an accepted bid's fill reservation is uncommitted until it is published).",
|
|
239
|
+
mutation: "read",
|
|
240
|
+
http: { method: "GET", path: "/internal/reservations/{reservationId}" },
|
|
241
|
+
input: reservationShowInput,
|
|
242
|
+
execute: async (ctx, input) => {
|
|
243
|
+
const parsed = reservationShowInput.parse(input);
|
|
244
|
+
return ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}`, { method: "GET" });
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
export const reservationListInput = z.object({
|
|
248
|
+
organizationId: z.string().optional(),
|
|
249
|
+
status: z.string().optional(),
|
|
250
|
+
cursor: z.string().optional(),
|
|
251
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
252
|
+
});
|
|
253
|
+
export const reservationListCapability = defineStaffMcpCapability({
|
|
254
|
+
id: "reservation.list",
|
|
255
|
+
domain: "reservations",
|
|
256
|
+
roles: ["reviewer", "admin"],
|
|
257
|
+
description: "Internal staff only: list commerce reservations, optionally filtered by tenant and status. " +
|
|
258
|
+
"Includes draft and uncommitted rows that customer-facing reads hide.",
|
|
259
|
+
mutation: "read",
|
|
260
|
+
http: { method: "GET", path: "/internal/reservations" },
|
|
261
|
+
input: reservationListInput,
|
|
262
|
+
execute: async (ctx, input) => {
|
|
263
|
+
const parsed = reservationListInput.parse(input ?? {});
|
|
264
|
+
const parts = [];
|
|
265
|
+
if (parsed.organizationId) {
|
|
266
|
+
parts.push(`organization_id=${encodeURIComponent(parsed.organizationId)}`);
|
|
267
|
+
}
|
|
268
|
+
if (parsed.status)
|
|
269
|
+
parts.push(`status=${encodeURIComponent(parsed.status)}`);
|
|
270
|
+
if (parsed.cursor)
|
|
271
|
+
parts.push(`cursor=${encodeURIComponent(parsed.cursor)}`);
|
|
272
|
+
if (parsed.limit)
|
|
273
|
+
parts.push(`limit=${encodeURIComponent(String(parsed.limit))}`);
|
|
274
|
+
const qs = parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
275
|
+
return ctx.fetch(`/internal/reservations${qs}`, { method: "GET" });
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
// Kept a plain object because tool registration reads `schema.shape`; the
|
|
279
|
+
// required-ness is enforced in execute below.
|
|
166
280
|
export const bidAcceptInput = z.object({
|
|
167
|
-
bidId: z.string(),
|
|
281
|
+
bidId: z.string().optional(),
|
|
282
|
+
bid_id: z.string().optional(),
|
|
168
283
|
acceptedGpuCount: z.number().int().min(1).optional(),
|
|
284
|
+
accepted_gpu_count: z.number().int().min(1).optional(),
|
|
285
|
+
fillNodeCount: z.number().int().min(1).optional(),
|
|
286
|
+
fill_node_count: z.number().int().min(1).optional(),
|
|
169
287
|
confirm: z.boolean().optional(),
|
|
170
288
|
});
|
|
171
289
|
export const bidAcceptCapability = defineStaffMcpCapability({
|
|
172
290
|
id: "bid.accept",
|
|
173
291
|
domain: "bids",
|
|
174
292
|
roles: ["admin"],
|
|
175
|
-
description: "Internal staff only: accept an active bid.
|
|
293
|
+
description: "Internal staff only: accept an active bid. acceptedGpuCount (alias fillNodeCount) is required - commerce " +
|
|
294
|
+
"has no default; pass the bid's full nodeCount to fill it completely, or a smaller number for a partial " +
|
|
295
|
+
"fill. Mirrors the internal dashboard's Accept action. Automatically publishes the resulting reservation " +
|
|
296
|
+
"so it holds capacity and is immediately visible to customer reads.",
|
|
176
297
|
mutation: "preview-confirm",
|
|
177
298
|
http: { method: "POST", path: "/internal/bids/{bidId}/accept" },
|
|
178
299
|
input: bidAcceptInput,
|
|
179
300
|
execute: async (ctx, input) => {
|
|
180
301
|
const parsed = bidAcceptInput.parse(input);
|
|
181
|
-
const
|
|
302
|
+
const bidId = parsed.bidId || parsed.bid_id;
|
|
303
|
+
if (!bidId) {
|
|
304
|
+
throw new Error("bidId is required");
|
|
305
|
+
}
|
|
306
|
+
const count = parsed.acceptedGpuCount ?? parsed.accepted_gpu_count ?? parsed.fillNodeCount ?? parsed.fill_node_count;
|
|
307
|
+
// commerce's acceptBidRequest.Validate rejects a missing count outright
|
|
308
|
+
// (services/commerce/internal/api/admin/bid_types.go), so refuse here
|
|
309
|
+
// rather than sending a call that always 400s.
|
|
310
|
+
if (count === undefined) {
|
|
311
|
+
throw new Error("acceptedGpuCount (or fillNodeCount) is required; commerce has no default fill count. Pass the bid's nodeCount to fill it completely.");
|
|
312
|
+
}
|
|
313
|
+
const body = { accepted_gpu_count: count };
|
|
182
314
|
if (!ctx.confirmed)
|
|
183
|
-
return preview("bid.accept", { bidId
|
|
184
|
-
|
|
315
|
+
return preview("bid.accept", { bidId, ...body });
|
|
316
|
+
const accepted = (await ctx.fetch(`/internal/bids/${seg(bidId)}/accept`, { method: "POST", body }));
|
|
317
|
+
const reservationId = (accepted?.reservationId ?? accepted?.reservation_id);
|
|
318
|
+
if (reservationId) {
|
|
319
|
+
try {
|
|
320
|
+
await ctx.fetch(`/internal/reservations/${seg(reservationId)}/publish`, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
body: {},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (publishErr) {
|
|
326
|
+
return {
|
|
327
|
+
...accepted,
|
|
328
|
+
publishWarning: `Bid accepted successfully, but automatic reservation publish failed: ${publishErr instanceof Error ? publishErr.message : String(publishErr)}. Use admin_reservation_publish to retry.`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return accepted;
|
|
185
333
|
},
|
|
186
334
|
});
|
|
187
335
|
export const bidRejectInput = z.object({
|
|
@@ -203,10 +351,44 @@ export const bidRejectCapability = defineStaffMcpCapability({
|
|
|
203
351
|
return ctx.fetch(`/internal/bids/${seg(parsed.bidId)}/reject`, { method: "POST" });
|
|
204
352
|
},
|
|
205
353
|
});
|
|
354
|
+
export const bidShowInput = z.object({
|
|
355
|
+
bidId: z.string().optional(),
|
|
356
|
+
bid_id: z.string().optional(),
|
|
357
|
+
});
|
|
358
|
+
export const bidShowCapability = defineStaffMcpCapability({
|
|
359
|
+
id: "bid.show",
|
|
360
|
+
domain: "bids",
|
|
361
|
+
roles: ["reviewer", "admin"],
|
|
362
|
+
description: "Internal staff only: fetch a single buyer bid by ID.",
|
|
363
|
+
mutation: "read",
|
|
364
|
+
http: { method: "GET", path: "/internal/bids/{bidId}" },
|
|
365
|
+
input: bidShowInput,
|
|
366
|
+
execute: async (ctx, input) => {
|
|
367
|
+
const parsed = bidShowInput.parse(input);
|
|
368
|
+
const bidId = parsed.bidId || parsed.bid_id;
|
|
369
|
+
if (!bidId) {
|
|
370
|
+
throw new Error("bidId is required");
|
|
371
|
+
}
|
|
372
|
+
return ctx.fetch(`/internal/bids/${seg(bidId)}`, { method: "GET" });
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
/** Commerce parses listing windows as RFC3339 timestamps, not bare dates. */
|
|
376
|
+
function rfc3339(value) {
|
|
377
|
+
const trimmed = value.trim();
|
|
378
|
+
if (!trimmed.includes("T"))
|
|
379
|
+
return `${trimmed}T00:00:00Z`;
|
|
380
|
+
return trimmed.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(trimmed) ? trimmed : `${trimmed}Z`;
|
|
381
|
+
}
|
|
206
382
|
export const inventoryUpdateInput = z.object({
|
|
207
383
|
inventoryId: z.string(),
|
|
208
|
-
|
|
209
|
-
|
|
384
|
+
version: z
|
|
385
|
+
.number()
|
|
386
|
+
.int()
|
|
387
|
+
.positive()
|
|
388
|
+
.optional()
|
|
389
|
+
.describe("Optimistic-concurrency version. Read from the listing when omitted."),
|
|
390
|
+
operatorId: z.string().min(1).optional(),
|
|
391
|
+
facilityId: z.string().min(1).optional(),
|
|
210
392
|
gpuType: z.string().min(1).optional(),
|
|
211
393
|
nodeCount: z
|
|
212
394
|
.number()
|
|
@@ -215,16 +397,15 @@ export const inventoryUpdateInput = z.object({
|
|
|
215
397
|
.optional()
|
|
216
398
|
.describe("Total nodes (listing capacity), not total GPUs."),
|
|
217
399
|
gpusPerNode: z.number().int().min(1).max(32).optional(),
|
|
218
|
-
availableFrom: z.string().
|
|
219
|
-
availableTo: z.string().
|
|
220
|
-
availableStartAt: z.string().
|
|
221
|
-
availableEndAt: z.string().
|
|
400
|
+
availableFrom: z.string().optional(),
|
|
401
|
+
availableTo: z.string().optional(),
|
|
402
|
+
availableStartAt: z.string().optional(),
|
|
403
|
+
availableEndAt: z.string().optional(),
|
|
222
404
|
cpu: z.string().nullable().optional(),
|
|
223
405
|
ram: z.string().nullable().optional(),
|
|
224
406
|
fabricType: z.string().nullable().optional(),
|
|
225
407
|
storage: z.string().nullable().optional(),
|
|
226
408
|
internet: z.string().nullable().optional(),
|
|
227
|
-
networkHardware: z.string().nullable().optional(),
|
|
228
409
|
buyNowPricePerGpuHour: z.number().positive().nullable().optional(),
|
|
229
410
|
confirm: z.boolean().optional(),
|
|
230
411
|
});
|
|
@@ -233,30 +414,40 @@ export const inventoryUpdateCapability = defineStaffMcpCapability({
|
|
|
233
414
|
domain: "listings",
|
|
234
415
|
roles: ["admin"],
|
|
235
416
|
description: "Internal staff only: patch editable fields on a forward inventory listing. Requires confirm: true. " +
|
|
236
|
-
"At least one editable field is required. buyNowPricePerGpuHour may be null to clear the price."
|
|
417
|
+
"At least one editable field is required. buyNowPricePerGpuHour may be null to clear the price. " +
|
|
418
|
+
"operatorId/facilityId take UUIDs. Site nickname and network hardware are not patchable on a listing.",
|
|
237
419
|
mutation: "preview-confirm",
|
|
238
420
|
http: { method: "PATCH", path: "/inventory/{inventoryId}" },
|
|
239
421
|
input: inventoryUpdateInput,
|
|
240
422
|
execute: async (ctx, input) => {
|
|
241
423
|
const parsed = inventoryUpdateInput.parse(input);
|
|
242
424
|
const body = mapBody(parsed, {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
gpuType: "
|
|
246
|
-
nodeCount: "
|
|
247
|
-
gpusPerNode: "
|
|
248
|
-
availableFrom: "available_from",
|
|
249
|
-
availableTo: "available_to",
|
|
250
|
-
availableStartAt: "available_start_at",
|
|
251
|
-
availableEndAt: "available_end_at",
|
|
425
|
+
operatorId: "operatorId",
|
|
426
|
+
facilityId: "facilityId",
|
|
427
|
+
gpuType: "gpuType",
|
|
428
|
+
nodeCount: "capacityNodes",
|
|
429
|
+
gpusPerNode: "gpusPerNode",
|
|
252
430
|
cpu: "cpu",
|
|
253
431
|
ram: "ram",
|
|
254
|
-
fabricType: "
|
|
432
|
+
fabricType: "fabricType",
|
|
255
433
|
storage: "storage",
|
|
256
434
|
internet: "internet",
|
|
257
|
-
networkHardware: "network_hardware",
|
|
258
|
-
buyNowPricePerGpuHour: "buy_now_price_per_gpu_hour",
|
|
259
435
|
});
|
|
436
|
+
const startAt = parsed.availableStartAt ?? parsed.availableFrom;
|
|
437
|
+
if (startAt !== undefined)
|
|
438
|
+
body.startAt = rfc3339(startAt);
|
|
439
|
+
const endAt = parsed.availableEndAt ?? parsed.availableTo;
|
|
440
|
+
if (endAt !== undefined)
|
|
441
|
+
body.endAt = rfc3339(endAt);
|
|
442
|
+
// The rate is a decimal string on the wire; null clears the buy-now price.
|
|
443
|
+
if (parsed.buyNowPricePerGpuHour !== undefined) {
|
|
444
|
+
body.buyNowRatePerGpuHour =
|
|
445
|
+
parsed.buyNowPricePerGpuHour === null ? null : String(parsed.buyNowPricePerGpuHour);
|
|
446
|
+
}
|
|
447
|
+
// commerce's patchListingRequest.Validate refuses version <= 0, and the
|
|
448
|
+
// schema never carried one, so every update 400'd with "missing version".
|
|
449
|
+
// Resolve it the way inventory.delist does.
|
|
450
|
+
body.version = await resolveVersion(ctx, `/inventory/${seg(parsed.inventoryId)}`, parsed.version);
|
|
260
451
|
if (!ctx.confirmed)
|
|
261
452
|
return preview("inventory.update", body);
|
|
262
453
|
return ctx.fetch(`/inventory/${seg(parsed.inventoryId)}`, { method: "PATCH", body });
|
|
@@ -273,18 +464,226 @@ export const tenantCreditProfileShowCapability = defineStaffMcpCapability({
|
|
|
273
464
|
input: tenantCreditProfileShowInput,
|
|
274
465
|
execute: async (ctx, input) => {
|
|
275
466
|
const parsed = tenantCreditProfileShowInput.parse(input);
|
|
276
|
-
|
|
467
|
+
try {
|
|
468
|
+
return await ctx.fetch(`/internal/organizations/${seg(parsed.tenantId)}`, { method: "GET" });
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
// Commerce holds a projection, not the roster: an organization only
|
|
472
|
+
// appears there once authorization has pushed it across, so this read
|
|
473
|
+
// returned ORGANIZATION_NOT_FOUND for live tenants that had never
|
|
474
|
+
// transacted. Authorization owns the record, so fall back to it and say
|
|
475
|
+
// plainly that no commerce credit profile exists yet.
|
|
476
|
+
if (statusOf(error) !== 404)
|
|
477
|
+
throw error;
|
|
478
|
+
const organization = await ctx.fetch(`/v1/organizations/${seg(parsed.tenantId)}`, {
|
|
479
|
+
method: "GET",
|
|
480
|
+
});
|
|
481
|
+
return { organization, creditProfile: null, creditProfileSource: "authorization" };
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
export const reservationDraftCreateInput = z.object({
|
|
486
|
+
organizationId: z.string(),
|
|
487
|
+
nodeCount: z.number().int().positive(),
|
|
488
|
+
startAt: z.string(),
|
|
489
|
+
endAt: z.string(),
|
|
490
|
+
billingMode: z.string().default("reserved"),
|
|
491
|
+
pricePerGpuHr: z.string(),
|
|
492
|
+
listingId: z.string().optional(),
|
|
493
|
+
accessMode: z.string().optional(),
|
|
494
|
+
notes: z.string().optional(),
|
|
495
|
+
confirm: z.boolean().optional(),
|
|
496
|
+
});
|
|
497
|
+
export const reservationDraftCreateCapability = defineStaffMcpCapability({
|
|
498
|
+
id: "reservation.draft-create",
|
|
499
|
+
domain: "reservations",
|
|
500
|
+
roles: ["admin"],
|
|
501
|
+
description: "Internal staff only: create a draft reservation in Commerce. Requires confirm: true.",
|
|
502
|
+
mutation: "preview-confirm",
|
|
503
|
+
http: { method: "POST", path: "/internal/reservations" },
|
|
504
|
+
input: reservationDraftCreateInput,
|
|
505
|
+
execute: async (ctx, input) => {
|
|
506
|
+
const parsed = reservationDraftCreateInput.parse(input);
|
|
507
|
+
const body = {
|
|
508
|
+
organizationId: parsed.organizationId,
|
|
509
|
+
nodeCount: parsed.nodeCount,
|
|
510
|
+
startAt: parsed.startAt,
|
|
511
|
+
endAt: parsed.endAt,
|
|
512
|
+
billingMode: parsed.billingMode,
|
|
513
|
+
pricePerGpuHr: parsed.pricePerGpuHr,
|
|
514
|
+
draft: true,
|
|
515
|
+
...mapBody(parsed, {
|
|
516
|
+
listingId: "listingId",
|
|
517
|
+
accessMode: "accessMode",
|
|
518
|
+
notes: "notes",
|
|
519
|
+
}),
|
|
520
|
+
};
|
|
521
|
+
if (!ctx.confirmed)
|
|
522
|
+
return preview("reservation.draft-create", body);
|
|
523
|
+
return ctx.fetch("/internal/reservations", { method: "POST", body });
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
export const reservationUpdateInput = z.object({
|
|
527
|
+
reservationId: z.string(),
|
|
528
|
+
startAt: z.string().optional(),
|
|
529
|
+
endAt: z.string().optional(),
|
|
530
|
+
nodeCount: z.number().int().positive().optional(),
|
|
531
|
+
notes: z.string().nullable().optional(),
|
|
532
|
+
expectedStatus: z.string().optional(),
|
|
533
|
+
confirm: z.boolean().optional(),
|
|
534
|
+
});
|
|
535
|
+
export const reservationUpdateCapability = defineStaffMcpCapability({
|
|
536
|
+
id: "reservation.update",
|
|
537
|
+
domain: "reservations",
|
|
538
|
+
roles: ["admin"],
|
|
539
|
+
description: "Internal staff only: update an existing reservation's dates, node count, notes, or draft status. Requires confirm: true.",
|
|
540
|
+
mutation: "preview-confirm",
|
|
541
|
+
http: { method: "PATCH", path: "/internal/reservations/{reservationId}" },
|
|
542
|
+
input: reservationUpdateInput,
|
|
543
|
+
execute: async (ctx, input) => {
|
|
544
|
+
const parsed = reservationUpdateInput.parse(input);
|
|
545
|
+
const body = mapBody(parsed, {
|
|
546
|
+
startAt: "startAt",
|
|
547
|
+
endAt: "endAt",
|
|
548
|
+
nodeCount: "nodeCount",
|
|
549
|
+
notes: "notes",
|
|
550
|
+
expectedStatus: "expectedStatus",
|
|
551
|
+
});
|
|
552
|
+
if (!ctx.confirmed) {
|
|
553
|
+
return preview("reservation.update", { reservationId: parsed.reservationId, ...body });
|
|
554
|
+
}
|
|
555
|
+
return ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}`, {
|
|
556
|
+
method: "PATCH",
|
|
557
|
+
body,
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
export const reservationActivateInput = z.object({
|
|
562
|
+
reservationId: z.string(),
|
|
563
|
+
version: z.number().int().optional(),
|
|
564
|
+
confirm: z.boolean().optional(),
|
|
565
|
+
});
|
|
566
|
+
export const reservationActivateCapability = defineStaffMcpCapability({
|
|
567
|
+
id: "reservation.activate",
|
|
568
|
+
domain: "reservations",
|
|
569
|
+
roles: ["admin"],
|
|
570
|
+
description: "Internal staff only: activate a confirmed reservation. Requires confirm: true.",
|
|
571
|
+
mutation: "preview-confirm",
|
|
572
|
+
http: { method: "POST", path: "/internal/reservations/{reservationId}/activate" },
|
|
573
|
+
input: reservationActivateInput,
|
|
574
|
+
execute: async (ctx, input) => {
|
|
575
|
+
const parsed = reservationActivateInput.parse(input);
|
|
576
|
+
const body = {
|
|
577
|
+
version: await resolveVersion(ctx, `/internal/reservations/${seg(parsed.reservationId)}`, parsed.version),
|
|
578
|
+
};
|
|
579
|
+
if (!ctx.confirmed)
|
|
580
|
+
return preview("reservation.activate", { reservationId: parsed.reservationId, ...body });
|
|
581
|
+
return ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}/activate`, {
|
|
582
|
+
method: "POST",
|
|
583
|
+
body,
|
|
584
|
+
});
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
export const reservationCompleteInput = z.object({
|
|
588
|
+
reservationId: z.string(),
|
|
589
|
+
version: z.number().int().optional(),
|
|
590
|
+
confirm: z.boolean().optional(),
|
|
591
|
+
});
|
|
592
|
+
export const reservationCompleteCapability = defineStaffMcpCapability({
|
|
593
|
+
id: "reservation.complete",
|
|
594
|
+
domain: "reservations",
|
|
595
|
+
roles: ["admin"],
|
|
596
|
+
description: "Internal staff only: complete an active reservation. Requires confirm: true.",
|
|
597
|
+
mutation: "preview-confirm",
|
|
598
|
+
http: { method: "POST", path: "/internal/reservations/{reservationId}/complete" },
|
|
599
|
+
input: reservationCompleteInput,
|
|
600
|
+
execute: async (ctx, input) => {
|
|
601
|
+
const parsed = reservationCompleteInput.parse(input);
|
|
602
|
+
const body = {
|
|
603
|
+
version: await resolveVersion(ctx, `/internal/reservations/${seg(parsed.reservationId)}`, parsed.version),
|
|
604
|
+
};
|
|
605
|
+
if (!ctx.confirmed)
|
|
606
|
+
return preview("reservation.complete", { reservationId: parsed.reservationId, ...body });
|
|
607
|
+
return ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}/complete`, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
body,
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
export const inventoryDelistInput = z.object({
|
|
614
|
+
inventoryId: z.string(),
|
|
615
|
+
version: z.number().int().positive().optional(),
|
|
616
|
+
confirm: z.boolean().optional(),
|
|
617
|
+
});
|
|
618
|
+
export const inventoryDelistCapability = defineStaffMcpCapability({
|
|
619
|
+
id: "inventory.delist",
|
|
620
|
+
domain: "listings",
|
|
621
|
+
roles: ["admin"],
|
|
622
|
+
description: "Internal staff only: delist an inventory listing. Sets its state to delisted. Requires confirm: true.",
|
|
623
|
+
mutation: "preview-confirm",
|
|
624
|
+
http: { method: "PATCH", path: "/inventory/{inventoryId}" },
|
|
625
|
+
input: inventoryDelistInput,
|
|
626
|
+
execute: async (ctx, input) => {
|
|
627
|
+
const parsed = inventoryDelistInput.parse(input);
|
|
628
|
+
const body = {
|
|
629
|
+
state: "delisted",
|
|
630
|
+
version: await resolveVersion(ctx, `/inventory/${seg(parsed.inventoryId)}`, parsed.version),
|
|
631
|
+
};
|
|
632
|
+
if (!ctx.confirmed)
|
|
633
|
+
return preview("inventory.delist", { inventoryId: parsed.inventoryId, ...body });
|
|
634
|
+
return ctx.fetch(`/inventory/${seg(parsed.inventoryId)}`, {
|
|
635
|
+
method: "PATCH",
|
|
636
|
+
body,
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
export const billingInvoicesListInput = z.object({
|
|
641
|
+
status: z
|
|
642
|
+
.enum(["due", "open", "paid", "draft", "void", "uncollectible"])
|
|
643
|
+
.default("due")
|
|
644
|
+
.optional(),
|
|
645
|
+
cursor: z.string().optional(),
|
|
646
|
+
limit: z.number().int().positive().max(100).optional(),
|
|
647
|
+
});
|
|
648
|
+
export const billingInvoicesListCapability = defineStaffMcpCapability({
|
|
649
|
+
id: "billing.invoices-list",
|
|
650
|
+
domain: "billing",
|
|
651
|
+
roles: ["reviewer", "admin"],
|
|
652
|
+
description: "Internal staff only: list commerce invoices with an optional status filter (defaults to due).",
|
|
653
|
+
mutation: "read",
|
|
654
|
+
http: { method: "GET", path: "/internal/invoices" },
|
|
655
|
+
input: billingInvoicesListInput,
|
|
656
|
+
execute: async (ctx, input) => {
|
|
657
|
+
const parsed = billingInvoicesListInput.parse(input ?? {});
|
|
658
|
+
const parts = [];
|
|
659
|
+
if (parsed.status)
|
|
660
|
+
parts.push(`status=${encodeURIComponent(parsed.status)}`);
|
|
661
|
+
if (parsed.cursor)
|
|
662
|
+
parts.push(`cursor=${encodeURIComponent(parsed.cursor)}`);
|
|
663
|
+
if (parsed.limit)
|
|
664
|
+
parts.push(`limit=${encodeURIComponent(String(parsed.limit))}`);
|
|
665
|
+
const qs = parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
666
|
+
return ctx.fetch(`/internal/invoices${qs}`, { method: "GET" });
|
|
277
667
|
},
|
|
278
668
|
});
|
|
279
669
|
export const staffCommerceCapabilities = [
|
|
670
|
+
reservationShowCapability,
|
|
671
|
+
reservationListCapability,
|
|
280
672
|
reservationCancelCapability,
|
|
281
673
|
reservationTransferCapability,
|
|
282
674
|
reservationMachinesListCapability,
|
|
283
675
|
reservationSshKeyAddCapability,
|
|
284
676
|
reservationSshKeyRemoveCapability,
|
|
285
677
|
reservationPublishCapability,
|
|
678
|
+
reservationDraftCreateCapability,
|
|
679
|
+
reservationUpdateCapability,
|
|
680
|
+
reservationActivateCapability,
|
|
681
|
+
reservationCompleteCapability,
|
|
682
|
+
bidShowCapability,
|
|
286
683
|
bidAcceptCapability,
|
|
287
684
|
bidRejectCapability,
|
|
288
685
|
inventoryUpdateCapability,
|
|
686
|
+
inventoryDelistCapability,
|
|
289
687
|
tenantCreditProfileShowCapability,
|
|
688
|
+
billingInvoicesListCapability,
|
|
290
689
|
];
|