feat(slack): channel identity, proactive messaging, and per-agent bot icon (#350 Phase 1 & 3, #292)#1414
Merged
Merged
Conversation
…Phase 1 & 3) Slack channel behavior lacked two of the three things Telegram #349 already had. This lands the two that reuse the shipped shared infra and need no OAuth re-consent; observation mode (Phase 2) is split to sub-issue #1413. Phase 1 — user identity: - New async `ChannelAdapter.enrich_message` hook (default no-op); the router calls it after agent resolution. `SlackAdapter.enrich_message` resolves the sender display name (users.info) and channel name (conversations.info) into message metadata — best-effort, degrades to bare IDs. - `message_router._format_channel_identity` prepends `[Channel: #name]\n [From: Display (@user)]` for enriched channel (non-DM) messages. Gated on `channel_name` presence, so DMs and un-enriched/Telegram paths are unchanged and `is_group` access semantics are untouched. - slack_service: add get_user_info / get_channel_info. Phase 3 — proactive channel messaging: - Backend: GET /api/agents/{name}/slack/channels (list bound channels) and POST .../slack/channels/{channel_id}/messages (owner-gated, rate-limited 10/hr/channel + 100/hr/agent via the shared limiter, optional thread_ts, posts via chat.postMessage with the agent identity). - db.get_slack_channels_for_agent (cross-workspace); SlackChannelMessageRequest. - MCP list_channel_groups / send_group_message now accept channel_type "telegram" | "slack" (+ Slack thread_ts); new TrinityClient methods. Tests: tests/unit/test_350_slack_identity.py (identity prefix + enrichment, 9 cases). Updated the message-router access-gate double for the new async hook. Related to #350 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Slack bot posted under the app's default name/icon regardless of which agent replied. The username was already correct (`send_response` passes `username=agent_name` since #63, and `chat:write.customize` is in the requested scopes) — the missing piece was the icon: nothing ever populated `agent_avatar_url`, so `icon_url` was always None. `message_router` now attaches a best-effort public avatar URL (`{public_chat_url}/api/agents/{name}/avatar`, cache-busted by `avatar_updated_at`) to the reply metadata, which `SlackAdapter.send_response` already forwards as `icon_url`. Applies to channels, DMs, and threads (the shared reply path). Best-effort: None when no public base URL is configured; the avatar endpoint 404s cleanly for avatar-less agents, so a private/ unreachable base just falls back to the channel default. Channel-agnostic — non-Slack adapters ignore the key. Tests: 4 cases for `_agent_avatar_url` (cache-bust, no-version, no-base, error) in test_350_slack_identity.py. Related to #292 Bundled with the #350 Slack identity/proactive work (same PR, same adapter). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Resolve by running |
vybe
approved these changes
Jul 2, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Validated via /validate-pr: #350 Phase 1&3 + #292 — Slack channel identity, proactive messaging, per-agent bot icon. All CI green (pytest×6, CodeQL, schema-parity, verify-non-root, prod-image-smoke); tests for slack identity + message-router access gate. Three surfaces in sync (router + adapters + MCP channels.ts). Note: #350 is an epic — this is a phase, so no closing keyword by design. Approving for merge to dev.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Advances epic #350 (Slack channel behavior, parallel to the shipped Telegram #349). Lands the two phases that reuse existing shared infra and need no OAuth re-consent. Observation mode (Phase 2) is split to sub-issue #1413 (needs new
channels:historyscopes → Slack app re-install,message-event subscription, a dual-track migration).Phase 1 — user identity (AC: agent sees
[Channel: #name] [From: Display Name (@username)])ChannelAdapter.enrich_messagehook (default no-op) —parse_messageis sync, but Slack events carry only opaque IDs, so identity needs an async API call. The router calls it once after agent resolution.SlackAdapter.enrich_messageresolves sender display name (users.info) + channel name (conversations.info) intomessage.metadata; best-effort (degrades to bare IDs, never raises).message_router._format_channel_identityprepends the identity block for enriched channel (non-DM) messages. Gated onchannel_namepresence — so DMs, un-enriched channels, and Telegram are unchanged, and I deliberately do not flipis_group(that would change the email-access-gate/group-bypass semantics — out of scope).slack_service:get_user_info/get_channel_info.Phase 3 — proactive channel messaging (AC:
list_channel_groups+send_group_messagefor Slack, rate-limited)GET /api/agents/{name}/slack/channels(list bound channels) andPOST /api/agents/{name}/slack/channels/{channel_id}/messages— owner-gated, rate-limited 10/hr/channel + 100/hr/agent via the shared limiter (refactor(backend): unified Redis sliding-window rate limiter (replace ad-hoc per-endpoint limiters) #1023), optionalthread_ts, posts viachat.postMessagewith the agent identity.db.get_slack_channels_for_agent(cross-workspace);SlackChannelMessageRequestinmodels.py(Invariant Add internal health route, without which main didn't start #14).list_channel_groups/send_group_messagenow acceptchannel_type: "telegram" | "slack"(+ Slackthread_ts); newTrinityClientmethods. Thechat_with_agent/tool bodies are untouched.Tests
tests/unit/test_350_slack_identity.py— 9 cases (identity-prefix builder incl. DM/empty cases, enrichment metadata population, DM-skips-channel, best-effort error swallow, no-token no-op). Updated the access-gate test double for the new async hook. Full run: 81 passed across the router + slack suites. MCPtscclean for the changed files (only a pre-existing unrelatedyamldep error remains).Scope notes
is_groupsemantics unchanged (access-gate safety).Related to #350
🤖 Generated with Claude Code
Also in this PR — #292 (Slack bot icon)
Bundled per request (same adapter, same Slack identity surface). The bot username was already correct (
send_responsepassesusername=agent_namesince #63;chat:write.customizeis in the requested scopes) — the gap was the icon: nothing populatedagent_avatar_url, soicon_urlwas always None.message_routernow attaches a best-effort public avatar URL ({public_chat_url}/api/agents/{name}/avatar, cache-busted byavatar_updated_at) which the Slack adapter forwards asicon_url. Channels, DMs, threads. Best-effort (None without a public base URL; 404s fall back to the default icon). 4 unit cases added.Related to #292