ops-jrz1/docs/dev-slack-direct.md
Dan 92d7646d52 Migrate Slack tokens to sops-nix, improve egress rate limits
- Remove beads from VPS deployment (kept locally for dev workflow)
- Add slack-bot-token and slack-app-token secrets with devs group access
- Remove dead acme-email secret reference
- Increase egress limits from 30/min to 150/min (burst 60→300)
- Change egress blocking from REJECT to DROP for better app behavior
- Add egress-status script for user self-diagnosis
- Update dev-slack-direct.md with new /run/secrets access patterns

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:14:19 -08:00

6.2 KiB

Direct Slack Bot Development for Devs

Design doc for the "direct to Slack" dev path, bypassing Matrix/maubot.

Overview

Devs write Python bots using slack-bolt that connect directly to Slack via Socket Mode. No Matrix, no bridge, no maubot.

┌─────────────────────────────────────────┐
│ VPS (ops-jrz1)                          │
│                                         │
│  alice's bot.py ──┐                     │
│  bob's bot.py ────┼── Socket Mode ─────────► Slack API
│  carol's bot.py ──┘    (WebSocket)      │
│                                         │
└─────────────────────────────────────────┘

Current Setup

Credentials

Shared Slack App tokens managed via sops-nix, deployed to /run/secrets/:

Secret File Purpose
/run/secrets/slack-bot-token Bot identity (xoxb-...)
/run/secrets/slack-app-token Socket Mode connection (xapp-...)

These come from the existing mautrix-slack bridge login (Chochacho workspace, vlad's account).

Access Control

  • Files owned by root:devs, mode 0440
  • Dev users added to devs group on creation
  • Secrets encrypted in git, decrypted at boot via sops-nix

Scripts

/usr/local/bin/dev-add.sh <username> '<ssh-pubkey>'
/usr/local/bin/dev-remove.sh <username> [--archive]

Dev Workflow

1. Get Access

Send SSH pubkey to admin. Admin runs:

ssh root@ops-jrz1 'dev-add.sh alice "ssh-ed25519 AAAA..."'

2. Connect

ssh alice@<server-ip>

Load tokens from secrets:

export SLACK_BOT_TOKEN=$(cat /run/secrets/slack-bot-token)
export SLACK_APP_TOKEN=$(cat /run/secrets/slack-app-token)

# Verify they're set
echo $SLACK_BOT_TOKEN  # xoxb-...
echo $SLACK_APP_TOKEN  # xapp-...

3. Write a Bot

# ~/bot.py
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import os

# Read tokens from sops-nix secrets (or fall back to env vars)
def read_secret(path, env_fallback):
    try:
        with open(path) as f:
            return f.read().strip()
    except FileNotFoundError:
        return os.environ.get(env_fallback, "")

BOT_TOKEN = read_secret("/run/secrets/slack-bot-token", "SLACK_BOT_TOKEN")
APP_TOKEN = read_secret("/run/secrets/slack-app-token", "SLACK_APP_TOKEN")

app = App(token=BOT_TOKEN)

@app.message("hello")
def hello(message, say):
    say(f"Hey <@{message['user']}>!")

@app.message("dice")
def dice(message, say):
    import random
    say(f"You rolled a {random.randint(1, 6)}!")

if __name__ == "__main__":
    print("Bot starting...")
    SocketModeHandler(app, APP_TOKEN).start()

4. Run It

pip install slack-bolt
python bot.py

Bot responds to "hello" and "dice" in Slack.

slack-bolt Basics

Event Handlers

# Match exact text
@app.message("hello")
def handle_hello(message, say):
    say("Hello!")

# Match regex
@app.message(re.compile(r"^roll (\d+)d(\d+)"))
def handle_roll(message, say, context):
    # context["matches"] contains regex groups
    pass

# Match any message
@app.event("message")
def handle_all(event, say):
    pass

# React to app mentions (@bot)
@app.event("app_mention")
def handle_mention(event, say):
    say("You mentioned me!")

Slash Commands

Requires additional Slack App config (not set up yet):

@app.command("/mycommand")
def handle_command(ack, respond, command):
    ack()  # Must acknowledge within 3 seconds
    respond(f"You said: {command['text']}")

Sending Messages

# Reply in thread
say("Reply!", thread_ts=message["ts"])

# Send to specific channel
app.client.chat_postMessage(channel="C1234567", text="Hello")

# Rich formatting (blocks)
say(blocks=[
    {"type": "section", "text": {"type": "mrkdwn", "text": "*Bold* and _italic_"}}
])

Tradeoffs

vs Maubot/Matrix Path

Aspect Direct Slack Maubot/Matrix
Setup complexity Lower - just Python Higher - Matrix + bridge + maubot
Feedback loop Fast - direct to Slack Slower - goes through bridge
Mental model Simple - just Slack Complex - Matrix concepts
Documentation Excellent (Bolt is popular) Sparse (maubot is niche)
Process management Manual (you run it) Managed (maubot lifecycle)
Bot identity Shared (all bots = same user) Per-instance configurable
Crash recovery Manual restart Auto-managed
Production path Direct - same code works Need to rewrite for Slack-only

Risks

  1. Shared tokens - All devs use same bot identity
  2. Token exposure - If leaked, affects everyone
  3. Rate limits - Shared across all bots
  4. Process sprawl - N devs = N processes to manage
  5. No guardrails - Bad code can spam/crash easily

Mitigations

  • Dedicated test channel in Slack
  • Supervisor/systemd for process management (not yet set up)
  • Tokens in sops-nix secrets, encrypted in git, decrypted only at runtime
  • Can rotate tokens if leaked (re-login bridge, update secrets.yaml)

Future Improvements

Process Management

Options:

  1. systemd user services - Each dev manages their own
  2. supervisor - Central process manager
  3. tmux/screen - Manual but simple
  4. Container per dev - Isolation but complex

Starter Template

Create ~/slack-bot-template/ with:

  • bot.py - Hello world
  • requirements.txt - slack-bolt pinned
  • Makefile - run, install targets
  • README.md - Quick start

Per-Dev Bot Identity

Create separate Slack App per dev:

  • More setup friction
  • Full isolation
  • Each bot has own name/avatar

Test Channel

Create #dev-sandbox in Slack:

  • All dev bots invited
  • Safe place to spam
  • Doesn't pollute real channels

References