Skip to content

fix(msteams): add graphMediaFallback so group-chat file attachments reach the agent#101096

Closed
packwood wants to merge 4 commits into
openclaw:mainfrom
packwood:msteams-group-graph-media-fallback
Closed

fix(msteams): add graphMediaFallback so group-chat file attachments reach the agent#101096
packwood wants to merge 4 commits into
openclaw:mainfrom
packwood:msteams-group-graph-media-fallback

Conversation

@packwood

@packwood packwood commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Files attached to Microsoft Teams group chat messages never reach the agent, even with all recommended Graph Application permissions granted (#89594). Teams strips the <attachment id="…"> tags from the message HTML it delivers to bots in group chats, so extractMSTeamsHtmlAttachmentIds returns an empty list and the existing Graph media fallback in resolveMSTeamsInboundMedia never fires. The Graph copy of the same message retains both the tag and the full reference attachment (SharePoint contentUrl + name), so the already-implemented downloadMSTeamsGraphMedia path works fine once it is allowed to run — the gate is the only blocker.

Verified discrepancy on a live tenant (same message, 19:…@thread.v2 group chat, plugin 2026.6.11):

  • Bot Framework activity: one text/html attachment, plugin diagnostics {'htmlAttachments': 1, 'attachmentTags': 0, 'attachmentIds': []}
  • Graph GET /chats/{chatId}/messages/{messageId}: body HTML contains <attachment id="…"></attachment> and attachments: [{ contentType: "reference", contentUrl: "https://…sharepoint.com/…/file.pdf", name: "file.pdf" }]

Why This Change Was Made

The gate exists for a good reason — mention-only group messages also arrive as text/html attachments, and keying on contentType alone caused spurious Graph lookups and 404 diagnostics (#58617). So instead of relaxing the gate unconditionally, this adds an explicit opt-in:

channels.msteams.graphMediaFallback (default: false) — for group-chat conversations that carried a text/html attachment and yielded no media, attempt the Graph message fetch even when no <attachment id> tags were extracted. Channel (team-scope) conversations are excluded and keep the tag-gated behavior: the channel Graph URL builder has separate known team-GUID and reply-URL defects (per ClawSweeper review, tracked with the broader channel repair in #90738), so this flag must not route channel traffic into that path.

  • Default false: behavior is byte-for-byte unchanged, including the [Bug]: msteams channel file attachments not downloaded — Graph fallback triggers on non-file HTML attachments #58617 protection (covered by existing tests, which still pass unmodified).
  • Enabled: costs one Graph message lookup per group-chat message that yields no downloadable media (including mention-only messages) — the trade-off is documented in docs/channels/msteams.md. Requires Chat.Read.All and Files.Read.All Application permissions with admin consent.
  • Personal (Bot Framework a:/8:orgid:) chats are unaffected — they keep the Bot Framework v3 attachments path.

User Impact

Operators whose agents live in Teams group chats can enable one config flag and file attachments (PDFs, docs, images uploaded as files) become readable by the agent. Channel (team-scope) attachment behavior is intentionally unchanged — that repair lands separately (#90738). Everyone else sees no change.

Evidence

  • Live validation: an equivalent gate relaxation applied to a production 2026.6.11 install (macOS, single-tenant bot) — a PDF attached to a group-chat message was fetched via the Graph reference path (downloaded attachments {'count': 1}) and the agent answered questions about its content. Before the change, the same message produced graph fallback never attempted (extraction all-zero) and the agent saw no file.
  • pnpm vitest run extensions/msteams/src/monitor-handler/inbound-media.test.ts — 16 passed (12 existing unmodified + 4 new: enabled-flag group fetch, default-off unchanged, personal-chat exclusion, channel-conversation exclusion)
  • pnpm test:extension msteams — 70 files, 999 tests passed
  • pnpm check — clean
  • pnpm test:contracts:channels — 34 passed

Relates to #89594 — fixes the group-chat variant with live proof. Channel-specific root causes (team GUID, reply-URL shape; #90733 analysis) are intentionally left to the broader channel repair (#90738), so this PR does not close the canonical issue.

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: msteams Channel integration: msteams size: S labels Jul 6, 2026
@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 6, 2026, 11:29 PM ET / 03:29 UTC.

Summary
Adds channels.msteams.graphMediaFallback, wires it into Microsoft Teams inbound media resolution for group chats, documents the option, regenerates config/docs metadata, and adds focused fallback tests.

PR surface: Source +49, Tests +122, Docs +19, Generated 0. Total +190 across 8 files.

Reproducibility: yes. Current main still skips the Graph fallback when Teams-delivered HTML has no extracted attachment IDs, and the PR plus linked issue include live tenant evidence that Graph retains the attachment reference.

Review metrics: 1 noteworthy metric.

  • MSTeams config surface: 1 optional key added. The new graphMediaFallback key is operator-facing and changes upgrade, permission, and support review scope before merge.

Stored data model
Persistent data-model change detected: unknown-data-model-change: src/config/bundled-channel-config-metadata.generated.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #89594
Summary: This PR is a partial candidate fix for the canonical Microsoft Teams inbound attachment issue, specifically the group-chat HTML-without-attachment-id variant; channel team GUID and reply URL repair remain in the broader open PR.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P2] Get maintainer sign-off on the default-off Teams config option versus folding this behavior into the broader Teams attachment repair.

Risk before merge

Maintainer options:

  1. Accept the default-off group-chat opt-in (recommended)
    Maintainers can merge once they explicitly accept the new config key, permission expectations, and extra Graph lookup tradeoff for opted-in group chats.
  2. Fold the opt-in into the broader repair
    Pause this PR and ask the Teams attachment work to land as one combined branch with channel URL fixes and group-chat fallback policy reconciled.
  3. Require a no-new-config shape
    Ask the author to remove the config surface and implement an owner-approved fallback policy that preserves the mention-only protection without operator setup.

Next step before merge

  • [P2] Manual review is needed because the remaining blocker is maintainer product acceptance of a new default-off Microsoft Teams config surface, not a narrow automated repair.

Maintainer decision needed

  • Question: Should OpenClaw accept channels.msteams.graphMediaFallback as a default-off group-chat attachment recovery option, or require this behavior to be folded into the broader Teams attachment repair?
  • Rationale: The code path is now narrow, but adding an operator-facing config option is a compatibility and support choice because it changes Graph call volume, permission expectations, and the message-delivery story for Teams attachment recovery.
  • Likely owner: steipete — This is a Teams plugin config/product tradeoff, and steipete has the strongest broad owner-boundary history signal among the likely owners listed.
  • Options:
    • Accept narrow default-off option (recommended): Land this PR as the group-chat recovery path while existing installs stay unchanged unless operators opt in.
    • Combine with broader Teams repair: Ask for one reconciled PR that handles group chats, channel team GUIDs, reply URLs, and fallback policy together.
    • Reject new config surface: Pause or close this PR if maintainers prefer automatic behavior or another product surface instead of a Teams-specific fallback knob.

Security
Cleared: No concrete security or supply-chain regression found; the diff reuses existing Teams Graph token, SSRF guard, host allowlist, and media downloader paths without dependency, workflow, lockfile, or secret-handling changes.

Review details

Best possible solution:

Land this narrow group-chat opt-in only if maintainers accept the default-off config surface; keep channel team-GUID and thread-reply URL repair with #90738 or combine the paths deliberately.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main still skips the Graph fallback when Teams-delivered HTML has no extracted attachment IDs, and the PR plus linked issue include live tenant evidence that Graph retains the attachment reference.

Is this the best way to solve the issue?

Yes as an implementation shape, with a product caveat. The patch stays inside the Microsoft Teams plugin, reuses the existing Graph downloader, keeps default behavior unchanged, and now scopes the opt-in to group chats; the unresolved question is whether this config surface should exist.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against d90c9a1ede8e.

Label changes

Label justifications:

  • P2: This is a focused Microsoft Teams inbound attachment bug fix with limited blast radius to one bundled channel plugin.
  • merge-risk: 🚨 compatibility: The PR adds a new default-off Teams config path whose enabled behavior changes Graph lookup scope and operator permission expectations.
  • merge-risk: 🚨 message-delivery: The enabled fallback controls whether Teams group-chat file attachments reach the agent-visible message context.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes redacted live production before/after evidence for the group-chat attachment path plus focused validation, so no additional contributor proof is needed for this scope.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted live production before/after evidence for the group-chat attachment path plus focused validation, so no additional contributor proof is needed for this scope.
Evidence reviewed

PR surface:

Source +49, Tests +122, Docs +19, Generated 0. Total +190 across 8 files.

View PR surface stats
Area Files Added Removed Net
Source 4 50 1 +49
Tests 1 122 0 +122
Docs 2 19 0 +19
Config 0 0 0 0
Generated 1 10 10 0
Other 0 0 0 0
Total 8 201 11 +190

What I checked:

Likely related people:

  • sudie-codes: Authored the focused Teams fallback change that added the current HTML attachment-id gate to avoid mention-only Graph fallback noise, which this PR must preserve. (role: introduced related gate behavior; confidence: high; commits: 2c211d171e88, 4fc5016f8fd2, ab9be8dba547; files: extensions/msteams/src/monitor-handler/inbound-media.ts, extensions/msteams/src/attachments/graph.ts, extensions/msteams/src/attachments/bot-framework.ts)
  • steipete: Commit history shows substantial adjacent Microsoft Teams plugin and security-sensitive attachment-flow work on the same owner boundary. (role: feature-history and plugin-boundary contributor; confidence: medium; commits: c56b56e514f8, 67035a6af8c7, 18acfe735239; files: extensions/msteams/src/attachments/graph.ts, extensions/msteams/src/monitor-handler/inbound-media.ts, extensions/msteams/src/monitor-handler/message-handler.ts)
  • vincentkoc: Recent history includes Teams Graph media diagnostic and release-source work adjacent to the same inbound media and Graph attachment path. (role: recent area contributor; confidence: medium; commits: b62251817e57, e085fa1a3ffd; files: extensions/msteams/src/monitor-handler/inbound-media.ts, extensions/msteams/src/attachments/graph.ts)
  • huangjianxiong: Current-main blame for the split Teams attachment modules points to a recent large file move/re-addition that carried this path forward. (role: recent current-main contributor; confidence: low; commits: c3c4d03f900b; files: extensions/msteams/src/monitor-handler/inbound-media.ts, extensions/msteams/src/attachments/graph.ts, extensions/msteams/src/attachments/shared.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (5 earlier review cycles)
  • reviewed 2026-07-06T19:44:22.804Z sha 098ae38 :: found issues before merge. :: [P2] Restrict channel fallback or fix channel URLs
  • reviewed 2026-07-06T20:46:51.601Z sha 098ae38 :: found issues before merge. :: [P2] Restrict channel fallback or fix channel URLs
  • reviewed 2026-07-06T21:18:05.806Z sha 098ae38 :: found issues before merge. :: [P2] Restrict channel fallback or fix channel URLs
  • reviewed 2026-07-06T23:41:12.269Z sha e3a28d6 :: needs changes before merge. :: [P2] Limit the fallback to group chats
  • reviewed 2026-07-07T02:45:46.845Z sha 6174d95 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jul 6, 2026
packwood added a commit to packwood/openclaw that referenced this pull request Jul 6, 2026
Channel (team-scope) conversations keep the tag-gated fallback: the
channel Graph URL builder still has known team-GUID and reply-URL
defects tracked with the broader channel attachment repair, so the
widened trigger must not route channel traffic into that path.
Addresses ClawSweeper review P2 on openclaw#101096.

Co-Authored-By: Claude Fable 5 <[email protected]>
@packwood

packwood commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the P2 in e3a28d6 (PR body updated to match):

  • Fallback scoped to group chats onlyconversationType === "channel" is now excluded from the widened trigger; channel conversations keep the existing <attachment id>-gated behavior so the flag cannot route channel traffic into the Graph URL builder's known team-GUID / reply-URL defects. That repair stays with the broader channel fix (fix(msteams): recover inbound channel and group-chat files safely #90738).
  • New test: channel conversation + flag enabled + no tags → no Graph fetch attempted (16/16 in inbound-media.test.ts, 999 in pnpm test:extension msteams, pnpm check clean).
  • Docs and JSDoc narrowed to group-chat scope; channel permissions removed from the requirement list; explicit note that channels are unaffected.
  • Closing language corrected: the PR no longer auto-closes [Bug]: In msteams channel messages, OpenClaw can't access inbound attachments #89594 — it fixes the group-chat variant only.

packwood added a commit to packwood/openclaw that referenced this pull request Jul 7, 2026
…not-channel

The prior scope fix excluded channel conversations with a denylist, but
personal DMs also arrive as conversationType 'personal' and can carry
Graph-compatible 19:[email protected] chat IDs that isBotFrameworkPersonalChatId
does not catch — so a documented group-chat option silently changed DM
Graph-lookup behavior. Gate on conversationType === 'groupChat' (normalized)
instead. Adds a regression: personal + Graph-compatible 19: ID + flag on
does not call Graph. Addresses ClawSweeper P1 on openclaw#101096.

Co-Authored-By: Claude Fable 5 <[email protected]>
@packwood

packwood commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the P1 in 6174d95.

The prior scope fix used a denylist (conversationType !== "channel"), which — as flagged — still let personal DMs through: they arrive as conversationType: "personal" and can carry Graph-compatible 19:[email protected] chat IDs that isBotFrameworkPersonalChatId does not catch, so the documented group-chat option would have changed DM Graph-lookup behavior.

  • Now an allowlist: the widened trigger fires only when conversationType normalizes to groupChat. Personal and channel conversations both keep the existing <attachment id>-gated behavior.
  • Regression test added (exactly the one requested): conversationType: "personal" + 19:[email protected] + flag enabled + no tags → no Graph call.
  • JSDoc clarified to "group chats only (conversationType groupChat); personal DMs and channel messages keep the tag-gated fallback."
  • Validation: 17/17 in inbound-media.test.ts, 1000 in pnpm test:extension msteams, pnpm check clean.

Remaining item is the maintainer product call on the default-off config surface vs. folding into #90738 — flagged as needing maintainer direction, not an automated edit.

@packwood

packwood commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Note on the two red checks (check-lint, checks-node-compact-small-whole-2): these are pre-existing main-side failures, not introduced by this PR.

Per CONTRIBUTING ("do not submit fixes for failures already red on main CI"), I've left them alone. This PR's own surfaces are green: inbound-media tests (17/17), pnpm test:extension msteams (1000), pnpm check, docs, and config-metadata checks. Happy to rebase once main is green if that helps re-run.

packwood and others added 4 commits July 6, 2026 22:45
…each the agent

Teams strips <attachment id> tags from the message HTML it delivers to
bots in group chats, so a real file attachment arrives with no reference
the plugin can extract and the existing Graph fallback gate never fires
(openclaw#89594). The Graph copy of the same message retains both the tag and
the reference attachment, so the already-implemented Graph media path
works once it is allowed to run.

Add channels.msteams.graphMediaFallback (default: false) which, for
non-personal conversations that carried a text/html attachment and
yielded no media, attempts the Graph message fetch even when no
attachment ids were extracted. Default stays off so mention-only
messages do not generate spurious Graph lookups (openclaw#58617).

Fixes openclaw#89594

Co-Authored-By: Claude Fable 5 <[email protected]>
Channel (team-scope) conversations keep the tag-gated fallback: the
channel Graph URL builder still has known team-GUID and reply-URL
defects tracked with the broader channel attachment repair, so the
widened trigger must not route channel traffic into that path.
Addresses ClawSweeper review P2 on openclaw#101096.

Co-Authored-By: Claude Fable 5 <[email protected]>
…not-channel

The prior scope fix excluded channel conversations with a denylist, but
personal DMs also arrive as conversationType 'personal' and can carry
Graph-compatible 19:[email protected] chat IDs that isBotFrameworkPersonalChatId
does not catch — so a documented group-chat option silently changed DM
Graph-lookup behavior. Gate on conversationType === 'groupChat' (normalized)
instead. Adds a regression: personal + Graph-compatible 19: ID + flag on
does not call Graph. Addresses ClawSweeper P1 on openclaw#101096.

Co-Authored-By: Claude Fable 5 <[email protected]>
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jul 7, 2026
@packwood
packwood force-pushed the msteams-group-graph-media-fallback branch from 6174d95 to 3ec3e9a Compare July 7, 2026 02:51
@packwood

packwood commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Update: rebased onto current main (4fa8af486f) — the earlier model-fallback.test.ts lint break and node-compact failure are resolved. The two red checks now (check-lint, checks-fast-bundled-protocol) are again pre-existing main-side failures unrelated to this PR:

  • check-lint: oxc(no-map-spread) at src/skills/workshop/curator.ts:521 — not a file this PR touches; present in main HEAD.
  • checks-fast-bundled-protocol: Swift protocol-binding drift (lasterror: AnyCodable) — unrelated to the TS/config change here.

Both reproduce on sibling PRs (#101271-#101274). Per CONTRIBUTING I'm leaving main-side failures alone. This PR's own surfaces remain green (msteams tests 17/17 + 1000 lane, pnpm check, config-metadata, docs-map). Will rebase again whenever main goes fully green.

@steipete

Copy link
Copy Markdown
Contributor

Incorporated and superseded by #90738, landed as dc8c0aaaf273ae5f56e58fba2ebb40e66cf73b84.

The landed rewrite preserves this PR's safe default-off channels.msteams.graphMediaFallback idea and group-chat recovery, while also canonicalizing channel AAD identity, separating root/reply/chat Graph URLs, failing closed against wrong-root files, bounding enrichment, retaining personal Bot Framework media, and removing invalid app-only /me fallbacks. Focused and full Teams Testbox suites, changed gates, fresh autoreview, and exact-head hosted CI passed.

Thank you @packwood for the investigation and implementation; your contribution is credited in the landed commit and PR body.

@steipete steipete closed this Jul 10, 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 docs Improvements or additions to documentation merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants