hermes-agent/website/docs/user-guide/messaging/msgraph-webhook.md
Teknium 2d099fed1e
docs: deep audit — registry drift, stale claims, 2-week PR coverage, dashboard screenshot (#40952)
Full-corpus correctness audit of the hand-written docs against the codebase,
plus a 2-week merged-PR coverage sweep and one live dashboard screenshot.

Correctness (verified against COMMAND_REGISTRY / PROVIDER_REGISTRY / TOOLSETS /
tools.registry / DEFAULT_CONFIG / source):
- reference: add /version slash command, context_engine toolset, openai-api +
  novita-ai to --provider; fix tool count 64->71; model_catalog ttl 24->1;
  add profile describe to summary table; add real provider env vars
  (LM_API_KEY/LM_BASE_URL, KIMI_CODING_API_KEY, ALIBABA_CODING_PLAN_*,
  ANTHROPIC_BASE_URL, COPILOT_API_BASE_URL); fix faq "Windows: not natively".
- user-guide: fix broken `hermes -w -q` (->-z) and `hermes logs --tail` (->-f);
  language list 8->16; aux slots 8->11; docker separate-dashboard claim;
  _SECURITY_ARGS -> _BASE_SECURITY_ARGS.
- features: curator prune_builtins truth + missing CLI verbs; codex-runtime aux
  keys (context_compression->compression, vision_detect->vision); kanban
  terminate endpoint + promote/reassign/schedule/diagnostics/edit + per-profile
  cap; mcp mTLS (client_cert/client_key); built-in-plugins nemo_relay +
  teams_pipeline; api-server run approval endpoint; computer-use frontmatter.
- features N-Z + integrations: StepFun step-3-mini->step-3.5-flash; web-search
  backends 4->8; tool-gateway image-model IDs; voice-mode STT/TTS enums; remove
  phantom `rl` toolset; nous-portal status subcommand.
- messaging: WeCom typing/streaming cols; telegram transport default edit->auto;
  sms host default; simplex/ntfy `gateway setup` + pairing approve; line
  smart-chunking; matrix MATRIX_DM_AUTO_THREAD.
- developer-guide: build-a-plugin code examples (register_command signature,
  ContextEngine/ImageGenProvider/MemoryProvider ABCs); model-provider-plugin
  entry-point group hermes.plugins->hermes_agent.plugins; PLUGIN.yaml->plugin.yaml;
  agent-loop stale LOC; web-search-provider phantom crawl().

PR coverage (2-week window, 149 feat PRs):
- desktop.md refreshed for ~15 shipped features (zh-Hans switcher, rebindable
  shortcuts + zoom + Cmd+K, status-bar model picker + YOLO toggle, session-by-id
  + archive, multi-profile concurrent + cross-profile @session, composer history,
  Providers pane, per-profile remote hosts, Grok OAuth, aux-pin warning).
- configuration.md gateway-streaming default corrected to per-platform.
- tool-gateway.md free tool pool entitlement note.

Media:
- New /img/dashboard/admin-config.png — live dashboard Config admin page
  (captured from a clean profile, no secrets/personalization).
2026-06-07 01:39:06 -07:00

142 lines
8.1 KiB
Markdown

---
sidebar_position: 23
title: "Microsoft Graph Webhook Listener"
description: "Receive Microsoft Graph change notifications (meetings, calendar, chat, etc.) in Hermes"
---
# Microsoft Graph Webhook Listener
The `msgraph_webhook` gateway platform is an inbound event listener. It's how Hermes receives **change notifications** from Microsoft Graph — "a Teams meeting ended," "a new message landed in this chat," "this calendar event was updated." Different from the `teams` platform (which is a chat bot users type to) — this one is M365 telling Hermes something happened, not a person.
Right now the primary consumer is the Teams meeting summary pipeline: Graph notifies when a meeting produces a transcript, the pipeline fetches it, and Hermes posts a summary back into Teams. Other Graph resources (`/chats/.../messages`, `/users/.../events`) use the same listener — the pipeline consumers land with their own PRs.
## Prerequisites
- Microsoft Graph application credentials — [Register a Microsoft Graph Application](/guides/microsoft-graph-app-registration)
- A **public HTTPS URL** that Microsoft Graph can reach (Graph does not call private endpoints). A dev tunnel works for testing; production needs a real domain with a valid certificate.
- A strong shared secret to use as the `clientState` value. Generate with `openssl rand -hex 32` and put it in `~/.hermes/.env` as `MSGRAPH_WEBHOOK_CLIENT_STATE`.
## Quick Start
Minimum `~/.hermes/config.yaml`:
```yaml
platforms:
msgraph_webhook:
enabled: true
extra:
host: 127.0.0.1
port: 8646
client_state: "replace-with-a-strong-secret"
accepted_resources:
- "communications/onlineMeetings"
```
Or via env vars in `~/.hermes/.env` (auto-merged on startup):
```bash
MSGRAPH_WEBHOOK_ENABLED=true
MSGRAPH_WEBHOOK_PORT=8646
MSGRAPH_WEBHOOK_CLIENT_STATE=<generate-with-openssl-rand-hex-32>
MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES=communications/onlineMeetings
```
Note: the bind host is read from `extra.host` in `config.yaml` (see the example above); there is no `MSGRAPH_WEBHOOK_HOST` env-var override.
Start the gateway: `hermes gateway run`. The listener exposes:
- `POST /msgraph/webhook` — change notifications from Graph
- `GET /msgraph/webhook?validationToken=...` — Graph subscription validation handshake
- `GET /health` — readiness probe with accepted/duplicate counters
Expose the listener publicly (reverse proxy, dev tunnel, ingress). Your notification URL for Graph subscriptions is your public HTTPS origin followed by `/msgraph/webhook`:
```
https://ops.example.com/msgraph/webhook
```
## Configuration
All settings go under `platforms.msgraph_webhook.extra`:
| Setting | Default | Description |
|---------|---------|-------------|
| `host` | `0.0.0.0` | Bind address for the HTTP listener. Non-loopback binds require `allowed_source_cidrs`; loopback (`127.0.0.1` / `::1`) is the easiest dev-tunnel / reverse-proxy setup. |
| `port` | `8646` | Bind port. |
| `webhook_path` | `/msgraph/webhook` | URL path Graph POSTs to. |
| `health_path` | `/health` | Readiness endpoint. |
| `client_state` | — | Shared secret Graph echoes in every notification. Compared with `hmac.compare_digest` — generate with `openssl rand -hex 32`. |
| `accepted_resources` | `[]` (accept all) | Allowlist of Graph resource paths/patterns. Trailing `*` acts as prefix match. Leading `/` is tolerated. Example: `["communications/onlineMeetings", "chats/*/messages"]`. |
| `max_seen_receipts` | `5000` | Dedupe cache size for notification IDs. Oldest entries evicted when the cap is hit. |
| `allowed_source_cidrs` | `[]` | Required for non-loopback binds. Leave empty only when the listener is bound to loopback and fronted by a local tunnel / reverse proxy. |
Most settings also have an equivalent env var (`MSGRAPH_WEBHOOK_*`) that merges into the config at gateway startup (the exception is `host`, which is config-only — see the note above) — see the [environment variables reference](/reference/environment-variables#microsoft-graph-teams-meetings).
## Security Hardening
### clientState is the primary auth check
Every Graph notification includes the `clientState` string your subscription registered with. The listener rejects any notification whose `clientState` doesn't match, using timing-safe comparison. This is Microsoft's documented mechanism — treat the value as a strong shared secret.
If `client_state` is unset, the listener refuses to start.
### Source-IP allowlisting (production deployments)
For production, restrict the listener to Microsoft's published Graph webhook source IP ranges. Microsoft documents the egress ranges under the [Office 365 IP Address and URL Web service](https://learn.microsoft.com/en-us/microsoft-365/enterprise/urls-and-ip-address-ranges). Configure them as:
```yaml
platforms:
msgraph_webhook:
enabled: true
extra:
host: 0.0.0.0
client_state: "..."
allowed_source_cidrs:
- "52.96.0.0/14"
- "52.104.0.0/14"
# ...add the current Microsoft 365 "Common" + "Teams" category egress ranges
```
Or as an env var:
```bash
MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS="52.96.0.0/14,52.104.0.0/14"
```
Binding a non-loopback host such as `0.0.0.0`, `::`, or a LAN IP without `allowed_source_cidrs` is refused at startup. If you're using a dev tunnel or reverse proxy on the same machine, bind Hermes to `127.0.0.1` or `::1` and leave the allowlist empty there. Invalid CIDR strings log a warning and are ignored. **Review the Microsoft IP list quarterly** — it changes.
### HTTPS termination
The listener speaks plain HTTP. Terminate TLS at your reverse proxy (Caddy, Nginx, Cloudflare Tunnel, AWS ALB) and proxy to the listener over the local network. Graph refuses to deliver to non-HTTPS endpoints, so there's no path for unencrypted traffic to reach you from Graph itself.
### Response hygiene
On success the listener returns `202 Accepted` with an empty body — internal counters stay out of the wire response. Operators can observe counts via `/health`, which is guarded by the same source-IP rules as the webhook path.
Status code table:
| Outcome | Status |
|---------|--------|
| Notification(s) accepted or deduped | 202 |
| Validation handshake (GET with `validationToken`) | 200 (echoes the token) |
| Every item in batch failed clientState | 403 |
| Malformed JSON / missing `value` array / unknown resource | 400 |
| Source IP not in allowlist | 403 |
| Bare GET without `validationToken` | 400 |
## Troubleshooting
| Problem | What to check |
|---------|---------------|
| Graph subscription validation fails | Public URL is reachable, `/msgraph/webhook` path matches, GET with `validationToken` echoes the token verbatim as `text/plain` within 10 seconds. |
| Notifications POST but nothing ingests | `client_state` matches what you registered the subscription with. Re-run `openssl rand -hex 32` and create a new subscription if the value drifted. Check `accepted_resources` includes the resource path Graph is sending. |
| Every notification 403s | `clientState` mismatch (forged, or subscription registered with a different value). Re-create the subscription with `hermes teams-pipeline subscribe --client-state "$MSGRAPH_WEBHOOK_CLIENT_STATE" ...` (ships with the pipeline runtime PR). |
| Listener refuses to start on `0.0.0.0` | Set `allowed_source_cidrs` to Microsoft's current webhook egress ranges, or bind Hermes to `127.0.0.1` / `::1` behind your tunnel or reverse proxy. |
| Listener starts but `curl http://localhost:8646/health` hangs | Port binding collision. Check `ss -tlnp \| grep 8646` and change `port:` if needed. |
| Real Graph requests from Microsoft get 403'd | Source IP allowlist is too narrow. Widen the list to include the current Microsoft egress ranges. If you're still validating the tunnel path, bind Hermes to loopback and let the tunnel handle public exposure. |
## Related Docs
- [Register a Microsoft Graph Application](/guides/microsoft-graph-app-registration) — Azure app registration prereq
- [Environment Variables → Microsoft Graph](/reference/environment-variables#microsoft-graph-teams-meetings) — full env var list
- [Microsoft Teams bot setup](/user-guide/messaging/teams) — the different platform that lets users chat with Hermes in Teams