Skip to main content

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

CI License: Apache 2.0

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:

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

  1. Create a Slack app; install it to the workspace with bot scope chat:write (plus channels:history for a public channel target or im:history for a DM target); copy the bot token into CHANNEL_SLACK_BOT_TOKEN.
  2. Copy the app's signing secret into CHANNEL_SLACK_SIGNING_SECRET.
  3. Event Subscriptions → enable, Request URL = {INTERACTIONS_PUBLIC_BASE_URL}/api/channels/slack/inbound (the app must already be running so the signed url_verification handshake succeeds), and subscribe to bot events message.channels (and/or message.im).
  4. Invite the bot to every target channel; put the fallback channel id in CHANNEL_SLACK_DEFAULT_RECIPIENT and list the ids callers may request in CHANNEL_SLACK_ALLOWED_RECIPIENTS.
  5. There is no boot-time registration call — the dashboard Request URL is the registration.

How an answer travels

  1. A tool calls ask_user(question, channel="slack", ...). The runtime persists the interaction, mints a single-use callback ticket, and hands the plugin a ChannelDelivery.
  2. SlackChannel.deliver resolves the recipient — the caller-requested id if it is on CHANNEL_SLACK_ALLOWED_RECIPIENTS (unlisted refuses loudly, nothing sent), else CHANNEL_SLACK_DEFAULT_RECIPIENT — and posts the question there via chat.postMessage. Success is the JSON ok: true field — Slack answers HTTP 200 even for failed sends, so the status code is never the signal; any failure raises ChannelDeliveryError loudly. One send attempt, no retry (chat.postMessage offers no idempotency key).
  3. For text and select questions the message says to reply in the thread and shows the deadline; the response's ts is stored in Redis as ts → callback_url with TTL = the question's remaining budget. For confirm and external questions the callback URL rides the message as a plain link instead — the human answers through the callback door directly, and no correlation state exists.
  4. 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 the X-Slack-Signature v0 HMAC over the raw bytes (constant-time, ±300 s replay window, fail-closed), dedupes on event_id, matches the reply's thread_ts against the correlation store, and forwards {"answer": "<typed text>"} to the stored callback URL.
  5. The public callback door validates the answer against the question's stored format and records it; the blocked ask_user returns 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tai42_channel_slack-0.1.1.tar.gz (38.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tai42_channel_slack-0.1.1-py3-none-any.whl (23.5 kB view details)

Uploaded Python 3

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

Hashes for tai42_channel_slack-0.1.1.tar.gz
Algorithm Hash digest
SHA256 5736a1a7c91a3d2aeb03aae399fb0b2ee7b8d84b34fd10cbdfbee2619ea077ed
MD5 39d4e2b40f6aa3c30279864202918434
BLAKE2b-256 6962c1ea9dc0325dcb36273e2547667ac7fff446f402509f4a74f53bab7865bc

See more details on using hashes here.

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

Hashes for tai42_channel_slack-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6553b56f1d21a11eda16f9f2999ac78ea4f5a6fd6018485e63aa2b2d26a5f422
MD5 a011623a6520648aa2404111face45a9
BLAKE2b-256 714147770ef2e56a496217afd6584ecea116eeb5b04baa79ee35ea46f4124b98

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page