mirror of https://github.com/openclaw/openclaw.git
refactor(doctor): continue doctor flow extraction (#51920)
* refactor(doctor): extract shared warning formatters * refactor(doctor): extract provider warning previews * style(doctor): sort telegram imports
This commit is contained in:
parent
37d5cbe43a
commit
91f404dc7e
|
|
@ -12,6 +12,7 @@ import { runDoctorConfigPreflight } from "./doctor-config-preflight.js";
|
|||
import { normalizeCompatibilityConfigValues } from "./doctor-legacy-config.js";
|
||||
import type { DoctorOptions } from "./doctor-prompter.js";
|
||||
import {
|
||||
collectDiscordNumericIdWarnings,
|
||||
maybeRepairDiscordNumericIds,
|
||||
scanDiscordNumericIdEntries,
|
||||
} from "./doctor/providers/discord.js";
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
formatMatrixLegacyStatePreview,
|
||||
} from "./doctor/providers/matrix.js";
|
||||
import {
|
||||
collectTelegramAllowFromUsernameWarnings,
|
||||
collectTelegramEmptyAllowlistExtraWarnings,
|
||||
maybeRepairTelegramAllowFromUsernames,
|
||||
scanTelegramAllowFromUsernameEntries,
|
||||
|
|
@ -33,16 +35,25 @@ import {
|
|||
} from "./doctor/shared/default-account-warnings.js";
|
||||
import { scanEmptyAllowlistPolicyWarnings } from "./doctor/shared/empty-allowlist-scan.js";
|
||||
import {
|
||||
collectExecSafeBinCoverageWarnings,
|
||||
collectExecSafeBinTrustedDirHintWarnings,
|
||||
maybeRepairExecSafeBinProfiles,
|
||||
scanExecSafeBinCoverage,
|
||||
scanExecSafeBinTrustedDirHints,
|
||||
} from "./doctor/shared/exec-safe-bins.js";
|
||||
import {
|
||||
collectLegacyToolsBySenderWarnings,
|
||||
maybeRepairLegacyToolsBySenderKeys,
|
||||
scanLegacyToolsBySenderKeys,
|
||||
} from "./doctor/shared/legacy-tools-by-sender.js";
|
||||
import { scanMutableAllowlistEntries } from "./doctor/shared/mutable-allowlist.js";
|
||||
import { maybeRepairOpenPolicyAllowFrom } from "./doctor/shared/open-policy-allowfrom.js";
|
||||
import {
|
||||
collectMutableAllowlistWarnings,
|
||||
scanMutableAllowlistEntries,
|
||||
} from "./doctor/shared/mutable-allowlist.js";
|
||||
import {
|
||||
collectOpenPolicyAllowFromWarnings,
|
||||
maybeRepairOpenPolicyAllowFrom,
|
||||
} from "./doctor/shared/open-policy-allowfrom.js";
|
||||
|
||||
export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
options: DoctorOptions;
|
||||
|
|
@ -225,25 +236,22 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
|||
} else {
|
||||
const hits = scanTelegramAllowFromUsernameEntries(candidate);
|
||||
if (hits.length > 0) {
|
||||
const sampleEntry = sanitizeForLog(hits[0]?.entry ?? "@");
|
||||
note(
|
||||
[
|
||||
`- Telegram allowFrom contains ${hits.length} non-numeric entries (e.g. ${sampleEntry}); Telegram authorization requires numeric sender IDs.`,
|
||||
`- Run "${formatCliCommand("openclaw doctor --fix")}" to auto-resolve @username entries to numeric IDs (requires a Telegram bot token).`,
|
||||
].join("\n"),
|
||||
collectTelegramAllowFromUsernameWarnings({
|
||||
hits,
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
}).join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
|
||||
const discordHits = scanDiscordNumericIdEntries(candidate);
|
||||
if (discordHits.length > 0) {
|
||||
const samplePath = sanitizeForLog(discordHits[0]?.path ?? "channels.discord.allowFrom");
|
||||
const sampleEntry = sanitizeForLog(String(discordHits[0]?.entry ?? ""));
|
||||
note(
|
||||
[
|
||||
`- Discord allowlists contain ${discordHits.length} numeric entries (e.g. ${samplePath}=${sampleEntry}).`,
|
||||
`- Discord IDs must be strings; run "${formatCliCommand("openclaw doctor --fix")}" to convert numeric IDs to quoted strings.`,
|
||||
].join("\n"),
|
||||
collectDiscordNumericIdWarnings({
|
||||
hits: discordHits,
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
}).join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
|
|
@ -251,10 +259,10 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
|||
const allowFromScan = maybeRepairOpenPolicyAllowFrom(candidate);
|
||||
if (allowFromScan.changes.length > 0) {
|
||||
note(
|
||||
[
|
||||
...allowFromScan.changes.map((line) => sanitizeForLog(line)),
|
||||
`- Run "${formatCliCommand("openclaw doctor --fix")}" to add missing allowFrom wildcards.`,
|
||||
].join("\n"),
|
||||
collectOpenPolicyAllowFromWarnings({
|
||||
changes: allowFromScan.changes,
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
}).join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
|
|
@ -272,101 +280,38 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
|||
|
||||
const toolsBySenderHits = scanLegacyToolsBySenderKeys(candidate);
|
||||
if (toolsBySenderHits.length > 0) {
|
||||
const sample = toolsBySenderHits[0];
|
||||
const sampleLabel = sanitizeForLog(
|
||||
sample ? `${sample.pathLabel}.${sample.key}` : "toolsBySender",
|
||||
);
|
||||
note(
|
||||
[
|
||||
`- Found ${toolsBySenderHits.length} legacy untyped toolsBySender key${toolsBySenderHits.length === 1 ? "" : "s"} (for example ${sampleLabel}).`,
|
||||
"- Untyped sender keys are deprecated; use explicit prefixes (id:, e164:, username:, name:).",
|
||||
`- Run "${formatCliCommand("openclaw doctor --fix")}" to migrate legacy keys to typed id: entries.`,
|
||||
].join("\n"),
|
||||
collectLegacyToolsBySenderWarnings({
|
||||
hits: toolsBySenderHits,
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
}).join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
|
||||
const safeBinCoverage = scanExecSafeBinCoverage(candidate);
|
||||
if (safeBinCoverage.length > 0) {
|
||||
const interpreterHits = safeBinCoverage.filter((hit) => hit.isInterpreter);
|
||||
const customHits = safeBinCoverage.filter((hit) => !hit.isInterpreter);
|
||||
const lines: string[] = [];
|
||||
if (interpreterHits.length > 0) {
|
||||
for (const hit of interpreterHits.slice(0, 5)) {
|
||||
lines.push(
|
||||
`- ${sanitizeForLog(hit.scopePath)}.safeBins includes interpreter/runtime '${sanitizeForLog(hit.bin)}' without profile.`,
|
||||
);
|
||||
}
|
||||
if (interpreterHits.length > 5) {
|
||||
lines.push(
|
||||
`- ${interpreterHits.length - 5} more interpreter/runtime safeBins entries are missing profiles.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (customHits.length > 0) {
|
||||
for (const hit of customHits.slice(0, 5)) {
|
||||
lines.push(
|
||||
`- ${sanitizeForLog(hit.scopePath)}.safeBins entry '${sanitizeForLog(hit.bin)}' is missing safeBinProfiles.${sanitizeForLog(hit.bin)}.`,
|
||||
);
|
||||
}
|
||||
if (customHits.length > 5) {
|
||||
lines.push(
|
||||
`- ${customHits.length - 5} more custom safeBins entries are missing profiles.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
`- Run "${formatCliCommand("openclaw doctor --fix")}" to scaffold missing custom safeBinProfiles entries.`,
|
||||
note(
|
||||
collectExecSafeBinCoverageWarnings({
|
||||
hits: safeBinCoverage,
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
}).join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
note(lines.join("\n"), "Doctor warnings");
|
||||
}
|
||||
|
||||
const safeBinTrustedDirHints = scanExecSafeBinTrustedDirHints(candidate);
|
||||
if (safeBinTrustedDirHints.length > 0) {
|
||||
const lines = safeBinTrustedDirHints
|
||||
.slice(0, 5)
|
||||
.map(
|
||||
(hit) =>
|
||||
`- ${sanitizeForLog(hit.scopePath)}.safeBins entry '${sanitizeForLog(hit.bin)}' resolves to '${sanitizeForLog(hit.resolvedPath)}' outside trusted safe-bin dirs.`,
|
||||
);
|
||||
if (safeBinTrustedDirHints.length > 5) {
|
||||
lines.push(
|
||||
`- ${safeBinTrustedDirHints.length - 5} more safeBins entries resolve outside trusted safe-bin dirs.`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
"- If intentional, add the binary directory to tools.exec.safeBinTrustedDirs (global or agent scope).",
|
||||
note(
|
||||
collectExecSafeBinTrustedDirHintWarnings(safeBinTrustedDirHints).join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
note(lines.join("\n"), "Doctor warnings");
|
||||
}
|
||||
}
|
||||
|
||||
const mutableAllowlistHits = scanMutableAllowlistEntries(candidate);
|
||||
if (mutableAllowlistHits.length > 0) {
|
||||
const channels = Array.from(new Set(mutableAllowlistHits.map((hit) => hit.channel))).toSorted();
|
||||
const exampleLines = mutableAllowlistHits
|
||||
.slice(0, 8)
|
||||
.map((hit) => `- ${sanitizeForLog(hit.path)}: ${sanitizeForLog(hit.entry)}`)
|
||||
.join("\n");
|
||||
const remaining =
|
||||
mutableAllowlistHits.length > 8
|
||||
? `- +${mutableAllowlistHits.length - 8} more mutable allowlist entries.`
|
||||
: null;
|
||||
const flagPaths = Array.from(new Set(mutableAllowlistHits.map((hit) => hit.dangerousFlagPath)));
|
||||
const flagHint =
|
||||
flagPaths.length === 1
|
||||
? sanitizeForLog(flagPaths[0] ?? "")
|
||||
: `${sanitizeForLog(flagPaths[0] ?? "")} (and ${flagPaths.length - 1} other scope flags)`;
|
||||
note(
|
||||
[
|
||||
`- Found ${mutableAllowlistHits.length} mutable allowlist ${mutableAllowlistHits.length === 1 ? "entry" : "entries"} across ${channels.join(", ")} while name matching is disabled by default.`,
|
||||
exampleLines,
|
||||
...(remaining ? [remaining] : []),
|
||||
`- Option A (break-glass): enable ${flagHint}=true to keep name/email/nick matching.`,
|
||||
"- Option B (recommended): resolve names/emails/nicks to stable sender IDs and rewrite the allowlist entries.",
|
||||
].join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
note(collectMutableAllowlistWarnings(mutableAllowlistHits).join("\n"), "Doctor warnings");
|
||||
}
|
||||
|
||||
const unknown = stripUnknownConfigKeys(candidate);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { maybeRepairDiscordNumericIds, scanDiscordNumericIdEntries } from "./discord.js";
|
||||
import {
|
||||
collectDiscordNumericIdWarnings,
|
||||
maybeRepairDiscordNumericIds,
|
||||
scanDiscordNumericIdEntries,
|
||||
} from "./discord.js";
|
||||
|
||||
describe("doctor discord provider repairs", () => {
|
||||
it("finds numeric id entries across discord scopes", () => {
|
||||
|
|
@ -66,4 +70,16 @@ describe("doctor discord provider repairs", () => {
|
|||
"456",
|
||||
]);
|
||||
});
|
||||
|
||||
it("formats numeric id warnings", () => {
|
||||
const warnings = collectDiscordNumericIdWarnings({
|
||||
hits: [{ path: "channels.discord.allowFrom[0]", entry: 123 }],
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining("Discord allowlists contain 1 numeric entries"),
|
||||
expect.stringContaining('run "openclaw doctor --fix"'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { asObjectRecord } from "../shared/object.js";
|
||||
import type { DoctorAccountRecord } from "../types.js";
|
||||
|
||||
|
|
@ -114,6 +115,21 @@ export function scanDiscordNumericIdEntries(cfg: OpenClawConfig): DiscordNumeric
|
|||
return hits;
|
||||
}
|
||||
|
||||
export function collectDiscordNumericIdWarnings(params: {
|
||||
hits: DiscordNumericIdHit[];
|
||||
doctorFixCommand: string;
|
||||
}): string[] {
|
||||
if (params.hits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const samplePath = sanitizeForLog(params.hits[0]?.path ?? "channels.discord.allowFrom");
|
||||
const sampleEntry = sanitizeForLog(String(params.hits[0]?.entry ?? ""));
|
||||
return [
|
||||
`- Discord allowlists contain ${params.hits.length} numeric entries (e.g. ${samplePath}=${sampleEntry}).`,
|
||||
`- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`,
|
||||
];
|
||||
}
|
||||
|
||||
export function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectTelegramAllowFromUsernameWarnings,
|
||||
collectTelegramEmptyAllowlistExtraWarnings,
|
||||
collectTelegramGroupPolicyWarnings,
|
||||
scanTelegramAllowFromUsernameEntries,
|
||||
|
|
@ -120,4 +121,16 @@ describe("doctor telegram provider warnings", () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("formats allowFrom username warnings", () => {
|
||||
const warnings = collectTelegramAllowFromUsernameWarnings({
|
||||
hits: [{ path: "channels.telegram.allowFrom", entry: "@top" }],
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining("Telegram allowFrom contains 1 non-numeric entries"),
|
||||
expect.stringContaining('Run "openclaw doctor --fix"'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -127,6 +127,20 @@ export function scanTelegramAllowFromUsernameEntries(
|
|||
return hits;
|
||||
}
|
||||
|
||||
export function collectTelegramAllowFromUsernameWarnings(params: {
|
||||
hits: TelegramAllowFromUsernameHit[];
|
||||
doctorFixCommand: string;
|
||||
}): string[] {
|
||||
if (params.hits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const sampleEntry = sanitizeForLog(params.hits[0]?.entry ?? "@");
|
||||
return [
|
||||
`- Telegram allowFrom contains ${params.hits.length} non-numeric entries (e.g. ${sampleEntry}); Telegram authorization requires numeric sender IDs.`,
|
||||
`- Run "${params.doctorFixCommand}" to auto-resolve @username entries to numeric IDs (requires a Telegram bot token).`,
|
||||
];
|
||||
}
|
||||
|
||||
export async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promise<{
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { delimiter, join } from "node:path";
|
|||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
collectExecSafeBinCoverageWarnings,
|
||||
collectExecSafeBinTrustedDirHintWarnings,
|
||||
maybeRepairExecSafeBinProfiles,
|
||||
scanExecSafeBinCoverage,
|
||||
scanExecSafeBinTrustedDirHints,
|
||||
|
|
@ -29,6 +31,22 @@ describe("doctor exec safe bin helpers", () => {
|
|||
expect(hits).toEqual([{ scopePath: "tools.exec", bin: "node", isInterpreter: true }]);
|
||||
});
|
||||
|
||||
it("formats coverage warnings", () => {
|
||||
const warnings = collectExecSafeBinCoverageWarnings({
|
||||
hits: [
|
||||
{ scopePath: "tools.exec", bin: "node", isInterpreter: true },
|
||||
{ scopePath: "agents.list.runner.tools.exec", bin: "jq", isInterpreter: false },
|
||||
],
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining("tools.exec.safeBins includes interpreter/runtime 'node'"),
|
||||
expect.stringContaining("agents.list.runner.tools.exec.safeBins entry 'jq'"),
|
||||
expect.stringContaining('Run "openclaw doctor --fix"'),
|
||||
]);
|
||||
});
|
||||
|
||||
it("scaffolds custom safeBin profiles but warns on interpreters", () => {
|
||||
const result = maybeRepairExecSafeBinProfiles({
|
||||
tools: {
|
||||
|
|
@ -70,6 +88,13 @@ describe("doctor exec safe bin helpers", () => {
|
|||
resolvedPath: binPath,
|
||||
});
|
||||
|
||||
expect(collectExecSafeBinTrustedDirHintWarnings(hits)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("tools.exec.safeBins entry 'custom-safe-bin'"),
|
||||
expect.stringContaining("tools.exec.safeBinTrustedDirs"),
|
||||
]),
|
||||
);
|
||||
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
isTrustedSafeBinPath,
|
||||
normalizeTrustedSafeBinDirs,
|
||||
} from "../../../infra/exec-safe-bin-trust.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
export type ExecSafeBinCoverageHit = {
|
||||
|
|
@ -153,6 +154,65 @@ export function scanExecSafeBinTrustedDirHints(
|
|||
return hits;
|
||||
}
|
||||
|
||||
export function collectExecSafeBinCoverageWarnings(params: {
|
||||
hits: ExecSafeBinCoverageHit[];
|
||||
doctorFixCommand: string;
|
||||
}): string[] {
|
||||
if (params.hits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const interpreterHits = params.hits.filter((hit) => hit.isInterpreter);
|
||||
const customHits = params.hits.filter((hit) => !hit.isInterpreter);
|
||||
const lines: string[] = [];
|
||||
if (interpreterHits.length > 0) {
|
||||
for (const hit of interpreterHits.slice(0, 5)) {
|
||||
lines.push(
|
||||
`- ${sanitizeForLog(hit.scopePath)}.safeBins includes interpreter/runtime '${sanitizeForLog(hit.bin)}' without profile.`,
|
||||
);
|
||||
}
|
||||
if (interpreterHits.length > 5) {
|
||||
lines.push(
|
||||
`- ${interpreterHits.length - 5} more interpreter/runtime safeBins entries are missing profiles.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (customHits.length > 0) {
|
||||
for (const hit of customHits.slice(0, 5)) {
|
||||
lines.push(
|
||||
`- ${sanitizeForLog(hit.scopePath)}.safeBins entry '${sanitizeForLog(hit.bin)}' is missing safeBinProfiles.${sanitizeForLog(hit.bin)}.`,
|
||||
);
|
||||
}
|
||||
if (customHits.length > 5) {
|
||||
lines.push(`- ${customHits.length - 5} more custom safeBins entries are missing profiles.`);
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
`- Run "${params.doctorFixCommand}" to scaffold missing custom safeBinProfiles entries.`,
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function collectExecSafeBinTrustedDirHintWarnings(
|
||||
hits: ExecSafeBinTrustedDirHintHit[],
|
||||
): string[] {
|
||||
if (hits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const lines = hits
|
||||
.slice(0, 5)
|
||||
.map(
|
||||
(hit) =>
|
||||
`- ${sanitizeForLog(hit.scopePath)}.safeBins entry '${sanitizeForLog(hit.bin)}' resolves to '${sanitizeForLog(hit.resolvedPath)}' outside trusted safe-bin dirs.`,
|
||||
);
|
||||
if (hits.length > 5) {
|
||||
lines.push(`- ${hits.length - 5} more safeBins entries resolve outside trusted safe-bin dirs.`);
|
||||
}
|
||||
lines.push(
|
||||
"- If intentional, add the binary directory to tools.exec.safeBinTrustedDirs (global or agent scope).",
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function maybeRepairExecSafeBinProfiles(cfg: OpenClawConfig): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
collectLegacyToolsBySenderWarnings,
|
||||
maybeRepairLegacyToolsBySenderKeys,
|
||||
scanLegacyToolsBySenderKeys,
|
||||
} from "./legacy-tools-by-sender.js";
|
||||
|
|
@ -59,4 +60,24 @@ describe("doctor legacy toolsBySender helpers", () => {
|
|||
"id:alice": { deny: ["exec"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("formats legacy sender key warnings", () => {
|
||||
const warnings = collectLegacyToolsBySenderWarnings({
|
||||
hits: [
|
||||
{
|
||||
toolsBySenderPath: ["channels", "whatsapp", "groups", "123@g.us", "toolsBySender"],
|
||||
pathLabel: "channels.whatsapp.groups.123@g.us.toolsBySender",
|
||||
key: "owner",
|
||||
targetKey: "id:owner",
|
||||
},
|
||||
],
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining("legacy untyped toolsBySender key"),
|
||||
expect.stringContaining("explicit prefixes"),
|
||||
expect.stringContaining('Run "openclaw doctor --fix"'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { parseToolsBySenderTypedKey } from "../../../config/types.tools.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { formatConfigPath, resolveConfigPathTarget } from "../../doctor-config-analysis.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
|
|
@ -58,6 +59,24 @@ export function scanLegacyToolsBySenderKeys(cfg: OpenClawConfig): LegacyToolsByS
|
|||
return hits;
|
||||
}
|
||||
|
||||
export function collectLegacyToolsBySenderWarnings(params: {
|
||||
hits: LegacyToolsBySenderKeyHit[];
|
||||
doctorFixCommand: string;
|
||||
}): string[] {
|
||||
if (params.hits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const sample = params.hits[0];
|
||||
const sampleLabel = sanitizeForLog(
|
||||
sample ? `${sample.pathLabel}.${sample.key}` : "toolsBySender",
|
||||
);
|
||||
return [
|
||||
`- Found ${params.hits.length} legacy untyped toolsBySender key${params.hits.length === 1 ? "" : "s"} (for example ${sampleLabel}).`,
|
||||
"- Untyped sender keys are deprecated; use explicit prefixes (id:, e164:, username:, name:).",
|
||||
`- Run "${params.doctorFixCommand}" to migrate legacy keys to typed id: entries.`,
|
||||
];
|
||||
}
|
||||
|
||||
export function maybeRepairLegacyToolsBySenderKeys(cfg: OpenClawConfig): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { scanMutableAllowlistEntries } from "./mutable-allowlist.js";
|
||||
import {
|
||||
collectMutableAllowlistWarnings,
|
||||
scanMutableAllowlistEntries,
|
||||
} from "./mutable-allowlist.js";
|
||||
|
||||
describe("doctor mutable allowlist scanner", () => {
|
||||
it("finds mutable discord, irc, and zalouser entries when dangerous matching is disabled", () => {
|
||||
|
|
@ -74,4 +77,31 @@ describe("doctor mutable allowlist scanner", () => {
|
|||
|
||||
expect(hits).toEqual([]);
|
||||
});
|
||||
|
||||
it("formats mutable allowlist warnings", () => {
|
||||
const warnings = collectMutableAllowlistWarnings([
|
||||
{
|
||||
channel: "discord",
|
||||
path: "channels.discord.allowFrom",
|
||||
entry: "alice",
|
||||
dangerousFlagPath: "channels.discord.dangerouslyAllowNameMatching",
|
||||
},
|
||||
{
|
||||
channel: "irc",
|
||||
path: "channels.irc.allowFrom",
|
||||
entry: "bob",
|
||||
dangerousFlagPath: "channels.irc.dangerouslyAllowNameMatching",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("mutable allowlist entries across discord, irc"),
|
||||
expect.stringContaining("channels.discord.allowFrom: alice"),
|
||||
expect.stringContaining("channels.irc.allowFrom: bob"),
|
||||
expect.stringContaining("Option A"),
|
||||
expect.stringContaining("Option B"),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
isSlackMutableAllowEntry,
|
||||
isZalouserMutableGroupEntry,
|
||||
} from "../../../security/mutable-allowlist-detectors.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
export type MutableAllowlistHit = {
|
||||
|
|
@ -303,3 +304,27 @@ export function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowli
|
|||
|
||||
return hits;
|
||||
}
|
||||
|
||||
export function collectMutableAllowlistWarnings(hits: MutableAllowlistHit[]): string[] {
|
||||
if (hits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const channels = Array.from(new Set(hits.map((hit) => hit.channel))).toSorted();
|
||||
const exampleLines = hits
|
||||
.slice(0, 8)
|
||||
.map((hit) => `- ${sanitizeForLog(hit.path)}: ${sanitizeForLog(hit.entry)}`);
|
||||
const remaining =
|
||||
hits.length > 8 ? `- +${hits.length - 8} more mutable allowlist entries.` : null;
|
||||
const flagPaths = Array.from(new Set(hits.map((hit) => hit.dangerousFlagPath)));
|
||||
const flagHint =
|
||||
flagPaths.length === 1
|
||||
? sanitizeForLog(flagPaths[0] ?? "")
|
||||
: `${sanitizeForLog(flagPaths[0] ?? "")} (and ${flagPaths.length - 1} other scope flags)`;
|
||||
return [
|
||||
`- Found ${hits.length} mutable allowlist ${hits.length === 1 ? "entry" : "entries"} across ${channels.join(", ")} while name matching is disabled by default.`,
|
||||
...exampleLines,
|
||||
...(remaining ? [remaining] : []),
|
||||
`- Option A (break-glass): enable ${flagHint}=true to keep name/email/nick matching.`,
|
||||
"- Option B (recommended): resolve names/emails/nicks to stable sender IDs and rewrite the allowlist entries.",
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { maybeRepairOpenPolicyAllowFrom } from "./open-policy-allowfrom.js";
|
||||
import {
|
||||
collectOpenPolicyAllowFromWarnings,
|
||||
maybeRepairOpenPolicyAllowFrom,
|
||||
} from "./open-policy-allowfrom.js";
|
||||
|
||||
describe("doctor open-policy allowFrom repair", () => {
|
||||
it('adds top-level wildcard when dmPolicy="open" has no allowFrom', () => {
|
||||
|
|
@ -51,4 +54,16 @@ describe("doctor open-policy allowFrom repair", () => {
|
|||
]);
|
||||
expect(result.config.channels?.discord?.dm?.allowFrom).toEqual(["123", "*"]);
|
||||
});
|
||||
|
||||
it("formats open-policy wildcard warnings", () => {
|
||||
const warnings = collectOpenPolicyAllowFromWarnings({
|
||||
changes: ['- channels.signal.allowFrom: set to ["*"] (required by dmPolicy="open")'],
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining('channels.signal.allowFrom: set to ["*"]'),
|
||||
expect.stringContaining('Run "openclaw doctor --fix"'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { resolveAllowFromMode, type AllowFromMode } from "./allow-from-mode.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
|
|
@ -6,6 +7,19 @@ function hasWildcard(list?: Array<string | number>) {
|
|||
return list?.some((v) => String(v).trim() === "*") ?? false;
|
||||
}
|
||||
|
||||
export function collectOpenPolicyAllowFromWarnings(params: {
|
||||
changes: string[];
|
||||
doctorFixCommand: string;
|
||||
}): string[] {
|
||||
if (params.changes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
...params.changes.map((line) => sanitizeForLog(line)),
|
||||
`- Run "${params.doctorFixCommand}" to add missing allowFrom wildcards.`,
|
||||
];
|
||||
}
|
||||
|
||||
export function maybeRepairOpenPolicyAllowFrom(cfg: OpenClawConfig): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
|
|
|
|||
Loading…
Reference in New Issue