@bli-cockpit/cli 0.2.14 → 0.2.15
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/dist/commands/doctor.js +13 -1
- package/dist/commands/local.js +19 -7
- package/dist/commands/public-root.js +1 -1
- package/dist/health-detail.js +80 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { autostartStatus, installAutostartAgent } from "../autostart.js";
|
|
4
4
|
import { savedDiscoveryLimitArgs } from "../discovery-limits.js";
|
|
5
|
+
import { redactedHealthDetail } from "../health-detail.js";
|
|
5
6
|
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
6
7
|
import { backfillCompletionCovers, readBackfillCompletionMarker, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
7
8
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, } from "../local-state.js";
|
|
@@ -383,10 +384,21 @@ function doctorEvent(row) {
|
|
|
383
384
|
: row.status === "skipped"
|
|
384
385
|
? "skipped"
|
|
385
386
|
: "ok";
|
|
387
|
+
if (status === "ok")
|
|
388
|
+
return { step: row.id, status };
|
|
389
|
+
// BLI-2542. The step already computed why it failed and already printed it to
|
|
390
|
+
// the operator; before this, only the bucket travelled. `autostart-alive`
|
|
391
|
+
// reports two distinct problems at once on Windows, so the detail carries the
|
|
392
|
+
// whole message rather than a first line.
|
|
393
|
+
const detail = redactedHealthDetail(row.message);
|
|
386
394
|
return {
|
|
387
395
|
step: row.id,
|
|
388
396
|
status,
|
|
389
|
-
|
|
397
|
+
error_code: sanitizeEventCode(row.code),
|
|
398
|
+
// A detail that only repeats the bucket is noise, not a reason.
|
|
399
|
+
...(detail && detail !== sanitizeEventCode(row.code)
|
|
400
|
+
? { error_detail: detail }
|
|
401
|
+
: {}),
|
|
390
402
|
};
|
|
391
403
|
}
|
|
392
404
|
function doctorMark(row) {
|
package/dist/commands/local.js
CHANGED
|
@@ -7,6 +7,7 @@ import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../a
|
|
|
7
7
|
import { backfillRetryCommand, runBackfill, runBackfillCommand, } from "./backfill.js";
|
|
8
8
|
import { runDoctor } from "./doctor.js";
|
|
9
9
|
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
10
|
+
import { maskLocalIdentifiers, redactedHealthDetail, } from "../health-detail.js";
|
|
10
11
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
11
12
|
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from "../autostart.js";
|
|
12
13
|
import { DEFAULT_DASHBOARD_URL, ensureLocalCollectorConfig, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
@@ -640,11 +641,17 @@ function updateCollectionRoots(command) {
|
|
|
640
641
|
}
|
|
641
642
|
return deduped;
|
|
642
643
|
}
|
|
643
|
-
function addInstallEvent(events, step, status, errorCode
|
|
644
|
+
function addInstallEvent(events, step, status, errorCode,
|
|
645
|
+
// BLI-2542: the bucket alone cannot be acted on. Callers that hold the reason
|
|
646
|
+
// pass it; it is redacted at this boundary, not at the call site.
|
|
647
|
+
errorMessage) {
|
|
648
|
+
const detail = errorMessage ? redactedHealthDetail(errorMessage) : "";
|
|
649
|
+
const code = errorCode ? sanitizeInstallErrorCode(errorCode) : undefined;
|
|
644
650
|
events.push({
|
|
645
651
|
step,
|
|
646
652
|
status,
|
|
647
|
-
...(
|
|
653
|
+
...(code ? { error_code: code } : {}),
|
|
654
|
+
...(detail && detail !== code ? { error_detail: detail } : {}),
|
|
648
655
|
});
|
|
649
656
|
}
|
|
650
657
|
function addOnboardFailureEvent(events, blocker) {
|
|
@@ -660,10 +667,10 @@ function addAutostartInstallEvent(events, result) {
|
|
|
660
667
|
return;
|
|
661
668
|
}
|
|
662
669
|
if (result.status === "unsupported") {
|
|
663
|
-
addInstallEvent(events, "autostart", "skipped", "unsupported");
|
|
670
|
+
addInstallEvent(events, "autostart", "skipped", "unsupported", result.message);
|
|
664
671
|
return;
|
|
665
672
|
}
|
|
666
|
-
addInstallEvent(events, "autostart", result.loaded === false ? "fail" : "ok", result.loaded === false ? "autostart_load_failed" : undefined);
|
|
673
|
+
addInstallEvent(events, "autostart", result.loaded === false ? "fail" : "ok", result.loaded === false ? "autostart_load_failed" : undefined, result.loaded === false ? result.message : undefined);
|
|
667
674
|
}
|
|
668
675
|
function onboardAutostartFailed(result) {
|
|
669
676
|
return (result !== null &&
|
|
@@ -2272,9 +2279,14 @@ export function redactedSyncErrorDetail(error) {
|
|
|
2272
2279
|
const { text } = redactSecretLikeContent(message, {
|
|
2273
2280
|
appliedBy: "local_collector",
|
|
2274
2281
|
});
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2282
|
+
// BLI-2542: the comment above always said this text can carry absolute paths,
|
|
2283
|
+
// and until now nothing removed them — secret redaction matches token shapes,
|
|
2284
|
+
// not filesystem paths. Same masking the doctor receipts use, so one boundary
|
|
2285
|
+
// rule covers every health receipt.
|
|
2286
|
+
const masked = maskLocalIdentifiers(text);
|
|
2287
|
+
return masked.length > SYNC_ERROR_DETAIL_MAX_CHARS
|
|
2288
|
+
? `${masked.slice(0, SYNC_ERROR_DETAIL_MAX_CHARS - 1)}…`
|
|
2289
|
+
: masked;
|
|
2278
2290
|
}
|
|
2279
2291
|
/**
|
|
2280
2292
|
* Turn a finished run into an exit code and the reasons behind it.
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.15");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { redactSecretLikeContent } from "@bli-cockpit/telemetry-core";
|
|
3
|
+
/**
|
|
4
|
+
* The reason a health receipt carries, redacted on the machine that produced it.
|
|
5
|
+
*
|
|
6
|
+
* `error_code` is the aggregation bucket. This is the cause — the text a step
|
|
7
|
+
* already computed and already showed the operator. BLI-2542: without it a
|
|
8
|
+
* receipt can say `autostart_load_failed` and nothing else, which names no cause
|
|
9
|
+
* and supports no repair. Measured on 2026-08-13, every receipt step except
|
|
10
|
+
* `sync_complete` uploaded a null detail: `npm_install` 6 failures, `gc-checked`
|
|
11
|
+
* 3, `backfill-complete` 3, `sync-fresh` 3, `autostart-alive` 1, all with the
|
|
12
|
+
* reason computed locally and discarded at the boundary.
|
|
13
|
+
*
|
|
14
|
+
* Two things are stripped before it leaves, in this order:
|
|
15
|
+
*
|
|
16
|
+
* 1. Secret-like fragments, via the same deterministic redaction the collector
|
|
17
|
+
* applies to evidence.
|
|
18
|
+
* 2. Local identifiers — absolute paths, UNC shares, the machine name, the
|
|
19
|
+
* account name. Step messages are static strings today, but several of them
|
|
20
|
+
* embed the output of `schtasks` or `launchctl`, and that output does carry
|
|
21
|
+
* the state directory and the host. Masking at the boundary means a future
|
|
22
|
+
* step cannot leak one by writing a more helpful message.
|
|
23
|
+
*
|
|
24
|
+
* Redaction is deliberately here rather than at each call site: the boundary is
|
|
25
|
+
* what leaves the machine, so the boundary is what has to be safe.
|
|
26
|
+
*/
|
|
27
|
+
export const HEALTH_DETAIL_MAX_CHARS = 600;
|
|
28
|
+
const WINDOWS_ABSOLUTE_PATH = /[A-Za-z]:\\[^\s"';]+/gu;
|
|
29
|
+
const UNC_PATH = /\\\\[^\s"';]+/gu;
|
|
30
|
+
// An absolute POSIX path of at least two segments. One segment ("/tmp") carries
|
|
31
|
+
// nothing identifying and masking it would make messages harder to read.
|
|
32
|
+
const POSIX_ABSOLUTE_PATH = /\/[\w.@-]+(?:\/[\w.@ -]+)+/gu;
|
|
33
|
+
export function redactedHealthDetail(message) {
|
|
34
|
+
const collapsed = message.replace(/\s+/gu, " ").trim();
|
|
35
|
+
if (!collapsed)
|
|
36
|
+
return "";
|
|
37
|
+
const { text } = redactSecretLikeContent(collapsed, {
|
|
38
|
+
appliedBy: "local_collector",
|
|
39
|
+
});
|
|
40
|
+
const masked = maskLocalIdentifiers(text);
|
|
41
|
+
return masked.length > HEALTH_DETAIL_MAX_CHARS
|
|
42
|
+
? `${masked.slice(0, HEALTH_DETAIL_MAX_CHARS - 1)}…`
|
|
43
|
+
: masked;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Paths first, because a path usually contains the account name; whatever
|
|
47
|
+
* mentions of the host or account survive on their own are masked after.
|
|
48
|
+
*/
|
|
49
|
+
export function maskLocalIdentifiers(value) {
|
|
50
|
+
let text = value
|
|
51
|
+
.replace(UNC_PATH, "[path]")
|
|
52
|
+
.replace(WINDOWS_ABSOLUTE_PATH, "[path]")
|
|
53
|
+
.replace(POSIX_ABSOLUTE_PATH, "[path]");
|
|
54
|
+
for (const [identifier, placeholder] of localIdentifiers()) {
|
|
55
|
+
if (!identifier)
|
|
56
|
+
continue;
|
|
57
|
+
text = text.replace(literalPattern(identifier), placeholder);
|
|
58
|
+
}
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
function localIdentifiers() {
|
|
62
|
+
const identifiers = [];
|
|
63
|
+
try {
|
|
64
|
+
identifiers.push([os.hostname(), "[host]"]);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A host with no resolvable name has nothing to leak.
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
identifiers.push([os.userInfo().username, "[user]"]);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Same: no account name available means none can travel.
|
|
74
|
+
}
|
|
75
|
+
// A one- or two-character name would match far too much ordinary text.
|
|
76
|
+
return identifiers.filter(([value]) => value && value.length > 2);
|
|
77
|
+
}
|
|
78
|
+
function literalPattern(value) {
|
|
79
|
+
return new RegExp(value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), "giu");
|
|
80
|
+
}
|