fix(msteams): prefer personal conversation in findByUserId to prevent DM misrouting#53458
fix(msteams): prefer personal conversation in findByUserId to prevent DM misrouting#53458openperf wants to merge 4 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a DM misrouting bug in the MS Teams integration where proactive sends could be delivered to a team channel instead of the intended 1:1 chat when a user has multiple conversations with the bot. The fix updates Key changes:
Confidence Score: 5/5
Reviews (1): Last reviewed commit: "fix(msteams): prefer personal conversati..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 726c42dd20
ℹ️ 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".
…e check The rest of the codebase lowercases conversationType before comparison (graph.ts, file-consent-helpers.ts, messenger.ts, monitor-handler.ts, reply-dispatcher.ts). Align findByUserId to do the same so upstream values like "Personal" or "PERSONAL" are correctly recognized. Addresses Codex review feedback on openclaw#53458.
e539f94 to
a10611d
Compare
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
Vulnerabilities1. 🟡 TypeError DoS in findByUserId via non-string conversationType .toLowerCase() call (FS store)
Description
Vulnerable code: const personal = matches.find(
(m) => m.reference.conversation?.conversationType?.toLowerCase() === "personal",
);RecommendationHarden the check to only call const ct = m.reference.conversation?.conversationType;
return typeof ct === "string" && ct.toLowerCase() === "personal";Alternatively, use optional-call chaining on the method, but still ensure the value is a string if the semantics matter: const ct = m.reference.conversation?.conversationType;
return ct?.toLowerCase?.() === "personal";(Prefer the 2. 🟡 TypeError DoS in findByUserId via non-string conversationType .toLowerCase() call (memory store)
Description
Vulnerable code: matches.find(
(m) => m.reference.conversation?.conversationType?.toLowerCase() === "personal",
)RecommendationGuard the type before calling const ct = m.reference.conversation?.conversationType;
return typeof ct === "string" && ct.toLowerCase() === "personal";If you prefer a shorter form, use optional-call chaining but be aware it allows objects with a const ct = m.reference.conversation?.conversationType;
return ct?.toLowerCase?.() === "personal";3. 🟡 Conversation reference lookup not scoped by tenant, enabling cross-tenant proactive message misrouting
Description
Vulnerable code: if (reference.user?.aadObjectId === target || reference.user?.id === target) {
matches.push(entry);
}
...
const personal = matches.find(
(m) => m.reference.conversation?.conversationType?.toLowerCase() === "personal",
);RecommendationScope conversation reference storage and lookup by tenant (and ideally by bot/app installation). Option A (recommended): store keyed by tenantId + user id
Example: async function findByUser(params: { tenantId: string; userId: string }) {
const target = params.userId.trim();
const tenantId = params.tenantId;
const matches = (await list()).filter((e) =>
(e.reference.user?.aadObjectId === target || e.reference.user?.id === target) &&
e.reference.conversation?.tenantId === tenantId,
);
// then apply personal/lastSeen selection within the tenant
}Option B: maintain separate stores per tenant (separate file per tenant) so collisions cannot occur. Also consider verifying that proactive send flows always provide tenant context (from config/recipient) so lookups cannot accidentally cross tenants. Analyzed PR: #53458 at commit Last updated on: 2026-03-27T05:38:00Z Latest run failed. Keeping previous successful results. Trace ID: Last updated on: 2026-03-27T08:34:16Z |
a10611d to
6b6352e
Compare
…e check The rest of the codebase lowercases conversationType before comparison (graph.ts, file-consent-helpers.ts, messenger.ts, monitor-handler.ts, reply-dispatcher.ts). Align findByUserId to do the same so upstream values like "Personal" or "PERSONAL" are correctly recognized. Addresses Codex review feedback on openclaw#53458.
… DM misrouting When a user has both a 1:1 DM and a channel/group conversation with the bot, findByUserId returned whichever entry happened to appear first in the store iteration order. This caused proactive sends initiated from a DM session (e.g. agent sending an Adaptive Card via the send tool) to be delivered to the wrong conversation. Collect all matching entries and prefer the one with conversationType === "personal". When no personal conversation exists, the fs store falls back to the most recently seen entry (by lastSeenAt) and the memory store falls back to the first match. Fixes openclaw#51947
…e check The rest of the codebase lowercases conversationType before comparison (graph.ts, file-consent-helpers.ts, messenger.ts, monitor-handler.ts, reply-dispatcher.ts). Align findByUserId to do the same so upstream values like "Personal" or "PERSONAL" are correctly recognized. Addresses Codex review feedback on openclaw#53458.
6b6352e to
b050e2f
Compare
…serId-prefer-personal
e9111fa to
3dd4696
Compare
Problem
Fixes #51947
When a user has both a 1:1 DM and a channel/group conversation with the bot, proactive sends
initiated from a DM session may be delivered to the wrong conversation. For example, an agent
sending an Adaptive Card via the
sendtool in a 1:1 chat could have that card appear in ateam channel instead.
Root cause
For DMs, the inbound context sets
Toasuser:{aadObjectId}. When thesendtool laterresolves the target via
resolveMSTeamsSendContext, it callsfindByUserIdin the conversationstore, which iterates all stored conversations and returns the first entry matching the
user's AAD object ID or Bot Framework ID — with no awareness of conversation type or recency.
Since
Object.entries()iteration order depends on insertion order in the JSON store, theresult is non-deterministic from the user's perspective: if the channel/group conversation
was stored before the personal DM, the proactive message is delivered there instead.
Both the filesystem store (
conversation-store-fs.ts) and the in-memory store(
conversation-store-memory.ts) share this behavior.Solution
Update
findByUserIdin both store implementations to collect all matching entries for auser, then apply a preference:
personal(1:1) conversations — when a user lookup is performed (typically forDM-initiated proactive sends), the personal conversation is the semantically correct target.
the most recently seen entry (by
lastSeenAttimestamp, which already exists in thepersisted data), and the memory store returns the first match.
This approach was chosen over changing the
Toformat (Option 2 in the issue) or threadingsession context (Option 3/4) because:
findByUserId) without changing the inbound context format,avoiding downstream breakage in code paths that parse
user:prefix for DM detection.conversationTypefield already stored in every conversation reference.findByUserId, not just the DM inbound path.lastSeenAtfield already exists in the fs store data, so no schema changes are needed.Changed files
extensions/msteams/src/conversation-store-fs.tsfindByUserIdcollects all matches, preferspersonal, falls back to most recentextensions/msteams/src/conversation-store-memory.tsfindByUserIdcollects all matches, preferspersonal, falls back to first matchextensions/msteams/src/conversation-store-fs.test.tsextensions/msteams/src/conversation-store-memory.test.tsTesting
New test cases cover:
findByUserIdreturns the personal one regardless of insertion order
seen entry
match deterministically
Existing tests continue to pass (no behavioral change for single-conversation users).
Reproduction scenario
the
sendtool)