@openclaw/msteams 2026.3.2 → 2026.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.7
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.3.3
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
3
15
  ## 2026.3.2
4
16
 
5
17
  ### Changes
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/msteams";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/msteams";
3
3
  import { msteamsPlugin } from "./src/channel.js";
4
4
  import { setMSTeamsRuntime } from "./src/runtime.js";
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/msteams",
3
- "version": "2026.3.2",
3
+ "version": "2026.3.7",
4
4
  "description": "OpenClaw Microsoft Teams channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,4 +1,4 @@
1
- import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk";
1
+ import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/msteams";
2
2
  import { getMSTeamsRuntime } from "../runtime.js";
3
3
  import { downloadMSTeamsAttachments } from "./download.js";
4
4
  import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js";
@@ -1,4 +1,4 @@
1
- import { buildMediaPayload } from "openclaw/plugin-sdk";
1
+ import { buildMediaPayload } from "openclaw/plugin-sdk/msteams";
2
2
 
3
3
  export function buildMSTeamsMediaPayload(
4
4
  mediaList: Array<{ path: string; contentType?: string }>,
@@ -1,4 +1,4 @@
1
- import type { SsrFPolicy } from "openclaw/plugin-sdk";
1
+ import type { SsrFPolicy } from "openclaw/plugin-sdk/msteams";
2
2
  import { getMSTeamsRuntime } from "../runtime.js";
3
3
  import { inferPlaceholder } from "./shared.js";
4
4
  import type { MSTeamsInboundMedia } from "./types.js";
@@ -4,8 +4,8 @@ import {
4
4
  isHttpsUrlAllowedByHostnameSuffixAllowlist,
5
5
  isPrivateIpAddress,
6
6
  normalizeHostnameSuffixAllowlist,
7
- } from "openclaw/plugin-sdk";
8
- import type { SsrFPolicy } from "openclaw/plugin-sdk";
7
+ } from "openclaw/plugin-sdk/msteams";
8
+ import type { SsrFPolicy } from "openclaw/plugin-sdk/msteams";
9
9
  import type { MSTeamsAttachmentLike } from "./types.js";
10
10
 
11
11
  type InlineImageCandidate =
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime, SsrFPolicy } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime, SsrFPolicy } from "openclaw/plugin-sdk/msteams";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
4
4
  import {
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { msteamsPlugin } from "./channel.js";
4
4
 
package/src/channel.ts CHANGED
@@ -1,14 +1,21 @@
1
- import type { ChannelMessageActionName, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
2
1
  import {
3
- buildBaseChannelStatusSummary,
2
+ collectAllowlistProviderRestrictSendersWarnings,
3
+ formatAllowFromLowercase,
4
+ } from "openclaw/plugin-sdk/compat";
5
+ import type {
6
+ ChannelMessageActionName,
7
+ ChannelPlugin,
8
+ OpenClawConfig,
9
+ } from "openclaw/plugin-sdk/msteams";
10
+ import {
11
+ buildProbeChannelStatusSummary,
12
+ buildRuntimeAccountStatusSnapshot,
4
13
  buildChannelConfigSchema,
5
14
  createDefaultChannelRuntimeState,
6
15
  DEFAULT_ACCOUNT_ID,
7
16
  MSTeamsConfigSchema,
8
17
  PAIRING_APPROVED_MESSAGE,
9
- resolveAllowlistProviderRuntimeGroupPolicy,
10
- resolveDefaultGroupPolicy,
11
- } from "openclaw/plugin-sdk";
18
+ } from "openclaw/plugin-sdk/msteams";
12
19
  import { listMSTeamsDirectoryGroupsLive, listMSTeamsDirectoryPeersLive } from "./directory-live.js";
13
20
  import { msteamsOnboardingAdapter } from "./onboarding.js";
14
21
  import { msteamsOutbound } from "./outbound.js";
@@ -120,27 +127,20 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
120
127
  configured: account.configured,
121
128
  }),
122
129
  resolveAllowFrom: ({ cfg }) => cfg.channels?.msteams?.allowFrom ?? [],
123
- formatAllowFrom: ({ allowFrom }) =>
124
- allowFrom
125
- .map((entry) => String(entry).trim())
126
- .filter(Boolean)
127
- .map((entry) => entry.toLowerCase()),
130
+ formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }),
128
131
  resolveDefaultTo: ({ cfg }) => cfg.channels?.msteams?.defaultTo?.trim() || undefined,
129
132
  },
130
133
  security: {
131
134
  collectWarnings: ({ cfg }) => {
132
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
133
- const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
135
+ return collectAllowlistProviderRestrictSendersWarnings({
136
+ cfg,
134
137
  providerConfigPresent: cfg.channels?.msteams !== undefined,
135
- groupPolicy: cfg.channels?.msteams?.groupPolicy,
136
- defaultGroupPolicy,
138
+ configuredGroupPolicy: cfg.channels?.msteams?.groupPolicy,
139
+ surface: "MS Teams groups",
140
+ openScope: "any member",
141
+ groupPolicyPath: "channels.msteams.groupPolicy",
142
+ groupAllowFromPath: "channels.msteams.groupAllowFrom",
137
143
  });
138
- if (groupPolicy !== "open") {
139
- return [];
140
- }
141
- return [
142
- `- MS Teams groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.msteams.groupPolicy="allowlist" + channels.msteams.groupAllowFrom to restrict senders.`,
143
- ];
144
144
  },
145
145
  },
146
146
  setup: {
@@ -246,11 +246,43 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
246
246
  name: undefined as string | undefined,
247
247
  note: undefined as string | undefined,
248
248
  }));
249
+ type ResolveTargetResultEntry = (typeof results)[number];
250
+ type PendingTargetEntry = { input: string; query: string; index: number };
249
251
 
250
252
  const stripPrefix = (value: string) => normalizeMSTeamsUserInput(value);
253
+ const markPendingLookupFailed = (pending: PendingTargetEntry[]) => {
254
+ pending.forEach(({ index }) => {
255
+ const entry = results[index];
256
+ if (entry) {
257
+ entry.note = "lookup failed";
258
+ }
259
+ });
260
+ };
261
+ const resolvePending = async <T>(
262
+ pending: PendingTargetEntry[],
263
+ resolveEntries: (entries: string[]) => Promise<T[]>,
264
+ applyResolvedEntry: (target: ResolveTargetResultEntry, entry: T) => void,
265
+ ) => {
266
+ if (pending.length === 0) {
267
+ return;
268
+ }
269
+ try {
270
+ const resolved = await resolveEntries(pending.map((entry) => entry.query));
271
+ resolved.forEach((entry, idx) => {
272
+ const target = results[pending[idx]?.index ?? -1];
273
+ if (!target) {
274
+ return;
275
+ }
276
+ applyResolvedEntry(target, entry);
277
+ });
278
+ } catch (err) {
279
+ runtime.error?.(`msteams resolve failed: ${String(err)}`);
280
+ markPendingLookupFailed(pending);
281
+ }
282
+ };
251
283
 
252
284
  if (kind === "user") {
253
- const pending: Array<{ input: string; query: string; index: number }> = [];
285
+ const pending: PendingTargetEntry[] = [];
254
286
  results.forEach((entry, index) => {
255
287
  const trimmed = entry.input.trim();
256
288
  if (!trimmed) {
@@ -266,37 +298,21 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
266
298
  pending.push({ input: entry.input, query: cleaned, index });
267
299
  });
268
300
 
269
- if (pending.length > 0) {
270
- try {
271
- const resolved = await resolveMSTeamsUserAllowlist({
272
- cfg,
273
- entries: pending.map((entry) => entry.query),
274
- });
275
- resolved.forEach((entry, idx) => {
276
- const target = results[pending[idx]?.index ?? -1];
277
- if (!target) {
278
- return;
279
- }
280
- target.resolved = entry.resolved;
281
- target.id = entry.id;
282
- target.name = entry.name;
283
- target.note = entry.note;
284
- });
285
- } catch (err) {
286
- runtime.error?.(`msteams resolve failed: ${String(err)}`);
287
- pending.forEach(({ index }) => {
288
- const entry = results[index];
289
- if (entry) {
290
- entry.note = "lookup failed";
291
- }
292
- });
293
- }
294
- }
301
+ await resolvePending(
302
+ pending,
303
+ (entries) => resolveMSTeamsUserAllowlist({ cfg, entries }),
304
+ (target, entry) => {
305
+ target.resolved = entry.resolved;
306
+ target.id = entry.id;
307
+ target.name = entry.name;
308
+ target.note = entry.note;
309
+ },
310
+ );
295
311
 
296
312
  return results;
297
313
  }
298
314
 
299
- const pending: Array<{ input: string; query: string; index: number }> = [];
315
+ const pending: PendingTargetEntry[] = [];
300
316
  results.forEach((entry, index) => {
301
317
  const trimmed = entry.input.trim();
302
318
  if (!trimmed) {
@@ -319,48 +335,32 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
319
335
  pending.push({ input: entry.input, query, index });
320
336
  });
321
337
 
322
- if (pending.length > 0) {
323
- try {
324
- const resolved = await resolveMSTeamsChannelAllowlist({
325
- cfg,
326
- entries: pending.map((entry) => entry.query),
327
- });
328
- resolved.forEach((entry, idx) => {
329
- const target = results[pending[idx]?.index ?? -1];
330
- if (!target) {
331
- return;
332
- }
333
- if (!entry.resolved || !entry.teamId) {
334
- target.resolved = false;
335
- target.note = entry.note;
336
- return;
337
- }
338
- target.resolved = true;
339
- if (entry.channelId) {
340
- target.id = `${entry.teamId}/${entry.channelId}`;
341
- target.name =
342
- entry.channelName && entry.teamName
343
- ? `${entry.teamName}/${entry.channelName}`
344
- : (entry.channelName ?? entry.teamName);
345
- } else {
346
- target.id = entry.teamId;
347
- target.name = entry.teamName;
348
- target.note = "team id";
349
- }
350
- if (entry.note) {
351
- target.note = entry.note;
352
- }
353
- });
354
- } catch (err) {
355
- runtime.error?.(`msteams resolve failed: ${String(err)}`);
356
- pending.forEach(({ index }) => {
357
- const entry = results[index];
358
- if (entry) {
359
- entry.note = "lookup failed";
360
- }
361
- });
362
- }
363
- }
338
+ await resolvePending(
339
+ pending,
340
+ (entries) => resolveMSTeamsChannelAllowlist({ cfg, entries }),
341
+ (target, entry) => {
342
+ if (!entry.resolved || !entry.teamId) {
343
+ target.resolved = false;
344
+ target.note = entry.note;
345
+ return;
346
+ }
347
+ target.resolved = true;
348
+ if (entry.channelId) {
349
+ target.id = `${entry.teamId}/${entry.channelId}`;
350
+ target.name =
351
+ entry.channelName && entry.teamName
352
+ ? `${entry.teamName}/${entry.channelName}`
353
+ : (entry.channelName ?? entry.teamName);
354
+ } else {
355
+ target.id = entry.teamId;
356
+ target.name = entry.teamName;
357
+ target.note = "team id";
358
+ }
359
+ if (entry.note) {
360
+ target.note = entry.note;
361
+ }
362
+ },
363
+ );
364
364
 
365
365
  return results;
366
366
  },
@@ -425,23 +425,17 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
425
425
  outbound: msteamsOutbound,
426
426
  status: {
427
427
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
428
- buildChannelSummary: ({ snapshot }) => ({
429
- ...buildBaseChannelStatusSummary(snapshot),
430
- port: snapshot.port ?? null,
431
- probe: snapshot.probe,
432
- lastProbeAt: snapshot.lastProbeAt ?? null,
433
- }),
428
+ buildChannelSummary: ({ snapshot }) =>
429
+ buildProbeChannelStatusSummary(snapshot, {
430
+ port: snapshot.port ?? null,
431
+ }),
434
432
  probeAccount: async ({ cfg }) => await probeMSTeams(cfg.channels?.msteams),
435
433
  buildAccountSnapshot: ({ account, runtime, probe }) => ({
436
434
  accountId: account.accountId,
437
435
  enabled: account.enabled,
438
436
  configured: account.configured,
439
- running: runtime?.running ?? false,
440
- lastStartAt: runtime?.lastStartAt ?? null,
441
- lastStopAt: runtime?.lastStopAt ?? null,
442
- lastError: runtime?.lastError ?? null,
437
+ ...buildRuntimeAccountStatusSnapshot({ runtime, probe }),
443
438
  port: runtime?.port ?? null,
444
- probe,
445
439
  }),
446
440
  },
447
441
  gateway: {
@@ -1,4 +1,4 @@
1
- import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
1
+ import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/msteams";
2
2
  import { searchGraphUsers } from "./graph-users.js";
3
3
  import {
4
4
  type GraphChannel,
package/src/file-lock.ts CHANGED
@@ -1 +1 @@
1
- export { withFileLock } from "openclaw/plugin-sdk";
1
+ export { withFileLock } from "openclaw/plugin-sdk/msteams";
package/src/graph.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MSTeamsConfig } from "openclaw/plugin-sdk";
1
+ import type { MSTeamsConfig } from "openclaw/plugin-sdk/msteams";
2
2
  import { GRAPH_ROOT } from "./attachments/shared.js";
3
3
  import { loadMSTeamsSdkWithAuth } from "./sdk.js";
4
4
  import { readAccessToken } from "./token-response.js";
@@ -8,7 +8,7 @@ import {
8
8
  extensionForMime,
9
9
  extractOriginalFilename,
10
10
  getFileExtension,
11
- } from "openclaw/plugin-sdk";
11
+ } from "openclaw/plugin-sdk/msteams";
12
12
 
13
13
  /**
14
14
  * Detect MIME type from URL extension or data URL.
@@ -1,7 +1,7 @@
1
1
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { SILENT_REPLY_TOKEN, type PluginRuntime } from "openclaw/plugin-sdk";
4
+ import { SILENT_REPLY_TOKEN, type PluginRuntime } from "openclaw/plugin-sdk/msteams";
5
5
  import { beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
7
7
  import type { StoredConversationReference } from "./conversation-store.js";
@@ -72,6 +72,17 @@ const createRecordedSendActivity = (
72
72
  };
73
73
  };
74
74
 
75
+ const REVOCATION_ERROR = "Cannot perform 'set' on a proxy that has been revoked";
76
+
77
+ const createFallbackAdapter = (proactiveSent: string[]): MSTeamsAdapter => ({
78
+ continueConversation: async (_appId, _reference, logic) => {
79
+ await logic({
80
+ sendActivity: createRecordedSendActivity(proactiveSent),
81
+ });
82
+ },
83
+ process: async () => {},
84
+ });
85
+
75
86
  describe("msteams messenger", () => {
76
87
  beforeEach(() => {
77
88
  setMSTeamsRuntime(runtimeStub);
@@ -297,18 +308,11 @@ describe("msteams messenger", () => {
297
308
 
298
309
  const ctx = {
299
310
  sendActivity: async () => {
300
- throw new TypeError("Cannot perform 'set' on a proxy that has been revoked");
311
+ throw new TypeError(REVOCATION_ERROR);
301
312
  },
302
313
  };
303
314
 
304
- const adapter: MSTeamsAdapter = {
305
- continueConversation: async (_appId, _reference, logic) => {
306
- await logic({
307
- sendActivity: createRecordedSendActivity(proactiveSent),
308
- });
309
- },
310
- process: async () => {},
311
- };
315
+ const adapter = createFallbackAdapter(proactiveSent);
312
316
 
313
317
  const ids = await sendMSTeamsMessages({
314
318
  replyStyle: "thread",
@@ -338,18 +342,11 @@ describe("msteams messenger", () => {
338
342
  threadSent.push(content);
339
343
  return { id: `id:${content}` };
340
344
  }
341
- throw new TypeError("Cannot perform 'set' on a proxy that has been revoked");
345
+ throw new TypeError(REVOCATION_ERROR);
342
346
  },
343
347
  };
344
348
 
345
- const adapter: MSTeamsAdapter = {
346
- continueConversation: async (_appId, _reference, logic) => {
347
- await logic({
348
- sendActivity: createRecordedSendActivity(proactiveSent),
349
- });
350
- },
351
- process: async () => {},
352
- };
349
+ const adapter = createFallbackAdapter(proactiveSent);
353
350
 
354
351
  const ids = await sendMSTeamsMessages({
355
352
  replyStyle: "thread",
package/src/messenger.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  type ReplyPayload,
8
8
  SILENT_REPLY_TOKEN,
9
9
  sleep,
10
- } from "openclaw/plugin-sdk";
10
+ } from "openclaw/plugin-sdk/msteams";
11
11
  import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
12
12
  import type { StoredConversationReference } from "./conversation-store.js";
13
13
  import { classifyMSTeamsSendError } from "./errors.js";
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
4
4
  import { setMSTeamsRuntime } from "../runtime.js";
@@ -2,20 +2,24 @@ import {
2
2
  DEFAULT_ACCOUNT_ID,
3
3
  buildPendingHistoryContextFromMap,
4
4
  clearHistoryEntriesIfEnabled,
5
+ dispatchReplyFromConfigWithSettledDispatcher,
5
6
  DEFAULT_GROUP_HISTORY_LIMIT,
6
7
  createScopedPairingAccess,
7
8
  logInboundDrop,
9
+ evaluateSenderGroupAccessForPolicy,
10
+ resolveSenderScopedGroupPolicy,
8
11
  recordPendingHistoryEntryIfEnabled,
9
12
  resolveControlCommandGate,
10
13
  resolveDefaultGroupPolicy,
11
14
  isDangerousNameMatchingEnabled,
12
15
  readStoreAllowFromForDmPolicy,
13
16
  resolveMentionGating,
17
+ resolveInboundSessionEnvelopeContext,
14
18
  formatAllowlistMatchMeta,
15
19
  resolveEffectiveAllowFromLists,
16
20
  resolveDmGroupAccessWithLists,
17
21
  type HistoryEntry,
18
- } from "openclaw/plugin-sdk";
22
+ } from "openclaw/plugin-sdk/msteams";
19
23
  import {
20
24
  buildMSTeamsAttachmentPlaceholder,
21
25
  buildMSTeamsMediaPayload,
@@ -172,12 +176,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
172
176
  conversationId,
173
177
  channelName,
174
178
  });
175
- const senderGroupPolicy =
176
- groupPolicy === "disabled"
177
- ? "disabled"
178
- : effectiveGroupAllowFrom.length > 0
179
- ? "allowlist"
180
- : "open";
179
+ const senderGroupPolicy = resolveSenderScopedGroupPolicy({
180
+ groupPolicy,
181
+ groupAllowFrom: effectiveGroupAllowFrom,
182
+ });
181
183
  const access = resolveDmGroupAccessWithLists({
182
184
  isGroup: !isDirectMessage,
183
185
  dmPolicy,
@@ -228,46 +230,57 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
228
230
  }
229
231
 
230
232
  if (!isDirectMessage && msteamsCfg) {
231
- if (groupPolicy === "disabled") {
232
- log.debug?.("dropping group message (groupPolicy: disabled)", {
233
+ if (channelGate.allowlistConfigured && !channelGate.allowed) {
234
+ log.debug?.("dropping group message (not in team/channel allowlist)", {
233
235
  conversationId,
236
+ teamKey: channelGate.teamKey ?? "none",
237
+ channelKey: channelGate.channelKey ?? "none",
238
+ channelMatchKey: channelGate.channelMatchKey ?? "none",
239
+ channelMatchSource: channelGate.channelMatchSource ?? "none",
234
240
  });
235
241
  return;
236
242
  }
237
-
238
- if (groupPolicy === "allowlist") {
239
- if (channelGate.allowlistConfigured && !channelGate.allowed) {
240
- log.debug?.("dropping group message (not in team/channel allowlist)", {
241
- conversationId,
242
- teamKey: channelGate.teamKey ?? "none",
243
- channelKey: channelGate.channelKey ?? "none",
244
- channelMatchKey: channelGate.channelMatchKey ?? "none",
245
- channelMatchSource: channelGate.channelMatchSource ?? "none",
246
- });
247
- return;
248
- }
249
- if (effectiveGroupAllowFrom.length === 0 && !channelGate.allowlistConfigured) {
250
- log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
251
- conversationId,
252
- });
253
- return;
254
- }
255
- if (effectiveGroupAllowFrom.length > 0 && access.decision !== "allow") {
256
- const allowMatch = resolveMSTeamsAllowlistMatch({
257
- allowFrom: effectiveGroupAllowFrom,
243
+ const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
244
+ groupPolicy,
245
+ groupAllowFrom:
246
+ effectiveGroupAllowFrom.length > 0 || !channelGate.allowlistConfigured
247
+ ? effectiveGroupAllowFrom
248
+ : ["*"],
249
+ senderId,
250
+ isSenderAllowed: (_senderId, allowFrom) =>
251
+ resolveMSTeamsAllowlistMatch({
252
+ allowFrom,
258
253
  senderId,
259
254
  senderName,
260
255
  allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
261
- });
262
- if (!allowMatch.allowed) {
263
- log.debug?.("dropping group message (not in groupAllowFrom)", {
264
- sender: senderId,
265
- label: senderName,
266
- allowlistMatch: formatAllowlistMatchMeta(allowMatch),
267
- });
268
- return;
269
- }
270
- }
256
+ }).allowed,
257
+ });
258
+
259
+ if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
260
+ log.debug?.("dropping group message (groupPolicy: disabled)", {
261
+ conversationId,
262
+ });
263
+ return;
264
+ }
265
+ if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
266
+ log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
267
+ conversationId,
268
+ });
269
+ return;
270
+ }
271
+ if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") {
272
+ const allowMatch = resolveMSTeamsAllowlistMatch({
273
+ allowFrom: effectiveGroupAllowFrom,
274
+ senderId,
275
+ senderName,
276
+ allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
277
+ });
278
+ log.debug?.("dropping group message (not in groupAllowFrom)", {
279
+ sender: senderId,
280
+ label: senderName,
281
+ allowlistMatch: formatAllowlistMatchMeta(allowMatch),
282
+ });
283
+ return;
271
284
  }
272
285
  }
273
286
 
@@ -451,12 +464,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
451
464
 
452
465
  const mediaPayload = buildMSTeamsMediaPayload(mediaList);
453
466
  const envelopeFrom = isDirectMessage ? senderName : conversationType;
454
- const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
467
+ const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
468
+ cfg,
455
469
  agentId: route.agentId,
456
- });
457
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
458
- const previousTimestamp = core.channel.session.readSessionUpdatedAt({
459
- storePath,
460
470
  sessionKey: route.sessionKey,
461
471
  });
462
472
  const body = core.channel.reply.formatAgentEnvelope({
@@ -559,18 +569,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
559
569
 
560
570
  log.info("dispatching to agent", { sessionKey: route.sessionKey });
561
571
  try {
562
- const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
572
+ const { queuedFinal, counts } = await dispatchReplyFromConfigWithSettledDispatcher({
573
+ cfg,
574
+ ctxPayload,
563
575
  dispatcher,
564
576
  onSettled: () => {
565
577
  markDispatchIdle();
566
578
  },
567
- run: () =>
568
- core.channel.reply.dispatchReplyFromConfig({
569
- ctx: ctxPayload,
570
- cfg,
571
- dispatcher,
572
- replyOptions,
573
- }),
579
+ replyOptions,
574
580
  });
575
581
 
576
582
  log.info("dispatch complete", { queuedFinal, counts });
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import type { MSTeamsConversationStore } from "./conversation-store.js";
4
4
  import type { MSTeamsAdapter } from "./messenger.js";
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
2
2
  import type { MSTeamsConversationStore } from "./conversation-store.js";
3
3
  import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
4
4
  import { normalizeMSTeamsConversationId } from "./inbound.js";