diff --git a/CHANGELOG.md b/CHANGELOG.md index 8652b32666a..56a73ce552f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai - Podman/launch: remove noisy container output from `scripts/run-openclaw-podman.sh` and align the Podman install guidance with the quieter startup flow. (#59368) Thanks @sallyom. - MS Teams/streaming: strip already-streamed text from fallback block delivery when replies exceed the 4000-character streaming limit so long responses stop duplicating content. (#59297) Thanks @bradgroux. - MS Teams/logging: format non-`Error` failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into `[object Object]`. (#59321) Thanks @bradgroux. +- Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380) ## 2026.4.1-beta.1 diff --git a/extensions/slack/src/monitor/media.ts b/extensions/slack/src/monitor/media.ts index c11683b198b..22e6ee2b9d9 100644 --- a/extensions/slack/src/monitor/media.ts +++ b/extensions/slack/src/monitor/media.ts @@ -337,6 +337,7 @@ export async function resolveSlackAttachmentContent(params: { export type SlackThreadStarter = { text: string; userId?: string; + botId?: string; ts?: string; files?: SlackFile[]; }; @@ -391,7 +392,15 @@ export async function resolveSlackThreadStarter(params: { ts: params.threadTs, limit: 1, inclusive: true, - })) as { messages?: Array<{ text?: string; user?: string; ts?: string; files?: SlackFile[] }> }; + })) as { + messages?: Array<{ + text?: string; + user?: string; + bot_id?: string; + ts?: string; + files?: SlackFile[]; + }>; + }; const message = response?.messages?.[0]; const text = (message?.text ?? "").trim(); if (!message || !text) { @@ -400,6 +409,7 @@ export async function resolveSlackThreadStarter(params: { const starter: SlackThreadStarter = { text, userId: message.user, + botId: message.bot_id, ts: message.ts, files: message.files, }; diff --git a/extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts b/extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts new file mode 100644 index 00000000000..ca5b9b016a0 --- /dev/null +++ b/extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts @@ -0,0 +1,146 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { App } from "@slack/bolt"; +import { resolveEnvelopeFormatOptions } from "openclaw/plugin-sdk/channel-inbound"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type { SlackMessageEvent } from "../../types.js"; +import { resolveSlackThreadContextData } from "./prepare-thread-context.js"; +import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js"; + +describe("resolveSlackThreadContextData", () => { + let fixtureRoot = ""; + let caseId = 0; + + function makeTmpStorePath() { + if (!fixtureRoot) { + throw new Error("fixtureRoot missing"); + } + const dir = path.join(fixtureRoot, `case-${caseId++}`); + fs.mkdirSync(dir); + return { dir, storePath: path.join(dir, "sessions.json") }; + } + + beforeAll(() => { + fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-thread-context-")); + }); + + afterAll(() => { + if (fixtureRoot) { + fs.rmSync(fixtureRoot, { recursive: true, force: true }); + fixtureRoot = ""; + } + }); + + function createThreadContext(params: { replies: unknown }) { + return createInboundSlackTestContext({ + cfg: { + channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, + } as OpenClawConfig, + appClient: { conversations: { replies: params.replies } } as App["client"], + defaultRequireMention: false, + replyToMode: "all", + }); + } + + function createThreadMessage(overrides: Partial = {}): SlackMessageEvent { + return { + channel: "C123", + channel_type: "channel", + user: "U1", + text: "current message", + ts: "101.000", + thread_ts: "100.000", + ...overrides, + } as SlackMessageEvent; + } + + it("omits non-allowlisted starter text and thread history messages", async () => { + const { storePath } = makeTmpStorePath(); + const replies = vi.fn().mockResolvedValue({ + messages: [ + { text: "starter secret", user: "U2", ts: "100.000" }, + { text: "assistant reply", bot_id: "B1", ts: "100.500" }, + { text: "blocked follow-up", user: "U2", ts: "100.700" }, + { text: "allowed follow-up", user: "U1", ts: "100.800" }, + { text: "current message", user: "U1", ts: "101.000" }, + ], + response_metadata: { next_cursor: "" }, + }); + const ctx = createThreadContext({ replies }); + ctx.resolveUserName = async (id: string) => ({ + name: id === "U1" ? "Alice" : "Mallory", + }); + + const result = await resolveSlackThreadContextData({ + ctx, + account: createSlackTestAccount({ thread: { initialHistoryLimit: 20 } }), + message: createThreadMessage(), + isThreadReply: true, + threadTs: "100.000", + threadStarter: { + text: "starter secret", + userId: "U2", + ts: "100.000", + }, + roomLabel: "#general", + storePath, + sessionKey: "thread-session", + allowFromLower: ["u1"], + allowNameMatching: false, + envelopeOptions: resolveEnvelopeFormatOptions({} as OpenClawConfig), + effectiveDirectMedia: null, + }); + + expect(result.threadStarterBody).toBeUndefined(); + expect(result.threadLabel).toBe("Slack thread #general"); + expect(result.threadHistoryBody).toContain("assistant reply"); + expect(result.threadHistoryBody).toContain("allowed follow-up"); + expect(result.threadHistoryBody).not.toContain("starter secret"); + expect(result.threadHistoryBody).not.toContain("blocked follow-up"); + expect(result.threadHistoryBody).not.toContain("current message"); + expect(replies).toHaveBeenCalledTimes(1); + }); + + it("keeps starter text and history when allowNameMatching authorizes the sender", async () => { + const { storePath } = makeTmpStorePath(); + const replies = vi.fn().mockResolvedValue({ + messages: [ + { text: "starter from Alice", user: "U1", ts: "100.000" }, + { text: "blocked follow-up", user: "U2", ts: "100.700" }, + { text: "current message", user: "U1", ts: "101.000" }, + ], + response_metadata: { next_cursor: "" }, + }); + const ctx = createThreadContext({ replies }); + ctx.resolveUserName = async (id: string) => ({ + name: id === "U1" ? "Alice" : "Mallory", + }); + + const result = await resolveSlackThreadContextData({ + ctx, + account: createSlackTestAccount({ thread: { initialHistoryLimit: 20 } }), + message: createThreadMessage(), + isThreadReply: true, + threadTs: "100.000", + threadStarter: { + text: "starter from Alice", + userId: "U1", + ts: "100.000", + }, + roomLabel: "#general", + storePath, + sessionKey: "thread-session", + allowFromLower: ["alice"], + allowNameMatching: true, + envelopeOptions: resolveEnvelopeFormatOptions({} as OpenClawConfig), + effectiveDirectMedia: null, + }); + + expect(result.threadStarterBody).toBe("starter from Alice"); + expect(result.threadLabel).toContain("starter from Alice"); + expect(result.threadHistoryBody).toContain("starter from Alice"); + expect(result.threadHistoryBody).not.toContain("blocked follow-up"); + }); +}); diff --git a/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts b/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts index e1cfc33088a..b383d223bea 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts @@ -3,6 +3,7 @@ import { readSessionUpdatedAt } from "openclaw/plugin-sdk/config-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import type { ResolvedSlackAccount } from "../../accounts.js"; import type { SlackMessageEvent } from "../../types.js"; +import { resolveSlackAllowListMatch } from "../allow-list.js"; import type { SlackMonitorContext } from "../context.js"; import { resolveSlackMedia, @@ -19,6 +20,27 @@ export type SlackThreadContextData = { threadStarterMedia: SlackMediaResult[] | null; }; +function isSlackThreadContextSenderAllowed(params: { + allowFromLower: string[]; + allowNameMatching: boolean; + userId?: string; + userName?: string; + botId?: string; +}): boolean { + if (params.allowFromLower.length === 0 || params.botId) { + return true; + } + if (!params.userId) { + return false; + } + return resolveSlackAllowListMatch({ + allowList: params.allowFromLower, + id: params.userId, + name: params.userName, + allowNameMatching: params.allowNameMatching, + }).allowed; +} + export async function resolveSlackThreadContextData(params: { ctx: SlackMonitorContext; account: ResolvedSlackAccount; @@ -29,6 +51,8 @@ export async function resolveSlackThreadContextData(params: { roomLabel: string; storePath: string; sessionKey: string; + allowFromLower: string[]; + allowNameMatching: boolean; envelopeOptions: ReturnType< typeof import("openclaw/plugin-sdk/channel-inbound").resolveEnvelopeFormatOptions >; @@ -51,7 +75,21 @@ export async function resolveSlackThreadContextData(params: { } const starter = params.threadStarter; - if (starter?.text) { + const starterSenderName = + params.allowNameMatching && starter?.userId + ? (await params.ctx.resolveUserName(starter.userId))?.name + : undefined; + const starterAllowed = + !starter || + isSlackThreadContextSenderAllowed({ + allowFromLower: params.allowFromLower, + allowNameMatching: params.allowNameMatching, + userId: starter.userId, + userName: starterSenderName, + botId: starter.botId, + }); + + if (starter?.text && starterAllowed) { threadStarterBody = starter.text; const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80); threadLabel = `Slack thread ${params.roomLabel}${snippet ? `: ${snippet}` : ""}`; @@ -69,6 +107,9 @@ export async function resolveSlackThreadContextData(params: { } else { threadLabel = `Slack thread ${params.roomLabel}`; } + if (starter?.text && !starterAllowed) { + logVerbose("slack: omitted non-allowlisted thread starter from context"); + } const threadInitialHistoryLimit = params.account.config?.thread?.initialHistoryLimit ?? 20; threadSessionPreviousTimestamp = readSessionUpdatedAt({ @@ -101,8 +142,25 @@ export async function resolveSlackThreadContextData(params: { }), ); + const allowedThreadHistory = threadHistory.filter((historyMsg) => { + const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null; + return isSlackThreadContextSenderAllowed({ + allowFromLower: params.allowFromLower, + allowNameMatching: params.allowNameMatching, + userId: historyMsg.userId, + userName: msgUser?.name, + botId: historyMsg.botId, + }); + }); + const omittedHistoryCount = threadHistory.length - allowedThreadHistory.length; + if (omittedHistoryCount > 0) { + logVerbose( + `slack: omitted ${omittedHistoryCount} non-allowlisted thread message(s) from context`, + ); + } + const historyParts: string[] = []; - for (const historyMsg of threadHistory) { + for (const historyMsg of allowedThreadHistory) { const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null; const msgSenderName = msgUser?.name ?? (historyMsg.botId ? `Bot (${historyMsg.botId})` : "Unknown"); @@ -120,10 +178,12 @@ export async function resolveSlackThreadContextData(params: { }), ); } - threadHistoryBody = historyParts.join("\n\n"); - logVerbose( - `slack: populated thread history with ${threadHistory.length} messages for new session`, - ); + if (historyParts.length > 0) { + threadHistoryBody = historyParts.join("\n\n"); + logVerbose( + `slack: populated thread history with ${allowedThreadHistory.length} messages for new session`, + ); + } } } diff --git a/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts b/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts index f6d3ab21ce9..07f97b3a137 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts @@ -2,6 +2,7 @@ import type { App } from "@slack/bolt"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import type { ResolvedSlackAccount } from "../../accounts.js"; +import type { SlackChannelConfigEntries } from "../channel-config.js"; import { createSlackMonitorContext } from "../context.js"; export function createInboundSlackTestContext(params: { @@ -9,7 +10,7 @@ export function createInboundSlackTestContext(params: { appClient?: App["client"]; defaultRequireMention?: boolean; replyToMode?: "off" | "all" | "first"; - channelsConfig?: Record; + channelsConfig?: SlackChannelConfigEntries; }) { return createSlackMonitorContext({ cfg: params.cfg, diff --git a/extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts b/extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts new file mode 100644 index 00000000000..623fab36b57 --- /dev/null +++ b/extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts @@ -0,0 +1,288 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { App } from "@slack/bolt"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type { SlackMessageEvent } from "../../types.js"; + +type PrepareSlackMessage = typeof import("./prepare.js").prepareSlackMessage; +type CreateInboundSlackTestContext = + typeof import("./prepare.test-helpers.js").createInboundSlackTestContext; +type CreateSlackTestAccount = typeof import("./prepare.test-helpers.js").createSlackTestAccount; + +let prepareSlackMessage: PrepareSlackMessage; +let createInboundSlackTestContext: CreateInboundSlackTestContext; +let createSlackTestAccount: CreateSlackTestAccount; +let fixtureRoot = ""; +let caseId = 0; + +async function loadSlackPrepareModules() { + const [{ prepareSlackMessage: loadedPrepareSlackMessage }, helpers] = await Promise.all([ + import("./prepare.js"), + import("./prepare.test-helpers.js"), + ]); + prepareSlackMessage = loadedPrepareSlackMessage; + createInboundSlackTestContext = helpers.createInboundSlackTestContext; + createSlackTestAccount = helpers.createSlackTestAccount; +} + +function makeTmpStorePath() { + if (!fixtureRoot) { + throw new Error("fixtureRoot missing"); + } + const dir = path.join(fixtureRoot, `case-${caseId++}`); + fs.mkdirSync(dir); + return path.join(dir, "sessions.json"); +} + +describe("prepareSlackMessage thread context allowlists", () => { + beforeAll(async () => { + await loadSlackPrepareModules(); + fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-room-thread-context-")); + }); + + afterAll(() => { + if (fixtureRoot) { + fs.rmSync(fixtureRoot, { recursive: true, force: true }); + fixtureRoot = ""; + } + }); + + it("uses room users allowlist for thread context filtering", async () => { + const replies = vi + .fn() + .mockResolvedValueOnce({ + messages: [{ text: "starter from room user", user: "U1", ts: "100.000" }], + }) + .mockResolvedValueOnce({ + messages: [ + { text: "starter from room user", user: "U1", ts: "100.000" }, + { text: "assistant reply", bot_id: "B1", ts: "100.500" }, + { text: "allowed follow-up", user: "U1", ts: "100.800" }, + { text: "current message", user: "U1", ts: "101.000" }, + ], + response_metadata: { next_cursor: "" }, + }); + const storePath = makeTmpStorePath(); + const ctx = createInboundSlackTestContext({ + cfg: { + session: { store: storePath }, + channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, + } as OpenClawConfig, + appClient: { conversations: { replies } } as unknown as App["client"], + defaultRequireMention: false, + replyToMode: "all", + channelsConfig: { + C123: { + users: ["U1"], + requireMention: false, + }, + }, + }); + ctx.allowFrom = ["u-owner"]; + ctx.resolveUserName = async (id: string) => ({ + name: id === "U1" ? "Alice" : "Owner", + }); + ctx.resolveChannelName = async () => ({ name: "general", type: "channel" }); + + const prepared = await prepareSlackMessage({ + ctx, + account: createSlackTestAccount({ + replyToMode: "all", + thread: { initialHistoryLimit: 20 }, + }), + message: { + channel: "C123", + channel_type: "channel", + user: "U1", + text: "current message", + ts: "101.000", + thread_ts: "100.000", + } as SlackMessageEvent, + opts: { source: "message" }, + }); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from room user"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from room user"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("allowed follow-up"); + expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message"); + expect(replies).toHaveBeenCalledTimes(2); + }); + + it("does not apply the owner allowlist to open-room thread context", async () => { + const replies = vi + .fn() + .mockResolvedValueOnce({ + messages: [{ text: "starter from open room", user: "U2", ts: "200.000" }], + }) + .mockResolvedValueOnce({ + messages: [ + { text: "starter from open room", user: "U2", ts: "200.000" }, + { text: "assistant reply", bot_id: "B1", ts: "200.500" }, + { text: "open-room follow-up", user: "U2", ts: "200.800" }, + { text: "current message", user: "U2", ts: "201.000" }, + ], + response_metadata: { next_cursor: "" }, + }); + const storePath = makeTmpStorePath(); + const ctx = createInboundSlackTestContext({ + cfg: { + session: { store: storePath }, + channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, + } as OpenClawConfig, + appClient: { conversations: { replies } } as unknown as App["client"], + defaultRequireMention: false, + replyToMode: "all", + channelsConfig: { + C124: { + requireMention: false, + }, + }, + }); + ctx.allowFrom = ["u-owner"]; + ctx.resolveUserName = async (id: string) => ({ + name: id === "U2" ? "Bob" : "Owner", + }); + ctx.resolveChannelName = async () => ({ name: "general", type: "channel" }); + + const prepared = await prepareSlackMessage({ + ctx, + account: createSlackTestAccount({ + replyToMode: "all", + thread: { initialHistoryLimit: 20 }, + }), + message: { + channel: "C124", + channel_type: "channel", + user: "U2", + text: "current message", + ts: "201.000", + thread_ts: "200.000", + } as SlackMessageEvent, + opts: { source: "message" }, + }); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from open room"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from open room"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("open-room follow-up"); + expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message"); + expect(replies).toHaveBeenCalledTimes(2); + }); + + it("does not apply the owner allowlist to open DMs when dmPolicy is open", async () => { + const replies = vi + .fn() + .mockResolvedValueOnce({ + messages: [{ text: "starter from open dm", user: "U3", ts: "300.000" }], + }) + .mockResolvedValueOnce({ + messages: [ + { text: "starter from open dm", user: "U3", ts: "300.000" }, + { text: "assistant reply", bot_id: "B1", ts: "300.500" }, + { text: "dm follow-up", user: "U3", ts: "300.800" }, + { text: "current message", user: "U3", ts: "301.000" }, + ], + response_metadata: { next_cursor: "" }, + }); + const storePath = makeTmpStorePath(); + const ctx = createInboundSlackTestContext({ + cfg: { + session: { store: storePath }, + channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, + } as OpenClawConfig, + appClient: { conversations: { replies } } as unknown as App["client"], + defaultRequireMention: false, + replyToMode: "all", + }); + ctx.allowFrom = ["u-owner"]; + ctx.resolveUserName = async (id: string) => ({ + name: id === "U3" ? "Dana" : "Owner", + }); + + const prepared = await prepareSlackMessage({ + ctx, + account: createSlackTestAccount({ + replyToMode: "all", + thread: { initialHistoryLimit: 20 }, + }), + message: { + channel: "D300", + channel_type: "im", + user: "U3", + text: "current message", + ts: "301.000", + thread_ts: "300.000", + } as SlackMessageEvent, + opts: { source: "message" }, + }); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from open dm"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from open dm"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("dm follow-up"); + expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message"); + expect(replies).toHaveBeenCalledTimes(2); + }); + + it("does not apply the owner allowlist to MPIM thread context", async () => { + const replies = vi + .fn() + .mockResolvedValueOnce({ + messages: [{ text: "starter from mpim", user: "U4", ts: "400.000" }], + }) + .mockResolvedValueOnce({ + messages: [ + { text: "starter from mpim", user: "U4", ts: "400.000" }, + { text: "assistant reply", bot_id: "B1", ts: "400.500" }, + { text: "mpim follow-up", user: "U4", ts: "400.800" }, + { text: "current message", user: "U4", ts: "401.000" }, + ], + response_metadata: { next_cursor: "" }, + }); + const storePath = makeTmpStorePath(); + const ctx = createInboundSlackTestContext({ + cfg: { + session: { store: storePath }, + channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, + } as OpenClawConfig, + appClient: { conversations: { replies } } as unknown as App["client"], + defaultRequireMention: false, + replyToMode: "all", + }); + ctx.allowFrom = ["u-owner"]; + ctx.resolveUserName = async (id: string) => ({ + name: id === "U4" ? "Evan" : "Owner", + }); + + const prepared = await prepareSlackMessage({ + ctx, + account: createSlackTestAccount({ + replyToMode: "all", + thread: { initialHistoryLimit: 20 }, + }), + message: { + channel: "G400", + channel_type: "mpim", + user: "U4", + text: "current message", + ts: "401.000", + thread_ts: "400.000", + } as SlackMessageEvent, + opts: { source: "message" }, + }); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from mpim"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from mpim"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply"); + expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("mpim follow-up"); + expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message"); + expect(replies).toHaveBeenCalledTimes(2); + }); +}); diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 3e7db4aa392..85ef60094e4 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -37,6 +37,7 @@ import { hasSlackThreadParticipation } from "../../sent-thread-cache.js"; import { resolveSlackThreadContext } from "../../threading.js"; import type { SlackMessageEvent } from "../../types.js"; import { + normalizeAllowListLower, normalizeSlackAllowOwnerEntry, resolveSlackAllowListMatch, resolveSlackUserAllowed, @@ -436,6 +437,15 @@ export async function prepareSlackMessage(params: { }).allowed; const channelUsersAllowlistConfigured = isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0; + const threadContextAllowFromLower = isRoom + ? channelUsersAllowlistConfigured + ? normalizeAllowListLower(channelConfig?.users) + : [] + : isDirectMessage + ? ctx.dmPolicy === "open" + ? [] + : allowFromLower + : []; const channelCommandAuthorized = isRoom && channelUsersAllowlistConfigured ? resolveSlackUserAllowed({ @@ -669,6 +679,8 @@ export async function prepareSlackMessage(params: { roomLabel, storePath, sessionKey, + allowFromLower: threadContextAllowFromLower, + allowNameMatching: ctx.allowNameMatching, envelopeOptions, effectiveDirectMedia, });