@ornncompute/cli 0.2.2 → 0.2.4
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 +20 -20
- package/package.json +1 -1
- package/src/catalog-dispatch.mjs +155 -24
- package/src/cli.mjs +93 -274
- package/vendor/capabilities/capabilities/fleet.d.ts +4 -1
- package/vendor/capabilities/capabilities/fleet.js +130 -29
- package/vendor/capabilities/capabilities/tokens.d.ts +11 -0
- package/vendor/capabilities/capabilities/tokens.js +155 -0
- package/vendor/capabilities/catalog.js +2 -0
- package/vendor/capabilities/index.d.ts +2 -1
- package/vendor/capabilities/index.js +2 -1
- package/vendor/capabilities/names.js +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ ornn whoami [--json]
|
|
|
44
44
|
ornn status [--json]
|
|
45
45
|
ornn listings list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
|
|
46
46
|
ornn listings show <listing-id> [--open] [--json]
|
|
47
|
+
ornn listings create [--gpu-type <type>] [--node-count <n>] [--yes] [--json]
|
|
47
48
|
ornn buy <listing-id> [--no-open] [--json]
|
|
48
49
|
ornn exchange create <listing-id> --node-count <n> [--min-node-count <n>] --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]
|
|
49
50
|
ornn exchange list [--limit <1-500>] [--cursor <last-id>] [--json]
|
|
@@ -74,11 +75,11 @@ ornn node diagnose <node-id> [--json]
|
|
|
74
75
|
ornn node list [--operator <id-or-slug>] [--facility <id>] [--json]
|
|
75
76
|
ornn node reboot <node-id> [--json]
|
|
76
77
|
ornn node hard-reset <node-id> [--json]
|
|
77
|
-
ornn node off-grid <node-id> [--json]
|
|
78
|
-
ornn node on-grid <node-id> [--json]
|
|
79
|
-
ornn node
|
|
78
|
+
ornn node off-grid <node-id> [--reason <text>] [--yes] [--json]
|
|
79
|
+
ornn node on-grid <node-id> [--yes] [--json]
|
|
80
|
+
ornn node exec <node-id> --command <shell> --timeout-seconds <n> [--yes] [--json]
|
|
81
|
+
ornn node terminate <node-id> [--reason <text>] [--force] [--yes] [--json]
|
|
80
82
|
ornn node admin-key <node-id> [--generate] [--json]
|
|
81
|
-
ornn node deenroll <node-id> [--reason <text>] [--keep-record] [--json]
|
|
82
83
|
ornn reservations commerce list --tenant <email-or-id> [--listing <listing-id>] [--json]
|
|
83
84
|
ornn reservations commerce create --tenant <email> --listing <listing-id> [--node-count <n>] [--node <node-id>] [--start-at <timestamp>] [--end-at <timestamp>] [--price-per-gpu-hour <rate>] [--json]
|
|
84
85
|
ornn reservations deploy <node-id> --target-tenant <id> --commerce-reservation <id> [--target-user <id>] [--network public|private] [--json]
|
|
@@ -243,23 +244,21 @@ path.
|
|
|
243
244
|
- `ornn node hard-reset <node-id>` wipes tenant storage/users and reboots, then
|
|
244
245
|
re-pushes the tenant's keys on reconnect. The tenant keeps ownership of the
|
|
245
246
|
host.
|
|
246
|
-
- `ornn node off-grid <node-id> [--reason <text>]`
|
|
247
|
-
|
|
248
|
-
- `ornn node on-grid <node-id
|
|
249
|
-
stored admin key and re-
|
|
250
|
-
- `ornn node
|
|
251
|
-
|
|
252
|
-
|
|
247
|
+
- `ornn node off-grid <node-id> [--reason <text>] [--yes]` takes a node off-grid
|
|
248
|
+
(remove Ornn agent management, leave the host powered on). Asks y/N unless `--yes`.
|
|
249
|
+
- `ornn node on-grid <node-id> [--yes]` re-enrolls an off-grid host: orchestrator
|
|
250
|
+
SSHs with the stored admin key and re-runs the installer. Asks y/N unless `--yes`.
|
|
251
|
+
- `ornn node exec <node-id> --command <shell> --timeout-seconds <n> [--yes]` runs
|
|
252
|
+
one unrestricted command over orchestrator SSH with the stored admin key. Not
|
|
253
|
+
an interactive shell (`ornn ssh`). Asks y/N unless `--yes`.
|
|
254
|
+
- `ornn node terminate <node-id> [--reason <text>] [--force] [--yes]` best-effort
|
|
255
|
+
cleans the host, removes the Ornn agent, and hard-deletes the node record.
|
|
256
|
+
It does not return the host to a tenant. Asks y/N unless `--yes`. There is no
|
|
257
|
+
`deenroll` verb; use terminate.
|
|
253
258
|
- `ornn node admin-key <node-id> [--generate]` prints the Ornn admin SSH keypair
|
|
254
259
|
for the node (including the private key) so an operator can SSH into the host.
|
|
255
260
|
`--generate` always mints a new pair, stores it, and installs the public key
|
|
256
261
|
on the live host. Treat the output as a secret.
|
|
257
|
-
- `ornn node deenroll <node-id>` gracefully terminates the enrollment (revokes
|
|
258
|
-
the node claim, retires any listing, and queues the node-agent self-uninstall
|
|
259
|
-
packet) while retaining the node row as a `deenrolled` audit record. The
|
|
260
|
-
legacy `--keep-record` flag remains accepted but is now a no-op. Pass
|
|
261
|
-
`--reason <text>` to annotate the audit log. The command is idempotent, and
|
|
262
|
-
termination is forced by default so an unreachable node does not block drain.
|
|
263
262
|
|
|
264
263
|
The operator resource commands share the same reviewer auth:
|
|
265
264
|
|
|
@@ -268,9 +267,10 @@ The operator resource commands share the same reviewer auth:
|
|
|
268
267
|
filtered to one operator.
|
|
269
268
|
- `ornn tokens list` lists enrollment tokens.
|
|
270
269
|
- `ornn tokens create --operator <id> [--facility <id>]
|
|
271
|
-
[--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>]
|
|
272
|
-
|
|
273
|
-
command.
|
|
270
|
+
[--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>] [--host <ip>]
|
|
271
|
+
[--identity-file <path>] [--yes]` mints an enrollment token and prints a
|
|
272
|
+
`curl … | sudo bash` install command. With `--host` and `--identity-file`,
|
|
273
|
+
orchestrator SSHs and runs that curl (same as Bulk Enroll). Asks y/N unless `--yes`.
|
|
274
274
|
- `ornn tokens revoke <token-id>` revokes an enrollment token.
|
|
275
275
|
|
|
276
276
|
Staff Commerce reservations are listing-backed, not fleet-backed:
|
package/package.json
CHANGED
package/src/catalog-dispatch.mjs
CHANGED
|
@@ -2,7 +2,13 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
cliRequest,
|
|
7
|
+
loadRequiredSession,
|
|
8
|
+
operatorRequest,
|
|
9
|
+
resolveApiBaseUrl,
|
|
10
|
+
resolveInstallerBaseUrl,
|
|
11
|
+
} from "./api-client.mjs";
|
|
6
12
|
|
|
7
13
|
async function collectSse(url, headers, durationMs, maxEvents, fetchImpl) {
|
|
8
14
|
const controller = new AbortController();
|
|
@@ -260,29 +266,6 @@ export async function usersQueryCommand(args, context) {
|
|
|
260
266
|
return 0;
|
|
261
267
|
}
|
|
262
268
|
|
|
263
|
-
export async function nodesConsoleCommand(args, context) {
|
|
264
|
-
const { nodesConsoleCapability } = await loadCapabilities();
|
|
265
|
-
const [nodeId, ...rest] = args;
|
|
266
|
-
if (!nodeId) {
|
|
267
|
-
throw new Error("Usage: ornn nodes console <node-id> [--identity-file <path>] [--json]");
|
|
268
|
-
}
|
|
269
|
-
const options = context.parseCommandOptions(
|
|
270
|
-
rest,
|
|
271
|
-
{ boolean: ["json"], value: ["identity-file"] },
|
|
272
|
-
"Usage: ornn nodes console <node-id> [--identity-file <path>] [--json]",
|
|
273
|
-
);
|
|
274
|
-
const result = await runCatalogCapability(
|
|
275
|
-
nodesConsoleCapability,
|
|
276
|
-
{ node_id: nodeId, identity_file: options.identityFile },
|
|
277
|
-
{
|
|
278
|
-
...context,
|
|
279
|
-
role: options.identityFile ? "user" : context.staff ? "reviewer" : "user",
|
|
280
|
-
},
|
|
281
|
-
);
|
|
282
|
-
context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
283
|
-
return 0;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
269
|
export async function telemetryLatestCommand(args, context) {
|
|
287
270
|
const { telemetryLatestCapability } = await loadCapabilities();
|
|
288
271
|
const [nodeId, ...rest] = args;
|
|
@@ -372,3 +355,151 @@ export async function logsTailCommand(args, context) {
|
|
|
372
355
|
context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
373
356
|
return 0;
|
|
374
357
|
}
|
|
358
|
+
|
|
359
|
+
export async function tokensListCommand(args, context) {
|
|
360
|
+
const { tokensListCapability } = await loadCapabilities();
|
|
361
|
+
context.parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn tokens list [--json]");
|
|
362
|
+
const result = await runCatalogCapability(tokensListCapability, {}, {
|
|
363
|
+
...context,
|
|
364
|
+
staff: true,
|
|
365
|
+
role: "reviewer",
|
|
366
|
+
});
|
|
367
|
+
const rows = Array.isArray(result) ? result : (result?.tokens ?? []);
|
|
368
|
+
if (args.includes("--json")) {
|
|
369
|
+
context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
370
|
+
} else if (!rows.length) {
|
|
371
|
+
context.stdout.write("No enrollment tokens found.\n");
|
|
372
|
+
} else {
|
|
373
|
+
for (const token of rows) {
|
|
374
|
+
context.stdout.write(
|
|
375
|
+
`${token?.id ?? "unknown"} ${token?.status ?? ""} operator=${token?.operator_id ?? ""} expires=${token?.expires_at ?? ""}\n`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function tokensCreateCommand(input, context) {
|
|
383
|
+
const { tokensCreateCapability } = await loadCapabilities();
|
|
384
|
+
const preview = await runCatalogCapability(tokensCreateCapability, input, {
|
|
385
|
+
...context,
|
|
386
|
+
confirmed: false,
|
|
387
|
+
staff: true,
|
|
388
|
+
role: "admin",
|
|
389
|
+
});
|
|
390
|
+
if (preview?.ok === false) {
|
|
391
|
+
throw new Error(preview.error ?? "token create failed");
|
|
392
|
+
}
|
|
393
|
+
const confirmed = await confirmCatalogMutation({
|
|
394
|
+
stdin: context.stdin,
|
|
395
|
+
stdout: context.stdout,
|
|
396
|
+
yes: input.yes === true,
|
|
397
|
+
label: input.host || input.hosts?.length ? "Mint token and SSH enroll these hosts?" : "Mint this enrollment token?",
|
|
398
|
+
});
|
|
399
|
+
if (!confirmed) {
|
|
400
|
+
context.stdout.write("Aborted.\n");
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
const result = await runCatalogCapability(tokensCreateCapability, input, {
|
|
404
|
+
...context,
|
|
405
|
+
confirmed: true,
|
|
406
|
+
staff: true,
|
|
407
|
+
role: "admin",
|
|
408
|
+
});
|
|
409
|
+
context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function runNodeLifecycle(capabilityName, input, context, label) {
|
|
414
|
+
const caps = await loadCapabilities();
|
|
415
|
+
const capability = caps[capabilityName];
|
|
416
|
+
const preview = await runCatalogCapability(capability, input, {
|
|
417
|
+
...context,
|
|
418
|
+
confirmed: false,
|
|
419
|
+
staff: true,
|
|
420
|
+
role: "admin",
|
|
421
|
+
});
|
|
422
|
+
if (preview?.ok === false) {
|
|
423
|
+
throw new Error(preview.error ?? `${capabilityName} failed`);
|
|
424
|
+
}
|
|
425
|
+
const confirmed = await confirmCatalogMutation({
|
|
426
|
+
stdin: context.stdin,
|
|
427
|
+
stdout: context.stdout,
|
|
428
|
+
yes: input.yes === true,
|
|
429
|
+
label,
|
|
430
|
+
});
|
|
431
|
+
if (!confirmed) {
|
|
432
|
+
context.stdout.write("Aborted.\n");
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
const result = await runCatalogCapability(capability, input, {
|
|
436
|
+
...context,
|
|
437
|
+
confirmed: true,
|
|
438
|
+
staff: true,
|
|
439
|
+
role: "admin",
|
|
440
|
+
});
|
|
441
|
+
context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export async function nodeOffGridCommand(args, context) {
|
|
446
|
+
const [nodeId, ...rest] = args;
|
|
447
|
+
if (!nodeId) throw new Error("Usage: ornn node off-grid <node-id> [--reason <text>] [--yes] [--json]");
|
|
448
|
+
const options = context.parseCommandOptions(
|
|
449
|
+
rest,
|
|
450
|
+
{ boolean: ["json", "yes"], value: ["reason"] },
|
|
451
|
+
"Usage: ornn node off-grid <node-id> [--reason <text>] [--yes] [--json]",
|
|
452
|
+
);
|
|
453
|
+
return runNodeLifecycle(
|
|
454
|
+
"nodeOffGridCapability",
|
|
455
|
+
{ node_id: nodeId, reason: options.reason, yes: options.yes === true },
|
|
456
|
+
context,
|
|
457
|
+
`Take node ${nodeId} off-grid?`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function nodeOnGridCommand(args, context) {
|
|
462
|
+
const [nodeId, ...rest] = args;
|
|
463
|
+
if (!nodeId) throw new Error("Usage: ornn node on-grid <node-id> [--yes] [--json]");
|
|
464
|
+
const options = context.parseCommandOptions(rest, { boolean: ["json", "yes"] }, "Usage: ornn node on-grid <node-id> [--yes] [--json]");
|
|
465
|
+
return runNodeLifecycle("nodeOnGridCapability", { node_id: nodeId, yes: options.yes === true }, context, `Re-enroll node ${nodeId} on-grid?`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function nodeExecCommand(args, context) {
|
|
469
|
+
const [nodeId, ...rest] = args;
|
|
470
|
+
if (!nodeId) {
|
|
471
|
+
throw new Error("Usage: ornn node exec <node-id> --command <shell> --timeout-seconds <n> [--yes] [--json]");
|
|
472
|
+
}
|
|
473
|
+
const options = context.parseCommandOptions(
|
|
474
|
+
rest,
|
|
475
|
+
{ boolean: ["json", "yes"], value: ["command", "timeout-seconds"] },
|
|
476
|
+
"Usage: ornn node exec <node-id> --command <shell> --timeout-seconds <n> [--yes] [--json]",
|
|
477
|
+
);
|
|
478
|
+
const command = String(options.command ?? "").trim();
|
|
479
|
+
const timeoutSeconds = Number(options.timeoutSeconds);
|
|
480
|
+
if (!command || !Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
481
|
+
throw new Error("Usage: ornn node exec <node-id> --command <shell> --timeout-seconds <n> [--yes] [--json]");
|
|
482
|
+
}
|
|
483
|
+
return runNodeLifecycle(
|
|
484
|
+
"nodeExecCapability",
|
|
485
|
+
{ node_id: nodeId, command, timeout_seconds: timeoutSeconds, yes: options.yes === true },
|
|
486
|
+
context,
|
|
487
|
+
`Run ${JSON.stringify(command)} on node ${nodeId} (${timeoutSeconds}s)?`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function nodeTerminateCommand(args, context) {
|
|
492
|
+
const [nodeId, ...rest] = args;
|
|
493
|
+
if (!nodeId) throw new Error("Usage: ornn node terminate <node-id> [--reason <text>] [--force] [--yes] [--json]");
|
|
494
|
+
const options = context.parseCommandOptions(
|
|
495
|
+
rest,
|
|
496
|
+
{ boolean: ["json", "yes", "force"], value: ["reason"] },
|
|
497
|
+
"Usage: ornn node terminate <node-id> [--reason <text>] [--force] [--yes] [--json]",
|
|
498
|
+
);
|
|
499
|
+
return runNodeLifecycle(
|
|
500
|
+
"nodeTerminateCapability",
|
|
501
|
+
{ node_id: nodeId, reason: options.reason, force: options.force === true, yes: options.yes === true },
|
|
502
|
+
context,
|
|
503
|
+
`Terminate node ${nodeId}?`,
|
|
504
|
+
);
|
|
505
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { createReadStream, createWriteStream, existsSync } from "node:fs";
|
|
2
|
+
import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
4
3
|
import {
|
|
5
|
-
chmod,
|
|
6
|
-
mkdir,
|
|
7
|
-
mkdtemp,
|
|
8
|
-
open,
|
|
9
4
|
readFile,
|
|
10
5
|
rename,
|
|
11
6
|
rm,
|
|
@@ -13,9 +8,8 @@ import {
|
|
|
13
8
|
writeFile,
|
|
14
9
|
} from "node:fs/promises";
|
|
15
10
|
import { createRequire } from "node:module";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
19
13
|
import { Readable } from "node:stream";
|
|
20
14
|
import { pipeline } from "node:stream/promises";
|
|
21
15
|
|
|
@@ -46,10 +40,15 @@ import {
|
|
|
46
40
|
listingsCreateCommand,
|
|
47
41
|
logsLatestCommand,
|
|
48
42
|
logsTailCommand,
|
|
49
|
-
|
|
43
|
+
nodeExecCommand,
|
|
44
|
+
nodeOffGridCommand,
|
|
45
|
+
nodeOnGridCommand,
|
|
46
|
+
nodeTerminateCommand,
|
|
50
47
|
operatorListCommand,
|
|
51
48
|
telemetryLatestCommand,
|
|
52
49
|
telemetryTailCommand,
|
|
50
|
+
tokensCreateCommand,
|
|
51
|
+
tokensListCommand,
|
|
53
52
|
usersQueryCommand,
|
|
54
53
|
} from "./catalog-dispatch.mjs";
|
|
55
54
|
import { maybeNotifyUpdate, updateCommand } from "./update.mjs";
|
|
@@ -138,7 +137,6 @@ Usage:
|
|
|
138
137
|
ornn gpus checkout <gpu-id> [--no-open] [--json]
|
|
139
138
|
ornn nodes list [--json]
|
|
140
139
|
ornn nodes show <node-id> [--json]
|
|
141
|
-
ornn nodes console <node-id> [--identity-file <path>] [--json]
|
|
142
140
|
ornn telemetry latest <node-id> [--span 15m] [--start <iso>] [--end <iso>] [--max-points <n>] [--json]
|
|
143
141
|
ornn telemetry tail <node-id> [--duration <seconds>] [--json]
|
|
144
142
|
ornn logs latest <node-id> [--stream kernel] [--count <n>] [--before <iso>] [--cursor <token>] [--json]
|
|
@@ -153,18 +151,18 @@ Usage:
|
|
|
153
151
|
ornn nodes keys list <node-id> [--json]
|
|
154
152
|
ornn node health <node-id> [--json]
|
|
155
153
|
ornn node diagnose <node-id> [--json]
|
|
156
|
-
ornn node deenroll <node-id> [--reason <text>] [--keep-record] [--json]
|
|
157
154
|
ornn node list [--operator <id-or-slug>] [--facility <id>] [--email <email>] [--json]
|
|
158
155
|
ornn node reboot <node-id> [--json]
|
|
159
156
|
ornn node hard-reset <node-id> [--json]
|
|
160
|
-
ornn node off-grid <node-id> [--reason <text>] [--json]
|
|
161
|
-
ornn node on-grid <node-id> [--json]
|
|
162
|
-
ornn node
|
|
157
|
+
ornn node off-grid <node-id> [--reason <text>] [--yes] [--json]
|
|
158
|
+
ornn node on-grid <node-id> [--yes] [--json]
|
|
159
|
+
ornn node exec <node-id> --command <shell> --timeout-seconds <n> [--yes] [--json]
|
|
160
|
+
ornn node terminate <node-id> [--reason <text>] [--force] [--yes] [--json]
|
|
163
161
|
ornn node admin-key <node-id> [--generate] [--json]
|
|
164
162
|
ornn operators list [--json]
|
|
165
163
|
ornn facilities list [--operator <id-or-slug>] [--json]
|
|
166
164
|
ornn tokens list [--json]
|
|
167
|
-
ornn tokens create --operator <id> [--facility <id>] [--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>] [--
|
|
165
|
+
ornn tokens create --operator <id> [--facility <id>] [--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>] [--host <ip>] [--identity-file <path>] [--yes] [--json]
|
|
168
166
|
ornn tokens revoke <token-id> [--json]
|
|
169
167
|
ornn reservations transfer <reservation-id> --target-tenant <id> [--target-user <id>] [--node <id>] [--strategy reject|park] [--confirm] [--json]
|
|
170
168
|
ornn reservations commerce list --tenant <email-or-id> [--listing <listing-id>] [--json]
|
|
@@ -339,12 +337,12 @@ const HELP_TOPICS = new Set([
|
|
|
339
337
|
const NODE_OP_SUBCOMMANDS = new Set([
|
|
340
338
|
"health",
|
|
341
339
|
"diagnose",
|
|
342
|
-
"deenroll",
|
|
343
340
|
"list",
|
|
344
341
|
"reboot",
|
|
345
342
|
"hard-reset",
|
|
346
343
|
"off-grid",
|
|
347
344
|
"on-grid",
|
|
345
|
+
"exec",
|
|
348
346
|
"terminate",
|
|
349
347
|
"admin-key",
|
|
350
348
|
]);
|
|
@@ -540,11 +538,22 @@ async function dispatch(argv = [], io = {}) {
|
|
|
540
538
|
}
|
|
541
539
|
|
|
542
540
|
if (command === "node") {
|
|
543
|
-
return await nodeOps(args, {
|
|
541
|
+
return await nodeOps(args, {
|
|
542
|
+
env,
|
|
543
|
+
fetchImpl,
|
|
544
|
+
parseCommandOptions,
|
|
545
|
+
stdin: io.stdin ?? process.stdin,
|
|
546
|
+
stdout,
|
|
547
|
+
});
|
|
544
548
|
}
|
|
545
549
|
|
|
546
550
|
if (OPERATOR_COMMANDS.has(command)) {
|
|
547
|
-
return await operatorResource(command, args, {
|
|
551
|
+
return await operatorResource(command, args, {
|
|
552
|
+
env,
|
|
553
|
+
fetchImpl,
|
|
554
|
+
stdout,
|
|
555
|
+
stdin: io.stdin ?? process.stdin,
|
|
556
|
+
});
|
|
548
557
|
}
|
|
549
558
|
|
|
550
559
|
if (command === "nodes") {
|
|
@@ -855,15 +864,24 @@ function validateHelpInvocation(command, args) {
|
|
|
855
864
|
|
|
856
865
|
if (command === "node") {
|
|
857
866
|
const parsed = parseHelpArgs(args, {
|
|
858
|
-
booleanOptions: ["--json", "--keep-record", "--force"],
|
|
859
|
-
valueOptions: [
|
|
867
|
+
booleanOptions: ["--json", "--keep-record", "--force", "--yes"],
|
|
868
|
+
valueOptions: [
|
|
869
|
+
"--reason",
|
|
870
|
+
"--operator",
|
|
871
|
+
"--facility",
|
|
872
|
+
"--email",
|
|
873
|
+
"--ssh-username",
|
|
874
|
+
"--ssh-port",
|
|
875
|
+
"--command",
|
|
876
|
+
"--timeout-seconds",
|
|
877
|
+
],
|
|
860
878
|
});
|
|
861
879
|
if (parsed.error) {
|
|
862
880
|
return parsed.error;
|
|
863
881
|
}
|
|
864
882
|
const [subcommand, , ...extra] = parsed.positionals;
|
|
865
883
|
if (!subcommand || !NODE_OP_SUBCOMMANDS.has(subcommand)) {
|
|
866
|
-
return "Usage: ornn node list|health|diagnose|reboot|hard-reset|off-grid|on-grid|terminate|admin-key
|
|
884
|
+
return "Usage: ornn node list|health|diagnose|reboot|hard-reset|off-grid|on-grid|exec|terminate|admin-key <node-id>";
|
|
867
885
|
}
|
|
868
886
|
return extra.length ? nodeOpUsage(subcommand) : null;
|
|
869
887
|
}
|
|
@@ -908,7 +926,7 @@ function validateHelpInvocation(command, args) {
|
|
|
908
926
|
return !id ? null : "Usage: ornn nodes keys list|attach|push|status <node-id>";
|
|
909
927
|
}
|
|
910
928
|
if (subcommand === "console") {
|
|
911
|
-
return
|
|
929
|
+
return "ornn nodes console is removed. Use `ornn ssh <node-id>` for an interactive session.";
|
|
912
930
|
}
|
|
913
931
|
if (["show", "wait", "reboot", "hard-reset", "ssh-command"].includes(subcommand)) {
|
|
914
932
|
return nested || extra.length
|
|
@@ -920,7 +938,7 @@ function validateHelpInvocation(command, args) {
|
|
|
920
938
|
? `Usage: ornn nodes ${subcommand} <reservation-id> --key <path|id|label>`
|
|
921
939
|
: null;
|
|
922
940
|
}
|
|
923
|
-
return "Usage: ornn nodes list|show|
|
|
941
|
+
return "Usage: ornn nodes list|show|launch|switch|wait|reboot|hard-reset|ssh-command|keys";
|
|
924
942
|
}
|
|
925
943
|
|
|
926
944
|
if (command === "ssh") {
|
|
@@ -1338,8 +1356,8 @@ function validateHelpInvocation(command, args) {
|
|
|
1338
1356
|
// Operator resource commands accept a small, permissive option set; the
|
|
1339
1357
|
// handlers do the strict validation. Just surface parse errors here.
|
|
1340
1358
|
const parsed = parseHelpArgs(args, {
|
|
1341
|
-
booleanOptions: ["--json", "--confirm", "--force"],
|
|
1342
|
-
valueOptions: ["--operator", "--facility", "--expires-in", "--mode", "--ip"],
|
|
1359
|
+
booleanOptions: ["--json", "--confirm", "--force", "--yes"],
|
|
1360
|
+
valueOptions: ["--operator", "--facility", "--expires-in", "--mode", "--ip", "--host", "--identity-file"],
|
|
1343
1361
|
});
|
|
1344
1362
|
return parsed.error || null;
|
|
1345
1363
|
}
|
|
@@ -2085,7 +2103,7 @@ async function reservations(args, context) {
|
|
|
2085
2103
|
// Operator-only surface (`ornn node`); distinct from tenant `ornn nodes`.
|
|
2086
2104
|
async function nodeOps(args, context) {
|
|
2087
2105
|
const usage =
|
|
2088
|
-
"Usage: ornn node list|health|diagnose|reboot|hard-reset|off-grid|on-grid|terminate|admin-key
|
|
2106
|
+
"Usage: ornn node list|health|diagnose|reboot|hard-reset|off-grid|on-grid|exec|terminate|admin-key <node-id>";
|
|
2089
2107
|
const [subcommand, id, ...rest] = args;
|
|
2090
2108
|
if (!subcommand || !NODE_OP_SUBCOMMANDS.has(subcommand)) {
|
|
2091
2109
|
throw new Error(usage);
|
|
@@ -2102,24 +2120,24 @@ async function nodeOps(args, context) {
|
|
|
2102
2120
|
throw new Error(nodeOpUsage(subcommand));
|
|
2103
2121
|
}
|
|
2104
2122
|
|
|
2105
|
-
if (subcommand === "deenroll") {
|
|
2106
|
-
return await nodeDeenroll(id, rest, context);
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
2123
|
if (subcommand === "reboot" || subcommand === "hard-reset") {
|
|
2110
2124
|
return await nodeRebootOp(subcommand, id, rest, context);
|
|
2111
2125
|
}
|
|
2112
2126
|
|
|
2113
2127
|
if (subcommand === "off-grid") {
|
|
2114
|
-
return await
|
|
2128
|
+
return await nodeOffGridCommand([id, ...rest], context);
|
|
2115
2129
|
}
|
|
2116
2130
|
|
|
2117
2131
|
if (subcommand === "on-grid") {
|
|
2118
|
-
return await
|
|
2132
|
+
return await nodeOnGridCommand([id, ...rest], context);
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
if (subcommand === "exec") {
|
|
2136
|
+
return await nodeExecCommand([id, ...rest], context);
|
|
2119
2137
|
}
|
|
2120
2138
|
|
|
2121
2139
|
if (subcommand === "terminate") {
|
|
2122
|
-
return await
|
|
2140
|
+
return await nodeTerminateCommand([id, ...rest], context);
|
|
2123
2141
|
}
|
|
2124
2142
|
|
|
2125
2143
|
if (subcommand === "admin-key") {
|
|
@@ -2164,20 +2182,20 @@ async function nodeOps(args, context) {
|
|
|
2164
2182
|
}
|
|
2165
2183
|
|
|
2166
2184
|
function nodeOpUsage(subcommand) {
|
|
2167
|
-
if (subcommand === "deenroll") {
|
|
2168
|
-
return "Usage: ornn node deenroll <node-id> [--reason <text>] [--keep-record] [--json]";
|
|
2169
|
-
}
|
|
2170
2185
|
if (subcommand === "list") {
|
|
2171
2186
|
return "Usage: ornn node list [--operator <id-or-slug>] [--facility <id>] [--email <email>] [--json]";
|
|
2172
2187
|
}
|
|
2173
2188
|
if (subcommand === "off-grid") {
|
|
2174
|
-
return "Usage: ornn node off-grid <node-id> [--reason <text>] [--json]";
|
|
2189
|
+
return "Usage: ornn node off-grid <node-id> [--reason <text>] [--yes] [--json]";
|
|
2175
2190
|
}
|
|
2176
2191
|
if (subcommand === "on-grid") {
|
|
2177
|
-
return "Usage: ornn node on-grid <node-id> [--json]";
|
|
2192
|
+
return "Usage: ornn node on-grid <node-id> [--yes] [--json]";
|
|
2193
|
+
}
|
|
2194
|
+
if (subcommand === "exec") {
|
|
2195
|
+
return "Usage: ornn node exec <node-id> --command <shell> --timeout-seconds <n> [--yes] [--json]";
|
|
2178
2196
|
}
|
|
2179
2197
|
if (subcommand === "terminate") {
|
|
2180
|
-
return "Usage: ornn node terminate <node-id> [--reason <text>] [--force] [--json]";
|
|
2198
|
+
return "Usage: ornn node terminate <node-id> [--reason <text>] [--force] [--yes] [--json]";
|
|
2181
2199
|
}
|
|
2182
2200
|
if (subcommand === "admin-key") {
|
|
2183
2201
|
return "Usage: ornn node admin-key <node-id> [--generate] [--json]";
|
|
@@ -2260,98 +2278,6 @@ async function nodeRebootOp(subcommand, id, rest, context) {
|
|
|
2260
2278
|
return 0;
|
|
2261
2279
|
}
|
|
2262
2280
|
|
|
2263
|
-
// Take a node off-grid: queue the agentless handoff that removes Ornn agent
|
|
2264
|
-
// management from the still-powered-on host.
|
|
2265
|
-
async function nodeOffGridOp(id, rest, context) {
|
|
2266
|
-
const options = parseCommandOptions(
|
|
2267
|
-
rest,
|
|
2268
|
-
{ boolean: ["json"], value: ["reason"] },
|
|
2269
|
-
nodeOpUsage("off-grid")
|
|
2270
|
-
);
|
|
2271
|
-
const reason = optionalStringOption(options.reason);
|
|
2272
|
-
const result = await operatorRequest({
|
|
2273
|
-
body: {
|
|
2274
|
-
request_id: `cli-off-grid:${id}:${Date.now()}`,
|
|
2275
|
-
...(reason ? { reason } : {}),
|
|
2276
|
-
},
|
|
2277
|
-
endpoint: `/enrollment/nodes/${encodeURIComponent(id)}/agentless-handoff`,
|
|
2278
|
-
env: context.env,
|
|
2279
|
-
fetchImpl: context.fetchImpl,
|
|
2280
|
-
method: "POST",
|
|
2281
|
-
});
|
|
2282
|
-
if (options.json) {
|
|
2283
|
-
writeJson(context.stdout, result);
|
|
2284
|
-
} else {
|
|
2285
|
-
context.stdout.write(`Off-grid handoff queued for node ${id}.\n`);
|
|
2286
|
-
writeOptionalStatusLine(context.stdout, "Status", result?.status);
|
|
2287
|
-
writeOptionalStatusLine(context.stdout, "Request", result?.packet_request_id);
|
|
2288
|
-
}
|
|
2289
|
-
return 0;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
// Bring an off-grid node back on-grid: SSH in with the stored admin key and
|
|
2293
|
-
// re-run the installer to restore Ornn agent management.
|
|
2294
|
-
async function nodeOnGridOp(id, rest, context) {
|
|
2295
|
-
const options = parseCommandOptions(
|
|
2296
|
-
rest,
|
|
2297
|
-
{ boolean: ["json"], value: ["ssh-username", "ssh-port"] },
|
|
2298
|
-
nodeOpUsage("on-grid")
|
|
2299
|
-
);
|
|
2300
|
-
const body = {};
|
|
2301
|
-
const sshUsername = optionalStringOption(options.sshUsername);
|
|
2302
|
-
const sshPort = optionalStringOption(options.sshPort);
|
|
2303
|
-
if (sshUsername) {
|
|
2304
|
-
body.ssh_username = sshUsername;
|
|
2305
|
-
}
|
|
2306
|
-
if (sshPort) {
|
|
2307
|
-
body.ssh_port = Number.parseInt(sshPort, 10);
|
|
2308
|
-
}
|
|
2309
|
-
const result = await operatorRequest({
|
|
2310
|
-
body,
|
|
2311
|
-
endpoint: `/enrollment/nodes/${encodeURIComponent(id)}/agentless-handoff/on-grid`,
|
|
2312
|
-
env: context.env,
|
|
2313
|
-
fetchImpl: context.fetchImpl,
|
|
2314
|
-
method: "POST",
|
|
2315
|
-
});
|
|
2316
|
-
if (options.json) {
|
|
2317
|
-
writeJson(context.stdout, result);
|
|
2318
|
-
} else {
|
|
2319
|
-
context.stdout.write(`On-grid restore queued for node ${id}.\n`);
|
|
2320
|
-
writeOptionalStatusLine(context.stdout, "Status", result?.status);
|
|
2321
|
-
}
|
|
2322
|
-
return 0;
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
// Terminate a node: best-effort clean the host, remove the Ornn agent, and
|
|
2326
|
-
// hard-delete the node record from the platform. Does NOT return it to a tenant.
|
|
2327
|
-
async function nodeTerminateOp(id, rest, context) {
|
|
2328
|
-
const options = parseCommandOptions(
|
|
2329
|
-
rest,
|
|
2330
|
-
{ boolean: ["json", "force"], value: ["reason"] },
|
|
2331
|
-
nodeOpUsage("terminate")
|
|
2332
|
-
);
|
|
2333
|
-
const reason = optionalStringOption(options.reason) || "cli-terminate";
|
|
2334
|
-
const result = await operatorRequest({
|
|
2335
|
-
body: {
|
|
2336
|
-
request_id: `cli-terminate:${id}:${Date.now()}`,
|
|
2337
|
-
reason,
|
|
2338
|
-
force: options.force === true,
|
|
2339
|
-
},
|
|
2340
|
-
endpoint: `/enrollment/nodes/${encodeURIComponent(id)}/terminate`,
|
|
2341
|
-
env: context.env,
|
|
2342
|
-
fetchImpl: context.fetchImpl,
|
|
2343
|
-
method: "POST",
|
|
2344
|
-
});
|
|
2345
|
-
if (options.json) {
|
|
2346
|
-
writeJson(context.stdout, result);
|
|
2347
|
-
} else {
|
|
2348
|
-
context.stdout.write(`Termination queued for node ${id}.\n`);
|
|
2349
|
-
writeOptionalStatusLine(context.stdout, "Status", result?.status);
|
|
2350
|
-
writeOptionalStatusLine(context.stdout, "Request", result?.packet_request_id);
|
|
2351
|
-
}
|
|
2352
|
-
return 0;
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
2281
|
// Fetch the Ornn admin SSH keypair for a node, including the private key, so an
|
|
2356
2282
|
// operator can SSH into the host directly. Prints the private key; handle it as
|
|
2357
2283
|
// a secret.
|
|
@@ -2539,39 +2465,21 @@ async function tokensCommand(args, context) {
|
|
|
2539
2465
|
const usage = "Usage: ornn tokens list|create|revoke ...";
|
|
2540
2466
|
const [subcommand, ...rest] = args;
|
|
2541
2467
|
if (subcommand === "list" || subcommand === undefined) {
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
);
|
|
2547
|
-
const tokens = await operatorRequest({
|
|
2548
|
-
endpoint: "/enrollment/tokens",
|
|
2549
|
-
env: context.env,
|
|
2550
|
-
fetchImpl: context.fetchImpl,
|
|
2468
|
+
return await tokensListCommand(rest, {
|
|
2469
|
+
...context,
|
|
2470
|
+
parseCommandOptions,
|
|
2471
|
+
stdin: context.stdin,
|
|
2551
2472
|
});
|
|
2552
|
-
const rows = Array.isArray(tokens) ? tokens : [];
|
|
2553
|
-
if (options.json) {
|
|
2554
|
-
writeJson(context.stdout, rows);
|
|
2555
|
-
} else if (!rows.length) {
|
|
2556
|
-
context.stdout.write("No enrollment tokens found.\n");
|
|
2557
|
-
} else {
|
|
2558
|
-
for (const token of rows) {
|
|
2559
|
-
context.stdout.write(
|
|
2560
|
-
`${token?.id ?? "unknown"} ${token?.status ?? ""} operator=${token?.operator_id ?? ""} expires=${token?.expires_at ?? ""}\n`
|
|
2561
|
-
);
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
return 0;
|
|
2565
2473
|
}
|
|
2566
2474
|
|
|
2567
2475
|
if (subcommand === "create") {
|
|
2568
2476
|
const createUsage =
|
|
2569
|
-
"Usage: ornn tokens create --operator <id-or-slug> [--facility <id>] [--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>] [--
|
|
2477
|
+
"Usage: ornn tokens create --operator <id-or-slug> [--facility <id>] [--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>] [--host <ip>] [--identity-file <path>] [--yes] [--json]";
|
|
2570
2478
|
const options = parseCommandOptions(
|
|
2571
2479
|
rest,
|
|
2572
2480
|
{
|
|
2573
|
-
boolean: ["json", "force"],
|
|
2574
|
-
value: ["operator", "facility", "expires-in", "mode", "ip"],
|
|
2481
|
+
boolean: ["json", "force", "yes"],
|
|
2482
|
+
value: ["operator", "facility", "expires-in", "mode", "ip", "host", "identity-file"],
|
|
2575
2483
|
},
|
|
2576
2484
|
createUsage
|
|
2577
2485
|
);
|
|
@@ -2580,44 +2488,33 @@ async function tokensCommand(args, context) {
|
|
|
2580
2488
|
throw new Error(createUsage);
|
|
2581
2489
|
}
|
|
2582
2490
|
const operatorId = await resolveOperatorId(operatorFilter, context);
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
body.facility_id = facility;
|
|
2491
|
+
const identityFile = optionalStringOption(options.identityFile);
|
|
2492
|
+
let privateKey;
|
|
2493
|
+
if (identityFile) {
|
|
2494
|
+
privateKey = readFileSync(identityFile, "utf8");
|
|
2588
2495
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
writeOptionalStatusLine(context.stdout, "Expires", token?.expires_at);
|
|
2612
|
-
if (installCommand) {
|
|
2613
|
-
context.stdout.write(`Install command:\n${installCommand}\n`);
|
|
2614
|
-
} else {
|
|
2615
|
-
context.stdout.write(
|
|
2616
|
-
"Install command unavailable: set ORNN_INSTALLER_URL to the origin serving /enrollment/install.\n"
|
|
2617
|
-
);
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
return 0;
|
|
2496
|
+
return await tokensCreateCommand(
|
|
2497
|
+
{
|
|
2498
|
+
operator_id: operatorId,
|
|
2499
|
+
...(optionalStringOption(options.facility)
|
|
2500
|
+
? { facility_id: optionalStringOption(options.facility) }
|
|
2501
|
+
: {}),
|
|
2502
|
+
...(optionalStringOption(options.expiresIn)
|
|
2503
|
+
? { expires_in_seconds: Number.parseInt(optionalStringOption(options.expiresIn), 10) }
|
|
2504
|
+
: {}),
|
|
2505
|
+
...(optionalStringOption(options.mode) ? { node_mode: optionalStringOption(options.mode) } : {}),
|
|
2506
|
+
...(optionalStringOption(options.ip) ? { ip: optionalStringOption(options.ip) } : {}),
|
|
2507
|
+
...(optionalStringOption(options.host) ? { host: optionalStringOption(options.host) } : {}),
|
|
2508
|
+
...(privateKey ? { private_key: privateKey } : {}),
|
|
2509
|
+
installer_base: resolveInstallerBaseUrl({ env: context.env }) ?? undefined,
|
|
2510
|
+
yes: options.yes === true || options.force === true,
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
...context,
|
|
2514
|
+
parseCommandOptions,
|
|
2515
|
+
stdin: context.stdin ?? process.stdin,
|
|
2516
|
+
},
|
|
2517
|
+
);
|
|
2621
2518
|
}
|
|
2622
2519
|
|
|
2623
2520
|
if (subcommand === "revoke") {
|
|
@@ -2644,78 +2541,6 @@ async function tokensCommand(args, context) {
|
|
|
2644
2541
|
throw new Error(usage);
|
|
2645
2542
|
}
|
|
2646
2543
|
|
|
2647
|
-
// Mirrors apps/web/lib/operator-enrollment.ts and the MCP token tool.
|
|
2648
|
-
function buildEnrollmentInstallCommand({ env = process.env, token, mode, ip, force = false } = {}) {
|
|
2649
|
-
if (!token) {
|
|
2650
|
-
return null;
|
|
2651
|
-
}
|
|
2652
|
-
const base = resolveInstallerBaseUrl({ env });
|
|
2653
|
-
if (!base) {
|
|
2654
|
-
return null;
|
|
2655
|
-
}
|
|
2656
|
-
const url = new URL(`${base.replace(/\/+$/, "")}/enrollment/install`);
|
|
2657
|
-
url.searchParams.set("token", token);
|
|
2658
|
-
if (mode) {
|
|
2659
|
-
url.searchParams.set("node_mode", mode);
|
|
2660
|
-
}
|
|
2661
|
-
if (ip) {
|
|
2662
|
-
url.searchParams.set("ip", ip);
|
|
2663
|
-
}
|
|
2664
|
-
if (force) {
|
|
2665
|
-
url.searchParams.set("force", "true");
|
|
2666
|
-
}
|
|
2667
|
-
return `curl -fsSL '${url.toString().replace(/'/g, "'\\''")}' | sudo bash`;
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
// De-enroll a node by gracefully terminating its enrollment. The audit row is
|
|
2671
|
-
// intentionally retained; --keep-record remains accepted for script compatibility.
|
|
2672
|
-
async function nodeDeenroll(id, rest, context) {
|
|
2673
|
-
const options = parseCommandOptions(
|
|
2674
|
-
rest,
|
|
2675
|
-
{ boolean: ["json", "keep-record", "force"], value: ["reason"] },
|
|
2676
|
-
nodeOpUsage("deenroll")
|
|
2677
|
-
);
|
|
2678
|
-
const reason = optionalStringOption(options.reason) || "cli-deenroll";
|
|
2679
|
-
// Dead/unreachable test nodes can't drain workloads, so default to force.
|
|
2680
|
-
const force = options.force !== false;
|
|
2681
|
-
const result = { node_id: id, terminated: false, record_retained: true };
|
|
2682
|
-
|
|
2683
|
-
try {
|
|
2684
|
-
const terminate = await operatorRequest({
|
|
2685
|
-
body: {
|
|
2686
|
-
request_id: `cli-deenroll:${id}:${Date.now()}`,
|
|
2687
|
-
reason,
|
|
2688
|
-
force,
|
|
2689
|
-
},
|
|
2690
|
-
endpoint: `/enrollment/nodes/${encodeURIComponent(id)}/terminate`,
|
|
2691
|
-
env: context.env,
|
|
2692
|
-
fetchImpl: context.fetchImpl,
|
|
2693
|
-
method: "POST",
|
|
2694
|
-
});
|
|
2695
|
-
result.terminated = true;
|
|
2696
|
-
result.terminate = terminate;
|
|
2697
|
-
} catch (error) {
|
|
2698
|
-
// Already-terminated / unknown node is a success for an idempotent teardown.
|
|
2699
|
-
if (!(error instanceof CliApiError) || error.status !== 404) {
|
|
2700
|
-
throw error;
|
|
2701
|
-
}
|
|
2702
|
-
result.already_gone = true;
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
if (options.json) {
|
|
2706
|
-
writeJson(context.stdout, result);
|
|
2707
|
-
} else {
|
|
2708
|
-
if (result.already_gone && !result.terminated) {
|
|
2709
|
-
context.stdout.write(`Node ${id} already de-enrolled (nothing to remove).\n`);
|
|
2710
|
-
} else {
|
|
2711
|
-
context.stdout.write(`Node ${id} de-enrolled.\n`);
|
|
2712
|
-
writeOptionalStatusLine(context.stdout, "Terminated", result.terminated ? "yes" : "skipped");
|
|
2713
|
-
writeOptionalStatusLine(context.stdout, "Record retained", "yes");
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
return 0;
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
2544
|
function writeNodeHealth(stdout, node) {
|
|
2720
2545
|
stdout.write(`${node?.id ?? "unknown"}\n`);
|
|
2721
2546
|
stdout.write(`Status: ${node?.status || "unknown"}\n`);
|
|
@@ -2768,8 +2593,8 @@ async function nodes(args, context) {
|
|
|
2768
2593
|
return 0;
|
|
2769
2594
|
}
|
|
2770
2595
|
|
|
2771
|
-
if (subcommand === "console"
|
|
2772
|
-
|
|
2596
|
+
if (subcommand === "console") {
|
|
2597
|
+
throw new Error("ornn nodes console is removed. Use `ornn ssh <node-id>` for an interactive session.");
|
|
2773
2598
|
}
|
|
2774
2599
|
|
|
2775
2600
|
if (subcommand === "show" && id) {
|
|
@@ -8690,12 +8515,6 @@ function validateMinNodeCount(minNodeCount, nodeCount) {
|
|
|
8690
8515
|
}
|
|
8691
8516
|
}
|
|
8692
8517
|
|
|
8693
|
-
function validateMinGpuCount(minGpuCount, gpuCount) {
|
|
8694
|
-
if (minGpuCount > gpuCount) {
|
|
8695
|
-
throw new Error("--min-gpu-count must be less than or equal to --gpu-count.");
|
|
8696
|
-
}
|
|
8697
|
-
}
|
|
8698
|
-
|
|
8699
8518
|
function arrayOption(value) {
|
|
8700
8519
|
if (value === undefined || value === null || value === "") {
|
|
8701
8520
|
return [];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare const listOperatorsCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
2
2
|
export declare const listFacilitiesCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
3
3
|
export declare const listNodesCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const nodeOffGridCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
5
|
+
export declare const nodeOnGridCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
6
|
+
export declare const nodeExecCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
7
|
+
export declare const nodeTerminateCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
5
8
|
export declare const fleetCapabilities: import("../capability.ts").Capability<unknown, unknown>[];
|
|
@@ -22,12 +22,23 @@ const nodesInput = z.object({
|
|
|
22
22
|
status: z.string().optional(),
|
|
23
23
|
email: z.string().optional(),
|
|
24
24
|
});
|
|
25
|
-
const
|
|
25
|
+
const nodeLifecycleInput = z.object({
|
|
26
26
|
node_id: z.string().min(1),
|
|
27
27
|
nodeId: z.string().optional(),
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
reason: z.string().optional(),
|
|
29
|
+
force: z.boolean().optional(),
|
|
30
30
|
});
|
|
31
|
+
const nodeExecInput = z.object({
|
|
32
|
+
node_id: z.string().min(1),
|
|
33
|
+
nodeId: z.string().optional(),
|
|
34
|
+
command: z.string().min(1),
|
|
35
|
+
timeout_seconds: z.number().int().positive(),
|
|
36
|
+
timeoutSeconds: z.number().int().positive().optional(),
|
|
37
|
+
yes: z.boolean().optional(),
|
|
38
|
+
});
|
|
39
|
+
function nodeIdOf(input) {
|
|
40
|
+
return (input.node_id || input.nodeId || "").trim();
|
|
41
|
+
}
|
|
31
42
|
export const listOperatorsCapability = defineCapability({
|
|
32
43
|
id: "operator.list",
|
|
33
44
|
domain: "fleet",
|
|
@@ -104,44 +115,131 @@ export const listNodesCapability = defineCapability({
|
|
|
104
115
|
}), { method: "GET" });
|
|
105
116
|
},
|
|
106
117
|
});
|
|
107
|
-
export const
|
|
108
|
-
id: "
|
|
118
|
+
export const nodeOffGridCapability = defineCapability({
|
|
119
|
+
id: "node.off-grid",
|
|
109
120
|
domain: "fleet",
|
|
110
|
-
roles: ["
|
|
111
|
-
description: "
|
|
112
|
-
mutation: "
|
|
113
|
-
http: { method: "POST", path: "/internal/nodes/{id}/
|
|
121
|
+
roles: ["admin"],
|
|
122
|
+
description: "Admin-only. Take a node off-grid (remove Ornn agent management, leave the host powered on). CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
|
|
123
|
+
mutation: "preview-confirm",
|
|
124
|
+
http: { method: "POST", path: "/internal/nodes/{id}/off-grid" },
|
|
114
125
|
slack: {
|
|
115
126
|
parameters: {
|
|
116
127
|
type: "object",
|
|
117
128
|
properties: {
|
|
118
|
-
node_id: { type: "string", description: "Node UUID from nodes.list
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
node_id: { type: "string", description: "Node UUID from nodes.list." },
|
|
130
|
+
reason: { type: "string" },
|
|
131
|
+
},
|
|
132
|
+
required: ["node_id"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
input: nodeLifecycleInput,
|
|
136
|
+
execute: async (ctx, input) => {
|
|
137
|
+
const parsed = nodeLifecycleInput.parse(input);
|
|
138
|
+
const id = nodeIdOf(parsed);
|
|
139
|
+
const body = { ...(parsed.reason ? { reason: parsed.reason } : {}) };
|
|
140
|
+
if (!ctx.confirmed) {
|
|
141
|
+
return { ok: true, action: "node.off-grid", payload: { node_id: id, ...body } };
|
|
142
|
+
}
|
|
143
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/off-grid`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body,
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
export const nodeOnGridCapability = defineCapability({
|
|
150
|
+
id: "node.on-grid",
|
|
151
|
+
domain: "fleet",
|
|
152
|
+
roles: ["admin"],
|
|
153
|
+
description: "Admin-only. Re-enroll an off-grid node: orchestrator SSHs with the stored admin key and runs the installer with restore_node_id. No caller key. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
|
|
154
|
+
mutation: "preview-confirm",
|
|
155
|
+
http: { method: "POST", path: "/internal/nodes/{id}/on-grid" },
|
|
156
|
+
slack: {
|
|
157
|
+
parameters: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
node_id: { type: "string", description: "Off-grid node UUID from nodes.list." },
|
|
161
|
+
},
|
|
162
|
+
required: ["node_id"],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
input: nodeLifecycleInput,
|
|
166
|
+
execute: async (ctx, input) => {
|
|
167
|
+
const parsed = nodeLifecycleInput.parse(input);
|
|
168
|
+
const id = nodeIdOf(parsed);
|
|
169
|
+
if (!ctx.confirmed) {
|
|
170
|
+
return { ok: true, action: "node.on-grid", payload: { node_id: id } };
|
|
171
|
+
}
|
|
172
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/on-grid`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
body: {},
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
export const nodeExecCapability = defineCapability({
|
|
179
|
+
id: "node.exec",
|
|
180
|
+
domain: "fleet",
|
|
181
|
+
roles: ["admin"],
|
|
182
|
+
description: "Admin-only. Run one unrestricted command on an enrolled node over orchestrator SSH with the stored admin key. Always pass timeout_seconds. Not an interactive shell. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
|
|
183
|
+
mutation: "preview-confirm",
|
|
184
|
+
http: { method: "POST", path: "/internal/remote" },
|
|
185
|
+
slack: {
|
|
186
|
+
parameters: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
node_id: { type: "string", description: "Enrolled node UUID from nodes.list." },
|
|
190
|
+
command: { type: "string", description: "Exact shell command to run. No allowlist." },
|
|
191
|
+
timeout_seconds: {
|
|
192
|
+
type: "integer",
|
|
193
|
+
description: "Seconds to wait before killing the SSH command. Required. 1-900.",
|
|
123
194
|
},
|
|
124
195
|
},
|
|
196
|
+
required: ["node_id", "command", "timeout_seconds"],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
input: nodeExecInput,
|
|
200
|
+
execute: async (ctx, input) => {
|
|
201
|
+
const parsed = nodeExecInput.parse(input);
|
|
202
|
+
const id = nodeIdOf(parsed);
|
|
203
|
+
const timeoutSeconds = parsed.timeout_seconds ?? parsed.timeoutSeconds;
|
|
204
|
+
const payload = { node_id: id, command: parsed.command, timeout_seconds: timeoutSeconds };
|
|
205
|
+
if (!ctx.confirmed) {
|
|
206
|
+
return { ok: true, action: "node.exec", payload };
|
|
207
|
+
}
|
|
208
|
+
return ctx.fetch("/internal/remote", { method: "POST", body: payload });
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
export const nodeTerminateCapability = defineCapability({
|
|
212
|
+
id: "node.terminate",
|
|
213
|
+
domain: "fleet",
|
|
214
|
+
roles: ["admin"],
|
|
215
|
+
description: "Admin-only. Terminate a node record (best-effort clean + delete). Off-grid nodes need force: true. There is no deenroll verb. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
|
|
216
|
+
mutation: "preview-confirm",
|
|
217
|
+
http: { method: "POST", path: "/internal/nodes/{id}/terminate" },
|
|
218
|
+
slack: {
|
|
219
|
+
parameters: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
node_id: { type: "string", description: "Node UUID from nodes.list." },
|
|
223
|
+
reason: { type: "string" },
|
|
224
|
+
force: { type: "boolean", description: "Required to delete an off-grid node." },
|
|
225
|
+
},
|
|
125
226
|
required: ["node_id"],
|
|
126
227
|
},
|
|
127
228
|
},
|
|
128
|
-
input:
|
|
229
|
+
input: nodeLifecycleInput,
|
|
129
230
|
execute: async (ctx, input) => {
|
|
130
|
-
const parsed =
|
|
131
|
-
const id = (parsed
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
method: "POST",
|
|
139
|
-
body: { identity_file: identity, commands: parsed.commands },
|
|
140
|
-
});
|
|
231
|
+
const parsed = nodeLifecycleInput.parse(input);
|
|
232
|
+
const id = nodeIdOf(parsed);
|
|
233
|
+
const body = {
|
|
234
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
235
|
+
...(parsed.force !== undefined ? { force: parsed.force } : {}),
|
|
236
|
+
};
|
|
237
|
+
if (!ctx.confirmed) {
|
|
238
|
+
return { ok: true, action: "node.terminate", payload: { node_id: id, ...body } };
|
|
141
239
|
}
|
|
142
|
-
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/
|
|
240
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(id)}/terminate`, {
|
|
143
241
|
method: "POST",
|
|
144
|
-
body
|
|
242
|
+
body,
|
|
145
243
|
});
|
|
146
244
|
},
|
|
147
245
|
});
|
|
@@ -149,5 +247,8 @@ export const fleetCapabilities = [
|
|
|
149
247
|
listOperatorsCapability,
|
|
150
248
|
listFacilitiesCapability,
|
|
151
249
|
listNodesCapability,
|
|
152
|
-
|
|
250
|
+
nodeOffGridCapability,
|
|
251
|
+
nodeOnGridCapability,
|
|
252
|
+
nodeExecCapability,
|
|
253
|
+
nodeTerminateCapability,
|
|
153
254
|
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function buildEnrollmentInstallCommand(options: {
|
|
2
|
+
token: string;
|
|
3
|
+
base?: string;
|
|
4
|
+
nodeMode?: string;
|
|
5
|
+
ip?: string;
|
|
6
|
+
restoreNodeId?: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare const tokensListCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
10
|
+
export declare const tokensCreateCapability: import("../capability.ts").Capability<unknown, unknown>;
|
|
11
|
+
export declare const tokenCapabilities: import("../capability.ts").Capability<unknown, unknown>[];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
const optionalText = z.string().nullish();
|
|
4
|
+
const tokenCreateInput = z.object({
|
|
5
|
+
operator_id: z.string().min(1),
|
|
6
|
+
operatorId: optionalText,
|
|
7
|
+
facility_id: optionalText,
|
|
8
|
+
facilityId: optionalText,
|
|
9
|
+
expires_in_seconds: z.number().int().positive().nullish(),
|
|
10
|
+
expiresInSeconds: z.number().int().positive().nullish(),
|
|
11
|
+
node_mode: z.enum(["bare-metal", "virtual-machine", "vm"]).nullish(),
|
|
12
|
+
nodeMode: z.enum(["bare-metal", "virtual-machine", "vm"]).nullish(),
|
|
13
|
+
ip: optionalText,
|
|
14
|
+
installer_base: optionalText,
|
|
15
|
+
host: optionalText,
|
|
16
|
+
hosts: z.array(z.string()).nullish(),
|
|
17
|
+
username: optionalText,
|
|
18
|
+
port: z.number().int().min(1).max(65535).nullish(),
|
|
19
|
+
private_key: optionalText,
|
|
20
|
+
privateKey: optionalText,
|
|
21
|
+
yes: z.boolean().optional(),
|
|
22
|
+
});
|
|
23
|
+
const emptyInput = z.object({});
|
|
24
|
+
export function buildEnrollmentInstallCommand(options) {
|
|
25
|
+
const base = (options.base ?? "https://api.ornn.com/v1/orchestrator").replace(/\/+$/, "");
|
|
26
|
+
const params = [`token=${encodeURIComponent(options.token)}`];
|
|
27
|
+
if (options.nodeMode) {
|
|
28
|
+
const mode = options.nodeMode === "vm" ? "virtual-machine" : options.nodeMode;
|
|
29
|
+
params.push(`node_mode=${encodeURIComponent(mode)}`);
|
|
30
|
+
}
|
|
31
|
+
if (options.ip)
|
|
32
|
+
params.push(`ip=${encodeURIComponent(options.ip)}`);
|
|
33
|
+
if (options.restoreNodeId)
|
|
34
|
+
params.push(`restore_node_id=${encodeURIComponent(options.restoreNodeId)}`);
|
|
35
|
+
if (options.force)
|
|
36
|
+
params.push("force=true");
|
|
37
|
+
const href = `${base}/enrollment/install?${params.join("&")}`;
|
|
38
|
+
return `curl -fsSL '${href.replace(/'/g, "'\\''")}' | sudo bash`;
|
|
39
|
+
}
|
|
40
|
+
function tokenString(row, keys) {
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
const value = row[key];
|
|
43
|
+
if (typeof value === "string" && value.trim())
|
|
44
|
+
return value.trim();
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
export const tokensListCapability = defineCapability({
|
|
49
|
+
id: "tokens.list",
|
|
50
|
+
domain: "fleet",
|
|
51
|
+
roles: ["reviewer", "admin"],
|
|
52
|
+
description: "List issued enrollment tokens (id, operator, status, expiry). Does not return plaintext secrets.",
|
|
53
|
+
mutation: "read",
|
|
54
|
+
http: { method: "GET", path: "/internal/enrollment-tokens" },
|
|
55
|
+
slack: { parameters: { type: "object", properties: {} } },
|
|
56
|
+
input: emptyInput,
|
|
57
|
+
execute: async (ctx) => ctx.fetch("/internal/enrollment-tokens", { method: "GET" }),
|
|
58
|
+
});
|
|
59
|
+
export const tokensCreateCapability = defineCapability({
|
|
60
|
+
id: "tokens.create",
|
|
61
|
+
domain: "fleet",
|
|
62
|
+
roles: ["admin"],
|
|
63
|
+
description: "Admin-only. Mint an enrollment token. Without a host this returns the curl installer. With host + private_key, orchestrator SSHs and runs that curl (same as Bulk Enroll). Never invent a command. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
|
|
64
|
+
mutation: "preview-confirm",
|
|
65
|
+
http: { method: "POST", path: "/internal/enrollment-tokens" },
|
|
66
|
+
slack: {
|
|
67
|
+
parameters: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
operator_id: { type: "string", description: "Commerce operator UUID from operator.list." },
|
|
71
|
+
facility_id: { type: "string", description: "Optional facility UUID." },
|
|
72
|
+
expires_in_seconds: { type: "integer", description: "Token TTL in seconds." },
|
|
73
|
+
node_mode: { type: "string", enum: ["bare-metal", "virtual-machine"] },
|
|
74
|
+
ip: { type: "string", description: "Optional IP stamped into the installer URL." },
|
|
75
|
+
host: { type: "string", description: "SSH host/IP to enroll now." },
|
|
76
|
+
hosts: {
|
|
77
|
+
type: "array",
|
|
78
|
+
items: { type: "string" },
|
|
79
|
+
description: "Optional extra SSH hosts to enroll with the same key.",
|
|
80
|
+
},
|
|
81
|
+
username: { type: "string", description: "SSH user. Default admin." },
|
|
82
|
+
port: { type: "integer", description: "SSH port. Default 22." },
|
|
83
|
+
private_key: { type: "string", description: "PEM private key. Required to enroll over SSH." },
|
|
84
|
+
},
|
|
85
|
+
required: ["operator_id"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
input: tokenCreateInput,
|
|
89
|
+
execute: async (ctx, input) => {
|
|
90
|
+
const parsed = tokenCreateInput.parse(input);
|
|
91
|
+
const operatorId = (parsed.operator_id || parsed.operatorId || "").trim();
|
|
92
|
+
const hosts = [
|
|
93
|
+
...(parsed.host?.trim() ? [parsed.host.trim()] : []),
|
|
94
|
+
...(parsed.hosts ?? []).map((host) => host.trim()).filter(Boolean),
|
|
95
|
+
];
|
|
96
|
+
const privateKey = parsed.private_key?.trim() || parsed.privateKey?.trim() || "";
|
|
97
|
+
const nodeMode = parsed.node_mode ?? parsed.nodeMode;
|
|
98
|
+
const payload = {
|
|
99
|
+
operator_id: operatorId,
|
|
100
|
+
facility_id: parsed.facility_id ?? parsed.facilityId,
|
|
101
|
+
expires_in_seconds: parsed.expires_in_seconds ?? parsed.expiresInSeconds,
|
|
102
|
+
node_mode: nodeMode,
|
|
103
|
+
ip: parsed.ip,
|
|
104
|
+
hosts,
|
|
105
|
+
username: parsed.username?.trim() || "admin",
|
|
106
|
+
port: parsed.port ?? 22,
|
|
107
|
+
remote: hosts.length > 0 && privateKey.length > 0,
|
|
108
|
+
};
|
|
109
|
+
if (!ctx.confirmed) {
|
|
110
|
+
return { ok: true, action: "tokens.create", payload: { ...payload, private_key: privateKey ? "[redacted]" : "" } };
|
|
111
|
+
}
|
|
112
|
+
const minted = (await ctx.fetch("/internal/enrollment-tokens", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
body: {
|
|
115
|
+
operator_id: operatorId,
|
|
116
|
+
...(payload.facility_id ? { facility_id: payload.facility_id } : {}),
|
|
117
|
+
...(payload.expires_in_seconds ? { expires_in_seconds: payload.expires_in_seconds } : {}),
|
|
118
|
+
},
|
|
119
|
+
}));
|
|
120
|
+
const token = tokenString(minted, ["token", "plaintext"]) ?? "";
|
|
121
|
+
const installCommand = token
|
|
122
|
+
? buildEnrollmentInstallCommand({
|
|
123
|
+
token,
|
|
124
|
+
base: parsed.installer_base ?? undefined,
|
|
125
|
+
nodeMode: nodeMode ?? undefined,
|
|
126
|
+
ip: parsed.ip ?? undefined,
|
|
127
|
+
force: payload.remote,
|
|
128
|
+
})
|
|
129
|
+
: null;
|
|
130
|
+
if (!payload.remote || !installCommand) {
|
|
131
|
+
return { ...minted, install_command: installCommand };
|
|
132
|
+
}
|
|
133
|
+
const results = [];
|
|
134
|
+
for (const host of hosts) {
|
|
135
|
+
results.push(await ctx.fetch("/internal/remote", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: {
|
|
138
|
+
host,
|
|
139
|
+
username: payload.username,
|
|
140
|
+
port: payload.port,
|
|
141
|
+
private_key: privateKey,
|
|
142
|
+
command: installCommand,
|
|
143
|
+
timeout_seconds: 600,
|
|
144
|
+
},
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: minted.id,
|
|
149
|
+
operator_id: minted.operator_id ?? operatorId,
|
|
150
|
+
expires_at: minted.expires_at,
|
|
151
|
+
hosts: results,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
export const tokenCapabilities = [tokensListCapability, tokensCreateCapability];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { kubernetesCapabilities, slurmCapabilities } from "./capabilities/clusters.js";
|
|
2
2
|
import { fleetCapabilities } from "./capabilities/fleet.js";
|
|
3
|
+
import { tokenCapabilities } from "./capabilities/tokens.js";
|
|
3
4
|
import { identityCapabilities } from "./capabilities/identity.js";
|
|
4
5
|
import { listingCapabilities } from "./capabilities/listings.js";
|
|
5
6
|
import { observabilityCapabilities } from "./capabilities/observability.js";
|
|
@@ -9,6 +10,7 @@ export const catalog = [
|
|
|
9
10
|
...identityCapabilities,
|
|
10
11
|
...listingCapabilities,
|
|
11
12
|
...fleetCapabilities,
|
|
13
|
+
...tokenCapabilities,
|
|
12
14
|
...userCapabilities,
|
|
13
15
|
...observabilityCapabilities,
|
|
14
16
|
...slurmCapabilities,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { byCliCommand, byMcpName, bySlackName, defineCapability, forRole, slackStaffVisible, visibleToClient, type Capability, type CapabilityBindings, type CapabilityContext, type CapabilitySpec, type JsonSchema, type CapabilityTransport, type Domain, type HttpSpec, type Mutation, } from "./capability.ts";
|
|
2
2
|
export { namesFromId } from "./names.ts";
|
|
3
3
|
export { catalog } from "./catalog.ts";
|
|
4
|
-
export { fleetCapabilities, listFacilitiesCapability, listNodesCapability, listOperatorsCapability,
|
|
4
|
+
export { fleetCapabilities, listFacilitiesCapability, listNodesCapability, listOperatorsCapability, nodeExecCapability, nodeOffGridCapability, nodeOnGridCapability, nodeTerminateCapability, } from "./capabilities/fleet.ts";
|
|
5
|
+
export { buildEnrollmentInstallCommand, tokenCapabilities, tokensCreateCapability, tokensListCapability, } from "./capabilities/tokens.ts";
|
|
5
6
|
export { identityCapabilities, statusCapability, whoamiCapability } from "./capabilities/identity.ts";
|
|
6
7
|
export { createListingCapability, listingCreateInput, listCatalogTermsCapability, listingCapabilities, listListingsCapability, proposeListingCapability, } from "./capabilities/listings.ts";
|
|
7
8
|
export { userCapabilities, usersQueryCapability } from "./capabilities/users.ts";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { byCliCommand, byMcpName, bySlackName, defineCapability, forRole, slackStaffVisible, visibleToClient, } from "./capability.js";
|
|
2
2
|
export { namesFromId } from "./names.js";
|
|
3
3
|
export { catalog } from "./catalog.js";
|
|
4
|
-
export { fleetCapabilities, listFacilitiesCapability, listNodesCapability, listOperatorsCapability,
|
|
4
|
+
export { fleetCapabilities, listFacilitiesCapability, listNodesCapability, listOperatorsCapability, nodeExecCapability, nodeOffGridCapability, nodeOnGridCapability, nodeTerminateCapability, } from "./capabilities/fleet.js";
|
|
5
|
+
export { buildEnrollmentInstallCommand, tokenCapabilities, tokensCreateCapability, tokensListCapability, } from "./capabilities/tokens.js";
|
|
5
6
|
export { identityCapabilities, statusCapability, whoamiCapability } from "./capabilities/identity.js";
|
|
6
7
|
export { createListingCapability, listingCreateInput, listCatalogTermsCapability, listingCapabilities, listListingsCapability, proposeListingCapability, } from "./capabilities/listings.js";
|
|
7
8
|
export { userCapabilities, usersQueryCapability } from "./capabilities/users.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Shared wire names derived from `Capability.id`. */
|
|
2
2
|
export function namesFromId(id) {
|
|
3
|
-
const wire = id.replaceAll(".", "_");
|
|
3
|
+
const wire = id.replaceAll(".", "_").replaceAll("-", "_");
|
|
4
4
|
const cli = id === "identity.whoami" ? ["whoami"] : id === "identity.status" ? ["status"] : id.split(".");
|
|
5
5
|
return { mcp: wire, slack: wire, cli };
|
|
6
6
|
}
|