From d3bfbdec5dc5c85305caa0f129f5d4b3c504f559 Mon Sep 17 00:00:00 2001 From: chilu18 Date: Mon, 23 Feb 2026 20:18:26 +0000 Subject: [PATCH] fix(config): add actionable guidance for dmPolicy open allowFrom mismatch --- src/config/io.ts | 27 ++++++++++++++++++++++++++- src/config/io.write-config.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/config/io.ts b/src/config/io.ts index 574b52ee293..c63c2e64fae 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -71,6 +71,9 @@ const SHELL_ENV_EXPECTED_KEYS = [ "OPENCLAW_GATEWAY_PASSWORD", ]; +const OPEN_DM_POLICY_ALLOW_FROM_RE = + /^(?[a-z0-9_.-]+)\s*=\s*"open"\s+requires\s+(?[a-z0-9_.-]+)(?:\s+\(or\s+[a-z0-9_.-]+\))?\s+to include "\*"$/i; + const CONFIG_AUDIT_LOG_FILENAME = "config-audit.jsonl"; const loggedInvalidConfigs = new Set(); @@ -136,6 +139,27 @@ function hashConfigRaw(raw: string | null): string { .digest("hex"); } +function formatConfigValidationFailure(pathLabel: string, issueMessage: string): string { + const match = issueMessage.match(OPEN_DM_POLICY_ALLOW_FROM_RE); + const policyPath = match?.groups?.policyPath?.trim(); + const allowPath = match?.groups?.allowPath?.trim(); + if (!policyPath || !allowPath) { + return `Config validation failed: ${pathLabel}: ${issueMessage}`; + } + + return [ + `Config validation failed: ${pathLabel}`, + "", + `Configuration mismatch: ${policyPath} is "open", but ${allowPath} does not include "*".`, + "", + "Fix with:", + ` openclaw config set ${allowPath} '["*"]'`, + "", + "Or switch policy:", + ` openclaw config set ${policyPath} "pairing"`, + ].join("\n"); +} + function isNumericPathSegment(raw: string): boolean { return /^[0-9]+$/.test(raw); } @@ -999,7 +1023,8 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { if (!validated.ok) { const issue = validated.issues[0]; const pathLabel = issue?.path ? issue.path : ""; - throw new Error(`Config validation failed: ${pathLabel}: ${issue?.message ?? "invalid"}`); + const issueMessage = issue?.message ?? "invalid"; + throw new Error(formatConfigValidationFailure(pathLabel, issueMessage)); } if (validated.warnings.length > 0) { const details = validated.warnings diff --git a/src/config/io.write-config.test.ts b/src/config/io.write-config.test.ts index 17f1951de33..66aeebf3e77 100644 --- a/src/config/io.write-config.test.ts +++ b/src/config/io.write-config.test.ts @@ -96,6 +96,32 @@ describe("config io write", () => { }); }); + it('shows actionable guidance for dmPolicy="open" without wildcard allowFrom', async () => { + await withTempHome("openclaw-config-io-", async (home) => { + const io = createConfigIO({ + env: {} as NodeJS.ProcessEnv, + homedir: () => home, + logger: silentLogger, + }); + + const invalidConfig = { + channels: { + telegram: { + dmPolicy: "open", + allowFrom: [], + }, + }, + }; + + await expect(io.writeConfigFile(invalidConfig)).rejects.toThrow( + "openclaw config set channels.telegram.allowFrom '[\"*\"]'", + ); + await expect(io.writeConfigFile(invalidConfig)).rejects.toThrow( + 'openclaw config set channels.telegram.dmPolicy "pairing"', + ); + }); + }); + it("honors explicit unset paths when schema defaults would otherwise reappear", async () => { await withTempHome("openclaw-config-io-", async (home) => { const { configPath, io, snapshot } = await writeConfigAndCreateIo({