Skip to content

fix(msteams): use General channel conversation ID as team key for Bot Framework compatibility#41453

Closed
BradGroux wants to merge 1 commit intoopenclaw:mainfrom
BradGroux:fix/msteams-team-key-format
Closed

fix(msteams): use General channel conversation ID as team key for Bot Framework compatibility#41453
BradGroux wants to merge 1 commit intoopenclaw:mainfrom
BradGroux:fix/msteams-team-key-format

Conversation

@BradGroux
Copy link
Copy Markdown
Contributor

Summary

When OpenClaw starts up and resolves the MS Teams channel allowlist, it queries the Microsoft Graph API for team info and stores the Graph API group GUID (e.g. fa101332-cf00-431b-b0ea-f701a85fde81) as the team key in its allowlist config. At runtime, the Bot Framework sends activity.channelData.team.id as the General channel's conversation ID (e.g. 19:[email protected]) — a completely different format. The two keys never match, so every inbound channel message is silently dropped regardless of config. This PR fixes the mismatch by resolving the General channel's conversation ID during startup and storing that as the team key instead.

Problem

Observed behavior

After configuring a Teams channel in OpenClaw, channel messages are never processed. No errors appear — messages just vanish. The configured team matches by display name at startup, the resolved: true response looks correct, but at runtime nothing gets through.

Diagnostic evidence

Patching the message handler to log activity.channelData:

{
  "team": {
    "id": "19:[email protected]",
    "name": "Digital Meld"
  },
  "channel": {
    "id": "19:[email protected]"
  }
}

The startup resolver stored fa101332-cf00-431b-b0ea-f701a85fde81 (Graph GUID). The runtime key is 19:[email protected] (General channel conversation ID). These never match.

A workaround confirmed the root cause: manually replacing the Graph GUID in config with the General channel's conversation ID caused messages to flow correctly.

Root Cause

The Microsoft Graph API /teams endpoint returns groups by their AAD group GUID (e.g. fa101332-cf00-431b-b0ea-f701a85fde81). This is what listTeamsByName returns as team.id.

The Bot Framework, however, identifies the team context of a channel message using the General channel's conversation ID — the 19:…@thread.tacv2 format used for all Teams conversations. These are two different identifiers for the same team, and there is no documented 1:1 mapping without calling the channels API.

resolveMSTeamsChannelAllowlist was using the Graph GUID directly as teamId in the resolved config entry. At runtime, the allowlist check compares channelData.team.id (conversation ID format) against the stored teamId (GUID format) — a comparison that can never succeed.

Fix

In resolveMSTeamsChannelAllowlist's mapInput callback, after obtaining graphTeamId (the Graph GUID), the fix:

  1. Always calls listChannelsForTeam(token, graphTeamId) — necessary to find the General channel regardless of whether a specific channel was requested.
  2. Finds the General channel by displayName === "general" (case-insensitive).
  3. Uses the General channel's conversation ID as teamId — this matches what Bot Framework sends as channelData.team.id at runtime.
  4. Falls back to the Graph GUID if the General channel is not found (renamed or deleted edge case) so resolution still succeeds rather than silently breaking.
  5. Reuses the channel list when a specific channel is also configured, avoiding a redundant second API call.
// Before
const teamId = teamMatch.id?.trim(); // Graph GUID — wrong format at runtime

// After
const graphTeamId = teamMatch.id?.trim();
const teamChannels = await listChannelsForTeam(token, graphTeamId);
const generalChannel = teamChannels.find(
  (ch) => ch.displayName?.toLowerCase() === "general",
);
const teamId = generalChannel?.id ?? graphTeamId; // Conversation ID — matches Bot Framework

Testing

Unit tests (extensions/msteams/src/resolve-allowlist.test.ts):

  • Updated existing test "resolves team/channel by team name + channel display name" to assert that teamId is now the General channel's conversation ID, not the Graph GUID.
  • Added "uses General channel conversation ID as team key for team-only entry" — verifies team-only resolution stores the correct key.
  • Added "falls back to Graph GUID when General channel not found" — verifies the edge case gracefully degrades.

All 5 tests pass:

✓ extensions/msteams/src/resolve-allowlist.test.ts (5 tests) 3ms

Manual validation: Confirmed the workaround (manually setting the General channel conversation ID as config key) makes messages flow correctly. This fix automates that resolution at startup.

Breaking Changes

None. This is fully backward compatible:

  • Existing configs using a conversation ID as the team key (the manual workaround) continue to work — if the input matches the GUID regex (/^[0-9a-fA-F-]{16,}$/), it goes through listChannelsForTeam against that ID, which will still resolve correctly.
  • Configs using a team display name now automatically resolve to the correct conversation ID key.
  • No changes to the config file format or external-facing API.

Secondary Issue (noted, not fixed here)

The compiled logger wrapper in the MS Teams extension swallows structured data — debug() and info() only pass the first argument to the underlying logger, silently dropping context objects. This made diagnosing the root cause significantly harder. Tracked separately.

Related

Fixes #41390

… Framework compatibility

Bot Framework sends `activity.channelData.team.id` as the General channel's
conversation ID (e.g. `19:[email protected]`), not the Graph API group GUID
(e.g. `fa101332-cf00-431b-b0ea-f701a85fde81`). The startup resolver was
storing the Graph GUID as the team config key, so runtime matching always
failed and every channel message was silently dropped.

Fix: always call `listChannelsForTeam` during resolution to find the General
channel, then use its conversation ID as the stored `teamId`. When a specific
channel is also configured, reuse the same channel list rather than issuing a
second API call. Falls back to the Graph GUID if the General channel cannot
be found (renamed/deleted edge case).

Fixes openclaw#41390
Copilot AI review requested due to automatic review settings March 9, 2026 21:32
@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: M labels Mar 9, 2026
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: d618c71809

ℹ️ 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 +1 to +3
import { mapAllowlistResolutionInputs } from "openclaw/plugin-sdk/compat";
import { searchGraphUsers } from "./graph-users.js";
import {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Badge Replace unresolved imports at top of resolver module

The new imports in resolve-allowlist.ts are not resolvable in this tree: openclaw/plugin-sdk/compat is not exported by package.json (only ./plugin-sdk is), and there are no local extensions/msteams/src/graph*.ts modules backing ./graph.js or ./graph-users.js. This causes module-resolution failures when the MS Teams resolver is loaded (and during type-check/build), so the extension cannot run outside the mocked unit test context.

Useful? React with 👍 / 👎.

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

Fixes MS Teams allowlist resolution so the stored team key matches what Bot Framework sends at runtime (channelData.team.id) by deriving the key from the General channel conversation ID.

Changes:

  • Update Teams allowlist resolution to use the General channel’s conversation ID as teamId (fallback to Graph GUID if unavailable).
  • Refactor allowlist resolution to use a shared mapping helper (mapAllowlistResolutionInputs) and a shared Graph user search helper.
  • Add unit tests covering the new resolution behavior and fallback path.

Reviewed changes

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

File Description
extensions/msteams/src/resolve-allowlist.ts Changes team key derivation to use General channel conversation ID; refactors resolution flow to a helper-based mapping approach.
extensions/msteams/src/resolve-allowlist.test.ts Adds unit tests validating the new team key behavior, including fallback when General isn’t found.

Comment on lines +17 to +26
vi.mock("./graph.js", () => ({
listTeamsByName,
listChannelsForTeam,
normalizeQuery,
resolveGraphToken,
}));

vi.mock("./graph-users.js", () => ({
searchGraphUsers,
}));
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

The test mocks ./graph.js and ./graph-users.js, but those modules are not present under extensions/msteams/src in this PR. Vitest will fail to resolve these imports even if the functions are mocked. Add the missing modules (or adjust the test to mock the actual existing module(s) used by resolve-allowlist.ts).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +8
import { mapAllowlistResolutionInputs } from "openclaw/plugin-sdk/compat";
import { searchGraphUsers } from "./graph-users.js";
import {
listChannelsForTeam,
listTeamsByName,
normalizeQuery,
resolveGraphToken,
} from "./graph.js";
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

The new imports point to modules that don't exist in this repo (openclaw/plugin-sdk/compat, ./graph.js, ./graph-users.js). As-is, this file will fail to compile/run. Either add the missing modules (and ensure they are exported via package exports), or revert to the previous in-file Graph helpers / import from an existing local module (e.g. factor the shared Graph helpers into a new extensions/msteams/src/graph.ts and update imports accordingly).

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR fixes a critical silent message-drop bug in the MS Teams extension where inbound channel messages were never processed. The root cause was a key format mismatch: during startup, resolveMSTeamsChannelAllowlist stored the Microsoft Graph API group GUID (e.g., fa101332-cf00-431b-b0ea-f701a85fde81) as the team key, while the Bot Framework identifies teams at runtime using the General channel's conversation ID (e.g., 19:[email protected]) — two completely different identifiers with no overlap.

Key changes:

  • Always calls listChannelsForTeam during resolution to look up the General channel's conversation ID and use it as teamId, matching what Bot Framework sends as channelData.team.id at runtime.
  • Falls back to the Graph GUID if the General channel cannot be found (renamed or deleted edge case), ensuring graceful degradation.
  • Reuses the channel list when a specific channel entry is also being resolved, avoiding redundant API calls.
  • Refactors to use mapAllowlistResolutionInputs from the plugin SDK, replacing hand-rolled loops in both resolveMSTeamsChannelAllowlist and resolveMSTeamsUserAllowlist.
  • Adds three focused unit tests covering the main fix (team/channel resolution), team-only resolution, and the General-channel-not-found fallback edge case.

All tests pass, and the fix is fully backward-compatible with existing configurations.

Confidence Score: 4/5

  • Safe to merge — fixes a genuine silent message-drop bug with no API changes and appropriate test coverage.
  • The fix is correct and well-reasoned: using the General channel's conversation ID is the only way to match what Bot Framework sends as channelData.team.id at runtime. The fallback to the Graph GUID prevents silent failures in edge cases. Refactoring to mapAllowlistResolutionInputs is straightforward. The intentional choice to always call listChannelsForTeam even for team-only entries is necessary and well-commented. Test coverage is thorough. Score is 4 rather than 5 only because the PR description's backward-compatibility claim about conversation-ID keys is slightly inaccurate: a 19:…@thread.tacv2 conversation ID does not match the GUID regex check on line 116 (/^[0-9a-fA-F-]{16,}$/), so it would be passed to listTeamsByName as a display-name query and return no matches — but this was equally true before the PR, so it is a pre-existing limitation rather than a regression.
  • No files require special attention. Both changed files are reviewed and the logic is sound.

Last reviewed commit: d618c71

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

2 participants