Skip to content

feat(gateway): capability-gated Slack read tools (channel history) + capability model#1848

Open
kgabryje wants to merge 1 commit into
gateway-slack-image-ingestionfrom
gateway-slack-read-tools-capabilities
Open

feat(gateway): capability-gated Slack read tools (channel history) + capability model#1848
kgabryje wants to merge 1 commit into
gateway-slack-image-ingestionfrom
gateway-slack-read-tools-capabilities

Conversation

@kgabryje

@kgabryje kgabryje commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

Introduces the Slack capability model — per-channel checkboxes that map 1:1 to agent-visible MCP tools — and lands its first new consumer: reading whole-channel history (#1731).

Stacked on #1847 (gateway-slack-image-ingestion). Will be rebased onto main after PR B merges — only the last commit is this PR's diff.

Capability model (single source of truth)

  • New config.agent_tools object on Slack gateway channels (SlackAgentToolsConfig in packages/core/src/types/gateway.ts): booleans for agent-callable tools, currently thread_history and channel_history. Extending it (PR D: reactions, file upload) is one seam — add a key, a default, and a scope list.
  • requireGatewayCapability(channel, capability) gates tools at call time with an actionable error (which capability, that an admin enables it on the channel, and that enabling can add Slack scopes requiring a manifest update + reinstall). Fail-on-call rather than hide-at-discovery because the MCP tool registry is a global singleton built once for all channels/callers.
  • The check runs against the target channel (the one whose bot token performs the read), so the checkbox gates the tool for every caller — a non-gateway session on the same branch can't bypass it. The calling session's gateway_source is still the default way the new tool resolves its target channel.
  • Scopes cannot drift from gating: SLACK_AGENT_TOOL_SCOPES in slack-manifest.ts maps each capability to its OAuth scopes and requiredBotScopes consumes it via the new required SlackWizardOptions.agentTools. Toggles are surfaced in the UI wizard/edit form and agor_gateway_slack_manifest_generate exactly like ingestFiles in feat(gateway): ingest inbound Slack image attachments #1847.

Defaults (no behavior regression)

  • thread_history defaults enabled — the thread tool shipped ungated, so absent config keeps existing channels working. It contributes no extra scopes (mapped threads only exist on surfaces whose history scopes the manifest already requests).
  • channel_history defaults disabled — reading arbitrary channel content is a broader data surface than the mapped thread and needs scopes (channels:history, groups:history, mpim:history) the installed app may not hold, so it's explicit opt-in.

New read tool: agor_gateway_slack_channel_history_get (#1731)

  • New SlackConnector.fetchChannelHistory wraps conversations.history (shared pagination/normalization loop with fetchThreadHistory), enforces allowed_channel_ids, and returns the most recent matches in chronological order. Params mirror the thread tool (oldestTs/latestTs/limit≤200/includeBotMessages/format: messages|markdown).
  • Branch-bound exactly like the feat(gateway): bind outbound emit + Slack reads to the calling session's branch #1799 thread tool: sessions may only read channels targeting their own branch (no admin bypass); callers without session context need admin or branch all.
  • Called from a gateway-created session with no args, it defaults to that session's own gateway channel and Slack conversation.
  • Existing agor_gateway_slack_thread_history_get is now gated behind thread_history.

Out of scope: Slack search is intentionally excluded — search.messages is user-token-only and assistant.search.context needs a user-interaction action_token, so neither works with the gateway bot token (noted in code). The optional user_lookup tool was deferred to keep the PR focused. Outbound reactions/file-upload/thread-reply land in PR D on top of this.

Tests

  • slack-manifest.test.ts: capability→scope mapping (on → history scopes present, off/absent → absent), thread_history contributes none, resolveSlackAgentTools defaults + malformed input.
  • slack.test.ts: fetchChannelHistory normalization/ordering/bot filtering, most-recent-first truncation, allowed_channel_ids allow/deny before any API call.
  • gateway-channels.test.ts: channel tool happy path via gateway-session defaults, markdown format, capability-disabled → actionable error, cross-branch denied even for admins, no-session path requires explicit IDs + admin/branch-all, thread tool denied when thread_history disabled (absent config covered by existing tests), manifest tool scope/config-hint wiring + schema defaults.

pnpm typecheck, lint, build, and core (2772) / daemon (1529) / UI (796) test suites green. The check:multitenancy-boundaries 5-file failure, @agor/git no-test-files exit, and 2 executor test failures are pre-existing on the base (verified via stash) and untouched by this PR.

🤖 Generated with Claude Code

…capability model

Per-channel agent_tools config gates agent-callable Slack MCP tools and
drives the manifest scopes from one source of truth:

- config.agent_tools.{thread_history,channel_history} booleans on Slack
  gateway channels; thread_history defaults enabled (preserves shipped
  behavior), channel_history defaults disabled (new data surface).
- requireGatewayCapability fails on call with an actionable error (the
  MCP tool registry is a global singleton, so per-channel hiding at
  discovery is not possible).
- SLACK_AGENT_TOOL_SCOPES maps each capability to its Slack OAuth
  scopes; requiredBotScopes consumes it via SlackWizardOptions.agentTools
  so tool-gating and manifest scopes cannot drift.
- New agor_gateway_slack_channel_history_get tool wrapping
  conversations.history: capability-gated, branch-bound like the thread
  tool, whitelist-enforced, defaulting to the calling gateway session's
  own channel.
- Existing thread-history tool now gated behind thread_history.
- Toggles surfaced in the UI wizard/edit form and the MCP manifest tool
  the same way ingest_files is wired.

Slack search is intentionally not exposed: search.messages is
user-token-only and assistant.search.context needs a user-interaction
action_token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@mistercrunch mistercrunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed-gpt-5-5 Reviewed by GPT-5.5 AI reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants