Skip to content

feat(gateway): Microsoft Teams platform adapter#13753

Closed
AlexLuzik wants to merge 10 commits into
NousResearch:mainfrom
AlexLuzik:main
Closed

feat(gateway): Microsoft Teams platform adapter#13753
AlexLuzik wants to merge 10 commits into
NousResearch:mainfrom
AlexLuzik:main

Conversation

@AlexLuzik

Copy link
Copy Markdown

What does this PR do?

Adds Microsoft Teams as a first-class Hermes gateway platform, with feature parity to openclaw's MS Teams channel. The adapter runs its own aiohttp webhook (default 3978, `/api/messages`), validates Bot Framework JWTs against Microsoft's JWKS, dispatches activities into the agent, and replies via `service_url` with Bearer tokens minted from the user's Azure credentials. Authentication supports secret, certificate, and Azure Managed Identity flows. Microsoft Graph integration handles channel history, user / @mention search, SharePoint uploads, and hosted-content downloads. Channel and group uploads render as first-class Teams file cards via SharePoint; DM uploads use the standard FileConsentCard flow. Adaptive Cards are emitted on intent.

Follows the 15-point `gateway/platforms/ADDING_A_PLATFORM.md` checklist plus two additional integration points discovered during production onboarding (`hermes_cli/platforms.py` and `hermes_cli/setup.py`) — both now documented in the checklist so future platform-adders don't retrace the same traps.

Verified end-to-end against real Azure credentials + LM Studio (Gemma 4 31B) in a Docker container against a live Teams tenant.

Related Issue

No existing issue — opening alongside the PR on request.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

Feature commits:

  • `e184449f` C1 — scaffolding & 15-point wiring checklist
  • `c2f0b521` C2 — `auth.py` (MSAL secret + certificate + Managed Identity) with shared per-scope token cache
  • `641bc607` C3 — `adapter.py` aiohttp server, JWT validation, markdown→Teams HTML, dm_policy / allowlist / require_mention / per-team overrides
  • `a79c6efa` C4 — `graph.py` + adapter hooks for history, user resolution, attachments, SharePoint
  • `77fcd21f` C5 — `cards.py` (Adaptive Cards, FileConsent, file.download.info), invoke-activity handler, federated-auth wizard, full docs page

Follow-up fixes discovered during onboarding:

  • `b3841487` register MS Teams in the `hermes setup` gateway checklist (`hermes_cli/setup.py`)
  • `ea50326c` register in `hermes_cli/platforms.py` PLATFORMS (`KeyError: 'msteams'` on first turn)
  • `967a1fd4` Docker onboarding: `docker/compose.msteams.yml`, env template, `entrypoint.sh` CRLF→LF + `.gitattributes` guard
  • `e4118faa` setup wizard: reject whitespace or >72-char pastes into client secret (Secret ID + Value concatenation trap)
  • `332e2770` setup end-of-onboarding hint: `🐳 docker restart ` when running inside a container (systemd/launchd can't reach pid 1)

New module layout: `gateway/platforms/msteams/{init.py, adapter.py, auth.py, cards.py, graph.py}`. Adapter delegates to specialised submodules so each concern (auth, Graph client, card builders, markdown converter) has its own unit tests and can be stubbed independently.

How to Test

  1. `pip install 'hermes-agent[msteams]'` (or use the full Dockerfile which includes `[msteams]` via `[all]`).
  2. Follow `website/docs/user-guide/messaging/msteams.md` — Azure app registration → Bot Framework resource → Teams app manifest → public HTTPS endpoint (ngrok / Cloudflare Tunnel).
  3. `hermes setup` → pick Microsoft Teams, choose auth type, enter App ID / tenant / secret (or certificate / MI).
  4. `hermes gateway` — on first inbound DM, Hermes sends a pairing code. Approve with `hermes pairing approve msteams `.
  5. Send a follow-up DM — the agent should reply in Teams.
  6. For the test suite: `pytest tests/gateway/test_msteams_*.py -q` — 116 new cases covering auth (21), wiring (15), adapter (43), Graph (22), cards (15).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (`feat(gateway/msteams):`, `fix(msteams):`, etc.)
  • I searched for existing PRs (no duplicate MSTeams work found)
  • My PR contains only changes related to MSTeams integration
  • I've run `pytest tests/ -q` — 116 new MSTeams tests pass; the 94 pre-existing failures on Windows are unrelated (cp1251 locale issues in `test_update_command`, missing `lark-oapi` for Feishu, etc., all observed without my changes)
  • I've added tests for my changes — 116 new cases
  • I've tested on my platform: Windows 11 (Docker Desktop) against a live Azure tenant + LM Studio Gemma 4 31B

Documentation & Housekeeping

  • Updated `website/docs/user-guide/messaging/msteams.md` (new), `index.md`, `reference/environment-variables.md`, `README.md`, `AGENTS.md`
  • Updated `cli-config.yaml.example` N/A — MSTeams options live in `PlatformConfig.extra`; env-var reference is in the new docs page
  • Updated `gateway/platforms/ADDING_A_PLATFORM.md` — §6b (PLATFORMS registry) and §13 (setup.py `_GATEWAY_PLATFORMS`) now call out the two previously-undocumented integration points
  • Cross-platform impact — adapter uses aiohttp (already a Hermes dep); compose `extra_hosts: host-gateway` makes `host.docker.internal` work on Linux hosts too
  • Tool descriptions — no tool schema changes; `send_message` / `cronjob` schemas extended to mention `msteams:` target format

Screenshots / Logs

Adapter startup and message round-trip verified in `hermes-agent:full` Docker image (built from the shipped `Dockerfile`) against a real tenant. Sample trace in the commit messages.

AlexLuzik and others added 10 commits April 21, 2026 17:05
Wires Platform.MSTEAMS through every integration point listed in
gateway/platforms/ADDING_A_PLATFORM.md with a stub adapter that parses
config but declines to connect.  The real Bot Framework + Microsoft
Graph protocol lands in follow-up commits (C2 auth, C3 DM protocol,
C4 Graph, C5 cards/wizard/docs).

- New package gateway/platforms/msteams/ (__init__ + stub adapter.py)
- Platform enum + env-var overrides + get_connected_platforms +
  token placeholder warning (gateway/config.py)
- Adapter factory + authorization maps + update-allowed list
  (gateway/run.py)
- PLATFORM_HINTS entry (agent/prompt_builder.py)
- hermes-msteams toolset + hermes-gateway composite (toolsets.py)
- Cron delivery map + home-channel env var (cron/scheduler.py)
- send_message platform_map + stub _send_msteams + target-ref
  parser branch for Teams conversation IDs (tools/send_message_tool.py)
- cronjob deliver schema example (tools/cronjob_tools.py)
- Status display + gateway setup wizard entry (hermes_cli/)
- [msteams] optional extra in pyproject.toml
- .env.example documentation for all MSTEAMS_* variables
- 15 smoke tests covering enum, env overrides, stub adapter,
  authorization maps, toolset, cron/delivery maps, target-ref parser,
  and check_msteams_requirements

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(C2)

Adds gateway/platforms/msteams/auth.py with three CredentialProvider
implementations and a factory that reads PlatformConfig.extra:

- SecretCredentialProvider — MSAL client-secret flow.  Default when
  MSTEAMS_AUTH_TYPE is unset or "secret".
- CertificateCredentialProvider — MSAL certificate flow for federated
  auth.  Reads a PEM containing both private key and cert body; honours
  a send_public_cert opt-in for subject-name / issuer trust setups.
- ManagedIdentityCredentialProvider — azure-identity ManagedIdentityCredential
  for deployments on Azure compute (App Service, AKS, Container Apps).
  Surfaces non-Azure misconfiguration as AuthError with a clear message.

All three providers share the same get_token(scope) surface and cache
tokens with a 5-minute early-refresh window, so Bot Framework and Graph
calls hit the network once per token lifetime.  asyncio.Lock per scope
coalesces concurrent refreshes.  MSAL / azure-identity synchronous calls
run via asyncio.to_thread so the gateway event loop stays free.

Scopes are exposed as BOT_FRAMEWORK_SCOPE and GRAPH_SCOPE constants so
callers in C3 (adapter) and C4 (Graph client) import them rather than
typing the URLs by hand.

Tests: 21 cases covering factory branching, PEM + thumbprint handling,
cache hits, refresh boundary, per-scope isolation, MSAL error surfacing,
concurrent-request coalescing, and MI failure modes.  All pass alongside
the C1 wiring tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the C1 stub adapter with the real Bot Framework webhook loop:

- Dedicated aiohttp server on config port/path (default 3978, /api/messages)
  with a scoped platform lock, graceful shutdown, and a /health probe.
- Incoming Activity JWT validation via JwtTokenValidation +
  SimpleCredentialProvider.  Federated-auth bots validate the same way
  because inbound tokens are signed by Microsoft's public JWKS —
  independent of how outbound tokens are minted.
- Activity → MessageEvent translation.  Conversation type maps to
  chat_type (personal→dm, channel→channel, groupChat→group); AAD object
  id fills user_id; channel_data.channel.id fills thread_id; team id is
  parked in chat_id_alt for later Graph calls.  @mention stripping via
  <at> tag detection + plain-@ prefix with app-id fallback.
- Policy gates mirroring openclaw:
    * dm_policy: pairing (default, hands off to Hermes pairing),
      allowlist (requires allow_from membership), open, disabled.
    * group_allow_from for channels / group chats.
    * require_mention with per-channel free_response_channels bypass.
    * Per-team and per-channel overrides layered onto the adapter
      defaults via PlatformConfig.extra['teams'][team_id].channels[...].
- markdown_to_teams_html converter: bold / italic / inline code /
  fenced code blocks / links / simple bullet & numbered lists.  HTML-
  escapes the input first so LLM-emitted <script> stays inert.  Emits
  text suitable for outgoing activities with textFormat="xml".
- Outbound send / send_typing: compose {serviceUrl}/v3/conversations/
  {chat_id}/activities, Bearer token from CredentialProvider for
  BOT_FRAMEWORK_SCOPE, retryable flag on transport + 5xx failures.
- service_urls.json sidecar under HERMES_HOME/msteams: every inbound
  activity records its serviceUrl so the standalone _send_msteams
  helper (cron, send_message outside the gateway) can reach the same
  conversation without the gateway process running.
- Credential provider is built lazily in connect() so the adapter is
  constructible for status probes and tests even when credentials are
  missing.  Construction errors surface as a fatal state with a
  msteams_auth error code.

Tests: 43 new cases for markdown shapes, mention stripping, URL
composition, effective-policy layering, every dm_policy branch,
require_mention + free_response_channels, group_allow_from,
_build_event for DM / channel / group, serviceUrl persistence,
outbound happy path, serviceUrl-missing error, AuthError propagation,
5xx retryable classification, send_typing shape, and connect() failure
modes.  Adjacent suites (slack, mattermost, allowlist, config,
channel_directory, delivery) still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New gateway/platforms/msteams/graph.py wraps msgraph.GraphServiceClient
with the narrow set of operations the adapter needs:

- fetch_channel_messages(team_id, channel_id, top) — oldest-first recent
  history, for later channel-prepend behaviour.
- resolve_user(aad_object_id) — display name, email, job title.
- search_users_by_display_name(prefix, top) — @mention search;
  single-quote-escapes the prefix before substituting into the OData
  filter.
- list_joined_teams + list_channels — enumeration helpers for future
  channel-directory integration.
- download_hosted_content — pulls bytes for inline channel attachments.
- upload_to_sharepoint — simple PUT upload to a SharePoint drive folder,
  returns the resulting webUrl.  Chunked/resumable upload (createUploadSession
  for >4 MB) is out of scope for the MVP.

Every method catches Graph SDK errors, logs the failure with a stable
action label, and returns an empty / None value.  A Graph outage never
takes the Bot Framework event loop down — the agent sees degraded
features, not a dead adapter.

Authentication: a _HermesTokenCredential thin wrapper adapts the
CredentialProvider from auth.py to azure.core.credentials_async's
AsyncTokenCredential contract, so the same provider drives both Bot
Framework (BOT_FRAMEWORK_SCOPE) and Graph (GRAPH_SCOPE) calls through
one shared cache.  The Graph client is built lazily on first use and
closed alongside the credential provider during disconnect().

Adapter integration:

- connect() instantiates a GraphClient; disconnect() closes it.
- _build_event records channel→team mapping in _team_ids_by_chat so
  subsequent get_chat_info / SharePoint upload calls can address the
  channel without the caller passing a team id.
- get_chat_info for channels now queries Graph's list_channels for the
  parent team and merges display_name / description / membership_type.
  DMs and missing-Graph cases fall back to the pre-existing stub.
- fetch_channel_history, resolve_user, and upload_channel_file helpers
  expose the Graph wrapper to future C5 code paths (FileConsent card,
  channel attachment cards).  upload_channel_file namespaces per chat
  so uploads from different channels don't collide on filename.

Tests: 22 new cases covering _HermesTokenCredential delegation and
scope requirement, _attr helper (attribute + dict + default), message
dict projection, resolve_user happy + error paths, user search with
quote-escaping, channel history newest→oldest reversal, team /
channel enumeration, hosted-content download and failure, adapter
get_chat_info with + without Graph, history limit propagation, upload
site-id gating, and Graph lifecycle in connect/disconnect.  All 101
msteams tests (C1+C2+C3+C4) remain green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the MS Teams integration for openclaw feature parity.

New gateway/platforms/msteams/cards.py with builders for:

- Adaptive Cards: schema-agnostic wrapper so agent code owns the layout
  while the adapter just transports the attachment.
- FileConsentCard + FileInfoCard: the DM upload consent flow.  Every
  consent card carries a stable upload_id the invoke handler uses to
  match the accept/decline response.
- file.download.info attachment: channel / group file card wired to a
  SharePoint webUrl — what Teams renders as a proper file tile.
- markdown_to_teams_html: the markdown→Teams-HTML converter moved from
  the adapter so it has its own test surface and both cards and plain
  sends can reuse it.  The adapter re-exports it for backward compat.

Adapter integration:

- send_document / send_image_file / send_video route by chat type.  DM
  → FileConsent (buffered in _pending_uploads, keyed by upload_id, PUT
  to the uploadInfo URL on accept, follow-up FileInfoCard so the user
  sees the file inline).  Channel / group → SharePoint upload via the
  Graph client + file.download.info attachment.  Decline drops the
  staged upload; unknown upload_ids are accepted (200) but ignored.
- send_image on a channel with SharePoint configured now downloads the
  image URL and re-uploads it through SharePoint for a native file
  tile instead of a plain hyperlink.
- _handle_messages routes fileConsent/invoke activities to the new
  _handle_file_consent_invoke method; everything else keeps the 200
  ack to stop Teams retrying.

Federated-auth wizard (hermes_cli/gateway.py):

- New _setup_msteams delegates core prompts to _setup_standard_platform
  then branches on MSTEAMS_AUTH_TYPE.  Federated users pick between a
  certificate path + thumbprint or Azure Managed Identity with an
  optional user-assigned client id.  Routed from the main setup switch.

Docs:

- New website/docs/user-guide/messaging/msteams.md: Azure registration,
  secret/certificate/Managed Identity walk-through, Bot Framework
  portal setup, Teams app manifest template with RSC permissions,
  full env var reference, per-team / per-channel override snippet,
  troubleshooting (JWT failure, missing SharePoint site, stale upload
  URL, Managed Identity outside Azure).
- index.md: platform feature table + architecture diagram + toolset
  table + Next Steps link all updated for MS Teams.
- environment-variables.md: every MSTEAMS_* variable documented.
- README.md messaging row, AGENTS.md platform adapter listing updated.

Tests: 15 new cases for card shapes (Adaptive, FileConsent, FileInfo,
file.download.info), file type inference for extension-less names,
markdown re-export through the adapter, DM send_document → FileConsent
payload shape + staged bytes, invoke accept → PUT + FileInfoCard
follow-up, invoke decline → no upload, unknown upload_id → silent 200,
channel send_document → SharePoint upload + file.download.info
attachment, missing site_id clear error, missing file clear error,
invoke routing from _handle_messages.  Combined with C1–C4, the
msteams suite now runs 116 cases green.  Adjacent suites (slack,
mattermost, allowlist, config, channel_directory, delivery, api_server)
remain green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C5 added a _setup_msteams() helper to hermes_cli/gateway.py's platform
list, but missed the parallel _GATEWAY_PLATFORMS list in
hermes_cli/setup.py — the checklist the default `hermes setup` wizard
shows.  With only gateway.py updated, MS Teams appeared under
`hermes gateway` but was absent from the main setup flow, which is
where most users land first.

Adds a fresh _setup_msteams() in setup.py that mirrors the gateway.py
wizard (App ID + tenant, secret/federated auth branch, allowlist,
home conversation) and plugs it into _GATEWAY_PLATFORMS between
Mattermost and WhatsApp so the numbering and grouping match docs.

Updates ADDING_A_PLATFORM.md §13 to call out that both lists must be
kept in sync — the two checklists serve different wizards and it's
easy to miss the second one.

Tests: existing MS Teams suite (116 cases) still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every inbound message funnels through
gateway.run._run_agent -> tools_config._get_platform_tools, which
indexes PLATFORMS[platform]["default_toolset"].  PLATFORMS lives in
hermes_cli/platforms.py — a registry I missed in the C1 checklist.
Without the msteams entry the first real Teams activity crashed with
KeyError: 'msteams' right after the pairing flow finished, so the user
saw "Sorry, I encountered an error (KeyError)" instead of an agent
response.

Adds an ordered entry mapping msteams → hermes-msteams so the
platform_toolsets fall-back in tools_config picks up the toolset that
C1 already defined in toolsets.py.

Also appends §6b to ADDING_A_PLATFORM.md so future platform-adders
don't retrace this trap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rding

Bundles everything needed to run the full Hermes agent image with the
MSTeams adapter exposed:

- docker/compose.msteams.yml: builds the repo Dockerfile (which pulls
  the [all] extra, including [msteams]), mounts state into
  ./hermes-data, publishes 3978 (MSTeams webhook), 8642 (OpenAI API),
  8644 (webhook receiver).  extra_hosts pins host.docker.internal to
  the host gateway so LM Studio / Ollama reachable from the host also
  works on Linux.
- docker/.env.msteams.example: skeleton for Azure credentials, auth
  type, access policy, SharePoint, and the model provider secret.
- docker/entrypoint.sh: converted CRLF → LF line endings.  Windows
  checkouts were producing "exec entrypoint.sh: no such file" inside
  the image because the Linux kernel tried to exec /bin/bash\r.
- .gitattributes: forces *.sh (especially docker/entrypoint.sh) to
  stay LF so the regression can't recur.
- .gitignore: excludes docker/.env.msteams (holds real secrets) and
  docker/hermes-data/ (container volume) from the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Observed during MSTeams onboarding: the hermes setup wizard silently
accepted a whole Azure 'Certificates & secrets' page pasted into the
client-secret prompt, saving the concatenation of Secret ID + label +
Secret Value (85 chars) to HERMES_HOME/.env.  The adapter then sent
that blob to MSAL, which returned AADSTS7000215 ("secret is not the
value"), and the gateway looped on send failures with the correct-
looking credentials in env.

Azure client secret Values are ~40 chars, single-line, no whitespace.
The wizard now strips leading/trailing whitespace and refuses to save
any input that contains internal whitespace OR exceeds 72 chars,
printing a warning that points to the right field in Azure Portal.
Valid secrets fall through unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When 'hermes setup' finishes inside a container, the wizard's
existing 'Restart the gateway to pick up changes?' branch bails out
silently — systemd_restart/launchd_restart don't apply to pid 1 of a
container, and the running gateway process keeps serving adapters
with the os.environ snapshot it was started with.  The credentials
the wizard just wrote to HERMES_HOME/.env are not picked up until the
container itself is restarted.  Users hit this MSTeams-first
integration and found the bot silent after a successful setup flow.

Adds a single-paragraph hint printed right after 'Setup complete!'
when the user is (a) inside a container per hermes_constants.is_container,
and (b) just configured a messaging platform.  Uses the container
hostname (which Docker/Podman set to the short id) to produce a
copy-pasteable 'docker restart <id>' command.

No effect outside containers or when the gateway section was skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins comp/cron Cron scheduler and job management labels Apr 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Supersedes #10037 (earlier Teams adapter PR) and addresses #9512 (Teams feature request).

@AlexLuzik AlexLuzik closed this Apr 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Supersedes #10037 (earlier Teams adapter PR) and addresses #9512 (Teams feature request).

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

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants