@ornncompute/cli 0.2.7 → 0.2.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ export declare const sshKeyPushInput: z.ZodObject<{
3
+ instanceId: z.ZodString;
4
+ reservationId: z.ZodString;
5
+ tenantId: z.ZodString;
6
+ authUserId: z.ZodString;
7
+ sshKeyIds: z.ZodArray<z.ZodString>;
8
+ requestId: z.ZodOptional<z.ZodString>;
9
+ confirm: z.ZodOptional<z.ZodBoolean>;
10
+ }, z.core.$strip>;
11
+ export declare const sshKeyPushCapability: import("../capability.ts").Capability<unknown, unknown>;
12
+ export declare const sshKeyAddInput: z.ZodObject<{
13
+ reservationId: z.ZodString;
14
+ tenantId: z.ZodString;
15
+ authUserId: z.ZodString;
16
+ publicKey: z.ZodString;
17
+ label: z.ZodOptional<z.ZodString>;
18
+ confirm: z.ZodOptional<z.ZodBoolean>;
19
+ }, z.core.$strip>;
20
+ export declare const sshKeyAddCapability: import("../capability.ts").Capability<unknown, unknown>;
21
+ export declare const sshKeyRevokeInput: z.ZodObject<{
22
+ tenantId: z.ZodString;
23
+ keyId: z.ZodString;
24
+ confirm: z.ZodOptional<z.ZodBoolean>;
25
+ }, z.core.$strip>;
26
+ export declare const sshKeyRevokeCapability: import("../capability.ts").Capability<unknown, unknown>;
27
+ export declare const staffAccessCapabilities: import("../capability.ts").Capability<unknown, unknown>[];
@@ -0,0 +1,192 @@
1
+ import { z } from "zod";
2
+ import { defineStaffMcpCapability, preview } from "../capability.js";
3
+ import { mapBody, seg, statusOf } from "./staff-shared.js";
4
+ // Staff SSH-access verbs. MCP-only — see `defineStaffMcpCapability`.
5
+ //
6
+ // Field contracts from the internal SSH-key routes:
7
+ // - push: PATCH /internal/nodes/{instanceId}/keys (or /internal/pods/... for a VM)
8
+ // body { ssh_authorized_keys, request_id? }
9
+ // - add: POST /internal/v1/reservations/{reservationId}/ssh-keys
10
+ // body { public_key, label? }, after authorization owns the public key
11
+ // - revoke: DELETE /v1/organizations/{tenantId}/ssh-keys/{keyId}
12
+ //
13
+ // The routes never forward tenantId/authUserId to the orchestrator as-is — they use them to
14
+ // verify the reservation actually belongs to that organization, and that authUserId is a member
15
+ // of it (commerce GET /internal/v1/reservations/{id} + authorization GET
16
+ // /internal/organizations/{id}/members). Those two checks live in
17
+ // `assertReservationOwnedByTenantUser` below so every surface runs them.
18
+ /** Acts as the caller for a write the tenant's own user is nominally making. */
19
+ const AUTH_USER_HEADER = "X-Auth-User-Id";
20
+ async function assertReservationOwnedByTenantUser(ctx, options) {
21
+ let reservation;
22
+ try {
23
+ reservation = (await ctx.fetch(`/internal/v1/reservations/${seg(options.reservationId)}`, {
24
+ method: "GET",
25
+ }));
26
+ }
27
+ catch (error) {
28
+ if (statusOf(error) === 404) {
29
+ throw new Error("Reservation not found.");
30
+ }
31
+ throw error;
32
+ }
33
+ if (!reservation?.reservationId) {
34
+ throw new Error("Reservation not found.");
35
+ }
36
+ const organizationId = reservation.organizationId ?? reservation.tenantId;
37
+ if (organizationId !== options.tenantId) {
38
+ throw new Error("Reservation does not belong to that tenant.");
39
+ }
40
+ const members = (await ctx.fetch(`/internal/organizations/${seg(options.tenantId)}/members`, {
41
+ method: "GET",
42
+ }));
43
+ if (!members.some((member) => member.user_id === options.authUserId)) {
44
+ throw new Error("Selected SSH key owner is not a member of this tenant.");
45
+ }
46
+ }
47
+ async function assertMachineBelongsToReservation(ctx, options) {
48
+ const listed = (await ctx.fetch(`/internal/nodes?reservation_id=${seg(options.reservationId)}`, {
49
+ method: "GET",
50
+ }));
51
+ if (!(listed.nodes ?? []).some((node) => node.node_id === options.instanceId || node.id === options.instanceId)) {
52
+ throw new Error("Machine does not belong to that reservation.");
53
+ }
54
+ }
55
+ export const sshKeyPushInput = z.object({
56
+ instanceId: z.string(),
57
+ reservationId: z.string(),
58
+ tenantId: z.string(),
59
+ authUserId: z.string(),
60
+ sshKeyIds: z.array(z.string()).min(1),
61
+ requestId: z.string().optional(),
62
+ confirm: z.boolean().optional(),
63
+ });
64
+ export const sshKeyPushCapability = defineStaffMcpCapability({
65
+ id: "ssh-key.push",
66
+ domain: "access",
67
+ roles: ["admin"],
68
+ description: "Internal staff only: push selected reservation SSH keys onto a live machine. " +
69
+ "This grants remote access to a live machine. Requires confirm: true.",
70
+ mutation: "preview-confirm",
71
+ http: { method: "PATCH", path: "/internal/nodes/{instanceId}/keys" },
72
+ input: sshKeyPushInput,
73
+ execute: async (ctx, input) => {
74
+ const parsed = sshKeyPushInput.parse(input);
75
+ if (!ctx.confirmed) {
76
+ return preview("ssh-key.push", {
77
+ instanceId: parsed.instanceId,
78
+ reservationId: parsed.reservationId,
79
+ sshKeyIds: parsed.sshKeyIds,
80
+ });
81
+ }
82
+ // Sequential, not Promise.all: this is a fail-fast authorization gate, not an independent
83
+ // read pair. If the reservation doesn't belong to this tenant/user, there's no reason to
84
+ // spend a second round trip checking machine membership.
85
+ await assertReservationOwnedByTenantUser(ctx, {
86
+ reservationId: parsed.reservationId,
87
+ tenantId: parsed.tenantId,
88
+ authUserId: parsed.authUserId,
89
+ });
90
+ await assertMachineBelongsToReservation(ctx, {
91
+ reservationId: parsed.reservationId,
92
+ instanceId: parsed.instanceId,
93
+ });
94
+ const listed = (await ctx.fetch(`/v1/organizations/${seg(parsed.tenantId)}/ssh-keys`, {
95
+ method: "GET",
96
+ }));
97
+ const wanted = new Set(parsed.sshKeyIds);
98
+ const sshAuthorizedKeys = (listed.ssh_keys ?? [])
99
+ .filter((key) => key.id &&
100
+ wanted.has(key.id) &&
101
+ key.status === "active" &&
102
+ !key.revoked_at &&
103
+ Boolean(key.public_key?.trim()))
104
+ .map((key) => key.public_key.trim());
105
+ const body = {
106
+ ssh_authorized_keys: sshAuthorizedKeys,
107
+ ...mapBody(parsed, { requestId: "request_id" }),
108
+ };
109
+ let keysPath = `/internal/nodes/${seg(parsed.instanceId)}/keys`;
110
+ try {
111
+ const pod = (await ctx.fetch(`/internal/pods/${seg(parsed.instanceId)}`, {
112
+ method: "GET",
113
+ }));
114
+ if (pod.kind === "virtual-machine") {
115
+ keysPath = `/internal/pods/${seg(parsed.instanceId)}/keys`;
116
+ }
117
+ }
118
+ catch {
119
+ // instanceId is a host node
120
+ }
121
+ return ctx.fetch(keysPath, {
122
+ method: "PATCH",
123
+ body,
124
+ headers: { [AUTH_USER_HEADER]: parsed.authUserId },
125
+ });
126
+ },
127
+ });
128
+ export const sshKeyAddInput = z.object({
129
+ reservationId: z.string(),
130
+ tenantId: z.string(),
131
+ authUserId: z.string(),
132
+ publicKey: z.string().min(1),
133
+ label: z.string().optional(),
134
+ confirm: z.boolean().optional(),
135
+ });
136
+ export const sshKeyAddCapability = defineStaffMcpCapability({
137
+ id: "ssh-key.add",
138
+ domain: "access",
139
+ roles: ["admin"],
140
+ description: "Internal staff only: register an SSH public key on a reservation for a tenant user. Requires confirm: true.",
141
+ mutation: "preview-confirm",
142
+ http: { method: "POST", path: "/internal/v1/reservations/{reservationId}/ssh-keys" },
143
+ input: sshKeyAddInput,
144
+ execute: async (ctx, input) => {
145
+ const parsed = sshKeyAddInput.parse(input);
146
+ const body = {
147
+ public_key: parsed.publicKey,
148
+ ...mapBody(parsed, { label: "label" }),
149
+ };
150
+ if (!ctx.confirmed) {
151
+ return preview("ssh-key.add", { reservationId: parsed.reservationId, ...body });
152
+ }
153
+ await assertReservationOwnedByTenantUser(ctx, {
154
+ reservationId: parsed.reservationId,
155
+ tenantId: parsed.tenantId,
156
+ authUserId: parsed.authUserId,
157
+ });
158
+ return ctx.fetch(`/internal/v1/reservations/${seg(parsed.reservationId)}/ssh-keys`, {
159
+ method: "POST",
160
+ body,
161
+ headers: { [AUTH_USER_HEADER]: parsed.authUserId },
162
+ });
163
+ },
164
+ });
165
+ export const sshKeyRevokeInput = z.object({
166
+ tenantId: z.string(),
167
+ keyId: z.string(),
168
+ confirm: z.boolean().optional(),
169
+ });
170
+ export const sshKeyRevokeCapability = defineStaffMcpCapability({
171
+ id: "ssh-key.revoke",
172
+ domain: "access",
173
+ roles: ["admin"],
174
+ description: "Internal staff only: revoke (delete) a tenant-scoped SSH key. Requires confirm: true.",
175
+ mutation: "preview-confirm",
176
+ http: { method: "DELETE", path: "/v1/organizations/{tenantId}/ssh-keys/{keyId}" },
177
+ input: sshKeyRevokeInput,
178
+ execute: async (ctx, input) => {
179
+ const parsed = sshKeyRevokeInput.parse(input);
180
+ if (!ctx.confirmed) {
181
+ return preview("ssh-key.revoke", { tenantId: parsed.tenantId, keyId: parsed.keyId });
182
+ }
183
+ return ctx.fetch(`/v1/organizations/${seg(parsed.tenantId)}/ssh-keys/${seg(parsed.keyId)}`, {
184
+ method: "DELETE",
185
+ });
186
+ },
187
+ });
188
+ export const staffAccessCapabilities = [
189
+ sshKeyPushCapability,
190
+ sshKeyAddCapability,
191
+ sshKeyRevokeCapability,
192
+ ];
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ export declare const reservationCancelInput: z.ZodObject<{
3
+ reservationId: z.ZodString;
4
+ confirm: z.ZodOptional<z.ZodBoolean>;
5
+ }, z.core.$strip>;
6
+ export declare const reservationCancelCapability: import("../capability.ts").Capability<unknown, unknown>;
7
+ export declare const reservationTransferInput: z.ZodObject<{
8
+ reservationId: z.ZodString;
9
+ targetTenantId: z.ZodString;
10
+ targetAuthUserId: z.ZodOptional<z.ZodString>;
11
+ nodeId: z.ZodOptional<z.ZodString>;
12
+ reservedStrategy: z.ZodEnum<{
13
+ reject: "reject";
14
+ park: "park";
15
+ }>;
16
+ confirmReservedTransfer: z.ZodOptional<z.ZodBoolean>;
17
+ notes: z.ZodOptional<z.ZodString>;
18
+ confirm: z.ZodOptional<z.ZodBoolean>;
19
+ }, z.core.$strip>;
20
+ export declare const reservationTransferCapability: import("../capability.ts").Capability<unknown, unknown>;
21
+ export declare const reservationMachinesListInput: z.ZodObject<{
22
+ reservationId: z.ZodString;
23
+ }, z.core.$strip>;
24
+ export declare const reservationMachinesListCapability: import("../capability.ts").Capability<unknown, unknown>;
25
+ export declare const reservationSshKeyAddInput: z.ZodObject<{
26
+ reservationId: z.ZodString;
27
+ userId: z.ZodString;
28
+ publicKey: z.ZodString;
29
+ label: z.ZodOptional<z.ZodString>;
30
+ nodeId: z.ZodOptional<z.ZodString>;
31
+ confirm: z.ZodOptional<z.ZodBoolean>;
32
+ }, z.core.$strip>;
33
+ export declare const reservationSshKeyAddCapability: import("../capability.ts").Capability<unknown, unknown>;
34
+ export declare const reservationSshKeyRemoveInput: z.ZodObject<{
35
+ reservationId: z.ZodString;
36
+ keyId: z.ZodString;
37
+ userId: z.ZodString;
38
+ nodeId: z.ZodOptional<z.ZodString>;
39
+ confirm: z.ZodOptional<z.ZodBoolean>;
40
+ }, z.core.$strip>;
41
+ export declare const reservationSshKeyRemoveCapability: import("../capability.ts").Capability<unknown, unknown>;
42
+ export declare const reservationPublishInput: z.ZodObject<{
43
+ reservationId: z.ZodString;
44
+ confirm: z.ZodOptional<z.ZodBoolean>;
45
+ }, z.core.$strip>;
46
+ export declare const reservationPublishCapability: import("../capability.ts").Capability<unknown, unknown>;
47
+ export declare const bidAcceptInput: z.ZodObject<{
48
+ bidId: z.ZodString;
49
+ acceptedGpuCount: z.ZodOptional<z.ZodNumber>;
50
+ confirm: z.ZodOptional<z.ZodBoolean>;
51
+ }, z.core.$strip>;
52
+ export declare const bidAcceptCapability: import("../capability.ts").Capability<unknown, unknown>;
53
+ export declare const bidRejectInput: z.ZodObject<{
54
+ bidId: z.ZodString;
55
+ confirm: z.ZodOptional<z.ZodBoolean>;
56
+ }, z.core.$strip>;
57
+ export declare const bidRejectCapability: import("../capability.ts").Capability<unknown, unknown>;
58
+ export declare const inventoryUpdateInput: z.ZodObject<{
59
+ inventoryId: z.ZodString;
60
+ siteOperator: z.ZodOptional<z.ZodString>;
61
+ siteNickname: z.ZodOptional<z.ZodString>;
62
+ gpuType: z.ZodOptional<z.ZodString>;
63
+ nodeCount: z.ZodOptional<z.ZodNumber>;
64
+ gpusPerNode: z.ZodOptional<z.ZodNumber>;
65
+ availableFrom: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ availableTo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ availableStartAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
+ availableEndAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
69
+ cpu: z.ZodOptional<z.ZodNullable<z.ZodString>>;
70
+ ram: z.ZodOptional<z.ZodNullable<z.ZodString>>;
71
+ fabricType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
72
+ storage: z.ZodOptional<z.ZodNullable<z.ZodString>>;
73
+ internet: z.ZodOptional<z.ZodNullable<z.ZodString>>;
74
+ networkHardware: z.ZodOptional<z.ZodNullable<z.ZodString>>;
75
+ buyNowPricePerGpuHour: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
76
+ confirm: z.ZodOptional<z.ZodBoolean>;
77
+ }, z.core.$strip>;
78
+ export declare const inventoryUpdateCapability: import("../capability.ts").Capability<unknown, unknown>;
79
+ export declare const tenantCreditProfileShowInput: z.ZodObject<{
80
+ tenantId: z.ZodString;
81
+ }, z.core.$strip>;
82
+ export declare const tenantCreditProfileShowCapability: import("../capability.ts").Capability<unknown, unknown>;
83
+ export declare const staffCommerceCapabilities: import("../capability.ts").Capability<unknown, unknown>[];
@@ -0,0 +1,290 @@
1
+ import { z } from "zod";
2
+ import { defineStaffMcpCapability, preview } from "../capability.js";
3
+ import { mapBody, seg } from "./staff-shared.js";
4
+ // Staff commerce verbs: reservations, bids, forward inventory, and the tenant
5
+ // credit profile. MCP-only — see `defineStaffMcpCapability`.
6
+ export const reservationCancelInput = z.object({
7
+ reservationId: z.string(),
8
+ confirm: z.boolean().optional(),
9
+ });
10
+ export const reservationCancelCapability = defineStaffMcpCapability({
11
+ id: "reservation.cancel",
12
+ domain: "reservations",
13
+ roles: ["admin"],
14
+ description: "Internal staff only: cancel a reservation. Mirrors the internal dashboard's Cancel/Reject action.",
15
+ mutation: "preview-confirm",
16
+ http: { method: "POST", path: "/internal/reservations/{reservationId}/cancel" },
17
+ input: reservationCancelInput,
18
+ execute: async (ctx, input) => {
19
+ const parsed = reservationCancelInput.parse(input);
20
+ if (!ctx.confirmed) {
21
+ return preview("reservation.cancel", { reservationId: parsed.reservationId });
22
+ }
23
+ return ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}/cancel`, {
24
+ method: "POST",
25
+ });
26
+ },
27
+ });
28
+ export const reservationTransferInput = z.object({
29
+ reservationId: z.string(),
30
+ targetTenantId: z.string(),
31
+ targetAuthUserId: z.string().optional(),
32
+ nodeId: z.string().optional(),
33
+ reservedStrategy: z.enum(["reject", "park"]),
34
+ confirmReservedTransfer: z.boolean().optional(),
35
+ notes: z.string().max(2000).optional(),
36
+ confirm: z.boolean().optional(),
37
+ });
38
+ export const reservationTransferCapability = defineStaffMcpCapability({
39
+ id: "reservation.transfer",
40
+ domain: "reservations",
41
+ roles: ["admin"],
42
+ description: "Internal staff only: transfer a reservation to a different tenant/node. Mirrors the internal node-overlay transfer action. " +
43
+ "reservedStrategy has no default and must be chosen explicitly: \"reject\" (transfer only if the reservation isn't " +
44
+ "currently provisioned/assigned) or \"park\" (clean up old access and park it for the target tenant, for a live handoff). " +
45
+ "The dashboard always computes this from the reservation's live state before sending it rather than relying on the " +
46
+ "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 confirm_reserved_transfer.",
48
+ mutation: "preview-confirm",
49
+ http: { method: "POST", path: "/internal/reservations/{reservationId}/transfer" },
50
+ input: reservationTransferInput,
51
+ execute: async (ctx, input) => {
52
+ const parsed = reservationTransferInput.parse(input);
53
+ const body = {
54
+ target_tenant_id: parsed.targetTenantId,
55
+ reserved_strategy: parsed.reservedStrategy,
56
+ confirm_reserved_transfer: parsed.confirmReservedTransfer ?? false,
57
+ ...mapBody(parsed, {
58
+ targetAuthUserId: "target_auth_user_id",
59
+ nodeId: "node_id",
60
+ notes: "notes",
61
+ }),
62
+ };
63
+ if (!ctx.confirmed)
64
+ return preview("reservation.transfer", body);
65
+ return ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}/transfer`, {
66
+ method: "POST",
67
+ body,
68
+ });
69
+ },
70
+ });
71
+ export const reservationMachinesListInput = z.object({ reservationId: z.string() });
72
+ export const reservationMachinesListCapability = defineStaffMcpCapability({
73
+ id: "reservation.machines.list",
74
+ domain: "reservations",
75
+ roles: ["reviewer", "admin"],
76
+ description: "Internal staff only: list machines for a reservation.",
77
+ mutation: "read",
78
+ http: { method: "GET", path: "/internal/nodes" },
79
+ input: reservationMachinesListInput,
80
+ execute: async (ctx, input) => {
81
+ const parsed = reservationMachinesListInput.parse(input);
82
+ return ctx.fetch(`/internal/nodes?reservation_id=${seg(parsed.reservationId)}`, {
83
+ method: "GET",
84
+ });
85
+ },
86
+ });
87
+ export const reservationSshKeyAddInput = z.object({
88
+ reservationId: z.string(),
89
+ userId: z.string(),
90
+ publicKey: z.string().min(1),
91
+ label: z.string().optional(),
92
+ nodeId: z.string().optional(),
93
+ confirm: z.boolean().optional(),
94
+ });
95
+ export const reservationSshKeyAddCapability = defineStaffMcpCapability({
96
+ id: "reservation.ssh-key.add",
97
+ domain: "reservations",
98
+ roles: ["admin"],
99
+ description: "Internal staff only: add an SSH key to a reservation and queue sync to its machines. Requires confirm: true.",
100
+ mutation: "preview-confirm",
101
+ http: { method: "POST", path: "/internal/v1/reservations/{reservationId}/ssh-keys" },
102
+ input: reservationSshKeyAddInput,
103
+ execute: async (ctx, input) => {
104
+ const parsed = reservationSshKeyAddInput.parse(input);
105
+ const body = {
106
+ user_id: parsed.userId,
107
+ public_key: parsed.publicKey,
108
+ ...mapBody(parsed, { label: "label", nodeId: "node_id" }),
109
+ };
110
+ if (!ctx.confirmed)
111
+ return preview("reservation.ssh-key.add", body);
112
+ return ctx.fetch(`/internal/v1/reservations/${seg(parsed.reservationId)}/ssh-keys`, {
113
+ method: "POST",
114
+ body,
115
+ });
116
+ },
117
+ });
118
+ export const reservationSshKeyRemoveInput = z.object({
119
+ reservationId: z.string(),
120
+ keyId: z.string(),
121
+ userId: z.string(),
122
+ nodeId: z.string().optional(),
123
+ confirm: z.boolean().optional(),
124
+ });
125
+ export const reservationSshKeyRemoveCapability = defineStaffMcpCapability({
126
+ id: "reservation.ssh-key.remove",
127
+ domain: "reservations",
128
+ roles: ["admin"],
129
+ description: "Internal staff only: remove an SSH key from a reservation and queue cleanup on its machines. Requires confirm: true.",
130
+ mutation: "preview-confirm",
131
+ http: { method: "DELETE", path: "/internal/v1/reservations/{reservationId}/ssh-keys/{keyId}" },
132
+ input: reservationSshKeyRemoveInput,
133
+ execute: async (ctx, input) => {
134
+ const parsed = reservationSshKeyRemoveInput.parse(input);
135
+ const body = {
136
+ user_id: parsed.userId,
137
+ ...mapBody(parsed, { nodeId: "node_id" }),
138
+ };
139
+ if (!ctx.confirmed)
140
+ return preview("reservation.ssh-key.remove", body);
141
+ return ctx.fetch(`/internal/v1/reservations/${seg(parsed.reservationId)}/ssh-keys/${seg(parsed.keyId)}`, { method: "DELETE", body });
142
+ },
143
+ });
144
+ export const reservationPublishInput = z.object({
145
+ reservationId: z.string(),
146
+ confirm: z.boolean().optional(),
147
+ });
148
+ export const reservationPublishCapability = defineStaffMcpCapability({
149
+ id: "reservation.publish",
150
+ domain: "reservations",
151
+ roles: ["admin"],
152
+ description: "Internal staff only: publish a commerce draft into a live Commerce reservation. Requires confirm: true.",
153
+ mutation: "preview-confirm",
154
+ http: { method: "POST", path: "/internal/reservations/{reservationId}/publish" },
155
+ input: reservationPublishInput,
156
+ execute: async (ctx, input) => {
157
+ const parsed = reservationPublishInput.parse(input);
158
+ if (!ctx.confirmed) {
159
+ return preview("reservation.publish", { reservationId: parsed.reservationId });
160
+ }
161
+ return ctx.fetch(`/internal/reservations/${seg(parsed.reservationId)}/publish`, {
162
+ method: "POST",
163
+ });
164
+ },
165
+ });
166
+ export const bidAcceptInput = z.object({
167
+ bidId: z.string(),
168
+ acceptedGpuCount: z.number().int().min(1).optional(),
169
+ confirm: z.boolean().optional(),
170
+ });
171
+ export const bidAcceptCapability = defineStaffMcpCapability({
172
+ id: "bid.accept",
173
+ domain: "bids",
174
+ roles: ["admin"],
175
+ description: "Internal staff only: accept an active bid. Optionally set acceptedGpuCount (defaults to the bid's full gpu_count). Mirrors the internal dashboard's Accept action.",
176
+ mutation: "preview-confirm",
177
+ http: { method: "POST", path: "/internal/bids/{bidId}/accept" },
178
+ input: bidAcceptInput,
179
+ execute: async (ctx, input) => {
180
+ const parsed = bidAcceptInput.parse(input);
181
+ const body = mapBody(parsed, { acceptedGpuCount: "accepted_gpu_count" });
182
+ if (!ctx.confirmed)
183
+ return preview("bid.accept", { bidId: parsed.bidId, ...body });
184
+ return ctx.fetch(`/internal/bids/${seg(parsed.bidId)}/accept`, { method: "POST", body });
185
+ },
186
+ });
187
+ export const bidRejectInput = z.object({
188
+ bidId: z.string(),
189
+ confirm: z.boolean().optional(),
190
+ });
191
+ export const bidRejectCapability = defineStaffMcpCapability({
192
+ id: "bid.reject",
193
+ domain: "bids",
194
+ roles: ["admin"],
195
+ description: "Internal staff only: reject an active bid. Mirrors the internal dashboard's Reject action.",
196
+ mutation: "preview-confirm",
197
+ http: { method: "POST", path: "/internal/bids/{bidId}/reject" },
198
+ input: bidRejectInput,
199
+ execute: async (ctx, input) => {
200
+ const parsed = bidRejectInput.parse(input);
201
+ if (!ctx.confirmed)
202
+ return preview("bid.reject", { bidId: parsed.bidId });
203
+ return ctx.fetch(`/internal/bids/${seg(parsed.bidId)}/reject`, { method: "POST" });
204
+ },
205
+ });
206
+ export const inventoryUpdateInput = z.object({
207
+ inventoryId: z.string(),
208
+ siteOperator: z.string().min(1).optional(),
209
+ siteNickname: z.string().min(1).optional(),
210
+ gpuType: z.string().min(1).optional(),
211
+ nodeCount: z
212
+ .number()
213
+ .int()
214
+ .positive()
215
+ .optional()
216
+ .describe("Total nodes (listing capacity), not total GPUs."),
217
+ gpusPerNode: z.number().int().min(1).max(32).optional(),
218
+ availableFrom: z.string().nullable().optional(),
219
+ availableTo: z.string().nullable().optional(),
220
+ availableStartAt: z.string().nullable().optional(),
221
+ availableEndAt: z.string().nullable().optional(),
222
+ cpu: z.string().nullable().optional(),
223
+ ram: z.string().nullable().optional(),
224
+ fabricType: z.string().nullable().optional(),
225
+ storage: z.string().nullable().optional(),
226
+ internet: z.string().nullable().optional(),
227
+ networkHardware: z.string().nullable().optional(),
228
+ buyNowPricePerGpuHour: z.number().positive().nullable().optional(),
229
+ confirm: z.boolean().optional(),
230
+ });
231
+ export const inventoryUpdateCapability = defineStaffMcpCapability({
232
+ id: "inventory.update",
233
+ domain: "listings",
234
+ roles: ["admin"],
235
+ 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.",
237
+ mutation: "preview-confirm",
238
+ http: { method: "PATCH", path: "/inventory/{inventoryId}" },
239
+ input: inventoryUpdateInput,
240
+ execute: async (ctx, input) => {
241
+ const parsed = inventoryUpdateInput.parse(input);
242
+ const body = mapBody(parsed, {
243
+ siteOperator: "site_operator",
244
+ siteNickname: "site_nickname",
245
+ gpuType: "gpu_type",
246
+ nodeCount: "node_count",
247
+ gpusPerNode: "gpus_per_node",
248
+ availableFrom: "available_from",
249
+ availableTo: "available_to",
250
+ availableStartAt: "available_start_at",
251
+ availableEndAt: "available_end_at",
252
+ cpu: "cpu",
253
+ ram: "ram",
254
+ fabricType: "fabric_type",
255
+ storage: "storage",
256
+ internet: "internet",
257
+ networkHardware: "network_hardware",
258
+ buyNowPricePerGpuHour: "buy_now_price_per_gpu_hour",
259
+ });
260
+ if (!ctx.confirmed)
261
+ return preview("inventory.update", body);
262
+ return ctx.fetch(`/inventory/${seg(parsed.inventoryId)}`, { method: "PATCH", body });
263
+ },
264
+ });
265
+ export const tenantCreditProfileShowInput = z.object({ tenantId: z.string() });
266
+ export const tenantCreditProfileShowCapability = defineStaffMcpCapability({
267
+ id: "tenant.credit-profile.show",
268
+ domain: "users",
269
+ roles: ["reviewer", "admin"],
270
+ description: "Internal staff only: show a tenant's credit profile, approval status, and onboarding details. Read-only — does not approve or disapprove.",
271
+ mutation: "read",
272
+ http: { method: "GET", path: "/internal/organizations/{tenantId}" },
273
+ input: tenantCreditProfileShowInput,
274
+ execute: async (ctx, input) => {
275
+ const parsed = tenantCreditProfileShowInput.parse(input);
276
+ return ctx.fetch(`/internal/organizations/${seg(parsed.tenantId)}`, { method: "GET" });
277
+ },
278
+ });
279
+ export const staffCommerceCapabilities = [
280
+ reservationCancelCapability,
281
+ reservationTransferCapability,
282
+ reservationMachinesListCapability,
283
+ reservationSshKeyAddCapability,
284
+ reservationSshKeyRemoveCapability,
285
+ reservationPublishCapability,
286
+ bidAcceptCapability,
287
+ bidRejectCapability,
288
+ inventoryUpdateCapability,
289
+ tenantCreditProfileShowCapability,
290
+ ];
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ export declare const enrollmentTokenRevokeInput: z.ZodObject<{
3
+ tokenId: z.ZodString;
4
+ confirm: z.ZodOptional<z.ZodBoolean>;
5
+ }, z.core.$strip>;
6
+ export declare const enrollmentTokenRevokeCapability: import("../capability.ts").Capability<unknown, unknown>;
7
+ export declare const operatorCreateInput: z.ZodObject<{
8
+ slug: z.ZodString;
9
+ displayName: z.ZodString;
10
+ mode: z.ZodOptional<z.ZodEnum<{
11
+ managed: "managed";
12
+ federated: "federated";
13
+ }>>;
14
+ status: z.ZodOptional<z.ZodEnum<{
15
+ active: "active";
16
+ unavailable: "unavailable";
17
+ revoked: "revoked";
18
+ }>>;
19
+ confirm: z.ZodOptional<z.ZodBoolean>;
20
+ }, z.core.$strip>;
21
+ export declare const operatorCreateCapability: import("../capability.ts").Capability<unknown, unknown>;
22
+ export declare const operatorUpdateInput: z.ZodObject<{
23
+ operatorId: z.ZodString;
24
+ displayName: z.ZodOptional<z.ZodString>;
25
+ slug: z.ZodOptional<z.ZodString>;
26
+ mode: z.ZodOptional<z.ZodEnum<{
27
+ managed: "managed";
28
+ federated: "federated";
29
+ }>>;
30
+ status: z.ZodOptional<z.ZodEnum<{
31
+ active: "active";
32
+ unavailable: "unavailable";
33
+ revoked: "revoked";
34
+ }>>;
35
+ confirm: z.ZodOptional<z.ZodBoolean>;
36
+ }, z.core.$strip>;
37
+ export declare const operatorUpdateCapability: import("../capability.ts").Capability<unknown, unknown>;
38
+ export declare const operatorDeleteInput: z.ZodObject<{
39
+ operatorId: z.ZodString;
40
+ confirm: z.ZodOptional<z.ZodBoolean>;
41
+ }, z.core.$strip>;
42
+ export declare const operatorDeleteCapability: import("../capability.ts").Capability<unknown, unknown>;
43
+ export declare const facilityCreateInput: z.ZodObject<{
44
+ operatorId: z.ZodString;
45
+ slug: z.ZodString;
46
+ displayName: z.ZodString;
47
+ region: z.ZodString;
48
+ zone: z.ZodOptional<z.ZodString>;
49
+ confirm: z.ZodOptional<z.ZodBoolean>;
50
+ }, z.core.$strip>;
51
+ export declare const facilityCreateCapability: import("../capability.ts").Capability<unknown, unknown>;
52
+ export declare const facilityUpdateInput: z.ZodObject<{
53
+ facilityId: z.ZodString;
54
+ displayName: z.ZodOptional<z.ZodString>;
55
+ region: z.ZodOptional<z.ZodString>;
56
+ zone: z.ZodOptional<z.ZodString>;
57
+ confirm: z.ZodOptional<z.ZodBoolean>;
58
+ }, z.core.$strip>;
59
+ export declare const facilityUpdateCapability: import("../capability.ts").Capability<unknown, unknown>;
60
+ export declare const nodeShowInput: z.ZodObject<{
61
+ nodeId: z.ZodString;
62
+ }, z.core.$strip>;
63
+ export declare const nodeShowCapability: import("../capability.ts").Capability<unknown, unknown>;
64
+ export declare const nodeDetailsUpdateInput: z.ZodObject<{
65
+ nodeId: z.ZodString;
66
+ hostname: z.ZodOptional<z.ZodString>;
67
+ publicLocation: z.ZodOptional<z.ZodString>;
68
+ facilityId: z.ZodOptional<z.ZodString>;
69
+ gpuType: z.ZodOptional<z.ZodString>;
70
+ gpuCount: z.ZodOptional<z.ZodNumber>;
71
+ ornnMode: z.ZodOptional<z.ZodString>;
72
+ cpu: z.ZodOptional<z.ZodString>;
73
+ ram: z.ZodOptional<z.ZodString>;
74
+ storage: z.ZodOptional<z.ZodString>;
75
+ fabricType: z.ZodOptional<z.ZodString>;
76
+ internet: z.ZodOptional<z.ZodString>;
77
+ networkHardware: z.ZodOptional<z.ZodString>;
78
+ buyNowPricePerGpuHour: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
79
+ confirm: z.ZodOptional<z.ZodBoolean>;
80
+ }, z.core.$strip>;
81
+ export declare const nodeDetailsUpdateCapability: import("../capability.ts").Capability<unknown, unknown>;
82
+ export declare const nodeTestTriggerInput: z.ZodObject<{
83
+ nodeId: z.ZodString;
84
+ burnTimeMinutes: z.ZodOptional<z.ZodNumber>;
85
+ constraints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
86
+ source: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
87
+ confirm: z.ZodOptional<z.ZodBoolean>;
88
+ }, z.core.$strip>;
89
+ export declare const nodeTestTriggerCapability: import("../capability.ts").Capability<unknown, unknown>;
90
+ export declare const nodeTestResultInput: z.ZodObject<{
91
+ nodeId: z.ZodOptional<z.ZodString>;
92
+ jobId: z.ZodString;
93
+ packetId: z.ZodOptional<z.ZodString>;
94
+ }, z.core.$strip>;
95
+ export declare const nodeTestResultCapability: import("../capability.ts").Capability<unknown, unknown>;
96
+ export declare const nodeBenchmarkInput: z.ZodObject<{
97
+ nodeId: z.ZodString;
98
+ }, z.core.$strip>;
99
+ export declare const nodeBenchmarkCapability: import("../capability.ts").Capability<unknown, unknown>;
100
+ export declare const nodeDeployInput: z.ZodObject<{
101
+ nodeId: z.ZodString;
102
+ targetTenantId: z.ZodString;
103
+ targetAuthUserId: z.ZodOptional<z.ZodString>;
104
+ networkMode: z.ZodOptional<z.ZodEnum<{
105
+ public: "public";
106
+ private: "private";
107
+ }>>;
108
+ notes: z.ZodOptional<z.ZodString>;
109
+ confirm: z.ZodOptional<z.ZodBoolean>;
110
+ }, z.core.$strip>;
111
+ export declare const nodeDeployCapability: import("../capability.ts").Capability<unknown, unknown>;
112
+ export declare const nodeUpdateInput: z.ZodObject<{
113
+ nodeId: z.ZodString;
114
+ targetVersion: z.ZodOptional<z.ZodString>;
115
+ confirm: z.ZodOptional<z.ZodBoolean>;
116
+ }, z.core.$strip>;
117
+ export declare const nodeUpdateCapability: import("../capability.ts").Capability<unknown, unknown>;
118
+ export declare const nodeRebootInput: z.ZodObject<{
119
+ instanceId: z.ZodString;
120
+ confirm: z.ZodOptional<z.ZodBoolean>;
121
+ }, z.core.$strip>;
122
+ export declare const nodeHardResetInput: z.ZodObject<{
123
+ instanceId: z.ZodString;
124
+ confirm: z.ZodOptional<z.ZodBoolean>;
125
+ }, z.core.$strip>;
126
+ export declare const nodeRebootCapability: import("../capability.ts").Capability<unknown, unknown>;
127
+ export declare const nodeHardResetCapability: import("../capability.ts").Capability<unknown, unknown>;
128
+ export declare const nodeAdminKeyInput: z.ZodObject<{
129
+ nodeId: z.ZodString;
130
+ generate: z.ZodOptional<z.ZodBoolean>;
131
+ confirm: z.ZodOptional<z.ZodBoolean>;
132
+ }, z.core.$strip>;
133
+ export declare const nodeAdminKeyCapability: import("../capability.ts").Capability<unknown, unknown>;
134
+ export declare const staffFleetCapabilities: import("../capability.ts").Capability<unknown, unknown>[];
@@ -0,0 +1,439 @@
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 nodeTestTriggerInput = z.object({
237
+ nodeId: z.string(),
238
+ burnTimeMinutes: z.number().optional(),
239
+ constraints: z.record(z.string(), z.unknown()).optional(),
240
+ source: z.record(z.string(), z.unknown()).optional(),
241
+ confirm: z.boolean().optional(),
242
+ });
243
+ export const nodeTestTriggerCapability = defineStaffMcpCapability({
244
+ id: "node.test-trigger",
245
+ domain: "fleet",
246
+ roles: ["admin"],
247
+ description: "Internal staff only: start a node job (host checks + GPU suites; optional burn-in minutes). Requires confirm: true.",
248
+ mutation: "preview-confirm",
249
+ http: { method: "POST", path: "/jobs" },
250
+ input: nodeTestTriggerInput,
251
+ execute: async (ctx, input) => {
252
+ const parsed = nodeTestTriggerInput.parse(input);
253
+ let constraints = { ...(parsed.constraints ?? {}) };
254
+ if (parsed.burnTimeMinutes !== undefined) {
255
+ constraints = {
256
+ ...constraints,
257
+ burn_time_minutes: parsed.burnTimeMinutes,
258
+ include_burn_samples: true,
259
+ };
260
+ }
261
+ const body = {
262
+ node_id: parsed.nodeId,
263
+ constraints,
264
+ source: parsed.source ?? { mode: "no_spec" },
265
+ burn_time_minutes: parsed.burnTimeMinutes ?? 0,
266
+ };
267
+ if (!ctx.confirmed)
268
+ return preview("node.test-trigger", body);
269
+ return ctx.fetch("/jobs", { method: "POST", body });
270
+ },
271
+ });
272
+ export const nodeTestResultInput = z.object({
273
+ nodeId: z.string().optional(),
274
+ jobId: z.string(),
275
+ packetId: z.string().optional(),
276
+ });
277
+ export const nodeTestResultCapability = defineStaffMcpCapability({
278
+ id: "node.test-result",
279
+ domain: "fleet",
280
+ roles: ["reviewer", "admin"],
281
+ description: "Internal staff only: fetch a node job by id.",
282
+ mutation: "read",
283
+ http: { method: "GET", path: "/jobs/{jobId}" },
284
+ input: nodeTestResultInput,
285
+ execute: async (ctx, input) => {
286
+ const parsed = nodeTestResultInput.parse(input);
287
+ return ctx.fetch(`/jobs/${seg(parsed.jobId)}`, { method: "GET" });
288
+ },
289
+ });
290
+ export const nodeBenchmarkInput = z.object({ nodeId: z.string() });
291
+ export const nodeBenchmarkCapability = defineStaffMcpCapability({
292
+ id: "node.benchmark",
293
+ domain: "fleet",
294
+ roles: ["reviewer", "admin"],
295
+ description: "Internal staff only: show the current burn-in / benchmark summary for a GPU node.",
296
+ mutation: "read",
297
+ http: { method: "GET", path: "/jobs" },
298
+ input: nodeBenchmarkInput,
299
+ execute: async (ctx, input) => {
300
+ const parsed = nodeBenchmarkInput.parse(input);
301
+ return ctx.fetch(`/jobs?node_id=${seg(parsed.nodeId)}`, { method: "GET" });
302
+ },
303
+ });
304
+ export const nodeDeployInput = z.object({
305
+ nodeId: z.string(),
306
+ targetTenantId: z.string(),
307
+ targetAuthUserId: z.string().optional(),
308
+ networkMode: z.enum(["public", "private"]).optional(),
309
+ notes: z.string().max(2000).optional(),
310
+ confirm: z.boolean().optional(),
311
+ });
312
+ export const nodeDeployCapability = defineStaffMcpCapability({
313
+ id: "node.deploy",
314
+ domain: "fleet",
315
+ roles: ["admin"],
316
+ 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.",
317
+ mutation: "preview-confirm",
318
+ http: { method: "POST", path: "/internal/nodes/{nodeId}/deploy" },
319
+ input: nodeDeployInput,
320
+ execute: async (ctx, input) => {
321
+ const parsed = nodeDeployInput.parse(input);
322
+ const body = {
323
+ target_tenant_id: parsed.targetTenantId,
324
+ ...mapBody(parsed, {
325
+ targetAuthUserId: "target_auth_user_id",
326
+ networkMode: "network_mode",
327
+ notes: "notes",
328
+ }),
329
+ };
330
+ if (!ctx.confirmed)
331
+ return preview("node.deploy", body);
332
+ return ctx.fetch(`/internal/nodes/${seg(parsed.nodeId)}/deploy`, { method: "POST", body });
333
+ },
334
+ });
335
+ export const nodeUpdateInput = z.object({
336
+ nodeId: z.string(),
337
+ targetVersion: z.string().optional(),
338
+ confirm: z.boolean().optional(),
339
+ });
340
+ export const nodeUpdateCapability = defineStaffMcpCapability({
341
+ id: "node.update",
342
+ domain: "fleet",
343
+ roles: ["admin"],
344
+ description: "Internal staff only: write the desired ornn-node version on the host row. Does not accept a force override. Requires confirm: true.",
345
+ mutation: "preview-confirm",
346
+ http: { method: "PATCH", path: "/internal/nodes/{nodeId}" },
347
+ input: nodeUpdateInput,
348
+ execute: async (ctx, input) => {
349
+ const parsed = nodeUpdateInput.parse(input);
350
+ const body = mapBody(parsed, { targetVersion: "desired_agent_version" });
351
+ if (!ctx.confirmed)
352
+ return preview("node.update", body);
353
+ return ctx.fetch(`/internal/nodes/${seg(parsed.nodeId)}`, { method: "PATCH", body });
354
+ },
355
+ });
356
+ const machineRebootInput = z.object({
357
+ instanceId: z.string(),
358
+ confirm: z.boolean().optional(),
359
+ });
360
+ export const nodeRebootInput = machineRebootInput;
361
+ export const nodeHardResetInput = machineRebootInput;
362
+ /**
363
+ * Reboot and hard-reset share one orchestrator endpoint and differ only by
364
+ * `mode`, but they stay separate verbs: "wipe the host" must never be one
365
+ * mistyped argument away from "restart the host".
366
+ */
367
+ function rebootCapability(options) {
368
+ return defineStaffMcpCapability({
369
+ id: options.id,
370
+ domain: "fleet",
371
+ roles: ["admin"],
372
+ description: options.description,
373
+ mutation: "preview-confirm",
374
+ http: { method: "POST", path: "/internal/nodes/{instanceId}/reboot" },
375
+ input: machineRebootInput,
376
+ execute: async (ctx, input) => {
377
+ const parsed = machineRebootInput.parse(input);
378
+ const body = { mode: options.mode };
379
+ if (!ctx.confirmed)
380
+ return preview(options.id, { instanceId: parsed.instanceId, ...body });
381
+ return ctx.fetch(`/internal/nodes/${seg(parsed.instanceId)}/reboot`, {
382
+ method: "POST",
383
+ body,
384
+ });
385
+ },
386
+ });
387
+ }
388
+ export const nodeRebootCapability = rebootCapability({
389
+ id: "node.reboot",
390
+ mode: "reboot",
391
+ 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.",
392
+ });
393
+ export const nodeHardResetCapability = rebootCapability({
394
+ id: "node.hard-reset",
395
+ mode: "hard_reset",
396
+ 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.",
397
+ });
398
+ export const nodeAdminKeyInput = z.object({
399
+ nodeId: z.string(),
400
+ generate: z.boolean().optional(),
401
+ confirm: z.boolean().optional(),
402
+ });
403
+ export const nodeAdminKeyCapability = defineStaffMcpCapability({
404
+ id: "node.admin-key",
405
+ domain: "fleet",
406
+ roles: ["admin"],
407
+ 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.",
408
+ // Reading the stored pair is a GET; minting a new one is the same path as a POST.
409
+ mutation: "preview-confirm",
410
+ http: { method: "GET", path: "/enrollment/admin-ssh-key" },
411
+ input: nodeAdminKeyInput,
412
+ execute: async (ctx, input) => {
413
+ const parsed = nodeAdminKeyInput.parse(input);
414
+ if (parsed.generate && !ctx.confirmed) {
415
+ return preview("node.admin-key", { nodeId: parsed.nodeId, generate: true });
416
+ }
417
+ return ctx.fetch(`/enrollment/admin-ssh-key?node_id=${seg(parsed.nodeId)}`, {
418
+ method: parsed.generate ? "POST" : "GET",
419
+ });
420
+ },
421
+ });
422
+ export const staffFleetCapabilities = [
423
+ enrollmentTokenRevokeCapability,
424
+ operatorCreateCapability,
425
+ operatorUpdateCapability,
426
+ operatorDeleteCapability,
427
+ facilityCreateCapability,
428
+ facilityUpdateCapability,
429
+ nodeShowCapability,
430
+ nodeDetailsUpdateCapability,
431
+ nodeTestTriggerCapability,
432
+ nodeTestResultCapability,
433
+ nodeBenchmarkCapability,
434
+ nodeDeployCapability,
435
+ nodeUpdateCapability,
436
+ nodeRebootCapability,
437
+ nodeHardResetCapability,
438
+ nodeAdminKeyCapability,
439
+ ];
@@ -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
+ }
@@ -71,6 +71,20 @@ export type CapabilitySpec<I = unknown, O = unknown> = Omit<Capability<I, O>, "c
71
71
  };
72
72
  /** Fill CLI/MCP/Slack names from `id`. Slack parameters stay optional. */
73
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;
74
88
  export declare function slackStaffVisible(capability: Pick<Capability, "roles">): boolean;
75
89
  export declare function visibleToClient(capability: Pick<Capability, "roles" | "cli" | "mcp" | "slack">, client: ClientKind, role: Role): boolean;
76
90
  export declare function forRole(catalog: readonly Capability[], role: Role, client: ClientKind): Capability[];
@@ -41,6 +41,20 @@ export function defineCapability(spec) {
41
41
  },
42
42
  };
43
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
+ }
44
58
  export function slackStaffVisible(capability) {
45
59
  return capability.roles.some((role) => role === "reviewer" || role === "admin");
46
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, preview, slackStaffVisible, visibleToClient, type Capability, type CapabilityBindings, type CapabilityContext, type CapabilitySpec, type JsonSchema, type CapabilityTransport, type Domain, type HttpSpec, type Mutation, type PreviewResult, } 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, 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";
@@ -1,4 +1,4 @@
1
- export { byCliCommand, byMcpName, bySlackName, defineCapability, forRole, preview, slackStaffVisible, visibleToClient, } from "./capability.js";
1
+ export { byCliCommand, byMcpName, bySlackName, defineCapability, defineStaffMcpCapability, forRole, preview, slackStaffVisible, visibleToClient, } from "./capability.js";
2
2
  export { namesFromId } from "./names.js";
3
3
  export { catalog } from "./catalog.js";
4
4
  export { fleetCapabilities, listFacilitiesCapability, listNodesCapability, listOperatorsCapability, nodeExecCapability, nodeOffGridCapability, nodeOnGridCapability, nodeTerminateCapability, } from "./capabilities/fleet.js";
@@ -8,5 +8,8 @@ export { createListingCapability, listingCreateInput, listCatalogTermsCapability
8
8
  export { userCapabilities, usersQueryCapability } from "./capabilities/users.js";
9
9
  export { kubernetesCapabilities, slurmCapabilities } from "./capabilities/clusters.js";
10
10
  export { logsLatestCapability, logsTailCapability, observabilityCapabilities, telemetryLatestCapability, telemetryTailCapability, } from "./capabilities/observability.js";
11
+ export { enrollmentTokenRevokeCapability, enrollmentTokenRevokeInput, facilityCreateCapability, facilityCreateInput, facilityUpdateCapability, facilityUpdateInput, nodeAdminKeyCapability, nodeAdminKeyInput, nodeBenchmarkCapability, nodeBenchmarkInput, nodeDeployCapability, nodeDeployInput, nodeDetailsUpdateCapability, nodeDetailsUpdateInput, nodeHardResetCapability, nodeHardResetInput, nodeRebootCapability, nodeRebootInput, nodeShowCapability, nodeShowInput, nodeTestResultCapability, nodeTestResultInput, nodeTestTriggerCapability, nodeTestTriggerInput, nodeUpdateCapability, nodeUpdateInput, operatorCreateCapability, operatorCreateInput, operatorDeleteCapability, operatorDeleteInput, operatorUpdateCapability, operatorUpdateInput, staffFleetCapabilities, } from "./capabilities/staff-fleet.js";
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.js";
13
+ export { sshKeyAddCapability, sshKeyAddInput, sshKeyPushCapability, sshKeyPushInput, sshKeyRevokeCapability, sshKeyRevokeInput, staffAccessCapabilities, } from "./capabilities/staff-access.js";
11
14
  export { CLIENTS, parseRole, roleAtLeast, ROLES } from "./role.js";
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, } from "./spec-catalog.js";