refactor: apply context visibility across channels

This commit is contained in:
Peter Steinberger
2026-04-03 04:34:03 +09:00
parent 35e1605147
commit 694d12a90b
34 changed files with 1279 additions and 131 deletions

View File

@@ -1148,7 +1148,7 @@ describe("handleFeishuMessage command authorization", () => {
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
});
it("drops quoted group context from senders outside the group sender allowlist", async () => {
it("drops quoted group context from senders outside the group sender allowlist in allowlist mode", async () => {
mockShouldComputeCommandAuthorized.mockReturnValue(false);
mockGetMessageFeishu.mockResolvedValueOnce({
messageId: "om_parent_blocked",
@@ -1164,6 +1164,7 @@ describe("handleFeishuMessage command authorization", () => {
feishu: {
groupPolicy: "open",
groupSenderAllowFrom: ["ou-allowed"],
contextVisibility: "allowlist",
groups: {
"oc-group": {
requireMention: false,
@@ -1199,6 +1200,57 @@ describe("handleFeishuMessage command authorization", () => {
);
});
it("keeps quoted group context from non-allowlisted senders in default all mode", async () => {
mockShouldComputeCommandAuthorized.mockReturnValue(false);
mockGetMessageFeishu.mockResolvedValueOnce({
messageId: "om_parent_visible",
chatId: "oc-group",
senderId: "ou-blocked",
senderType: "user",
content: "visible quoted content",
contentType: "text",
});
const cfg: ClawdbotConfig = {
channels: {
feishu: {
groupPolicy: "open",
groupSenderAllowFrom: ["ou-allowed"],
groups: {
"oc-group": {
requireMention: false,
},
},
},
},
} as ClawdbotConfig;
const event: FeishuMessageEvent = {
sender: {
sender_id: {
open_id: "ou-allowed",
},
},
message: {
message_id: "msg-group-quoted-visible",
parent_id: "om_parent_visible",
chat_id: "oc-group",
chat_type: "group",
message_type: "text",
content: JSON.stringify({ text: "hello" }),
},
};
await dispatchMessage({ cfg, event });
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
expect.objectContaining({
ReplyToId: "om_parent_visible",
ReplyToBody: "visible quoted content",
}),
);
});
it("dispatches group image message when groupPolicy is open (requireMention defaults to false)", async () => {
mockShouldComputeCommandAuthorized.mockReturnValue(false);
@@ -2552,6 +2604,7 @@ describe("handleFeishuMessage command authorization", () => {
feishu: {
groupPolicy: "open",
groupSenderAllowFrom: ["ou-allowed"],
contextVisibility: "allowlist",
groups: {
"oc-group": {
requireMention: false,

View File

@@ -12,9 +12,12 @@ import {
clearHistoryEntriesIfEnabled,
createChannelPairingController,
DEFAULT_GROUP_HISTORY_LIMIT,
evaluateSupplementalContextVisibility,
filterSupplementalContextItems,
type HistoryEntry,
normalizeAgentId,
recordPendingHistoryEntryIfEnabled,
resolveChannelContextVisibilityMode,
resolveAgentOutboundIdentity,
resolveOpenProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
@@ -218,7 +221,7 @@ export function buildFeishuAgentBody(params: {
return messageBody;
}
function shouldIncludeFetchedGroupContextMessage(params: {
function isFetchedGroupContextSenderAllowed(params: {
isGroup: boolean;
allowFrom: Array<string | number>;
senderId?: string;
@@ -231,15 +234,36 @@ function shouldIncludeFetchedGroupContextMessage(params: {
return true;
}
const senderId = params.senderId?.trim();
if (!senderId) {
return false;
}
return isFeishuGroupAllowed({
groupPolicy: "allowlist",
const senderAllowed =
!!senderId &&
isFeishuGroupAllowed({
groupPolicy: "allowlist",
allowFrom: params.allowFrom,
senderId,
senderName: undefined,
});
return senderAllowed;
}
function shouldIncludeFetchedGroupContextMessage(params: {
isGroup: boolean;
allowFrom: Array<string | number>;
mode: "all" | "allowlist" | "allowlist_quote";
kind: "quote" | "thread" | "history";
senderId?: string;
senderType?: string;
}): boolean {
const senderAllowed = isFetchedGroupContextSenderAllowed({
isGroup: params.isGroup,
allowFrom: params.allowFrom,
senderId,
senderName: undefined,
senderId: params.senderId,
senderType: params.senderType,
});
return evaluateSupplementalContextVisibility({
mode: params.mode,
kind: params.kind,
senderAllowed,
}).include;
}
function filterFetchedGroupContextMessages<
@@ -249,16 +273,22 @@ function filterFetchedGroupContextMessages<
params: {
isGroup: boolean;
allowFrom: Array<string | number>;
mode: "all" | "allowlist" | "allowlist_quote";
kind: "quote" | "thread" | "history";
},
): T[] {
return messages.filter((message) =>
shouldIncludeFetchedGroupContextMessage({
isGroup: params.isGroup,
allowFrom: params.allowFrom,
senderId: message.senderId,
senderType: message.senderType,
}),
);
return filterSupplementalContextItems({
items: messages,
mode: params.mode,
kind: params.kind,
isSenderAllowed: (message) =>
isFetchedGroupContextSenderAllowed({
isGroup: params.isGroup,
allowFrom: params.allowFrom,
senderId: message.senderId,
senderType: message.senderType,
}),
}).items;
}
export async function handleFeishuMessage(params: {
@@ -706,6 +736,11 @@ export async function handleFeishuMessage(params: {
const inboundLabel = isGroup
? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
: `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
const contextVisibilityMode = resolveChannelContextVisibilityMode({
cfg: effectiveCfg,
channel: "feishu",
accountId: account.accountId,
});
// Do not enqueue inbound user previews as system events.
// System events are prepended to future prompts and can be misread as
@@ -740,6 +775,8 @@ export async function handleFeishuMessage(params: {
shouldIncludeFetchedGroupContextMessage({
isGroup,
allowFrom: effectiveGroupSenderAllowFrom,
mode: contextVisibilityMode,
kind: "quote",
senderId: quotedMessageInfo.senderId,
senderType: quotedMessageInfo.senderType,
})
@@ -750,7 +787,7 @@ export async function handleFeishuMessage(params: {
);
} else if (quotedMessageInfo) {
log(
`feishu[${account.accountId}]: skipped quoted message from sender ${quotedMessageInfo.senderId ?? "unknown"} due to group sender allowlist`,
`feishu[${account.accountId}]: skipped quoted message from sender ${quotedMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
);
}
} catch (err) {
@@ -851,12 +888,14 @@ export async function handleFeishuMessage(params: {
!shouldIncludeFetchedGroupContextMessage({
isGroup,
allowFrom: effectiveGroupSenderAllowFrom,
mode: contextVisibilityMode,
kind: "thread",
senderId: rootMessageInfo.senderId,
senderType: rootMessageInfo.senderType,
})
) {
log(
`feishu[${account.accountId}]: skipped thread starter from sender ${rootMessageInfo.senderId ?? "unknown"} due to group sender allowlist`,
`feishu[${account.accountId}]: skipped thread starter from sender ${rootMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
);
rootMessageInfo = null;
}
@@ -929,6 +968,8 @@ export async function handleFeishuMessage(params: {
const allowlistedMessages = filterFetchedGroupContextMessages(threadMessages, {
isGroup,
allowFrom: effectiveGroupSenderAllowFrom,
mode: contextVisibilityMode,
kind: "history",
});
const relevantMessages =
(senderScoped