From b9e71240ed0b06d8e0675d5e5f8b730f3de8b2b1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 22 Mar 2026 08:47:16 -0700 Subject: [PATCH] refactor(doctor): centralize channel capability metadata (#52325) * refactor(doctor): centralize channel capabilities * fix(doctor): preserve msteams sender warnings --- CHANGELOG.md | 1 + .../doctor/channel-capabilities.test.ts | 40 ++++++++++ src/commands/doctor/channel-capabilities.ts | 75 +++++++++++++++++++ src/commands/doctor/shared/allow-from-mode.ts | 10 +-- .../doctor/shared/empty-allowlist-policy.ts | 25 +------ 5 files changed, 122 insertions(+), 29 deletions(-) create mode 100644 src/commands/doctor/channel-capabilities.test.ts create mode 100644 src/commands/doctor/channel-capabilities.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 35aed5a6b6d..a4514240f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,7 @@ Docs: https://docs.openclaw.ai - Telegram/setup: warn when setup leaves DMs on pairing without an allowlist, and show valid account-scoped remediation commands. (#50710) Thanks @ernestodeoliveira. - Doctor/Telegram: replace the fresh-install empty group-allowlist false positive with first-run guidance that explains DM pairing approval and the next group setup steps, so new Telegram installs get actionable setup help instead of a broken-config warning. Thanks @vincentkoc. - Doctor/extensions: keep Matrix DM `allowFrom` repairs on the canonical `dm.allowFrom` path and stop treating Zalouser group sender gating as if it fell back to `allowFrom`, so doctor warnings and `--fix` stay aligned with runtime access control. Thanks @vincentkoc. +- Doctor/refactor: centralize built-in channel doctor semantics in one static capability registry with conservative fallback behavior for unknown/external channels, so future extension changes stop depending on scattered shared string checks. Thanks @vincentkoc. - Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao. - Channels/plugins: keep shared interactive payloads merge-ready by fixing Slack custom callback routing and repeat-click dedupe, allowing interactive-only sends, and preserving ordered Discord shared text blocks. (#47715) Thanks @vincentkoc. - Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc. diff --git a/src/commands/doctor/channel-capabilities.test.ts b/src/commands/doctor/channel-capabilities.test.ts new file mode 100644 index 00000000000..87f609f2737 --- /dev/null +++ b/src/commands/doctor/channel-capabilities.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { getDoctorChannelCapabilities } from "./channel-capabilities.js"; + +describe("doctor channel capabilities", () => { + it("returns built-in capability overrides for matrix", () => { + expect(getDoctorChannelCapabilities("matrix")).toEqual({ + dmAllowFromMode: "nestedOnly", + groupModel: "sender", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: true, + }); + }); + + it("returns hybrid group semantics for zalouser", () => { + expect(getDoctorChannelCapabilities("zalouser")).toEqual({ + dmAllowFromMode: "topOnly", + groupModel: "hybrid", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: false, + }); + }); + + it("preserves empty sender allowlist warnings for msteams hybrid routing", () => { + expect(getDoctorChannelCapabilities("msteams")).toEqual({ + dmAllowFromMode: "topOnly", + groupModel: "hybrid", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: true, + }); + }); + + it("falls back conservatively for unknown external channels", () => { + expect(getDoctorChannelCapabilities("external-demo")).toEqual({ + dmAllowFromMode: "topOnly", + groupModel: "sender", + groupAllowFromFallbackToAllowFrom: true, + warnOnEmptyGroupSenderAllowlist: true, + }); + }); +}); diff --git a/src/commands/doctor/channel-capabilities.ts b/src/commands/doctor/channel-capabilities.ts new file mode 100644 index 00000000000..bb0b36dbdb4 --- /dev/null +++ b/src/commands/doctor/channel-capabilities.ts @@ -0,0 +1,75 @@ +import type { AllowFromMode } from "./shared/allow-from-mode.js"; + +export type DoctorGroupModel = "sender" | "route" | "hybrid"; + +export type DoctorChannelCapabilities = { + dmAllowFromMode: AllowFromMode; + groupModel: DoctorGroupModel; + groupAllowFromFallbackToAllowFrom: boolean; + warnOnEmptyGroupSenderAllowlist: boolean; +}; + +const DEFAULT_DOCTOR_CHANNEL_CAPABILITIES: DoctorChannelCapabilities = { + dmAllowFromMode: "topOnly", + groupModel: "sender", + groupAllowFromFallbackToAllowFrom: true, + warnOnEmptyGroupSenderAllowlist: true, +}; + +const DOCTOR_CHANNEL_CAPABILITIES: Record = { + discord: { + dmAllowFromMode: "topOrNested", + groupModel: "route", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: false, + }, + googlechat: { + dmAllowFromMode: "nestedOnly", + groupModel: "route", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: false, + }, + imessage: { + dmAllowFromMode: "topOnly", + groupModel: "sender", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: true, + }, + irc: { + dmAllowFromMode: "topOnly", + groupModel: "sender", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: true, + }, + matrix: { + dmAllowFromMode: "nestedOnly", + groupModel: "sender", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: true, + }, + msteams: { + dmAllowFromMode: "topOnly", + groupModel: "hybrid", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: true, + }, + slack: { + dmAllowFromMode: "topOrNested", + groupModel: "route", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: false, + }, + zalouser: { + dmAllowFromMode: "topOnly", + groupModel: "hybrid", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: false, + }, +}; + +export function getDoctorChannelCapabilities(channelName?: string): DoctorChannelCapabilities { + if (!channelName) { + return DEFAULT_DOCTOR_CHANNEL_CAPABILITIES; + } + return DOCTOR_CHANNEL_CAPABILITIES[channelName] ?? DEFAULT_DOCTOR_CHANNEL_CAPABILITIES; +} diff --git a/src/commands/doctor/shared/allow-from-mode.ts b/src/commands/doctor/shared/allow-from-mode.ts index 994824154ac..4a1ca6d5271 100644 --- a/src/commands/doctor/shared/allow-from-mode.ts +++ b/src/commands/doctor/shared/allow-from-mode.ts @@ -1,11 +1,7 @@ +import { getDoctorChannelCapabilities } from "../channel-capabilities.js"; + export type AllowFromMode = "topOnly" | "topOrNested" | "nestedOnly"; export function resolveAllowFromMode(channelName: string): AllowFromMode { - if (channelName === "googlechat" || channelName === "matrix") { - return "nestedOnly"; - } - if (channelName === "discord" || channelName === "slack") { - return "topOrNested"; - } - return "topOnly"; + return getDoctorChannelCapabilities(channelName).dmAllowFromMode; } diff --git a/src/commands/doctor/shared/empty-allowlist-policy.ts b/src/commands/doctor/shared/empty-allowlist-policy.ts index e98380c1a60..e98a63113fc 100644 --- a/src/commands/doctor/shared/empty-allowlist-policy.ts +++ b/src/commands/doctor/shared/empty-allowlist-policy.ts @@ -1,3 +1,4 @@ +import { getDoctorChannelCapabilities } from "../channel-capabilities.js"; import type { DoctorAccountRecord, DoctorAllowFromList } from "../types.js"; import { hasAllowFromEntries } from "./allowlist.js"; @@ -10,31 +11,11 @@ type CollectEmptyAllowlistPolicyWarningsParams = { }; function usesSenderBasedGroupAllowlist(channelName?: string): boolean { - if (!channelName) { - return true; - } - // These channels enforce group access via channel/space config, not sender-based - // groupAllowFrom lists. - return !( - channelName === "discord" || - channelName === "slack" || - channelName === "googlechat" || - channelName === "zalouser" - ); + return getDoctorChannelCapabilities(channelName).warnOnEmptyGroupSenderAllowlist; } function allowsGroupAllowFromFallback(channelName?: string): boolean { - if (!channelName) { - return true; - } - // Keep doctor warnings aligned with runtime access semantics. - return !( - channelName === "googlechat" || - channelName === "imessage" || - channelName === "matrix" || - channelName === "msteams" || - channelName === "irc" - ); + return getDoctorChannelCapabilities(channelName).groupAllowFromFallbackToAllowFrom; } export function collectEmptyAllowlistPolicyWarningsForAccount(