Skip to content

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

  1. Go to https://api.slack.com/apps -> "Create New App" -> "From an app manifest" -> pick the workspace.
  2. Paste the YAML manifest below, review the scopes/events it shows, and create the app.
  3. Basic Information -> App-Level Tokens: generate one with the connections:write scope. This is KAPELLE_SLACK_APP_TOKEN (xapp-...) -- Socket Mode (apps.connections.open) uses it.
  4. OAuth & Permissions: install the app to the workspace. The Bot User OAuth Token (xoxb-...) is KAPELLE_SLACK_BOT_TOKEN.
  5. Socket Mode: confirm it's enabled (the manifest already sets this, but the UI has its own toggle that occasionally needs confirming).
  6. 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.setStatus returns an agent_tasks_not_enabled-shaped error otherwise, and design doc §13 already flags that "some features need a paid plan."
  7. Invite the bot to every channel a team links (/invite @<bot name> in Slack, or --home slack team creation once kapelle_controller wires up conversations.invite, see kapelle_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 plain chat.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 of 0/false/no/off, case-insensitive).
  • Per-team channel links (/team link <team> slack <channel-id>, design doc §12) populate team_by_channel at 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:write user 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:

  1. api.slack.com/apps -> the Kapelle app -> Slash Commands -> Create New Command.
  2. Command /budget, description "Team budget: state, spend vs. budget", usage hint set 50, no Request URL (Socket Mode).
  3. Reinstall the app to the workspace (Slack requires this after any slash-command change) -- OAuth & Permissions -> Reinstall to Workspace. No new scope: commands was 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 /budget itself (not a built-in), but the reason /status was never attempted as a slash command here even setting aside the thread-payload limit above.
  • How long reinstalling the app after adding /budget takes to actually make the command available to type.
  • Real end-to-end delivery of a typed /budget once registered -- this task's own tests prove the gateway's dispatch code, not Slack's actual delivery.

Not implemented here

  • Wiring --home slack into kapelle_controller.teams.TeamService.create itself (the actual "/team create --home slack creates and invites" orchestration, design doc §12 step 1) -- kapelle_gateway.slack.client. SlackClient.create_channel/invite_to_channel are the primitives a kapelle_controller/slack.py (mirroring kapelle_controller/mattermost.py) would call; services/controller/ isn't this task's directory.
  • The interactive Socket Mode envelope type (button clicks other than the native agent stop button, which arrives as the agent_session_stopped EVENT, not an interactive payload) -- out of task 30's scope.