Skip to content

Commit c5f10b5

Browse files
authored
Doctor: expose config audit scrub findings (#84450)
* feat(doctor): expose config audit scrub findings * fix(doctor): keep audit scrub lint opt-in * fix(doctor): keep audit lint defaults internal * fix(doctor): remove duplicate lint default guard
1 parent f29dbd3 commit c5f10b5

5 files changed

Lines changed: 148 additions & 2 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import fs from "node:fs/promises";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import {
6+
configAuditScrubToHealthFinding,
7+
configAuditScrubToRepairEffect,
8+
detectConfigAuditScrubIssue,
9+
} from "./doctor-config-audit-scrub.js";
10+
11+
let tempRoot: string | null = null;
12+
13+
async function makeHome(): Promise<string> {
14+
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-doctor-config-audit-"));
15+
return tempRoot;
16+
}
17+
18+
afterEach(async () => {
19+
if (tempRoot !== null) {
20+
await fs.rm(tempRoot, { recursive: true, force: true });
21+
tempRoot = null;
22+
}
23+
});
24+
25+
describe("detectConfigAuditScrubIssue", () => {
26+
it("detects config-audit scrub work without rewriting the log", async () => {
27+
const home = await makeHome();
28+
const auditPath = path.join(home, ".openclaw", "logs", "config-audit.jsonl");
29+
await fs.mkdir(path.dirname(auditPath), { recursive: true, mode: 0o700 });
30+
const record = {
31+
ts: "2026-05-02T00:03:48.471Z",
32+
argv: ["node", "openclaw.mjs", "config", "set", "x", "xoxb-bad-token-1234567890abcdef"],
33+
execArgv: [],
34+
};
35+
await fs.writeFile(auditPath, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
36+
37+
const result = await detectConfigAuditScrubIssue({
38+
env: {} as NodeJS.ProcessEnv,
39+
homedir: () => home,
40+
});
41+
42+
expect(result).toEqual({
43+
scanned: 1,
44+
rewritten: 1,
45+
skipped: 0,
46+
aborted: false,
47+
auditPath,
48+
});
49+
expect(await fs.readFile(auditPath, "utf8")).toBe(`${JSON.stringify(record)}\n`);
50+
});
51+
52+
it("maps scrub work to structured findings and dry-run effects", async () => {
53+
const home = await makeHome();
54+
const auditPath = path.join(home, ".openclaw", "logs", "config-audit.jsonl");
55+
const result = { scanned: 2, rewritten: 1, skipped: 0, aborted: false, auditPath };
56+
57+
expect(configAuditScrubToHealthFinding(result)).toEqual(
58+
expect.objectContaining({
59+
checkId: "core/doctor/config-audit-scrub",
60+
severity: "warning",
61+
path: auditPath,
62+
}),
63+
);
64+
expect(configAuditScrubToRepairEffect(result)).toEqual({
65+
kind: "file",
66+
action: "would-scrub-config-audit-log",
67+
target: auditPath,
68+
dryRunSafe: false,
69+
});
70+
});
71+
});

src/commands/doctor-config-audit-scrub.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,62 @@
22
import fs from "node:fs/promises";
33
import os from "node:os";
44
import { note } from "../../packages/terminal-core/src/note.js";
5-
import { scrubConfigAuditLog } from "../config/io.audit.js";
5+
import {
6+
resolveConfigAuditLogPath,
7+
scrubConfigAuditLog,
8+
type ConfigAuditScrubResult,
9+
} from "../config/io.audit.js";
10+
import type { HealthFinding, HealthRepairEffect } from "../flows/health-checks.js";
611

712
const NOTE_TITLE = "Config audit";
13+
const CONFIG_AUDIT_SCRUB_CHECK_ID = "core/doctor/config-audit-scrub";
814

915
function formatEntryCount(count: number): string {
1016
return `${count} ${count === 1 ? "entry" : "entries"}`;
1117
}
1218

19+
export async function detectConfigAuditScrubIssue(params?: {
20+
env?: NodeJS.ProcessEnv;
21+
homedir?: () => string;
22+
}): Promise<ConfigAuditScrubResult & { auditPath: string }> {
23+
const env = params?.env ?? process.env;
24+
const homedir = params?.homedir ?? os.homedir;
25+
const result = await scrubConfigAuditLog({
26+
fs: { promises: fs },
27+
env,
28+
homedir,
29+
dryRun: true,
30+
});
31+
return {
32+
...result,
33+
auditPath: resolveConfigAuditLogPath(env, homedir),
34+
};
35+
}
36+
37+
export function configAuditScrubToHealthFinding(
38+
result: ConfigAuditScrubResult & { auditPath: string },
39+
): HealthFinding {
40+
return {
41+
checkId: CONFIG_AUDIT_SCRUB_CHECK_ID,
42+
severity: "warning",
43+
message: `${formatEntryCount(result.rewritten)} in config-audit.jsonl still contain pre-redactor argv values.`,
44+
path: result.auditPath,
45+
fixHint:
46+
"Run `openclaw doctor --fix` to rewrite argv/execArgv fields through the current redactor.",
47+
};
48+
}
49+
50+
export function configAuditScrubToRepairEffect(
51+
result: ConfigAuditScrubResult & { auditPath: string },
52+
): HealthRepairEffect {
53+
return {
54+
kind: "file",
55+
action: "would-scrub-config-audit-log",
56+
target: result.auditPath,
57+
dryRunSafe: false,
58+
};
59+
}
60+
1361
/**
1462
* Scrubs pre-redactor config audit records or previews the number of affected entries.
1563
*

src/config/io.audit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ function resolveConfigAuditAppendRecord(params: ConfigAuditAppendParams): Config
430430
return redactSecrets(record as ConfigAuditRecord);
431431
}
432432

433-
type ConfigAuditScrubResult = {
433+
export type ConfigAuditScrubResult = {
434434
scanned: number;
435435
rewritten: number;
436436
skipped: number;

src/flows/doctor-health-contributions.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,7 @@ describe("doctor health contributions", () => {
10131013
}
10141014
expect(contributionIds).toContain("core/doctor/sandbox/registry-files");
10151015
expect(contributionIds).toContain("core/doctor/gateway-services/extra");
1016+
expect(contributionIds).toContain("core/doctor/config-audit-scrub");
10161017
expect(contributionChecks.map((check) => check.id)).toEqual(contributionIds);
10171018
});
10181019

src/flows/doctor-health-contributions.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,32 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] {
13081308
createDoctorHealthContribution({
13091309
id: "doctor:config-audit-scrub",
13101310
label: "Config audit",
1311+
healthChecks: {
1312+
description:
1313+
"Historical config-audit argv redaction gaps are represented as structured findings.",
1314+
defaultEnabled: false,
1315+
async detect() {
1316+
const { configAuditScrubToHealthFinding, detectConfigAuditScrubIssue } =
1317+
await import("../commands/doctor-config-audit-scrub.js");
1318+
const result = await detectConfigAuditScrubIssue();
1319+
return result.rewritten > 0 ? [configAuditScrubToHealthFinding(result)] : [];
1320+
},
1321+
async repair(ctx) {
1322+
const { configAuditScrubToRepairEffect, detectConfigAuditScrubIssue } =
1323+
await import("../commands/doctor-config-audit-scrub.js");
1324+
const result = await detectConfigAuditScrubIssue();
1325+
const effects = result.rewritten > 0 ? [configAuditScrubToRepairEffect(result)] : [];
1326+
if (ctx.dryRun === true) {
1327+
return { status: "repaired", changes: [], effects };
1328+
}
1329+
return {
1330+
status: "skipped",
1331+
reason: "legacy doctor config audit contribution owns cleanup",
1332+
changes: [],
1333+
effects,
1334+
};
1335+
},
1336+
},
13111337
run: runConfigAuditScrubHealth,
13121338
}),
13131339
createDoctorHealthContribution({

0 commit comments

Comments
 (0)