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:
parent
2d1dca0804
commit
f59b293022
4 changed files with 110 additions and 3 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
61
agent-team/slack/README.md
Normal file
61
agent-team/slack/README.md
Normal 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)"
|
||||||
|
```
|
||||||
40
agent-team/slack/agent-team-manifest.json
Normal file
40
agent-team/slack/agent-team-manifest.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
Reference in a new issue