> ## Documentation Index
> Fetch the complete documentation index at: https://docs.komaa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> Configure teams_call via config.yaml + .env, pick a realtime backend, and run the bridge.

Two sources are supported: **`config.yaml` takes precedence, `.env` is the fallback.** The recommended
pattern keeps **secrets in `.env`** and references them from `config.yaml` with `${VAR}` (the loader
expands them), so config lives in one declarative file.

Most `config.yaml` keys have a matching `TEAMS_CALL_*` env var (e.g.
`realtime.azure_endpoint` ↔ `TEAMS_CALL_AZURE_ENDPOINT`); `config.yaml` wins where
both are set. Exceptions: `path` is config-only, and the SharePoint site id uses
`TEAMS_SHAREPOINT_SITE_ID` (shared with the chat plane).

## config.yaml

`%LOCALAPPDATA%\hermes\config.yaml` (Windows) / `~/.hermes/config.yaml`:

```yaml theme={null}
plugins:
  enabled:
    - teams_call
  entries:
    teams_call:
      config:
        shared_secret: ${TEAMS_CALL_SHARED_SECRET}   # must equal the secret set in StandIn
        host: 127.0.0.1
        port: 8443
        # Post meeting minutes to the chat with the .docx as a native file
        # card - no SharePoint needed; it rides the Bot Framework attachment
        # contract using the chat plane's bot credentials.
        meeting_recap: true
        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}           # secret stays in .env
          vad_threshold: 0.5
          prefix_padding_ms: 300
          silence_duration_ms: 500
```

## .env

`%LOCALAPPDATA%\hermes\.env` / `~/.hermes/.env` - the secret store (used directly, or referenced above).
A fully env-only setup works too:

```bash theme={null}
TEAMS_CALL_SHARED_SECRET=...        # must equal the secret set in StandIn
AZURE_FOUNDRY_API_KEY=...            # realtime key (also used by the gateway)
# fully env-only is fine too:
TEAMS_CALL_HOST=127.0.0.1
TEAMS_CALL_PORT=8443
TEAMS_CALL_REALTIME_BACKEND=azure
TEAMS_CALL_AZURE_ENDPOINT=https://<your-azure-resource>.cognitiveservices.azure.com
TEAMS_CALL_AZURE_DEPLOYMENT=gpt-realtime
TEAMS_CALL_AZURE_API_VERSION=2025-04-01-preview
TEAMS_CALL_REALTIME_VOICE=cedar
```

<Tip>
  `backend: openai` uses public OpenAI instead of Azure - set the OpenAI key in place of the Azure
  endpoint/deployment fields.
</Tip>

<Warning>
  For the **hosted StandIn** bridge to connect, bind a reachable address: set `host: 0.0.0.0` (or
  keep `127.0.0.1` behind a tunnel that forwards to it) - the default loopback bind is only reachable
  from the same machine. `allow_remote_worker` has **no effect on inbound connections**; it only
  governs where *outbound* "call me back" requests may be sent (see the key reference below).
</Warning>

## Key reference

Hermes implements the same feature set as the OpenClaw plugin; keys are `snake_case` and each has a
matching `TEAMS_CALL_*` env var.

| Key                                                                                | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shared_secret`                                                                    | HMAC secret - **must match the secret set in StandIn** (keep in `.env`)                                                                                                                                                                                                                                                                                                                                                     |
| `host` / `port` / `path`                                                           | media WebSocket the StandIn bridge connects to (default `127.0.0.1` : `8443`, `/voice/msteams/stream`)                                                                                                                                                                                                                                                                                                                      |
| `allow_remote_worker`                                                              | SSRF guard for **outbound** "call me back": permit dialing a non-loopback `worker_base_url` (default `false` = loopback only). Does not affect inbound bridge connections                                                                                                                                                                                                                                                   |
| `require_recording_status`                                                         | only engage once Teams reports recording active (default `true`)                                                                                                                                                                                                                                                                                                                                                            |
| `allowlist` / `allowlist_allow_names` / `allow_all`                                | inbound caller policy (see [below](#inbound-policy-voice-hermes-vs-openclaw)): allowlisted caller AAD ids; **empty = deny all** inbound callers unless `allow_all: true` (`TEAMS_CALL_ALLOW_ALL`) explicitly opens it. `allowlist_allow_names` additionally matches display names (weaker; spoofable). When empty, `allowlist` inherits the chat plane's `TEAMS_ALLOWED_USERS`, so voice + chat can share one AAD allowlist |
| `wake_phrases`                                                                     | group-call "speak only when addressed" wake words (default `assistant`, `hermes`)                                                                                                                                                                                                                                                                                                                                           |
| `watch_browser_tasks`                                                              | opt-in "watch it work" (default `false`): while a background task runs, the tile shows live frames of the agent's own browser session instead of the plain progress panel. Off by default because each capture can invoke the auxiliary vision model (`TEAMS_CALL_WATCH_BROWSER_TASKS`)                                                                                                                                     |
| `max_vision_per_minute`                                                            | vision spend cap (default `30`, `0` = unlimited)                                                                                                                                                                                                                                                                                                                                                                            |
| `session_scope`                                                                    | `per-call` \| `per-thread` \| `per-aad` agent-memory scope                                                                                                                                                                                                                                                                                                                                                                  |
| `meeting_recap`                                                                    | post end-of-meeting minutes (default `false`)                                                                                                                                                                                                                                                                                                                                                                               |
| `show_file_root`                                                                   | root directory `show_file` may display from (workspace containment; default `<hermes home>/workspace/teams_call_show`)                                                                                                                                                                                                                                                                                                      |
| `languages`                                                                        | (realtime block) list like `[en, fr, de, ar]` - reply in the caller's language when configured; empty = auto-detect and mirror. `bilingual` remains a deprecated alias for `[ar, en]`                                                                                                                                                                                                                                       |
| `share_point_site_id`                                                              | optional; reserved for a future large-file SharePoint path. The minutes `.docx` file card itself needs **no** SharePoint - it rides the Bot Framework attachment contract using the chat plane's `TEAMS_CLIENT_ID`/`SECRET`/`TENANT_ID`                                                                                                                                                                                     |
| `worker_base_url` / `tenant_id`                                                    | outbound "call me back" - StandIn's outbound endpoint + your tenant                                                                                                                                                                                                                                                                                                                                                         |
| `realtime.{backend,azure_endpoint,azure_deployment,voice,api_key,vad_threshold,…}` | realtime backend (`azure` \| `openai`); key stays in `.env`                                                                                                                                                                                                                                                                                                                                                                 |
| `hmac_window_ms` / `max_connections` / `max_connections_per_ip`                    | handshake window + connection caps                                                                                                                                                                                                                                                                                                                                                                                          |

## Inbound policy (voice): Hermes vs OpenClaw

Hermes has **no `inboundPolicy` key** and **no pairing mode for calls** - the `allowlist` **is** the
inbound policy:

* `allowlist` set → only those AAD object ids may call (add `allowlist_allow_names: true` to also
  match display names, which is weaker).
* `allowlist` empty → **all inbound callers are denied**, unless you explicitly opt out with
  `allow_all: true` (env: `TEAMS_CALL_ALLOW_ALL`). An unset allowlist never means "open to all".
* When `allowlist` is empty, it inherits `TEAMS_ALLOWED_USERS` (the chat plane's allowlist), so one
  AAD list can cover both voice and chat.

The `openclaw pairing approve msteams <CODE>` steps you will see in the Quickstart and Teams app
pages are **OpenClaw-only**. The Hermes equivalents are: edit `allowlist` for **voice** callers, and
for Teams **chat** DMs (handled by the Hermes gateway's Teams platform, not `teams_call`) either
approve the pairing code the bot sends an unknown sender (`hermes pairing approve teams <CODE>`) or
set `TEAMS_ALLOWED_USERS` - see
[the bot ignores Teams chat](/troubleshooting#the-bot-answers-calls-but-ignores-teams-chat-dm-pairing).

## Run

Show config + readiness:

```bash theme={null}
hermes teams-call status
```

Serve the bridge (use `--handler streaming` for streaming mode, which needs ffmpeg):

```bash theme={null}
hermes teams-call serve --handler realtime
```

Or run it standalone:

```bash theme={null}
python -m hermes_msteams_bridge.bridge_server
```

Register your **Agent voice URL** (`wss://<your-public-host>:8443/voice/msteams/stream` via your tunnel; local bind `ws://<host>:8443/...`) and a matching shared
secret in your StandIn dashboard.

## Microsoft Graph permissions

The bot's Azure AD app needs admin-consented **application** permissions - see the canonical
[Graph permissions table](/teams/azure-bot#8-grant-graph-permissions) on the Create the Azure bot page.

<Warning>
  The `shared_secret` **must byte-match** the secret set in StandIn, or the HMAC handshake fails and no
  call connects.
</Warning>

## Gateway-managed mode (two layers)

To have `hermes gateway run` host the voice bridge itself (one process, no
separate `serve`), enable both the plugin and the platform:

```yaml theme={null}
plugins:
  enabled:
    - teams_call          # layer 1: the plugin
  entries:
    teams_call:
      config:
        shared_secret: ${TEAMS_CALL_SHARED_SECRET}
platforms:
  teams_call:
    enabled: true         # layer 2: the gateway platform
```

In this mode scheduled jobs can deliver **by voice**: a cron job with
`deliver=teams_call` places a call and speaks the result. Set
`TEAMS_CALL_HOME_AAD` to the AAD object id the gateway should call for those
deliveries (the callee must also be on the `allowlist`). Run gateway mode
**or** standalone `serve`, never both - the second owner fails loudly on the
port bind.

## Voice & language tuning lives in Hermes

STT and TTS provider selection, voices, and language hints are **host
configuration**, not plugin keys: set Hermes's own `stt:` and `tts:` blocks in
`~/.hermes/config.yaml` (see the Hermes TTS and voice-mode feature docs). The
plugin inherits them - streaming mode dispatches the host's `text_to_speech`
tool and transcription chain, so all Hermes STT/TTS providers apply unchanged.
The call also answers with the host's `SOUL.md` identity and knows the host's
installed skills (chat/call parity).
