@ornncompute/cli 0.2.23 → 1.0.25
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 +304 -420
- package/package.json +12 -6
- package/src/admin-capacity-reservations.mjs +280 -0
- package/src/admin-command-handlers.mjs +57 -0
- package/src/admin-enrollment.mjs +680 -0
- package/src/admin-mutations.mjs +187 -0
- package/src/admin-node-diagnostics.mjs +412 -0
- package/src/admin-node-lifecycle.mjs +908 -0
- package/src/admin-shell.mjs +322 -0
- package/src/api-client.mjs +81 -28
- package/src/atomic-output.mjs +112 -0
- package/src/auth-store.mjs +182 -11
- package/src/capability-catalog.mjs +7 -0
- package/src/catalog-shell.mjs +1350 -0
- package/src/cli.mjs +123 -10266
- package/src/connect-mode-handlers.mjs +170 -0
- package/src/device-auth.mjs +178 -61
- package/src/evidence-commands.mjs +1340 -0
- package/src/external-bucket-commands.mjs +1374 -0
- package/src/interaction-runtime.mjs +310 -0
- package/src/kubernetes-commands.mjs +2229 -0
- package/src/launch-mode-handlers.mjs +119 -0
- package/src/machine-reference.mjs +277 -0
- package/src/mount-commands.mjs +1146 -0
- package/src/session-commands.mjs +565 -0
- package/src/slurm-commands.mjs +263 -0
- package/src/startup.mjs +34 -11
- package/src/storage-commands.mjs +1144 -0
- package/src/update.mjs +172 -24
- package/src/vm-commands.mjs +4273 -0
- package/src/vpn-secret-output.mjs +150 -0
- package/src/wait-runtime.mjs +231 -0
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -0
- package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
- package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
- package/vendor/capabilities/capabilities/admin.js +89 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +368 -0
- package/vendor/capabilities/capabilities/cli.js +156 -0
- package/vendor/capabilities/capabilities/clusters.js +150 -1
- package/vendor/capabilities/capabilities/compute.js +522 -0
- package/vendor/capabilities/capabilities/evidence.js +145 -0
- package/vendor/capabilities/capabilities/external-buckets.js +191 -0
- package/vendor/capabilities/capabilities/fleet.js +21 -5
- package/vendor/capabilities/capabilities/identity.js +114 -3
- package/vendor/capabilities/capabilities/listings.js +14 -8
- package/vendor/capabilities/capabilities/mounts.js +176 -0
- package/vendor/capabilities/capabilities/networks.js +1257 -0
- package/vendor/capabilities/capabilities/observability.js +38 -8
- package/vendor/capabilities/capabilities/tokens.js +20 -8
- package/vendor/capabilities/capabilities/volumes.js +178 -0
- package/vendor/capabilities/capability.js +19 -13
- package/vendor/capabilities/cli-catalog.js +65 -0
- package/vendor/capabilities/cli-migrations.js +288 -0
- package/vendor/capabilities/cli.js +7 -0
- package/vendor/capabilities/software-catalog.js +36 -0
- package/vendor/capabilities/spec-catalog.js +2 -2
- package/src/catalog-dispatch.mjs +0 -605
- package/src/color.mjs +0 -71
- package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
- package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
- package/vendor/capabilities/capabilities/identity.d.ts +0 -3
- package/vendor/capabilities/capabilities/listings.d.ts +0 -51
- package/vendor/capabilities/capabilities/observability.d.ts +0 -5
- package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
- package/vendor/capabilities/capabilities/staff-access.js +0 -278
- package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
- package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
- package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
- package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
- package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
- package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
- package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
- package/vendor/capabilities/capabilities/staff-shared.js +0 -98
- package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
- package/vendor/capabilities/capabilities/users.d.ts +0 -2
- package/vendor/capabilities/capability.d.ts +0 -93
- package/vendor/capabilities/catalog.d.ts +0 -3
- package/vendor/capabilities/catalog.js +0 -26
- package/vendor/capabilities/index.d.ts +0 -16
- package/vendor/capabilities/index.js +0 -16
- package/vendor/capabilities/names.d.ts +0 -6
- package/vendor/capabilities/role.d.ts +0 -6
- package/vendor/capabilities/role.js +0 -17
- package/vendor/capabilities/spec-catalog.d.ts +0 -78
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
import { buildEnrollmentInstallCommand, tokensCreateCapability, tokensListCapability, } from "./tokens.js";
|
|
4
|
+
const UUID = z.string().uuid();
|
|
5
|
+
const nonEmpty = z.string().trim().min(1);
|
|
6
|
+
const tokenTtl = z
|
|
7
|
+
.number()
|
|
8
|
+
.int()
|
|
9
|
+
.min(60)
|
|
10
|
+
.max(24 * 60 * 60);
|
|
11
|
+
function adminHelp(command, outcome, human, automation, acceptedReferences = [], review) {
|
|
12
|
+
return {
|
|
13
|
+
audience: "admin",
|
|
14
|
+
category: "resource",
|
|
15
|
+
command,
|
|
16
|
+
outcome,
|
|
17
|
+
human,
|
|
18
|
+
automation,
|
|
19
|
+
acceptedReferences,
|
|
20
|
+
...(review ? { review } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function withVnextBinding(capability, help) {
|
|
24
|
+
if (!help.command)
|
|
25
|
+
throw new Error(`${capability.id} requires a canonical CLI command`);
|
|
26
|
+
return {
|
|
27
|
+
...capability,
|
|
28
|
+
cli: {
|
|
29
|
+
command: [...help.command],
|
|
30
|
+
vnext: help,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export const adminEnrollmentTokensListCapability = {
|
|
35
|
+
...withVnextBinding(tokensListCapability, adminHelp(["admin", "enrollment-tokens", "list"], "List enrollment-token metadata without token bytes.", "ornn admin enrollment-tokens list [--limit 50] [--cursor CURSOR]", "ornn admin enrollment-tokens list --limit 50 --format json")),
|
|
36
|
+
execute: async (ctx) => {
|
|
37
|
+
const payload = await tokensListCapability.execute(ctx, {});
|
|
38
|
+
if (!ctx.cli)
|
|
39
|
+
return payload;
|
|
40
|
+
return {
|
|
41
|
+
enrollment_tokens: rowsFromTokenList(payload)
|
|
42
|
+
.map(tokenMetadata)
|
|
43
|
+
.filter((token) => token !== null),
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
export const adminEnrollmentTokenCreateCapability = withVnextBinding(tokensCreateCapability, adminHelp(["admin", "enrollment-tokens", "create"], "Create one expiring enrollment token through an explicit secret export.", "ornn admin enrollment-tokens create --operator UUID --expires-in SECONDS (--print | --output PATH) [--overwrite] [--yes]", "Unsupported: token creation requires an eligible human terminal.", ["canonical operator UUID"], "Review the operator, expiry, and secret destination before minting the token."));
|
|
48
|
+
const tokenRevokeInput = z.object({
|
|
49
|
+
token_id: UUID,
|
|
50
|
+
expected_created_at: nonEmpty.optional(),
|
|
51
|
+
idempotency_key: nonEmpty.optional(),
|
|
52
|
+
});
|
|
53
|
+
function rowsFromTokenList(payload) {
|
|
54
|
+
if (Array.isArray(payload))
|
|
55
|
+
return payload;
|
|
56
|
+
if (!payload || typeof payload !== "object")
|
|
57
|
+
return [];
|
|
58
|
+
const source = payload;
|
|
59
|
+
for (const key of ["enrollment_tokens", "tokens", "items", "data"]) {
|
|
60
|
+
if (Array.isArray(source[key]))
|
|
61
|
+
return source[key];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
function tokenMetadata(value) {
|
|
66
|
+
if (!value || typeof value !== "object")
|
|
67
|
+
return null;
|
|
68
|
+
const row = value;
|
|
69
|
+
if (typeof row.id !== "string" || !row.id)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
id: row.id,
|
|
73
|
+
operator_id: typeof row.operator_id === "string" ? row.operator_id : null,
|
|
74
|
+
status: typeof row.status === "string" ? row.status : "unknown",
|
|
75
|
+
expires_at: typeof row.expires_at === "string" ? row.expires_at : null,
|
|
76
|
+
created_at: typeof row.created_at === "string" ? row.created_at : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function codedError(code, message) {
|
|
80
|
+
const error = new Error(message);
|
|
81
|
+
error.detail = { code, retryable: false };
|
|
82
|
+
return error;
|
|
83
|
+
}
|
|
84
|
+
async function readExactToken(ctx, tokenId, expectedCreatedAt) {
|
|
85
|
+
// Filter at the owner before its list limit; an older active token may not be in the latest 500.
|
|
86
|
+
const listed = await ctx.fetch(`/enrollment-tokens?id=${encodeURIComponent(tokenId)}`, {
|
|
87
|
+
method: "GET",
|
|
88
|
+
});
|
|
89
|
+
const target = rowsFromTokenList(listed)
|
|
90
|
+
.map(tokenMetadata)
|
|
91
|
+
.find((candidate) => candidate?.id === tokenId);
|
|
92
|
+
if (!target)
|
|
93
|
+
return { disposition: "satisfied", target: null };
|
|
94
|
+
if (expectedCreatedAt && target.created_at !== expectedCreatedAt) {
|
|
95
|
+
return {
|
|
96
|
+
disposition: "conflict",
|
|
97
|
+
target,
|
|
98
|
+
message: `Enrollment token ${tokenId} was replaced after ${expectedCreatedAt}; refusing to revoke it.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (target.status === "revoked" || target.status === "expired") {
|
|
102
|
+
return { disposition: "satisfied", target };
|
|
103
|
+
}
|
|
104
|
+
return { disposition: "apply", target };
|
|
105
|
+
}
|
|
106
|
+
export const adminEnrollmentTokenRevokeCapability = defineCapability({
|
|
107
|
+
id: "tokens.revoke",
|
|
108
|
+
domain: "fleet",
|
|
109
|
+
roles: ["admin"],
|
|
110
|
+
description: "Revoke one exact enrollment-token generation after reconciling its durable goal.",
|
|
111
|
+
mutation: "preview-confirm",
|
|
112
|
+
http: { method: "POST", path: "/enrollment/tokens/{id}/revoke" },
|
|
113
|
+
cli: {
|
|
114
|
+
command: ["admin", "enrollment-tokens", "revoke"],
|
|
115
|
+
vnext: adminHelp(["admin", "enrollment-tokens", "revoke"], "Revoke one exact enrollment-token generation.", "ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--yes]", "ornn admin enrollment-tokens revoke <token-id> --expected-created-at TIME --yes --idempotency-key KEY --format json", ["canonical token UUID", "token created_at generation"], "Review the exact token generation and the loss of future enrollment access."),
|
|
116
|
+
},
|
|
117
|
+
mcp: false,
|
|
118
|
+
slack: false,
|
|
119
|
+
input: tokenRevokeInput,
|
|
120
|
+
execute: async (ctx, input) => {
|
|
121
|
+
const parsed = tokenRevokeInput.parse(input);
|
|
122
|
+
const current = await readExactToken(ctx, parsed.token_id, parsed.expected_created_at);
|
|
123
|
+
if (!ctx.confirmed)
|
|
124
|
+
return current;
|
|
125
|
+
if (current.disposition === "conflict") {
|
|
126
|
+
throw codedError("token_identity_changed", current.message);
|
|
127
|
+
}
|
|
128
|
+
if (current.disposition === "satisfied") {
|
|
129
|
+
return {
|
|
130
|
+
id: parsed.token_id,
|
|
131
|
+
created_at: parsed.expected_created_at ?? current.target?.created_at ?? null,
|
|
132
|
+
status: "revoked",
|
|
133
|
+
reconciled: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!current.target.created_at) {
|
|
137
|
+
throw codedError("token_identity_unavailable", `Enrollment token ${parsed.token_id} has no stable creation identity and cannot be revoked safely.`);
|
|
138
|
+
}
|
|
139
|
+
const revoked = (await ctx.fetch(`/enrollment/tokens/${encodeURIComponent(parsed.token_id)}/revoke`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
...(parsed.idempotency_key
|
|
142
|
+
? { headers: { "Idempotency-Key": parsed.idempotency_key } }
|
|
143
|
+
: {}),
|
|
144
|
+
}));
|
|
145
|
+
return {
|
|
146
|
+
id: parsed.token_id,
|
|
147
|
+
created_at: current.target.created_at,
|
|
148
|
+
operator_id: current.target.operator_id,
|
|
149
|
+
expires_at: current.target.expires_at,
|
|
150
|
+
status: "revoked",
|
|
151
|
+
reconciled: false,
|
|
152
|
+
...(typeof revoked?.deleted === "boolean" ? { deleted: revoked.deleted } : {}),
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const nodeEnrollInput = z.object({
|
|
157
|
+
node_id: UUID,
|
|
158
|
+
host: nonEmpty,
|
|
159
|
+
operator_id: UUID,
|
|
160
|
+
facility_id: UUID,
|
|
161
|
+
filter: z.string().optional(),
|
|
162
|
+
identity_file: nonEmpty,
|
|
163
|
+
private_key: z.string().min(1).optional(),
|
|
164
|
+
username: nonEmpty.default("root"),
|
|
165
|
+
port: z.number().int().min(1).max(65535).default(22),
|
|
166
|
+
expires_in_seconds: tokenTtl,
|
|
167
|
+
installer_base: nonEmpty.optional(),
|
|
168
|
+
replace_existing_management: z.literal(true),
|
|
169
|
+
verbose: z.boolean().default(false),
|
|
170
|
+
});
|
|
171
|
+
function safeRemoteResult(value) {
|
|
172
|
+
if (!value || typeof value !== "object")
|
|
173
|
+
return {};
|
|
174
|
+
const row = value;
|
|
175
|
+
const safe = {};
|
|
176
|
+
for (const key of ["id", "status", "state", "exit_code", "started_at", "completed_at"]) {
|
|
177
|
+
const field = row[key];
|
|
178
|
+
if (typeof field === "string" || typeof field === "number" || typeof field === "boolean") {
|
|
179
|
+
safe[key] = field;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return safe;
|
|
183
|
+
}
|
|
184
|
+
export const adminNodeEnrollCapability = defineCapability({
|
|
185
|
+
id: "nodes.enroll",
|
|
186
|
+
domain: "fleet",
|
|
187
|
+
roles: ["admin"],
|
|
188
|
+
description: "Enroll one explicit node over SSH with an explicit operator and facility assignment.",
|
|
189
|
+
mutation: "preview-confirm",
|
|
190
|
+
http: { method: "POST", path: "/internal/remote" },
|
|
191
|
+
cli: {
|
|
192
|
+
command: ["admin", "nodes", "enroll"],
|
|
193
|
+
vnext: adminHelp(["admin", "nodes", "enroll"], "Install Ornn management on one explicitly assigned node.", "ornn admin nodes enroll <node-id> --host HOST --operator UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose] [--yes]", "Unsupported: remote enrollment requires an eligible human terminal.", ["canonical node UUID", "canonical operator UUID", "canonical facility UUID"], "Review node identity, SSH target, assignment, compartment filter, and replacement of existing management."),
|
|
194
|
+
},
|
|
195
|
+
mcp: false,
|
|
196
|
+
slack: false,
|
|
197
|
+
input: nodeEnrollInput,
|
|
198
|
+
execute: async (ctx, input) => {
|
|
199
|
+
const parsed = nodeEnrollInput.parse(input);
|
|
200
|
+
if (!ctx.confirmed) {
|
|
201
|
+
return {
|
|
202
|
+
node_id: parsed.node_id,
|
|
203
|
+
host: parsed.host,
|
|
204
|
+
operator_id: parsed.operator_id,
|
|
205
|
+
facility_id: parsed.facility_id,
|
|
206
|
+
filter: parsed.filter,
|
|
207
|
+
expires_in_seconds: parsed.expires_in_seconds,
|
|
208
|
+
replace_existing_management: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!parsed.private_key) {
|
|
212
|
+
throw codedError("identity_file_unreadable", "The SSH identity file could not be read.");
|
|
213
|
+
}
|
|
214
|
+
const minted = (await ctx.fetch("/enrollment-tokens", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: {
|
|
217
|
+
operator_id: parsed.operator_id,
|
|
218
|
+
expires_in_seconds: parsed.expires_in_seconds,
|
|
219
|
+
},
|
|
220
|
+
}));
|
|
221
|
+
const token = typeof minted.token === "string"
|
|
222
|
+
? minted.token
|
|
223
|
+
: typeof minted.plaintext === "string"
|
|
224
|
+
? minted.plaintext
|
|
225
|
+
: "";
|
|
226
|
+
if (!token) {
|
|
227
|
+
throw codedError("token_material_unavailable", "The enrollment token was created without usable token material.");
|
|
228
|
+
}
|
|
229
|
+
const command = buildEnrollmentInstallCommand({
|
|
230
|
+
token,
|
|
231
|
+
base: parsed.installer_base ?? ctx.installerBaseUrl,
|
|
232
|
+
facilityId: parsed.facility_id,
|
|
233
|
+
filter: parsed.filter,
|
|
234
|
+
restoreNodeId: parsed.node_id,
|
|
235
|
+
force: true,
|
|
236
|
+
});
|
|
237
|
+
const remote = await ctx.fetch("/internal/remote", {
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: {
|
|
240
|
+
host: parsed.host,
|
|
241
|
+
username: parsed.username,
|
|
242
|
+
port: parsed.port,
|
|
243
|
+
private_key: parsed.private_key,
|
|
244
|
+
command,
|
|
245
|
+
timeout_seconds: 600,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
node_id: parsed.node_id,
|
|
250
|
+
host: parsed.host,
|
|
251
|
+
operator_id: parsed.operator_id,
|
|
252
|
+
facility_id: parsed.facility_id,
|
|
253
|
+
filter: parsed.filter,
|
|
254
|
+
management: "replaced",
|
|
255
|
+
token_metadata: {
|
|
256
|
+
id: typeof minted.id === "string" ? minted.id : null,
|
|
257
|
+
expires_at: typeof minted.expires_at === "string" ? minted.expires_at : null,
|
|
258
|
+
},
|
|
259
|
+
remote: safeRemoteResult(remote),
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
const nodeKeyInput = z.object({ node_id: UUID });
|
|
264
|
+
export const adminNodeExportKeyCapability = defineCapability({
|
|
265
|
+
id: "nodes.export-key",
|
|
266
|
+
domain: "fleet",
|
|
267
|
+
roles: ["admin"],
|
|
268
|
+
description: "Export an enrolled node's private admin key only through an explicit secret destination.",
|
|
269
|
+
mutation: "read",
|
|
270
|
+
http: { method: "GET", path: "/enrollment/admin-ssh-key" },
|
|
271
|
+
cli: {
|
|
272
|
+
command: ["admin", "nodes", "export-key"],
|
|
273
|
+
vnext: adminHelp(["admin", "nodes", "export-key"], "Export one node admin key deliberately.", "ornn admin nodes export-key <node-id> (--print | --output PATH) [--overwrite]", "ornn admin nodes export-key <node-id> --output PATH [--overwrite] --format json", ["canonical node UUID"]),
|
|
274
|
+
},
|
|
275
|
+
mcp: false,
|
|
276
|
+
slack: false,
|
|
277
|
+
input: nodeKeyInput,
|
|
278
|
+
execute: async (ctx, input) => {
|
|
279
|
+
const parsed = nodeKeyInput.parse(input);
|
|
280
|
+
return ctx.fetch(`/enrollment/admin-ssh-key?node_id=${encodeURIComponent(parsed.node_id)}`, {
|
|
281
|
+
method: "GET",
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
export const adminNodeRotateKeyCapability = defineCapability({
|
|
286
|
+
id: "nodes.rotate-key",
|
|
287
|
+
domain: "fleet",
|
|
288
|
+
roles: ["admin"],
|
|
289
|
+
description: "Replace a node's admin key and export the new private key explicitly.",
|
|
290
|
+
mutation: "preview-confirm",
|
|
291
|
+
http: { method: "POST", path: "/enrollment/admin-ssh-key" },
|
|
292
|
+
cli: {
|
|
293
|
+
command: ["admin", "nodes", "rotate-key"],
|
|
294
|
+
vnext: adminHelp(["admin", "nodes", "rotate-key"], "Rotate one node admin key and export the replacement deliberately.", "ornn admin nodes rotate-key <node-id> (--print | --output PATH) [--overwrite] [--yes]", "Unsupported: key rotation requires an eligible human terminal.", ["canonical node UUID"], "Review the node and loss of access through the previous private key."),
|
|
295
|
+
},
|
|
296
|
+
mcp: false,
|
|
297
|
+
slack: false,
|
|
298
|
+
input: nodeKeyInput,
|
|
299
|
+
execute: async (ctx, input) => {
|
|
300
|
+
const parsed = nodeKeyInput.parse(input);
|
|
301
|
+
if (!ctx.confirmed)
|
|
302
|
+
return { node_id: parsed.node_id };
|
|
303
|
+
return ctx.fetch(`/enrollment/admin-ssh-key?node_id=${encodeURIComponent(parsed.node_id)}`, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const replacements = {
|
|
309
|
+
"tokens.create": adminEnrollmentTokenCreateCapability,
|
|
310
|
+
"tokens.list": adminEnrollmentTokensListCapability,
|
|
311
|
+
};
|
|
312
|
+
/** Applies the canonical admin CLI bindings without changing MCP or Slack identities. */
|
|
313
|
+
export function bindAdminEnrollmentCapabilities(capabilities) {
|
|
314
|
+
return capabilities.map((capability) => replacements[capability.id] ?? capability);
|
|
315
|
+
}
|
|
316
|
+
export const adminEnrollmentCapabilities = [
|
|
317
|
+
adminEnrollmentTokenRevokeCapability,
|
|
318
|
+
adminNodeEnrollCapability,
|
|
319
|
+
adminNodeExportKeyCapability,
|
|
320
|
+
adminNodeRotateKeyCapability,
|
|
321
|
+
];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
import { nodeExecCapability } from "./fleet.js";
|
|
4
|
+
const nodeId = z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.refine((value) => value === value.trim(), {
|
|
8
|
+
message: "node_id must be an exact canonical node ID without surrounding whitespace",
|
|
9
|
+
});
|
|
10
|
+
export const adminNodeDiagnoseInput = z.object({
|
|
11
|
+
node_id: nodeId,
|
|
12
|
+
});
|
|
13
|
+
export const adminNodeExecInput = z.object({
|
|
14
|
+
node_id: nodeId,
|
|
15
|
+
command: z.string().min(1),
|
|
16
|
+
timeout_seconds: z.number().int().min(1).max(900),
|
|
17
|
+
});
|
|
18
|
+
function adminNodeHelp(command, outcome, human, automation, review) {
|
|
19
|
+
return {
|
|
20
|
+
audience: "admin",
|
|
21
|
+
category: "resource",
|
|
22
|
+
command,
|
|
23
|
+
outcome,
|
|
24
|
+
human,
|
|
25
|
+
automation,
|
|
26
|
+
acceptedReferences: ["canonical node ID"],
|
|
27
|
+
review,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export const adminNodeDiagnoseCapability = defineCapability({
|
|
31
|
+
id: "node.diagnose",
|
|
32
|
+
domain: "fleet",
|
|
33
|
+
roles: ["admin"],
|
|
34
|
+
description: "Queue the staff-authorized diagnosis workflow for one exact node after a live review.",
|
|
35
|
+
mutation: "preview-confirm",
|
|
36
|
+
http: { method: "POST", path: "/jobs" },
|
|
37
|
+
cli: {
|
|
38
|
+
command: ["admin", "nodes", "diagnose"],
|
|
39
|
+
vnext: adminNodeHelp(["admin", "nodes", "diagnose"], "Queue diagnosis for one exact node.", "ornn admin nodes diagnose <node-id>", "Unavailable in machine and redirected modes; use an eligible human terminal.", "Exact node, lifecycle, reachability, and diagnosis workflow."),
|
|
40
|
+
},
|
|
41
|
+
mcp: false,
|
|
42
|
+
slack: false,
|
|
43
|
+
input: adminNodeDiagnoseInput,
|
|
44
|
+
execute: async (ctx, input) => {
|
|
45
|
+
const parsed = adminNodeDiagnoseInput.parse(input);
|
|
46
|
+
if (!ctx.confirmed) {
|
|
47
|
+
return {
|
|
48
|
+
action: "node.diagnose",
|
|
49
|
+
payload: { node_id: parsed.node_id, source: { mode: "cli" }, burn_time_minutes: 0 },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return ctx.fetch("/jobs", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: { node_id: parsed.node_id, source: { mode: "cli" }, burn_time_minutes: 0 },
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
export const adminNodeExecCapability = {
|
|
59
|
+
...nodeExecCapability,
|
|
60
|
+
mutation: "preview-confirm",
|
|
61
|
+
cli: {
|
|
62
|
+
...nodeExecCapability.cli,
|
|
63
|
+
vnext: adminNodeHelp(["admin", "nodes", "exec"], "Execute exact command bytes on one exact node.", "ornn admin nodes exec <node-id> --command COMMAND --timeout DURATION", "Unavailable in machine and redirected modes; use an eligible human terminal.", "Exact node, lifecycle, reachability, command bytes, and timeout."),
|
|
64
|
+
},
|
|
65
|
+
input: adminNodeExecInput,
|
|
66
|
+
execute: async (ctx, input) => {
|
|
67
|
+
const parsed = adminNodeExecInput.parse(input);
|
|
68
|
+
if (ctx.cli && !ctx.confirmed) {
|
|
69
|
+
return {
|
|
70
|
+
action: "node.exec",
|
|
71
|
+
payload: {
|
|
72
|
+
node_id: parsed.node_id,
|
|
73
|
+
command: parsed.command,
|
|
74
|
+
timeout_seconds: parsed.timeout_seconds,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return nodeExecCapability.execute(ctx, {
|
|
79
|
+
node_id: parsed.node_id,
|
|
80
|
+
command: parsed.command,
|
|
81
|
+
timeout_seconds: parsed.timeout_seconds,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export const adminNodeDiagnosticCapabilities = [
|
|
86
|
+
adminNodeDiagnoseCapability,
|
|
87
|
+
adminNodeExecCapability,
|
|
88
|
+
];
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
const nodeReference = z.string().trim().min(1);
|
|
4
|
+
const optionalText = z.string().trim().min(1).optional();
|
|
5
|
+
const commonInput = {
|
|
6
|
+
node_ref: nodeReference,
|
|
7
|
+
node_id: optionalText,
|
|
8
|
+
idempotency_key: optionalText,
|
|
9
|
+
reason: optionalText,
|
|
10
|
+
yes: z.boolean().optional(),
|
|
11
|
+
wait: z.boolean().optional(),
|
|
12
|
+
timeout_seconds: z.number().positive().optional(),
|
|
13
|
+
};
|
|
14
|
+
const rebootInput = z.object({
|
|
15
|
+
...commonInput,
|
|
16
|
+
machine_id: optionalText,
|
|
17
|
+
});
|
|
18
|
+
const hardResetInput = rebootInput.extend({
|
|
19
|
+
erase_local_data: z.literal(true),
|
|
20
|
+
});
|
|
21
|
+
const terminationInput = z.object({
|
|
22
|
+
...commonInput,
|
|
23
|
+
delete_node_record: z.literal(true),
|
|
24
|
+
skip_cleanup: z.boolean().optional(),
|
|
25
|
+
});
|
|
26
|
+
const assignmentInput = z.object({
|
|
27
|
+
...commonInput,
|
|
28
|
+
reservation_id: z.string().trim().min(1),
|
|
29
|
+
});
|
|
30
|
+
const managementInput = z.object(commonInput);
|
|
31
|
+
function adminLifecycleHelp(command, outcome, human, automation, review) {
|
|
32
|
+
return {
|
|
33
|
+
audience: "admin",
|
|
34
|
+
category: "resource",
|
|
35
|
+
command,
|
|
36
|
+
outcome,
|
|
37
|
+
human,
|
|
38
|
+
automation,
|
|
39
|
+
acceptedReferences: ["canonical node ID", "exact unique node hostname in an eligible terminal"],
|
|
40
|
+
review,
|
|
41
|
+
resumableStates: ["accepted", "pending", "timed out", "interrupted"],
|
|
42
|
+
related: ["admin nodes health", "admin nodes list"],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function nodeId(input) {
|
|
46
|
+
if (!input.node_id)
|
|
47
|
+
throw new Error("The lifecycle target must be resolved before execution.");
|
|
48
|
+
return input.node_id;
|
|
49
|
+
}
|
|
50
|
+
function requestHeaders(idempotencyKey) {
|
|
51
|
+
return idempotencyKey ? { "Idempotency-Key": idempotencyKey } : undefined;
|
|
52
|
+
}
|
|
53
|
+
function preview(action, payload) {
|
|
54
|
+
return { ok: true, action, payload };
|
|
55
|
+
}
|
|
56
|
+
async function reboot(ctx, input, mode) {
|
|
57
|
+
const parsed = mode === "hard_reset" ? hardResetInput.parse(input) : rebootInput.parse(input);
|
|
58
|
+
const payload = {
|
|
59
|
+
node_id: parsed.node_id,
|
|
60
|
+
machine_id: parsed.machine_id,
|
|
61
|
+
mode,
|
|
62
|
+
...(mode === "hard_reset" ? { erase_local_data: true } : {}),
|
|
63
|
+
};
|
|
64
|
+
if (!ctx.confirmed)
|
|
65
|
+
return preview(`nodes.${mode}`, payload);
|
|
66
|
+
if (!parsed.machine_id)
|
|
67
|
+
throw new Error("The live machine target must be resolved before reboot.");
|
|
68
|
+
return ctx.fetch(`/nodes/${encodeURIComponent(parsed.machine_id)}/reboot`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: {
|
|
71
|
+
mode,
|
|
72
|
+
...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
|
|
73
|
+
},
|
|
74
|
+
headers: requestHeaders(parsed.idempotency_key),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export const adminNodeRebootCapability = defineCapability({
|
|
78
|
+
id: "nodes.reboot",
|
|
79
|
+
domain: "fleet",
|
|
80
|
+
roles: ["admin"],
|
|
81
|
+
description: "Reboot the exact live machine backing one managed node after a normalized review.",
|
|
82
|
+
mutation: "preview-confirm",
|
|
83
|
+
http: { method: "POST", path: "/nodes/{id}/reboot" },
|
|
84
|
+
cli: {
|
|
85
|
+
command: ["admin", "nodes", "reboot"],
|
|
86
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "reboot"], "Reboot one exact live machine while preserving local data and access configuration.", "ornn admin nodes reboot <node-ref> [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: reboot requires an eligible human terminal.", "Node and live-machine identities, operator, facility, capacity assignment, and preserved access."),
|
|
87
|
+
},
|
|
88
|
+
mcp: false,
|
|
89
|
+
slack: false,
|
|
90
|
+
input: rebootInput,
|
|
91
|
+
execute: (ctx, input) => reboot(ctx, input, "reboot"),
|
|
92
|
+
});
|
|
93
|
+
export const adminNodeHardResetCapability = defineCapability({
|
|
94
|
+
id: "nodes.hard_reset",
|
|
95
|
+
domain: "fleet",
|
|
96
|
+
roles: ["admin"],
|
|
97
|
+
description: "Hard reset the exact live machine backing one node with explicit local-data erasure scope.",
|
|
98
|
+
mutation: "preview-confirm",
|
|
99
|
+
http: { method: "POST", path: "/nodes/{id}/reboot" },
|
|
100
|
+
cli: {
|
|
101
|
+
command: ["admin", "nodes", "hard-reset"],
|
|
102
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "hard-reset"], "Erase local tenant data and hard reset one exact live machine.", "ornn admin nodes hard-reset <node-ref> --erase-local-data [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: hard reset requires an eligible human terminal.", "Node and live-machine identities, operator, facility, capacity assignment, erased local data, and restored keys."),
|
|
103
|
+
},
|
|
104
|
+
mcp: false,
|
|
105
|
+
slack: false,
|
|
106
|
+
input: hardResetInput,
|
|
107
|
+
execute: (ctx, input) => reboot(ctx, input, "hard_reset"),
|
|
108
|
+
});
|
|
109
|
+
export const adminNodeSuspendManagementCapability = defineCapability({
|
|
110
|
+
id: "nodes.suspend_management",
|
|
111
|
+
domain: "fleet",
|
|
112
|
+
roles: ["admin"],
|
|
113
|
+
description: "Suspend Ornn management while leaving the exact host powered on.",
|
|
114
|
+
mutation: "preview-confirm",
|
|
115
|
+
http: { method: "POST", path: "/internal/nodes/{id}/off-grid" },
|
|
116
|
+
cli: {
|
|
117
|
+
command: ["admin", "nodes", "suspend-management"],
|
|
118
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "suspend-management"], "Remove Ornn management and managed access from one powered-on node.", "ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--yes] [--no-wait]", "ornn admin nodes suspend-management <node-id> --yes --idempotency-key KEY --format json", "Node, operator, facility, capacity assignment, management removal, and managed-access loss."),
|
|
119
|
+
},
|
|
120
|
+
mcp: false,
|
|
121
|
+
slack: false,
|
|
122
|
+
input: managementInput,
|
|
123
|
+
execute: async (ctx, input) => {
|
|
124
|
+
const parsed = managementInput.parse(input);
|
|
125
|
+
const id = nodeId(parsed);
|
|
126
|
+
const body = {
|
|
127
|
+
...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
|
|
128
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
129
|
+
};
|
|
130
|
+
if (!ctx.confirmed)
|
|
131
|
+
return preview("nodes.suspend_management", { node_id: id, ...body });
|
|
132
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/off-grid`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
body,
|
|
135
|
+
headers: requestHeaders(parsed.idempotency_key),
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
export const adminNodeResumeManagementCapability = defineCapability({
|
|
140
|
+
id: "nodes.resume_management",
|
|
141
|
+
domain: "fleet",
|
|
142
|
+
roles: ["admin"],
|
|
143
|
+
description: "Resume Ornn management for one exact off-grid node.",
|
|
144
|
+
mutation: "preview-confirm",
|
|
145
|
+
http: { method: "POST", path: "/internal/nodes/{id}/on-grid" },
|
|
146
|
+
cli: {
|
|
147
|
+
command: ["admin", "nodes", "resume-management"],
|
|
148
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "resume-management"], "Restore Ornn management and managed access on one off-grid node.", "ornn admin nodes resume-management <node-ref> [--yes] [--no-wait]", "ornn admin nodes resume-management <node-id> --yes --idempotency-key KEY --format json", "Node, operator, facility, capacity assignment, remote re-enrollment, and restored managed access."),
|
|
149
|
+
},
|
|
150
|
+
mcp: false,
|
|
151
|
+
slack: false,
|
|
152
|
+
input: managementInput,
|
|
153
|
+
execute: async (ctx, input) => {
|
|
154
|
+
const parsed = managementInput.parse(input);
|
|
155
|
+
const id = nodeId(parsed);
|
|
156
|
+
if (!ctx.confirmed)
|
|
157
|
+
return preview("nodes.resume_management", { node_id: id });
|
|
158
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/on-grid`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: {
|
|
161
|
+
...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
|
|
162
|
+
},
|
|
163
|
+
headers: requestHeaders(parsed.idempotency_key),
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
export const adminNodeTerminateCapability = defineCapability({
|
|
168
|
+
id: "nodes.terminate",
|
|
169
|
+
domain: "fleet",
|
|
170
|
+
roles: ["admin"],
|
|
171
|
+
description: "Delete one exact node record with explicit cleanup scope and consequence review.",
|
|
172
|
+
mutation: "preview-confirm",
|
|
173
|
+
http: { method: "POST", path: "/internal/nodes/{id}/terminate" },
|
|
174
|
+
cli: {
|
|
175
|
+
command: ["admin", "nodes", "terminate"],
|
|
176
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "terminate"], "End managed access and permanently delete one exact node record.", "ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--yes]", "ornn admin nodes terminate <node-id> --delete-node-record --yes --idempotency-key KEY --format json", "Node, operator, facility, reservation/capacity, host cleanup, node-record deletion, and access loss."),
|
|
177
|
+
},
|
|
178
|
+
mcp: false,
|
|
179
|
+
slack: false,
|
|
180
|
+
input: terminationInput,
|
|
181
|
+
execute: async (ctx, input) => {
|
|
182
|
+
const parsed = terminationInput.parse(input);
|
|
183
|
+
const id = nodeId(parsed);
|
|
184
|
+
const body = {
|
|
185
|
+
force: parsed.skip_cleanup === true,
|
|
186
|
+
...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
|
|
187
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
188
|
+
};
|
|
189
|
+
if (!ctx.confirmed)
|
|
190
|
+
return preview("nodes.terminate", { node_id: id, ...body });
|
|
191
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/terminate`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
body,
|
|
194
|
+
headers: requestHeaders(parsed.idempotency_key),
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
export const adminNodeAssignCapability = defineCapability({
|
|
199
|
+
id: "nodes.assign",
|
|
200
|
+
domain: "fleet",
|
|
201
|
+
roles: ["admin"],
|
|
202
|
+
description: "Assign one exact node to one exact capacity reservation after consequence review.",
|
|
203
|
+
mutation: "preview-confirm",
|
|
204
|
+
http: { method: "POST", path: "/internal/nodes/{id}/assign" },
|
|
205
|
+
cli: {
|
|
206
|
+
command: ["admin", "nodes", "assign"],
|
|
207
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "assign"], "Assign one exact node to one exact capacity reservation.", "ornn admin nodes assign <node-ref> --reservation <reservation-id> [--yes] [--no-wait]", "ornn admin nodes assign <node-id> --reservation <reservation-id> --yes --idempotency-key KEY --format json", "Node, operator, facility, previous and requested capacity assignments, and tenant access change."),
|
|
208
|
+
},
|
|
209
|
+
mcp: false,
|
|
210
|
+
slack: false,
|
|
211
|
+
input: assignmentInput,
|
|
212
|
+
execute: async (ctx, input) => {
|
|
213
|
+
const parsed = assignmentInput.parse(input);
|
|
214
|
+
const id = nodeId(parsed);
|
|
215
|
+
const body = {
|
|
216
|
+
reservation_id: parsed.reservation_id,
|
|
217
|
+
...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
|
|
218
|
+
};
|
|
219
|
+
if (!ctx.confirmed)
|
|
220
|
+
return preview("nodes.assign", { node_id: id, ...body });
|
|
221
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/assign`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
body,
|
|
224
|
+
headers: requestHeaders(parsed.idempotency_key),
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
export const adminNodeLifecycleCapabilities = [
|
|
229
|
+
adminNodeRebootCapability,
|
|
230
|
+
adminNodeHardResetCapability,
|
|
231
|
+
adminNodeSuspendManagementCapability,
|
|
232
|
+
adminNodeResumeManagementCapability,
|
|
233
|
+
adminNodeTerminateCapability,
|
|
234
|
+
adminNodeAssignCapability,
|
|
235
|
+
];
|