ADR-0049: Slack approvals on a laptop: say what is needed, do not open a tunnel¶
Status: accepted (KAM-124)
Context¶
A spec with approval_notifier posts Approve and Reject buttons to Slack. Slack delivers a click by calling a
public HTTPS URL, which a laptop does not have. The mastra-projects/support-triage and incident-responder
demos hit four traps (a public URL is required; a relay such as smee.io re-sends the click as JSON, which breaks
the HMAC; Socket Mode on silently disables HTTP interactivity; an app has one Interactivity URL) and the fix for
the second was hand-written in each demo. V11 needs a stated answer for kampong dev and kampong serve: ship a
tunnel, or say plainly that approval needs a public URL.
Decision¶
- No first-party tunnel. Chosen by the product owner. A built-in tunnel sends a user's traffic through a third
party they did not pick, needs a downloaded binary or an account, and conflicts with "nothing phones home unless
you opt in". It would also be one more process to supervise, and a free tunnel's address changes on restart, so
it would not remove the setup step anyway. An opt-in
--tunnelflag was considered and left out for the same reasons; it can be added later without changing anything here. - Say it, once, where it matters.
kampong serveprints at startup what Slack needs (a public HTTPS URL to/slack/interactions, one Request URL per app, Socket Mode off) and warns whenSLACK_SIGNING_SECRETis unset, since every click would be refused.kampong devsays that the canvas uses its in-app dialog so nothing needs to be public, and points at the docs. Both print only for a spec that has anapproval_notifier(packages/cli/src/slack-notice.ts). - The relay problem is fixed in the product, not in docs.
recoverSlackRawBodyrebuilds the bytes Slack signed from a relayed{ payload }JSON body (payload=plus RFC 3986 encoding, spaces as+). The signature is then verified against that, so the check stays fully on: a forged or altered click is still a401. It is used bykampong serveand by the exported server. A body with no stringpayloadrecovers to the empty string, which never verifies. - A run never depends on Slack.
POST /runs/:id/approveandPOST /approvals/:key/approvealways work, so the whole approval path can be exercised without the buttons. - Documented in
docs/tutorial/slack-approvals.md: the four traps, the setup, and a checklist for a click that does nothing.
Consequences¶
- A person who wants the Slack buttons on a laptop still has to run their own tunnel, and the docs say how. That is the cost of the decision, accepted so the default stays local-first.
- Socket Mode and a wrong Request URL cannot be detected from this side (no request ever arrives), so the notice and the checklist are the only mitigation. The hosted app (V5/V10) has a public URL by construction and does not have this problem.
- The Slack interaction ingress for the hosted server remains V11-P2 (KAM-130).