Slack App: registration and manifest¶
Backlog task 30, design doc §13 "Slack". One Slack app per Kapelle
deployment, installed into each workspace a team lives in. Every method,
scope and event below was verified against docs.slack.dev while building
services/gateway/src/kapelle_gateway/slack/ (task 30's handoff), not
taken from memory.
Creating the app from the manifest¶
- Go to https://api.slack.com/apps -> "Create New App" -> "From an app manifest" -> pick the workspace.
- Paste the YAML manifest below, review the scopes/events it shows, and create the app.
- Basic Information -> App-Level Tokens: generate one with the
connections:writescope. This isKAPELLE_SLACK_APP_TOKEN(xapp-...) -- Socket Mode (apps.connections.open) uses it. - OAuth & Permissions: install the app to the workspace. The Bot
User OAuth Token (
xoxb-...) isKAPELLE_SLACK_BOT_TOKEN. - Socket Mode: confirm it's enabled (the manifest already sets this, but the UI has its own toggle that occasionally needs confirming).
- Agents & AI Apps (if present as a separate settings page for this
workspace's plan): confirm the "agent" capability is enabled for the
app --
agents.sessions.setStatusreturns anagent_tasks_not_enabled-shaped error otherwise, and design doc §13 already flags that "some features need a paid plan." - Invite the bot to every channel a team links (
/invite @<bot name>in Slack, or--home slackteam creation oncekapelle_controllerwires upconversations.invite, seekapelle_gateway.slack.client. SlackClient.create_channel's docstring).
Manifest¶
display_information:
name: Kapelle
description: Coding agent teams, working from your Slack threads.
features:
bot_user:
display_name: Kapelle
always_online: true
slash_commands:
# No `url` on either command -- Socket Mode delivers slash command
# payloads over the WebSocket connection, not to an HTTP endpoint
# (docs.slack.dev: omit `url` in the manifest for a Socket Mode app).
- command: /team
description: Team admin commands (create, link, archive, budget, ...)
usage_hint: create payments --template feature-team --repo <url>
should_escape: false
- command: /ask
description: Start a new Kapelle work item in this channel
usage_hint: add retries to the payment call
should_escape: false
- command: /budget
description: "Team budget: state, spend vs. budget"
usage_hint: set 50
should_escape: false
oauth_config:
scopes:
bot:
# Events this app subscribes to (below).
- app_mentions:read # app_mention
- channels:history # message.channels
- im:history # message.im (the /me link DM confirmation)
# Posting/updating/streaming/status (chat.postMessage, chat.update,
# chat.startStream/appendStream/stopStream, agents.sessions.setStatus
# all take the same bot scope -- verified individually against each
# method's own docs.slack.dev reference page).
- chat:write
# --home slack (kapelle_gateway.slack.client.SlackClient.
# create_channel/invite_to_channel): conversations.create and
# conversations.invite both accept channels:manage for a public
# channel (this app never creates private ones).
- channels:manage
# Slash commands.
- commands
settings:
socket_mode_enabled: true
event_subscriptions:
# No `request_url` -- Socket Mode again.
bot_events:
- app_mention
- message.channels
- message.im
# Shows Slack's native stop button while a session is `processing`,
# and is how a click reaches this app at all (docs.slack.dev:
# without this subscription, `agents.sessions.setStatus` warns
# `missing_agent_session_stopped_event_subscription` and Slack shows
# a plain, non-interactive spinner instead).
- agent_session_stopped
org_deploy_enabled: false
Env vars this needs¶
KAPELLE_SLACK_APP_TOKEN-- the app-level token (step 3 above).KAPELLE_SLACK_BOT_TOKEN-- the bot token (step 4).KAPELLE_SLACK_STREAMING-- optional, on by default whenever Slack is configured at all:chat.startStream/appendStream/stopStream(live-updating messages as an agent works) instead of plainchat.postMessage/chat.update.chat:write(already required above) is all the scope it needs. Streaming itself is a paid-plan-gated Slack feature though, so a workspace whose plan doesn't support it needs the opt-out:KAPELLE_SLACK_STREAMING=false(any of0/false/no/off, case-insensitive).- Per-team channel links (
/team link <team> slack <channel-id>, design doc §12) populateteam_by_channelat runtime -- no separate env var per channel.
For the live integration test
(services/gateway/tests/test_slack_live_integration.py), additionally:
KAPELLE_SLACK_TEST_USER_TOKEN-- a second, human account's user token (xoxp-...,chat:writeuser scope), standing in for the person who starts and answers the test work item (a bot can't message itself believably). Install a "companion" personal token for this via that same app's OAuth flow, or use a throwaway second app -- either way, it must belong to a real member of the test channel, distinct from the bot.KAPELLE_SLACK_TEST_USER_ID-- that user's Slack user id.KAPELLE_SLACK_TEST_CHANNEL-- a channel id both accounts are members of, used only for this test's real round trip.
Commands¶
/team, /ask, and (backlog task 85db7a3f) /budget are registered
Slack slash commands, typed in the channel itself, never inside a
thread. /status and /cancel are NOT registered as slash commands and
never will be as things stand: a custom Slack slash command's payload
carries no thread information at all, and docs.slack.dev is explicit that
one "cannot... be invoked in message threads" -- both commands need a
specific work item (a specific thread) to act on, so there is no channel-
level version of either that would mean anything. Typing /status or
/cancel in Slack's own message box today either does nothing this app
ever sees (Slack rejects an unregistered command client-side before
sending it) or, for /status specifically, triggers Slack's own built-in
/status (sets the person's away/active status, unrelated to Kapelle).
What to type instead: reply in the work item's own thread with
/status or /cancel as plain text -- every thread reply is parsed for
these commands the same way a message is, so this already works.
Adding /budget to an app created before this task¶
An app created from an earlier version of the manifest above needs this added by hand, once:
- api.slack.com/apps -> the Kapelle app -> Slash Commands -> Create New Command.
- Command
/budget, description "Team budget: state, spend vs. budget", usage hintset 50, no Request URL (Socket Mode). - Reinstall the app to the workspace (Slack requires this after any
slash-command change) -- OAuth & Permissions -> Reinstall to
Workspace. No new scope:
commandswas already granted for/team//ask.
Not tried against a real Slack workspace¶
Nothing below has been exercised against a live install -- there is no Slack workspace in this environment to check it against (backlog task 85db7a3f):
- Whether an installed app's own custom slash command with the same name
as a Slack built-in one (e.g. a hypothetical
/status) actually overrides it, coexists with it, or is refused at registration time -- docs.slack.dev documents the rule for two custom apps colliding ("Slack will always invoke the one installed most recently") but has no page describing a built-in/custom collision. Moot for/budgetitself (not a built-in), but the reason/statuswas never attempted as a slash command here even setting aside the thread-payload limit above. - How long reinstalling the app after adding
/budgettakes to actually make the command available to type. - Real end-to-end delivery of a typed
/budgetonce registered -- this task's own tests prove the gateway's dispatch code, not Slack's actual delivery.
Not implemented here¶
- Wiring
--home slackintokapelle_controller.teams.TeamService.createitself (the actual "/team create --home slackcreates and invites" orchestration, design doc §12 step 1) --kapelle_gateway.slack.client. SlackClient.create_channel/invite_to_channelare the primitives akapelle_controller/slack.py(mirroringkapelle_controller/mattermost.py) would call;services/controller/isn't this task's directory. - The
interactiveSocket Mode envelope type (button clicks other than the native agent stop button, which arrives as theagent_session_stoppedEVENT, not an interactive payload) -- out of task 30's scope.