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-manifest.json | ||
| README.md | ||
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 (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.
- Bot token (
xoxb-) — https://api.slack.com/apps/A0BC7AT8NUD/oauth → Install to Workspace → approve → copy the Bot User OAuth Token →SLACK_BOT_TOKEN. - App-level token (
xapp-) — https://api.slack.com/apps/A0BC7AT8NUD/general → App-Level Tokens → Generate Token and Scopes → add scopeconnections:write→ copy →SLACK_APP_TOKEN. - Channel — create/choose the clarifier channel,
/invite @agent-team, copy itsC0...id →SLACK_CHANNEL_ID. - 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
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)"