feat(gateway): Microsoft Teams platform adapter#13753
Closed
AlexLuzik wants to merge 10 commits into
Closed
Conversation
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>
Collaborator
Collaborator
19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Changes Made
Feature commits:
Follow-up fixes discovered during onboarding:
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
`.Checklist
Code
Documentation & Housekeeping
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.