mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix(matrix): resolve live allowlist updates
This commit is contained in:
@@ -1014,7 +1014,7 @@ Live directory lookup uses the logged-in Matrix account:
|
||||
- `allowBots`: allow messages from other configured OpenClaw Matrix accounts (`true` or `"mentions"`).
|
||||
- `groupPolicy`: `open`, `allowlist`, or `disabled`.
|
||||
- `contextVisibility`: supplemental room-context visibility mode (`all`, `allowlist`, `allowlist_quote`).
|
||||
- `groupAllowFrom`: allowlist of user IDs for room traffic. Entries should be full Matrix user IDs; unresolved names are ignored at runtime.
|
||||
- `groupAllowFrom`: allowlist of user IDs for room traffic. Full Matrix user IDs are safest; exact directory matches are resolved at startup and when the allowlist changes while the monitor is running. Unresolved names are ignored.
|
||||
- `historyLimit`: max room messages to include as group history context. Falls back to `messages.groupChat.historyLimit`; if both are unset, the effective default is `0`. Set `0` to disable.
|
||||
- `replyToMode`: `off`, `first`, `all`, or `batched`.
|
||||
- `markdown`: optional Markdown rendering configuration for outbound Matrix text.
|
||||
@@ -1035,7 +1035,7 @@ Live directory lookup uses the logged-in Matrix account:
|
||||
- `autoJoinAllowlist`: rooms/aliases allowed when `autoJoin` is `allowlist`. Alias entries are resolved to room IDs during invite handling; OpenClaw does not trust alias state claimed by the invited room.
|
||||
- `dm`: DM policy block (`enabled`, `policy`, `allowFrom`, `sessionScope`, `threadReplies`).
|
||||
- `dm.policy`: controls DM access after OpenClaw has joined the room and classified it as a DM. It does not change whether an invite is auto-joined.
|
||||
- `dm.allowFrom`: entries should be full Matrix user IDs unless you already resolved them through live directory lookup.
|
||||
- `dm.allowFrom`: allowlist of user IDs for DM traffic. Full Matrix user IDs are safest; exact directory matches are resolved at startup and when the allowlist changes while the monitor is running. Unresolved names are ignored.
|
||||
- `dm.sessionScope`: `per-user` (default) or `per-room`. Use `per-room` when you want each Matrix DM room to keep separate context even if the peer is the same.
|
||||
- `dm.threadReplies`: DM-only thread policy override (`off`, `inbound`, `always`). It overrides the top-level `threadReplies` setting for both reply placement and session isolation in DMs.
|
||||
- `execApprovals`: Matrix-native exec approval delivery (`enabled`, `approvers`, `target`, `agentFilter`, `sessionFilter`).
|
||||
|
||||
@@ -75,6 +75,36 @@ function listResolvedMatrixAllowlistEntries(params: {
|
||||
return resolvedEntries;
|
||||
}
|
||||
|
||||
function normalizeConfiguredMatrixAllowlistEntries(
|
||||
entries?: ReadonlyArray<string | number>,
|
||||
): string[] {
|
||||
const normalized: string[] = [];
|
||||
for (const entry of entries ?? []) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed) {
|
||||
normalized.push(trimmed);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function addUniqueMatrixAllowlistEntry(params: {
|
||||
entries: string[];
|
||||
seen: Set<string>;
|
||||
entry: string;
|
||||
}): void {
|
||||
const trimmed = params.entry.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
const key = trimmed.toLowerCase();
|
||||
if (params.seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
params.seen.add(key);
|
||||
params.entries.push(trimmed);
|
||||
}
|
||||
|
||||
function sanitizeMatrixRoomUserAllowlists(entries: MatrixRoomsConfig): MatrixRoomsConfig {
|
||||
const nextEntries: MatrixRoomsConfig = { ...entries };
|
||||
for (const [roomKey, roomConfig] of Object.entries(entries)) {
|
||||
@@ -187,6 +217,70 @@ async function resolveMatrixMonitorUserAllowlist(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveMatrixMonitorLiveUserAllowlist(params: {
|
||||
cfg: CoreConfig;
|
||||
accountId?: string | null;
|
||||
entries?: ReadonlyArray<string | number>;
|
||||
startupResolvedEntries?: readonly MatrixResolvedAllowlistEntry[];
|
||||
runtime: RuntimeEnv;
|
||||
resolveTargets?: ResolveMatrixTargetsFn;
|
||||
}): Promise<string[]> {
|
||||
const liveEntries = normalizeConfiguredMatrixAllowlistEntries(params.entries);
|
||||
if (liveEntries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const effective: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const startupByInput = new Map(
|
||||
(params.startupResolvedEntries ?? []).map((entry) => [entry.input, entry.id] as const),
|
||||
);
|
||||
const pending: string[] = [];
|
||||
|
||||
for (const entry of liveEntries) {
|
||||
const query = normalizeMatrixUserLookupEntry(entry);
|
||||
if (entry === "*") {
|
||||
addUniqueMatrixAllowlistEntry({ entries: effective, seen, entry });
|
||||
continue;
|
||||
}
|
||||
if (isMatrixQualifiedUserId(query)) {
|
||||
addUniqueMatrixAllowlistEntry({
|
||||
entries: effective,
|
||||
seen,
|
||||
entry: normalizeMatrixUserId(query),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const startupId = startupByInput.get(entry);
|
||||
if (startupId) {
|
||||
addUniqueMatrixAllowlistEntry({ entries: effective, seen, entry: startupId });
|
||||
continue;
|
||||
}
|
||||
pending.push(entry);
|
||||
}
|
||||
|
||||
if (pending.length === 0) {
|
||||
return effective;
|
||||
}
|
||||
|
||||
const resolution = await resolveMatrixMonitorUserEntries({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
entries: pending,
|
||||
runtime: params.runtime,
|
||||
resolveTargets: params.resolveTargets ?? resolveMatrixTargets,
|
||||
});
|
||||
const canonicalized = canonicalizeAllowlistWithResolvedIds({
|
||||
existing: pending,
|
||||
resolvedMap: resolution.resolvedMap,
|
||||
});
|
||||
for (const entry of filterResolvedMatrixAllowlistEntries(canonicalized)) {
|
||||
addUniqueMatrixAllowlistEntry({ entries: effective, seen, entry });
|
||||
}
|
||||
|
||||
return effective;
|
||||
}
|
||||
|
||||
async function resolveMatrixMonitorRoomsConfig(params: {
|
||||
cfg: CoreConfig;
|
||||
accountId?: string | null;
|
||||
|
||||
@@ -85,6 +85,7 @@ type MatrixHandlerTestHarnessOptions = {
|
||||
enqueueSystemEvent?: (...args: unknown[]) => void;
|
||||
getRoomInfo?: MatrixMonitorHandlerParams["getRoomInfo"];
|
||||
getMemberDisplayName?: MatrixMonitorHandlerParams["getMemberDisplayName"];
|
||||
resolveLiveUserAllowlist?: MatrixMonitorHandlerParams["resolveLiveUserAllowlist"];
|
||||
};
|
||||
|
||||
type MatrixHandlerTestHarness = {
|
||||
@@ -242,6 +243,7 @@ export function createMatrixHandlerTestHarness(
|
||||
getRoomInfo: options.getRoomInfo ?? (async () => ({ altAliases: [] })),
|
||||
getMemberDisplayName: options.getMemberDisplayName ?? (async () => "sender"),
|
||||
needsRoomAliasesForConfig: options.needsRoomAliasesForConfig ?? false,
|
||||
resolveLiveUserAllowlist: options.resolveLiveUserAllowlist,
|
||||
historyLimit: options.historyLimit ?? 0,
|
||||
});
|
||||
|
||||
|
||||
@@ -1984,6 +1984,54 @@ describe("matrix monitor handler live allowlist reload", () => {
|
||||
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("accepts a DM sender added as a live-resolved display name", async () => {
|
||||
const dispatchReplyFromConfig = createDispatchReplyFromConfig();
|
||||
const resolveLiveUserAllowlist = vi.fn(
|
||||
async (params: { entries?: ReadonlyArray<string | number> }) => {
|
||||
const entries = (params.entries ?? []).map(String);
|
||||
return entries.includes("Alice") ? ["@alice:example.org"] : [];
|
||||
},
|
||||
);
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
dm: { allowFrom: [] as string[] },
|
||||
},
|
||||
},
|
||||
};
|
||||
const { handler } = createMatrixHandlerTestHarness({
|
||||
cfg,
|
||||
dmPolicy: "allowlist",
|
||||
isDirectMessage: true,
|
||||
allowFrom: [],
|
||||
allowFromResolvedEntries: [],
|
||||
dispatchReplyFromConfig,
|
||||
resolveLiveUserAllowlist,
|
||||
});
|
||||
|
||||
await sendLiveAllowlistMessage(handler, {
|
||||
eventId: "$dm-live-name-before",
|
||||
sender: "@alice:example.org",
|
||||
body: "hello",
|
||||
});
|
||||
expect(dispatchReplyFromConfig).not.toHaveBeenCalled();
|
||||
|
||||
cfg.channels.matrix.dm.allowFrom = ["Alice"];
|
||||
await sendLiveAllowlistMessage(handler, {
|
||||
eventId: "$dm-live-name-after",
|
||||
sender: "@alice:example.org",
|
||||
body: "hello again",
|
||||
});
|
||||
|
||||
expect(resolveLiveUserAllowlist).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
accountId: "ops",
|
||||
entries: ["Alice"],
|
||||
}),
|
||||
);
|
||||
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("blocks a room sender removed from live groupAllowFrom while the group list remains configured", async () => {
|
||||
const dispatchReplyFromConfig = createDispatchReplyFromConfig();
|
||||
const cfg = {
|
||||
|
||||
@@ -34,11 +34,13 @@ import {
|
||||
import type { LocationMessageEventContent, MatrixClient } from "../sdk.js";
|
||||
import { MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY } from "../send/types.js";
|
||||
import { resolveMatrixStoredSessionMeta } from "../session-store-metadata.js";
|
||||
import { isMatrixQualifiedUserId } from "../target-ids.js";
|
||||
import { resolveMatrixMonitorAccessState } from "./access-state.js";
|
||||
import { resolveMatrixAckReactionConfig } from "./ack-config.js";
|
||||
import { normalizeMatrixUserId, resolveMatrixAllowListMatch } from "./allowlist.js";
|
||||
import type { MatrixResolvedAllowlistEntry } from "./config.js";
|
||||
import { resolveMatrixAllowListMatch } from "./allowlist.js";
|
||||
import {
|
||||
resolveMatrixMonitorLiveUserAllowlist,
|
||||
type MatrixResolvedAllowlistEntry,
|
||||
} from "./config.js";
|
||||
import type { MatrixInboundEventDeduper } from "./inbound-dedupe.js";
|
||||
import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
|
||||
import { downloadMatrixMedia } from "./media.js";
|
||||
@@ -191,63 +193,9 @@ export type MatrixMonitorHandlerParams = {
|
||||
) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
|
||||
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
|
||||
needsRoomAliasesForConfig: boolean;
|
||||
resolveLiveUserAllowlist?: typeof resolveMatrixMonitorLiveUserAllowlist;
|
||||
};
|
||||
|
||||
function normalizeConfiguredMatrixAllowlistEntries(
|
||||
entries?: ReadonlyArray<string | number>,
|
||||
): string[] {
|
||||
const normalized: string[] = [];
|
||||
for (const entry of entries ?? []) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed) {
|
||||
normalized.push(trimmed);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isMatrixHotReloadAllowlistEntry(entry: string): boolean {
|
||||
if (entry === "*") {
|
||||
return true;
|
||||
}
|
||||
return isMatrixQualifiedUserId(normalizeMatrixUserId(entry));
|
||||
}
|
||||
|
||||
function resolveEffectiveMatrixLiveAllowlist(params: {
|
||||
liveEntries?: ReadonlyArray<string | number>;
|
||||
startupResolvedEntries?: readonly MatrixResolvedAllowlistEntry[];
|
||||
}): string[] {
|
||||
const liveEntries = normalizeConfiguredMatrixAllowlistEntries(params.liveEntries);
|
||||
const liveInputs = new Set(liveEntries);
|
||||
const effective: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const add = (entry: string) => {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
const key = trimmed.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
seen.add(key);
|
||||
effective.push(trimmed);
|
||||
};
|
||||
|
||||
for (const entry of liveEntries) {
|
||||
if (isMatrixHotReloadAllowlistEntry(entry)) {
|
||||
add(entry);
|
||||
}
|
||||
}
|
||||
for (const entry of params.startupResolvedEntries ?? []) {
|
||||
if (liveInputs.has(entry.input)) {
|
||||
add(entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
return effective;
|
||||
}
|
||||
|
||||
function resolveMatrixMentionPrecheckText(params: {
|
||||
eventType: string;
|
||||
content: RoomMessageEventContent;
|
||||
@@ -439,6 +387,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
getRoomInfo,
|
||||
getMemberDisplayName,
|
||||
needsRoomAliasesForConfig,
|
||||
resolveLiveUserAllowlist = resolveMatrixMonitorLiveUserAllowlist,
|
||||
} = params;
|
||||
const contextVisibilityMode = resolveChannelContextVisibilityMode({
|
||||
cfg,
|
||||
@@ -449,6 +398,31 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
value: string[];
|
||||
expiresAtMs: number;
|
||||
} | null = null;
|
||||
type LiveAllowlistCacheEntry = { signature: string; entries: string[] };
|
||||
let liveDmAllowlistCache: LiveAllowlistCacheEntry | null = null;
|
||||
let liveGroupAllowlistCache: LiveAllowlistCacheEntry | null = null;
|
||||
const resolveCachedLiveAllowlist = async (params: {
|
||||
cfg: CoreConfig;
|
||||
entries?: ReadonlyArray<string | number>;
|
||||
startupResolvedEntries?: readonly MatrixResolvedAllowlistEntry[];
|
||||
cache: LiveAllowlistCacheEntry | null;
|
||||
updateCache: (next: LiveAllowlistCacheEntry) => void;
|
||||
}): Promise<string[]> => {
|
||||
const signature = JSON.stringify((params.entries ?? []).map((entry) => String(entry).trim()));
|
||||
if (params.cache?.signature === signature) {
|
||||
return params.cache.entries;
|
||||
}
|
||||
const entries = await resolveLiveUserAllowlist({
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
entries: params.entries,
|
||||
startupResolvedEntries: params.startupResolvedEntries,
|
||||
runtime,
|
||||
});
|
||||
const next = { signature, entries };
|
||||
params.updateCache(next);
|
||||
return entries;
|
||||
};
|
||||
const pairingReplySentAtMsBySender = new Map<string, number>();
|
||||
const resolveThreadContext = createMatrixThreadContextResolver({
|
||||
client,
|
||||
@@ -698,17 +672,28 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
};
|
||||
const storeAllowFrom = isDirectMessage ? await readStoreAllowFrom() : [];
|
||||
const roomUsers = roomConfig?.users ?? [];
|
||||
const liveCfg = core.config.loadConfig() as CoreConfig;
|
||||
const liveAccountAllowlists = resolveMatrixAccountAllowlistConfig({
|
||||
cfg: core.config.loadConfig() as CoreConfig,
|
||||
cfg: liveCfg,
|
||||
accountId,
|
||||
});
|
||||
const liveDmAllowFrom = resolveEffectiveMatrixLiveAllowlist({
|
||||
liveEntries: liveAccountAllowlists.dmAllowFrom,
|
||||
const liveDmAllowFrom = await resolveCachedLiveAllowlist({
|
||||
cfg: liveCfg,
|
||||
entries: liveAccountAllowlists.dmAllowFrom,
|
||||
startupResolvedEntries: allowFromResolvedEntries,
|
||||
cache: liveDmAllowlistCache,
|
||||
updateCache: (next) => {
|
||||
liveDmAllowlistCache = next;
|
||||
},
|
||||
});
|
||||
const liveGroupAllowFrom = resolveEffectiveMatrixLiveAllowlist({
|
||||
liveEntries: liveAccountAllowlists.groupAllowFrom,
|
||||
const liveGroupAllowFrom = await resolveCachedLiveAllowlist({
|
||||
cfg: liveCfg,
|
||||
entries: liveAccountAllowlists.groupAllowFrom,
|
||||
startupResolvedEntries: groupAllowFromResolvedEntries,
|
||||
cache: liveGroupAllowlistCache,
|
||||
updateCache: (next) => {
|
||||
liveGroupAllowlistCache = next;
|
||||
},
|
||||
});
|
||||
const accessState = resolveMatrixMonitorAccessState({
|
||||
allowFrom: liveDmAllowFrom,
|
||||
|
||||
Reference in New Issue
Block a user