Skip to content

Latest commit

 

History

History
576 lines (508 loc) · 42 KB

PLATFORMS.md

File metadata and controls

576 lines (508 loc) · 42 KB

Chat platforms

Message activation emoji settings

Admin Settings → Agent defaults → Message activation reactions stores independent values for Slack, Microsoft Teams and Google Chat. Comma-separated entries are supported; a blank list resets the platform defaults. Slack uses emoji names (such as robot_face or rocket). Teams accepts reaction IDs or pasted Unicode; its robot/Alien/Like/Heart families and single-code-point reference IDs are matched across native and Graph forms. For other named Teams IDs, include the pasted Unicode emoji as another entry because Graph can return Unicode rather than the picker name.

Teams saves take effect in reaction matching without reconnecting. The separate Observe edits and robot reactions on all messages option, installed app consent and healthy notification subscription are still required for reactions on unmentioned group/channel content. Changing an activation list does not change Stop/Tick controls. Old history and pending requests from a prior selection do not activate after a change; add a fresh reaction after saving.

Google Chat stores a Unicode-only selection (default 🤖). Chat apps receive no interaction event for a reaction, so the selection acts only once reaction events are enabled (Google Chat → Setup step 8): Google Workspace Events subscriptions then deliver each space's reactions.

ChannelGate speaks to three chat surfaces. Slack is GA; Google Chat and Microsoft Teams are in Beta — their in-chat feature set is deliberately smaller (see What works where below). Google Chat has been exercised against a live tenant; Teams still needs live tenant acceptance.

Every surface goes through the same seam: a capability descriptor in src/platforms/<id>.js, a ChatConnector that owns the wire format, and one platform-neutral ingest path (src/platforms/ingest.js) that gates, authorizes, runs the turn and posts the answer. The channel confinement, authorization model, and MCP allowlist are identical on all three — none of that is per-platform, and none of it is relaxed to make a surface work.


Google Chat (Beta)

Transport: Pub/Sub pull, plus an optional HTTPS endpoint. Google publishes your Chat app's events to a topic you own, and the daemon pulls them over an outbound HTTPS connection. No inbound endpoint, no tunnel, no firewall rule — the same posture as Slack's Socket Mode. A Chat app has exactly one connection setting, and Google never delivers card or button clicks to a Pub/Sub topic: with the topic connection, the Files / Variables / Settings buttons and every other card button fail with Chat's "unable to process your request". To make them work, point the app's connection at ChannelGate's HTTP endpoint URL instead (step 9): Google then POSTs every interaction event — messages, membership, commands and clicks — to <Public URL>/api/googlechat/events, each request signed with a Google ID token that the endpoint verifies before anything else happens. The Pub/Sub subscription stays connected either way: it is also where Workspace Events deliver reactions (step 8).

Setup

  1. Google Cloud project: enable the Google Chat API and Cloud Pub/Sub API in the same project. Keep its project ID for the resource names below.
  2. Pub/Sub topic and subscription: create a topic such as chat-events, then attach a Pull subscription such as chat-events-sub. The resource names have different forms: projects/<project>/topics/<topic> and projects/<project>/subscriptions/<subscription>.
  3. ChannelGate service account: create a service account in the project and download its JSON key. On the subscription's Permissions panel, grant that account Pub/Sub Subscriber (roles/pubsub.subscriber). This is the identity ChannelGate uses to pull events and call the Chat API; keep its JSON key private.
  4. Chat app (Google Chat API → Configuration): set its name, avatar, description, and App status → Live - available to users. Enable interactive features, including direct messages and, for spaces, Join spaces and group conversations. Set Connection settings → Cloud Pub/Sub to the full topic name projects/<project>/topics/<topic>. Set visibility so the intended testers can find the app, enable error logging, and Save.
  5. Google Chat publisher: on the topic's Permissions panel, grant Pub/Sub Publisher (roles/pubsub.publisher) to the identity for the Chat app's configuration mode:
    • Google Workspace add-on: copy Service account email from the Chat API Configuration page's Cloud Pub/Sub connection settings (it may look like service-<project-number>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com). Grant that exact address the publisher role on the topic. This is separate from the JSON-key account in step 3. If the add-on checkbox is already selected and disabled, leave it selected; this mode works with ChannelGate.
    • Standalone Chat app: grant chat-api-push@system.gserviceaccount.com the publisher role on the topic. Google instructs you to clear Build this Chat app as a Google Workspace add-on when configuring a new standalone app. This publisher is not the one to use for an add-on.
  6. ChannelGate: in Settings → Connection → Google Chat, paste the JSON key from step 3, enter projects/<project>/subscriptions/<subscription>, Save, then Connect. A Connected — pulling … status confirms the subscription can be polled; it does not establish that Google Chat can publish to the topic.
  7. Test: add the Chat app to a space or open a direct message, then send a new message mentioning it in a space. Check for an actual ChannelGate reply. The sender must also be an approved user in ChannelGate's Users settings; otherwise the app responds that the user is not approved.

If the pull fails with 403, recheck the step 3 subscription grant and reconnect. If it is connected but receives no messages, recheck the step 5 topic grant for the app's actual mode, then verify the Chat app is Live, interactive features are enabled, and its configured topic name matches step 2. Google's Workspace add-on Pub/Sub guide, standalone Chat app Pub/Sub guide, and Chat troubleshooting guide cover these distinct configurations.

The app's own users/… id is learned automatically the first time it is added to a space; the optional field exists only to short-circuit that.

  1. Optional — reaction controls (🤖 start, 🛑 stop, ✅ acknowledge). A Chat app never receives an interaction event for a reaction; Google delivers reactions only through Google Workspace Events subscriptions. ChannelGate creates and maintains one per space the app belongs to, with app authentication (generally available since 2026-03-31), so no user has to sign in:

    1. Enable the Google Workspace Events API in the same project.
    2. Configure app authentication with administrator approval for the scope https://www.googleapis.com/auth/chat.app.messages.readonly (Google's authorization guide: Marketplace SDK app configuration for the ChannelGate service account), then have a Google Workspace administrator approve the app. Without that approval the subscriptions fail; messaging is unaffected.
    3. Workspace Events publishes with the same publisher identity as step 5 (the add-on's Configuration-page service account, or chat-api-push@system.gserviceaccount.com for a standalone app), so the step 5 grant on the topic already covers it. Events then arrive through the subscription ChannelGate already pulls.
    4. In Settings → Connection → Google Chat, enter Reaction events topic projects/<project>/topics/<topic> (the same topic), Save and Connect. The status line reads reactions N/M spaces once subscriptions are active, or names the error.

    Subscriptions carry the reaction data, which Google caps at four hours, so ChannelGate renews each one about hourly, reactivates suspended ones, recreates expired ones, retires those for spaces the app left, and reconciles on startup, on joining a space and on Google's lifecycle events. Reactions made while the gateway is down for longer than that are not delivered.

  2. Optional — working card buttons (HTTP endpoint connection). Card and button clicks reach a Chat app only through an HTTP endpoint connection (Google's connection settings; the Workspace Events API has no click event type). ChannelGate serves that endpoint on the same HTTPS listener as the admin UI and the Teams webhook:

    1. Set the gateway Public URL (Settings → Connection). The Google Chat card then shows the endpoint, <Public URL>/api/googlechat/events.
    2. Workspace add-on apps: in Settings → Google Chat, enter the Chat app service account — the Service account email shown under Connection settings on the Chat API Configuration page (service-<project-number>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com, the step 5 identity). Google signs every endpoint request as that account; ChannelGate accepts no other sender, so the field is required for an add-on. Standalone apps sign as chat@system.gserviceaccount.com, which is always accepted; if the app's Authentication Audience is set to the project number rather than the URL, also enter the Google Cloud project number.
    3. Save, then Connect (the endpoint is armed at connect time; the status line reads endpoint https://…/api/googlechat/events).
    4. On the Chat API Configuration page set Connection settings → HTTP endpoint URL to that exact URL (for a standalone app choose the HTTP endpoint URL authentication audience unless you entered the project number) and Save. Keep the Pub/Sub subscription and topic: reaction events (step 8) still arrive through them.
    5. Test: mention the app, then press Files under its answer. A private "Conversation files" card must open with no red error line. Switching the connection back to the topic restores the previous behaviour; the endpoint keeps answering 401 to anything Google did not sign for this app.

    Every request is verified before it is read: RS256 against Google's published keys, issuer accounts.google.com, audience equal to the configured endpoint URL (or the project number for the standalone JWT form), email_verified, and the sender exactly as configured. Requests that fail answer 401 with no detail; an endpoint with no Public URL answers 503 naming the remedy. Google waits at most 30 seconds for a synchronous reply, so the endpoint acknowledges with an empty object as soon as the event is durably queued and everything ChannelGate says back goes through the Chat REST API, exactly as for a pulled event. Dialogs are still not used.

Notes and limits

  • Attachments in: files uploaded directly in Chat are downloaded through media.download. A file shared from the Drive picker needs a user-OAuth Drive scope ChannelGate deliberately does not request — those are reported as skipped rather than silently dropped.
  • Workspace files out: /files, /sendfile <path> and agent share_file provide private source-space file controls with text/Word previews and browser download/edit/upload links. Browser links require Public URL; this does not upload files to Drive or publish public links.
  • Threads: in a space, threads are honoured. In a DM, Google mints a new thread for every top-level message, so a thread seen for the first time is treated as the main flow (one continuous DM session) and a thread you explicitly reply into becomes a real side thread.
  • Editing: 1 write/sec per space, shared with every other app in it. Deleting leaves a tombstone, so ChannelGate edits rather than deletes.
  • Private controls: app-authenticated privateMessageViewer keeps settings, files and browser credential-entry links visible only to their requester in the original space. Opened drafts expire after 15 minutes; reply menu buttons are sealed and remain usable after restarts.
  • Commands: /help shows compact expandable Cards v2 with a full-text fallback. /settings, /secrets and /model open settings tabs and separate channel/session runtime scopes. In spaces mention the bot before a typed command; configured registered slash-command events also work. Credential entry uses private browser forms; no value is shown or prefilled. Cards render on both connections, but their buttons act only with the HTTP endpoint connection (Setup step 9); dialogs are never used. Stop/status use a separate durable control queue.
  • Reactions (with Setup step 8): the configured start emoji on a person's message runs its text as the reactor, and on the app's answer continues that task; 🛑 on the request or the working message stops that session's active and queued work, including a turn already running (its author or an admin); ✅ acknowledges a tracked reminder. The reactor's users/… id maps to the gateway user whose messages Google has delivered before (unknown or ambiguous ids never act) and then passes the ordinary authorization gate. A Stop older than 10 minutes, a start older than an hour, a Tick older than a day, a removed reaction and a redelivered event never act; reacted text is content, never a command. Reacting on another app's message never starts a run.
  • Replies: a temporary working line (edited with a compact elapsed/"Still connected" heartbeat, since Chat has no typing indicator) becomes the final answer. In group chats and unthreaded spaces, quote the request or a bot reply with a top-level /stop, /status or /clear to reach that turn; in threaded spaces send them in its thread. Space/group answers mention the requester and have Files/Variables/Settings plus referenced-file buttons below compact stats.

Microsoft Teams (Beta)

Transport: Bot Framework (public HTTPS endpoint). Every supported Teams bot path delivers messages as inbound POSTs from Azure Bot Service — there is no outbound-only receive path. The daemon already terminates public HTTPS for the admin UI and the run API, so the endpoint rides that server. Requests are authenticated against the Bot Framework JWKS (src/platforms/msteams/verify.js) before anything else happens; that check is the entire authentication boundary for this surface.

An Azure Relay Hybrid Connection would remove the public-URL requirement (the daemon holds an outbound websocket and the Relay tunnels POSTs down it). It is a transport swap in FRONT of the same handler — everything behind it is unchanged — and stays on the roadmap.

Setup

Use Microsoft's Teams Developer CLI to create the bot registration and Teams app together. These commands were checked against CLI 3.0.3; install the stable package below. The normal Teams-managed bot path does not need an Azure subscription or a hand-built manifest.

Before starting, identify the target ChannelGate installation and its public HTTPS origin. If multiple gateways exist, do not use the current checkout's URL or settings for another bot. You need a Microsoft 365 account permitted to register the app and install custom Teams apps.

  1. Install and sign in to the Teams CLI:

    npm install -g @microsoft/teams.cli
    teams --version
    teams login --device-code
    teams status

    Open the URL printed by the CLI, enter its short-lived code, and sign in to the intended organization. Keep the login process alive until it confirms success, then check the account, tenant and custom-app upload status with teams status. A browser saying "done" alone is not proof the CLI authenticated. On a desktop, teams login also supports browser sign-in. If teams is not found after installation, add the npm global prefix's bin directory to your shell's PATH (npm prefix -g prints the prefix).

    When an assistant runs this flow, it must keep the same interactive process alive, provide the code in a progress message, and poll until success or expiry before ending the turn. An expired code needs a fresh login. Do not request passwords or authentication tokens in chat.

  2. Create the public event URL. Teams cannot deliver events to localhost. For production, set Settings → Connection → Public URL to the daemon's public HTTPS origin. For local use, expose the daemon with a persistent HTTPS tunnel and use that origin. The Admin UI shows the resulting endpoint: <public-url>/api/teams/messages.

  3. Generate the bot and Teams app with that endpoint. Run this in a private operator terminal; creation can print the client secret. Change the display name to your bot's name. To capture credentials, add --env /absolute/private/path/teams.env with a protected destination outside the repository and channel work folders; use umask 077 before creation. Do not stream the creation output into chat or commit the credentials.

    umask 077

    Then create the app:

    teams app create \
      --name "ChannelGate" \
      --endpoint "https://<public-url>/api/teams/messages"

    Save the emitted CLIENT_ID, CLIENT_SECRET, and TENANT_ID; the secret is shown only once. Also retain the emitted Teams app ID for installation; it is distinct from the Application (client) ID. Keep personal, team and groupChat scopes for the conversations you intend to use. For an existing registration, use teams app list and teams app get <teamsAppId> to inspect it before creating a duplicate.

  4. Configure ChannelGate — Settings → Connection → Microsoft Teams: paste CLIENT_ID as the Application ID, CLIENT_SECRET as the client secret, and TENANT_ID as the tenant, Save, then Connect.

  5. Install the generated app in Teams:

    teams app get <teamsAppId> --install-link

    Open the printed link in a browser or Teams client and install the app. If the public URL later changes, update the registered event endpoint with teams app update <teamsAppId> --endpoint "https://<new-public-url>/api/teams/messages".

  6. Verify the connection with a real conversation. Approve the test user's Teams identity in the target ChannelGate installation; a Slack approval does not grant a separate Teams identity. Send Reply with TEAMS_OK in a personal chat, then @<bot-name> Reply with TEAMS_OK in a test team/channel. Confirm a real answer in the personal chat and a threaded channel reply. A "Connected" badge alone does not prove inbound delivery. See the live acceptance case in TEST-PLAN.md; Teams remains beta until tenant verification is completed.

Optional all-message edit and reaction events (experimental)

The teams-ms branch adds an opt-in Observe edits and robot reactions on all messages setting under Settings → Connection → Microsoft Teams (teamsAllMessageEvents, default false). It requests Graph subscriptions for known installed conversations, not a tenant-wide message feed. Normal group/channel messages still need a real bot mention. A robot reaction is an explicit activation by the reacting user and is checked against that user's current ChannelGate access.

For a test installation, extend the CLI setup above:

  1. Download the app's current manifest using teams app manifest download --help for the installed CLI's output options. Preserve its IDs, icons, scopes and existing permissions. Merge these entries into authorization.permissions.resourceSpecific (do not replace other grants):

    [
      { "name": "ChatMessage.Read.Chat", "type": "Application" },
      { "name": "ChannelMessage.Read.Group", "type": "Application" }
    ]

    Ensure webApplicationInfo.id identifies this bot's Entra application and retain the valid webApplicationInfo.resource field. These are resource-specific application grants, consented for the chat/team where the app is installed. See Microsoft's RSC manifest and consent instructions.

  2. Upload the reviewed manifest, using the separate Teams app ID:

    teams app manifest upload ./manifest.json <teamsAppId>
    teams app get <teamsAppId> --install-link

    Current CLI syntax puts the file before the app ID; verify with teams app manifest upload --help when using an older CLI. The upload command preserves icons and can bump the package version. Alternatively, teams app manifest update supports --set-json and --dry-run; preview the complete merged permission array before upload.

  3. Update or reinstall the app in each intended test chat/team and accept the new permissions under that tenant's policies. Updating the developer registration alone does not prove resource consent.

  4. Set the public HTTPS URL, enable the checkbox, save, then disconnect/reconnect Teams. Allow both /api/teams/messages (Bot Framework) and /api/teams/notifications (Graph notification validation and delivery) through the existing public reverse proxy. The Graph notification URL is separate from the bot messaging endpoint; do not replace the bot endpoint with it.

  5. Send a bot mention in each installed test conversation. An authenticated activity establishes the known conversation before automatic subscription creation. Existing messages from unknown chats are not swept or retroactively subscribed across the tenant. Subscriptions renew while enabled; inspect event logs for consent, renewal or delivery failures. A Connected banner verifies neither Graph consent nor subscription coverage.

  6. Perform the event tests in TEST-PLAN.md: add a real mention by editing an existing user message, react with 🤖 to a user message, and react to a bot reply to continue its session. Confirm actor, target and session attribution with both Claude and Codex. Include an external-member group and desktop/mobile clients. Do not declare all-message coverage from bot-reply reaction tests alone.

The chat Graph route uses Microsoft Graph beta, so this feature remains experimental. A granted read permission does not guarantee every client emits the same reaction shape or that every federated conversation supports the subscription. The switch does not grant file/SharePoint access, allow unapproved users, or enable agent responses to every observed ordinary message. Bot Framework edit/reaction handling and optional Graph observation have distinct delivery coverage. Real tenant acceptance has not been performed as part of this branch's local implementation.

See the full Slack-to-Teams parity audit for remaining UI and integration gaps.

Native cards, workspace files and voice (teams-ms branch)

These additions are available on beta; live acceptance remains required before stable release. Update the installed app's reviewed manifest so the bot entry has supportsFiles: true for native personal-chat file consent, then upload/install that app revision with the Teams CLI as described above. Adaptive Cards and their inline forms do not require additional Graph RSC permissions. Native settings dialogs are available; broadcast mentions remain unavailable.

  • /help provides a practical How to use me guide and the supported Teams command list in a native Adaptive Card, with separate headings, spaced paragraphs, individual emoji/command rows and monospace command text. Full spaced text is the fallback if card delivery fails. It explains personal/channel/group session continuity, mentions, voice, files, settings, connections, skills, memory, reminders, schedules and background work. In channels and group chats, mention the bot with the command; quote the original message or bot reply in a group chat to address that session. Help is returned by the gateway before invoking an engine. The reaction legend recommends 👽 Alien (:alien:) and also shows Heart eyes robot (:hearteyesrobot:), Stop sign (stopsign), and Tick button / Checkmark button (2705_whiteheavycheckmark). Typing an ID as a message does not add a reaction. Heart eyes robot activates a request as the reactor; bare hearteyesrobot and the picker shortcut :hearteyesrobot: are accepted. Alien accepts alien, :alien:, Unicode 👽 and 1f47d_extraterrestrialalien, with its own saved introduction cutoff to exclude pre-upgrade reactions. Like is an alternative; intake recognizes like, Unicode 👍 and their skin-tone variants. Legacy hearteyesrobot and smilerobot event IDs remain recognized. Graph's 😍 variant activates only when added history and the current reaction both identify Heart eyes robot with no custom content URL. Ordinary Heart eyes, unlabelled history and custom images do not activate. New alias recognition starts at a saved subscription cutoff to prevent old reactions replaying on unrelated updates. Microsoft calls the green tick Checkmark button, ID 2705_whiteheavycheckmark; Stop sign is stopsign. See the Teams reactions reference. Stop sign cancels the selected session’s active and queued requests, with author/admin checks. Tick button acknowledges a tracked reminder from its original or second-notice message; removing it does not reopen the reminder. A tick on ordinary content reports no pending reminder. Teams personal follow-up dismissal is not available. All actions require authorized reactors and delivered reaction events. Group/channel delivery uses configured Graph events and Microsoft permissions; personal chat accepts delivered Bot Framework reactions. /stop and /cancel remain available as typed commands.
  • /model shows the current session selection and opens a picker in the same conversation/thread, laid out like Settings → General: choose the engine and press Load models, then choose from only that engine's models and effort levels and Apply to this session. Changes affect its next turn. Enabled engines and dedicated Codex login restrictions apply. Runtime policy is rechecked on Apply, and busy sessions refuse changes. Typed /model <engine> <model|default> and /effort <level|default> remain available.
  • /settings posts a compact Open settings card in the original channel/thread or chat. Clicking it opens a large native Teams popup containing General, Variables, MCPs, Skills, Automations and Resume. /secrets uses the same launcher and opens Variables directly. Each popup retains the source conversation and session, rechecks the requesting user's current access, and keeps forms, navigation and save results out of chat. No proactive personal chat, additional website, TeamsJS page or new Graph permission is required for this card dialog. The launcher and each independently opened popup expire after 15 minutes. Closing with Teams' close control discards unapplied inputs; applied changes remain saved. Other members open their own requester-bound /settings launcher. General edits channel defaults and current-session engine/model/effort independently, with engine-labelled model choices, inherited labels and Follow channel default. One Apply to channel or Apply to thread button saves that scope's engine/model/effort together after compatibility checks. No intermediate runtime writes are needed; stale forms must be reopened. Other settings keep their explicit controls. Page navigation discards unsaved drafts. Authorized users can edit runtime, variables, connections, channel skills and automations, matching Slack Settings. Access controls require a current manager/admin, organization variable changes and Cloud MCP require admins. Native member selectors defer to the authenticated website when the complete roster exceeds 25 members, exceeds the card budget; no hidden selection is silently removed. Former-member selections have an explicit replacement warning. VPN reports the existing service's actual availability; operator provisioning and unsupported Teams service identifiers are not changed by this UI. Variables and tokens are write-only: stored values never prefill a card, new entries use Teams' masked input style, and removal/reset actions require a one-use confirmation. Native entries follow the existing 8,000-character field and 16-KiB submission limits; larger values need the authenticated browser editor. Catalogs, templates and automations paginate. Resume resolves the current session at display time and provides its terminal command to eligible users inside the popup. Personal/organization variable controls are available there under their existing role rules. The chat launcher contains no credential metadata, connection catalogs or session commands. Dialog actions use authenticated task/fetch and task/submit callbacks with server-held owner, conversation, tenant (when known), scope and expiry; chat-card submissions cannot operate a dialog state. The sender name is the installed Teams app/bot identity. If another bot name appears, verify that the installed manifest bot ID, the configured Teams App ID and the Azure bot messaging endpoint all belong to the intended installation; changing card text cannot rename that app. Native approvals provide Approve/Deny/Request changes and supported scope choices. An optional changes comment refuses the current action, including when Approve was clicked. Card submissions take identity from the verified Microsoft envelope and repeat membership/role checks. Verified Execute errors use the Teams invoke response envelope; invalid request authentication remains an HTTP rejection. Legacy Submit replacements use only the card's server-stored message target.
  • Every answer, error and stop carries a reply menu under its stats line — 📂 Files, 🔑 Variables, ⚙️ Settings and up to three 📄 buttons for workspace files the answer named — opening the same private windows as the commands below. The buttons are HMAC-sealed (conversation, session, requester, path) and hold no server state, so they work on old replies and after restarts; only the requester can use them, in their own conversation, with live access checks on every click.
  • /files [folder] posts Open files in the source conversation/thread; its Teams dialog browses that workspace. Open a file for a literal text/Markdown preview (first 2,600 bytes), a complete download, or eligible browser text editing (up to 250,000 characters); a .docx shows a read-only plain-text preview of its body, and PDF and other binary files show download guidance. Preview works without a Public URL. Users with file-write access can open the uploader. Short-lived browser grants recheck current Teams membership and gateway policy. Listings and links stay inside the owner's window. Back to files returns to browsing. No personal chat installation or proactive DM is required. Read-only file callbacks have a 3.5-second response budget, cancel stalled roster requests and reuse a single current membership check per callback. File launchers persist across restarts and reconnects within their original 15-minute lifetime; private dialog state remains ephemeral. Missing/expired/unknown references show distinct dialog errors with fresh /files guidance. Settings and approval writes are not abandoned by this file-read deadline.
  • Agents use share_file to deliver Open file in the current Teams conversation. No Composio connection or public link is needed; the gateway Public URL is required for its authenticated browser downloads. Slack-only tools are not exposed to Teams or Google Chat.
  • /settings → MCPs → My connections saves or disconnects personal Composio and Toolbox tokens inside the Teams window. Stored values are never prefilled; inputs are masked.
  • Native approvals stay on their source-thread card without supplemental decision-link DMs. If card delivery fails, the action is refused with retry guidance in that same conversation.
  • /sendfile <workspace-relative-path> in a channel/group posts Open file in that same conversation, opening a Teams dialog with a browser download. In an existing bot DM, it sends native file consent there. Accept uploads the prepared snapshot; Decline does not upload. The native limit is a nonempty file of at most 10 MB, consent expires after ten minutes, and uncertain upload outcomes are not replayed. Larger files use the browser download path. Native file consent cannot open a new personal chat for a group/channel request.
  • Downloadable audio uses local Whisper only, controlled by the existing Whisper setting. No Slack-generated transcript is requested. A failed audio-only request explains the missing transcript without invoking an engine; accompanying typed text can continue. Stop cancels local transcription as well as engine work. Separate intake directories keep simultaneous uploads and edits from overwriting another request's audio or files.

For optional group/channel file reading, enable Read group and channel files from allowed drives under Microsoft Teams settings and enter up to 32 exact Microsoft drive IDs, one per line (teamsFilesEnabled, teamsFileDriveIds). Configure application read access to those sites externally using Microsoft's selected permissions, then save and reconnect Teams. The deployment's configured Graph identity must have access to those drives; neither the checkbox nor the allowlist grants Microsoft permissions. This is a gateway-wide allowlist for authorized conversations, not a per-user Microsoft file entitlement.

Use canonical SharePoint file URLs whose paths lie inside an allowed drive root. The resolver reads only configured drive roots, addresses the matching item within that drive, verifies its returned identity, and downloads with no Graph bearer on the file-host request. Redirects and oversized responses are refused. Sharing shortlinks are unsupported: use a canonical file link or upload directly in a personal chat. The implementation deliberately does not use Graph's sharing-link endpoint, whose documented application permissions include broad write access. Selected-site consent and real SharePoint download compatibility remain live acceptance gates.

See TEST-PLAN.md for exact native-card, file, voice and both-engine fixtures. Native file flow reference: Microsoft bot file consent.

Notes and limits

  • No public URL ⇒ no inbound. The bot will connect and can send, but Azure has nowhere to deliver to. ChannelGate says so at boot and on the health check rather than looking merely quiet.
  • Attachments in: 1:1 uploads arrive with a pre-authenticated download URL and are fetched (only from Microsoft-owned hosts). Group/channel references require the optional scoped drive configuration above; unsupported or unconsented files are explicitly reported as skipped.
  • Threads and sessions: channel replies thread under the user's message. Personal chats keep one continuous session. Group chats remain visually flat, but every new message to the bot starts a separate session. Quote an earlier user message or bot reply to continue that session; quote mappings survive daemon restarts. Include an @mention in group-chat quoted replies too, because Teams does not deliver them to the bot by default without one. Current quotedReply entities and legacy Reply blockquotes identify the quoted message; ordinary blockquotes do not. See Microsoft's quoted reply format.
  • Formatting: no tables, no headings, no inline images, and lists render on desktop only — all four are degraded on the way out by src/platforms/format/degrade.js.
  • Mentions are structural: the text carries <at>Name</at> and the activity must carry a matching entity. The formatter emits both together.
  • Private controls: settings/files use private Teams windows. Approval controls stay in the source conversation; no supplemental decision links are sent to a separate personal chat.

What works where

Slack Google Chat Teams
Turns in channels and DMs ✅ ✅ ✅
Channel folder confinement, authorization, MCP allowlist ✅ ✅ ✅
Attachments in ✅ partial partial
Threads ✅ spaces only channels only
Live progress rendering ✅ working placeholder → answer working placeholder → answer (branch)
Interactive approval buttons ✅ native Cards v2 native cards (branch)
Approvals by signed link ✅ (in addition to the buttons) native cards; legacy connector fallback native thread cards; no supplemental links
Session commands full Slack controls native help/settings/files + text controls text subset + native settings/files (branch)
Reaction controls (start / stop / acknowledge) ✅ with Workspace Events (setup step 8) with event delivery
Native tables / charts / Lists / canvases ✅ ❌ ❌
Escalation to full-access in an admin-mode channel ✅ ❌ ❌

The last row is not an oversight. Escalation requires an interactive permission prompt the author can answer; until a surface has one, a run there uses the channel folder's permission allowlist like every other run — which fails closed.

Approvals use native controls or private links. Slack's Block Kit buttons are the primary control and are unchanged. Alongside them, each approval card and each busy-thread card is also minted as short-lived, single-use, HMAC-signed URLs — one per action the recipient may take — delivered privately to the person who raised the request: an ephemeral where the platform has one (Slack). Google Chat and Teams use their original conversation's native approval card and does not mint or send supplemental links; failed card delivery refuses the action with retry guidance there. Opening a link shows a confirmation page and decides nothing; the page's Confirm button performs the decision through exactly the code a button click runs. The gateway needs a Public URL for a link to be reachable from outside the host, and the behaviour is Settings → Connection → Approval links (auto / always / off). See FEATURES.md → Modes & approvals for the security properties.

On teams-ms, native Teams approval cards call the shared actor-checked decision path. Inline cards do not automatically widen the engine permission policy: escalation remains separately gated, and its live acceptance must pass before release. Google Chat uses requester-only Cards v2 settings panels; Teams native settings dialogs are implemented. Slack's full busy-thread interaction flow remains unavailable in Teams.


Operating both at once

Channel folders are namespaced by surface: ~/ChannelGate/slack/<slug>, ~/ChannelGate/google-chat/<slug>, ~/ChannelGate/teams/<slug>. A Slack #ops and a Teams "Ops" are separate conversations with separate folders, sessions, memory, and settings — they never share state, and neither can see the other's files.

Conversation ids are namespaced too (gchat:spaces/AAA, teams:19:…); Slack ids stay bare, so every row written before multi-platform support keeps resolving with no migration.

Graph event processing intentionally accepts only edits/reaction additions from the preceding 24 hours (and after subscription activation). This is shorter than the seven-day deduplication retention, so later updates cannot replay old message history. If Graph omits messageHistory, activation may use a present reaction's own createdDateTime, never the message's modification time. A persisted snapshot-support cutoff excludes reactions added before the upgraded transport starts, so remove and re-add the activation emoji for a fresh post-upgrade test. Existing removal history prevents fallback. Stop/Tick may use the same fallback under their own persisted cutoff, only within ten minutes of the reaction's createdDateTime. Personal Bot Framework conversations use different IDs from Graph chats: personal edits and reactions to bot answers stay on the native bot event path. Graph subscriptions here cover group chats and channels. Removing Xavier retires the corresponding subscription and invalidates its queued event work. Graph file descriptors are reported when unavailable; this option does not grant SharePoint file-download permissions.

For Graph delivery troubleshooting, filter the admin Audit feed for teams_graph_callback_*. received proves a notification POST reached the Graph handler; accepted records the verified conversation/message target only after successful processing or durable acceptance. rejected records a fixed reason (invalid batch, unknown subscription, invalid resource, tenant/client-state mismatch or unsupported change), and failed records a retryable 503 and optional numeric provider status. Validation challenges have their own validated event and do not prove notification delivery. Diagnostics exclude tokens, request headers, bodies and unverified identifiers. If no receipt exists, correlate provider delivery with ingress/security logs; an empty queue cannot distinguish a blocked POST from an absent provider notification.