From 48bf4a8290c0836887e835381377c32102f3073d Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 22 Jun 2026 13:12:04 -0400 Subject: [PATCH] fix(agent-team): repair Slack listener block_actions matcher; add dedicated Slack app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Socket Mode inbound listener crashed at registration time on first live run: `@app.action({})` raised `BoltError: action ({}) must be any of str, Pattern, and dict` under slack_bolt 1.28.0, killing the listener thread (the whole inbound answer path — message/app_mention/block_actions — went down, caught only by the coordinator's respawn watchdog). serve() is marked `# pragma: no cover - live socket`, so this was never exercised until the R720 bring-up. Replace the unsupported empty-dict matcher with a catch-all `re.compile(r".*")` action_id regex; handle_event still does the real filtering + AUTHZ-01 owner-allowlist gate, so over-matching is safe. Verified on sh-secrev: listener connects (live Socket Mode WebSocket), outbound chat.postMessage works, 0 errors. /sh-security-review PASS (no confirmed critical/high; matcher change introduces no new findings). Also adds the dedicated Slack app (manifest + README) backing the clarifier gate — "Sea Haven agent-team" (A0BC7AT8NUD), workspace-scoped install to avoid the Enterprise-Grid `scope_not_allowed_on_enterprise` org-install trap — and patches the provisioning runbook's stale langgraph pin (1.1.10 -> 1.2.5). --- .../agent_team/transport/slack_listener.py | 8 ++- agent-team/slack/README.md | 61 +++++++++++++++++++ agent-team/slack/agent-team-manifest.json | 40 ++++++++++++ docs/provisioning/PROVISIONING-RUNBOOK.md | 4 +- 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 agent-team/slack/README.md create mode 100644 agent-team/slack/agent-team-manifest.json diff --git a/agent-team/agent_team/transport/slack_listener.py b/agent-team/agent_team/transport/slack_listener.py index d0472dd..a089e3a 100644 --- a/agent-team/agent_team/transport/slack_listener.py +++ b/agent-team/agent_team/transport/slack_listener.py @@ -73,6 +73,7 @@ SECURITY from __future__ import annotations import logging +import re from collections.abc import Mapping from pathlib import Path from typing import Any @@ -322,7 +323,12 @@ class SlackListener: def _forward(body: Mapping[str, Any]) -> None: self.handle_event(body) - @app.action({}) # any block_actions interaction + # Match ANY block_actions interaction. slack_bolt rejects an empty-dict + # constraint (``BoltError: action ({}) must be any of str, Pattern, and + # dict``); a catch-all ``action_id`` regex is the supported way to + # register a single handler for every block action. ``handle_event`` does + # the real filtering + auth, so over-matching here is safe. + @app.action(re.compile(r".*")) # any block_actions interaction def _on_action(ack: Any, body: Mapping[str, Any]) -> None: ack() _forward(body) diff --git a/agent-team/slack/README.md b/agent-team/slack/README.md new file mode 100644 index 0000000..222f1d7 --- /dev/null +++ b/agent-team/slack/README.md @@ -0,0 +1,61 @@ +# agent-team Slack app + +Dedicated Slack app backing the Plane-2 clarifier human-gate (Socket Mode). +Kept separate from the webhook-only **Tech Notifications** app (`A0ARYQZU3KJ`) +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`) | +| Manifest | [`agent-team-manifest.json`](./agent-team-manifest.json) (source of truth) | +| Settings | https://api.slack.com/apps/A0BC7AT8NUD | + +## 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`. + +| 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 | + +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) + +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** → + `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. + +## Reproduce / update the app from the manifest + +```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)" +``` diff --git a/agent-team/slack/agent-team-manifest.json b/agent-team/slack/agent-team-manifest.json new file mode 100644 index 0000000..64c1e30 --- /dev/null +++ b/agent-team/slack/agent-team-manifest.json @@ -0,0 +1,40 @@ +{ + "display_information": { + "name": "Sea Haven agent-team", + "description": "Human-gated clarifier for the R720 agent-team SDLC pipeline (Plane-2 coordinator).", + "background_color": "#0c4f6f" + }, + "features": { + "bot_user": { + "display_name": "agent-team", + "always_online": true + } + }, + "oauth_config": { + "scopes": { + "bot": [ + "chat:write", + "channels:history", + "groups:history", + "im:history", + "app_mentions:read" + ] + } + }, + "settings": { + "event_subscriptions": { + "bot_events": [ + "message.channels", + "message.groups", + "message.im", + "app_mention" + ] + }, + "interactivity": { + "is_enabled": true + }, + "socket_mode_enabled": true, + "org_deploy_enabled": false, + "token_rotation_enabled": false + } +} diff --git a/docs/provisioning/PROVISIONING-RUNBOOK.md b/docs/provisioning/PROVISIONING-RUNBOOK.md index d2e0328..8453b3b 100644 --- a/docs/provisioning/PROVISIONING-RUNBOOK.md +++ b/docs/provisioning/PROVISIONING-RUNBOOK.md @@ -172,11 +172,11 @@ so the deps MUST land here. cd ~/orchestrator/agent-team python3 -m venv .venv . .venv/bin/activate -pip install langgraph==1.1.10 langgraph-checkpoint-sqlite==3.1.0 \ +pip install langgraph==1.2.5 langgraph-checkpoint-sqlite==3.1.0 \ claude-agent-sdk slack_sdk slack_bolt requests ``` -> **Pin note (D-5):** `requirements.txt` pins `langgraph==1.1.10` / +> **Pin note (D-5):** `requirements.txt` pins `langgraph==1.2.5` / > `langgraph-checkpoint-sqlite==3.1.0`; match those exactly here. The other > runtime deps (`claude-agent-sdk`, `slack_sdk`, `slack_bolt`, `requests`) are > not yet in `requirements.txt` (D-5 open) — installed ad-hoc here. `requests`