Skip to content

feat(slack): add read-only Slack channel history tool - #54535

Open
sk-holmes wants to merge 21 commits into
NousResearch:mainfrom
sk-holmes:fix/slack-channel-history-tool
Open

feat(slack): add read-only Slack channel history tool#54535
sk-holmes wants to merge 21 commits into
NousResearch:mainfrom
sk-holmes:fix/slack-channel-history-tool

Conversation

@sk-holmes

@sk-holmes sk-holmes commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Adds an opt-in, service-gated, read-only slack tool for bounded Slack history using the selected profile’s existing live Slack adapter and SDK client.

Actions:

  • list_channels — list bounded public/private channels that the bot belongs to;
  • fetch_history — read a bounded channel or DM timeline;
  • fetch_thread — read a thread parent and replies, including from a validated Slack permalink;
  • find_messages — scan bounded pages using text and URL-domain filters.

The tool provides explicit continuation metadata when Slack pagination, match limits, or local serialization limits leave more data.

Authorization model

By default, the tool can read only the active Slack conversation.

An optional cross-channel exception is available only when all of these conditions hold:

  1. the inbound turn came directly from the local Slack adapter;
  2. the active conversation is a 1:1 D… DM;
  3. the actor’s Slack member ID is explicitly configured in history_cross_channel_user_ids for the selected profile;
  4. the target belongs to the same single connected workspace;
  5. the authenticated credential is proven by auth.test to be a bot token;
  6. the bot is a member of the target channel;
  7. the target passes the selected profile’s allowed_channels policy, when configured;
  8. the target is not another DM or an MPIM/group DM.

Shared channels and threads cannot use the owner exception, preventing private cross-channel results from being disclosed to other participants.

Security and privacy boundaries

  • Direct-Slack provenance is a wire-invisible, per-turn capability stamped only by the live adapter.
  • Queued recursive turns rebind the complete actor/session identity, preventing a later participant from inheriting a previous owner’s authorization.
  • Synthetic, restored, relayed, and background turns cannot borrow local Slack history authority.
  • Multiplex authorization resolves through the selected profile’s adapter and workspace.
  • YAML allowed_channels remains profile-local rather than flowing through a process-global environment bridge.
  • Missing or explicitly empty channel allowlists remain unrestricted; malformed allowlist types or channel IDs fail closed before history APIs and deny normal channel intake.
  • User-token installations may still connect with a warning for existing gateway behavior, but cannot expose the agent-facing history tool or call history/list APIs.
  • Multi-workspace adapters are ineligible; use one bot workspace per served profile.
  • Disallowed targets fail before Slack metadata, list, history, or replies API calls.
  • Permalinks are parsed locally and never fetched as URLs.
  • Returned Slack text is treated as untrusted external data and best-effort redacted.
  • Per-field, per-item, page-count, match-count, collection, and final serialized-response budgets are enforced.
  • Local truncation never advances a remote cursor past omitted items; find_messages fails safely if no match can fit.
  • Slack history results never enter the tool-result persistence backend.
  • The tool exposes no post, reaction, edit, delete, or other Slack mutation action.

Tool and service gating

slack_history is an explicit Slack platform toolset. The model schema is exposed only when the selected profile's running adapter has exactly one authenticated bot workspace and both history entry points are available. Profile identity and live eligibility are part of the availability-cache key, so one eligible sibling profile or stale connect/disconnect state cannot advertise a dead capability.

Example toolset opt-in:

platform_toolsets:
  slack: [hermes-slack, slack_history]

Optional owner-only cross-channel configuration:

slack:
  history_cross_channel_user_ids:
    - "U01ABC2DEF3"

The owner list is profile-local and has no environment fallback. No new credential is introduced.

Related Issue

Fixes #6345

Partially addresses #35291 by adding a read-only Slack history tool and documentation. It does not implement that issue's broader setup wizard, OAuth scope manifest, or event-subscription work.

CI and compatibility repairs included

  • Adds the original contributor’s nmh9097@gmail.com attribution mapping.
  • Corrects the quick-command fixture so a bare MagicMock does not accidentally become an internal message.
  • Initializes _primary_profile_name in the bare multiplex runner fixture.
  • Rebinds queued-turn identity with stack-safe ContextVar tokens.
  • Preserves pagination integrity across remote cursors and local output truncation.
  • Keeps Slack YAML channel allowlists profile-local under multiplex operation.
  • Makes dynamic tool-availability caches profile- and lifecycle-aware while preserving the existing zero-argument registry check contract.
  • Stabilizes dynamic availability snapshots across computation races and bounds contextual result/last-good caches.
  • Binds each contextual availability verdict to the same immutable sample used by its outer cache key, including ABA lifecycle transitions.
  • Validates loaded Slack channel allowlists and fails closed on malformed mapping, numeric, mixed, or invalid-ID values.
  • Validates history-owner settings atomically so a valid owner mixed with malformed input cannot retain cross-channel authority.

Related and overlapping work

Duplicate/overlap search was performed before finalization:

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Slack history tool and toolset

  • tools/slack_tool.py
    • Adds the service-gated, read-only slack tool with list_channels, fetch_history, fetch_thread, and find_messages actions.
    • Reuses the selected profile's live Slack adapter/client instead of introducing another token path.
    • Enforces bounded pagination, query/domain filtering, permalink parsing, redaction, continuation metadata, and final response-size limits.
  • toolsets.py and hermes_cli/tools_config.py
    • Add the explicit slack_history platform toolset and keep it opt-in alongside hermes-slack.

Authorization, identity, and adapter boundaries

  • plugins/platforms/slack/adapter.py
    • Proves bot-token identity with auth.test, binds reads to a single authenticated workspace, and denies user-token or multi-workspace history access.
    • Enforces active-conversation access by default and the narrow direct-1:1-DM owner exception before Slack list, metadata, history, or replies API calls.
    • Applies bot membership and profile-local channel allowlists, rejects cross-DM/MPIM targets, clears principal capability on disconnect, and atomically rejects malformed owner/allowlist configuration.
  • gateway/authz_mixin.py, gateway/session_context.py, and gateway/run.py
    • Stamp trusted direct-Slack provenance, resolve authorization through the selected profile, and rebind/restore complete actor identity for queued or recursive turns.
    • Keep synthetic, restored, relayed, background, and mismatched-profile turns fail closed.
  • gateway/session.py
    • Adds capability-aware Slack prompt context while preserving conservative limitations when the history tool is unavailable.

Configuration and service availability

  • gateway/config.py, hermes_cli/config.py, and cli-config.yaml.example
    • Add profile-local history_cross_channel_user_ids configuration and preserve profile-local allowed_channels values without a process-global environment fallback.
  • model_tools.py and tools/registry.py
    • Scope model-facing tool availability to the selected profile and live adapter lifecycle.
    • Bind availability verdicts to the exact sampled context used by cache keys, prevent cross-profile and ABA lifecycle cache errors, and bound contextual caches.

Output and persistence safety

  • tools/tool_result_storage.py and agent/tool_dispatch_helpers.py
    • Prevent Slack history results from entering the tool-result persistence backend while preserving aggregate tool-result naming.

Tests, documentation, and attribution

  • tests/tools/test_slack_tool.py and the changed gateway, session, multiplex, registry, cache, configuration, CLI, and persistence tests
    • Cover action behavior, pagination/truncation integrity, provenance, bot-principal enforcement, profile/workspace isolation, direct-DM owner boundaries, malformed configuration, queued identity, cache lifecycle/ABA behavior, and no-persistence guarantees.
  • website/docs/reference/tools-reference.md, website/docs/reference/toolsets-reference.md, and website/docs/user-guide/messaging/slack.md
    • Document setup, actions, access boundaries, configuration, pagination, and limitations.
  • scripts/release.py
    • Preserves the original hardening contributor's GitHub attribution.

How to Test

scripts/run_tests.sh \
  tests/tools/test_slack_tool.py \
  tests/gateway/test_slack.py \
  tests/gateway/test_session.py \
  tests/gateway/test_session_env.py \
  tests/gateway/test_slack_history_config.py \
  tests/gateway/test_slack_history_cross_channel.py \
  tests/gateway/test_multiplex_profile_authz.py \
  tests/gateway/test_multiplex_pairing_stores.py \
  tests/gateway/test_background_process_notifications.py \
  tests/gateway/test_run_progress_topics.py \
  tests/gateway/test_relay_upstream_authz.py \
  tests/agent/test_tool_dispatch_helpers.py \
  tests/tools/test_tool_result_storage.py \
  tests/tools/test_registry.py \
  tests/tools/test_terminal_tool_requirements.py \
  tests/test_get_tool_definitions_cache_isolation.py \
  tests/hermes_cli/test_tools_config.py \
  tests/cli/test_quick_commands.py \
  -q

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.5.1

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Screenshot 2026-06-28 at 5 46 49 PM

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/slack Slack app adapter comp/tools Tool registry, model_tools, toolsets labels Jun 29, 2026
@sk-holmes

Copy link
Copy Markdown
Contributor Author

@alt-glitch Is this a P3 though? it's part of core workflows because I will ask the agent to see my last few messages and then process them one by one.

I have a few queues with different things so this becomes a core part of my workflows and could benefit others who may use it this way

@sk-holmes
sk-holmes force-pushed the fix/slack-channel-history-tool branch 7 times, most recently from a6d2ce8 to a80c40b Compare July 5, 2026 06:55
@sk-holmes
sk-holmes force-pushed the fix/slack-channel-history-tool branch from a80c40b to 14202a6 Compare July 6, 2026 22:28

NaMinhyeok commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Hi @sk-holmes — I reviewed and hardened this branch with the original commit/authorship preserved.

GitHub would not allow me to open a PR directly against your fork without collaborator access, so I created a ready-for-review diff here: NaMinhyeok#1

It is based on the exact current head of this PR (14202a6df2f52433efed64efed71c8f63324c8b7) and contains exactly one follow-up commit:

c709a8605
fix(slack): scope history reads to active conversation

The hardening covers trusted direct-Slack turn provenance, current-channel/workspace enforcement before SDK calls, relay/restored/synthetic/background/goal/queued-turn fail-closed boundaries, static prompt-cache safety, bounded untrusted output, MCP alias compatibility, and lifecycle/multiplex cases. It adds no write action.

Thread lookup is not permalink-only: the tool supports the active thread, an explicit thread_ts in the active conversation, or an optional permalink. A permalink is only a locator and never widens channel authorization.

Validation: 837 focused/integration tests pass, plus 45/45 Slack tool tests after the locator-description refinement; the full suite reports 39,283 passed and the same 45 local-environment failures in 15 unchanged files; Ruff, byte-compilation, Windows-footgun, diff checks, and both docs builds pass. Security, architecture, product, test, maintainability, and plan-compliance review rounds are all LGTM.

If this direction looks good, the single commit can be cherry-picked onto your branch.

@sk-holmes
sk-holmes force-pushed the fix/slack-channel-history-tool branch from 14202a6 to f704aa7 Compare July 13, 2026 11:10
@sk-holmes

Copy link
Copy Markdown
Contributor Author

Hi @sk-holmes — I reviewed and hardened this branch with the original commit/authorship preserved.

GitHub would not allow me to open a PR directly against your fork without collaborator access, so I created a ready-for-review diff here: NaMinhyeok#1

It is based on the exact current head of this PR (14202a6df2f52433efed64efed71c8f63324c8b7) and contains exactly one follow-up commit:

c709a8605
fix(slack): scope history reads to active conversation

The hardening covers trusted direct-Slack turn provenance, current-channel/workspace enforcement before SDK calls, relay/restored/synthetic/background/goal/queued-turn fail-closed boundaries, static prompt-cache safety, bounded untrusted output, MCP alias compatibility, and lifecycle/multiplex cases. It adds no write action.

Thread lookup is not permalink-only: the tool supports the active thread, an explicit thread_ts in the active conversation, or an optional permalink. A permalink is only a locator and never widens channel authorization.

Validation: 837 focused/integration tests pass, plus 45/45 Slack tool tests after the locator-description refinement; the full suite reports 39,283 passed and the same 45 local-environment failures in 15 unchanged files; Ruff, byte-compilation, Windows-footgun, diff checks, and both docs builds pass. Security, architecture, product, test, maintainability, and plan-compliance review rounds are all LGTM.

If this direction looks good, the single commit can be cherry-picked onto your branch.

Thanks 👍
I cherry picked it with some mods on top

@sk-holmes
sk-holmes force-pushed the fix/slack-channel-history-tool branch from f704aa7 to 7f1bec2 Compare July 15, 2026 03:13
@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@sk-holmes
sk-holmes force-pushed the fix/slack-channel-history-tool branch from 7f1bec2 to 75ef8a1 Compare July 16, 2026 00:25
@sk-holmes

Copy link
Copy Markdown
Contributor Author

resimplified this branch to stay on original target

# Conflicts:
#	gateway/run.py
#	gateway/session.py
#	gateway/session_context.py
@andrexibiza

Copy link
Copy Markdown
Contributor

Campaign interlock

Part of #79772. Fixes #6345 remains the implementation claim carried by this PR; collision adjudication retains all contributor credit.

@alt-glitch alt-glitch added comp/plugins Plugin system and bundled plugins and removed sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) labels Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have platform/slack Slack app adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Slack gateway should expose conversations.history as an agent tool

5 participants