mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(doctor): repair googlechat open dm wildcard auto-fix
This commit is contained in:
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091)
|
||||
- CLI/QR: restore fail-fast validation for `openclaw qr --remote` when neither `gateway.remote.url` nor tailscale `serve`/`funnel` is configured, preventing unusable remote pairing QR flows. (#18166) Thanks @mbelinky.
|
||||
- CLI/Doctor: ensure `openclaw doctor --fix --non-interactive --yes` exits promptly after completion so one-shot automation no longer hangs. (#18502)
|
||||
- CLI/Doctor: auto-repair `dmPolicy="open"` configs missing wildcard allowlists and write channel-correct repair paths (including `channels.googlechat.dm.allowFrom`) so `openclaw doctor --fix` no longer leaves Google Chat configs invalid after attempted repair. (#18544)
|
||||
- Auto-reply/Subagents: propagate group context (`groupId`, `groupChannel`, `space`) when spawning via `/subagents spawn`, matching tool-triggered subagent spawn behavior.
|
||||
- Agents/Tools/exec: add a preflight guard that detects likely shell env var injection (e.g. `$DM_JSON`, `$TMPDIR`) in Python/Node scripts before execution, preventing recurring cron failures and wasted tokens when models emit mixed shell+language source. (#12836)
|
||||
- Agents/Tools: make loop detection progress-aware and phased by hard-blocking known `process(action=poll|log)` no-progress loops, warning on generic identical-call repeats, warning + no-progress-blocking ping-pong alternation loops (10/20), coalescing repeated warning spam into threshold buckets (including canonical ping-pong pairs), adding a global circuit breaker at 30 no-progress repeats, and emitting structured diagnostic `tool.loop` warning/error events for loop actions. (#16808) Thanks @akramcodez and @beca-oc.
|
||||
|
||||
@@ -346,4 +346,103 @@ describe("doctor config flow", () => {
|
||||
};
|
||||
expect(cfg.channels.discord.accounts.work.allowFrom).toEqual(["*"]);
|
||||
});
|
||||
|
||||
it("repairs googlechat dm.policy open by setting dm.allowFrom on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
channels: {
|
||||
googlechat: {
|
||||
dm: {
|
||||
policy: "open",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cfg = result.cfg as unknown as {
|
||||
channels: {
|
||||
googlechat: {
|
||||
dm: {
|
||||
policy: string;
|
||||
allowFrom: string[];
|
||||
};
|
||||
allowFrom?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(cfg.channels.googlechat.dm.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.googlechat.allowFrom).toBeUndefined();
|
||||
});
|
||||
|
||||
it("repairs googlechat account dm.policy open by setting dm.allowFrom on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
work: {
|
||||
dm: {
|
||||
policy: "open",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cfg = result.cfg as unknown as {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
work: {
|
||||
dm: {
|
||||
policy: string;
|
||||
allowFrom: string[];
|
||||
};
|
||||
allowFrom?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(cfg.channels.googlechat.accounts.work.dm.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.googlechat.accounts.work.allowFrom).toBeUndefined();
|
||||
});
|
||||
|
||||
it("recovers from stale googlechat top-level allowFrom by repairing dm.allowFrom", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
channels: {
|
||||
googlechat: {
|
||||
allowFrom: ["*"],
|
||||
dm: {
|
||||
policy: "open",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cfg = result.cfg as unknown as {
|
||||
channels: {
|
||||
googlechat: {
|
||||
dm: {
|
||||
policy: string;
|
||||
allowFrom: string[];
|
||||
};
|
||||
allowFrom?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(cfg.channels.googlechat.dm.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.googlechat.allowFrom).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { ZodIssue } from "zod";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ZodIssue } from "zod";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { DoctorOptions } from "./doctor-prompter.js";
|
||||
import {
|
||||
isNumericTelegramUserId,
|
||||
normalizeTelegramAllowFromEntry,
|
||||
} from "../channels/telegram/allow-from.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
OpenClawSchema,
|
||||
CONFIG_PATH,
|
||||
@@ -18,7 +19,6 @@ import { listTelegramAccountIds, resolveTelegramAccount } from "../telegram/acco
|
||||
import { note } from "../terminal/note.js";
|
||||
import { isRecord, resolveHomeDir } from "../utils.js";
|
||||
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
|
||||
import type { DoctorOptions } from "./doctor-prompter.js";
|
||||
import { autoMigrateLegacyStateDir } from "./doctor-state-migrations.js";
|
||||
|
||||
type UnrecognizedKeysIssue = ZodIssue & {
|
||||
@@ -571,34 +571,81 @@ function maybeRepairOpenPolicyAllowFrom(cfg: OpenClawConfig): {
|
||||
const next = structuredClone(cfg);
|
||||
const changes: string[] = [];
|
||||
|
||||
const ensureWildcard = (account: Record<string, unknown>, prefix: string) => {
|
||||
type OpenPolicyAllowFromMode = "topOnly" | "topOrNested" | "nestedOnly";
|
||||
|
||||
const resolveAllowFromMode = (channelName: string): OpenPolicyAllowFromMode => {
|
||||
if (channelName === "googlechat") {
|
||||
return "nestedOnly";
|
||||
}
|
||||
if (channelName === "discord" || channelName === "slack") {
|
||||
return "topOrNested";
|
||||
}
|
||||
return "topOnly";
|
||||
};
|
||||
|
||||
const hasWildcard = (list?: Array<string | number>) =>
|
||||
list?.some((v) => String(v).trim() === "*") ?? false;
|
||||
|
||||
const ensureWildcard = (
|
||||
account: Record<string, unknown>,
|
||||
prefix: string,
|
||||
mode: OpenPolicyAllowFromMode,
|
||||
) => {
|
||||
const dmEntry = account.dm;
|
||||
const dm =
|
||||
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||
? (dmEntry as Record<string, unknown>)
|
||||
: undefined;
|
||||
const dmPolicy =
|
||||
(account.dmPolicy as string | undefined) ??
|
||||
((account.dm as Record<string, unknown> | undefined)?.policy as string | undefined);
|
||||
(account.dmPolicy as string | undefined) ?? (dm?.policy as string | undefined) ?? undefined;
|
||||
|
||||
if (dmPolicy !== "open") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check top-level allowFrom first, then nested dm.allowFrom
|
||||
const topAllowFrom = account.allowFrom as Array<string | number> | undefined;
|
||||
const dm = account.dm as Record<string, unknown> | undefined;
|
||||
const nestedAllowFrom = dm?.allowFrom as Array<string | number> | undefined;
|
||||
|
||||
const hasWildcard = (list?: Array<string | number>) =>
|
||||
list?.some((v) => String(v).trim() === "*") ?? false;
|
||||
|
||||
if (hasWildcard(topAllowFrom) || hasWildcard(nestedAllowFrom)) {
|
||||
if (mode === "nestedOnly") {
|
||||
if (hasWildcard(nestedAllowFrom)) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(nestedAllowFrom)) {
|
||||
nestedAllowFrom.push("*");
|
||||
changes.push(`- ${prefix}.dm.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
return;
|
||||
}
|
||||
const nextDm = dm ?? {};
|
||||
nextDm.allowFrom = ["*"];
|
||||
account.dm = nextDm;
|
||||
changes.push(`- ${prefix}.dm.allowFrom: set to ["*"] (required by dmPolicy="open")`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer setting top-level allowFrom (it takes precedence)
|
||||
if (mode === "topOrNested") {
|
||||
if (hasWildcard(topAllowFrom) || hasWildcard(nestedAllowFrom)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(topAllowFrom)) {
|
||||
topAllowFrom.push("*");
|
||||
changes.push(`- ${prefix}.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else if (Array.isArray(nestedAllowFrom)) {
|
||||
nestedAllowFrom.push("*");
|
||||
changes.push(`- ${prefix}.dm.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else {
|
||||
account.allowFrom = ["*"];
|
||||
changes.push(`- ${prefix}.allowFrom: set to ["*"] (required by dmPolicy="open")`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasWildcard(topAllowFrom)) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(topAllowFrom)) {
|
||||
(account.allowFrom as Array<string | number>).push("*");
|
||||
topAllowFrom.push("*");
|
||||
changes.push(`- ${prefix}.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else if (Array.isArray(nestedAllowFrom)) {
|
||||
(dm!.allowFrom as Array<string | number>).push("*");
|
||||
changes.push(`- ${prefix}.dm.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else {
|
||||
account.allowFrom = ["*"];
|
||||
changes.push(`- ${prefix}.allowFrom: set to ["*"] (required by dmPolicy="open")`);
|
||||
@@ -611,15 +658,21 @@ function maybeRepairOpenPolicyAllowFrom(cfg: OpenClawConfig): {
|
||||
continue;
|
||||
}
|
||||
|
||||
const allowFromMode = resolveAllowFromMode(channelName);
|
||||
|
||||
// Check the top-level channel config
|
||||
ensureWildcard(channelConfig, `channels.${channelName}`);
|
||||
ensureWildcard(channelConfig, `channels.${channelName}`, allowFromMode);
|
||||
|
||||
// Check per-account configs (e.g. channels.discord.accounts.mybot)
|
||||
const accounts = channelConfig.accounts as Record<string, Record<string, unknown>> | undefined;
|
||||
if (accounts && typeof accounts === "object") {
|
||||
for (const [accountName, accountConfig] of Object.entries(accounts)) {
|
||||
if (accountConfig && typeof accountConfig === "object") {
|
||||
ensureWildcard(accountConfig, `channels.${channelName}.accounts.${accountName}`);
|
||||
ensureWildcard(
|
||||
accountConfig,
|
||||
`channels.${channelName}.accounts.${accountName}`,
|
||||
allowFromMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user