Skip to content

Add Slack channel support#7

Open
Rikul wants to merge 8 commits into
mainfrom
claude/slack-channel-support-ipLnV
Open

Add Slack channel support#7
Rikul wants to merge 8 commits into
mainfrom
claude/slack-channel-support-ipLnV

Conversation

@Rikul

@Rikul Rikul commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add SlackChannel in app/channels/slack.py using slack-bolt with Socket Mode (no public webhook needed)
  • Wire Slack into bg_server.py alongside existing Discord/Telegram channels
  • Add SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_ALLOW_FROM env var support in config.py
  • Add slack-bolt (1.18.0 or newer) dependency

Configuration

Set the following env vars (or add a [slack] section to ~/.crafterscode/config.toml):

  • SLACK_BOT_TOKENxoxb-... bot OAuth token
  • SLACK_APP_TOKENxapp-... app-level token (requires connections:write scope for Socket Mode)
  • SLACK_ALLOW_FROM — comma-separated Slack user IDs to restrict access (empty = deny all; must be non-empty to allow any user)

This matches the existing Telegram/Discord ALLOW_FROM behavior, which also denies all users when empty.

Test plan

  • 30 unit tests in tests/test_slack_channel.py all pass
  • Existing Discord/Telegram channel tests unaffected
  • Manual: set tokens, start server, send a DM or channel message to the bot, verify response
  • Manual: /whoami returns your Slack user ID
  • Manual: /stop halts the agent loop
  • Manual: other slash commands (e.g. /status) are forwarded to BackgroundAgent

Generated by Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a major refactoring and expansion of the AI agent codebase, restructuring the project into dedicated core, infra, and cli modules. It adds Discord and Slack channels alongside Telegram, implements a SQLite-backed scheduled tasks runner, and introduces several new tools (calculator, HackerNews, web search, etc.). The code review highlights critical bugs, including an infinite loop risk in the agent loop when finish_reason is not "stop", and a delivery routing failure for scheduled tasks after restarts. Additionally, several performance and stability concerns were raised, such as synchronous blocking calls freezing the asyncio event loop, potential startup crashes from unhandled environment variable parsing, and dead code in the newly added channels.

Comment thread app/core/agent.py Outdated
Comment on lines +177 to +181
else:
await self._on_response(assistant_message.content)
if finish_reason == "stop":
messages.append(self._serialize_assistant_msg(assistant_message))
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a critical infinite loop bug when finish_reason is not "stop" (e.g., "length"). Currently, if finish_reason is not "stop", the assistant's partial response is not appended to messages, and the loop continues to the next iteration. This causes the next API call to send the exact same messages, resulting in the LLM generating the exact same response and causing an infinite loop of identical API calls until max_iterations is reached.\n\nTo fix this, always append the assistant's response to messages before checking if the loop should terminate.

            else:\n                await self._on_response(assistant_message.content)\n                messages.append(self._serialize_assistant_msg(assistant_message))\n                if finish_reason == "stop":\n                    break

if mq and channel:
try:
from ..channels.message import OutgoingMessage
await mq.outgoing_msg(OutgoingMessage(content=output, channel=channel, metadata=channel.default_metadata))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a critical routing and delivery bug for scheduled tasks. Relying on channel.default_metadata (which uses the volatile _last_channel_id) has two major issues:\n1. Delivery Failure after Restart: If the bot restarts, _last_channel_id is None, so default_metadata is {}, causing delivery to fail completely for Slack and Telegram.\n2. Cross-Channel Leakage: Since _last_channel_id is overwritten by the most recent message across any channel/chat, a scheduled task created in Channel A will be delivered to Channel B if Channel B was active more recently.\n\nTo fix this, the tasks table should store the destination metadata (like chat_id or channel_id) when the task is created, and use that stored metadata during delivery instead of relying on the channel's volatile global state.

Comment thread app/core/agent.py Outdated
if not await self._check_permission(tool_name, tool_args):
return "User denied permission to run this tool. Ask for permission to run the tool again if you want to try running it."
await self._on_tool_start(tool_name, tool_args)
return run_tool(tool_name=tool_name, tool_args=tool_args)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling run_tool synchronously blocks the entire asyncio event loop. Since this is a multi-channel background server running Telegram, Discord, and Slack concurrently, any blocking tool call (like bash running a long command, or web_fetch taking several seconds) will freeze the entire server for all users.\n\nTo prevent this, tools should ideally be run in a thread pool (e.g., using asyncio.to_thread). However, note that run_tool currently uses os.chdir to manage the working directory, which is not thread-safe. To make it thread-safe, you should avoid process-wide directory changes and instead pass absolute paths or the workspace directory directly to the tools.

Comment thread app/tools/calculator.py
Comment on lines +84 to +88
def call(command: str, argument1: int, argument2: int = None) -> str:
log.info(f"calculator, command: {command}")

try:
if command == "add":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The argument2 parameter is optional in the tool spec, but its type is annotated as int instead of int | None. Furthermore, if the LLM calls a two-argument command (like add, subtract, multiply, divide, exponentiate) and omits argument2, it will be passed as None, causing a TypeError when performing the operation.\n\nYou should update the type annotation and add a check to ensure argument2 is not None for these commands.

    def call(command: str, argument1: int, argument2: int | None = None) -> str:\n        log.info(f"calculator, command: {command}")\n\n        try:\n            if command in ["add", "subtract", "multiply", "divide", "exponentiate"] and argument2 is None:\n                return f"Error: command '{command}' requires a second argument"\n\n            if command == "add":

Comment thread app/core/agent.py
Comment on lines +152 to +156
chat = await self.client.chat.completions.create(
model = runtime.get("model", "deepseek/deepseek-v4-flash"),
messages = messages,
tools = tool_specs,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Passing an empty list to the tools parameter in chat.completions.create can cause API errors with some LLM providers or OpenRouter. It is safer to only include the tools parameter if tool_specs is non-empty.

            kwargs = {\n                "model": runtime.get("model", "deepseek/deepseek-v4-flash"),\n                "messages": messages,\n            }\n            if tool_specs:\n                kwargs["tools"] = tool_specs\n            chat = await self.client.chat.completions.create(**kwargs)

Comment thread app/channels/discord.py
Comment on lines +109 to +110
async def error_handler(self, update, context) -> None:
log.error(f"Discord error: {context}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error_handler method is dead code. It has a signature copied from python-telegram-bot (update, context) which is incorrect for discord.py, and it is never registered. It should be removed.

Comment on lines +143 to +147
finally:
duration = (datetime.now() - start).total_seconds()
self.save_output(name=name, prompt=prompt, output=output,
status=status, duration_secs=duration)
self._after_run(task=task, now=datetime.now())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Raising an exception inside a finally block (such as a SQLite OperationalError like "database is locked" inside _after_run) will discard and mask any original exception that occurred in the try block. It is safer to wrap the database operations inside finally in their own try...except blocks to prevent masking.

Comment thread app/config.py
if v := os.environ.get("TELEGRAM_BOT_TOKEN"):
_config.setdefault("telegram", {})["BOT_TOKEN"] = v
if v := os.environ.get("TELEGRAM_ALLOW_FROM"):
_config.setdefault("telegram", {})["ALLOW_FROM"] = [int(x.strip()) for x in v.split(",") if x.strip()]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the TELEGRAM_ALLOW_FROM environment variable contains any non-integer values (e.g., due to a typo like "123, abc"), calling int(x.strip()) will raise a ValueError on startup, crashing the entire bot. Consider filtering out non-digit values or catching the exception to fail gracefully.

Suggested change
_config.setdefault("telegram", {})["ALLOW_FROM"] = [int(x.strip()) for x in v.split(",") if x.strip()]
_config.setdefault("telegram", {})["ALLOW_FROM"] = [int(x.strip()) for x in v.split(",") if x.strip().lstrip('-').isdigit()]

Comment thread app/config.py
if v := os.environ.get("DISCORD_BOT_TOKEN"):
_config.setdefault("discord", {})["TOKEN"] = v
if v := os.environ.get("DISCORD_ALLOW_FROM"):
_config.setdefault("discord", {})["ALLOW_FROM"] = [int(x.strip()) for x in v.split(",") if x.strip()]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the DISCORD_ALLOW_FROM environment variable contains any non-integer values, calling int(x.strip()) will raise a ValueError on startup, crashing the entire bot. Consider filtering out non-digit values or catching the exception to fail gracefully.

Suggested change
_config.setdefault("discord", {})["ALLOW_FROM"] = [int(x.strip()) for x in v.split(",") if x.strip()]
_config.setdefault("discord", {})["ALLOW_FROM"] = [int(x.strip()) for x in v.split(",") if x.strip().lstrip('-').isdigit()]

Comment thread app/channels/slack.py Outdated
Comment on lines +110 to +111
def error_handler(self, update, context) -> None:
log.error(f"Slack error: {context}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error_handler method is dead code. It has a signature copied from python-telegram-bot (update, context) which is incorrect for Slack Bolt, and it is never registered with self.app. It should be removed to keep the codebase clean.

@Rikul Rikul changed the base branch from dev to main May 28, 2026 03:21
@Rikul Rikul requested a review from Copilot May 28, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Slack channel implementation alongside the existing Telegram and Discord channels, using slack-bolt in Socket Mode so no public webhook is required. The new channel follows the same message-queue / BackgroundAgent wiring as the others.

Changes:

  • New SlackChannel (app/channels/slack.py) with message handling, /whoami / /stop interception, send chunking, and async Socket Mode polling; new ChannelType.SLACK enum value.
  • bg_server.py wires Slack alongside Telegram/Discord (own MessageQueue, BackgroundAgent, and coroutines), and config.py adds SLACK_BOT_TOKEN / SLACK_APP_TOKEN / SLACK_ALLOW_FROM env handling plus a [slack] section in the default config.
  • Adds slack-bolt>=1.18.0 to pyproject.toml/uv.lock and 21 unit tests in tests/test_slack_channel.py.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
app/channels/slack.py New SlackChannel mirroring Discord/Telegram patterns.
app/channels/channel.py Adds ChannelType.SLACK.
app/bg_server.py Wires the Slack channel/agent/queue into startup.
app/config.py Adds Slack env-var parsing and default [slack] config section.
pyproject.toml Adds slack-bolt>=1.18.0 dependency.
uv.lock Locks slack-bolt 1.28.0 and slack-sdk 3.42.0.
tests/test_slack_channel.py Unit tests covering init, message handling, commands, send chunking, and error paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/channels/slack.py Outdated
Comment thread app/channels/slack.py Outdated
Comment thread app/channels/slack.py Outdated

log = logging.getLogger(__name__)

MAX_SLACK_LENGTH = 3000
Comment thread app/channels/slack.py Outdated
Comment thread app/config.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated no new comments.

claude added 2 commits June 4, 2026 23:53
Implements SlackChannel following the same conventions as the existing
Discord and Telegram channels. Wires it into bg_server and config
(env vars SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_ALLOW_FROM).

https://claude.ai/code/session_01Fa64TcstSLyfugG93cjYR2
- Remove dead _bot_user_id field
- Only filter system subtypes (message_changed/deleted, join/leave, etc.)
  instead of all subtypes — allows file_share, me_message, thread_broadcast
- Register a slack_bolt-compatible error handler via app.error()
- Update README: add Slack to feature list, config, Docker section, env table
- Update Dockerfile: add SLACK_ALLOW_FROM env default and usage comments
- Remove Slack from roadmap (shipped)

https://claude.ai/code/session_01Fa64TcstSLyfugG93cjYR2
@Rikul Rikul force-pushed the claude/slack-channel-support-ipLnV branch from fe367d3 to ce9039b Compare June 4, 2026 23:54
@Rikul Rikul requested a review from Copilot June 4, 2026 23:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.

Comment thread app/channels/slack.py Outdated
Comment thread app/channels/slack.py
Comment thread app/config.py
Comment thread app/bg_server.py
Comment thread tests/test_slack_channel.py Outdated
- Fix exc_info=error -> exc_info=True in _slack_error_handler (logging API)
- Guard against events with empty/missing channel_id before enqueuing
- Raise MAX_SLACK_LENGTH 3000 -> 3800 (plain text= field allows ~40k chars)
- Remove unused MagicMock import from test_slack_channel.py
- Add test for empty-channel guard
- Add Slack env var tests to test_config.py (BOT_TOKEN, APP_TOKEN, ALLOW_FROM)
- Add Slack branch tests to test_bg_server.py (happy path, missing tokens, gather coros)

https://claude.ai/code/session_01Fa64TcstSLyfugG93cjYR2
@Rikul Rikul marked this pull request as ready for review June 5, 2026 02:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef23be7ea5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/channels/slack.py
Empty ALLOW_FROM now means no one is permitted, consistent with the
principle of least privilege. Previously [] was treated as "no filter"
(allow everyone); now it is treated as an empty allowlist (allow nobody).

Update config comments, README, and all affected tests accordingly.

https://claude.ai/code/session_01Fa64TcstSLyfugG93cjYR2

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.

Comment thread app/channels/slack.py
Comment thread app/channels/slack.py Outdated
Comment thread app/channels/slack.py
Comment thread app/channels/telegram.py
…am auth gap

- Route Slack slash commands through app.command() handler (not just event("message")),
  matching how Slack actually delivers /commands
- Update _last_channel_id before the command branch in _handle_message so it is
  always set even when the message triggers a whoami/stop inline response
- Add allow_from auth check to Telegram command_handler, which previously had no
  authorization guard and allowed any user to invoke slash commands
- Add tests for all three cases

https://claude.ai/code/session_01Fa64TcstSLyfugG93cjYR2
Copilot AI added a commit that referenced this pull request Jun 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 41 changed files in this pull request and generated 3 comments.

Comment thread app/tools/helper_agent.py
Comment thread CLAUDE.md Outdated
Comment thread app/channels/slack.py
claude added 2 commits June 20, 2026 05:24
…ompts

- CLAUDE.md described run_tool() as synchronous and claimed MCP tool routing
  uses rpartition; both are wrong (run_tool is async, routing uses partition)
- helper_agent tool logged the full user prompt at INFO level, which can leak
  sensitive data (tokens, credentials) into application logs; log only the
  prompt length instead
…support-ipLnV

# Conflicts:
#	CLAUDE.md
#	README.md
#	app/tools/helper_agent.py
#	pyproject.toml
@Rikul Rikul requested a review from Copilot June 22, 2026 23:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 5 comments.

Comment thread app/config.py
Comment on lines +47 to +52
if v := os.environ.get("SLACK_BOT_TOKEN"):
_config.setdefault("slack", {})["BOT_TOKEN"] = v
if v := os.environ.get("SLACK_APP_TOKEN"):
_config.setdefault("slack", {})["APP_TOKEN"] = v
if v := os.environ.get("SLACK_ALLOW_FROM"):
_config.setdefault("slack", {})["ALLOW_FROM"] = [x.strip() for x in v.split(",") if x.strip()]
Comment thread app/bg_server.py
Comment on lines +90 to +92
if slack_channel:
channels["slack"] = slack_channel
mqs["slack"] = slack_mq
Comment thread README.md
Comment on lines 53 to +57
ALLOW_FROM = [] # List of allowed Telegram user IDs (integers).

[discord]
TOKEN = ""
ALLOW_FROM = [] # List of allowed Discord user IDs (integers). Empty means allow all.
ALLOW_FROM = [] # List of allowed Discord user IDs (integers). Empty = deny all.
Comment thread README.md
Comment on lines 124 to +127
[discord]
TOKEN = "your-discord-bot-token"
ALLOW_FROM = [] # restrict by user ID; empty = allow all
ALLOW_FROM = [] # restrict by user ID

Comment thread README.md
Comment on lines +128 to +131
[slack]
BOT_TOKEN = "xoxb-..." # bot token from Slack app settings
APP_TOKEN = "xapp-..." # app-level token with connections:write scope
ALLOW_FROM = [] # restrict by Slack user ID
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants