You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two surfaces that together give a monitored user complete legibility and control over Slack channel monitoring — shipped as one because the transparency surface IS the control surface (the opt-out lives inside it).
Per-user opt-out from Slack-sourced feedback (composes against the per-actor opt-out scope locked at #1213):
Transparency surface for monitored Slack channels (composes against the contributor transparency framework from #1213):
Webapp account / privacy page renders a "Monitored Slack channels" section listing every channel currently monitored for each workspace the user is in: channel name, workspace, retention window, the admin who allow-listed it, allow-list date, and the user's current opt-out state.
Each row links to a per-channel detail view: announce template + user's last 10 findings from this channel.
Slack-side: DMing the bot transparency (or near-synonyms) returns the same content as a Block Kit response.
The surface answers four legibility questions per channel (Hoel et al. 2018 transparency-to-the-observed): What is monitored? Who decided? How long is it kept? How can I stop it?
Why
The privacy epic locks "opt-out suppresses delivery, not generation or storage" as the per-actor opt-out scope. The user needs that lever, and the Slack-side button on the very DM they want to stop is the autonomy-supportive design. Monitoring chat without making the monitoring visible to the user is the failure mode this epic fights against — the transparency surface is the legibility surface, and it is also the surface that makes the DPIA defensible. Shipping the opt-out and the transparency surface together prevents one shipping without the other, which is exactly the failure mode the literature warns against.
Acceptance criteria
user_workspace_preference.suppress_slack_sourced_feedback column exists via Liquibase changeset; default false
Webapp account settings exposes one toggle per workspace; saving writes the preference; integration test exercises the toggle + asserts suppression on next finding
Slack DM-side: every chat-sourced finding DM carries a "Pause Slack-sourced feedback for this workspace" Block Kit button; clicking writes the preference + posts an ephemeral confirmation
Toggle-off (resume) restores normal delivery; toggle is reversible without admin action; findings produced during the opt-out window remain in suppressed_by_user_opt_out permanently (readable via subject-access export, not via DM backfill)
Webapp account / privacy page renders a "Monitored Slack channels" section; integration test loads the page for a user in two workspaces with three monitored channels each and asserts all six rows render with correct metadata
Each row links to a per-channel detail view showing the announce template + the user's last 10 findings from this channel
Slack-side transparency DM returns parallel content as a Block Kit response; integration test asserts block count + content
The four legibility questions are explicitly answered per channel row; a content test asserts each row's serialized payload contains the four field names
Both surfaces reflect opt-out state changes within one minute (no caching > 60s)
A workspace with zero monitored Slack channels renders the section as "No Slack channels are currently monitored for this workspace" rather than an empty block
An opt-out reflection test (transparency surface mirrors current opt-out state).
Implementation notes
The preference is per-workspace because a user in two workspaces may consent to Slack-sourced feedback in one and not the other; per-source consent specificity is the GDPR Art. 7 requirement reflected in the rest of the epic.
The Slack DM button writes through a server-side endpoint authenticated via Slack's request signing + the user's identity_link lookup; an ArchUnit test asserts the endpoint does not trust a Slack user_id from the request body — only the verified payload.
The opt-out does not disable backfill ingestion or live event normalization; events still become IntegrationEvent rows and feed practice detection. Opt-out is at the delivery surface only.
The webapp transparency surface is rendered server-side via /api/me/transparency/slack; webapp and Slack DM handler consume the same endpoint shape.
The detail view's "last 10 findings" reads practice_finding filtered by data.source_channel = <channel_id>; for findings produced before source-channel attribution was added, the row renders as "older finding — channel not recorded".
The Block Kit button's action_id is stable + namespaced (hephaestus:scm:opt-out:<workspace_slug>) so the Slack interaction handler routes it predictably.
The transparency surface lists every monitored channel the user is a member of, even channels they've never posted in — the channel is monitored regardless of the user's participation; the user has a right to know.
Part of #1205.
What ships
Two surfaces that together give a monitored user complete legibility and control over Slack channel monitoring — shipped as one because the transparency surface IS the control surface (the opt-out lives inside it).
Per-user opt-out from Slack-sourced feedback (composes against the per-actor opt-out scope locked at #1213):
user_workspace_preference.suppress_slack_sourced_feedback(boolean, defaultfalse) on the table from feat(server,webapp): per-user per-workspace public-attribution opt-in #1275.practice_finding_delivery.status=suppressed_by_user_opt_out.practice_findingand surface in the subject-access export from feat(server): subject-access export (CSV + JSON) via admin impersonation #1214.Transparency surface for monitored Slack channels (composes against the contributor transparency framework from #1213):
transparency(or near-synonyms) returns the same content as a Block Kit response.Why
The privacy epic locks "opt-out suppresses delivery, not generation or storage" as the per-actor opt-out scope. The user needs that lever, and the Slack-side button on the very DM they want to stop is the autonomy-supportive design. Monitoring chat without making the monitoring visible to the user is the failure mode this epic fights against — the transparency surface is the legibility surface, and it is also the surface that makes the DPIA defensible. Shipping the opt-out and the transparency surface together prevents one shipping without the other, which is exactly the failure mode the literature warns against.
Acceptance criteria
user_workspace_preference.suppress_slack_sourced_feedbackcolumn exists via Liquibase changeset; defaultfalsepractice_finding;practice_finding_delivery.status=suppressed_by_user_opt_outsuppressed_by_user_opt_outpermanently (readable via subject-access export, not via DM backfill)transparencyDM returns parallel content as a Block Kit response; integration test asserts block count + contentkind=SLACK; ArchUnit test asserts no Slack-specific transparency UI lives outside the framework's slotchat_finding.delivery.suppressed_by_user_opt_out{workspace_id}is exposed via feat(server): per-integration health endpoint and structured-log MDC #1217delivery_suppressed_by_userTests to write
SlackSourcedFeedbackOptOutIT— toggle on, finding produced, no DM, row present inpractice_finding.SlackTransparencySurfaceWebappIT— full webapp render for a multi-workspace user.SlackTransparencyDmIT— Slack-sidetransparencyDM returns parallel content.Implementation notes
identity_linklookup; an ArchUnit test asserts the endpoint does not trust a Slackuser_idfrom the request body — only the verified payload.IntegrationEventrows and feed practice detection. Opt-out is at the delivery surface only./api/me/transparency/slack; webapp and Slack DM handler consume the same endpoint shape.transparencyDM handler is registered on the per-workspace BoltAppfrom refactor(server): per-workspace Bolt App factory replacing the singleton #1244; the trigger string matches case-insensitively + handles a few common phrasings through a small allow-list of synonyms.practice_findingfiltered bydata.source_channel = <channel_id>; for findings produced before source-channel attribution was added, the row renders as "older finding — channel not recorded".action_idis stable + namespaced (hephaestus:scm:opt-out:<workspace_slug>) so the Slack interaction handler routes it predictably.Dependencies
Depends on #1268, #1275, #1213, #1214, #1244.