Skip to main content
The plugin (msteams_bridge) is the Python half of the Teams CVI for Hermes Agent. It hosts the HMAC WebSocket that StandIn connects to, runs the dialogue + perception, and emits avatar driver cues. Chat-plane features live in the existing Teams adapter.
This adds voice/video (CVI) on top of Hermes Agent’s existing Teams messaging - run both for chat + calls.

Requirements

  • A working Hermes Agent install (the host).
  • A StandIn subscription (standin.komaa.com, free package) - the hosted media bridge connecting to this plugin. See Architecture.
  • Realtime mode: an OpenAI / Azure OpenAI realtime key. Streaming mode: needs ffmpeg on PATH.

Install

Install into the same Python environment as Hermes so the hermes_agent.plugins entry-point is discovered:
The documents extra enables show_file document rendering (pypdfium2 + Pillow). Optional system tools: LibreOffice (soffice on PATH) for showing Office documents, ffmpeg for streaming-mode TTS decode. The show_web_page tool uses the host Hermes browser tools - no extra install. Or with pip:
Then enable the plugin by adding it to plugins.enabled in ~/.hermes/config.yaml (the configuration page shows the full block):
hermes plugins enable msteams_bridge works too when run interactively, but it may prompt for tool-override consent, so scripts and installers should write the YAML instead.

Verify

Show config + readiness - the status probe checks every Hermes surface the plugin depends on (vision, STT, TTS, chat delivery, browser, document rendering) and reports two levels: boundaries_ok (the contract surface exists) and operational_ok (providers/credentials/dependencies actually work). Anything missing is named explicitly, so a broken Hermes integration surfaces here and at serve startup rather than mid-call. External paths (Teams reachability, the StandIn endpoint, provider round trips) are not probed - those surface on the first real call. Tested against Hermes Agent 0.19.0 (PyPI floor) and the current 0.20.x line:
Run the bridge server (foreground):
serve hosts the calling lane: the media WebSocket on 9442, at /msteams/calling. Chat is a separate process on a separate port - the Hermes gateway’s own Teams platform - so a full connection needs both of these reachable: Calling alone gives you a bot that talks but cannot read chat; chat alone gives you chat with no calls.

Expose both endpoints

StandIn connects to your agent from the internet, so both paths must be reachable - a public host, or a tunnel. The mount commands, the probe recipes, and the values you register live in one place:

Expose your agent

The two mount commands for Hermes (9442 for voice, 3978 for chat), how to check a mount actually works, and what to paste into the StandIn dashboard.
Mount each path onto your public host with the target path repeated, and neither public URL carries a port. Register the resulting wss://<machine>.<tailnet>.ts.net/msteams/calling and https://<machine>.<tailnet>.ts.net/api/messages (with the same connection secret) on your connection in the dashboard, then place a Teams call to your bot.
Probing the voice path by hand? Pass --http1.1. Through a funnel, curl without it sends a plain GET and gets a 404 that looks exactly like a missing mount. And on Hermes the route is /msteams/calling/{call_id}, so a bare /msteams/calling legitimately 404s even when the bridge is healthy. Add any segment and a live bridge answers 401. Full details on Expose your agent.
Used the one-line installer? Check both lanes are mounted. A current run provisions chat alongside voice on Tailscale and prints the commands to run yourself on ngrok, cloudflared or devtunnel. An older run mounted voice only on Hermes: no /api/messages rule, an empty chat URL at pairing, no chat step printed, and the run still reported success. Voice is unaffected either way, which is exactly why this went unnoticed. Confirm both public paths with tailscale funnel status before you test, and if chat is missing, add the mount and set the chat URL on your connection: Voice works, chat is silent.
Because both lanes ride path routing on 443, the 8443 and 10000 funnel slots stay free - so a second runtime (the OpenClaw plugin, say) can share the same machine. Register each runtime as its own connection with its own secret.
On a StandIn Managed Bot connection the chat lane is the plugin’s own /msteams/messages listener on 9444 rather than /api/messages on 3978, and the one-line installer sets both lanes up for you with --connection managed --runtime hermes and the connection secret from the dashboard: Quickstart: StandIn Managed Bot.
The bot answers but does nothing until recording starts. require_recording_status defaults to true, so the plugin engages only once Teams reports recording active - which is why a call can connect and the agent stay silent or refuse to act. Turn recording on in the call, or set require_recording_status: false. What the agent then does with a request - replying, posting a message, running a skill - is Hermes Agent’s own logic and configuration, not the bridge’s.
Next: the full configuration reference - config.yaml + .env, realtime backend, and Graph permissions.

Source

PyPI

Configuration

Gateway-managed mode (one process)

With the msteams_bridge platform enabled in the gateway config, hermes gateway run hosts the voice bridge itself - no separate serve process. Two layers must both be on:
  1. the plugin: msteams_bridge under plugins.enabled in config.yaml;
  2. the platform: a msteams_bridge platform entry enabled in the gateway configuration (same place as your Teams chat platform).
The complete config.yaml shape:
In this mode, streaming-mode utterances run as real gateway agent turns (sessions, authorization, approvals), and scheduled jobs can deliver by voice (deliver=msteams_bridge - the bot calls you and speaks the result). If a standalone hermes msteams-bridge serve is already bound to the port, the adapter fails loudly instead of splitting traffic - run one or the other. Using the StandIn one-line installer? Set STANDIN_GATEWAY_MODE=1 in the environment before running it: the installer then writes both layers above, stops any standalone bridge, and restarts the gateway instead of launching serve. hermes msteams-bridge smoke is an offline install check: it proves config resolution, the HMAC handshake, wire decode, and session lifecycle against an ephemeral local listener. It does not test the configured listener, gateway platform activation, Teams connectivity, provider credentials, or the public tunnel - the first real call is the test for those.