Skip to content

msteams dmPolicy=pairing silently drops unpaired senders with HTTP 200, no log line, no auto-reply #62765

@tmote

Description

@tmote

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

  1. 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>"
        }
      }
    }
  2. Confirm the bot is reachable: send a probe through your public ingress, gateway returns 401 Unauthorized from the bot auth middleware (correct — not authenticated).

  3. From a real Teams client (or Azure portal "Test in Web Chat"), send the bot a message.

  4. 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.

  5. 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.

  6. 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:

  1. 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.
  2. No auto-reply to the user — they have no way to discover the pair code without SSH access to the box.
  3. 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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions