fix(msteams): mark external system events as non-owner

Marks skipped and supplemental Microsoft Teams system events as non-owner/untrusted while preserving active primary message dispatch behavior.

Verified before merge:
- PR was open, not draft, mergeable, and clean against main
- Matched head: 4f79f46205
- GitHub checks passed, including Real behavior proof, auto-response, build artifacts, type/lint checks, channel/runtime critical quality checks, and security-fast
- ClawSweeper marked proof sufficient with no concrete contributor-facing blocker remaining

Co-authored-by: GuoJiaming <804436395@qq.com>
This commit is contained in:
Jiaming Guo
2026-05-20 10:48:17 +08:00
committed by GitHub
parent e1c1c57242
commit 125f0c31dd
5 changed files with 127 additions and 6 deletions
@@ -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);
@@ -38,11 +38,20 @@ vi.mock("../graph-thread.js", () => {
describe("msteams thread parent context injection", () => {
type MessageHandler = ReturnType<typeof createMSTeamsMessageHandler>;
type ParentSystemEventCall = [
string,
{
sessionKey: string;
contextKey?: string;
forceSenderIsOwnerFalse?: boolean;
trusted?: boolean;
},
];
function findParentSystemEventCall(
mock: ReturnType<typeof vi.fn>,
): [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 () => {
@@ -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);
}
@@ -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 () => {
@@ -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,
});
}
};