Skip to content

feat(voice-call): add Microsoft Teams voice provider (OpenClawTeamsBridge)#89724

Closed
ahenawy wants to merge 10 commits into
openclaw:mainfrom
ahenawy:main
Closed

feat(voice-call): add Microsoft Teams voice provider (OpenClawTeamsBridge)#89724
ahenawy wants to merge 10 commits into
openclaw:mainfrom
ahenawy:main

Conversation

@ahenawy

@ahenawy ahenawy commented Jun 3, 2026

Copy link
Copy Markdown

Summary

What problem does this PR solve?

  • OpenClaw's voice-call extension supports Twilio, Telnyx, Plivo, and a mock provider, but has no way to handle Microsoft Teams calls. Teams audio cannot reach the existing webhook/media-stream plane because Teams calls are delivered through Graph Calling signalling + the Microsoft.Skype.Bots.Media AudioSocket, which only run on .NET/Windows.
  • This adds a 5th voice-call provider, msteams, that bridges Teams call audio into the existing voice-call machinery (CallManager, realtime transcription, response generation, telephony TTS).

Why does this matter now?

  • It unlocks real-time voice agents on Microsoft Teams using the same OpenClaw agent/STT/TTS stack already used for PSTN providers, with no changes to those providers.

What is the intended outcome?

  • An operator can set provider: "msteams" and OpenClaw will accept per-call WebSocket connections from an external Windows worker ("OpenClawTeamsBridge" / AzureBot), authenticate them, and run the full inbound->STT->agent->TTS->outbound loop with barge-in - mirroring the streaming Twilio provider but speaking PCM 16 kHz instead of mu-law 8 kHz.

What is intentionally out of scope?

  • The Windows-side worker (Graph Calling notification endpoint + Skype.Bots.Media AudioSocket) is a separate component, not included in this repo. This PR only adds the OpenClaw-side provider + WebSocket transport it connects to.
  • Outbound dialing (initiating Teams calls) - the provider is inbound-only and throws on initiateCall.

What does success look like?

  • provider: "msteams" binds an HMAC-authenticated WebSocket server; a live Teams call routed through the external worker produces a transcript, an agent reply, and synthesized speech back to the caller, with barge-in.

What should reviewers focus on?

  • The WebSocket auth/transport in msteams-media-stream.ts (HMAC verification, replay window, payload cap, and loopback-by-default bind).
  • The msteams config-validation in config.ts: required port + SecretRef-compatible sharedSecret, the fromNumber exemption, and inboundPolicy defaulting to a safe "allowlist".
  • The runtime lifecycle in runtime.ts: the media server starts via an awaited provider.start() (a failed bind aborts init) and stops on teardown so the port is released.
  • That the new provider reuses existing exported helpers (generateVoiceResponse, CallManager.speak, telephony TTS) rather than duplicating webhook logic.

Linked context

Which issue does this close?

Closes #

Which issues, PRs, or discussions are related?

Related #

Was this requested by a maintainer or owner?


Real behavior proof (required for external PRs)

  • Behavior or issue addressed: voice-call had no Microsoft Teams provider - provider: "msteams" was rejected as unsupported. This PR adds the provider, the HMAC-authenticated WebSocket transport, and the inbound->STT->agent->TTS->outbound pipeline.
  • Real environment tested: OpenClaw 2026.6.2 local gateway on Windows Server (Azure VM); voice-call provider msteams; agent microsoft-foundry/gpt-5.4; ElevenLabs realtime STT (scribe_v2_realtime, pcm_16000) + ElevenLabs TTS; an external Windows worker bridged a real inbound Microsoft Teams call.
  • Exact steps or command run after this patch:
    1. pnpm build
    2. node openclaw.mjs config validate -> prints Config valid
    3. node openclaw.mjs gateway run --force
    4. Placed a real Microsoft Teams call to the bot, spoke several turns out loud, heard the bot's spoken replies, then hung up.
  • Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output):
    (a) Gateway lifecycle log during the call (private identifiers redacted):
    MsteamsMediaStream listening port=8443 path=/voice/msteams/stream
    [voice-call] msteams realtime transcription provider: elevenlabs
    [voice-call] msteams TTS provider configured
    MsteamsMediaStream: connection open <callId>
    MsteamsProvider: session.start callId=<callId> threadId=<threadId> caller.aadId=<aadId>
    MsteamsMediaStream: connection closed <callId>
    
    (b) Agent session trajectory for the same call (sessionKey: voice:<redacted>, model microsoft-foundry/gpt-5.4) - the inbound caller audio was transcribed by ElevenLabs STT and answered by the agent; each {"spoken":...} reply is what was synthesized back over the bridge as TTS the caller heard:
    user  (STT): "How are you, man?"
    asst (spoken): "I'm doing well, man  -  thanks. What can I help you with?"
    user  (STT): "Tell me more about you."
    asst (spoken): "I'm your phone assistant in OpenClaw  -  calm, practical, and here to help with
                    questions, quick research, reminders, messages, and getting things done..."
    user  (STT): "Thank you. And, uh, what is your-"
    asst (spoken): "I'm an AI phone assistant running in OpenClaw. I don't have a body or a personal
                    life, but I can talk with you, look things up, help you plan..."
    user  (STT): "Mm-hmm"
    asst (spoken): "Yeah  -  kind of like a calm digital helper you can just talk to..."
    
  • Observed result after fix: full bidirectional audio round-trip on a live Teams call - caller speech -> ElevenLabs STT transcript -> gpt-5.4 agent reply -> ElevenLabs TTS played back to the caller, across multiple turns over ~70 s. The speech disfluencies ("uh", "Mm-hmm") and natural turn-taking confirm these are live ASR transcripts (not typed input); the {"spoken":...} payloads are exactly what the provider sends to TTS. Session lifecycle (connection open / session.start / connection closed) is verified in the gateway log.
  • What was not tested: outbound dialing (the provider is inbound-only and out of scope); sustained barge-in and high-concurrency load have not yet been captured under real calls.
  • Proof limitations or environment constraints: transcript/reply text is taken from the agent session trajectory because the gateway file log does not record transcript text at info level. Private identifiers (callId, threadId, caller aadId, sessionKey) are redacted.
  • Before evidence (optional but encouraged): before this patch, provider: "msteams" produced Unsupported voice-call provider: msteams.

After-fix proof: realtime delegation + recording-status gate (latest commits)

Captured on real inbound Microsoft Teams calls using the realtime (speech-to-speech) path (realtime.enabled, provider openai, toolPolicy: "owner"), with the recording gate at its default (requireRecordingStatus: true). Private identifiers redacted; call ids shortened.

  • Recording gate refuses, then opens. With no recording signal, asking the assistant to do work was refused out loud ("recording isn't active") and the gateway logged no recording.status. Once the external worker reports recording active on call establish, the gateway opens the gate:

    # delegation refused (no recording signal yet)
    MsteamsProvider: realtime session.start callId=0c005180... caller.aadId=<redacted>
    
    # next call: worker reports recording active -> gate opens
    MsteamsProvider: realtime session.start callId=2d005480... caller.aadId=<redacted>
    MsteamsProvider: recording.status 2d005480... = active (realtime)
    
  • Inline consult (openclaw_agent_consult). With the gate open, the caller asked a question; the model spoke the "working on it" filler and the OpenClaw agent ran and replied through the realtime model -- confirming realtime model -> openclaw_agent_consult -> agent -> spoken reply.

  • Background task (openclaw_agent_task) -> Teams chat delivery. The caller said "take care of local time in the background and ping me on Teams." The model invoked the task, spoke the ack ("I'll message you on Teams"), the caller hung up, the agent ran the task in the background, and delivered the result to the caller's Teams chat:

    MsteamsProvider: realtime session.start callId=22005480... caller.aadId=<redacted>
    MsteamsProvider: recording.status 22005480... = active (realtime)        # gate open
    [trace:embedded-run] runId=voice-realtime-task:22005480...               # background agent run (post-ack)
    sent proactive message                                                    # interim "checking now..."
    sent proactive message                                                    # final result to caller's Teams chat
    MsteamsMediaStream: connection closed 22005480...
    

    The two messages received in the caller's Teams chat:

    I'm checking the local machine time now and will send it here shortly.

    Local machine time: 2026-06-04 05:28:13 +00:00.

    The agent did real work (read the actual machine clock -- 05:28:13, matching the run time), not a canned reply.

  • Pre-recording transcripts are not retained: recordTranscript drops turns while the gate is closed, so the consult context only ever contains recording-active text (regression test in msteams-realtime.test.ts).


Tests and validation

Which commands did you run?

node scripts/run-vitest.mjs run \
  extensions/voice-call/src/providers/msteams.test.ts \
  extensions/voice-call/src/msteams-media-stream.test.ts
node openclaw.mjs config validate
pnpm build      # clean compile of the changed extension into dist

Result:

 Test Files  2 passed (2)
      Tests  18 passed (18)

node openclaw.mjs config validate -> Config valid.

What regression coverage was added or updated?

  • extensions/voice-call/src/providers/msteams.test.ts - provider lifecycle: session.start -> CallManager registration, transcript -> response, barge-in (assistant.cancel), playTts framing/pacing, hangup/teardown.
  • extensions/voice-call/src/msteams-media-stream.test.ts - WebSocket server: HMAC upgrade verification (accept/reject), replay-window rejection, session.start/session.end/audio.frame/ping parsing, outbound send/close, and inbound payload-cap rejection (oversized frame -> close 1009, added for GHSA-vw3h-q6xq-jjm5).

What failed before this fix, if known?

  • provider: "msteams" was rejected as an unsupported voice-call provider; there was no transport for Teams audio.

If no test was added, why not? N/A - unit tests added for both the provider and the transport.


Risk checklist

Did user-visible behavior change? Yes (additive, opt-in) - adds a new msteams provider option; existing Twilio/Telnyx/Plivo/mock behavior is unchanged. For msteams specifically, inboundPolicy defaults to a safe "allowlist" (Teams is inbound-first): no caller is accepted until the operator opts callers in (the allowlist accepts AAD object ids) or sets "open" explicitly.

Did config, environment, or migration behavior change? Yes - adds voice-call config under provider: "msteams" (msteams.{port,bindAddress,path,sharedSecret}, plus streaming and tts blocks). msteams-scoped config/validation behavior in config.ts:

  1. msteams is exempt from the fromNumber requirement (it never places outbound PSTN calls).
  2. inboundPolicy defaults to a safe "allowlist" for msteams when unset.
  3. Required transport: when provider: "msteams", validateProviderConfig now requires msteams.port and msteams.sharedSecret (so a misconfigured runtime fails validation instead of starting but never binding). msteams.path has a schema default (/voice/msteams/stream).
  4. SecretRef-compatible secret: msteams.sharedSecret accepts the same SecretInput shape as twilio.authToken (string or ${ENV}/secret ref), resolved via resolveMsteamsSharedSecret and registered in configContracts.secretInputs.
    No data migration.

Did security, auth, secrets, network, or tool execution behavior change? Yes - introduces a WebSocket server (operator-configured port) that accepts connections from the external worker.

What is the highest-risk area? The WebSocket transport (msteams-media-stream.ts) - it listens on a network port and accepts external connections carrying call audio.

How is that risk mitigated?

  • Loopback by default: the WS server binds 127.0.0.1 unless the operator sets msteams.bindAddress to a specific trusted-network interface - it is never exposed on all interfaces implicitly.
  • Every HTTP upgrade is authenticated with HMAC-SHA256 over timestamp + callId using a shared secret, compared in constant time (safeEqualSecret).
  • Replay protection: upgrades whose timestamp drifts beyond a configurable window (default 60 s) are rejected.
  • DoS bounds: 64 KB inbound payload cap; caps on total / pending / per-IP connections.
  • Inbound message bodies are validated with a strict zod discriminated union.
  • SecretRef secret: the shared secret is a SecretInput (supports ${ENV}/secret refs), required by config validation, and never logged.
  • Lifecycle-managed: the server is started as an awaited step of runtime init (a failed bind aborts startup and is cleaned up, rather than being swallowed) and stopped on runtime teardown (the port is released; no orphaned listener).

Current review state

What is the next action?

  • Maintainer decision on the product boundary (see below) and review of the WebSocket auth/transport.

What is still waiting on author, maintainer, CI, or external proof?

  • Maintainer guidance on whether msteams ships bundled in voice-call or as a separate optional plugin (see Product boundary).

Which bot or reviewer comments were addressed?

  • Bind-address control (P1): the WS listener now binds loopback by default and exposes msteams.bindAddress; it no longer implicitly binds all interfaces.
  • Required transport + SecretRef secret (P1): provider: "msteams" now fails validation without msteams.port + msteams.sharedSecret; the secret is SecretInput/SecretRef-compatible and registered in configContracts.secretInputs (so it no longer starts a runtime that cannot accept calls safely).
  • Lifecycle (P1): the WS server start is an awaited runtime-init step (failed binds surface and are cleaned up) and is stopped on teardown (no orphaned port). Moved out of the provider constructor's fire-and-forget start.
  • Live audio round-trip (P1): end-to-end caller->STT->agent->TTS->caller was captured on a real multi-turn Teams call (see Real behavior proof).
  • GitHub Advanced Security (GHSA-vw3h-q6xq-jjm5): 64 KB maxPayload on inbound frames; regression test (oversized frame -> close 1009).
  • Internal worker/product names and phased-task references were removed from comments for upstream.

Product boundary (maintainer decision)

VISION.md sets a high bar for bundling optional/commercial integrations, while the docs state voice-call owns call transport. This PR is written to be additive and opt-in and to keep that decision easy either way:

  • It adds a 5th provider behind provider: "msteams"; default behavior of Twilio/Telnyx/Plivo/mock is unchanged, and nothing activates unless an operator configures it.
  • It introduces no new runtime dependencies (reuses ws, zod, and existing plugin-sdk helpers) and no commercial SDK - Teams-specific Graph/Skype.Bots.Media code lives entirely in the out-of-repo Windows worker.
  • msteams-specific logic is isolated (providers/msteams.ts, msteams-media-stream.ts, msteams-tts.ts, msteams.runtime.ts) with only thin, guarded hooks in shared files, so it can be lifted into a separate optional plugin if maintainers prefer that over bundling.

Author is happy to follow maintainer preference on bundled-vs-external packaging.


Files changed (feature scope)

File Change
extensions/voice-call/src/providers/msteams.ts New msteams provider: explicit awaited start() (binds the WS server; replaces constructor fire-and-forget), inbound audio -> STT, response generation, TTS playback with paced PCM framing, barge-in, CallManager integration, stop() for teardown.
extensions/voice-call/src/providers/msteams.test.ts Provider unit tests (updated to await provider.start(); connect over 127.0.0.1).
extensions/voice-call/src/msteams-media-stream.ts HMAC-authenticated WebSocket server; binds loopback by default with optional bindAddress; session.start/end/audio.frame/ping protocol; 64 KB payload cap; outbound send/close.
extensions/voice-call/src/msteams-media-stream.test.ts Transport unit tests (connect over 127.0.0.1).
extensions/voice-call/src/msteams-tts.ts Teams PCM TTS helper.
extensions/voice-call/src/msteams.runtime.ts Resolves the realtime-transcription provider + telephony TTS and injects CallManager/response runtime for msteams.
extensions/voice-call/src/runtime.ts Resolves msteams provider with bindAddress + resolved sharedSecret; starts the WS server as an awaited runtime-init step and registers provider.stop() in the resource lifecycle (bind failures surface; port released on teardown).
extensions/voice-call/src/config.ts MsteamsConfigSchema (port/bindAddress/path(default)/sharedSecret as SecretInput); resolveMsteamsSharedSecret; msteams exempt from fromNumber; inboundPolicy defaults to a safe "allowlist"; requires msteams.port + msteams.sharedSecret.
extensions/voice-call/openclaw.plugin.json configSchema adds msteams.bindAddress and sharedSecret (string/object); registers msteams.sharedSecret in configContracts.secretInputs.

@openclaw-barnacle openclaw-barnacle Bot added channel: voice-call Channel integration: voice-call size: XL triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 3, 2026
@ahenawy
ahenawy marked this pull request as ready for review June 3, 2026 06:15
Comment thread extensions/voice-call/src/msteams-media-stream.ts Fixed
@clawsweeper

clawsweeper Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 19, 2026, 5:02 AM ET / 09:02 UTC.

Summary
Adds a Microsoft Teams msteams provider to the voice-call plugin with an HMAC WebSocket bridge, config/docs/schema updates, recording-gated streaming and realtime paths, Teams chat background-task delivery, and tests.

PR surface: Source +2191, Tests +1610, Docs +139. Total +3940 across 24 files.

Reproducibility: not applicable. as a bug reproduction; source inspection confirms current main rejects provider: "msteams", and the PR supplies live after-fix Teams-call proof for the new feature.

Review metrics: 2 noteworthy metrics.

  • Config/default surfaces: 5 added or expanded. The PR adds the msteams provider, msteams.* config, SecretRef sharedSecret, AAD-capable allowFrom, and a realtime echo option that need setup and upgrade review.
  • External bridge protocol: 1 new HMAC WebSocket protocol. The authenticated bridge and recording.status messages create an operator/security contract that unit tests inside this repo cannot fully settle.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

Risk before merge

  • [P1] Bundling a commercial Microsoft Teams voice provider inside voice-call needs maintainer product acceptance because VISION sets a high bar for optional integrations and prefers plugin/ClawHub ownership when current seams suffice.
  • [P1] The out-of-repo Windows worker owns Graph Calling, Skype media, and the recording.status signal, so the full security/compliance boundary cannot be reviewed from this repository alone.
  • [P1] The PR adds new provider/config/default surfaces, including a listening port, SecretRef shared secret, AAD allowlist support, and a safe allowlist default that operators must understand before upgrade or setup.
  • [P1] Realtime background tasks carry call context into a detached agent run and proactively deliver to Teams chat, which is a session-state and message-delivery policy decision even though the implementation is gated.
  • [P1] Open supersets feat(voice-call): Microsoft Teams provider — CVI voice/video calls #91438 and feat(msteams): Teams voice (CVI) + chat + governance integration #92081 now cover broader Teams/CVI scope, so maintainers need to choose a canonical landing path before parallel Teams branches drift further.

Maintainer options:

  1. Require maintainer product and security signoff (recommended)
    Before merge, maintainers should explicitly accept the bundled provider home, external worker trust boundary, and new config/default contract for Teams voice.
  2. Land only the voice foundation
    If maintainers want the smallest path, keep this PR as the Phase 1 voice-only branch and park the broader CVI supersets until the foundation ships.
  3. Move Teams voice out of core distribution
    If the VISION guardrail wins, close or pause the bundled PRs and ask for an external plugin/ClawHub path using any missing SDK seams as separate core work.

Next step before merge

  • [P2] The remaining blocker is a maintainer product/security decision about Teams provider ownership and external bridge trust, not a concrete ClawSweeper repair task.

Security
Needs attention: No discrete line-level security defect was confirmed, but the new bridge and external worker create a maintainer-owned security boundary that must be accepted before merge.

Review details

Best possible solution:

Have maintainers choose one canonical Teams direction and explicitly accept the bundled-vs-external provider home plus the external bridge trust contract before landing any Teams voice PR.

Do we have a high-confidence way to reproduce the issue?

Not applicable as a bug reproduction; source inspection confirms current main rejects provider: "msteams", and the PR supplies live after-fix Teams-call proof for the new feature.

Is this the best way to solve the issue?

Unclear as the best product solution: the patch is a plausible implementation, but VISION and the open supersets require maintainer choice between bundled voice foundation, broader CVI, or external plugin ownership.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against dba291ed3559.

Label changes

Label justifications:

  • P2: This is a normal-priority additive provider feature with meaningful user value but no current-main regression or emergency impact.
  • merge-risk: 🚨 compatibility: The PR adds a new provider/config/default setup surface and safe fail-closed validation that operators must configure correctly.
  • merge-risk: 🚨 session-state: Realtime background tasks derive session keys from Teams call identity and continue detached agent work after the call ends.
  • merge-risk: 🚨 security-boundary: The new HMAC bridge and out-of-repo Teams worker carry media, recording status, and proactive delivery authority across a new trust boundary.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The PR body and comments include copied live gateway logs, transcripts, and Teams chat delivery output from real Microsoft Teams calls after the patch.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body and comments include copied live gateway logs, transcripts, and Teams chat delivery output from real Microsoft Teams calls after the patch.
Evidence reviewed

PR surface:

Source +2191, Tests +1610, Docs +139. Total +3940 across 24 files.

View PR surface stats
Area Files Added Removed Net
Source 12 2214 23 +2191
Tests 8 1614 4 +1610
Docs 4 142 3 +139
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 24 3970 30 +3940

Security concerns:

  • [medium] Review the out-of-repo Teams worker trust boundary — extensions/voice-call/src/msteams-media-stream.ts:279
    The OpenClaw side verifies HMAC and gates on recording.status, but Graph Calling, Skype media, and the recording-status signal are emitted by the separate Windows worker that is not included in this repository.
    Confidence: 0.86

Acceptance criteria:

What I checked:

Likely related people:

  • steipete: Peter Steinberger appears in the voice-call history for restoring plugin parity and normalizing voice-call runtime defaults, and also in msteams extension boundary/refactor history. (role: voice-call and extension boundary contributor; confidence: high; commits: 42c17adb5e4d, 3087893ef968, 3126809cb06a; files: extensions/voice-call/src/config.ts, extensions/voice-call/src/runtime.ts, extensions/msteams)
  • vincentkoc: Vincent Koc is current-line blame on central voice-call config/runtime lines in this checkout and appears repeatedly in recent msteams refactor/test history. (role: recent area contributor; confidence: medium; commits: e172f64f3fcc, f6e99bd5141a, 875c3813aab2; files: extensions/voice-call/src/config.ts, extensions/voice-call/src/runtime.ts, extensions/msteams)
  • joshavant: Josh Avant authored a recent voice-call spoken-output and stream TTS regression fix in the runtime surface this PR extends. (role: adjacent voice-call runtime contributor; confidence: medium; commits: 3f7f2c8dc96e; files: extensions/voice-call/src)
  • shakkernerd: Shakker Nerd appears in the voice-call configuration history for environment-variable-backed settings resolution, which is relevant to the new provider config surface. (role: voice-call config contributor; confidence: medium; commits: d37df28319da; files: extensions/voice-call/src/config.ts)
  • sudie-codes: sudie-codes appears repeatedly in current msteams channel fixes around proactive sends, threading, media, SSO, and Teams message handling used by the PR's chat-delivery path. (role: msteams channel contributor; confidence: medium; commits: 2b5b58194b7e, 9edfefedf7bf, 38aa1edf76da; files: extensions/msteams)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label Jun 3, 2026
@ahenawy

ahenawy commented Jun 3, 2026

Copy link
Copy Markdown
Author

Addressed the review feedback:

  • GHSA-vw3h-q6xq-jjm5 (oversized WebSocket frames): the msteams media-stream WebSocketServer now sets a 64 KB maxPayload, so oversized inbound frames are rejected before JSON parsing. Added a regression test asserting an oversized frame closes the connection with code 1009. (commit c2379e53c3)
  • Real behavior proof: the PR body now uses the exact template fields, including redacted real-environment evidence of the full live audio round-trip on a Microsoft Teams call (see the later comment and the "Real behavior proof" section).

(Edited to fix character encoding.)

@openclaw-barnacle openclaw-barnacle Bot added the proof: supplied External PR includes structured after-fix real behavior proof. label Jun 3, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 3, 2026
@ahenawy

ahenawy commented Jun 3, 2026

Copy link
Copy Markdown
Author

Update: the full live Teams audio round-trip is now proven on a real call; the PR body "Real behavior proof" section is updated accordingly.

Captured from a real inbound Microsoft Teams call (OpenClaw 2026.6.2, provider msteams, agent microsoft-foundry/gpt-5.4, ElevenLabs scribe_v2_realtime STT + ElevenLabs TTS), private identifiers redacted.

Gateway lifecycle log:

MsteamsMediaStream listening port=8443 path=/voice/msteams/stream
[voice-call] msteams realtime transcription provider: elevenlabs
[voice-call] msteams TTS provider configured
MsteamsMediaStream: connection open <callId>
MsteamsProvider: session.start callId=<callId> threadId=<threadId> caller.aadId=<aadId>
MsteamsMediaStream: connection closed <callId>

Agent session trajectory for the same call (sessionKey: voice:<redacted>) - inbound caller speech transcribed by ElevenLabs STT, answered by the agent; each spoken reply is what was synthesized back as TTS the caller heard:

user  (STT): "How are you, man?"
asst (spoken): "I'm doing well, man - thanks. What can I help you with?"
user  (STT): "Tell me more about you."
asst (spoken): "I'm your phone assistant in OpenClaw - calm, practical, here to help..."
user  (STT): "Thank you. And, uh, what is your-"
asst (spoken): "I'm an AI phone assistant running in OpenClaw..."

The disfluencies ("uh", "Mm-hmm") confirm live ASR; the spoken payloads are exactly what the provider sends to TTS. This exercises the full path: inbound audio frames -> STT transcript -> agent reply -> outbound TTS, across multiple turns (~70 s).

(Edited to fix character encoding.)

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 3, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 3, 2026
@ahenawy

ahenawy commented Jun 3, 2026

Copy link
Copy Markdown
Author

P1 review points - addressed

Hardening pushed (commit 267f147536); PR body updated (Risk / Current review state / Product boundary).

  • bind-address (P1): the Teams WS server binds 127.0.0.1 by default; operators opt into a trusted-network interface via msteams.bindAddress. It no longer binds all interfaces implicitly.
  • required transport + SecretRef (P1): validateProviderConfig now fails when provider: "msteams" is set without msteams.port or msteams.sharedSecret, so a runtime cannot start and silently never bind. sharedSecret is SecretInput/SecretRef-compatible (resolved via resolveMsteamsSharedSecret, registered in configContracts.secretInputs).
  • runtime lifecycle (P1): the media server starts via an awaited provider.start() inside createVoiceCallRuntime (a failed bind aborts init and is cleaned up, instead of being swallowed in a fire-and-forget constructor call), and provider.stop() is registered with the runtime lifecycle so teardown releases the port.
  • full audio round-trip (P1): proven on a real multi-turn Teams call - redacted gateway lifecycle log + agent session trajectory (caller speech -> ElevenLabs STT -> gpt-5.4 reply -> ElevenLabs TTS). See the "Real behavior proof" section.
  • product boundary (P1): deferring to maintainers. msteams is structurally a transport provider like twilio/telnyx/plivo and reuses the shared STT/agent/TTS pipeline with no new commercial dependency (Teams Graph / Skype.Bots.Media lives in the out-of-repo worker). The msteams-specific logic is isolated, so it can ship as a separate optional plugin instead if you prefer - happy to follow your call on bundled-vs-plugin packaging.

Tests: 45 passing across provider + transport + config; pnpm build green; config validate -> Config valid.

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 3, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 3, 2026
@ahenawy
ahenawy requested a review from a team as a code owner June 3, 2026 07:42
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Jun 3, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jun 3, 2026
@ahenawy

ahenawy commented Jun 3, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough re-review, and for confirming the proof.

Two findings are genuinely maintainer-policy calls, so I would value your steer before building further:

  1. Product boundary (bundled voice-call vs. optional plugin). Happy either way - the msteams logic is already isolated (providers/msteams.ts, msteams-media-stream.ts, msteams-tts.ts, msteams.runtime.ts) behind thin guarded hooks, so lifting it into a separate optional plugin is low-friction. Please let me know which home you prefer before I invest more.

  2. Teams recording-status contract (Media Access API). Agreed this must gate persistence of media-derived transcripts. Proposed approach for your review: extend the bridge protocol with a recording-status signal from the external Windows worker (it owns the Graph updateRecordingStatus call), gate call.speech persistence + response generation on a confirmed "recording active" state on the OpenClaw side, and hold/drop transcripts until that state is confirmed. Since this spans the external-worker contract, I wanted to confirm the shape with you before implementing.

In parallel I am addressing the code findings that apply regardless of the boundary decision:

  • Connection caps: port the Twilio total / pending / per-IP guardrails to the Teams listener.
  • Fail-fast: reject provider: "msteams" when streaming.enabled is false or no STT provider resolves, before binding.
  • callId integrity: require session.start.callId to match the HMAC-authenticated URL callId.
  • Early audio: buffer caller audio until the STT session connects instead of dropping frames.
  • Docs: a Teams provider setup/security section (external worker, HMAC headers, bindAddress posture, required streaming/TTS, recording-status obligation, inbound-only).

I will push these shortly and follow your lead on the two policy items above.

@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 3, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 3, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 3, 2026
@ahenawy

ahenawy commented Jun 4, 2026

Copy link
Copy Markdown
Author

Closed the last author-side finding: msteams setup/status webhook-exposure

Pushed 0e8c105. doctor / voicecall status no longer reports a valid provider: "msteams" setup as needing public webhook exposure. msteams receives calls over its own authenticated bridge WebSocket listener (msteams.port/path), not the public provider-webhook plane, so it needs no publicUrl / tunnel / tailscale. (providerRequiresPublicWebhook already excluded it; resolveWebhookExposureStatus only exempted mock and fell through to a "set publicUrl…" failure for msteams.) Carrier webhook-plane providers (twilio/telnyx/plivo) still require exposure. Tests added for the msteams exemption, mock, and the still-required carrier path.

With this, all author-side code findings from the reviews are addressed, the live realtime-delegation + recording-gate proof is in the PR body, and CI is green (the earlier checks-fast-contracts-plugins-a red was an upstream testing.ts guardrail drift, restored to its canonical single-line form in 314c9ad). The remaining items are the maintainer decisions: bundled-vs-plugin product home, and accepting the config-default + external-worker security/availability boundary.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. and removed merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 4, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 4, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 4, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 4, 2026
@clawsweeper clawsweeper Bot added rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. proof: sufficient ClawSweeper judged the real behavior proof convincing. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 4, 2026
@ahenawy

ahenawy commented Jun 5, 2026

Copy link
Copy Markdown
Author

🗺️ Roadmap — where Microsoft Teams support is heading

This PR is Phase 1 of a larger Teams presence for OpenClaw. Sharing the full trajectory so the bundled-vs-plugin home and the worker/security-boundary decisions can be weighed against where this is going. Every phase reuses the same authenticated worker bridge and session model this PR establishes — so the security contract accepted here is the foundation the later phases build on.

Phase 1 — Inbound voice (this PR)
Teams 1:1 voice bridged to OpenClaw: streaming (STT → agent → TTS) and realtime (speech-to-speech) modes, HMAC + loopback media bridge, recording-status gate, and an AAD-aware allowlist with a safe default.

Phase 2 — Outbound voice · "call me when it's done" (built; follow-up PR)
OpenClaw places a Teams call back to the user to deliver a finished result. Two delivery modes on openclaw_agent_task: deliverVia: "message" (Teams chat) and deliverVia: "call" (a callback that speaks the result). Correlated by callId over the same authenticated bridge.

Phase 3 — Video calls 🎥 (in progress in the worker)
The Windows worker's app-hosted media session already declares a video socket alongside the audio socket and runs a combined audio+video frame player; flipping the video direction active and feeding it frames is the remaining wiring. Inbound video (camera / screen-share) lets the agent see what the caller shows it.

Phase 4 — OpenClaw animated avatar · lip-synced 🧑‍🚀 (active R&D)
Give OpenClaw a face on the call: an animated avatar whose mouth is lip-synced to the realtime / TTS audio, generated as video frames and enqueued alongside the audio in the same frame player (so picture and voice stay in lockstep). The assistant becomes a visible Teams participant — a presence, not just a voice.

The endgame is a full see-and-be-seen Teams assistant: it hears you, talks back, calls you when work is done, watches your screen, and shows a lip-synced face while doing it — all over one authenticated bridge.


Contributed by the Ports, Customs and Free Zone Corporation (PCFC), Dubai 🇦🇪 — building the "Virtual Employee" Teams assistant in production.

ahenawy and others added 10 commits June 6, 2026 08:28
Adds a 5th voice-call provider, `msteams`, that bridges Microsoft Teams call
audio into the existing voice-call machinery (CallManager, realtime
transcription, response generation, telephony TTS) via an external Windows
worker over an HMAC-authenticated WebSocket (inbound-only, PCM 16 kHz).

Transport & security:
- HMAC-SHA256 authenticated WebSocket upgrade (x-openclawteamsbridge-timestamp /
  -signature over `${ts}.${callId}`), constant-time compare, replay window.
- Binds loopback by default (msteams.bindAddress); 64 KB max payload; total +
  per-IP connection caps; pre-start idle reaper; callId integrity check.
- sharedSecret is SecretRef-compatible (registered in the credential matrix).

Two modes:
- Agent (streaming): caller audio -> realtime STT -> OpenClaw agent (tools) ->
  TTS. Early-audio buffered until STT connects; STT connect failure tears the
  call down; runtime fails fast if streaming is enabled without a usable STT.
- Realtime (speech-to-speech): caller bridged to a realtime voice provider, which
  delegates real work to the OpenClaw agent via openclaw_agent_consult (inline)
  and openclaw_agent_task (owner-policy background task that delivers the result
  to the caller's Teams chat). 16k<->24k resampling; barge-in.

Recording-status gate (Microsoft Media Access API): media-derived data is not
processed/persisted before Teams recording status is active. The streaming path
drops raw audio + transcripts; the realtime path refuses consult/task and does
not retain pre-recording transcripts. requireRecordingStatus (default true) is
operator-overridable.

setup/status exempts msteams from the public-webhook-exposure check (it uses the
bridge listener, not the webhook plane). Docs: voice-call README provider
section. Restores packages/plugin-sdk/src/testing.ts to its canonical single
re-export (an upstream docs pass had tripped the package-contract guardrail).

Tests cover the transport (HMAC/caps/recording protocol), provider audio loop,
realtime consult/async + recording gate, runtime fail-fast, and webhook-exposure
exemption.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…o option

Addresses the two author-side review findings on the inbound provider:

- [P1] Teams allowlists now match AAD callers. isAllowlistedCaller matches by
  phone number (digits) OR by exact caller id (case-insensitive), so a Teams
  aadId is no longer phone-normalized into a non-matching value. The msteams
  paths (realtime isInboundCallAllowed + streaming shouldAcceptInbound) pass the
  raw caller id; carrier paths still match by phone. Tests cover AAD-id matching.
- [P2] Wire the documented realtime echo option. realtime.suppressInputDuringPlayback
  is now a real config field (schema + manifest + threaded into the msteams
  realtime deps), matching the README; it was previously documented but only an
  internal default.

oxlint/oxfmt/tsgo prod+test clean; allowlist + webhook + config suites green;
base-config-schema unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Completes the allowlist fix. Runtime matching already accepted AAD caller ids,
but config + manifest validation still required E.164 for every allowFrom entry,
so `inboundPolicy: "allowlist"` was unusable for Teams (a GUID failed validation
before runtime ever saw it). allowFrom entries now accept an E.164 phone number
OR a Teams AAD object id (GUID) in both the Zod schema and the plugin manifest;
README documents the AAD-id form. Carrier providers are unchanged (E.164).

Tests cover the schema accepting phone + GUID (either case) and rejecting
non-E.164/non-GUID entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Addresses the reviewer's inbound-posture decision: msteams previously defaulted
inboundPolicy to "open", accepting every authenticated Teams caller out of the
box. It now defaults to an allowlist-oriented policy — with an empty allowFrom no
caller is accepted until the operator opts callers in (the allowlist accepts AAD
object ids) or sets inboundPolicy: "open" explicitly. No unsafe default; explicit
policies are honored unchanged. README + tests updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
[P3] Keep manifest provider metadata in sync — the provider enum includes
"msteams" but the human-readable description still only listed Twilio/Telnyx/
Plivo. Mention Microsoft Teams voice so the manifest metadata matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
[P1/P2] The public voice-call docs described only Twilio/Telnyx/Plivo/mock, so
the new msteams provider shipped undocumented. Add it to the summary and
provider list and add a "Microsoft Teams voice (msteams)" section covering the
external Windows-worker WebSocket bridge, streaming vs realtime modes, the safe
allowlist default + AAD-id allowFrom, the recording-status gate, and the
HMAC/loopback security model, linking to the extension README for the full
protocol.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ty in session keys

Two source-review defects:

- Socket-close teardown: a started Teams session that dropped without an explicit
  session.end frame (worker crash, network loss, hangup) only released local
  connection tracking — onSessionEnd never fired, so the provider/manager call
  record leaked until the stale-call reaper. The ws "close" handler now delivers
  onSessionEnd (reason "socket-closed") for a started-but-not-ended session,
  guarded against double-delivery when an explicit session.end preceded it.

- Session-key caller identity: when a Teams caller has no aadId, `from` fell back
  to the shared literal "teams", so every anonymous caller collided into one
  session key (cross-caller memory bleed under sessionScope: per-phone) and would
  match an allowlist as a generic caller. Fall back to a per-call-unique
  `teams:<callId>` instead; the AAD id is still preferred when present.

Tests cover abrupt-close onSessionEnd, no double-fire after session.end, no fire
before session.start, and the per-call-unique session key.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…arget

A realtime openclaw_agent_task delivers its result to the caller's Teams chat
(message tool -> user:<aadId>). When the bridge session has no caller.aadId
(anonymous caller), handleAsyncTask still acked "I'll message you on Teams" and
ran the task — but runAsyncTask had no delivery target, so the promised result
was silently dropped. Refuse the task up front when caller.aadId is absent,
returning a message that tells the model to offer to answer on the call instead.
Regression test covers the no-aadId refusal (no false delivery ack).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…rd Teams worker docs

- The two new socket-close tests used `(resolve) => setTimeout(resolve, 100)`,
  whose arrow body implicitly returns the timer handle — oxlint
  no-promise-executor-return. Use a block body so nothing is returned. This was
  failing both check-lint and check-additional-extension-bundled
  (lint:extensions:bundled).
- Docs: describe the Teams bridge as a "Windows-side bridge worker" rather than
  "an external Windows worker (not part of this repo)".

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… posture

Reviewer follow-up: the setup examples showed `inboundPolicy: "open"` as the
headline, which undercuts the safe default. The README and public-docs snippets
now use `inboundPolicy: "allowlist"` with a sample AAD `allowFrom` entry, and
note `"open"` only as the explicit accept-all alternative. Matches the code/
schema default (allowlist) and the now-corrected PR description.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@ahenawy

ahenawy commented Jun 19, 2026

Copy link
Copy Markdown
Author

Consolidating the Microsoft Teams CVI voice/video work into a single refreshed PR (rebased onto current upstream); the foundation PR #91438 stays open. Closing as superseded - its content/summary is carried into the consolidated PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: voice-call Channel integration: voice-call docs Improvements or additions to documentation feature: ✨ showcase ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: XL status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants