Skip to content

feat(msteams): add native plugin interactivity parity#55828

Open
Evizero wants to merge 5 commits intomainfrom
teams-parity-minimal
Open

feat(msteams): add native plugin interactivity parity#55828
Evizero wants to merge 5 commits intomainfrom
teams-parity-minimal

Conversation

@Evizero
Copy link
Copy Markdown
Member

@Evizero Evizero commented Mar 27, 2026

Summary

DO NOT MERGE YET -- for review

  • Problem: OpenClaw's Teams channel could send messages/cards, but the native plugin interactivity path still stopped short of Teams. The Codex app-server Teams work therefore had to rely on a command bridge instead of the same core plugin/runtime flow used by first-class channels.
  • Why it matters: I am working on this so Teams support for the Codex app-server path is grounded in real OpenClaw channel support, not a partial workaround. Without these core changes, Teams actions, binding flows, message edits, and channel-scoped Graph operations do not have the same native foundation as Discord/Telegram-style plugin integrations.
  • What changed: this PR adds native Teams plugin interactive dispatch in src/plugins/interactive.ts and src/plugins/interactive-dispatch-adapters.ts, adds the Teams runtime surface in src/plugins/runtime/runtime-msteams.ts, src/plugins/runtime/runtime-msteams-typing.ts, and src/plugins/runtime/runtime-msteams-ops.runtime.ts, wires Teams invoke handling through extensions/msteams/src/plugin-interactions.ts and extensions/msteams/src/monitor-handler.ts, extends command/target normalization in src/plugins/commands.ts and src/channels/plugins/target-parsing.ts, and fixes Teams Graph/edit/allowlist correctness in extensions/msteams/src/send.ts, extensions/msteams/src/graph.ts, extensions/msteams/src/graph-messages.ts, extensions/msteams/src/graph-thread.ts, extensions/msteams/src/resolve-allowlist.ts, and extensions/msteams/src/policy.ts.
  • What did NOT change (scope boundary): this does not try to fix unrelated latest-main agent/skills TypeScript failures, and it does not claim live-tenant validation for every Teams topology (for example shared-channel behavior still needs real-environment confirmation outside this PR).

Change Type

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

Scope

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

Linked Issue/PR

  • Closes: N/A
  • Related: cross-repo Teams parity work in openclaw-codex-app-server
  • This PR fixes a bug or regression

Root Cause / Regression History

  • Root cause: Teams support in core had been built around message/card sending and text-command handling, but not the native plugin interactive dispatch/runtime path. That left Codex app-server Teams support depending on a hidden command bridge instead of the same first-class interactive surface exposed to other channels.
  • Missing detection / guardrail: there was no Teams-native seam/integration coverage for plugin invoke handling, source-message edits/clears, Graph channel id translation, or fail-closed allowlist resolution.
  • Prior context (git blame, prior PR, issue, or refactor if known): the current code shape reflected the earlier minimal Teams MVP, where src/plugins/interactive.ts and runtime channel exposure were intentionally limited and Teams only had the smaller send-oriented surface.
  • Why this regressed now: the cross-repo Codex Teams work pushed beyond that MVP and exposed the remaining OpenClaw core gaps.
  • If unknown, what was ruled out: N/A

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • extensions/msteams/src/monitor-handler.plugin-interactions.test.ts
    • extensions/msteams/src/graph-messages.test.ts
    • extensions/msteams/src/send.test.ts
    • extensions/msteams/src/resolve-allowlist.test.ts
    • extensions/msteams/src/policy.test.ts
    • src/plugins/interactive.test.ts
    • src/plugins/runtime/index.test.ts
    • src/plugins/commands.test.ts
  • Scenario the test should lock in: Teams invoke payloads dispatch through the native plugin interactive handler, reply/follow-up/edit/clear flows update the expected Teams message/card surface, command/binding targets normalize consistently, Graph channel operations resolve the correct team/channel ids, and allowlist failures fail closed instead of widening access.
  • Why this is the smallest reliable guardrail: the failures crossed the Teams adapter, plugin interactive core, and Graph-backed Teams operations; narrower unit tests would miss the integration seams that were actually broken.
  • Existing test that already covers this (if any): the files above now cover the main Teams seams.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Teams plugin actions now go through native OpenClaw plugin interactivity instead of relying on a command-only bridge.
  • Teams plugin replies/follow-ups/edit-source/clear-actions now preserve better source-message behavior for Adaptive Card flows.
  • Teams plugin command binding and explicit target parsing now normalize teams: / msteams: conversation targets consistently.
  • Teams channel-scoped Graph operations use the resolved Graph team id path more safely.
  • Teams allowlist resolution now fails closed when primary-channel lookup cannot safely resolve a usable runtime key.

Diagram

Before:
Teams Action.Submit -> Teams-specific command bridge -> plugin workaround -> partial updates

After:
Teams Action.Submit -> extensions/msteams/src/plugin-interactions.ts
                  -> src/plugins/interactive.ts
                  -> plugin handler/runtime.channel.msteams
                  -> native reply/edit/typing/channel actions

Security Impact

  • New permissions/capabilities? (Yes/No) Yes
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) Yes
  • Command/tool execution surface changed? (Yes/No) Yes
  • Data access scope changed? (Yes/No) Yes
  • If any Yes, explain risk + mitigation:
    • This PR gives plugins a new trusted runtime.channel.msteams surface for Teams-native operations and adds Teams-native interactive dispatch. The risk is widening what a plugin can do in Teams or routing to the wrong target. Mitigations in this PR are explicit runtime typing in src/plugins/runtime/types-channel.ts, targeted runtime wiring instead of broad generic exposure, conversation-scoped interactive handling in extensions/msteams/src/plugin-interactions.ts, Graph team-id resolution fixes in extensions/msteams/src/graph-thread.ts / extensions/msteams/src/graph.ts, and fail-closed allowlist behavior in extensions/msteams/src/resolve-allowlist.ts / extensions/msteams/src/policy.ts.

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: local pnpm workspace checkout
  • Model/provider: N/A
  • Integration/channel (if any): Microsoft Teams (extensions/msteams)
  • Relevant config (redacted): local dev config only; no secrets included in this PR

Steps

  1. Enable the Teams channel and a plugin that uses OpenClaw interactive handlers/runtime operations.
  2. Trigger a Teams plugin interaction that needs source-message edits, follow-ups, binding, or Teams-native runtime actions.
  3. Exercise channel-scoped Teams paths that resolve Graph ids and allowlist-target normalization.

Expected

  • Teams interactive actions route through the native plugin handler path.
  • Teams runtime operations are available through runtime.channel.msteams.
  • Teams message/card edit flows keep the intended source-card behavior.
  • Teams channel Graph operations resolve the correct team/channel path.
  • Unsafe Teams allowlist fallbacks do not widen access.

Actual

  • This branch implements those paths in code and verifies them with the Teams-focused tests listed below.
  • After rebasing onto latest main, repo-wide pnpm check is blocked by unrelated src/agents/** TypeScript failures already present on top of main; those are out of scope for this PR.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Concrete issues fixed in this branch and covered by passing tests/review loops:

  • Teams plugin interactions could not run through the native OpenClaw interactive dispatch path.
  • Teams source-message update paths could clear or duplicate replies incorrectly.
  • Teams channel Graph operations could use the wrong team identifier for channel-scoped requests.
  • Teams allowlist resolution briefly risked widening access when primary-channel lookup failed; this branch keeps that path fail-closed.

Evidence run locally during this work:

  • pnpm test -- extensions/msteams/src/graph-messages.test.ts extensions/msteams/src/monitor-handler.plugin-interactions.test.ts extensions/msteams/src/resolve-allowlist.test.ts extensions/msteams/src/policy.test.ts
  • Earlier Teams-scoped validation on this branch also included:
    • extensions/msteams/src/send.test.ts
    • src/plugins/interactive.test.ts
    • src/plugins/runtime/index.test.ts
    • src/plugins/commands.test.ts
  • Before the rebase to latest main, this branch also passed:
    • pnpm check
    • pnpm build
    • pnpm plugin-sdk:api:check

Human Verification

NOT YET!

  • Verified scenarios:
    • reviewed the Teams interactive dispatch flow from extensions/msteams/src/plugin-interactions.ts into src/plugins/interactive.ts
    • verified the new Teams runtime surface and SDK baseline updates
    • verified Teams target parsing / command binding normalization
    • verified Teams Graph id resolution and allowlist behavior through targeted tests and review-fix loops
    • rebased the branch onto latest origin/main and resolved the resulting conflicts
  • Edge cases checked:
    • Teams source-message edit vs clear-card behavior
    • Teams interaction fallthrough when a handler declines handling
    • Graph team-id recovery / paging behavior
    • fail-closed allowlist resolution when primary-channel lookup is unavailable
  • What you did not verify:
    • no live Microsoft Teams tenant verification in this PR
    • no shared/private-channel live validation beyond code/test coverage
    • no attempt to fix unrelated latest-main src/agents/** failures

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/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Teams interactive/message-edit behavior can differ from Telegram/Discord because Adaptive Cards and Teams invoke semantics are different.
    • Mitigation: keep Teams-specific behavior isolated under extensions/msteams/**, cover the adapter seams with tests, and preserve explanatory comments in the non-obvious parsing/Graph-resolution paths.
  • Risk: Graph team/channel id translation could still behave differently in real tenants than in mocked tests.
    • Mitigation: the code now handles Graph-usable ids directly, pages through group lookup fallback, and fails closed when it cannot safely resolve a usable target.
  • Risk: exposing Teams runtime actions to plugins could widen the trusted plugin surface.
    • Mitigation: the surface is explicit and typed in the runtime channel layer rather than being an unbounded Teams SDK export.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: msteams Channel integration: msteams size: XL maintainer Maintainer-authored PR labels Mar 27, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 27, 2026

Greptile Summary

This PR adds native Microsoft Teams plugin interactivity to OpenClaw, bringing Teams to parity with Discord, Telegram, and Slack for the core plugin interactive dispatch path. The changes wire invoke/message.submitAction payloads from the Teams Bot Framework adapter through the new handleMSTeamsPluginInteraction entry point into dispatchPluginInteractiveHandler, expose a typed runtime.channel.msteams surface to plugins (send, adaptive-card, typing lease, edit/delete/edit-channel), fix Graph team-id resolution so it correctly maps Bot Framework runtime team keys to Graph group GUIDs, and tighten allowlist resolution to fail closed instead of widening access on API failure.

Key points:

  • extensions/msteams/src/plugin-interactions.ts — new entry point for Teams invoke handling; authorization, conversation-ID normalization, and respond-callback construction look correct.
  • extensions/msteams/src/graph-thread.ts — adds a paged group-scan fallback for Bot Framework team-key → Graph group-id resolution. Contains a URL-construction bug: graphPathFromNextLink returns url.pathname + url.search, but Microsoft Graph @odata.nextLink paths include the API version segment (e.g. /v1.0/groups?...). When fetchGraphJson prepends GRAPH_ROOT, the URL doubles to .../v1.0/v1.0/groups, causing a 404 on every paginated request. This silently breaks team-id resolution for any tenant with more than 999 provisioned teams.
  • extensions/msteams/src/resolve-allowlist.ts — intentional behavioral change: primary-channel lookup failure now returns resolved: false instead of falling back to the Graph GUID; this is the described fail-closed fix.
  • src/plugins/interactive.ts — adds msteams as a first-class channel alongside telegram, discord, slack; dispatch routing and overload signatures are correct.
  • looksLikeGraphTeamId is defined identically in three separate files (graph-thread.ts, graph.ts, graph-messages.ts); worth consolidating.

Confidence Score: 4/5

Safe to merge for the common case; one P1 pagination bug in the team-id fallback path affects large tenants (more than 999 teams) where the Bot Framework team key is not directly usable as a Graph GUID.

The bulk of the PR (interactive dispatch wiring, runtime surface, allowlist hardening, Graph message operations, send/edit/typing primitives) is well-structured and backed by targeted tests. The single P1 finding — graphPathFromNextLink doubling the /v1.0 version prefix on paginated @odata.nextLink URLs — only affects the deep fallback path in resolveTeamGroupId, which is only reached when the direct /teams/{id} call fails and the raw ID does not look like a GUID. Most tenants will never reach this code; large enterprises could. The fix is a one-line regex in graphPathFromNextLink. All other findings are P2.

extensions/msteams/src/graph-thread.ts — graphPathFromNextLink pagination URL construction.

Important Files Changed

Filename Overview
extensions/msteams/src/graph-thread.ts Adds paged group-scan fallback for Bot Framework team-key → Graph group-id resolution; contains a URL-construction bug that doubles the API version prefix on pagination, and a per-team primaryChannel fan-out that can be expensive in large tenants.
extensions/msteams/src/plugin-interactions.ts New file wiring Teams invoke payloads through native OpenClaw plugin interactive dispatch; authorization, conversation-ID extraction, and respond-callback wiring look correct.
extensions/msteams/src/resolve-allowlist.ts Switches from find-General-channel to primaryChannel API for team-key resolution and adds fail-closed behavior when the lookup fails; behavioral change is intentional and covered by tests.
extensions/msteams/src/graph-messages.ts Rewrites conversation-target resolution to a typed discriminated union, adding proper team-group-id resolution for channel paths; logic is sound and test coverage updated.
extensions/msteams/src/graph.ts Adds PATCH support, getPrimaryChannelForTeam, and editChannelMSTeams; channel rename correctly resolves the Graph team id before issuing the PATCH.
extensions/msteams/src/send.ts Makes edit-message text optional and adds card support; adds sendTypingMSTeams with correct channel-type guard.
src/plugins/interactive.ts Adds msteams as a first-class interactive channel alongside telegram/discord/slack; dispatch routing and overload signatures look correct.
src/plugins/runtime/runtime-msteams.ts New file exposing the Teams runtime surface to plugins via lazy-loaded operations; typing.start correctly validates cfg presence before creating a lease.
extensions/msteams/src/sdk.ts Uses string-concatenated module IDs to prevent bundler static analysis from eagerly resolving optional Teams SDK deps; intentional lazy-load pattern.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/graph-thread.ts
Line: 27-33

Comment:
**Pagination URL doubles the API version prefix**

`graphPathFromNextLink` returns `url.pathname + url.search`. Microsoft Graph `@odata.nextLink` values embed the API version in the path (the pathname is `/v1.0/groups`, not `/groups`). When `fetchGraphJson` prepends `GRAPH_ROOT` (`https://graph.microsoft.com/v1.0`), the resulting URL becomes `https://graph.microsoft.com/v1.0/v1.0/groups?...` — a 404. Team-ID resolution silently fails to paginate in any tenant with more than 999 teams.

Strip the API-version segment from the pathname before returning it:
```typescript
function graphPathFromNextLink(nextLink: string): string | null {
  try {
    const url = new URL(nextLink);
    // nextLink includes the API version (e.g. /v1.0/groups).
    // Strip it so the caller can prepend GRAPH_ROOT without doubling the prefix.
    const pathWithoutVersion = url.pathname.replace(/^\/(?:v\d+(?:\.\d+)*|beta)\b/, "");
    return pathWithoutVersion + url.search;
  } catch {
    return null;
  }
}
```

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

---

This is a comment left during a code review.
Path: extensions/msteams/src/graph-thread.ts
Line: 86-120

Comment:
**O(N) per-team primary-channel fan-out in the fallback path**

The new fallback enumerates all provisioned teams with `$top=999`, then calls `/teams/{id}/primaryChannel` individually for each candidate until one matches. In a large tenant this can be hundreds of sequential Graph calls and will hit rate limits before succeeding. Consider batching with `$expand=primaryChannel` on the groups query so the primary channel id is returned alongside the group id in a single response per page, reducing the fan-out to one call per page instead of one call per team.

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

---

This is a comment left during a code review.
Path: extensions/msteams/src/graph-thread.ts
Line: 19-21

Comment:
**`looksLikeGraphTeamId` is duplicated across three files**

The same helper (`/^[0-9a-fA-F-]{16,}$/.test(value.trim())`) is now defined identically in `graph-thread.ts` (line 19), `graph.ts`, and `graph-messages.ts`. Extracting it to a shared internal utility (e.g., `graph-id-utils.ts`) would remove the drift risk and make the regex easier to update or tighten in one place.

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

Reviews (1): Last reviewed commit: "Teams: add native plugin interactivity p..." | Re-trigger Greptile

Comment thread extensions/msteams/src/graph-thread.ts
Comment thread extensions/msteams/src/graph-thread.ts
Comment thread extensions/msteams/src/graph-thread.ts Outdated
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: df84e99e95

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/msteams/src/graph-thread.ts Outdated
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: 08c6cb5eb6

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/channels/plugins/target-parsing.ts Outdated
@Evizero Evizero force-pushed the teams-parity-minimal branch from 08c6cb5 to 25463ab Compare March 28, 2026 11:21
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: 25463ab775

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/channels/plugins/target-parsing.ts Outdated
@Evizero Evizero force-pushed the teams-parity-minimal branch from 25463ab to e0ca60c Compare March 28, 2026 11:40
@Evizero Evizero requested a review from a team as a code owner March 28, 2026 11:40
@openclaw-barnacle openclaw-barnacle Bot added the agents Agent runtime and tooling label Mar 28, 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: e0ca60cfae

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/msteams/src/plugin-interactions.ts
@Evizero Evizero force-pushed the teams-parity-minimal branch from e0ca60c to d94e1ee Compare March 28, 2026 11:49
@openclaw-barnacle openclaw-barnacle Bot removed the agents Agent runtime and tooling label Mar 28, 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: 0e0d02cc10

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +140 to +143
if (!teamId || !channelId) {
throw new Error(
`Conversation ${cleaned} is a Teams channel but missing teamId or graphChannelId in the conversation store.`,
);
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 Add fallback for channel records missing graphChannelId

This branch makes channel Graph operations hard-fail when a stored channel conversation lacks graphChannelId, but that field is newly introduced here and older msteams-conversations.json entries will not have it. After upgrade, existing bound channel conversations can throw missing teamId or graphChannelId for getMessage/pin/react/search until a new inbound message refreshes the store, which is a regression for already-persisted state. Please add a compatibility fallback (or migration/backfill) instead of immediately throwing.

Useful? React with 👍 / 👎.

Comment on lines 287 to 289
teamId,
graphChannelId: activity.channelData?.channel?.id,
channelId: activity.channelId,
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 Preserve graphChannelId when channel metadata is absent

The upsert payload always includes graphChannelId: activity.channelData?.channel?.id, so activities without channelData.channel write undefined for this field. Because the store merge path spreads incoming fields and does not preserve graphChannelId, this can erase previously known channel metadata for that conversation, and later channel Graph calls will fail on the new required-metadata check. Only write graphChannelId when it is present.

Useful? React with 👍 / 👎.

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 maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant