Skip to content

feat(msteams): add Teams reaction support#37853

Closed
bradgcoza wants to merge 7 commits intoopenclaw:mainfrom
bradgcoza:feat/msteams-reaction-support
Closed

feat(msteams): add Teams reaction support#37853
bradgcoza wants to merge 7 commits intoopenclaw:mainfrom
bradgcoza:feat/msteams-reaction-support

Conversation

@bradgcoza
Copy link
Copy Markdown

Summary

Add handler for Bot Framework reaction events (reactionsAdded/reactionsRemoved) in the MSTeams extension.

What it does

When a user reacts to a message in Teams (thumbs up, heart, etc.), the extension now:

  • Extracts reaction type from activity.reactionsAdded/activity.reactionsRemoved
  • Resolves sender identity and runs allowlist authorization
  • Routes to the correct agent via resolveAgentRoute()
  • Fires a system event with emoji-mapped summary (e.g. Teams DM: Bradley reacted 👍 on message 1772788362699)

Reaction mapping

Teams type Emoji
like 👍
heart ❤️
laugh 😆
surprised 😮
sad 😢
angry 😡

Files changed

  • extensions/msteams/src/monitor-handler.ts — Extended MSTeamsActivityHandler type with onReactionsAdded/onReactionsRemoved, registered handlers
  • extensions/msteams/src/monitor-handler/reaction-handler.ts — New file implementing reaction event handling with auth, routing, and dispatch

…moved)

Add handler for Bot Framework reaction events in the MSTeams extension.

When a user reacts to a message in Teams (thumbs up, heart, etc.), the
extension now:
- Extracts reaction type from activity.reactionsAdded/reactionsRemoved
- Resolves sender identity and runs allowlist authorization
- Routes to the correct agent via resolveAgentRoute()
- Fires a system event with emoji-mapped summary
  (e.g. 'Teams DM: Bradley reacted 👍 on message 1772788362699')

Reaction type mapping: like→👍, heart→❤️, laugh→😆, surprised→😮, sad→😢, angry→😡

Files changed:
- extensions/msteams/src/monitor-handler.ts: Extended MSTeamsActivityHandler
  type with onReactionsAdded/onReactionsRemoved, registered handlers
- extensions/msteams/src/monitor-handler/reaction-handler.ts: New file
  implementing reaction event handling with auth, routing, and dispatch
Copilot AI review requested due to automatic review settings March 6, 2026 13:36
@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S labels Mar 6, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds Teams reaction event support (reactionsAdded/reactionsRemoved) to the MSTeams extension. The wiring in monitor-handler.ts is clean and consistent with existing patterns. The new reaction-handler.ts correctly handles identity resolution, allowlist authorization for DMs, and agent routing, but its authorization logic for group chats and channels diverges from message-handler.ts in two ways that allow reactions to bypass intended restrictions:

  • groupPolicy: "disabled" is silently ignoredsenderGroupPolicy is always "allowlist" or "open", never "disabled", so reactions from group chats fire events even when the operator has disabled group-chat processing.
  • Channel/team allowlists are never enforcedresolveMSTeamsRouteConfig (the channelGate) is not called, meaning reactions from channels/teams that are explicitly blocked in config still reach the agent via enqueueSystemEvent.

Confidence Score: 2/5

  • Not safe to merge until the group-policy and channel-allowlist gaps in reaction-handler.ts are addressed.
  • Two logic bugs in the authorization path mean that a groupPolicy: "disabled" config and per-channel/team allowlists are both silently bypassed for reaction events. These are real correctness issues that mirror existing protections in message-handler.ts and could expose the agent to events from conversations the operator explicitly intended to block.
  • extensions/msteams/src/monitor-handler/reaction-handler.ts — specifically the senderGroupPolicy computation (lines 93-94) and the missing channelGate check before enqueueSystemEvent (around line 122).

Last reviewed commit: 652d7d0

Comment on lines +93 to +94
const senderGroupPolicy =
effectiveGroupAllowFrom.length > 0 ? "allowlist" : ("open" as const);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

groupPolicy: "disabled" is not respected for group chats

senderGroupPolicy is always computed as "allowlist" or "open" — it never becomes "disabled". This means reactions from group chats will still be processed and fire system events even when the user has configured msteamsCfg.groupPolicy === "disabled".

In message-handler.ts, the equivalent computation correctly derives "disabled" by first evaluating the configured groupPolicy:

// message-handler.ts (correct approach)
const groupPolicy = !isDirectMessage && msteamsCfg
  ? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
  : "disabled";

const senderGroupPolicy =
  groupPolicy === "disabled" ? "disabled"
  : effectiveGroupAllowFrom.length > 0 ? "allowlist"
  : "open";

The reaction handler needs the same gate. Without it a groupPolicy: "disabled" config silences regular messages but still leaks reaction events to the agent.

Suggested change
const senderGroupPolicy =
effectiveGroupAllowFrom.length > 0 ? "allowlist" : ("open" as const);
const configuredGroupPolicy =
!isDirectMessage && msteamsCfg
? (msteamsCfg.groupPolicy ?? "allowlist")
: "disabled";
const senderGroupPolicy =
configuredGroupPolicy === "disabled" ? "disabled" : effectiveGroupAllowFrom.length > 0 ? "allowlist" : ("open" as const);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/reaction-handler.ts
Line: 93-94

Comment:
**`groupPolicy: "disabled"` is not respected for group chats**

`senderGroupPolicy` is always computed as `"allowlist"` or `"open"` — it never becomes `"disabled"`. This means reactions from group chats will still be processed and fire system events even when the user has configured `msteamsCfg.groupPolicy === "disabled"`.

In `message-handler.ts`, the equivalent computation correctly derives `"disabled"` by first evaluating the configured `groupPolicy`:

```ts
// message-handler.ts (correct approach)
const groupPolicy = !isDirectMessage && msteamsCfg
  ? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
  : "disabled";

const senderGroupPolicy =
  groupPolicy === "disabled" ? "disabled"
  : effectiveGroupAllowFrom.length > 0 ? "allowlist"
  : "open";
```

The reaction handler needs the same gate. Without it a `groupPolicy: "disabled"` config silences regular messages but still leaks reaction events to the agent.

```suggestion
    const configuredGroupPolicy =
      !isDirectMessage && msteamsCfg
        ? (msteamsCfg.groupPolicy ?? "allowlist")
        : "disabled";
    const senderGroupPolicy =
      configuredGroupPolicy === "disabled" ? "disabled" : effectiveGroupAllowFrom.length > 0 ? "allowlist" : ("open" as const);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +122 to +130
// Resolve agent route
const route = core.channel.routing.resolveAgentRoute({
cfg,
channel: "msteams",
peer: {
kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
id: isDirectMessage ? senderId : conversationId,
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Channel/team-level allowlist is not enforced for reactions

message-handler.ts calls resolveMSTeamsRouteConfig and then explicitly drops events when a team or channel is not on the configured allowlist:

// message-handler.ts
const channelGate = resolveMSTeamsRouteConfig({ cfg: msteamsCfg, teamId, teamName, conversationId, channelName });
// ...
if (groupPolicy === "allowlist") {
  if (channelGate.allowlistConfigured && !channelGate.allowed) {
    // drop: not in team/channel allowlist
    return;
  }
  if (effectiveGroupAllowFrom.length === 0 && !channelGate.allowlistConfigured) {
    // drop: allowlist policy but nothing configured
    return;
  }
}

The reaction handler skips resolveMSTeamsRouteConfig entirely. As a result, reactions arriving from a channel/team that is explicitly blocked in config will still reach the agent via enqueueSystemEvent. This is inconsistent with message handling and can surface information from channels the operator intended to block.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/reaction-handler.ts
Line: 122-130

Comment:
**Channel/team-level allowlist is not enforced for reactions**

`message-handler.ts` calls `resolveMSTeamsRouteConfig` and then explicitly drops events when a team or channel is not on the configured allowlist:

```ts
// message-handler.ts
const channelGate = resolveMSTeamsRouteConfig({ cfg: msteamsCfg, teamId, teamName, conversationId, channelName });
// ...
if (groupPolicy === "allowlist") {
  if (channelGate.allowlistConfigured && !channelGate.allowed) {
    // drop: not in team/channel allowlist
    return;
  }
  if (effectiveGroupAllowFrom.length === 0 && !channelGate.allowlistConfigured) {
    // drop: allowlist policy but nothing configured
    return;
  }
}
```

The reaction handler skips `resolveMSTeamsRouteConfig` entirely. As a result, reactions arriving from a channel/team that is explicitly blocked in config will still reach the agent via `enqueueSystemEvent`. This is inconsistent with message handling and can surface information from channels the operator intended to block.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Microsoft Teams “reaction” (reactionsAdded/reactionsRemoved) handling to the MSTeams monitor so reactions can be routed to the correct agent and surfaced as system events.

Changes:

  • Added a new reaction handler that authorizes the sender, resolves routing, and enqueues a system event summary.
  • Extended the MSTeams activity handler surface and registered reaction-added/removed callbacks.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
extensions/msteams/src/monitor-handler/reaction-handler.ts Implements reaction event parsing, authz, routing, and system-event dispatch.
extensions/msteams/src/monitor-handler.ts Extends handler type and wires reaction handlers into the MSTeams ActivityHandler pipeline.

Comment on lines +74 to +110
// Authorization — reuse the same allowlist logic as message-handler
const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
const storedAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "msteams",
accountId: pairing.accountId,
dmPolicy,
readStore: pairing.readStoreForDmPolicy,
});

const dmAllowFrom = msteamsCfg?.allowFrom ?? [];
const configuredDmAllowFrom = dmAllowFrom.map((v) => String(v));
const groupAllowFrom = msteamsCfg?.groupAllowFrom;
const resolvedAllowFromLists = resolveEffectiveAllowFromLists({
allowFrom: configuredDmAllowFrom,
groupAllowFrom,
storeAllowFrom: storedAllowFrom,
dmPolicy,
});
const effectiveGroupAllowFrom = resolvedAllowFromLists.effectiveGroupAllowFrom;
const senderGroupPolicy =
effectiveGroupAllowFrom.length > 0 ? "allowlist" : ("open" as const);
const access = resolveDmGroupAccessWithLists({
isGroup: !isDirectMessage,
dmPolicy,
groupPolicy: isDirectMessage ? dmPolicy : senderGroupPolicy,
allowFrom: configuredDmAllowFrom,
groupAllowFrom,
storeAllowFrom: storedAllowFrom,
groupAllowFromFallbackToAllowFrom: false,
isSenderAllowed: (allowFrom) =>
resolveMSTeamsAllowlistMatch({
allowFrom,
senderId,
senderName,
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
}).allowed,
});
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reaction authorization is not aligned with the message handler: this logic ignores msteamsCfg.groupPolicy (including disabled) and the team/channel allowlist gate (resolveMSTeamsRouteConfig). As written, reactions in group/channel conversations can be enqueued even when group messaging is disabled or restricted by team/channel allowlist. Please reuse the same groupPolicy + channelGate checks as message-handler.ts before allowing reactions in non-DM conversations.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +154
core.system.enqueueSystemEvent(summary, {
sessionKey: route.sessionKey,
contextKey: `msteams:reaction:${conversationId}:${direction}:${activity.id ?? Date.now()}`,
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contextKey falls back to Date.now() when activity.id is missing. This makes reaction events non-deterministic across retries/replays and can create duplicate system events. Consider using a stable fallback (e.g. "unknown") and/or incorporating stable fields like replyToId, senderId, direction, and reaction types, similar to how the message handler uses activity.id ?? "unknown".

Suggested change
core.system.enqueueSystemEvent(summary, {
sessionKey: route.sessionKey,
contextKey: `msteams:reaction:${conversationId}:${direction}:${activity.id ?? Date.now()}`,
const rawReactionTypes = reactions.map((r) => r.type).sort().join(",");
core.system.enqueueSystemEvent(summary, {
sessionKey: route.sessionKey,
contextKey: `msteams:reaction:${conversationId}:${direction}:${replyToId ?? "none"}:${senderId}:${rawReactionTypes}:${activity.id ?? "unknown"}`,

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +44
export function createMSTeamsReactionHandler(deps: MSTeamsMessageHandlerDeps) {
const { cfg, log } = deps;
const core = getMSTeamsRuntime();
const pairing = createScopedPairingAccess({
core,
channel: "msteams",
accountId: DEFAULT_ACCOUNT_ID,
});
const msteamsCfg = cfg.channels?.msteams;

return async function handleTeamsReaction(
context: MSTeamsTurnContext,
direction: "added" | "removed",
) {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handler is newly introduced but there are currently no MSTeams tests exercising reaction events (added/removed), authorization decisions (dmPolicy/groupPolicy/allowlists), and the resulting enqueueSystemEvent payload. Adding a small Vitest suite for createMSTeamsReactionHandler (similar to existing message-handler.*.test.ts) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +25
const REACTION_EMOJI: Record<string, string> = {
like: "👍",
heart: "❤️",
laugh: "😆",
surprised: "😮",
sad: "😢",
angry: "😡",
};
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says reaction summaries are emoji-mapped using colon codes like :thumbsup:, but the implementation emits raw Unicode emoji (e.g. 👍). Please either update the PR description/docs to match the actual output or change the mapping/formatter to emit the documented :name: form for consistency with the documented behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +28
onReactionsAdded: (
handler: (context: unknown, next: () => Promise<void>) => Promise<void>,
) => MSTeamsActivityHandler;
onReactionsRemoved: (
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSTeamsActivityHandler now requires onReactionsAdded/onReactionsRemoved. This breaks existing MSTeams tests that construct minimal handler stubs (e.g. monitor-handler.file-consent.test.ts's createActivityHandler()), and will also throw at runtime if a stub/older SDK handler lacks these hooks. Either update the stubs/tests to implement these methods, or make the new hooks optional and guard the registrations in registerMSTeamsHandlers.

Suggested change
onReactionsAdded: (
handler: (context: unknown, next: () => Promise<void>) => Promise<void>,
) => MSTeamsActivityHandler;
onReactionsRemoved: (
onReactionsAdded?: (
handler: (context: unknown, next: () => Promise<void>) => Promise<void>,
) => MSTeamsActivityHandler;
onReactionsRemoved?: (

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 652d7d0ffc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +93 to +94
const senderGroupPolicy =
effectiveGroupAllowFrom.length > 0 ? "allowlist" : ("open" as const);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor configured groupPolicy in reaction access checks

senderGroupPolicy is computed solely from whether effectiveGroupAllowFrom is empty, which forces group reactions into open when no list exists and never honors channels.msteams.groupPolicy (including disabled). Message intake explicitly resolves and enforces group policy in extensions/msteams/src/monitor-handler/message-handler.ts:159-236, so this regression allows group/channel reactions to be enqueued even when group traffic is configured to be blocked.

Useful? React with 👍 / 👎.

Comment on lines +95 to +99
const access = resolveDmGroupAccessWithLists({
isGroup: !isDirectMessage,
dmPolicy,
groupPolicy: isDirectMessage ? dmPolicy : senderGroupPolicy,
allowFrom: configuredDmAllowFrom,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce team/channel allowlist gate for reactions

This authorization path checks sender allowlists but never applies the team/channel route allowlist (teams/channels) that message handling enforces via resolveMSTeamsRouteConfig in extensions/msteams/src/monitor-handler/message-handler.ts:168-247. In deployments that scope MSTeams ingestion to specific teams/channels, reactions from excluded conversations will still pass here and be delivered to enqueueSystemEvent.

Useful? React with 👍 / 👎.

Address Greptile review findings:
- Respect groupPolicy: disabled for group chat reactions
- Add resolveMSTeamsRouteConfig channelGate check
- Propagate disabled through senderGroupPolicy
- Add explicit group gating block mirroring message-handler.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e55eb51b7e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


const handleReaction = createMSTeamsReactionHandler(deps);

handler.onReactionsAdded(async (context, next) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard reaction hook registration against missing SDK methods

registerMSTeamsHandlers now calls handler.onReactionsAdded(...) and handler.onReactionsRemoved(...) unconditionally, but the previous handler contract only required onMessage/onMembersAdded, and existing in-repo stubs (for example in extensions/msteams/src/monitor-handler.file-consent.test.ts) still omit reaction hooks. In any environment where the provided handler object lacks these methods (custom wrappers, older SDK surfaces, or test doubles), initialization throws a TypeError and the Teams monitor cannot start.

Useful? React with 👍 / 👎.

Jarvis added 3 commits March 6, 2026 14:10
Add onReactionsAdded and onReactionsRemoved to createActivityHandler()
mock in file-consent test to satisfy MSTeamsActivityHandler type.
Replace openclaw/plugin-sdk with openclaw/plugin-sdk/msteams in
monitor-handler.ts and reaction-handler.ts to satisfy the
no-monolithic-plugin-sdk-entry-imports lint rule.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: efaf060b20

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}).allowed,
});

if (access.decision !== "allow") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect open groupPolicy in reaction authorization

createMSTeamsReactionHandler unconditionally returns when access.decision !== "allow", but access is computed with senderGroupPolicy that becomes "allowlist" whenever effectiveGroupAllowFrom is non-empty. In a configuration with channels.msteams.groupPolicy: "open" plus a populated groupAllowFrom, group reactions from users outside that list are incorrectly dropped, even though open policy should allow them; createMSTeamsMessageHandler only applies this pre-check to DMs (message-handler.ts), so this is a reaction-only regression.

Useful? React with 👍 / 👎.

The import used "openclaw/plugin-sdk/msteams" which doesn't exist as a subpath export. Changed to "openclaw/plugin-sdk" to match every other file in the extension.
@BradGroux BradGroux self-assigned this Mar 10, 2026
@BradGroux
Copy link
Copy Markdown
Contributor

Hi @bradgcoza — thanks for the submission. I’m the new Microsoft Teams maintainer for OpenClaw. Please give me a day or two to work through the open Teams backlog. Also, join the Twitter community for daily MS Teams feedback + updates: https://x.com/i/communities/2031170403607974228

@BradGroux
Copy link
Copy Markdown
Contributor

Closing as superseded by #51646, which implements the same Teams reaction support feature with an updated approach. Thank you for the initial work on this.

@BradGroux BradGroux closed this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants