@ornncompute/cli 1.0.137 → 1.0.138
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 +8 -3
- package/package.json +1 -1
- package/src/admin-enrollment.mjs +14 -161
- package/vendor/api-client/orchestrator.js +517 -548
- package/vendor/capabilities/cli.js +212 -313
package/README.md
CHANGED
|
@@ -210,9 +210,14 @@ node ID. Changing filters requires a new first-page request. Legacy raw owner
|
|
|
210
210
|
cursors require the original `--before` value and filters.
|
|
211
211
|
Metrics and logs use node-scoped staff gateway routes. Exported private keys
|
|
212
212
|
include the final newline required by OpenSSH, both in files and with `--print`.
|
|
213
|
-
`ornn admin
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
`ornn admin enrollment-tokens create` exports the configurable run
|
|
214
|
+
`install_command` as a secret. Its file receipt separates `enrollment_run_id` from
|
|
215
|
+
`token_metadata.id`, the credential ID used for revocation. Read its exact
|
|
216
|
+
`created_at` with `enrollment-tokens list` before machine revocation; the run
|
|
217
|
+
timestamp is not the credential generation. `ornn admin nodes enroll` creates and SSH-launches
|
|
218
|
+
a configurable agent recovery run for the existing node. The former `--filter`
|
|
219
|
+
and `--verbose` compartment options are removed; inspect the saved run for step
|
|
220
|
+
progress. The gateway authoritatively enforces the admin role.
|
|
216
221
|
`ornn admin nodes sanitize`, `sanitize-status`, and `sanitize-decide` drive one
|
|
217
222
|
node's held sanitize job, sharing the capability with MCP
|
|
218
223
|
`admin_node_sanitize_*` and Slack `node_sanitize_*`. `sanitize` needs
|
package/package.json
CHANGED
package/src/admin-enrollment.mjs
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
enrollmentTokensPath,
|
|
3
|
-
remoteRunPath,
|
|
4
|
-
listEnrollmentLogs,
|
|
5
|
-
} from "../vendor/api-client/orchestrator.js";
|
|
6
1
|
import { createHash } from "node:crypto";
|
|
7
2
|
import { lstat, readFile } from "node:fs/promises";
|
|
8
3
|
|
|
9
|
-
import { CliApiError, resolveInstallerBaseUrl } from "./api-client.mjs";
|
|
10
|
-
import { AtomicOutputExistsError, writeAtomicOutput } from "./atomic-output.mjs";
|
|
11
4
|
import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
|
|
5
|
+
import { CliApiError } from "./api-client.mjs";
|
|
6
|
+
import { AtomicOutputExistsError, writeAtomicOutput } from "./atomic-output.mjs";
|
|
12
7
|
|
|
13
8
|
const SECRET_OUTPUT = new Set(["tokens.create", "nodes.rotate-key"]);
|
|
14
|
-
const ENROLLMENT_LOG_POLL_MS = 750;
|
|
15
|
-
const ENROLLMENT_LOG_READ_TIMEOUT_MS = 5_000;
|
|
16
9
|
|
|
17
10
|
const USAGE = {
|
|
18
11
|
"tokens.create":
|
|
@@ -20,7 +13,7 @@ const USAGE = {
|
|
|
20
13
|
"enrollment.token.revoke":
|
|
21
14
|
"Usage: ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--idempotency-key KEY]",
|
|
22
15
|
"nodes.enroll":
|
|
23
|
-
"Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID
|
|
16
|
+
"Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID --identity-file PATH --token-ttl SECONDS --replace-existing-management",
|
|
24
17
|
"nodes.export-key":
|
|
25
18
|
"Usage: ornn admin nodes export-key <node-id> (--print | --output PATH) [--overwrite]",
|
|
26
19
|
"nodes.rotate-key":
|
|
@@ -167,22 +160,12 @@ function parseNodeEnroll(argv) {
|
|
|
167
160
|
const { options, positionals } = parseOptions(
|
|
168
161
|
argv,
|
|
169
162
|
{
|
|
170
|
-
booleans: ["replace-existing-management"
|
|
163
|
+
booleans: ["replace-existing-management"],
|
|
171
164
|
positionals: 1,
|
|
172
|
-
values: [
|
|
173
|
-
"facility",
|
|
174
|
-
"filter",
|
|
175
|
-
"host",
|
|
176
|
-
"identity-file",
|
|
177
|
-
"port",
|
|
178
|
-
"supplier",
|
|
179
|
-
"token-ttl",
|
|
180
|
-
"username",
|
|
181
|
-
],
|
|
165
|
+
values: ["facility", "host", "identity-file", "port", "supplier", "token-ttl", "username"],
|
|
182
166
|
},
|
|
183
167
|
usage
|
|
184
168
|
);
|
|
185
|
-
const filter = options.filter?.trim();
|
|
186
169
|
if (!options["replace-existing-management"]) {
|
|
187
170
|
throw new AdminMutationError(`${usage}\n--replace-existing-management is required.`, {
|
|
188
171
|
field: "replace-existing-management",
|
|
@@ -194,7 +177,6 @@ function parseNodeEnroll(argv) {
|
|
|
194
177
|
host: required(options, "host", usage),
|
|
195
178
|
organization_id: uuid(required(options, "supplier", usage), "supplier"),
|
|
196
179
|
facility_id: uuid(required(options, "facility", usage), "facility"),
|
|
197
|
-
...(filter ? { filter } : {}),
|
|
198
180
|
identity_file: required(options, "identity-file", usage),
|
|
199
181
|
expires_in_seconds: boundedInteger(
|
|
200
182
|
required(options, "token-ttl", usage),
|
|
@@ -205,7 +187,6 @@ function parseNodeEnroll(argv) {
|
|
|
205
187
|
username: options.username?.trim() || "root",
|
|
206
188
|
port: options.port ? boundedInteger(options.port, "port", 1, 65_535) : 22,
|
|
207
189
|
replace_existing_management: true,
|
|
208
|
-
verbose: options.verbose === true,
|
|
209
190
|
},
|
|
210
191
|
mutation: {
|
|
211
192
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
@@ -246,17 +227,18 @@ function text(value, fallback = "") {
|
|
|
246
227
|
|
|
247
228
|
function safeTokenMetadata(payload) {
|
|
248
229
|
return {
|
|
249
|
-
id: text(payload?.
|
|
230
|
+
id: text(payload?.credential_id) || null,
|
|
250
231
|
organization_id: text(payload?.organization_id) || null,
|
|
251
|
-
status:
|
|
252
|
-
expires_at: text(payload?.
|
|
253
|
-
|
|
232
|
+
status: "active",
|
|
233
|
+
expires_at: text(payload?.credential_expires_at) || null,
|
|
234
|
+
// The run's creation time is not the credential's generation identity.
|
|
235
|
+
created_at: null,
|
|
254
236
|
};
|
|
255
237
|
}
|
|
256
238
|
|
|
257
239
|
function secretMaterial(capabilityId, payload) {
|
|
258
240
|
if (capabilityId === "tokens.create") {
|
|
259
|
-
return text(payload?.
|
|
241
|
+
return text(payload?.install_command);
|
|
260
242
|
}
|
|
261
243
|
const key = payload?.private_key;
|
|
262
244
|
if (typeof key !== "string" || !key.length) return "";
|
|
@@ -302,7 +284,7 @@ async function exportSecret({ capabilityId, input, options, payload, stdout }) {
|
|
|
302
284
|
}
|
|
303
285
|
const metadata =
|
|
304
286
|
capabilityId === "tokens.create"
|
|
305
|
-
? { token_metadata: safeTokenMetadata(payload) }
|
|
287
|
+
? { enrollment_run_id: text(payload?.id) || null, token_metadata: safeTokenMetadata(payload) }
|
|
306
288
|
: { node_key: keyMetadata(input, payload) };
|
|
307
289
|
if (options.print) {
|
|
308
290
|
stdout.write(material);
|
|
@@ -341,7 +323,7 @@ async function exportSecret({ capabilityId, input, options, payload, stdout }) {
|
|
|
341
323
|
function reviewFor(capabilityId, input, prepared) {
|
|
342
324
|
if (capabilityId === "tokens.create") {
|
|
343
325
|
return {
|
|
344
|
-
title: "Create enrollment
|
|
326
|
+
title: "Create enrollment command",
|
|
345
327
|
fields: [
|
|
346
328
|
{ label: "Supplier", value: input.organization_id },
|
|
347
329
|
{ label: "Expires in", value: `${input.expires_in_seconds} seconds` },
|
|
@@ -374,7 +356,6 @@ function reviewFor(capabilityId, input, prepared) {
|
|
|
374
356
|
{ label: "SSH target", value: `${input.username}@${input.host}:${input.port}` },
|
|
375
357
|
{ label: "Supplier", value: input.organization_id },
|
|
376
358
|
{ label: "Facility", value: input.facility_id },
|
|
377
|
-
...(input.filter ? [{ label: "Compartment filter", value: input.filter }] : []),
|
|
378
359
|
{ label: "Token lifetime", value: `${input.expires_in_seconds} seconds` },
|
|
379
360
|
],
|
|
380
361
|
consequence:
|
|
@@ -403,7 +384,6 @@ async function enrollmentIdentityInput(input) {
|
|
|
403
384
|
}
|
|
404
385
|
return {
|
|
405
386
|
...input,
|
|
406
|
-
installer_base: resolveInstallerBaseUrl({ env: input.env }) ?? undefined,
|
|
407
387
|
private_key: privateKey,
|
|
408
388
|
};
|
|
409
389
|
} catch (error) {
|
|
@@ -415,129 +395,6 @@ async function enrollmentIdentityInput(input) {
|
|
|
415
395
|
}
|
|
416
396
|
}
|
|
417
397
|
|
|
418
|
-
function enrollmentRuns(payload) {
|
|
419
|
-
if (!payload || typeof payload !== "object") return [];
|
|
420
|
-
if (Array.isArray(payload.logs)) return payload.logs;
|
|
421
|
-
return Array.isArray(payload.runs) ? payload.runs : [];
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function redactEnrollmentText(value, secrets) {
|
|
425
|
-
let text = typeof value === "string" ? value : String(value ?? "");
|
|
426
|
-
for (const secret of secrets) {
|
|
427
|
-
if (!secret) continue;
|
|
428
|
-
text = text.replaceAll(secret, "[redacted]");
|
|
429
|
-
text = text.replaceAll(encodeURIComponent(secret), "[redacted]");
|
|
430
|
-
}
|
|
431
|
-
return text;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function freshEnrollmentLogLines(payload, tokenId, secrets, seen) {
|
|
435
|
-
const session = enrollmentRuns(payload).find((candidate) => candidate?.token_id === tokenId);
|
|
436
|
-
if (!session || typeof session !== "object") return [];
|
|
437
|
-
const compartments = Array.isArray(session.compartments) ? session.compartments : [];
|
|
438
|
-
const total = Number.isInteger(session.progress_total)
|
|
439
|
-
? session.progress_total
|
|
440
|
-
: compartments.length;
|
|
441
|
-
const lines = [];
|
|
442
|
-
for (const [index, compartment] of compartments.entries()) {
|
|
443
|
-
if (!compartment || typeof compartment !== "object") continue;
|
|
444
|
-
const id = String(compartment.id ?? index);
|
|
445
|
-
const numericId = Number(id);
|
|
446
|
-
const current = Number.isInteger(numericId) ? numericId + 1 : session.progress_index;
|
|
447
|
-
const title =
|
|
448
|
-
typeof compartment.title === "string" && compartment.title ? compartment.title : `Step ${id}`;
|
|
449
|
-
const state = typeof compartment.state === "string" ? compartment.state : "";
|
|
450
|
-
const stateKey = `${id}:state:${state}`;
|
|
451
|
-
if (state && !seen.has(stateKey)) {
|
|
452
|
-
seen.add(stateKey);
|
|
453
|
-
lines.push(`[enroll ${current}/${total}] ${title}: ${state}`);
|
|
454
|
-
}
|
|
455
|
-
const output = Array.isArray(compartment.output) ? compartment.output : [];
|
|
456
|
-
for (const [outputIndex, entry] of output.entries()) {
|
|
457
|
-
const stream =
|
|
458
|
-
entry && typeof entry === "object" && typeof entry.stream === "string"
|
|
459
|
-
? entry.stream
|
|
460
|
-
: "stdout";
|
|
461
|
-
const seq =
|
|
462
|
-
entry && typeof entry === "object" && Number.isInteger(entry.seq) ? entry.seq : outputIndex;
|
|
463
|
-
const key = `${id}:output:${seq}:${stream}`;
|
|
464
|
-
if (seen.has(key)) continue;
|
|
465
|
-
seen.add(key);
|
|
466
|
-
const raw = entry && typeof entry === "object" ? entry.text : entry;
|
|
467
|
-
const text = redactEnrollmentText(raw, secrets);
|
|
468
|
-
if (text) lines.push(` ${stream === "stderr" ? "[stderr] " : ""}${text}`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return lines;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function enrollmentProgressTransport(fetch, runtime, privateKey) {
|
|
475
|
-
let tokenId = "";
|
|
476
|
-
const secrets = [privateKey];
|
|
477
|
-
const seen = new Set();
|
|
478
|
-
const request = (path, init = {}) =>
|
|
479
|
-
fetch(
|
|
480
|
-
path,
|
|
481
|
-
init.signal === undefined && runtime.signal ? { ...init, signal: runtime.signal } : init
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
return async (path, init = {}) => {
|
|
485
|
-
if (path === enrollmentTokensPath) {
|
|
486
|
-
const minted = await request(path, init);
|
|
487
|
-
if (minted && typeof minted === "object") {
|
|
488
|
-
tokenId = typeof minted.id === "string" ? minted.id : "";
|
|
489
|
-
const token =
|
|
490
|
-
typeof minted.token === "string"
|
|
491
|
-
? minted.token
|
|
492
|
-
: typeof minted.plaintext === "string"
|
|
493
|
-
? minted.plaintext
|
|
494
|
-
: "";
|
|
495
|
-
if (token) secrets.push(token);
|
|
496
|
-
}
|
|
497
|
-
return minted;
|
|
498
|
-
}
|
|
499
|
-
if (path !== remoteRunPath || !tokenId) return request(path, init);
|
|
500
|
-
|
|
501
|
-
const remote = request(path, init).then(
|
|
502
|
-
(value) => ({ ok: true, value }),
|
|
503
|
-
(error) => ({ error, ok: false })
|
|
504
|
-
);
|
|
505
|
-
let stopped = false;
|
|
506
|
-
let timer;
|
|
507
|
-
let currentPoll = Promise.resolve();
|
|
508
|
-
const poll = async () => {
|
|
509
|
-
try {
|
|
510
|
-
const payload = await listEnrollmentLogs(
|
|
511
|
-
fetch,
|
|
512
|
-
{},
|
|
513
|
-
AbortSignal.timeout(ENROLLMENT_LOG_READ_TIMEOUT_MS)
|
|
514
|
-
);
|
|
515
|
-
const lines = freshEnrollmentLogLines(payload, tokenId, secrets, seen);
|
|
516
|
-
for (const line of lines) runtime.stderr.write(`${line}\n`);
|
|
517
|
-
} catch {
|
|
518
|
-
// Progress is best-effort; the authoritative remote request still determines success.
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
const schedule = () => {
|
|
522
|
-
timer = setTimeout(() => {
|
|
523
|
-
currentPoll = poll().finally(() => {
|
|
524
|
-
if (!stopped) schedule();
|
|
525
|
-
});
|
|
526
|
-
}, ENROLLMENT_LOG_POLL_MS);
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
await poll();
|
|
530
|
-
schedule();
|
|
531
|
-
const result = await remote;
|
|
532
|
-
stopped = true;
|
|
533
|
-
clearTimeout(timer);
|
|
534
|
-
await currentPoll;
|
|
535
|
-
await poll();
|
|
536
|
-
if (!result.ok) throw result.error;
|
|
537
|
-
return result.value;
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
|
|
541
398
|
function assertSecretOutputContext(input, runtime) {
|
|
542
399
|
if (input.export?.print && runtime.formatProvided) {
|
|
543
400
|
throw new AdminMutationError("--print cannot be combined with --format.", {
|
|
@@ -626,11 +483,7 @@ async function runEnrollmentMutation({ capability, execute, fetch, input, mutati
|
|
|
626
483
|
capabilityId === "nodes.enroll"
|
|
627
484
|
? await enrollmentIdentityInput({ ...value, env: runtime.env })
|
|
628
485
|
: value;
|
|
629
|
-
|
|
630
|
-
capabilityId === "nodes.enroll" && executionInput.verbose
|
|
631
|
-
? enrollmentProgressTransport(fetch, runtime, executionInput.private_key)
|
|
632
|
-
: undefined;
|
|
633
|
-
return execute(executionInput, { confirmed: true, transport });
|
|
486
|
+
return execute(executionInput, { confirmed: true });
|
|
634
487
|
},
|
|
635
488
|
});
|
|
636
489
|
|