Bug: msteams dmPolicy=pairing silently drops unpaired senders with HTTP 200, no log line, no auto-reply
Environment
- OpenClaw
2026.4.5 (3e72c03)
- Ubuntu 24.04.4 LTS, Node v24.14.1 via nvm
@microsoft/teams.apps SDK (bundled)
- single-tenant Bot Framework registration in Entra (
MsaAppType: SingleTenant)
channels.msteams.dmPolicy = "pairing" (the default)
Steps to reproduce
-
Configure msteams with the default pairing policy:
{
"channels": {
"msteams": {
"enabled": true,
"dmPolicy": "pairing",
"groupPolicy": "disabled",
"appId": "<your-bot-app-id>",
"tenantId": "<your-tenant-id>",
"appPassword": "<your-secret>"
}
}
}
-
Confirm the bot is reachable: send a probe through your public ingress, gateway returns 401 Unauthorized from the bot auth middleware (correct — not authenticated).
-
From a real Teams client (or Azure portal "Test in Web Chat"), send the bot a message.
-
Observe: nothing happens. No reply in Teams. No log line in journalctl -u openclaw-gateway.service. No pleres sessions.json mtime change. The bot looks dead.
-
Check ~/.openclaw/credentials/msteams-pairing.json:
{
"version": 1,
"requests": [
{ "id": "<sender-id>", "code": "RWLGC4G5",
"createdAt": "...", "lastSeenAt": "...",
"meta": { "accountId": "default" } }
]
}
The pairing request has been recorded — lastSeenAt updates on every dropped message. So the channel IS receiving the activity, recognising it as unpaired, and dropping it.
-
Check the gateway journal for signal pairing request sender= and signal pairing reply failed for log lines — those exist for Signal in dist/monitor-ssbAt9_T.js. There is no analogous code for msteams. The msteams pairing path has detection but no auto-reply implementation.
Expected behavior
Match Signal's pairing flow:
- (a) Log a line at info level when dropping an unpaired msteams message:
[msteams] pairing request sender=<id> code=<code> (so operators can see it in the journal).
- (b) Auto-reply to the sender with the pair code and a one-line instruction: "Send
pair RWLGC4G5 from an admin shell with openclaw pairing approve --channel msteams RWLGC4G5 to allow this sender." Or, equivalently, surface the code in a Teams adaptive card.
- (c) At the very least, set
Content-Length to a non-empty value in the 200 response so the operator can see SOMETHING came back via tcpdump on loopback:3978.
Actual behavior
Three failure modes stacked:
- Zero log output. Tcpdump on loopback shows the gateway returns
HTTP/1.1 200 OK with Content-Length: 0 to every BFS POST, regardless of pairing state. The 200 makes BFS think the bot is healthy.
- No auto-reply to the user — they have no way to discover the pair code without SSH access to the box.
- The pairing-request entry is silently created in
msteams-pairing.json, with no surfacing anywhere.
Severity
Medium. Once you know the workaround (openclaw pairing list --channel msteams; openclaw pairing approve --channel msteams <code>), the ops loop is straightforward. But the silent-drop trap cost ~2 hours of debugging during our initial deployment — we chased Cloudflare Tunnel reachability, JWT auth validation, Entra secret rotation, and gateway env injection before tcpdumping loopback and finding the gateway was already happily 200-OK'ing every BFS POST. The fix wasn't network or auth — it was pairing.
Workaround
# After the user sends their first message, on the VM:
openclaw pairing list --channel msteams # find the pending code
openclaw pairing approve --channel msteams XXXXXXXX
# Then user sends another message — works.
For first-time bootstrap, an operator with shell access has to be present. There's no in-band way for a user to self-pair.
Suggested fix paths
- Log on drop (smallest possible fix): add an info log line in the msteams pairing-detection path when an unpaired sender's request is recorded. Mirror the Signal
monitor-ssbAt9_T.js line.
- Auto-reply with code (medium fix): have the msteams provider send an in-band reply with the pair code. Bot Framework reply path is already wired (replies to other types work fine). The reply doesn't need to be fancy — even a one-line "Pair code:
RWLGC4G5 (admin: openclaw pairing approve --channel msteams RWLGC4G5)" would be enough.
- Adaptive card with self-service link (full fix): show an adaptive card on first contact that links the user to a
/pairing-approve?code=... endpoint on the gateway's web UI. This is the Signal-equivalent UX.
Where this bit us
Phase 21 of Synap deployment (Cloudflare Tunnel migration, 2026-04-07). After fixing every layer of the public ingress path (Tailscale Funnel → Cloudflare Tunnel migration, bot endpoint update in Azure, JWT validation verified end-to-end with tcpdump), the gateway was returning 200 OK to every BFS POST but no agent reply ever materialised. Tracking down dmPolicy as the silent dropper took hours we'd have saved with a single log line on drop.
Bug: msteams
dmPolicy=pairingsilently drops unpaired senders with HTTP 200, no log line, no auto-replyEnvironment
2026.4.5 (3e72c03)@microsoft/teams.appsSDK (bundled)MsaAppType: SingleTenant)channels.msteams.dmPolicy = "pairing"(the default)Steps to reproduce
Configure msteams with the default pairing policy:
{ "channels": { "msteams": { "enabled": true, "dmPolicy": "pairing", "groupPolicy": "disabled", "appId": "<your-bot-app-id>", "tenantId": "<your-tenant-id>", "appPassword": "<your-secret>" } } }Confirm the bot is reachable: send a probe through your public ingress, gateway returns
401 Unauthorizedfrom the bot auth middleware (correct — not authenticated).From a real Teams client (or Azure portal "Test in Web Chat"), send the bot a message.
Observe: nothing happens. No reply in Teams. No log line in
journalctl -u openclaw-gateway.service. Nopleres sessions.jsonmtime change. The bot looks dead.Check
~/.openclaw/credentials/msteams-pairing.json:{ "version": 1, "requests": [ { "id": "<sender-id>", "code": "RWLGC4G5", "createdAt": "...", "lastSeenAt": "...", "meta": { "accountId": "default" } } ] }The pairing request has been recorded —
lastSeenAtupdates on every dropped message. So the channel IS receiving the activity, recognising it as unpaired, and dropping it.Check the gateway journal for
signal pairing request sender=andsignal pairing reply failed forlog lines — those exist for Signal indist/monitor-ssbAt9_T.js. There is no analogous code for msteams. The msteams pairing path has detection but no auto-reply implementation.Expected behavior
Match Signal's pairing flow:
[msteams] pairing request sender=<id> code=<code>(so operators can see it in the journal).pair RWLGC4G5from an admin shell withopenclaw pairing approve --channel msteams RWLGC4G5to allow this sender." Or, equivalently, surface the code in a Teams adaptive card.Content-Lengthto a non-empty value in the 200 response so the operator can see SOMETHING came back via tcpdump on loopback:3978.Actual behavior
Three failure modes stacked:
HTTP/1.1 200 OKwithContent-Length: 0to every BFS POST, regardless of pairing state. The 200 makes BFS think the bot is healthy.msteams-pairing.json, with no surfacing anywhere.Severity
Medium. Once you know the workaround (
openclaw pairing list --channel msteams; openclaw pairing approve --channel msteams <code>), the ops loop is straightforward. But the silent-drop trap cost ~2 hours of debugging during our initial deployment — we chased Cloudflare Tunnel reachability, JWT auth validation, Entra secret rotation, and gateway env injection before tcpdumping loopback and finding the gateway was already happily 200-OK'ing every BFS POST. The fix wasn't network or auth — it was pairing.Workaround
For first-time bootstrap, an operator with shell access has to be present. There's no in-band way for a user to self-pair.
Suggested fix paths
monitor-ssbAt9_T.jsline.RWLGC4G5(admin:openclaw pairing approve --channel msteams RWLGC4G5)" would be enough./pairing-approve?code=...endpoint on the gateway's web UI. This is the Signal-equivalent UX.Where this bit us
Phase 21 of Synap deployment (Cloudflare Tunnel migration, 2026-04-07). After fixing every layer of the public ingress path (Tailscale Funnel → Cloudflare Tunnel migration, bot endpoint update in Azure, JWT validation verified end-to-end with tcpdump), the gateway was returning 200 OK to every BFS POST but no agent reply ever materialised. Tracking down dmPolicy as the silent dropper took hours we'd have saved with a single log line on drop.