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

Merged
amoussa1229 merged 1 commit from fix/agent-team-slack-listener-matcher into main 2026-06-22 17:16:35 +00:00
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`