openclaw - ✅(Solved) Fix [Bug]: msteams search action fails — $search not supported on /chats/messages with Application permissions [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#70127Fetched 2026-04-23 07:28:55
View on GitHub
Comments
0
Participants
1
Timeline
7
Reactions
0
Author
Participants
Timeline (top)
referenced ×5cross-referenced ×1labeled ×1

The msteams:search tool action fails 100% of the time with Graph API HTTP 400 because it uses the $search query parameter which is not supported on /chats/{id}/messages with Application permissions.

Error Message

[tools] message failed: Graph /chats/19%3A...%40thread.tacv2/messages?$search=%22foo%22&$top=5 failed (400): {"error":{"code":"","message":"The query specified in the URI is not valid. Query option 'Search' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings."}} Severity: High — the msteams:search tool is unusable; any agent that invokes it receives an error instead of search results. Consequence: Agents cannot perform message search in MS Teams. Workflows depending on chat-history search are blocked, and error output is surfaced to users as a tool failure.

Root Cause

The msteams:search tool action fails 100% of the time with Graph API HTTP 400 because it uses the $search query parameter which is not supported on /chats/{id}/messages with Application permissions.

Fix Action

Fixed

PR fix notes

PR #70287: fix(msteams): drop unsupported $search on msteams:search (AI-assisted)

Description (problem / solution / changelog)

Summary

  • Problem: msteams:search action hits HTTP 400: Search is not supported because Graph API blocks $search on /chats/{id}/messages and /teams/*/channels/*/messages when called with Application permissions (the default for bot auth).
  • Why it matters: The action is unusable for any bot running on app-only credentials — which is the most common msteams deployment.
  • What changed: Replace $search with a list-and-local-filter path that works under Application permissions. Sender filtering still pushes down via $filter (which Graph supports app-only). Local filter strips HTML bodies so queries match rendered text.
  • What did NOT change (scope boundary): Delegated-auth code paths, other msteams actions, Graph token resolution, or /search/query callers elsewhere in the repo. Search range is now bounded to the recent list window rather than the full archive — full-archive search requires Delegated auth and is out of scope for this fix.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #70127
  • This PR fixes a bug or regression

Root Cause

  • Root cause: The original implementation of searchMessagesMSTeams used $search in the Graph query string. Graph API documentation is explicit that $search on message collections is not available for Application permissions — only /search/query with Delegated auth supports it for chat messages, and even that blocks the chatMessage entity for app-only.
  • Missing detection / guardrail: No integration test hit a Graph-like 400 for $search. Unit tests mocked successful responses, so the permission mismatch never surfaced in CI.
  • Contributing context: The original PR (#54832, merged) assumed $search worked because it does on some other Graph collections under app-only.

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
  • Target test or file: extensions/msteams/src/graph-messages.search.test.ts
  • Scenario the test should lock in: The outgoing path must never contain $search, regardless of query value. New test does not use Graph $search (unsupported under Application permissions) asserts this directly.
  • Why this is the smallest reliable guardrail: The failure mode is a URL-shape contract with Graph. Locking the path-building logic at the unit seam catches any regression to $search without needing a live Graph call.
  • Existing test that already covers this (if any): None — prior tests only verified happy-path response parsing.

User-visible / Behavior Changes

  • msteams:search now works on app-only (bot) credentials instead of returning Graph 400.
  • Search range is now the most recent N messages (clamped 50–200 depending on requested limit) rather than the full conversation archive. This is a reduction in coverage for the (previously broken) Delegated-auth case, but the action was not functional under app-only at all before.
  • Local filter treats message bodies as HTML by default (stripping tags before matching), and only skips the strip for contentType: "text".

Diagram

Before (broken for app-only):
search(query="hi") -> GET /chats/{id}/messages?$search="hi"  -> HTTP 400: Search not supported

After:
search(query="hi") -> GET /chats/{id}/messages?$top=50 (+ optional $filter)
                   -> stripHtml(body) when contentType=html
                   -> local includes("hi") -> return up to limit

Security Impact (required)

  • New permissions/capabilities? No — uses the same Graph scopes already required for the action.
  • Secrets/tokens handling changed? No
  • New/changed network calls? No new endpoints. Same /chats/{id}/messages and /teams/*/channels/*/messages endpoints, different query string.
  • Command/tool execution surface changed? No
  • Data access scope changed? No — identical data returned, just filtered locally instead of server-side.

Repro + Verification

Environment

  • OS: macOS 24.5.0 (Darwin)
  • Runtime/container: Node v25.9.0, pnpm
  • Model/provider: N/A (API-path fix, no model interaction)
  • Integration/channel: msteams bundled plugin, Application permissions (app-only bot token)
  • Relevant config: N/A

Steps

  1. Configure an msteams bot with app-only (Application-permission) credentials.
  2. Send messages to a chat where the bot is a member.
  3. Invoke the msteams:search action against that chat with any query.

Expected

  • Matching messages returned.

Actual (before this PR)

  • Graph returned HTTP 400: Search is not supported on this organization regardless of query.

Actual (after this PR)

  • Matching messages returned. Reproduced via unit tests covering $search removal, HTML stripping, $filter pushdown for sender, pagination window sizing, and user: target resolution.

Evidence

  • Failing test/log before + passing after: extensions/msteams/src/graph-messages.search.test.ts now has 17 assertions locking the new contract; one specifically asserts the path never contains $search.
  • Targeted run: pnpm test extensions/msteams/src/graph-messages.search.test.ts → 17/17 passing.
  • Full extension lane: pnpm test extensions/msteams → 874/874 passing (68 files).
  • Typecheck: pnpm tsgo:prod and pnpm check:test-types both green.
  • Lint: pnpm lint:extensions → 0 warnings, 0 errors.
  • Format: pnpm format:check extensions/msteams/src/graph-messages*.ts → clean.

Human Verification (required)

  • Verified scenarios:
    • $search is absent from the outgoing query string (asserted by unit test)
    • Case-insensitive local filtering works
    • HTML-tagged bodies are stripped before matching (query <b> does not match <b>world</b>)
    • @mention display names are preserved for matching
    • contentType: "text" bodies pass through unstripped
    • user:<aadId> targets resolve through the conversation store
    • Sender $filter escaping handles single quotes (e.g., O'BrienO''Brien)
  • Edge cases checked:
    • Empty query (after quote strip) returns the full list window
    • Missing contentType defaults to HTML-safe (strip) rather than raw
    • Channel target (teamId/channelId) routes through channel path
    • Result cap honors the effective limit after local filtering
  • What I did NOT verify: Live Graph API round-trip against a real tenant (no test Graph tenant available). The Graph-behavior claim ($search blocked under Application permissions) matches Microsoft's documented constraint referenced by the issue reporter and the Microsoft Graph known issue.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — same action shape, same result type, same caller contract.
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: Search now only covers the most recent 50–200 messages instead of the full archive.
    • Mitigation: Documented in the JSDoc on searchMessagesMSTeams. Full-archive search requires Delegated auth and is a separate follow-up. The prior behavior was fully broken under app-only, so this is net-positive.
  • Risk: HTML-strip default broadens — any non-text contentType now goes through stripHtmlFromTeamsMessage.
    • Mitigation: The strip function is regex-based and a no-op on non-tagged content. A dedicated unit test asserts contentType: "text" bodies are NOT stripped.

AI/Vibe-Coded PR Disclosure

  • Marked as AI-assisted — built with Claude Code.
  • Degree of testing: Fully tested locally (unit + typecheck + lint + format) but not verified against a live Graph tenant.
  • Confirm I understand what the code does: Yes.
  • Bot review conversations will be addressed or replied to.

CC msteams maintainers per CONTRIBUTING: @onutc @osolmaz

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/msteams/src/graph-messages.search.test.ts (modified, +238/-35)
  • extensions/msteams/src/graph-messages.ts (modified, +66/-13)

Code Example

Verified empirically against Microsoft Graph with a fresh app-only access token (client_credentials flow, ChannelMessage.Read.All granted to the app):

Endpoint                                                     Method  Result
/chats/{id}/messages?$search="foo"&$top=5                    GET     400"$search not allowed"
/chats/{id}/messages?$top=50 (no search)                     GET     200 — works
/search/query with entityTypes: ["chatMessage"]              POST    403"Application permission is only supported for: site, list, listItem, drive and driveItem"

Microsoft Graph does not support full-text search of chat messages with Application permissions — by any endpoint. This is a documented Microsoft platform restriction:
https://learn.microsoft.com/en-us/graph/search-concept-chatmessage

Current code in channel.runtime-*.jssearchMessagesMSTeams():

  const parts = [`$search=${encodeURIComponent(`"${sanitizedQuery}"`)}`];
  parts.push(`$top=${top}`);
  // ...fetches /chats/{id}/messages?$search=... → always fails with 400
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

The msteams:search tool action fails 100% of the time with Graph API HTTP 400 because it uses the $search query parameter which is not supported on /chats/{id}/messages with Application permissions.

Steps to reproduce

  1. Configure msteams plugin with a bot registered with Application (client-credentials) authentication.
  2. Grant Graph permissions including ChannelMessage.Read.All (Application).
  3. Invoke the search action via any agent, or manually trigger searchMessagesMSTeams with any query string.
  4. Observe 400 response from Microsoft Graph in the gateway log.

Expected behavior

The msteams:search action (added in #40865 / #54832) should return matching chat messages for the given query, consistent with the read action's result shape.

Actual behavior

Every request fails with HTTP 400:

[tools] message failed: Graph /chats/19%3A...%40thread.tacv2/messages?$search=%22foo%22&$top=5 failed (400): {"error":{"code":"","message":"The query specified in the URI is not valid. Query option 'Search' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings."}}

The searchMessagesMSTeams() function in channel.runtime-*.js constructs a $search query that Microsoft Graph rejects for /chats/{id}/messages when using Application permissions.

OpenClaw version

2026.4.15 (verified bug persists in 2026.4.21 — searchMessagesMSTeams function is byte-identical between both versions)

Operating system

macOS 15.4

Install method

npm global

Model

anthropic/claude-opus-4.6

Provider / routing chain

openclaw -> anthropic

Additional provider/model setup details

Not relevant — the bug is in the Graph API call path, not the LLM path. The msteams plugin is configured with Application permission flow via client_credentials against an Azure app (tenant: single-tenant).

Logs, screenshots, and evidence

Verified empirically against Microsoft Graph with a fresh app-only access token (client_credentials flow, ChannelMessage.Read.All granted to the app):

Endpoint                                                     Method  Result
/chats/{id}/messages?$search="foo"&$top=5                    GET     400"$search not allowed"
/chats/{id}/messages?$top=50 (no search)                     GET     200 — works
/search/query with entityTypes: ["chatMessage"]              POST    403"Application permission is only supported for: site, list, listItem, drive and driveItem"

Microsoft Graph does not support full-text search of chat messages with Application permissions — by any endpoint. This is a documented Microsoft platform restriction:
https://learn.microsoft.com/en-us/graph/search-concept-chatmessage

Current code in channel.runtime-*.js — searchMessagesMSTeams():

  const parts = [`$search=${encodeURIComponent(`"${sanitizedQuery}"`)}`];
  parts.push(`$top=${top}`);
  // ...fetches /chats/{id}/messages?$search=... → always fails with 400

Impact and severity

Affected: every OpenClaw deployment using msteams with Application permission flow. Severity: High — the msteams:search tool is unusable; any agent that invokes it receives an error instead of search results. Frequency: 100% reproducible — every invocation fails. Consequence: Agents cannot perform message search in MS Teams. Workflows depending on chat-history search are blocked, and error output is surfaced to users as a tool failure.

Additional information

Proposed fix: replace $search with list + local filter (only app-only-compatible path).

const listWindow = Math.min(200, Math.max(top * 10, 50)); const parts = [$top=${listWindow}]; if (params.from) parts.push($filter=${encodeURIComponent(...)});

const raw = await fetchGraphJson({ token, path: ${basePath}/messages?${parts.join("&")} });

const sanitizedQuery = params.query.replace(/"/g, "").toLowerCase(); const filtered = (raw.value ?? []) .filter((msg) => (msg.body?.content ?? "").toLowerCase().includes(sanitizedQuery)) .slice(0, top);

return { messages: filtered.map(/* same shape as before */) };

Trade-off: search bounded by listWindow (recent messages only). For full-archive search, Delegated auth would be required — separate architectural decision.

Happy to submit a PR if this direction is acceptable.

extent analysis

TL;DR

Replace the $search query parameter with a list and local filter to make the msteams:search tool compatible with Application permissions.

Guidance

  • The current implementation of searchMessagesMSTeams() uses the $search query parameter, which is not supported by Microsoft Graph for /chats/{id}/messages with Application permissions.
  • To fix this, modify the searchMessagesMSTeams() function to use a list and local filter instead of the $search query parameter.
  • Implement a local filter to search for messages containing the query string, as shown in the proposed fix.
  • Note that this approach has a trade-off: search results will be bounded by the listWindow variable, which limits the number of recent messages that can be searched.

Example

const listWindow = Math.min(200, Math.max(top * 10, 50));
const parts = [`$top=${listWindow}`];
if (params.from) parts.push(`$filter=${encodeURIComponent(...)}`);

const raw = await fetchGraphJson({ token, path: `${basePath}/messages?${parts.join("&")}` });

const sanitizedQuery = params.query.replace(/"/g, "").toLowerCase();
const filtered = (raw.value ?? [])
  .filter((msg) => (msg.body?.content ?? "").toLowerCase().includes(sanitizedQuery))
  .slice(0, top);

return { messages: filtered.map(/* same shape as before */) };

Notes

  • This fix only works for recent messages and does not support full-archive search, which would require Delegated authentication.
  • The proposed fix has been verified to work with Microsoft Graph and should be submitted as a PR for review.

Recommendation

Apply the proposed workaround by replacing the $search query parameter with a list and local filter, as shown in the example code. This fix makes the msteams:search tool compatible with Application permissions, although it has limitations on search results.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

The msteams:search action (added in #40865 / #54832) should return matching chat messages for the given query, consistent with the read action's result shape.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: msteams search action fails — $search not supported on /chats/messages with Application permissions [1 pull requests, 1 participants]