Summary
slack_explore_workspace currently surfaces channels (public + private) and users, but the user listing has no link back to the per-user direct-message channel. Callers that want to send a DM must either ask the model to call conversations.open themselves or carry that mapping out-of-band.
The Slack Web API exposes existing DM channels via conversations.list?types=im, which returns { id, user } pairs for every IM channel the user already has open. Including this in the workspace listing lets the model resolve user_id → dm_channel_id directly from one discovery call, avoiding a follow-up conversations.open round-trip in the common case.
Current behavior
The ## Users section of the tool's output looks like:
## Users
- Alice (U01234567)
- Bob (U01234568)
Proposed behavior
Append — DM channel: <id> to a user row when an IM channel already exists for that user:
## Users
- Alice (U01234567) — DM channel: D01234567
- Bob (U01234568)
Rows for users without an existing IM channel are unchanged. The docstring example for the tool should be updated to match.
Implementation sketch
- Scope — add
SlackScope.IM_READ (already defined in apron_tools.providers.slack.scopes) to slack_explore_workspace's entry in SCOPES.
- Fetch — after fetching
users.list, paginate conversations.list?types=im&limit=1000, threading cursor from response_metadata.next_cursor until exhausted. Collect { user_id: channel_id }.
- Annotate — when emitting each user row, look up the user's id in that map and append
" — DM channel: {id}" if present; otherwise emit the row unchanged.
- Partial failure — if the
conversations.list IM call raises, treat it as a soft failure consistent with the existing per-type channel fetch (contextlib.suppress(SlackApiError)): users without DM channel IDs are still listed and the rest of the output is unaffected. If the error is missing_scope, the result should still degrade gracefully (the scope set already recommends im:read so the consent modal can surface it).
Why im:read (not im:history)
im:read grants the bare minimum: list IM channels and their members. The tool only needs the { id, user } mapping; it does not read message history. im:history would be over-privileged for this use case.
Test coverage to add
- Happy path: two users, one with an existing IM channel, one without — assert the row format differs.
- DM pagination:
conversations.list returns next_cursor, second page contains the matching IM — assert that user's row gains the suffix.
missing_scope on conversations.list (im): assert the user listing still renders (without DM suffixes).
- Generic
conversations.list failure: assert the listing still renders.
Out of scope (and why)
- Changes to
slack_send_direct_message (echoing the DM channel ID in success output, improved error formatting). Why: these are separable, additive improvements with no shared code path with the discovery tool — folding them in would widen the diff and couple two unrelated changes. Happy to file separately if useful.
- Caching the user → DM-channel map between invocations. Why: the tool is discovery-shaped — a fresh fetch per call keeps it stateless and avoids stale mappings when DMs are opened/closed between calls. The added latency (one paginated
conversations.list) is acceptable for a discovery call.
Compatibility
Pure additive: the change adds one scope to the tool's recommended set and one optional suffix per user row. Callers that already grant im:read see DM channels appear; callers that don't see the existing output unchanged. No tool name, parameter shape, or output schema change beyond the suffix.
Summary
slack_explore_workspacecurrently surfaces channels (public + private) and users, but the user listing has no link back to the per-user direct-message channel. Callers that want to send a DM must either ask the model to callconversations.openthemselves or carry that mapping out-of-band.The Slack Web API exposes existing DM channels via
conversations.list?types=im, which returns{ id, user }pairs for every IM channel the user already has open. Including this in the workspace listing lets the model resolveuser_id → dm_channel_iddirectly from one discovery call, avoiding a follow-upconversations.openround-trip in the common case.Current behavior
The
## Userssection of the tool's output looks like:Proposed behavior
Append
— DM channel: <id>to a user row when an IM channel already exists for that user:Rows for users without an existing IM channel are unchanged. The docstring example for the tool should be updated to match.
Implementation sketch
SlackScope.IM_READ(already defined inapron_tools.providers.slack.scopes) toslack_explore_workspace's entry inSCOPES.users.list, paginateconversations.list?types=im&limit=1000, threadingcursorfromresponse_metadata.next_cursoruntil exhausted. Collect{ user_id: channel_id }." — DM channel: {id}"if present; otherwise emit the row unchanged.conversations.listIM call raises, treat it as a soft failure consistent with the existing per-type channel fetch (contextlib.suppress(SlackApiError)): users without DM channel IDs are still listed and the rest of the output is unaffected. If the error ismissing_scope, the result should still degrade gracefully (the scope set already recommendsim:readso the consent modal can surface it).Why
im:read(notim:history)im:readgrants the bare minimum: list IM channels and their members. The tool only needs the{ id, user }mapping; it does not read message history.im:historywould be over-privileged for this use case.Test coverage to add
conversations.listreturnsnext_cursor, second page contains the matching IM — assert that user's row gains the suffix.missing_scopeonconversations.list (im): assert the user listing still renders (without DM suffixes).conversations.listfailure: assert the listing still renders.Out of scope (and why)
slack_send_direct_message(echoing the DM channel ID in success output, improved error formatting). Why: these are separable, additive improvements with no shared code path with the discovery tool — folding them in would widen the diff and couple two unrelated changes. Happy to file separately if useful.conversations.list) is acceptable for a discovery call.Compatibility
Pure additive: the change adds one scope to the tool's recommended set and one optional suffix per user row. Callers that already grant
im:readsee DM channels appear; callers that don't see the existing output unchanged. No tool name, parameter shape, or output schema change beyond the suffix.