Skip to content

MSTeams: harden channel integration and readable focus labels#48659

Closed
hddevteam wants to merge 27 commits intoopenclaw:mainfrom
hddevteam:integration/oc-fullstack-msteams
Closed

MSTeams: harden channel integration and readable focus labels#48659
hddevteam wants to merge 27 commits intoopenclaw:mainfrom
hddevteam:integration/oc-fullstack-msteams

Conversation

@hddevteam
Copy link
Copy Markdown

Summary

  • Problem: MS Teams channel conversations still had rough edges around archive persistence, thread-default replies, JSON/media ingress handling, and DM follow-up context could only remember a raw recent channel target instead of readable team/channel labels.
  • Why it matters: these gaps made channel recovery more fragile and left DM follow-ups harder to understand when users wanted the assistant to keep working against the most recent channel target.
  • What changed: this branch adds channel archive persistence/cleanup and related tests, hardens monitor/media ingress paths, defaults channel replies to thread, persists recent DM channel focus in a sidecar, and resolves readable team/channel labels via conversation-tenant Graph lookups.
  • What did NOT change (scope boundary): this does not merge channel history into DM session memory and does not change the existing DM/channel isolation model.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • 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 #
  • Related #

User-visible / Behavior Changes

  • MS Teams channel archive state is persisted and cleaned up explicitly.
  • Channel replies default to the originating thread more reliably.
  • DM follow-ups can remember the most recent channel target without polluting the main DM session.
  • When Graph permissions are present in the conversation tenant, recent channel focus now shows readable team/channel labels instead of raw IDs.

Security Impact (required)

  • New permissions/capabilities? (Yes)
  • Secrets/tokens handling changed? (Yes)
  • New/changed network calls? (Yes)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (Yes)
  • If any Yes, explain risk + mitigation:
    • The readable-label resolution path now uses conversation-tenant Graph lookups for teams/channels and depends on the target tenant enterprise app having Team.ReadBasic.All and Channel.ReadBasic.All consented. The code keeps DM/channel isolation intact, uses stable IDs for persistence keys, and treats labels as cached diagnostics/display metadata instead of authority.

Repro + Verification

Environment

  • OS: macOS (local verification) + remote Linux gateway host for rollout validation
  • Runtime/container: Node 24 / pnpm build
  • Model/provider: N/A
  • Integration/channel (if any): Microsoft Teams
  • Relevant config (redacted): Teams bot installed in conversation tenant with Graph app-role consent for readable team/channel lookup

Steps

  1. Send/receive MS Teams channel traffic and DM follow-ups against the same bot installation.
  2. Trigger recent-channel focus persistence from DM after interacting with a channel thread.
  3. Verify sidecar and conversation-store entries after Graph consent is available in the conversation tenant.

Expected

  • Channel archive and cleanup state persist correctly.
  • Channel replies stay in-thread by default.
  • DM recent-channel focus persists separately from the main DM session.
  • Team/channel labels resolve to readable names when the conversation tenant grants Graph read-basic consent.

Actual

  • Verified readable labels were persisted successfully after target-tenant consent.
  • Verified conversation diagnostics include teamName / channelName and recent focus sidecar stores graph-backed labels.
  • Remote shared deployment built successfully and three OpenClaw instances restarted active.

Evidence

Attach at least one:

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

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • Confirmed recent channel focus sidecar persisted readable labels with teamLabelSource=graph and channelLabelSource=graph.
    • Confirmed conversation store persisted teamName and channelName after conversation-tenant consent.
    • Built the shared deployment directory on the gateway host and restarted moltbot-main, oc-pm, and oc-tl successfully.
  • Edge cases checked:
    • Target tenant without sufficient Graph consent fails cleanly until admin consent is granted.
    • Recent channel memory remains a sidecar/cache instead of merging channel history into DM context.
  • What you did not verify:
    • I did not complete a full real-message end-to-end Teams traffic run on every rolled-out instance after restart; validation there was limited to service health and smoke checks.

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.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (Yes)
  • Migration needed? (Yes)
  • If yes, exact upgrade steps:
    1. Deploy the updated MS Teams integration code.
    2. In each conversation tenant that needs readable team/channel labels, grant admin consent for Team.ReadBasic.All and Channel.ReadBasic.All on the bot enterprise app.
    3. Re-run traffic through the bot so labels are refreshed and cached.

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly:
    • Roll back the deployment to the previous OpenClaw build or revert this branch from the gateway deployment source.
  • Files/config to restore:
    • Restore the previous OpenClaw build output and, if needed, remove the recent-channel sidecar file for the affected instance.
  • Known bad symptoms reviewers should watch for:
    • Recent channel focus remains raw-ID only despite expected consent.
    • Unexpected Graph lookup failures in conversation-tenant label resolution.
    • Channel replies falling back out of thread.

Risks and Mitigations

  • Risk: conversation-tenant Graph consent may be missing in some tenants, so readable labels remain unavailable.
    • Mitigation: the feature degrades to stable IDs/diagnostics and only upgrades labels when Graph lookups succeed.
  • Risk: the branch is a stacked MSTeams integration branch that includes earlier archive/media hardening work along with the recent focus-label fixes.
    • Mitigation: the PR body calls out the broader scope explicitly so reviewers can review it as a single MSTeams stack.

Ming and others added 27 commits March 11, 2026 16:38
…inlined

Graph API's /hostedContents collection endpoint does not return contentBytes
inline in the response — the field is always null/absent. The previous code
would skip every item with an empty contentBytes, causing all image/file
attachments sent via Teams to be silently dropped.

Fix: when contentBytes is absent but the item has an id, fall back to fetching
the actual bytes via the individual /{id}/$value endpoint, which always returns
the binary content. The content-type header from that response is also used to
set itemContentType for accurate MIME detection downstream.

Verified working against Microsoft Teams (channel messages) with both image
and file attachments.
When a Teams channel message contains a mix of image and html attachments,
every() required ALL attachments to be html before trying the Graph path.
This caused image attachments to be silently skipped.

Use some() so the Graph path is attempted whenever at least one attachment
has a text/html content type.
…LIST

DM image attachments in Microsoft Teams use a direct download URL on
smba.trafficmanager.net (the Bot Framework attachment service, SMBA =
Service Messaging Bot Attachments). This domain was absent from the auth
token allowlist, so download requests were sent without a Bearer token,
resulting in 401 responses and silent download failures.

Add "trafficmanager.net" so that Bot Framework tokens are automatically
attached to requests targeting this domain.
…lback for channels

Two related fixes for buildMSTeamsGraphMessageUrls:

1. DM/group chat: prefer channelData.chatId (the proper Graph chat GUID)
   over conversationId, which may be a Bot Framework conversation ID in
   the form "a:xxx" that Graph does not recognise.

2. Channel messages: Teams channel IDs surfaced in channelData.team.id
   are sometimes in thread format ("19:xxx@thread.tacv2") rather than a
   GUID. The /teams/{guid}/channels/ Graph endpoint rejects these with
   400. /chats/{threadId}/messages/ accepts thread-format IDs, so append
   /chats/ candidate URLs as a fallback after the /teams/ URLs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- archive-store: tolerate malformed JSONL lines (skip + warn instead of throw)
- archive-store: bounded top-N accumulator in searchMessages/searchAttachments
  avoids O(n) memory with large archives; O(limit) memory instead
- channel-cleanup: cache Graph team/channel resolution across archives in
  one sweep to avoid N× full-tenant Graph scan per archived channel
- graph: check content-length header before materialising hosted-content
  arrayBuffer to prevent large allocations beyond maxBytes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er context

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ctivities

Some Bot Framework clients (e.g. certain MS Teams clients) send activity
payloads that contain invalid JSON escape sequences such as bare backslashes
followed by characters not defined in RFC 8259 (e.g. \p, \q, \c).

The previous `express.json()` middleware uses body-parser's strict JSON
parser which throws `SyntaxError: Bad escaped character in JSON` on such
payloads. This causes the webhook to return a non-200 response, putting
Azure Bot Service into exponential backoff and dropping subsequent messages.

This fix replaces `express.json()` with `express.raw()` (raw Buffer body),
then adds a two-pass JSON parse middleware:

1. First tries standard JSON.parse on the raw body.
2. If that fails (SyntaxError), attempts to repair the payload by
   double-escaping bare backslashes: /\\([^"\\/bfnrtu])/g → \\\\$1
3. If the repaired payload parses successfully, logs a warning and
   continues normally so the activity is processed.
4. If the payload cannot be repaired, responds with HTTP 200 to prevent
   Azure Bot Service backoff, and logs the error for investigation.

Fixes: SyntaxError: Bad escaped character in JSON at position N
Tested: conversationUpdate and message activities from MS Teams clients
…iddleware

Covers the bug where express.json()'s strict parser throws SyntaxError on
Bot Framework activities containing invalid escape sequences (e.g. \\p, \\q),
causing Azure Bot Service to enter exponential backoff and drop messages.

Tests verify:
1. Original JSON.parse() fails on bad-escape payloads (proving the bug)
2. Two-pass repair middleware recovers such payloads
3. Normal well-formed payloads still work (no regression)
4. Valid escape sequences (\\n \\t \\\\ etc.) are not double-escaped
5. Completely unrecoverable JSON is re-thrown (caller returns HTTP 200)"
Greptile code review (PR comment) identified an edge case in the
repair regex: /\\([^\"\\\/bfnrtu])/g incorrectly matches the second \
of a valid \\\\ pair followed by an invalid-escape char.

Example of the bug:
  Input JSON text: \"C:\\\\q\" (valid: literal string C:\\q)
  Old regex transforms \\q → \\\\q producing \"C:\\\\\\\\q\" (C:\\\\q) — wrong!

Fix: use alternation so valid \\\\ pairs are consumed first before
the invalid-escape branch can inspect their constituent backslashes.

  /(\\\\\\\\)+|\\\\([^\"\\\\\\\/bfnrtu])/g
    ^^^^^^^^^ consume valid \\\\ pairs (leave unchanged)
              ^^^^^^^^^^^^^^^^^^^^^^^^^ only then repair lone bare escapes

This ensures that:
  \\\\q  → unchanged   (valid: single literal backslash + q)
  \\q    → \\\\q       (repaired: invalid bare escape)
  \\\\\\q → \\\\\\\\q  (repaired: two-backslash pair OK, trailing lone \\q repaired)

Also adds a regression test for the consecutive-backslash edge case."
Copilot Reviewer identified that the express mock in monitor.lifecycle.test.ts
exported json() but not raw(). Since monitor.ts now uses express.raw() instead
of express.json(), the test suite would throw 'express.raw is not a function'
at startup.

Add raw() as a no-op middleware factory alongside json() in the mock, matching
the pattern already used for json().
CI 'Check types and lint and oxfmt' step failed because our new files
were not formatted with oxfmt. Run oxfmt --write on both files to fix.
- viewer-client.ts: import RegisteredCustomThemes and implement ensurePierreThemeLoaders to register theme loaders.
- monitor.ts: add a newline for better readability before messageHandler definition.
- remove outdated openclaw-2026.3.9.tgz file.
Copilot AI review requested due to automatic review settings March 17, 2026 02:31
@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: XL labels Mar 17, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft Teams integration (webhook JSON/media ingestion, threading defaults, and DM “recent channel focus” labeling) and adds a new persistent Teams channel archive extension that can prune archived data when channels are deleted.

Changes:

  • Add a new channel_deleted plugin hook and wire MS Teams conversationUpdate (channelDeleted) events to it.
  • Improve MS Teams webhook robustness (JSON escape repair middleware, cross-tenant Graph token usage, improved attachment/media fallback) and default channel replies to threads.
  • Introduce msteams-channel-archive extension for durable message/attachment archiving plus cleanup service + query tools.

Reviewed changes

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

Show a summary per file
File Description
src/plugins/wired-hooks-message.test.ts Adds coverage for runChannelDeleted hook runner behavior.
src/plugins/types.ts Registers new channel_deleted hook type and handler signature.
src/plugins/hooks.ts Adds runChannelDeleted to the hook runner API.
src/plugin-sdk/msteams.ts Exposes additional SDK exports needed by Teams extensions (sessions + global hook runner + service type).
src/hooks/message-hook-mappers.ts Extends canonical inbound context mapping with Teams-specific metadata/media arrays.
src/hooks/message-hook-mappers.test.ts Updates tests for new inbound context fields.
src/auto-reply/templating.ts Adds ProviderMetadata to message context typing for templating/tooling.
pnpm-lock.yaml Adds new workspace importer deps for extensions/msteams-channel-archive.
extensions/msteams/src/policy.ts Adds alternate team id matching + defaults channel reply style to thread.
extensions/msteams/src/policy.test.ts Tests legacy vs alternate team id matching + new reply-style defaults.
extensions/msteams/src/monitor.ts Switches to raw JSON parsing with escape repair; improves error handling/backoff behavior.
extensions/msteams/src/monitor.lifecycle.test.ts Updates express mock to include raw() middleware.
extensions/msteams/src/monitor-handler/message-handler.ts Persists recent channel focus sidecar; resolves readable labels via Graph; enriches ProviderMetadata.
extensions/msteams/src/monitor-handler/message-handler.focus.test.ts Adds focused tests for sidecar persistence, Graph label resolution, and DM injection.
extensions/msteams/src/monitor-handler/inbound-media.ts Forwards conversation tenant id to Graph media fallback; broadens HTML attachment detection.
extensions/msteams/src/monitor-handler/inbound-media.test.ts Adds regression test for tenant forwarding to Graph media download.
extensions/msteams/src/monitor-handler.ts Forwards Teams channelDeleted events to channel_deleted plugin hooks via global hook runner.
extensions/msteams/src/monitor-handler.file-consent.test.ts Adds coverage ensuring channelDeleted events reach hooks; updates mocks for hook runner.
extensions/msteams/src/graph.ts Adds conversation-tenant Graph token support + getTeamById.
extensions/msteams/src/conversation-store.ts Stores additional mutable display label fields + runtime/Graph team ids.
extensions/msteams/src/channel-focus-store.ts New sidecar store for “recent channel focus” metadata keyed by main session.
extensions/msteams/src/attachments/shared.ts Tightens allowlists for Bot Framework attachment hosts.
extensions/msteams/src/attachments/html.ts Avoids counting non-downloadable HTML/card attachments as documents.
extensions/msteams/src/attachments/graph.ts Adds /chats URL fallback, hosted content $value fetching, and cross-tenant token support.
extensions/msteams/src/attachments.test.ts Adds placeholder regression cases for HTML-only attachments.
extensions/msteams/src/tests/monitor-json-repair.test.ts Adds regression tests for JSON escape repair logic.
extensions/msteams-channel-archive/src/types.ts Defines archive index/message/attachment types.
extensions/msteams-channel-archive/src/tools.ts Registers archive query tools with TypeBox schemas.
extensions/msteams-channel-archive/src/channel-cleanup.ts Adds periodic Graph-based “deleted channel” cleanup sweep + pruning.
extensions/msteams-channel-archive/src/channel-cleanup.test.ts Tests pruning rules, failure behavior, and token/team-id caching.
extensions/msteams-channel-archive/src/archive-store.ts Implements on-disk JSONL archive storage + attachment copying + search/prune APIs.
extensions/msteams-channel-archive/src/archive-store.test.ts Tests archiving, dedupe, search, and prune/orphan attachment cleanup.
extensions/msteams-channel-archive/package.json Adds new private extension package manifest.
extensions/msteams-channel-archive/openclaw.plugin.json Declares plugin id/channels/config schema.
extensions/msteams-channel-archive/index.ts Wires hooks (message_received, channel_deleted), tools, and cleanup service.
extensions/msteams-channel-archive/index.test.ts Verifies hook registration and mediaTypes alignment behavior.
extensions/diffs/src/viewer-client.ts Registers Pierre theme loaders via RegisteredCustomThemes.
CHANGELOG.md Documents Teams channel archive + hook runner hardening entries.
.github/labeler.yml Adds labeler rule for the new msteams-channel-archive extension.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +52 to +55
export async function resolveGraphToken(params: {
cfg: unknown;
conversationTenantId?: string;
}): Promise<string> {

export async function getTeamById(token: string, teamId: string): Promise<GraphGroup | null> {
const path = `/teams/${encodeURIComponent(teamId)}?$select=id,displayName`;
return await fetchGraphJson<GraphGroup>({ token, path });
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 17, 2026

Greptile Summary

This is a substantial MS Teams integration hardening PR that stacks several related improvements: channel archive persistence (new msteams-channel-archive extension), JSON escape repair middleware, inbound media improvements, channel-to-DM focus sidecar, and Graph-backed readable team/channel labels. The changes are well-scoped, include meaningful test coverage for the new paths, and degrade gracefully when Graph consent is absent.

Key findings:

  • JSON repair middleware (monitor.ts): The repairJsonEscapes regex is correct — the (\\\\)+ alternation properly consumes already-valid escaped pairs before repairing lone bare-backslash escapes. The two-pass middleware is a solid fix for the Azure Bot Service backoff problem and has a dedicated regression test suite.
  • replyStyle default change (policy.ts): The default reply style is now unconditionally "thread" instead of requireMention ? "thread" : "top-level". This is an intentional behaviour change documented in the PR, but it is a silent breaking change for any deployment that relied on requireMention: false producing top-level replies without an explicit replyStyle config. Operators upgrading should be aware they may need to add replyStyle: "top-level" to preserve their current behaviour.
  • Cross-tenant token via private SDK internals (attachments/graph.ts): The as unknown as { connectionSettings?: ... } cast probes for private MsalTokenProvider internals to build a cross-tenant token. This is fragile across SDK versions; the public resolveGraphToken({ cfg, conversationTenantId }) helper already does this correctly and could be reused here instead.
  • getTeamById return type (graph.ts): Annotated as Promise<GraphGroup | null> but fetchGraphJson throws on non-2xx (including 404) rather than returning null — the | null is dead code. All callers use try-catch so there is no runtime impact, but the annotation could mislead future readers.
  • console.warn in parseJsonLine (archive-store.ts): Uses console.warn directly instead of the injected Logger, so malformed-JSONL warnings bypass the host's log handler.

Confidence Score: 3/5

  • Safe to merge with awareness of the replyStyle default change and the fragile cross-tenant token path in the hosted-content downloader.
  • Good test coverage for new functionality and the JSON repair path. All failure modes are wrapped in try-catch with graceful degradation. The main concerns are: (1) the replyStyle default change silently breaks deployments that relied on requireMention: false → top-level replies; (2) the cross-tenant token block in attachments/graph.ts uses private SDK internals that could silently stop working after an SDK upgrade; and (3) minor type annotation and logging inconsistencies. None of these are blocking regressions, but the behaviour change in reply policy should be called out clearly in the upgrade notes.
  • extensions/msteams/src/attachments/graph.ts (private SDK internal access for cross-tenant token), extensions/msteams/src/policy.ts (default replyStyle behaviour change)
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/graph.ts
Line: 88-92

Comment:
**`getTeamById` return type is misleading**

The function signature promises `GraphGroup | null`, but since `fetchGraphJson` throws a `new Error(...)` on any non-OK HTTP response (including 404), this function will never actually return `null` — it either returns the group or throws. All call-sites already use try-catch, so this is functionally fine, but the `| null` annotation is dead code and could mislead a future caller into skipping the try-catch wrapper in favour of a null check.

Consider updating to match the real contract:

```suggestion
export async function getTeamById(token: string, teamId: string): Promise<GraphGroup> {
  const path = `/teams/${encodeURIComponent(teamId)}?$select=id,displayName`;
  return await fetchGraphJson<GraphGroup>({ token, path });
}
```

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/attachments/graph.ts
Line: 307-335

Comment:
**Cross-tenant token acquisition via private SDK internals**

The cast `params.tokenProvider as unknown as { connectionSettings?: ...; getAccessToken(authConfig, scope) }` accesses internal properties and an overloaded signature that are not part of the public `tokenProvider` contract. If the SDK refactors `MsalTokenProvider` internals, this silently falls back to the bot-tenant token (because `_provider.connectionSettings?.clientId` will be `undefined` and the cross-tenant branch is skipped). This is fail-safe behaviour today, but makes the cross-tenant path invisible in test coverage and fragile across SDK upgrades.

The cleaner, SDK-version-proof approach already used elsewhere in this PR is `resolveGraphToken({ cfg, conversationTenantId })` — that function explicitly constructs the right auth config via `sdk.getAuthConfigWithDefaults(...)`. Consider replacing this block with a call to `resolveGraphToken` (or an equivalent helper) so the cross-tenant override doesn't rely on private properties.

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-channel-archive/src/archive-store.ts
Line: 224-234

Comment:
**`console.warn` bypasses the injected logger**

`parseJsonLine` uses `console.warn` directly rather than the `Logger` that is injected via `MSTeamsChannelArchiveStore`. This means malformed JSONL warnings won't flow through the host's configured log handler (level filtering, structured-log sinks, etc.).

One option is to thread the logger through as a parameter. Alternatively, since this is a static parse helper, it can return a sentinel and let the calling method emit the warning through `this.logger`:

```suggestion
function parseJsonLine<T>(line: string): T | null {
  try {
    return JSON.parse(line) as T;
  } catch {
    return null;
  }
}
```

And in the callers (e.g. `readMessagesFromFile`), emit the warning via `this.logger.warn(...)` when `null` is returned, passing `filePath` and `lineNumber` from the calling context.

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

Last reviewed commit: 6679564

Comment on lines 88 to +92
return res.value ?? [];
}

export async function getTeamById(token: string, teamId: string): Promise<GraphGroup | null> {
const path = `/teams/${encodeURIComponent(teamId)}?$select=id,displayName`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 getTeamById return type is misleading

The function signature promises GraphGroup | null, but since fetchGraphJson throws a new Error(...) on any non-OK HTTP response (including 404), this function will never actually return null — it either returns the group or throws. All call-sites already use try-catch, so this is functionally fine, but the | null annotation is dead code and could mislead a future caller into skipping the try-catch wrapper in favour of a null check.

Consider updating to match the real contract:

Suggested change
return res.value ?? [];
}
export async function getTeamById(token: string, teamId: string): Promise<GraphGroup | null> {
const path = `/teams/${encodeURIComponent(teamId)}?$select=id,displayName`;
export async function getTeamById(token: string, teamId: string): Promise<GraphGroup> {
const path = `/teams/${encodeURIComponent(teamId)}?$select=id,displayName`;
return await fetchGraphJson<GraphGroup>({ token, path });
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/graph.ts
Line: 88-92

Comment:
**`getTeamById` return type is misleading**

The function signature promises `GraphGroup | null`, but since `fetchGraphJson` throws a `new Error(...)` on any non-OK HTTP response (including 404), this function will never actually return `null` — it either returns the group or throws. All call-sites already use try-catch, so this is functionally fine, but the `| null` annotation is dead code and could mislead a future caller into skipping the try-catch wrapper in favour of a null check.

Consider updating to match the real contract:

```suggestion
export async function getTeamById(token: string, teamId: string): Promise<GraphGroup> {
  const path = `/teams/${encodeURIComponent(teamId)}?$select=id,displayName`;
  return await fetchGraphJson<GraphGroup>({ token, path });
}
```

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

Comment on lines 307 to 335
const messageUrl = params.messageUrl;
let accessToken: string;
try {
accessToken = await params.tokenProvider.getAccessToken("https://graph.microsoft.com");
// Cross-tenant: if conversation tenant differs from bot tenant, use conversation tenant token
const _provider = params.tokenProvider as unknown as {
connectionSettings?: { clientId?: string; clientSecret?: string; tenantId?: string };
getAccessToken(authConfig: Record<string, unknown>, scope: string): Promise<string>;
getAccessToken(scope: string): Promise<string>;
};
const _convTenantId = params.conversationTenantId;
if (
_convTenantId &&
_provider.connectionSettings?.clientId &&
_provider.connectionSettings?.clientSecret &&
_convTenantId !== _provider.connectionSettings.tenantId
) {
accessToken = await _provider.getAccessToken(
{
clientId: _provider.connectionSettings.clientId,
clientSecret: _provider.connectionSettings.clientSecret,
tenantId: _convTenantId,
},
"https://graph.microsoft.com",
);
} else {
accessToken = await params.tokenProvider.getAccessToken("https://graph.microsoft.com");
}
} catch {
return { media: [], messageUrl, tokenError: true };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Cross-tenant token acquisition via private SDK internals

The cast params.tokenProvider as unknown as { connectionSettings?: ...; getAccessToken(authConfig, scope) } accesses internal properties and an overloaded signature that are not part of the public tokenProvider contract. If the SDK refactors MsalTokenProvider internals, this silently falls back to the bot-tenant token (because _provider.connectionSettings?.clientId will be undefined and the cross-tenant branch is skipped). This is fail-safe behaviour today, but makes the cross-tenant path invisible in test coverage and fragile across SDK upgrades.

The cleaner, SDK-version-proof approach already used elsewhere in this PR is resolveGraphToken({ cfg, conversationTenantId }) — that function explicitly constructs the right auth config via sdk.getAuthConfigWithDefaults(...). Consider replacing this block with a call to resolveGraphToken (or an equivalent helper) so the cross-tenant override doesn't rely on private properties.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/attachments/graph.ts
Line: 307-335

Comment:
**Cross-tenant token acquisition via private SDK internals**

The cast `params.tokenProvider as unknown as { connectionSettings?: ...; getAccessToken(authConfig, scope) }` accesses internal properties and an overloaded signature that are not part of the public `tokenProvider` contract. If the SDK refactors `MsalTokenProvider` internals, this silently falls back to the bot-tenant token (because `_provider.connectionSettings?.clientId` will be `undefined` and the cross-tenant branch is skipped). This is fail-safe behaviour today, but makes the cross-tenant path invisible in test coverage and fragile across SDK upgrades.

The cleaner, SDK-version-proof approach already used elsewhere in this PR is `resolveGraphToken({ cfg, conversationTenantId })` — that function explicitly constructs the right auth config via `sdk.getAuthConfigWithDefaults(...)`. Consider replacing this block with a call to `resolveGraphToken` (or an equivalent helper) so the cross-tenant override doesn't rely on private properties.

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

Comment on lines +224 to +234
threadId?: string;
rootMessageId?: string;
limit?: number;
}): Promise<ArchiveMessageRecord[]> {
const threadKey = params.rootMessageId?.trim() || params.threadId?.trim();
if (!threadKey) {
throw new Error("threadId or rootMessageId is required");
}

const messages = await this.readArchiveMessages(params.conversationId);
return (await Promise.all(messages.map((message) => this.withAttachmentStatus(message))))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 console.warn bypasses the injected logger

parseJsonLine uses console.warn directly rather than the Logger that is injected via MSTeamsChannelArchiveStore. This means malformed JSONL warnings won't flow through the host's configured log handler (level filtering, structured-log sinks, etc.).

One option is to thread the logger through as a parameter. Alternatively, since this is a static parse helper, it can return a sentinel and let the calling method emit the warning through this.logger:

Suggested change
threadId?: string;
rootMessageId?: string;
limit?: number;
}): Promise<ArchiveMessageRecord[]> {
const threadKey = params.rootMessageId?.trim() || params.threadId?.trim();
if (!threadKey) {
throw new Error("threadId or rootMessageId is required");
}
const messages = await this.readArchiveMessages(params.conversationId);
return (await Promise.all(messages.map((message) => this.withAttachmentStatus(message))))
function parseJsonLine<T>(line: string): T | null {
try {
return JSON.parse(line) as T;
} catch {
return null;
}
}

And in the callers (e.g. readMessagesFromFile), emit the warning via this.logger.warn(...) when null is returned, passing filePath and lineNumber from the calling context.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams-channel-archive/src/archive-store.ts
Line: 224-234

Comment:
**`console.warn` bypasses the injected logger**

`parseJsonLine` uses `console.warn` directly rather than the `Logger` that is injected via `MSTeamsChannelArchiveStore`. This means malformed JSONL warnings won't flow through the host's configured log handler (level filtering, structured-log sinks, etc.).

One option is to thread the logger through as a parameter. Alternatively, since this is a static parse helper, it can return a sentinel and let the calling method emit the warning through `this.logger`:

```suggestion
function parseJsonLine<T>(line: string): T | null {
  try {
    return JSON.parse(line) as T;
  } catch {
    return null;
  }
}
```

And in the callers (e.g. `readMessagesFromFile`), emit the warning via `this.logger.warn(...)` when `null` is returned, passing `filePath` and `lineNumber` from the calling context.

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

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: 6679564015

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +323 to +330
accessToken = await _provider.getAccessToken(
{
clientId: _provider.connectionSettings.clientId,
clientSecret: _provider.connectionSettings.clientSecret,
tenantId: _convTenantId,
},
"https://graph.microsoft.com",
);
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 Call token provider with scope-first signature

In the cross-tenant branch, _provider.getAccessToken is invoked with an auth-config object as the first argument and the Graph scope second, but the provider used in this path is the same MsalTokenProvider/MSTeamsAccessTokenProvider that is otherwise called as getAccessToken(scope). When that single-argument implementation is used (the normal case), this passes a non-scope object where the scope is expected, causing token acquisition to fail and downloadMSTeamsGraphMedia to return tokenError, so cross-tenant inbound media extraction silently stops working.

Useful? React with 👍 / 👎.

if (!sourcePath) {
continue;
}
const content = await fs.promises.readFile(sourcePath);
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 Skip unreadable attachments instead of aborting archive writes

Archiving currently reads each sourcePath without error handling, so a single missing or unreadable media file throws and aborts archiveMessage before the message record is appended. In that case the whole channel message is lost from the archive (not just the bad attachment), which makes archive persistence fragile whenever media files are cleaned up or inaccessible.

Useful? React with 👍 / 👎.

@vincentkoc
Copy link
Copy Markdown
Member

Thanks for the contribution. I am closing this because the branch appears dirty and too broad for the core queue: the stated MS Teams hardening spans 39 files, adds a new msteams-channel-archive extension, changes unrelated surfaces such as .github/labeler.yml and extensions/diffs, and still has unresolved automated review findings. Please recreate the PR from current main with one scoped change and current validation if this is still needed.

@vincentkoc vincentkoc closed this Apr 27, 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 size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants