fix(doctor): align matrix and zalouser allowlist semantics (#52096)

* fix(doctor): align extension allowlist semantics

* fix(doctor): skip generic zalouser group warning
This commit is contained in:
Vincent Koc
2026-03-22 08:19:24 -07:00
committed by GitHub
parent 52a0aa0672
commit 4685fc7e77
6 changed files with 73 additions and 2 deletions

View File

@@ -111,6 +111,7 @@ Docs: https://docs.openclaw.ai
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
- 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.
- 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.

View File

@@ -1,7 +1,7 @@
export type AllowFromMode = "topOnly" | "topOrNested" | "nestedOnly";
export function resolveAllowFromMode(channelName: string): AllowFromMode {
if (channelName === "googlechat") {
if (channelName === "googlechat" || channelName === "matrix") {
return "nestedOnly";
}
if (channelName === "discord" || channelName === "slack") {

View File

@@ -0,0 +1,36 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { maybeRepairAllowlistPolicyAllowFrom } from "./allowlist-policy-repair.js";
const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({
readChannelAllowFromStoreMock: vi.fn(),
}));
vi.mock("../../../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: readChannelAllowFromStoreMock,
}));
describe("doctor allowlist-policy repair", () => {
beforeEach(() => {
readChannelAllowFromStoreMock.mockReset();
});
it("restores matrix dm allowFrom from the pairing store into the nested path", async () => {
readChannelAllowFromStoreMock.mockResolvedValue(["@alice:example.org"]);
const result = await maybeRepairAllowlistPolicyAllowFrom({
channels: {
matrix: {
dm: {
policy: "allowlist",
},
},
},
});
expect(result.changes).toEqual([
'- channels.matrix.dm.allowFrom: restored 1 sender entry from pairing store (dmPolicy="allowlist").',
]);
expect(result.config.channels?.matrix?.dm?.allowFrom).toEqual(["@alice:example.org"]);
expect(result.config.channels?.matrix?.allowFrom).toBeUndefined();
});
});

View File

@@ -28,6 +28,17 @@ describe("doctor empty allowlist policy warnings", () => {
]);
});
it("stays quiet for zalouser hybrid route-and-sender group access", () => {
const warnings = collectEmptyAllowlistPolicyWarningsForAccount({
account: { groupPolicy: "allowlist" },
channelName: "zalouser",
doctorFixCommand: "openclaw doctor --fix",
prefix: "channels.zalouser",
});
expect(warnings).toEqual([]);
});
it("stays quiet for channels that do not use sender-based group allowlists", () => {
const warnings = collectEmptyAllowlistPolicyWarningsForAccount({
account: { groupPolicy: "allowlist" },

View File

@@ -15,7 +15,12 @@ function usesSenderBasedGroupAllowlist(channelName?: string): boolean {
}
// These channels enforce group access via channel/space config, not sender-based
// groupAllowFrom lists.
return !(channelName === "discord" || channelName === "slack" || channelName === "googlechat");
return !(
channelName === "discord" ||
channelName === "slack" ||
channelName === "googlechat" ||
channelName === "zalouser"
);
}
function allowsGroupAllowFromFallback(channelName?: string): boolean {

View File

@@ -37,6 +37,24 @@ describe("doctor open-policy allowFrom repair", () => {
expect(result.config.channels?.googlechat?.dm?.allowFrom).toEqual(["*"]);
});
it("repairs nested-only matrix dm allowFrom", () => {
const result = maybeRepairOpenPolicyAllowFrom({
channels: {
matrix: {
dm: {
policy: "open",
},
},
},
});
expect(result.changes).toEqual([
'- channels.matrix.dm.allowFrom: set to ["*"] (required by dmPolicy="open")',
]);
expect(result.config.channels?.matrix?.dm?.allowFrom).toEqual(["*"]);
expect(result.config.channels?.matrix?.allowFrom).toBeUndefined();
});
it("appends wildcard to discord nested dm allowFrom when top-level is absent", () => {
const result = maybeRepairOpenPolicyAllowFrom({
channels: {