docs/src/slack.md
Rama main · réplica de lectura del repositorio original.
# Slack
Moltis can connect to Slack as a bot, letting you chat with your agent from any
Slack workspace. The integration supports both **Socket Mode** (default, no
public URL needed) and **Events API** (webhook-based).
## How It Works
```
┌──────────────────────────────────────────────────────┐
│ Slack API │
│ (Socket Mode / Events API) │
└──────────────────┬───────────────────────────────────┘
│ WebSocket (Socket Mode)
│ or HTTP POST (Events API)
▼
┌──────────────────────────────────────────────────────┐
│ moltis-slack crate │
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │ Handler │ │ Outbound │ │ Plugin │ │
│ │ (inbound) │ │ (replies) │ │ (lifecycle) │ │
│ └────────────┘ └────────────┘ └────────────────┘ │
└──────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Moltis Gateway │
│ (chat dispatch, tools, memory) │
└──────────────────────────────────────────────────────┘
```
With **Socket Mode** (the default), the bot opens an outbound WebSocket
connection to Slack — no public URL, port forwarding, or TLS certificate is
needed. With **Events API** mode, Slack sends HTTP POST requests to your server,
requiring a publicly reachable endpoint.
## Prerequisites
Before configuring Moltis, create a Slack app:
1. Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App**
2. Choose **From scratch**, name the app, and select your workspace
3. Navigate to **OAuth & Permissions** and add these Bot Token Scopes:
- `app_mentions:read` — read @mentions
- `chat:write` — send messages
- `files:write` — upload files generated by the agent
- `im:history` — read DM history
- `reactions:write` — add acknowledgment reactions (👀/✅/❌; see [Acknowledgment Reactions](#acknowledgment-reactions))
- `reactions:read` — read reactions (only for inbound reaction triggers)
4. Under **Event Subscriptions > Subscribe to bot events**, add:
- `app_mention` — channel messages that @mention the bot
- `message.im` — direct messages to the bot (paired with `im:history`)
- `reaction_added` — only if using inbound reaction triggers (paired with `reactions:read`)
5. If you use `mention_mode = "always"`, add the matching scope and bot event for every conversation type where the bot should receive all messages:
| Conversation type | Bot Token Scope | Bot event |
|-------------------|-----------------|-----------|
| Public channels | `channels:history` | `message.channels` |
| Private channels | `groups:history` | `message.groups` |
| Multiparty direct messages (MPIMs) | `mpim:history` | `message.mpim` |
Both entries in a row are required: the history scope permits access, while
the `message.*` event delivers new messages to Moltis. Private channels and
MPIMs are handled as channels by Moltis, so `group_policy`, `mention_mode`,
and `channel_allowlist` apply to them.
6. Click **Install to Workspace** and copy the **Bot User OAuth Token** (`xoxb-...`)
7. For Socket Mode (recommended):
- Go to **Socket Mode** and enable it
- Generate an **App-Level Token** (`xapp-...`) with the `connections:write` scope
8. For Events API mode:
- Go to **Event Subscriptions** and enable it
- Set the exact URLs shown in [Events API Mode](#events-api-mode)
- Copy the **Signing Secret** from **Basic Information**
```admonish warning
The bot token and app token are secrets — treat them like passwords. Never
commit them to version control. Moltis stores them with `secrecy::Secret` and
redacts them from logs.
```
## Configuration
Add a `[channels.slack.<account-id>]` section to your `moltis.toml`:
```toml
[channels.slack.my-bot]
bot_token = "xoxb-your-bot-token"
app_token = "xapp-your-app-token"
```
Make sure `"slack"` is included in `channels.offered`:
```toml
[channels]
offered = ["slack"]
```
### Configuration Fields
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `bot_token` | **yes** | — | Bot user OAuth token (`xoxb-...`) |
| `app_token` | **yes**\* | — | App-level token for Socket Mode (`xapp-...`). \*Required for `socket_mode`. |
| `api_base_url` | no | `"https://slack.com/api"` | Slack Web API base URL. Override only for Slack-compatible proxies, mock servers, or gateways. |
| `connection_mode` | no | `"socket_mode"` | Connection method: `"socket_mode"` or `"events_api"` |
| `signing_secret` | no\* | — | Signing secret for Events API request verification. \*Required for `events_api`. |
| `dm_policy` | no | `"allowlist"` | Who can DM the bot: `"open"`, `"allowlist"`, or `"disabled"` |
| `group_policy` | no | `"open"` | Who can talk to the bot in channels: `"open"`, `"allowlist"`, or `"disabled"` |
| `mention_mode` | no | `"mention"` | When the bot responds in channels: `"always"`, `"mention"`, or `"none"` |
| `allowlist` | no | `[]` | Slack user IDs allowed to DM the bot (when `dm_policy = "allowlist"`) |
| `operators` | no | `[]` | Exact Slack user IDs eligible for privileged commands in proven direct chats. Empty means nobody. |
| `channel_allowlist` | no | `[]` | Slack channel IDs allowed to interact with the bot |
| `untrusted_audience` | no | `"public"` | Tool audience ceiling for turns outside an operator direct chat: `"public"` or `"trusted"` |
| `untrusted_tools` | no | `"deny_all"` | Tool name policy for those turns: `"deny_all"`, or `"policy"` to let configured policy layers decide |
| `otp_self_approval` | no | `true` | Enable OTP self-approval for non-allowlisted DM users |
| `otp_cooldown_secs` | no | `300` | Cooldown after failed OTP attempts |
| `model` | no | — | Override the default model for this channel |
| `model_provider` | no | — | Provider for the overridden model |
| `stream_mode` | no | `"edit_in_place"` | Streaming mode: `"edit_in_place"`, `"native"`, or `"off"`. Native mode renders live text and tool task cards in one message. |
| `edit_throttle_ms` | no | `500` | Minimum milliseconds between streaming edit updates |
| `thread_replies` | no | `true` | Reply in threads |
| `ack_reactions` | no | `true` | Acknowledge inbound messages with emoji reactions (👀 → ✅/❌). Only applied when the bot is directly addressed (DM or @mention). |
| `reaction_triggers` | no | `false` | Route inbound user reactions into the agent as messages (e.g. react ✅ to approve). |
| `reaction_trigger_emojis` | no | `[]` | When `reaction_triggers` is on, only these emoji shortcodes trigger the agent. Empty = any emoji. |
| `rich_blocks` | no | `false` | Render completed replies as Block Kit blocks (headings, dividers, code) with a plain-text fallback. Rich rendering takes precedence and disables streaming. |
| `channel_overrides` | no | `{}` | Per-channel model/provider overrides (see below) |
| `user_overrides` | no | `{}` | Per-user model/provider overrides (see below) |
```admonish important title="Allowlist values are strings"
All allowlist entries must be **strings**. Use Slack user IDs like
`["U0123456789"]`. Matching is case-insensitive and supports glob wildcards.
```
### Full Example
```toml
[channels]
offered = ["slack"]
[channels.slack.my-bot]
bot_token = "xoxb-..."
app_token = "xapp-..."
api_base_url = "https://slack.com/api"
connection_mode = "socket_mode"
dm_policy = "allowlist"
group_policy = "open"
mention_mode = "mention"
allowlist = ["U0123456789", "U9876543210"]
operators = ["U0123456789"]
channel_allowlist = ["C0123456789"]
otp_self_approval = true
otp_cooldown_secs = 300
model = "claude-sonnet-4-20250514"
model_provider = "anthropic"
stream_mode = "edit_in_place"
edit_throttle_ms = 500
thread_replies = true
ack_reactions = true
# Per-channel override: use a different model in a specific Slack channel
[channels.slack.my-bot.channel_overrides.C0123456789]
model = "gpt-4o"
# Per-user override: use a specific model/provider for a Slack user
[channels.slack.my-bot.user_overrides.U0123456789]
model = "claude-sonnet-4-20250514"
model_provider = "anthropic"
```
### Tools in Shared Channels
By default, Slack channels and DMs from non-operators receive no tools. To let
those turns use tools, lift both parts of the untrusted-turn ceiling and use a
tool policy to restrict what is available:
```toml
[channels.slack.my-bot]
untrusted_audience = "trusted"
untrusted_tools = "policy"
[channels.slack.my-bot.tools.groups.channel]
allow = ["linear_*"]
[channels.slack.my-bot.tools.groups.direct]
allow = ["linear_*"]
```
`untrusted_audience = "trusted"` makes MCP, WASM, and other trusted-audience
tools eligible. `untrusted_tools = "policy"` removes the blanket name denial.
Both settings are required for MCP tools, and without a restrictive policy the
turn can reach every tool allowed by the remaining policy layers.
The ceiling applies to every turn outside an operator direct chat, so restrict
both `channel` and `direct` when non-operator DMs are enabled. For accounts
created in the web UI, Advanced Config can set the two ceiling fields, but
database-backed `tools.groups` policies are not currently part of runtime
policy resolution. Configure a restrictive global or provider policy before
lifting the ceiling on a UI-managed account.
These settings do not expose `/sh`, privileged commands, or owner-private
prompt context in shared rooms. Those remain restricted to operators in proven
direct chats.
### Events API Mode
Set these Request URLs in your Slack app (replace `<id>` with the account ID you
configured in Moltis):
| Slack setting | URL |
|---------------|-----|
| Event Subscriptions | `https://your-host/api/channels/slack/<id>/events` |
| Interactivity & Shortcuts | `https://your-host/api/channels/slack/<id>/interactions` |
| Slash Commands | `https://your-host/api/channels/slack/<id>/commands` |
These endpoints are reachable without a Moltis session — Slack cannot present
one — and instead verify Slack's request signature (HMAC) and timestamp before
doing any work.
If you prefer webhook-based delivery instead of Socket Mode:
```toml
[channels.slack.my-bot]
bot_token = "xoxb-..."
connection_mode = "events_api"
signing_secret = "abc123..."
```
This requires your Moltis instance to be reachable from the internet (or use
[Tailscale Funnel](configuration.md#tailscale-integration)).
### Slack-Compatible Proxies
Moltis normally talks to Slack at `https://slack.com/api`. For testing, local
development, or Slack-compatible gateways, set `api_base_url`:
```toml
[channels.slack.proxy]
bot_token = "xoxb-proxy-token"
app_token = "xapp-proxy-token"
api_base_url = "https://proxy.example/api"
connection_mode = "socket_mode"
```
For Socket Mode, Moltis calls `apps.connections.open` on this endpoint and
connects to the WebSocket URL returned by that API.
The endpoint must be a public HTTP(S) URL. Localhost, private-network,
link-local, and other non-public IP targets are rejected because Slack API calls
carry the bot token. Hostnames are resolved via DNS when the account starts, and
every resolved address is checked against the same policy — a DNS name pointing
at a private or cloud-metadata address is rejected, and a hostname that fails to
resolve is rejected too (validation fails closed).
#### Allowing internal hosts
If you deliberately front Slack with a proxy on an internal host, an operator can
allow specific hosts back in with the `MOLTIS_SLACK_API_BASE_URL_ALLOWLIST`
environment variable — a comma-separated list of exact hostnames or IPs:
```sh
MOLTIS_SLACK_API_BASE_URL_ALLOWLIST="proxy.internal,127.0.0.1"
```
Only hosts on this list bypass the private-address guard, and matching is on the
exact host (so `localhost` and `127.0.0.1` must each be listed if you want both).
The allowlist is intentionally an environment variable, not a web-editable
setting, so this SSRF exception stays under operator control. Cloud metadata
addresses (`169.254.169.254`, `fd00:ec2::254`) remain blocked even when
allowlisted, including when an allowlisted hostname resolves to one.
If using a proxy, ensure it supports Slack's native streaming methods before
setting `stream_mode = "native"`.
## Access Control
Slack uses the same gating system as Telegram, Discord, and other channels.
### DM Policy
| Value | Behavior |
|-------|----------|
| `"allowlist"` | Only users listed in `allowlist` can DM (default). If the allowlist is empty, unknown users receive an OTP challenge when `otp_self_approval = true`. |
| `"open"` | Anyone in the workspace can DM the bot |
| `"disabled"` | DMs are silently ignored |
When `dm_policy = "allowlist"` and `otp_self_approval = true`, unknown DM users
receive a verification prompt. The PIN is visible to the bot owner in the web UI
under **Channels → Senders**. After a correct PIN reply, Moltis adds the sender's
Slack user ID, such as `U0123456789`, to the account allowlist.
### Group Policy
| Value | Behavior |
|-------|----------|
| `"open"` | Bot responds in any channel it's invited to (default) |
| `"allowlist"` | Only channels listed in `channel_allowlist` are allowed. An empty channel allowlist denies all channels. |
| `"disabled"` | Channel messages are silently ignored |
### Mention Mode
| Value | Behavior |
|-------|----------|
| `"mention"` | Bot only responds when @mentioned (default) |
| `"always"` | Bot responds to every message in allowed channels |
| `"none"` | Bot never responds in channels (useful for DM-only bots) |
Slack only sends general public-channel messages when the app has both the
`channels:history` Bot Token Scope and the `message.channels` bot event. For
private channels, use `groups:history` with `message.groups`; for MPIMs, use
`mpim:history` with `message.mpim`. The default `"mention"` mode only needs the
`app_mentions:read` scope and `app_mention` event for channel mentions.
### Allowlist Matching
Allowlist entries support:
- **Exact match** (case-insensitive): `"U0123456789"`
- **Glob wildcards**: `"U012*"`, `"*admin*"`
## Streaming
Slack supports three streaming modes:
| Mode | Behavior |
|------|----------|
| `"edit_in_place"` | Sends a placeholder message and edits it as tokens arrive (default) |
| `"native"` | Uses Slack's streaming API for live response text and tool task cards |
| `"off"` | No streaming — sends the full response as a single message |
The `edit_in_place` mode throttles updates to `edit_throttle_ms` milliseconds
(default: 500) to avoid Slack API rate limits.
Native mode calls those three methods directly at `api_base_url` with the bot
token and requires a thread target. With `thread_replies = false`, Moltis falls
back to top-level edit-in-place streaming instead. For threaded replies, Moltis
does not probe whether the Slack app, workspace, or compatible proxy supports
the native methods, and a native API failure does not switch an active stream to
edit-in-place mode. The completed reply still falls back to normal delivery when
the native stream fails. Moltis first tries to stop and delete the partial native
message so the fallback does not duplicate content already accepted by Slack. If
Slack cannot delete it, Moltis retains that message with a visible streaming-error
notice and retries the stop after draining the remaining response. Only after
Slack confirms the stream is stopped does Moltis replace the retained message
with the complete text through `chat.update`. If Slack still cannot stop or update
the message, Moltis makes a final deletion attempt. A successful deletion preserves
normal fallback delivery; if deletion also fails after Slack confirmed the stop,
Moltis retains the terminal failure notice and suppresses duplicate fallback
content.
Responses too large for Slack's update limit are replaced by the visible error
notice instead of a misleading partial answer.
Native requests send standard Markdown unchanged through Slack's `markdown_text`
field and use `edit_throttle_ms`. Tool calls appear in the same streamed message
as Slack plan/task cards. Cards show only the tool name and lifecycle state
(`in_progress`, `complete`, or `error`); Moltis does not send tool arguments,
reasoning, paths, commands, or raw tool results to the card. Parallel calls are
tracked by their stable tool-call IDs, and unfinished calls are marked as errors
before the stream closes. When `rich_blocks = true`, rich rendering takes
precedence: streaming (including
native streaming) is disabled and the completed response is sent once through
the Block Kit path.
## Thread Replies
By default (`thread_replies = true`), the bot replies in a thread attached to
the user's message. Set `thread_replies = false` to have the bot reply directly
in the channel.
## Acknowledgment Reactions
Because Slack bots cannot show a typing indicator, Moltis acknowledges messages
with emoji reactions so you know your message was received and is being worked
on:
- 👀 (`eyes`) is added as soon as the message starts processing.
- While the agent runs, the reaction swaps to a **phase** emoji reflecting the
current tool — 🌐 web, 💻 shell, ✏️ file edits, 🛠️ other tools — and shows ⏳ if
a step runs long. Rapid changes are debounced so the reaction doesn't flicker.
- ✅ (`white_check_mark`) replaces it when the reply is delivered.
- ❌ (`x`) replaces it if the turn fails. A cancelled turn just removes 👀.
Reactions are only added when the bot is **directly addressed** — a direct
message, or a channel message that @mentions the bot — never on general channel
chatter. Set `ack_reactions = false` to disable them.
Each inbound message owns its own reaction for its whole life, including while
it waits behind an in-flight turn: a message queued behind another keeps its 👀
and is resolved when its own turn runs. In `collect` queue mode one reply
answers several messages, and every one of them receives the terminal reaction.
A reply that fails to deliver is marked ❌ rather than ✅.
The bot needs the `reactions:write` OAuth scope (and `reactions:read` if you
also use inbound reaction triggers). Reaction failures are non-fatal and never
block the reply.
## Reaction Triggers
With `reaction_triggers = true`, when a user adds an emoji reaction to a message,
Moltis routes it into the agent as a synthetic message (threaded under the
reacted message so the agent sees the original content as context). This enables
flows like "react ✅ to approve" or "react 👍 to continue".
- The bot's own acknowledgment reactions (👀/✅/❌) are always ignored, so
triggers never loop.
- Only *added* reactions trigger (not removals), and only from senders who pass
the normal DM/channel access policy.
- Only reactions on the **bot's own messages** trigger. Without this, any member
could point the agent at an unrelated person's message just by reacting to it.
- Restrict which emoji count with `reaction_trigger_emojis` (shortcodes without
colons, e.g. `["white_check_mark", "thumbsup"]`). Empty means any emoji.
This requires the `reactions:read` scope and the `reaction_added` event
subscription.
```admonish note title="Thread context on threaded replies"
Slack's `reaction_added` event only carries the reacted message's own
timestamp, not its thread root. When a user reacts to a message that is itself a
reply inside a thread, the agent is threaded under that reply and sees only the
reacted message as context — not the whole thread. Reactions on top-level
messages are unaffected.
```
## Troubleshooting
### Bot doesn't respond
- Verify the bot and app tokens are correct
- Check that Socket Mode is enabled in the Slack app settings
- Check `dm_policy` — if set to `"allowlist"`, make sure your Slack user ID
is in `allowlist`
- Ensure the bot has been invited to channels you want it to respond in
- Look at logs: `RUST_LOG=moltis_slack=debug moltis`
### Bot doesn't respond in channels
- Check `mention_mode` — if `"mention"`, you must @mention the bot
- Check `group_policy` — if `"disabled"`, channel messages are ignored
- Check `channel_allowlist` — if non-empty, the channel must be listed
- Ensure the bot is a member of the channel (invite it with `/invite @botname`)