|
| 1 | +# Chat bridges |
| 2 | + |
| 3 | +Bernstein ships bidirectional chat drivers so operators can drive a session, |
| 4 | +approve tool calls, and watch streamed agent output from a messaging app |
| 5 | +without keeping a terminal open. Bridges are configured per-platform via |
| 6 | +`bernstein chat serve --platform=<name>`. |
| 7 | + |
| 8 | +## Supported platforms |
| 9 | + |
| 10 | +| Platform | Status | Optional extra | Tokens | |
| 11 | +|----------|--------|---------------|--------| |
| 12 | +| Telegram | Production | `pip install 'bernstein[telegram]'` | `BERNSTEIN_TELEGRAM_TOKEN` | |
| 13 | +| Slack | Production | `pip install 'bernstein[slack]'` | `BERNSTEIN_SLACK_TOKEN` (bot) + `BERNSTEIN_SLACK_APP_TOKEN` (Socket Mode app) | |
| 14 | +| Discord | Stub only | n/a | n/a | |
| 15 | + |
| 16 | +## Slack setup |
| 17 | + |
| 18 | +1. Create a Slack app and enable Socket Mode. |
| 19 | +2. Add the scopes `chat:write`, `commands`, `app_mentions:read`, plus any |
| 20 | + scopes your slash command surface needs. |
| 21 | +3. Generate an app-level token (`xapp-...`) with the `connections:write` |
| 22 | + scope. Install the app to your workspace and copy the bot token |
| 23 | + (`xoxb-...`). |
| 24 | +4. Map a `/bernstein` slash command in your Slack app configuration. The |
| 25 | + driver routes subcommands (`run`, `approve`, `reject`, `status`, |
| 26 | + `switch`, `stop`, `handoff`) from the text body of the slash payload. |
| 27 | +5. Set the two env vars: |
| 28 | + |
| 29 | + ```sh |
| 30 | + export BERNSTEIN_SLACK_TOKEN=xoxb-... |
| 31 | + export BERNSTEIN_SLACK_APP_TOKEN=xapp-... |
| 32 | + ``` |
| 33 | + |
| 34 | +6. Start the bridge: |
| 35 | + |
| 36 | + ```sh |
| 37 | + bernstein chat serve --platform=slack |
| 38 | + ``` |
| 39 | + |
| 40 | +## What the driver guarantees |
| 41 | + |
| 42 | +- **Slash dispatch and button decode.** `/bernstein run "..."` and the |
| 43 | + inline `Approve` / `Reject` buttons are routed through the same handler |
| 44 | + surface as the Telegram driver. |
| 45 | +- **Edit debounce.** Streaming agent output collapses into one |
| 46 | + `chat.update` per channel per second so Slack's per-channel rate limit |
| 47 | + on `chat.update` stays comfortably out of reach. |
| 48 | +- **Attested approvals.** Every approval resolution is appended to the |
| 49 | + HMAC-chained audit log as a `chat.slack.approval` event whose details |
| 50 | + cover `(approver, message_ts, decision, tool_call_hash, worktree_id)`. |
| 51 | + Replaying the chain reproduces the post-approval scheduler state |
| 52 | + byte-identically. |
| 53 | +- **Worktree pinning.** An `/approve` for a worker bound to worktree |
| 54 | + `wt-a` cannot resolve a pending approval registered against a |
| 55 | + different worktree. Cross-worktree attempts log a |
| 56 | + `chat.slack.approval_rejected` audit entry and raise |
| 57 | + `CrossWorktreeApprovalError` so the bypass is visible to the operator. |
| 58 | +- **Outbound message signing.** Every outbound chat message carries an |
| 59 | + Ed25519 detached signature over `(install_id, session_id, |
| 60 | + content_hash)`. A recipient with the install's public key can verify |
| 61 | + the message was not injected by another bernstein install |
| 62 | + impersonating the workspace. |
| 63 | + |
| 64 | +## Verifying a Slack message |
| 65 | + |
| 66 | +```python |
| 67 | +from bernstein.core.chat.drivers.slack import verify_chat_signature |
| 68 | + |
| 69 | +ok = verify_chat_signature( |
| 70 | + install_id="<the install id that posted>", |
| 71 | + session_id="<session id from the metadata envelope>", |
| 72 | + content="<text content of the message>", |
| 73 | + signature="<base64 signature from metadata.event_payload>", |
| 74 | + public_key_pem=open(".bernstein/keys/slack/slack-bridge.ed25519.pub", "rb").read(), |
| 75 | +) |
| 76 | +``` |
| 77 | + |
| 78 | +Returns `True` on cryptographic match, `False` on tampered content or a |
| 79 | +foreign install public key. |
| 80 | + |
| 81 | +## Missing SDK behaviour |
| 82 | + |
| 83 | +`bernstein chat serve --platform=slack` raises a structured |
| 84 | +`SlackDependencyError` when `slack-sdk` is not installed, with a |
| 85 | +pointer to `pip install 'bernstein[slack]'`. Install the extra, or |
| 86 | +switch to a platform whose SDK is already on the host. |
0 commit comments