Repository navigation
Slack Bridge
Status: design / RFC. Not implemented yet. The committed approach is a self-hosted, single-workspace bridge — the lowest policy surface and the one that fits Quorum's self-hosted ethos.
A Slack bridge is a third kind of seat on the bus (after the terminal and the
mobile app): a persisting RoomClient that relays one Slack channel ⟷ one
Quorum room, bidirectionally, run by someone who holds the room key. Everything
here exists to keep that relay from eroding what makes Quorum Quorum.
- Multiplayer AI — multiple humans + multiple AI seats in one room; Slack users can talk to any of them, and several at once.
-
Model-agnostic seats —
@claude,@gpt,@llamaall reachable from Slack. - Full room behaviors — delegation and fork/merge threads stay usable.
- Provenance — who-said-what survives the crossing (per-user identity, below).
- Keys never touch Slack — provider and room keys stay on trusted devices.
- No data loss — a dropped connection or hard crash never eats messages; recovery is deterministic, not a coin flip.
- Honest encryption boundary — the bridge decrypts the room and re-posts plaintext into Slack, so Slack can read bridged content. This is a consensual gateway, announced in the room, never silent.
Per-user mapping (Model B): each Slack user maps to a stable Quorum handle,
and the bridge posts each message authored as that handle (one RoomClient
per active Slack user, which also gives real presence). The AI seat then receives
real { author, content } provenance and can address people by name. Outbound,
the bridge rewrites an AI's @alice into Slack's real <@U…> mention so the
right person is pinged. (Handles are shared-key labels, not cryptographic
identities — trusted because the bridge is trusted.)
Slack owns the / namespace: a raw /fork A B is intercepted as an unknown
slash command and never reaches the channel. The split that avoids all collision:
-
Talking to AIs is just chat.
@claude summarize thisis ordinary message text (an AI handle isn't a Slack member, so it stays literal), relayed into the room; the seat replies. Multiple seats and delegation (@claude delegate scribe using openai/gpt-5 …) — all plain text. -
Structured actions go under one registered command:
/quorum. Register exactly one Slack slash command and namespace everything beneath it, so it can never clash with Slack's built-ins or other apps:-
/quorum agent claude --provider anthropic— seat an AI (see below) -
/quorum fork A B·/quorum set A owner alice·/quorum merge A B -
/quorum status— who's live, branches, bridge state It works over Socket Mode (no public URL); the bridge echoes results withresponse_type: in_channel.
-
-
/quorum key …is refused, on purpose. Keys never go through Slack.
Where a Slack-summoned AI runs: on the bridge host, using the host's own
quorum setup credentials (a trusted machine). Slack users summon and talk to
AIs; they never see or supply a key. Seats running elsewhere (a laptop, the app)
are equally reachable — they're just seats on the bus.
Everything relayed into the room becomes a durable op, so native seats and the AI are always on the full-record (superset) side. Slack is a projection, not a CRDT replica. Close the seams:
-
The bridge is a persisting, auto-reconnecting seat (
RoomStore+ reconnect) — on reconnect it replays missed ops from the relay's log; the Quorum side self-heals. -
Two high-water-mark cursors, persisted to disk:
slackTs(last Slacktsrelayed in) andquorumOp(last op relayed out). On restart: pullconversations.historysinceslackTs; replay ops sincequorumOp. - Backfill on link (optional): post the last N Quorum messages into Slack for context — never the whole log.
-
Presence both ways so nobody addresses a ghost: Model B surfaces Slack users
in the roster; the bridge posts native joins/leaves + a
🔗 bridged to #generalbanner into Slack.
Correctness must not depend on a graceful shutdown; a kill -9 recovers
identically. Idempotency both ways:
-
Inbound (Slack → Quorum): derive the op id deterministically from the
Slack message
ts. Re-relaying yields the same id; the relay dedupes by id → re-delivery is a no-op. At-least-once + idempotent ⇒ effectively exactly-once. -
Outbound (Quorum → Slack): stamp each Slack post's
metadatawith itsquorumOpid. On restart, scan recent history for the highestquorumOpalready posted and resume past it → closes the post-then-crash duplicate window. - Cursors advance only after confirmation and persist atomically.
Graceful exit (SIGINT/SIGTERM) then flushes in-flight relays, persists cursors,
posts 🔌 bridge offline to both sides, and closes sockets — a nicety, not a
crutch, since an ungraceful exit loses nothing either.
This app is in Slack's sensitive category — its job is moving message content out of a workspace to external participants and third-party AI. The self-hosted, single-workspace path keeps the policy surface small. (Not legal advice; verify against Slack's current API Terms of Service, Acceptable Use Policy, and — only if you ever list publicly — Marketplace review requirements.)
Why single-workspace is the low-bar path: a workspace admin creates a custom app in their own workspace and installs it. No Slack review, no Marketplace listing — a trusted admin is opting their own org in. You still comply with the API TOS; the requirements are mostly hygiene:
-
Minimal OAuth scopes, each justified — likely
commands(the/quorumcommand),channels:history+channels:read,chat:write,users:read(identity mapping), andconnections:writefor Socket Mode; addfiles:*/reactions:*only when v2 needs them. Nothing extra. -
Security basics — verify the Slack signing secret on inbound, store the
bot token securely, HTTPS, respect the tiered rate limits (handle
429, don't spam with huge backfills). Socket Mode so no public URL is needed.
The crux — egress + AI (applies even self-hosted):
-
Transparency & consent. Because content leaves Slack (to other room
participants and to third-party LLMs), bridging must be explicit:
admin-installed, per-channel opt-in, and an in-channel banner
(
🔗 this channel is bridged to an external Quorum room + AI; messages are shared outside Slack). That banner is non-optional. - AI disclosure + no-training. Disclose that third-party models process content, and use providers' no-train API tiers (Anthropic/OpenAI don't train on API data by default). Slack's API TOS forbids using Slack data to train ML without consent — honor it.
- Respect admin governance. Enterprise Grid admins can approve/deny apps and run DLP; the bridge must function within those controls — and some workspaces will block it by policy, which is expected for an egress app. Don't fight it.
-
No secrets in Slack.
/quorum keyis refused; keys live on the bridge host.
How the design already leans compliant: the 🔗 bridged announcement is the
transparency Slack expects; keys-never-in-Slack is clean data hygiene; per-channel
opt-in + admin install is proper authorization; the single namespaced /quorum
command is least-privilege.
Public Marketplace listing — deferred. It triggers full Slack review (security questionnaire, published privacy policy, OAuth justification, and hard scrutiny of the egress + AI). Approvable with strong consent UX + a real privacy policy + no-train guarantees, but it's a deliberate later compliance project, not v1.
-
v1 — text both ways, per-user identity,
/quorumcommands, the full continuity + durability model above. Correct before rich. -
v2 — map Quorum control messages ↔ Slack edits/deletes/reactions (so the
AI never acts on stale text), blob attachments ↔
files.upload, replies ↔ Slack threads.
Ship as a separate package (@schady4/quorum-slack / a quorum bridge slack
entry) so @slack/bolt stays out of the core bundle. The bridge depends only on
the SDK's public surface (RoomClient, room crypto, control codecs) — a reference
consumer of "build on the bus," nothing privileged.
Getting started
Run & share
Build
Design / RFCs
Maintainers
📱 The chat app is a separate repo: quorum-app