Slack channel plugin for the TAI ecosystem — delivers ask_user questions to a Slack channel and bridges threaded replies back to the interactions callback door.
Project description
tai42-channel-slack
A Slack channel plugin for the TAI ecosystem. ask_user(..., channel="slack")
posts the question to a configured Slack channel via chat.postMessage; the
human replies in the message's thread; the Slack Events API delivers the
reply to this plugin's inbound door, which verifies the request signature and
forwards the typed answer to the interaction's public callback URL — so a tool
blocked on a question resumes with a real human answer, out-of-band.
The TAI ecosystem
TAI is an open-source runtime for MCP tools, agents, and workflows. A channel
is a registered deliverer that pushes an interaction question to a human on a
specific medium and bridges the reply back into the interactions store. This
package is one such deliverer (Slack); siblings back the same contract with
Telegram or Twilio (SMS/WhatsApp). The ecosystem is open-ended: any package can
back the same contract, so this repo is this channel's own full doc home, and
the documentation site covers the platform-level story:
- Interactions concept: https://tai42.ai/concepts/interactions
- Build a channel plugin (author guide): https://tai42.ai/guides/authors/channel
- Ecosystem catalog: https://tai42.ai/reference/catalog
Its only tai-* dependencies are tai42-contract (the Channel protocol,
ChannelDelivery, ChannelDeliveryError, and the tai42_app handle) and
tai42-kit[redis] (HttpxClient, RedisClient, TaiBaseSettings, and the
settings cache). Beyond those it depends on httpx, starlette, and
pydantic / pydantic-settings — the whole Slack surface is two HTTPS POSTs
plus stdlib hmac; no slack_sdk, no Bolt.
Install
Requires Python 3.13+. Install from PyPI into the environment that runs the server:
uv add tai42-channel-slack
Or from source — clone this repo and add it as an editable dependency:
git clone https://github.com/tai42ai/tai-channel-slack # next to your app checkout
cd /path/to/your/app
uv add --editable ../tai-channel-slack
Discovery
The skeleton discovers this plugin through the manifest's channel_modules
field — at app load it imports every module under each named package, and
tai42_channel_slack.register registers the "slack" channel and the inbound
route POST /api/channels/slack/inbound as a side-effect. A bare
import tai42_channel_slack registers nothing (library use).
channel_modules: [tai42_channel_slack]
ask_user(..., channel="slack") then selects it by name.
Configuration
Settings are read from the CHANNEL_SLACK_ environment group (see
SlackSettings / SlackRedisSettings). Credentials are operator-bound
environment configuration — never LLM-visible tool parameters. The recipient
is resolved per question: a caller may request one (ask_user(..., recipient=...)), and the plugin sends to it only if it is on the operator
allowlist — an unlisted recipient fails loudly, nothing is sent; a question
without a requested recipient goes to the operator default.
| Env var | Meaning |
|---|---|
CHANNEL_SLACK_BOT_TOKEN |
bot token, xoxb-…, scope chat:write (SecretStr) |
CHANNEL_SLACK_SIGNING_SECRET |
Events API signing secret (SecretStr) |
CHANNEL_SLACK_ALLOWED_RECIPIENTS |
whitelist of channel/DM ids a caller may request — comma-separated or a JSON list; empty rejects every requested recipient |
CHANNEL_SLACK_DEFAULT_RECIPIENT |
channel/DM id (C…/D…) used when the caller requests none |
CHANNEL_SLACK_REDIS_URL |
correlation store, e.g. redis://redis:6379/0 |
CHANNEL_SLACK_HTTP_TIMEOUT_SECONDS |
outbound HTTP budget, default 30 |
All credential/target fields default to unset, so importing the package never demands configuration; a delivery or inbound event against an unconfigured channel raises loudly, naming the missing env var. An unset or empty signing secret fails CLOSED — the inbound door raises rather than ever verifying against a forgeable key.
Slack app setup
All links: https://docs.slack.dev
- Create a Slack app; install it to the workspace with bot scope
chat:write(pluschannels:historyfor a public channel target orim:historyfor a DM target); copy the bot token intoCHANNEL_SLACK_BOT_TOKEN. - Copy the app's signing secret into
CHANNEL_SLACK_SIGNING_SECRET. - Event Subscriptions → enable, Request URL =
{INTERACTIONS_PUBLIC_BASE_URL}/api/channels/slack/inbound(the app must already be running so the signedurl_verificationhandshake succeeds), and subscribe to bot eventsmessage.channels(and/ormessage.im). - Invite the bot to every target channel; put the fallback channel id in
CHANNEL_SLACK_DEFAULT_RECIPIENTand list the ids callers may request inCHANNEL_SLACK_ALLOWED_RECIPIENTS. - There is no boot-time registration call — the dashboard Request URL is the registration.
How an answer travels
- A tool calls
ask_user(question, channel="slack", ...). The runtime persists the interaction, mints a single-use callback ticket, and hands the plugin aChannelDelivery. SlackChannel.deliverresolves the recipient — the caller-requested id if it is onCHANNEL_SLACK_ALLOWED_RECIPIENTS(unlisted refuses loudly, nothing sent), elseCHANNEL_SLACK_DEFAULT_RECIPIENT— and posts the question there viachat.postMessage. Success is the JSONok: truefield — Slack answers HTTP 200 even for failed sends, so the status code is never the signal; any failure raisesChannelDeliveryErrorloudly. One send attempt, no retry (chat.postMessageoffers no idempotency key).- For
textandselectquestions the message says to reply in the thread and shows the deadline; the response'stsis stored in Redis asts → callback_urlwith TTL = the question's remaining budget. Forconfirmandexternalquestions the callback URL rides the message as a plain link instead — the human answers through the callback door directly, and no correlation state exists. - The human replies in the thread. The Slack Events API POSTs the event to
POST /api/channels/slack/inbound, which reads a bounded body, verifies theX-Slack-Signaturev0 HMAC over the raw bytes (constant-time, ±300 s replay window, fail-closed), dedupes onevent_id, matches the reply'sthread_tsagainst the correlation store, and forwards{"answer": "<typed text>"}to the stored callback URL. - The public callback door validates the answer against the question's stored
format and records it; the blocked
ask_userreturns it.
Operational notes: answers must be typed in-thread; non-answer traffic (edits, bot echoes, other channels, top-level messages, threads with no pending question) is always acked 2xx so Slack never disables the event subscription; the question message shows the deadline, and the waiting ask surfaces an unanswered question through its own timeout.
Development
uv venv --python 3.13
uv pip install --no-sources --group dev --editable .
uv run --no-sync pytest
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync pyright
The live integration suite (pytest -m integration) posts a real message to
the configured workspace; it skips cleanly when the CHANNEL_SLACK_*
credentials are absent.
Deferred by design: Socket Mode inbound (needs a managed background task —
platform scope; webhook mode needs exactly the public URL the external
interactions flow already requires), fire-and-forget notify(), and Block Kit
buttons (block_actions interactivity) — v1 delivers plain text.
License
Apache-2.0. See LICENSE and NOTICE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tai42_channel_slack-0.1.1.tar.gz.
File metadata
- Download URL: tai42_channel_slack-0.1.1.tar.gz
- Upload date:
- Size: 38.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5736a1a7c91a3d2aeb03aae399fb0b2ee7b8d84b34fd10cbdfbee2619ea077ed
|
|
| MD5 |
39d4e2b40f6aa3c30279864202918434
|
|
| BLAKE2b-256 |
6962c1ea9dc0325dcb36273e2547667ac7fff446f402509f4a74f53bab7865bc
|
File details
Details for the file tai42_channel_slack-0.1.1-py3-none-any.whl.
File metadata
- Download URL: tai42_channel_slack-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6553b56f1d21a11eda16f9f2999ac78ea4f5a6fd6018485e63aa2b2d26a5f422
|
|
| MD5 |
a011623a6520648aa2404111face45a9
|
|
| BLAKE2b-256 |
714147770ef2e56a496217afd6584ecea116eeb5b04baa79ee35ea46f4124b98
|