Skip to content

Configuration Reference

Every setting the plugin reads, with its config.yaml key, environment variable, default, and meaning. All values match the code in config.py and realtime/openai_client.py.

Values are resolved in priority order:

  1. The plugins.entries.msteams_bridge.config block in config.yaml.
  2. Environment variables (typically in ~/.hermes/.env).
  3. Safe defaults.

The recommended pattern: keep secrets in .env and reference them from config.yaml with ${VAR} (Hermes’s loader expands them). The plugin ships no config of its own. Secrets are never logged.

plugins:
enabled:
- msteams_bridge
entries:
msteams_bridge:
config:
# ONE secret from the StandIn portal, covering calling AND messages. `shared_secret` is the
# per-lane CALLING override, for BYO deployments and split-key setups - not the starting point.
secret: ${MSTEAMS_BRIDGE_SECRET}
host: 127.0.0.1
calling_port: 9442
messages_port: 9444
# ...bridge keys below...
realtime:
# ...realtime keys below...

Set these when StandIn provides the Teams bot (installed from the Teams Store) rather than you running your own Azure bot. One secret covers calls and chat; per-lane calling_secret/messages_secret override it. There is no separate enable flag.

SettingEnvDefaultWhat it does
secretMSTEAMS_BRIDGE_SECRET-The connection secret, covering BOTH lanes. This is the one value the StandIn portal gives you.
calling_secretMSTEAMS_BRIDGE_CALLING_SECRETfalls back to secretPer-lane override for CALLING only. shared_secret is the older name for it.
messages_secretMSTEAMS_BRIDGE_MESSAGES_SECRETfalls back to secretPer-lane override for MESSAGES only. Set both per-lane keys for a split-key deployment, where neither key can sign for the other and they rotate independently.
messages_portMSTEAMS_BRIDGE_MANAGED_BOT_PORT9444HTTP port the StandIn gateway POSTs inbound messages to. Voice uses calling_port (9442).
messages_pathMSTEAMS_BRIDGE_MANAGED_BOT_PATH/msteams/messagesPath the gateway posts to.
gateway_reply_endpointMSTEAMS_BRIDGE_MANAGED_BOT_GATEWAY_REPLY_URLStandIn’s /api/chat/replyWhere replies are posted back.
hostMSTEAMS_BRIDGE_HOST127.0.0.1Shared with the voice lane - one machine, one interface. Loopback by default: the documented posture is a tunnel that terminates TLS publicly and proxies to loopback, so no port is exposed on your LAN. Set it to your tailnet/VPN address if the gateway reaches the agent directly.
transcribe_voice_messagesMSTEAMS_BRIDGE_TRANSCRIBE_VOICE_MESSAGESfalseTranscribe inbound Teams voice messages and fold the words into the agent’s turn, so “listen to this and tell me what you think” is a question it can answer instead of a filename it can only read back. Off by default because it costs money: every clip is an STT call and a voice note can run for minutes. Uses whatever stt.provider you already configured (local Whisper, Groq, OpenAI, Voxtral, Grok, or a custom command).

One agent instance serves ONE StandIn connection - the chat secret belongs to a single tenant binding. Run a second instance for a second organization; never share a secret across tenants.

config.yaml keyEnv varDefaultMeaning
shared_secretMSTEAMS_BRIDGE_SHARED_SECRET"" (unset)HMAC secret shared with StandIn. Required - with no secret the bridge won’t start. Must equal the value paired in StandIn.
hostMSTEAMS_BRIDGE_HOST127.0.0.1Bind address for the local WebSocket server. Non-loopback binds are warned about (they expose the secret).
calling_port (port still accepted)MSTEAMS_BRIDGE_PORT9442Bind port. StandIn dials ws://host:port/msteams/calling/{callId}.
path(config only)/msteams/callingURL path prefix StandIn connects to. Rarely changed.
hmac_window_msMSTEAMS_BRIDGE_HMAC_WINDOW_MS60000Clock-skew / replay window for the HMAC handshake, in milliseconds (±60 s).
max_call_duration_sMSTEAMS_BRIDGE_MAX_CALL_DURATION_S0.0Hard wall-clock cap on a single call, in seconds. 0 = unlimited. A wedged/never-ending call is torn down once exceeded.
require_recording_statusMSTEAMS_BRIDGE_REQUIRE_RECORDING_STATUStrueGate all media processing until Teams recording is active. Recommended on for compliance.
worker_base_urlMSTEAMS_BRIDGE_WORKER_BASE_URLhttp://127.0.0.1:9440Loopback HTTP endpoint StandIn exposes for outbound “call me back”. See Outbound Calls.
allow_remote_workerMSTEAMS_BRIDGE_ALLOW_REMOTE_WORKERfalsePermit an outbound place-call to a non-loopback worker_base_url. Off by default (SSRF guard - the secret would be sent to that host).
tenant_idMSTEAMS_BRIDGE_TENANT_ID (falls back to TEAMS_TENANT_ID)""Default Azure AD tenant for outbound calls.
allowlistMSTEAMS_BRIDGE_ALLOWLIST (falls back to TEAMS_ALLOWED_USERS)() (empty)Comma-separated caller AAD object ids allowed to call. Empty = deny ALL inbound callers unless allow_all is set.
allow_allMSTEAMS_BRIDGE_ALLOW_ALLfalseExplicit opt-in to accept any inbound caller when the allowlist is empty. Deny-by-default otherwise.
allowlist_allow_namesMSTEAMS_BRIDGE_ALLOWLIST_ALLOW_NAMESfalseAlso match the allowlist against caller display names (weaker / spoofable). Off by default.
session_scopeMSTEAMS_BRIDGE_SESSION_SCOPEper-callAgent memory continuity: per-call (fresh each call), per-thread (keyed by Teams thread), or per-aad (keyed by caller AAD id).
wake_phrasesMSTEAMS_BRIDGE_WAKE_PHRASESassistant, hermesGroup-call wake phrases - in a meeting the agent speaks only when addressed by one of these.
meeting_recapMSTEAMS_BRIDGE_MEETING_RECAPfalsePost end-of-call meeting minutes to the Teams chat.
share_point_site_idTEAMS_SHAREPOINT_SITE_ID""Optional; reserved for a future large-file SharePoint delivery path. The minutes .docx file card itself needs no SharePoint: it rides the Bot Framework attachment contract using the bot credentials (TEAMS_CLIENT_ID/SECRET/TENANT_ID).
max_vision_per_minuteMSTEAMS_BRIDGE_MAX_VISION_PER_MINUTE30Per-call vision spend cap across look_at_screen + ambient push. 0 = unlimited.

Internal defaults (not currently config-driven)

Section titled “Internal defaults (not currently config-driven)”

These have sensible fixed defaults and are not exposed as config keys today:

FieldDefaultMeaning
max_connections64Global concurrent-connection cap (DoS guard).
max_connections_per_ip8Per-IP concurrent-connection cap.
pre_start_timeout_s10.0A connection that doesn’t send session.start within this window is reaped.
MAX_CLIP_BYTES16 MiBByte cap per inbound voice message. Larger than an image gets: a voice note is minutes of audio.
MAX_CLIPS_PER_MESSAGE2Voice messages transcribed from one inbound message. Deliberately tight - each one is a paid STT call.
FETCH_TIMEOUT_S20.0Fetch budget per voice clip.

Voice-clip fetching is additionally pinned to the origin of gateway_reply_endpoint, refuses redirects, and aborts a body mid-read once it exceeds the cap. These are not tunable: an operator has no information with which to set an SSRF guard, and every value they could get wrong opens a fetch or costs money.

These live under plugins.entries.msteams_bridge.config.realtime (or the matching env vars) and configure the OpenAI/Azure Realtime speech-to-speech engine. Only used by --handler realtime.

config.yaml key (under realtime:)Env varDefaultMeaning
backendMSTEAMS_BRIDGE_REALTIME_BACKEND(auto - see below)openai or azure.
api_keyMSTEAMS_BRIDGE_REALTIME_API_KEY(see fallbacks)Provider key. OpenAI falls back to OPENAI_API_KEY; Azure falls back to AZURE_OPENAI_API_KEY then AZURE_FOUNDRY_API_KEY.
modelMSTEAMS_BRIDGE_REALTIME_MODELgpt-realtimeOpenAI realtime model. (On Azure the deployment name is used as the model.)
azure_endpointMSTEAMS_BRIDGE_AZURE_ENDPOINT""Azure OpenAI resource endpoint. Setting this auto-selects the Azure backend.
azure_deploymentMSTEAMS_BRIDGE_AZURE_DEPLOYMENT""Azure realtime deployment name (e.g. gpt-realtime).
azure_api_versionMSTEAMS_BRIDGE_AZURE_API_VERSION2024-10-01-previewAzure realtime API version.
urlMSTEAMS_BRIDGE_REALTIME_URL""Explicit Realtime WebSocket URL override. An *.azure.com URL auto-selects Azure.
voiceMSTEAMS_BRIDGE_REALTIME_VOICEalloyRealtime voice name (e.g. cedar).
instructionsMSTEAMS_BRIDGE_REALTIME_INSTRUCTIONS(built-in prompt)System prompt for the voice assistant. The default keeps replies brief and delegates real work to the agent.
vad_thresholdMSTEAMS_BRIDGE_VAD_THRESHOLD0.5Server-VAD activation threshold.
prefix_padding_msMSTEAMS_BRIDGE_PREFIX_PADDING_MS300Audio kept before detected speech start, in ms.
silence_duration_msMSTEAMS_BRIDGE_SILENCE_DURATION_MS500Trailing silence that ends a turn, in ms.
input_transcribe_modelMSTEAMS_BRIDGE_INPUT_TRANSCRIBE_MODELwhisper-1Model that transcribes caller audio (for wake words / verbal interrupts). Set to none / off / disabled (or empty) to turn off - VAD barge-in still works.
bilingualMSTEAMS_BRIDGE_BILINGUALfalsePin the model to detect/mirror the caller’s language (Arabic/English) and translate on request.

Azure is chosen when any of these is true; otherwise OpenAI (bearer auth):

  • backend: azure (or MSTEAMS_BRIDGE_REALTIME_BACKEND=azure), or
  • an azure_endpoint is set, or
  • the explicit url contains azure.com.

On Azure, the base URL is built as wss://<endpoint>/openai/realtime?api-version=<ver>&deployment=<deployment> and the api-key header is used instead of bearer auth.

realtime:
backend: openai
model: gpt-realtime
voice: alloy
api_key: ${OPENAI_API_KEY}
vad_threshold: 0.5
prefix_padding_ms: 300
silence_duration_ms: 500
bilingual: false
realtime:
backend: azure
azure_endpoint: https://<your-azure-resource>.cognitiveservices.azure.com
azure_deployment: gpt-realtime
azure_api_version: 2025-04-01-preview
voice: cedar
api_key: ${AZURE_FOUNDRY_API_KEY}

You can run entirely from environment variables (no config.yaml block):

Terminal window
MSTEAMS_BRIDGE_SHARED_SECRET=... # must equal the value paired in StandIn
MSTEAMS_BRIDGE_HOST=127.0.0.1
MSTEAMS_BRIDGE_PORT=9442
MSTEAMS_BRIDGE_SESSION_SCOPE=per-thread
MSTEAMS_BRIDGE_WAKE_PHRASES=assistant,hermes
# Realtime (Azure):
MSTEAMS_BRIDGE_REALTIME_BACKEND=azure
MSTEAMS_BRIDGE_AZURE_ENDPOINT=https://<your-azure-resource>.cognitiveservices.azure.com
MSTEAMS_BRIDGE_AZURE_DEPLOYMENT=gpt-realtime
MSTEAMS_BRIDGE_AZURE_API_VERSION=2025-04-01-preview
MSTEAMS_BRIDGE_REALTIME_VOICE=cedar
AZURE_FOUNDRY_API_KEY=...

config.yaml wins wherever both a key and its env var are set.