






















@@ -5,6 +5,11 @@ import { isCanonicalDottedDecimalIPv4, isLoopbackIpAddress } from "@openclaw/net
55import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
66import { sanitizeForLog } from "../../packages/terminal-core/src/ansi.js";
77import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
8+import {
9+resolveChannelDmAllowFrom,
10+resolveChannelDmPolicy,
11+} from "../channels/plugins/dm-access.js";
12+import { getDoctorChannelCapabilities } from "../commands/doctor/channel-capabilities.js";
813import { isPathInside } from "../infra/path-guards.js";
914import { planManifestModelCatalogSuppressions } from "../model-catalog/index.js";
1015import {
@@ -316,15 +321,6 @@ function formatRawChannelConfigIssueMessage(message: string): string {
316321return `invalid config: ${message}`;
317322}
318323319-function asAllowFromList(value: unknown): Array<string | number> | undefined {
320-if (!Array.isArray(value)) {
321-return undefined;
322-}
323-return value.filter(
324-(entry): entry is string | number => typeof entry === "string" || typeof entry === "number",
325-);
326-}
327-328324function buildDmPolicyDependencyWarning(params: {
329325channelId: string;
330326accountId?: string;
@@ -344,27 +340,37 @@ function buildDmPolicyDependencyWarning(params: {
344340return { path: allowFromPath, message };
345341}
346342343+// Channel map keys that are not channels and must be skipped while scanning DM policy.
344+const DM_POLICY_PSEUDO_CHANNEL_KEYS = new Set(["defaults", "modelByChannel", "tools"]);
345+347346/**
348347 * Surface dmPolicy/allowFrom dependency problems generically for every channel that
349- * uses the shared top-level dmPolicy/allowFrom contract. These configs parse fine but
350- * drop every DM at runtime, so we warn (rather than reject) to stay consistent with
351- * `security audit`/`doctor` and avoid breaking existing-but-usable configs on upgrade.
348+ * exposes DM policy via the canonical top-level `dmPolicy`/`allowFrom` fields. These
349+ * configs parse fine but drop every DM at runtime, so we warn (rather than reject) to
350+ * stay consistent with `security audit`/`doctor` and avoid breaking existing-but-usable
351+ * configs on upgrade.
352+ *
353+ * Resolution goes through the shared DM-access helpers so the warning matches the
354+ * effective policy/allowFrom the runtime sees, including the legacy `dm.*` aliases and
355+ * account->channel inheritance. `nestedOnly` channels (canonical fields under `dm.*`)
356+ * are skipped because their config shape does not match this warning's top-level paths.
352357 */
353358function collectChannelDmPolicyDependencyWarnings(config: OpenClawConfig): ConfigValidationIssue[] {
354359if (!config.channels || !isRecord(config.channels)) {
355360return [];
356361}
357362const warnings: ConfigValidationIssue[] = [];
358363for (const [channelId, channelValue] of Object.entries(config.channels)) {
359-if (channelId === "defaults" || channelId === "modelByChannel" || !isRecord(channelValue)) {
364+if (DM_POLICY_PSEUDO_CHANNEL_KEYS.has(channelId) || !isRecord(channelValue)) {
365+continue;
366+}
367+const mode = getDoctorChannelCapabilities(channelId).dmAllowFromMode;
368+if (mode === "nestedOnly") {
360369continue;
361370}
362-const channelPolicy =
363-typeof channelValue.dmPolicy === "string" ? channelValue.dmPolicy : undefined;
364-const channelAllowFrom = asAllowFromList(channelValue.allowFrom);
365371const channelViolation = evaluateDmPolicyAllowFromDependency({
366-policy: channelPolicy,
367-allowFrom: channelAllowFrom,
372+policy: resolveChannelDmPolicy({ account: channelValue, mode }),
373+allowFrom: resolveChannelDmAllowFrom({ account: channelValue, mode }),
368374});
369375if (channelViolation) {
370376warnings.push(buildDmPolicyDependencyWarning({ channelId, violation: channelViolation }));
@@ -377,8 +383,8 @@ function collectChannelDmPolicyDependencyWarnings(config: OpenClawConfig): Confi
377383continue;
378384}
379385const accountViolation = evaluateDmPolicyAllowFromDependency({
380-policy: typeof accountValue.dmPolicy === "string" ? accountValue.dmPolicy : channelPolicy,
381-allowFrom: asAllowFromList(accountValue.allowFrom) ?? channelAllowFrom,
386+policy: resolveChannelDmPolicy({ account: accountValue, parent: channelValue, mode }),
387+allowFrom: resolveChannelDmAllowFrom({ account: accountValue, parent: channelValue, mode }),
382388});
383389if (accountViolation) {
384390warnings.push(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。