diff --git a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts index 6d1a95817c91..6cf86a10d017 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts @@ -606,6 +606,98 @@ describe("msteams monitor handler authz", () => { expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).not.toHaveBeenCalled(); }); + it("marks skipped channel message system events as non-owner", async () => { + resetThreadMocks(); + const { deps, enqueueSystemEvent } = createDeps({ + channels: { + msteams: { + groupPolicy: "open", + requireMention: true, + }, + }, + } as OpenClawConfig); + + const handler = createMSTeamsMessageHandler(deps); + await handler( + createMessageActivity({ + id: "msg-skip-mention", + text: "please run the deployment", + from: { + id: "member-id", + aadObjectId: "member-aad", + name: "Member", + }, + conversation: { + id: "19:channel@thread.tacv2", + conversationType: "channel", + }, + channelData: { + team: { id: "team123", name: "Team 123" }, + channel: { name: "General" }, + }, + }), + ); + + expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).not.toHaveBeenCalled(); + const systemEventCall = enqueueSystemEvent.mock.calls.find( + ([text]) => typeof text === "string" && text.includes("please run the deployment"), + ); + if (!systemEventCall) { + throw new Error("expected skipped Teams message system event"); + } + expect(systemEventCall[1]).toMatchObject({ + forceSenderIsOwnerFalse: true, + trusted: false, + }); + }); + + it("keeps dispatched primary message system events owner-neutral", async () => { + resetThreadMocks(); + const { deps, enqueueSystemEvent } = createDeps({ + channels: { + msteams: { + groupPolicy: "open", + requireMention: false, + }, + }, + } as OpenClawConfig); + + const handler = createMSTeamsMessageHandler(deps); + await handler( + createMessageActivity({ + id: "msg-active", + text: "please check the build", + from: { + id: "member-id", + aadObjectId: "member-aad", + name: "Member", + }, + conversation: { + id: "19:channel@thread.tacv2", + conversationType: "channel", + }, + channelData: { + team: { id: "team123", name: "Team 123" }, + channel: { name: "General" }, + }, + }), + ); + + expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).toHaveBeenCalled(); + const systemEventCall = enqueueSystemEvent.mock.calls.find( + ([text]) => typeof text === "string" && text.includes("please check the build"), + ); + if (!systemEventCall) { + throw new Error("expected active Teams message system event"); + } + expect(systemEventCall[1]).not.toMatchObject({ + forceSenderIsOwnerFalse: true, + }); + expect(systemEventCall[1]).not.toMatchObject({ + trusted: false, + }); + }); + it("authorizes text control commands from static access groups", async () => { resetThreadMocks(); const hasControlCommand = vi.fn(() => true); diff --git a/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts b/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts index 23398c80ecc6..61f5b6d5ead4 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts @@ -38,11 +38,20 @@ vi.mock("../graph-thread.js", () => { describe("msteams thread parent context injection", () => { type MessageHandler = ReturnType; + type ParentSystemEventCall = [ + string, + { + sessionKey: string; + contextKey?: string; + forceSenderIsOwnerFalse?: boolean; + trusted?: boolean; + }, + ]; function findParentSystemEventCall( mock: ReturnType, - ): [string, { sessionKey: string; contextKey?: string }] | undefined { - const calls = mock.mock.calls as Array<[string, { sessionKey: string; contextKey?: string }]>; + ): ParentSystemEventCall | undefined { + const calls = mock.mock.calls as ParentSystemEventCall[]; return calls.find(([text]) => text.startsWith("Replying to @")); } @@ -93,6 +102,10 @@ describe("msteams thread parent context injection", () => { expect(parentCall[0]).toBe("Replying to @Alice: Can someone investigate the latency spike?"); expect(parentCall[1]?.contextKey).toContain("msteams:thread-parent:"); expect(parentCall[1]?.contextKey).toContain("thread-root-123"); + expect(parentCall[1]).toMatchObject({ + forceSenderIsOwnerFalse: true, + trusted: false, + }); }); it("caches parent fetches across thread replies in the same session", async () => { diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 60660ec91e4f..28e0193f1e2d 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -498,10 +498,15 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { ? `Teams DM from ${senderName}` : `Teams message in ${conversationType} from ${senderName}`; - core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, { - sessionKey: route.sessionKey, - contextKey: `msteams:message:${conversationId}:${activity.id ?? "unknown"}`, - }); + const enqueuePrimaryMessageSystemEvent = (opts?: { + forceSenderIsOwnerFalse?: boolean; + trusted?: boolean; + }) => + core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, { + sessionKey: route.sessionKey, + contextKey: `msteams:message:${conversationId}:${activity.id ?? "unknown"}`, + ...opts, + }); const channelId = conversationId; const { teamConfig, channelConfig } = channelGate; @@ -536,6 +541,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { requireMention, mentioned, }); + enqueuePrimaryMessageSystemEvent({ + forceSenderIsOwnerFalse: true, + trusted: false, + }); createChannelHistoryWindow({ historyMap: conversationHistories }).record({ historyKey: conversationId, limit: historyLimit, @@ -549,6 +558,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { return; } } + enqueuePrimaryMessageSystemEvent(); let graphConversationId = translateMSTeamsDmConversationIdForGraph({ isDirectMessage, conversationId, @@ -665,6 +675,8 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), { sessionKey: route.sessionKey, contextKey: `msteams:thread-parent:${conversationId}:${activity.replyToId}`, + forceSenderIsOwnerFalse: true, + trusted: false, }); markParentContextInjected(route.sessionKey, activity.replyToId); } diff --git a/extensions/msteams/src/monitor-handler/reaction-handler.test.ts b/extensions/msteams/src/monitor-handler/reaction-handler.test.ts index 39700ed42abb..33dec61e2de3 100644 --- a/extensions/msteams/src/monitor-handler/reaction-handler.test.ts +++ b/extensions/msteams/src/monitor-handler/reaction-handler.test.ts @@ -207,6 +207,8 @@ describe("createMSTeamsReactionHandler", () => { expect(label).toContain("added"); expect(meta.sessionKey).toBe("test-session"); expect(meta.contextKey).toContain("added"); + expect(meta.forceSenderIsOwnerFalse).toBe(true); + expect(meta.trusted).toBe(false); }); it("enqueues system event for reactionsRemoved", async () => { diff --git a/extensions/msteams/src/monitor-handler/reaction-handler.ts b/extensions/msteams/src/monitor-handler/reaction-handler.ts index 2490c89149cc..dfcefc64affd 100644 --- a/extensions/msteams/src/monitor-handler/reaction-handler.ts +++ b/extensions/msteams/src/monitor-handler/reaction-handler.ts @@ -116,6 +116,8 @@ export function createMSTeamsReactionHandler(deps: MSTeamsMessageHandlerDeps) { core.system.enqueueSystemEvent(label, { sessionKey: route.sessionKey, contextKey: `msteams:reaction:${conversationId}:${targetMessageId}:${senderId}:${reactionType}:${direction}`, + forceSenderIsOwnerFalse: true, + trusted: false, }); } };