fix(agent-team): repair Slack listener block_actions matcher; add dedicated Slack app (#25)

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).
This commit is contained in:
Adam Moussa 2026-06-22 13:16:35 -04:00 • committed by GitHub
parent 2d1dca0804
commit f59b293022
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 110 additions and 3 deletions

View file

@ -73,6 +73,7 @@ SECURITY
from __future__ import annotations from __future__ import annotations
import logging import logging
import re
from collections.abc import Mapping from collections.abc import Mapping
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -322,7 +323,12 @@ class SlackListener:
def _forward(body: Mapping[str, Any]) -> None: def _forward(body: Mapping[str, Any]) -> None:
self.handle_event(body) 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: def _on_action(ack: Any, body: Mapping[str, Any]) -> None:
ack() ack()
_forward(body) _forward(body)

View file

@ -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)"
```

View file

@ -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
}
}

View file

@ -172,11 +172,11 @@ so the deps MUST land here.
cd ~/orchestrator/agent-team cd ~/orchestrator/agent-team
python3 -m venv .venv python3 -m venv .venv
. .venv/bin/activate . .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 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 > `langgraph-checkpoint-sqlite==3.1.0`; match those exactly here. The other
> runtime deps (`claude-agent-sdk`, `slack_sdk`, `slack_bolt`, `requests`) are > runtime deps (`claude-agent-sdk`, `slack_sdk`, `slack_bolt`, `requests`) are
> not yet in `requirements.txt` (D-5 open) — installed ad-hoc here. `requests` > not yet in `requirements.txt` (D-5 open) — installed ad-hoc here. `requests`