diff --git a/CHANGELOG.md b/CHANGELOG.md index b507e12d4b..6ffadd966e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai - Onboarding: infer auth choice from non-interactive API key flags. (#8484) Thanks @f-trycua. - Security: keep untrusted channel metadata out of system prompts (Slack/Discord). Thanks @KonstantinMirin. - Discord: treat allowlisted senders as owner for system-prompt identity hints while keeping channel topics untrusted. +- Slack: strip `<@...>` mention tokens before command matching so `/new` and `/reset` work when prefixed with a mention. (#9971) Thanks @ironbyte-rgb. - Security: enforce sandboxed media paths for message tool attachments. (#9182) Thanks @victormier. - Security: require explicit credentials for gateway URL overrides to prevent credential leakage. (#8113) Thanks @victormier. - Security: gate `whatsapp_login` tool to owner senders and default-deny non-owner contexts. (#8768) Thanks @victormier. diff --git a/src/auto-reply/reply/session-resets.test.ts b/src/auto-reply/reply/session-resets.test.ts index b53d44aa6b..15d5e3275a 100644 --- a/src/auto-reply/reply/session-resets.test.ts +++ b/src/auto-reply/reply/session-resets.test.ts @@ -255,6 +255,107 @@ describe("initSessionState reset triggers in WhatsApp groups", () => { }); }); +describe("initSessionState reset triggers in Slack channels", () => { + async function createStorePath(prefix: string): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); + return path.join(root, "sessions.json"); + } + + async function seedSessionStore(params: { + storePath: string; + sessionKey: string; + sessionId: string; + }): Promise { + const { saveSessionStore } = await import("../../config/sessions.js"); + await saveSessionStore(params.storePath, { + [params.sessionKey]: { + sessionId: params.sessionId, + updatedAt: Date.now(), + }, + }); + } + + it("Reset trigger /reset works when Slack message has a leading <@...> mention token", async () => { + const storePath = await createStorePath("openclaw-slack-channel-reset-"); + const sessionKey = "agent:main:slack:channel:c1"; + const existingSessionId = "existing-session-123"; + await seedSessionStore({ + storePath, + sessionKey, + sessionId: existingSessionId, + }); + + const cfg = { + session: { store: storePath, idleMinutes: 999 }, + } as OpenClawConfig; + + const channelMessageCtx = { + Body: "<@U123> /reset", + RawBody: "<@U123> /reset", + CommandBody: "<@U123> /reset", + From: "slack:channel:C1", + To: "channel:C1", + ChatType: "channel", + SessionKey: sessionKey, + Provider: "slack", + Surface: "slack", + SenderId: "U123", + SenderName: "Owner", + }; + + const result = await initSessionState({ + ctx: channelMessageCtx, + cfg, + commandAuthorized: true, + }); + + expect(result.isNewSession).toBe(true); + expect(result.resetTriggered).toBe(true); + expect(result.sessionId).not.toBe(existingSessionId); + expect(result.bodyStripped).toBe(""); + }); + + it("Reset trigger /new preserves args when Slack message has a leading <@...> mention token", async () => { + const storePath = await createStorePath("openclaw-slack-channel-new-"); + const sessionKey = "agent:main:slack:channel:c2"; + const existingSessionId = "existing-session-123"; + await seedSessionStore({ + storePath, + sessionKey, + sessionId: existingSessionId, + }); + + const cfg = { + session: { store: storePath, idleMinutes: 999 }, + } as OpenClawConfig; + + const channelMessageCtx = { + Body: "<@U123> /new take notes", + RawBody: "<@U123> /new take notes", + CommandBody: "<@U123> /new take notes", + From: "slack:channel:C2", + To: "channel:C2", + ChatType: "channel", + SessionKey: sessionKey, + Provider: "slack", + Surface: "slack", + SenderId: "U123", + SenderName: "Owner", + }; + + const result = await initSessionState({ + ctx: channelMessageCtx, + cfg, + commandAuthorized: true, + }); + + expect(result.isNewSession).toBe(true); + expect(result.resetTriggered).toBe(true); + expect(result.sessionId).not.toBe(existingSessionId); + expect(result.bodyStripped).toBe("take notes"); + }); +}); + describe("applyResetModelOverride", () => { it("selects a model hint and strips it from the body", async () => { const cfg = {} as OpenClawConfig; diff --git a/src/channels/dock.ts b/src/channels/dock.ts index e30a10b3c5..6451643d1e 100644 --- a/src/channels/dock.ts +++ b/src/channels/dock.ts @@ -295,6 +295,9 @@ const DOCKS: Record = { resolveRequireMention: resolveSlackGroupRequireMention, resolveToolPolicy: resolveSlackGroupToolPolicy, }, + mentions: { + stripPatterns: () => ["<@[^>]+>"], + }, threading: { resolveReplyToMode: ({ cfg, accountId, chatType }) => resolveSlackReplyToMode(resolveSlackAccount({ cfg, accountId }), chatType),