Skip to content

Slack adapter silently drops @-mentions from other bots #55

@initializ-mk

Description

@initializ-mk

Summary

The Slack channel adapter ignores every event whose bot_id is non-empty, including events where another bot @-mentions the forge-agent. This blocks legitimate integration patterns: a scheduled bot, monitoring alert, CI/CD bot, or workflow that posts "<@FORGE_AGENT> please summarize this incident" will never be seen by the agent, even though a human posting the same text triggers a response.

The filter is hardcoded — there is no way to admit specific bot IDs. A correct fix also has to handle the inverse case: the agent must never respond to its own messages, even when the allowlist is wide open.

Root cause

forge-plugins/channels/slack/slack.go:328-331:

// Skip bot messages.
if payload.Event.BotID != "" {
    continue
}

This runs before any mention-matching logic. Slack sets bot_id on every message authored by an app, including the message event and the app_mention event Slack delivers when a bot mentions another bot. So the agent is doubly insulated:

Event type Slack delivers Filter that drops it
message with bot_id != "" BotID != "" check at slack.go:329
app_mention with bot_id != "" Same BotID != "" check at slack.go:329 (runs before the Type == "app_mention" skip at slack.go:341)

There is no allowlist or escape hatch, and equally importantly, no self-id guard — if the filter were simply lifted, the agent could respond to its own messages and hot-loop.

Reproduction

  1. Configure a forge-agent with the Slack adapter and confirm a human @-mention triggers a response in a channel.
  2. From a second Slack app (Workflow Builder, a scheduler bot, a monitoring bot, anything with chat:write), post "<@FORGE_AGENT_USER_ID> ping" to the same channel.
  3. Observed: no reaction, no response, no log line — the event is silently dropped at slack.go:329.
  4. Expected (or at least configurable): the agent recognizes the mention and responds the same way it would for a human.

Why the filter exists, and what it overshoots

Filtering bot messages by default is correct — bot-to-bot loops are a real risk (two agents @-mentioning each other can hot-loop a workspace, and the agent talking to itself is just as bad). But the current implementation is binary: all bot messages are dropped. Legitimate cases the binary filter blocks:

  • A cron / scheduler bot routing daily standup prompts to the agent.
  • A monitoring tool (PagerDuty, Datadog, custom) tagging the agent on an incident summary.
  • A workflow builder step that asks the agent to draft a response.
  • A CI/CD bot asking the agent to comment on a release.

In all of these the human set up the integration deliberately, and the loop risk is well-defined (one bot, known ID).

Proposed fix

Two pieces, both inside the Slack adapter:

1. Capture the agent's own bot_id at startup. resolveBotID already calls auth.test to discover user_id. The same response payload includes bot_id — parse it and store it on Plugin (e.g. p.ownBotID). This is the self-id guard.

2. Replace the unconditional skip with an allowlist + self-id check. A new optional setting on the channel YAML controls which other bots are admitted:

# slack-config.yaml
adapter: slack
settings:
  app_token_env: SLACK_APP_TOKEN
  bot_token_env: SLACK_BOT_TOKEN
  allow_bot_ids: B0123ABC,B0456DEF   # comma-separated; default empty = current behavior

Sketch:

botID := payload.Event.BotID
if botID != "" {
    // Hard rule, always on: never respond to our own messages, even if
    // the allowlist would otherwise admit them. Prevents self-mention loops.
    if botID == p.ownBotID {
        continue
    }
    // Allowlist gate for every other bot.
    if !p.allowBotIDs[botID] {
        continue
    }
}

The mention-matching block at slack.go:346-361 already requires <@botUserID> in the text, so an admitted bot still has to explicitly tag the agent — bots in the allowlist that chatter without tagging won't trigger responses.

Why bot_id rather than app_id or app name

Slack bot_id is stable per-bot-user within a workspace and visible in the same event payload (payload.event.bot_id) without an extra API call. app_id is also available but resolving an app name → app_id requires a separate apps.info call. For an MVP, bot_id is the lowest-friction key. Documentation would point operators at the easiest way to discover a bot's ID (Slack admin → Manage apps → the app's Bot User OAuth page).

Loop-safety

Three safeguards keep bot-to-bot and self-loops bounded even after admitting some bot mentions:

  1. Self-id guard (unconditional). payload.Event.BotID == p.ownBotID short-circuits before the allowlist is consulted. Even an allow_bot_ids: "*" style wildcard (if ever added) would still exclude the agent itself. This is the non-negotiable rule.
  2. The allowlist itself. Bots not on it remain ignored. So an inadvertent loop with another bot requires the operator to explicitly add a misbehaving bot.
  3. Mention requirement. Even admitted bots must include <@FORGE_AGENT> in the text. A bot posting general chatter without tagging the agent does not trigger a response.

Worth considering as a follow-up (not blocking): a configurable rate limit (max_bot_invocations_per_minute) for messages whose bot_id is non-empty — useful if a scheduler bot misfires.

Affected files

  • forge-plugins/channels/slack/slack.go:328-331 — the binary bot_id filter to replace with allowlist-aware admission plus self-id guard
  • forge-plugins/channels/slack/slack.go:71-103resolveBotID extend the auth.test response parsing to capture bot_id (the agent's own) alongside user_id
  • forge-plugins/channels/slack/slack.go:339-343app_mention dedup skip; revisit if the allowlist needs to apply to the app_mention path differently
  • forge-plugins/channels/slack/slack.go (type definition near bot_id JSON tag) — the existing BotID string json:"bot_id"` field already exists; no new event field is needed
  • forge-cli/templates/init/slack-config.yaml.tmpl — document the new optional setting in the template
  • docs/core-concepts/channels.md — operator docs describing the loop-safety trade-off, how to find a bot's bot_id, and that the agent's own messages are always ignored

Acceptance criteria

  • An operator can list one or more bot IDs in slack-config.yaml (allow_bot_ids) and have the forge-agent respond to @-mentions from those bots in exactly the same way it responds to human mentions.
  • With allow_bot_ids empty or absent, behavior is identical to today — every bot-authored event is dropped, no surprises.
  • The agent never responds to its own messages. Even if its own bot_id were listed in allow_bot_ids (whether by mistake or future wildcard support), the self-id guard at startup-resolved p.ownBotID short-circuits the event. This rule has no opt-out.
  • A bot in the allowlist still only triggers a response when it explicitly @-mentions the forge-agent (<@FORGE_AGENT_USER_ID> in the message text). Generic chatter from an allowed bot is ignored.
  • app_mention events authored by allowed bots are honored (since the regular message event is the existing primary path; the implementation must not regress to firing both for admitted bots).
  • A clear log line is emitted when an event is dropped for being a non-allowlisted bot or for being the agent itself, so operators can debug "why isn't my CI bot's mention working" — and "why isn't my agent reacting to its own scheduled prompt" — without reading the source.

Context

Surfaced from real usage: forge-agent in a Slack workspace responds to human mentions but is silent when another bot (scheduler / monitoring / workflow) routes a mention to it. The mention-aware filter at slack.go:346-361 is correct; the unconditional BotID != "" skip above it is what's overshooting.

Related: this is a separate concern from #50 (channel env vars in K8s manifests) but in the same Slack adapter file. Fixing both is independent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions