diff --git a/agent-team/slack/README.md b/agent-team/slack/README.md index 222f1d7..52abb50 100644 --- a/agent-team/slack/README.md +++ b/agent-team/slack/README.md @@ -7,55 +7,66 @@ that the nightly secrev sweep uses, to isolate the two-way bot's trust surface. | Field | Value | |---|---| | App name | Sea Haven agent-team | -| App ID | `A0BC7AT8NUD` | -| Org / team | seahaven (`E0A524V806L`) | +| App ID | `A0BCC7TTU66` | +| Bot | `agent-team` (handle `@agentteam2`) · user id `U0BCFSJ7SUC` | +| Scope | **Workspace-level** app, installed to **Sea Haven Industries** (`T0A46CP6QR3`) | | Manifest | [`agent-team-manifest.json`](./agent-team-manifest.json) (source of truth) | -| Settings | https://api.slack.com/apps/A0BC7AT8NUD | +| Settings | https://api.slack.com/apps/A0BCC7TTU66 | + +> ## ⚠️ Must be a WORKSPACE-LEVEL app (hard lesson, 2026-06-22) +> Socket Mode event delivery only works for **workspace-level** apps. An +> **org-owned** app (one created via the Enterprise **org/config token** / +> `apps.manifest.create`, even when workspace-granted with +> `--org-workspace-grant`) **connects the socket but receives ZERO workspace +> Events API events** — outbound `chat.postMessage` works, but inbound +> `message`/`app_mention` are never delivered, so the clarifier never hears +> answers. The first build hit exactly this with the now-deleted org app +> `A0BC7AT8NUD`. **Create this app from the dashboard** (api.slack.com/apps → +> Create New App → From manifest) and **pick the *workspace* "Sea Haven +> Industries" as the dev workspace**, NOT the Enterprise org. Then a normal +> *Install to Workspace* works (no org-grant dance). Do **not** recreate it via +> the org config token. ## Why these scopes (verified against the code) `agent_team/transport/slack_listener.py` subscribes over Socket Mode to `message`, `app_mention`, and Block Kit `block_actions`; `slack_live.py` posts -questions via `chat.postMessage`. +questions via `chat.postMessage`. Inbound message/app_mention arrive as the +Events API envelope `{"type":"event_callback","event":{...}}` and a free-text +thread reply is matched to its question by `thread_ts == channel_ref` (see +`find_open_question_by_channel_ref`). | Capability | Scope / setting | Why | |---|---|---| | Post clarifier questions | `chat:write` | `slack_live.py` `chat.postMessage` | -| Hear thread replies in the channel | `channels:history` / `groups:history` + `message.channels`/`message.groups` events | `@app.event("message")` | -| Hear DM replies | `im:history` + `message.im` event | DM answer path | -| Hear @mentions | `app_mentions:read` + `app_mention` event | `@app.event("app_mention")` | -| Block Kit button/select answers | `interactivity.is_enabled` | `@app.action({})` | -| Inbound WebSocket | `socket_mode_enabled` + an app-level token w/ `connections:write` | VPN-only box, no public HTTPS endpoint | +| Hear thread replies | `channels:history` / `groups:history` + `message.channels`/`message.groups` | `@app.event("message")` | +| Hear DM replies | `im:history` + `message.im` | DM answer path | +| Hear @mentions | `app_mentions:read` + `app_mention` | `@app.event("app_mention")` | +| Block Kit answers | `interactivity.is_enabled` | `@app.action(...)` | +| Inbound WebSocket | `socket_mode_enabled` + app-level token w/ `connections:write` | VPN-only box, no public HTTPS endpoint | Inbound auth is NOT scope-based: AUTHZ-01 (`AGENT_TEAM_SLACK_OWNER_IDS`) gates the *sender* and fails closed. Socket membership alone is never authorization. -## Remaining manual token mints (operator, in browser) +## Setup (operator, in browser — secrets never echoed) -Both produce secrets — paste them straight into `~/secrev.env` on the box -(mode 600), never into shell history. - -1. **Bot token (`xoxb-`)** — https://api.slack.com/apps/A0BC7AT8NUD/oauth → - *Install to Workspace* → approve → copy the **Bot User OAuth Token** → +1. **Create the app** — api.slack.com/apps → *Create New App* → *From an app + manifest* → **pick workspace "Sea Haven Industries"** → paste + `agent-team-manifest.json`. +2. **Install to Workspace** → copy the **Bot User OAuth Token** (`xoxb-`) → `SLACK_BOT_TOKEN`. -2. **App-level token (`xapp-`)** — https://api.slack.com/apps/A0BC7AT8NUD/general - → *App-Level Tokens* → *Generate Token and Scopes* → add scope - `connections:write` → copy → `SLACK_APP_TOKEN`. -3. **Channel** — create/choose the clarifier channel, `/invite @agent-team`, - copy its `C0...` id → `SLACK_CHANNEL_ID`. -4. **Owner allowlist** — `AGENT_TEAM_SLACK_OWNER_IDS` = Adam's Slack user id - (`U0A3SC48T47`), comma-separated if more than one. Gate fails closed if empty. +3. **Basic Information → App-Level Tokens → Generate** with scope + `connections:write` → `SLACK_APP_TOKEN`. +4. **Channel** — `/invite @agentteam2` into the clarifier channel; its `C0…` id + → `SLACK_CHANNEL_ID`. +5. **Owner allowlist** — `AGENT_TEAM_SLACK_OWNER_IDS` = Adam's Slack user id + (`U0A3SC48T47`), comma-separated if more than one. Fails closed if empty. -## Reproduce / update the app from the manifest +All five land in `~/secrev.env` on the box (mode 600), never shell history. -```bash -TOKEN=$(python3 -c "import json;print(json.load(open(os.path.expanduser('~/.slack/credentials.json')))['E0A524V806L']['token'])") -# validate -curl -s -X POST https://slack.com/api/apps.manifest.validate \ - -H "Authorization: Bearer $TOKEN" --data-urlencode "manifest=$(cat agent-team-manifest.json)" -# update existing app -curl -s -X POST https://slack.com/api/apps.manifest.update \ - -H "Authorization: Bearer $TOKEN" \ - --data-urlencode "app_id=A0BC7AT8NUD" \ - --data-urlencode "manifest=$(cat agent-team-manifest.json)" -``` +## Updating the app from the manifest + +Edit `agent-team-manifest.json`, then in the dashboard (App Manifest tab) paste +the updated manifest, or use `apps.manifest.update` with an app **config token +scoped to the workspace app** (the org config token can read/manage it as org +owner, but keep the app workspace-level — do not re-create it org-owned).