@ornncompute/cli 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/package.json +1 -1
- package/vendor/capabilities/capabilities/clusters.js +3 -3
- package/vendor/capabilities/capabilities/fleet.js +4 -4
- package/vendor/capabilities/capabilities/listings.js +2 -2
- package/vendor/capabilities/capabilities/staff-access.d.ts +27 -0
- package/vendor/capabilities/capabilities/staff-access.js +192 -0
- package/vendor/capabilities/capabilities/staff-commerce.d.ts +83 -0
- package/vendor/capabilities/capabilities/staff-commerce.js +290 -0
- package/vendor/capabilities/capabilities/staff-fleet.d.ts +141 -0
- package/vendor/capabilities/capabilities/staff-fleet.js +465 -0
- package/vendor/capabilities/capabilities/staff-shared.d.ts +18 -0
- package/vendor/capabilities/capabilities/staff-shared.js +34 -0
- package/vendor/capabilities/capabilities/tokens.js +2 -2
- package/vendor/capabilities/capability.d.ts +32 -0
- package/vendor/capabilities/capability.js +26 -0
- package/vendor/capabilities/catalog.js +6 -0
- package/vendor/capabilities/index.d.ts +4 -1
- package/vendor/capabilities/index.js +4 -1
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineStaffMcpCapability, preview } from "../capability.js";
|
|
3
|
+
import { mapBody, seg } from "./staff-shared.js";
|
|
4
|
+
// Staff fleet verbs: operators, facilities, nodes, node jobs, and enrollment
|
|
5
|
+
// token revocation. MCP-only — see `defineStaffMcpCapability`.
|
|
6
|
+
const slugSchema = z
|
|
7
|
+
.string()
|
|
8
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, "slug must be lowercase alphanumeric with dashes")
|
|
9
|
+
.max(80);
|
|
10
|
+
const operatorModeSchema = z.enum(["managed", "federated"]);
|
|
11
|
+
const operatorStatusSchema = z.enum(["active", "unavailable", "revoked"]);
|
|
12
|
+
export const enrollmentTokenRevokeInput = z.object({
|
|
13
|
+
tokenId: z.string(),
|
|
14
|
+
confirm: z.boolean().optional(),
|
|
15
|
+
});
|
|
16
|
+
export const enrollmentTokenRevokeCapability = defineStaffMcpCapability({
|
|
17
|
+
id: "enrollment.token.revoke",
|
|
18
|
+
domain: "fleet",
|
|
19
|
+
roles: ["admin"],
|
|
20
|
+
description: "Internal staff only: revoke an enrollment token.",
|
|
21
|
+
mutation: "preview-confirm",
|
|
22
|
+
http: { method: "POST", path: "/enrollment/tokens/{tokenId}/revoke" },
|
|
23
|
+
input: enrollmentTokenRevokeInput,
|
|
24
|
+
execute: async (ctx, input) => {
|
|
25
|
+
const parsed = enrollmentTokenRevokeInput.parse(input);
|
|
26
|
+
if (!ctx.confirmed)
|
|
27
|
+
return preview("enrollment.token.revoke", { tokenId: parsed.tokenId });
|
|
28
|
+
return ctx.fetch(`/enrollment/tokens/${seg(parsed.tokenId)}/revoke`, { method: "POST" });
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
export const operatorCreateInput = z.object({
|
|
32
|
+
slug: slugSchema,
|
|
33
|
+
displayName: z.string().min(1).max(160),
|
|
34
|
+
mode: operatorModeSchema.optional(),
|
|
35
|
+
status: operatorStatusSchema.optional(),
|
|
36
|
+
confirm: z.boolean().optional(),
|
|
37
|
+
});
|
|
38
|
+
export const operatorCreateCapability = defineStaffMcpCapability({
|
|
39
|
+
id: "operator.create",
|
|
40
|
+
domain: "fleet",
|
|
41
|
+
roles: ["admin"],
|
|
42
|
+
description: "Internal staff only: create an operator (managed or federated). Requires confirm: true. Does not touch any hardware.",
|
|
43
|
+
mutation: "preview-confirm",
|
|
44
|
+
http: { method: "POST", path: "/provisioning/operators" },
|
|
45
|
+
input: operatorCreateInput,
|
|
46
|
+
execute: async (ctx, input) => {
|
|
47
|
+
const parsed = operatorCreateInput.parse(input);
|
|
48
|
+
const body = {
|
|
49
|
+
slug: parsed.slug,
|
|
50
|
+
display_name: parsed.displayName,
|
|
51
|
+
...mapBody(parsed, { mode: "mode", status: "status" }),
|
|
52
|
+
};
|
|
53
|
+
if (!ctx.confirmed)
|
|
54
|
+
return preview("operator.create", body);
|
|
55
|
+
return ctx.fetch("/provisioning/operators", { method: "POST", body });
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
export const operatorUpdateInput = z.object({
|
|
59
|
+
operatorId: z.string(),
|
|
60
|
+
displayName: z.string().min(1).max(160).optional(),
|
|
61
|
+
slug: slugSchema.optional(),
|
|
62
|
+
mode: operatorModeSchema.optional(),
|
|
63
|
+
status: operatorStatusSchema.optional(),
|
|
64
|
+
confirm: z.boolean().optional(),
|
|
65
|
+
});
|
|
66
|
+
export const operatorUpdateCapability = defineStaffMcpCapability({
|
|
67
|
+
id: "operator.update",
|
|
68
|
+
domain: "fleet",
|
|
69
|
+
roles: ["admin"],
|
|
70
|
+
description: "Internal staff only: update an operator's display name, slug, mode, or status. Requires confirm: true.",
|
|
71
|
+
mutation: "preview-confirm",
|
|
72
|
+
http: { method: "PATCH", path: "/provisioning/operators/{operatorId}" },
|
|
73
|
+
input: operatorUpdateInput,
|
|
74
|
+
execute: async (ctx, input) => {
|
|
75
|
+
const parsed = operatorUpdateInput.parse(input);
|
|
76
|
+
const body = mapBody(parsed, {
|
|
77
|
+
displayName: "display_name",
|
|
78
|
+
slug: "slug",
|
|
79
|
+
mode: "mode",
|
|
80
|
+
status: "status",
|
|
81
|
+
});
|
|
82
|
+
if (!ctx.confirmed)
|
|
83
|
+
return preview("operator.update", body);
|
|
84
|
+
return ctx.fetch(`/provisioning/operators/${seg(parsed.operatorId)}`, {
|
|
85
|
+
method: "PATCH",
|
|
86
|
+
body,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
export const operatorDeleteInput = z.object({
|
|
91
|
+
operatorId: z.string(),
|
|
92
|
+
confirm: z.boolean().optional(),
|
|
93
|
+
});
|
|
94
|
+
export const operatorDeleteCapability = defineStaffMcpCapability({
|
|
95
|
+
id: "operator.delete",
|
|
96
|
+
domain: "fleet",
|
|
97
|
+
roles: ["admin"],
|
|
98
|
+
description: "Internal staff only: delete an operator. Requires confirm: true.",
|
|
99
|
+
mutation: "preview-confirm",
|
|
100
|
+
http: { method: "DELETE", path: "/provisioning/operators/{operatorId}" },
|
|
101
|
+
input: operatorDeleteInput,
|
|
102
|
+
execute: async (ctx, input) => {
|
|
103
|
+
const parsed = operatorDeleteInput.parse(input);
|
|
104
|
+
if (!ctx.confirmed)
|
|
105
|
+
return preview("operator.delete", { operatorId: parsed.operatorId });
|
|
106
|
+
return ctx.fetch(`/provisioning/operators/${seg(parsed.operatorId)}`, { method: "DELETE" });
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
export const facilityCreateInput = z.object({
|
|
110
|
+
operatorId: z.string(),
|
|
111
|
+
slug: slugSchema,
|
|
112
|
+
displayName: z.string().min(1).max(160),
|
|
113
|
+
region: z.string().min(1).max(120),
|
|
114
|
+
zone: z.string().max(120).optional(),
|
|
115
|
+
confirm: z.boolean().optional(),
|
|
116
|
+
});
|
|
117
|
+
export const facilityCreateCapability = defineStaffMcpCapability({
|
|
118
|
+
id: "facility.create",
|
|
119
|
+
domain: "fleet",
|
|
120
|
+
roles: ["admin"],
|
|
121
|
+
description: "Internal staff only: create a facility under an operator. Requires confirm: true. Does not touch any hardware.",
|
|
122
|
+
mutation: "preview-confirm",
|
|
123
|
+
http: { method: "POST", path: "/provisioning/facilities" },
|
|
124
|
+
input: facilityCreateInput,
|
|
125
|
+
execute: async (ctx, input) => {
|
|
126
|
+
const parsed = facilityCreateInput.parse(input);
|
|
127
|
+
const body = {
|
|
128
|
+
operator_id: parsed.operatorId,
|
|
129
|
+
slug: parsed.slug,
|
|
130
|
+
display_name: parsed.displayName,
|
|
131
|
+
region: parsed.region,
|
|
132
|
+
zone: parsed.zone ?? null,
|
|
133
|
+
};
|
|
134
|
+
if (!ctx.confirmed)
|
|
135
|
+
return preview("facility.create", body);
|
|
136
|
+
return ctx.fetch("/provisioning/facilities", { method: "POST", body });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
export const facilityUpdateInput = z.object({
|
|
140
|
+
facilityId: z.string(),
|
|
141
|
+
displayName: z.string().min(1).max(160).optional(),
|
|
142
|
+
region: z.string().min(1).max(120).optional(),
|
|
143
|
+
zone: z.string().max(120).optional(),
|
|
144
|
+
confirm: z.boolean().optional(),
|
|
145
|
+
});
|
|
146
|
+
export const facilityUpdateCapability = defineStaffMcpCapability({
|
|
147
|
+
id: "facility.update",
|
|
148
|
+
domain: "fleet",
|
|
149
|
+
roles: ["admin"],
|
|
150
|
+
description: "Internal staff only: update a facility's display name, region, or zone. Pass zone as an empty string to clear it. Requires confirm: true.",
|
|
151
|
+
mutation: "preview-confirm",
|
|
152
|
+
http: { method: "PATCH", path: "/provisioning/facilities/{facilityId}" },
|
|
153
|
+
input: facilityUpdateInput,
|
|
154
|
+
execute: async (ctx, input) => {
|
|
155
|
+
const parsed = facilityUpdateInput.parse(input);
|
|
156
|
+
// "" clears zone; omitting it leaves zone unchanged (mirrors the dashboard PATCH).
|
|
157
|
+
const body = mapBody(parsed, {
|
|
158
|
+
displayName: "display_name",
|
|
159
|
+
region: "region",
|
|
160
|
+
zone: "zone",
|
|
161
|
+
});
|
|
162
|
+
if (!ctx.confirmed)
|
|
163
|
+
return preview("facility.update", body);
|
|
164
|
+
return ctx.fetch(`/provisioning/facilities/${seg(parsed.facilityId)}`, {
|
|
165
|
+
method: "PATCH",
|
|
166
|
+
body,
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
export const nodeShowInput = z.object({ nodeId: z.string() });
|
|
171
|
+
export const nodeShowCapability = defineStaffMcpCapability({
|
|
172
|
+
id: "node.show",
|
|
173
|
+
domain: "fleet",
|
|
174
|
+
roles: ["reviewer", "admin"],
|
|
175
|
+
description: "Internal staff only: show details for a single GPU node.",
|
|
176
|
+
mutation: "read",
|
|
177
|
+
http: { method: "GET", path: "/internal/nodes/{nodeId}" },
|
|
178
|
+
input: nodeShowInput,
|
|
179
|
+
execute: async (ctx, input) => {
|
|
180
|
+
const parsed = nodeShowInput.parse(input);
|
|
181
|
+
return ctx.fetch(`/internal/nodes/${seg(parsed.nodeId)}`, { method: "GET" });
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
export const nodeDetailsUpdateInput = z.object({
|
|
185
|
+
nodeId: z.string(),
|
|
186
|
+
hostname: z.string().optional(),
|
|
187
|
+
publicLocation: z.string().optional(),
|
|
188
|
+
facilityId: z.string().optional(),
|
|
189
|
+
gpuType: z.string().optional(),
|
|
190
|
+
gpuCount: z.number().int().positive().optional(),
|
|
191
|
+
ornnMode: z.string().optional(),
|
|
192
|
+
cpu: z.string().optional(),
|
|
193
|
+
ram: z.string().optional(),
|
|
194
|
+
storage: z.string().optional(),
|
|
195
|
+
fabricType: z.string().optional(),
|
|
196
|
+
internet: z.string().optional(),
|
|
197
|
+
networkHardware: z.string().optional(),
|
|
198
|
+
buyNowPricePerGpuHour: z.number().nullable().optional(),
|
|
199
|
+
confirm: z.boolean().optional(),
|
|
200
|
+
});
|
|
201
|
+
export const nodeDetailsUpdateCapability = defineStaffMcpCapability({
|
|
202
|
+
id: "node.details-update",
|
|
203
|
+
domain: "fleet",
|
|
204
|
+
roles: ["admin"],
|
|
205
|
+
description: "Internal staff only: update core node fields and reserve-listing / spec-sheet values (hostname, location, GPUs, CPU/RAM/storage, buy-now price). Requires confirm: true.",
|
|
206
|
+
mutation: "preview-confirm",
|
|
207
|
+
http: { method: "PATCH", path: "/internal/nodes/{nodeId}/details" },
|
|
208
|
+
input: nodeDetailsUpdateInput,
|
|
209
|
+
execute: async (ctx, input) => {
|
|
210
|
+
const parsed = nodeDetailsUpdateInput.parse(input);
|
|
211
|
+
// Backend confirm is required for risky edits on reserved nodes; the caller's
|
|
212
|
+
// confirm already gated execution, so always send confirm: true on the wire.
|
|
213
|
+
const body = {
|
|
214
|
+
confirm: true,
|
|
215
|
+
...mapBody(parsed, {
|
|
216
|
+
hostname: "hostname",
|
|
217
|
+
publicLocation: "public_location",
|
|
218
|
+
facilityId: "facility_id",
|
|
219
|
+
gpuType: "gpu_type",
|
|
220
|
+
gpuCount: "gpu_count",
|
|
221
|
+
ornnMode: "ornn_mode",
|
|
222
|
+
cpu: "cpu",
|
|
223
|
+
ram: "ram",
|
|
224
|
+
storage: "storage",
|
|
225
|
+
fabricType: "fabric_type",
|
|
226
|
+
internet: "internet",
|
|
227
|
+
networkHardware: "network_hardware",
|
|
228
|
+
buyNowPricePerGpuHour: "buy_now_price_per_gpu_hour",
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
if (!ctx.confirmed)
|
|
232
|
+
return preview("node.details-update", body);
|
|
233
|
+
return ctx.fetch(`/internal/nodes/${seg(parsed.nodeId)}/details`, { method: "PATCH", body });
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
export const nodeReserveListingInput = z.object({
|
|
237
|
+
nodeId: z.string(),
|
|
238
|
+
availableFrom: z.string().optional(),
|
|
239
|
+
availableTo: z.string().nullable().optional(),
|
|
240
|
+
confirm: z.boolean().optional(),
|
|
241
|
+
});
|
|
242
|
+
export const nodeReserveListingCapability = defineStaffMcpCapability({
|
|
243
|
+
id: "node.reserve-listing",
|
|
244
|
+
domain: "fleet",
|
|
245
|
+
roles: ["admin"],
|
|
246
|
+
description: "Internal staff only: list a GPU node on the reserve marketplace inventory. Requires confirm: true.",
|
|
247
|
+
mutation: "preview-confirm",
|
|
248
|
+
http: { method: "POST", path: "/inventory/{nodeId}" },
|
|
249
|
+
input: nodeReserveListingInput,
|
|
250
|
+
execute: async (ctx, input) => {
|
|
251
|
+
const parsed = nodeReserveListingInput.parse(input);
|
|
252
|
+
const body = mapBody(parsed, {
|
|
253
|
+
availableFrom: "available_from",
|
|
254
|
+
availableTo: "available_to",
|
|
255
|
+
});
|
|
256
|
+
if (!ctx.confirmed)
|
|
257
|
+
return preview("node.reserve-listing", body);
|
|
258
|
+
return ctx.fetch(`/inventory/${seg(parsed.nodeId)}`, { method: "POST", body });
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
export const nodeTestTriggerInput = z.object({
|
|
262
|
+
nodeId: z.string(),
|
|
263
|
+
burnTimeMinutes: z.number().optional(),
|
|
264
|
+
constraints: z.record(z.string(), z.unknown()).optional(),
|
|
265
|
+
source: z.record(z.string(), z.unknown()).optional(),
|
|
266
|
+
confirm: z.boolean().optional(),
|
|
267
|
+
});
|
|
268
|
+
export const nodeTestTriggerCapability = defineStaffMcpCapability({
|
|
269
|
+
id: "node.test-trigger",
|
|
270
|
+
domain: "fleet",
|
|
271
|
+
roles: ["admin"],
|
|
272
|
+
description: "Internal staff only: start a node job (host checks + GPU suites; optional burn-in minutes). Requires confirm: true.",
|
|
273
|
+
mutation: "preview-confirm",
|
|
274
|
+
http: { method: "POST", path: "/jobs" },
|
|
275
|
+
input: nodeTestTriggerInput,
|
|
276
|
+
execute: async (ctx, input) => {
|
|
277
|
+
const parsed = nodeTestTriggerInput.parse(input);
|
|
278
|
+
let constraints = { ...(parsed.constraints ?? {}) };
|
|
279
|
+
if (parsed.burnTimeMinutes !== undefined) {
|
|
280
|
+
constraints = {
|
|
281
|
+
...constraints,
|
|
282
|
+
burn_time_minutes: parsed.burnTimeMinutes,
|
|
283
|
+
include_burn_samples: true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const body = {
|
|
287
|
+
node_id: parsed.nodeId,
|
|
288
|
+
constraints,
|
|
289
|
+
source: parsed.source ?? { mode: "no_spec" },
|
|
290
|
+
burn_time_minutes: parsed.burnTimeMinutes ?? 0,
|
|
291
|
+
};
|
|
292
|
+
if (!ctx.confirmed)
|
|
293
|
+
return preview("node.test-trigger", body);
|
|
294
|
+
return ctx.fetch("/jobs", { method: "POST", body });
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
export const nodeTestResultInput = z.object({
|
|
298
|
+
nodeId: z.string().optional(),
|
|
299
|
+
jobId: z.string(),
|
|
300
|
+
packetId: z.string().optional(),
|
|
301
|
+
});
|
|
302
|
+
export const nodeTestResultCapability = defineStaffMcpCapability({
|
|
303
|
+
id: "node.test-result",
|
|
304
|
+
domain: "fleet",
|
|
305
|
+
roles: ["reviewer", "admin"],
|
|
306
|
+
description: "Internal staff only: fetch a node job by id.",
|
|
307
|
+
mutation: "read",
|
|
308
|
+
http: { method: "GET", path: "/jobs/{jobId}" },
|
|
309
|
+
input: nodeTestResultInput,
|
|
310
|
+
execute: async (ctx, input) => {
|
|
311
|
+
const parsed = nodeTestResultInput.parse(input);
|
|
312
|
+
return ctx.fetch(`/jobs/${seg(parsed.jobId)}`, { method: "GET" });
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
export const nodeBenchmarkInput = z.object({ nodeId: z.string() });
|
|
316
|
+
export const nodeBenchmarkCapability = defineStaffMcpCapability({
|
|
317
|
+
id: "node.benchmark",
|
|
318
|
+
domain: "fleet",
|
|
319
|
+
roles: ["reviewer", "admin"],
|
|
320
|
+
description: "Internal staff only: show the current burn-in / benchmark summary for a GPU node.",
|
|
321
|
+
mutation: "read",
|
|
322
|
+
http: { method: "GET", path: "/jobs" },
|
|
323
|
+
input: nodeBenchmarkInput,
|
|
324
|
+
execute: async (ctx, input) => {
|
|
325
|
+
const parsed = nodeBenchmarkInput.parse(input);
|
|
326
|
+
return ctx.fetch(`/jobs?node_id=${seg(parsed.nodeId)}`, { method: "GET" });
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
export const nodeDeployInput = z.object({
|
|
330
|
+
nodeId: z.string(),
|
|
331
|
+
targetTenantId: z.string(),
|
|
332
|
+
targetAuthUserId: z.string().optional(),
|
|
333
|
+
networkMode: z.enum(["public", "private"]).optional(),
|
|
334
|
+
notes: z.string().max(2000).optional(),
|
|
335
|
+
confirm: z.boolean().optional(),
|
|
336
|
+
});
|
|
337
|
+
export const nodeDeployCapability = defineStaffMcpCapability({
|
|
338
|
+
id: "node.deploy",
|
|
339
|
+
domain: "fleet",
|
|
340
|
+
roles: ["admin"],
|
|
341
|
+
description: "Internal staff only: deploy a free GPU node to a target tenant (create reservation + queue handoff). Does not accept a force override. Requires confirm: true.",
|
|
342
|
+
mutation: "preview-confirm",
|
|
343
|
+
http: { method: "POST", path: "/internal/nodes/{nodeId}/deploy" },
|
|
344
|
+
input: nodeDeployInput,
|
|
345
|
+
execute: async (ctx, input) => {
|
|
346
|
+
const parsed = nodeDeployInput.parse(input);
|
|
347
|
+
const body = {
|
|
348
|
+
target_tenant_id: parsed.targetTenantId,
|
|
349
|
+
...mapBody(parsed, {
|
|
350
|
+
targetAuthUserId: "target_auth_user_id",
|
|
351
|
+
networkMode: "network_mode",
|
|
352
|
+
notes: "notes",
|
|
353
|
+
}),
|
|
354
|
+
};
|
|
355
|
+
if (!ctx.confirmed)
|
|
356
|
+
return preview("node.deploy", body);
|
|
357
|
+
return ctx.fetch(`/internal/nodes/${seg(parsed.nodeId)}/deploy`, { method: "POST", body });
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
export const nodeUpdateInput = z.object({
|
|
361
|
+
nodeId: z.string(),
|
|
362
|
+
targetVersion: z.string().optional(),
|
|
363
|
+
confirm: z.boolean().optional(),
|
|
364
|
+
});
|
|
365
|
+
export const nodeUpdateCapability = defineStaffMcpCapability({
|
|
366
|
+
id: "node.update",
|
|
367
|
+
domain: "fleet",
|
|
368
|
+
roles: ["admin"],
|
|
369
|
+
description: "Internal staff only: write the desired ornn-node version on the host row. Does not accept a force override. Requires confirm: true.",
|
|
370
|
+
mutation: "preview-confirm",
|
|
371
|
+
http: { method: "PATCH", path: "/internal/nodes/{nodeId}" },
|
|
372
|
+
input: nodeUpdateInput,
|
|
373
|
+
execute: async (ctx, input) => {
|
|
374
|
+
const parsed = nodeUpdateInput.parse(input);
|
|
375
|
+
const body = mapBody(parsed, { targetVersion: "desired_agent_version" });
|
|
376
|
+
if (!ctx.confirmed)
|
|
377
|
+
return preview("node.update", body);
|
|
378
|
+
return ctx.fetch(`/internal/nodes/${seg(parsed.nodeId)}`, { method: "PATCH", body });
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
const machineRebootInput = z.object({
|
|
382
|
+
instanceId: z.string(),
|
|
383
|
+
confirm: z.boolean().optional(),
|
|
384
|
+
});
|
|
385
|
+
export const nodeRebootInput = machineRebootInput;
|
|
386
|
+
export const nodeHardResetInput = machineRebootInput;
|
|
387
|
+
/**
|
|
388
|
+
* Reboot and hard-reset share one orchestrator endpoint and differ only by
|
|
389
|
+
* `mode`, but they stay separate verbs: "wipe the host" must never be one
|
|
390
|
+
* mistyped argument away from "restart the host".
|
|
391
|
+
*/
|
|
392
|
+
function rebootCapability(options) {
|
|
393
|
+
return defineStaffMcpCapability({
|
|
394
|
+
id: options.id,
|
|
395
|
+
domain: "fleet",
|
|
396
|
+
roles: ["admin"],
|
|
397
|
+
description: options.description,
|
|
398
|
+
mutation: "preview-confirm",
|
|
399
|
+
http: { method: "POST", path: "/internal/nodes/{instanceId}/reboot" },
|
|
400
|
+
input: machineRebootInput,
|
|
401
|
+
execute: async (ctx, input) => {
|
|
402
|
+
const parsed = machineRebootInput.parse(input);
|
|
403
|
+
const body = { mode: options.mode };
|
|
404
|
+
if (!ctx.confirmed)
|
|
405
|
+
return preview(options.id, { instanceId: parsed.instanceId, ...body });
|
|
406
|
+
return ctx.fetch(`/internal/nodes/${seg(parsed.instanceId)}/reboot`, {
|
|
407
|
+
method: "POST",
|
|
408
|
+
body,
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
export const nodeRebootCapability = rebootCapability({
|
|
414
|
+
id: "node.reboot",
|
|
415
|
+
mode: "reboot",
|
|
416
|
+
description: "Internal staff only: reboot a live machine (a node's running instance). Plain OS reboot -- keeps storage and SSH keys. Address the machine by its instance id (see ornn_admin_reservation_machines_list). Requires confirm: true.",
|
|
417
|
+
});
|
|
418
|
+
export const nodeHardResetCapability = rebootCapability({
|
|
419
|
+
id: "node.hard-reset",
|
|
420
|
+
mode: "hard_reset",
|
|
421
|
+
description: "Internal staff only: hard-reset a live machine (a node's running instance) -- clean the host (wipe tenant storage, users, keys, caches) then reboot. This does NOT restore SSH access; after the reset completes, run ornn_admin_ssh_key_push to re-push the reservation's organization SSH keys. The tenant KEEPS ownership. Address the machine by its instance id (see ornn_admin_reservation_machines_list). Requires confirm: true.",
|
|
422
|
+
});
|
|
423
|
+
export const nodeAdminKeyInput = z.object({
|
|
424
|
+
nodeId: z.string(),
|
|
425
|
+
generate: z.boolean().optional(),
|
|
426
|
+
confirm: z.boolean().optional(),
|
|
427
|
+
});
|
|
428
|
+
export const nodeAdminKeyCapability = defineStaffMcpCapability({
|
|
429
|
+
id: "node.admin-key",
|
|
430
|
+
domain: "fleet",
|
|
431
|
+
roles: ["admin"],
|
|
432
|
+
description: "Internal staff only: fetch the Ornn admin SSH keypair for a GPU node, including the decrypted PRIVATE key, so an operator can SSH into the host directly. Handle the private key as a secret. Set generate: true (requires confirm: true) to mint a new pair and install the public key on the live host.",
|
|
433
|
+
// Reading the stored pair is a GET; minting a new one is the same path as a POST.
|
|
434
|
+
mutation: "preview-confirm",
|
|
435
|
+
http: { method: "GET", path: "/enrollment/admin-ssh-key" },
|
|
436
|
+
input: nodeAdminKeyInput,
|
|
437
|
+
execute: async (ctx, input) => {
|
|
438
|
+
const parsed = nodeAdminKeyInput.parse(input);
|
|
439
|
+
if (parsed.generate && !ctx.confirmed) {
|
|
440
|
+
return preview("node.admin-key", { nodeId: parsed.nodeId, generate: true });
|
|
441
|
+
}
|
|
442
|
+
return ctx.fetch(`/enrollment/admin-ssh-key?node_id=${seg(parsed.nodeId)}`, {
|
|
443
|
+
method: parsed.generate ? "POST" : "GET",
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
export const staffFleetCapabilities = [
|
|
448
|
+
enrollmentTokenRevokeCapability,
|
|
449
|
+
operatorCreateCapability,
|
|
450
|
+
operatorUpdateCapability,
|
|
451
|
+
operatorDeleteCapability,
|
|
452
|
+
facilityCreateCapability,
|
|
453
|
+
facilityUpdateCapability,
|
|
454
|
+
nodeShowCapability,
|
|
455
|
+
nodeDetailsUpdateCapability,
|
|
456
|
+
nodeReserveListingCapability,
|
|
457
|
+
nodeTestTriggerCapability,
|
|
458
|
+
nodeTestResultCapability,
|
|
459
|
+
nodeBenchmarkCapability,
|
|
460
|
+
nodeDeployCapability,
|
|
461
|
+
nodeUpdateCapability,
|
|
462
|
+
nodeRebootCapability,
|
|
463
|
+
nodeHardResetCapability,
|
|
464
|
+
nodeAdminKeyCapability,
|
|
465
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Helpers shared by the staff MCP verbs in `staff-*.ts`. */
|
|
2
|
+
/**
|
|
3
|
+
* Build a request body from the fields that were actually supplied.
|
|
4
|
+
*
|
|
5
|
+
* Staff writes are almost all "PATCH the fields the caller named": `undefined`
|
|
6
|
+
* means leave alone, and `null` is a real value that clears the field. So the
|
|
7
|
+
* check is `!== undefined`, never truthiness.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mapBody(input: Record<string, unknown>, mapping: Record<string, string>): Record<string, unknown>;
|
|
10
|
+
/** Path segment escape. */
|
|
11
|
+
export declare function seg(value: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* A transport failure's HTTP status, when it carries one.
|
|
14
|
+
*
|
|
15
|
+
* Capabilities cannot import a surface's error class (`apps/mcp`'s
|
|
16
|
+
* `WebApiError`, say), so read the status structurally.
|
|
17
|
+
*/
|
|
18
|
+
export declare function statusOf(error: unknown): number | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Helpers shared by the staff MCP verbs in `staff-*.ts`. */
|
|
2
|
+
/**
|
|
3
|
+
* Build a request body from the fields that were actually supplied.
|
|
4
|
+
*
|
|
5
|
+
* Staff writes are almost all "PATCH the fields the caller named": `undefined`
|
|
6
|
+
* means leave alone, and `null` is a real value that clears the field. So the
|
|
7
|
+
* check is `!== undefined`, never truthiness.
|
|
8
|
+
*/
|
|
9
|
+
export function mapBody(input, mapping) {
|
|
10
|
+
const body = {};
|
|
11
|
+
for (const [field, wire] of Object.entries(mapping)) {
|
|
12
|
+
if (input[field] !== undefined)
|
|
13
|
+
body[wire] = input[field];
|
|
14
|
+
}
|
|
15
|
+
return body;
|
|
16
|
+
}
|
|
17
|
+
/** Path segment escape. */
|
|
18
|
+
export function seg(value) {
|
|
19
|
+
return encodeURIComponent(value);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A transport failure's HTTP status, when it carries one.
|
|
23
|
+
*
|
|
24
|
+
* Capabilities cannot import a surface's error class (`apps/mcp`'s
|
|
25
|
+
* `WebApiError`, say), so read the status structurally.
|
|
26
|
+
*/
|
|
27
|
+
export function statusOf(error) {
|
|
28
|
+
if (error && typeof error === "object" && "status" in error) {
|
|
29
|
+
const status = error.status;
|
|
30
|
+
if (typeof status === "number")
|
|
31
|
+
return status;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { defineCapability } from "../capability.js";
|
|
2
|
+
import { defineCapability, preview } from "../capability.js";
|
|
3
3
|
const optionalText = z.string().nullish();
|
|
4
4
|
const tokenCreateInput = z.object({
|
|
5
5
|
operator_id: z.string().min(1),
|
|
@@ -107,7 +107,7 @@ export const tokensCreateCapability = defineCapability({
|
|
|
107
107
|
remote: hosts.length > 0 && privateKey.length > 0,
|
|
108
108
|
};
|
|
109
109
|
if (!ctx.confirmed) {
|
|
110
|
-
return
|
|
110
|
+
return preview("tokens.create", { ...payload, private_key: privateKey ? "[redacted]" : "" });
|
|
111
111
|
}
|
|
112
112
|
const minted = (await ctx.fetch("/internal/enrollment-tokens", {
|
|
113
113
|
method: "POST",
|
|
@@ -19,6 +19,24 @@ export type CapabilityContext = {
|
|
|
19
19
|
/** Human (or MCP) confirmed a preview-confirm mutation. Execute never POSTs without this. */
|
|
20
20
|
confirmed?: boolean;
|
|
21
21
|
};
|
|
22
|
+
/** The un-executed result of a `preview-confirm` capability. */
|
|
23
|
+
export type PreviewResult = {
|
|
24
|
+
status: "preview";
|
|
25
|
+
requiresConfirm: true;
|
|
26
|
+
confirmed: false;
|
|
27
|
+
action: string;
|
|
28
|
+
payload: unknown;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Build a preview-confirm capability's un-executed result.
|
|
32
|
+
*
|
|
33
|
+
* Previews used to return `{ ok: true, action, payload }`, which is
|
|
34
|
+
* indistinguishable from a completed mutation to anyone skimming the JSON --
|
|
35
|
+
* a human, or an LLM deciding whether its call took effect. ORN-3417 recorded
|
|
36
|
+
* a live `node.off-grid` preview as a confirm-gate failure for exactly that
|
|
37
|
+
* reason. `status: "preview"` states it outright, and carries no `ok`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function preview(action: string, payload: unknown): PreviewResult;
|
|
22
40
|
export type JsonSchema = {
|
|
23
41
|
type: "object";
|
|
24
42
|
properties?: Record<string, unknown>;
|
|
@@ -53,6 +71,20 @@ export type CapabilitySpec<I = unknown, O = unknown> = Omit<Capability<I, O>, "c
|
|
|
53
71
|
};
|
|
54
72
|
/** Fill CLI/MCP/Slack names from `id`. Slack parameters stay optional. */
|
|
55
73
|
export declare function defineCapability<I, O>(spec: CapabilitySpec<I, O>): Capability;
|
|
74
|
+
/** A staff verb that only MCP dispatches: an MCP name, no Slack or CLI binding. */
|
|
75
|
+
export type StaffMcpCapabilitySpec<I = unknown, O = unknown> = Omit<CapabilitySpec<I, O>, "slack">;
|
|
76
|
+
/**
|
|
77
|
+
* Fill only the MCP name from `id`.
|
|
78
|
+
*
|
|
79
|
+
* `visibleToClient` gates Slack on `capability.slack != null` and CLI on
|
|
80
|
+
* `capability.cli != null`, so omitting a binding is how the catalog already
|
|
81
|
+
* says "not on that surface" — `defineCapability` just happens to set all
|
|
82
|
+
* three unconditionally. Staff admin verbs need the MCP one only: Slack's
|
|
83
|
+
* staff surface is a deliberately small, human-confirmed set, and `apps/cli`
|
|
84
|
+
* writes each command by hand, so a CLI binding here would advertise a
|
|
85
|
+
* command that does not exist.
|
|
86
|
+
*/
|
|
87
|
+
export declare function defineStaffMcpCapability<I, O>(spec: StaffMcpCapabilitySpec<I, O>): Capability;
|
|
56
88
|
export declare function slackStaffVisible(capability: Pick<Capability, "roles">): boolean;
|
|
57
89
|
export declare function visibleToClient(capability: Pick<Capability, "roles" | "cli" | "mcp" | "slack">, client: ClientKind, role: Role): boolean;
|
|
58
90
|
export declare function forRole(catalog: readonly Capability[], role: Role, client: ClientKind): Capability[];
|
|
@@ -16,6 +16,18 @@ export const DOMAINS = [
|
|
|
16
16
|
"billing",
|
|
17
17
|
"observability",
|
|
18
18
|
];
|
|
19
|
+
/**
|
|
20
|
+
* Build a preview-confirm capability's un-executed result.
|
|
21
|
+
*
|
|
22
|
+
* Previews used to return `{ ok: true, action, payload }`, which is
|
|
23
|
+
* indistinguishable from a completed mutation to anyone skimming the JSON --
|
|
24
|
+
* a human, or an LLM deciding whether its call took effect. ORN-3417 recorded
|
|
25
|
+
* a live `node.off-grid` preview as a confirm-gate failure for exactly that
|
|
26
|
+
* reason. `status: "preview"` states it outright, and carries no `ok`.
|
|
27
|
+
*/
|
|
28
|
+
export function preview(action, payload) {
|
|
29
|
+
return { status: "preview", requiresConfirm: true, confirmed: false, action, payload };
|
|
30
|
+
}
|
|
19
31
|
/** Fill CLI/MCP/Slack names from `id`. Slack parameters stay optional. */
|
|
20
32
|
export function defineCapability(spec) {
|
|
21
33
|
const names = namesFromId(spec.id);
|
|
@@ -29,6 +41,20 @@ export function defineCapability(spec) {
|
|
|
29
41
|
},
|
|
30
42
|
};
|
|
31
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Fill only the MCP name from `id`.
|
|
46
|
+
*
|
|
47
|
+
* `visibleToClient` gates Slack on `capability.slack != null` and CLI on
|
|
48
|
+
* `capability.cli != null`, so omitting a binding is how the catalog already
|
|
49
|
+
* says "not on that surface" — `defineCapability` just happens to set all
|
|
50
|
+
* three unconditionally. Staff admin verbs need the MCP one only: Slack's
|
|
51
|
+
* staff surface is a deliberately small, human-confirmed set, and `apps/cli`
|
|
52
|
+
* writes each command by hand, so a CLI binding here would advertise a
|
|
53
|
+
* command that does not exist.
|
|
54
|
+
*/
|
|
55
|
+
export function defineStaffMcpCapability(spec) {
|
|
56
|
+
return { ...spec, mcp: { name: namesFromId(spec.id).mcp } };
|
|
57
|
+
}
|
|
32
58
|
export function slackStaffVisible(capability) {
|
|
33
59
|
return capability.roles.some((role) => role === "reviewer" || role === "admin");
|
|
34
60
|
}
|
|
@@ -4,6 +4,9 @@ import { tokenCapabilities } from "./capabilities/tokens.js";
|
|
|
4
4
|
import { identityCapabilities } from "./capabilities/identity.js";
|
|
5
5
|
import { listingCapabilities } from "./capabilities/listings.js";
|
|
6
6
|
import { observabilityCapabilities } from "./capabilities/observability.js";
|
|
7
|
+
import { staffAccessCapabilities } from "./capabilities/staff-access.js";
|
|
8
|
+
import { staffCommerceCapabilities } from "./capabilities/staff-commerce.js";
|
|
9
|
+
import { staffFleetCapabilities } from "./capabilities/staff-fleet.js";
|
|
7
10
|
import { userCapabilities } from "./capabilities/users.js";
|
|
8
11
|
/** System of record for CLI, MCP, and Slack verbs. Add capabilities here. */
|
|
9
12
|
export const catalog = [
|
|
@@ -15,4 +18,7 @@ export const catalog = [
|
|
|
15
18
|
...observabilityCapabilities,
|
|
16
19
|
...slurmCapabilities,
|
|
17
20
|
...kubernetesCapabilities,
|
|
21
|
+
...staffFleetCapabilities,
|
|
22
|
+
...staffCommerceCapabilities,
|
|
23
|
+
...staffAccessCapabilities,
|
|
18
24
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { byCliCommand, byMcpName, bySlackName, defineCapability, forRole, slackStaffVisible, visibleToClient, type Capability, type CapabilityBindings, type CapabilityContext, type CapabilitySpec, type JsonSchema, type CapabilityTransport, type Domain, type HttpSpec, type Mutation, } from "./capability.ts";
|
|
1
|
+
export { byCliCommand, byMcpName, bySlackName, defineCapability, defineStaffMcpCapability, forRole, preview, slackStaffVisible, visibleToClient, type Capability, type CapabilityBindings, type CapabilityContext, type CapabilitySpec, type StaffMcpCapabilitySpec, type JsonSchema, type CapabilityTransport, type Domain, type HttpSpec, type Mutation, type PreviewResult, } from "./capability.ts";
|
|
2
2
|
export { namesFromId } from "./names.ts";
|
|
3
3
|
export { catalog } from "./catalog.ts";
|
|
4
4
|
export { fleetCapabilities, listFacilitiesCapability, listNodesCapability, listOperatorsCapability, nodeExecCapability, nodeOffGridCapability, nodeOnGridCapability, nodeTerminateCapability, } from "./capabilities/fleet.ts";
|
|
@@ -8,5 +8,8 @@ export { createListingCapability, listingCreateInput, listCatalogTermsCapability
|
|
|
8
8
|
export { userCapabilities, usersQueryCapability } from "./capabilities/users.ts";
|
|
9
9
|
export { kubernetesCapabilities, slurmCapabilities } from "./capabilities/clusters.ts";
|
|
10
10
|
export { logsLatestCapability, logsTailCapability, observabilityCapabilities, telemetryLatestCapability, telemetryTailCapability, } from "./capabilities/observability.ts";
|
|
11
|
+
export { enrollmentTokenRevokeCapability, enrollmentTokenRevokeInput, facilityCreateCapability, facilityCreateInput, facilityUpdateCapability, facilityUpdateInput, nodeAdminKeyCapability, nodeAdminKeyInput, nodeBenchmarkCapability, nodeBenchmarkInput, nodeDeployCapability, nodeDeployInput, nodeDetailsUpdateCapability, nodeDetailsUpdateInput, nodeHardResetCapability, nodeHardResetInput, nodeRebootCapability, nodeRebootInput, nodeReserveListingCapability, nodeReserveListingInput, nodeShowCapability, nodeShowInput, nodeTestResultCapability, nodeTestResultInput, nodeTestTriggerCapability, nodeTestTriggerInput, nodeUpdateCapability, nodeUpdateInput, operatorCreateCapability, operatorCreateInput, operatorDeleteCapability, operatorDeleteInput, operatorUpdateCapability, operatorUpdateInput, staffFleetCapabilities, } from "./capabilities/staff-fleet.ts";
|
|
12
|
+
export { bidAcceptCapability, bidAcceptInput, bidRejectCapability, bidRejectInput, inventoryUpdateCapability, inventoryUpdateInput, reservationCancelCapability, reservationCancelInput, reservationMachinesListCapability, reservationMachinesListInput, reservationPublishCapability, reservationPublishInput, reservationSshKeyAddCapability, reservationSshKeyAddInput, reservationSshKeyRemoveCapability, reservationSshKeyRemoveInput, reservationTransferCapability, reservationTransferInput, staffCommerceCapabilities, tenantCreditProfileShowCapability, tenantCreditProfileShowInput, } from "./capabilities/staff-commerce.ts";
|
|
13
|
+
export { sshKeyAddCapability, sshKeyAddInput, sshKeyPushCapability, sshKeyPushInput, sshKeyRevokeCapability, sshKeyRevokeInput, staffAccessCapabilities, } from "./capabilities/staff-access.ts";
|
|
11
14
|
export { CLIENTS, parseRole, roleAtLeast, ROLES, type ClientKind, type Role } from "./role.ts";
|
|
12
15
|
export { bindHardwareFields, HARDWARE_CATALOG_PATH, HARDWARE_KINDS, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, looksLikeSpecDump, normalizeCatalogQuery, parseHardwareCatalog, projectCatalogTerms, proposeListingPayload, resolveCatalogTerm, UNRESOLVED_CATALOG_HINT, type CatalogBindResult, type CatalogTerm, type HardwareCatalog, type HardwareKind, type ResolvedCatalogTerm, } from "./spec-catalog.ts";
|