Skip to content

feat(plugins): expose register_slack_action_handler API (salvage #20589) - #44664

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/slack-plugin-action-handlers
Jun 12, 2026
Merged

feat(plugins): expose register_slack_action_handler API (salvage #20589)#44664
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/slack-plugin-action-handlers

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

What

Salvages #20589 (@bcsmith528) onto current main, preserving authorship. Adds a public plugin API for Slack Block Kit action handlers so plugins can respond to button clicks / overflow menus / datepickers without monkey-patching SlackAdapter.connect.

def register(ctx):
    async def on_approve(ack, body, action):
        await ack()          # within 3s (slack_bolt requirement)
        # ...do work, post a follow-up...
    ctx.register_slack_action_handler("inbox_sweep_approve", on_approve)

action_id accepts whatever slack_bolt.App.action() accepts — a literal string, a compiled re.Pattern, or a constraint dict ({"action_id": "...", "block_id": "..."}).

Why this is a salvage

#20589's base had gone stale and the PR was DIRTY (conflicts with main's Socket-Mode startup refactor). This branch cherry-picks both of @bcsmith528's commits onto current main and resolves the conflicts:

  • hermes_cli/plugins.py — additive (_aux_tasks from main + _slack_action_handlers from the PR coexist).
  • gateway/platforms/slack.py — the plugin-handler registration block now runs before main's self._start_socket_mode_handler() / watchdog path (main replaced the old inline AsyncSocketModeHandler(...) + create_task block since the PR was cut), so handlers are wired before Socket Mode starts dispatching.

Authorship is preserved: both commits are Author: Brad Smith, committer kshitijk4poor.

Why this implementation (vs the competing #20936)

#20936 proposed a broader surface (slash commands + actions + view/modal handlers) but its connect() wiring captures the loop variable as a default argument (async def handler(ack, body, action, _ext=_ext)). slack_bolt resolves listener args by name, not by Python defaults — it injects None for any unrecognized param name, so _ext is None at dispatch and None.handler raises AttributeError on the first real event. That PR's tests pass only because they bypass the connect() loops.

This PR (the second commit, fix(gateway): keep plugin action wrapper signature to (ack, body, action)) deliberately avoids that with a closure factory (_make_wrapper) — the wrapper signature stays exactly (ack, body, action), and each callback is defensively wrapped so a misbehaving plugin can't crash the gateway (it's still best-effort ack()'d).

Files changed

  • gateway/platforms/slack.pyconnect() wires plugin action handlers after built-in approval/confirm buttons, before Socket Mode start.
  • hermes_cli/plugins.pyregister_slack_action_handler on PluginContext, get_slack_action_handlers() accessor, queue cleared on force-rediscover.
  • tests/gateway/test_slack_plugin_action_handlers.py — 14 tests: validation, multi-plugin registration, connect-time wiring, defensive exception handling, loader-failure fallback.
  • website/docs/guides/build-a-hermes-plugin.md — "Handle Slack Block Kit button clicks" section.

Testing

pytest tests/gateway/test_slack_plugin_action_handlers.py
# 14 passed

pytest tests/hermes_cli/test_plugins.py tests/gateway/test_slack.py tests/gateway/test_slack_approval_buttons.py
# 303 passed (1 pre-existing env-dependent failure in
# TestPluginDiscovery::test_discover_project_plugins_skipped_by_default that
# fails identically on clean main — it discovers the local dev plugin dir)

Verified the wrapper dispatches correctly against the real slack_bolt.kwargs_injection.utils.build_required_kwargs + a live BoltRequest (the closure-factory captures survive; default-arg capture would have resolved to None).

Closes #20589. Supersedes #20936 (broken wiring, see above).

bcsmith528 and others added 2 commits June 12, 2026 10:36
Plugins that post Block Kit messages with interactive elements (buttons,
overflow menus, datepickers, etc.) had no documented way to receive the
resulting click events. The plugin API exposed register_tool, register_hook,
register_command, register_platform, and register_context_engine, but
nothing for slack_bolt action handlers. The only workaround was to
monkey-patch SlackAdapter.connect from inside register(), which is
fragile and breaks on every Hermes update.

This change adds:

* PluginContext.register_slack_action_handler(action_id, callback) —
  validates inputs and queues the handler on the PluginManager.
  action_id accepts whatever slack_bolt.App.action() accepts (literal
  string, compiled re.Pattern, or constraint dict).
* PluginManager.get_slack_action_handlers() — accessor used by the
  Slack adapter at connect time.
* SlackAdapter.connect — after wiring its built-in approval and
  slash-confirm buttons, iterates the plugin-registered handlers
  and registers each via self._app.action(matcher)(callback). Each
  callback is wrapped defensively so a misbehaving plugin cannot
  crash slack_bolt's dispatch loop, with a best-effort ack on
  exception so Slack stops retrying the click.
* Defensive fallback when the plugin layer is unhealthy: a
  RuntimeError from get_plugin_manager() is logged and swallowed
  rather than blocking the gateway from starting.
* Test coverage in tests/gateway/test_slack_plugin_action_handlers.py
  for input validation, multi-plugin registration, the connect-time
  wiring, defensive exception handling, and the plugin-loader-
  failure fallback path.
* Documentation in website/docs/guides/build-a-hermes-plugin.md
  describing the new API alongside the existing register_command /
  dispatch_tool documentation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion)

The previous implementation captured loop vars via default arguments::

    async def _wrapped(ack, body, action, _cb=_cb, _plugin_name=_plugin_name):

slack_bolt's ``kwargs_injection`` introspects each listener's signature
via ``inspect.signature`` and passes ``None`` for any parameter name it
doesn't recognise (see ``slack_bolt/kwargs_injection/async_utils.py``
``build_async_required_kwargs``). That clobbered ``_cb`` to ``None`` at
dispatch time, so the wrapped plugin handler became ``NoneType`` —
``await _cb(...)`` then raised ``'NoneType' object is not callable`` and
no plugin action handler ever fired.

Replace the default-arg trick with a small closure factory so the
wrapper's public signature is exactly ``(ack, body, action)``. Add a
regression test that introspects the wrapped function's signature.

Found via real Slack click on a Block Kit button registered through
``ctx.register_slack_action_handler`` — gateway log showed
``[Slack] Plugin 'None' action handler raised: 'NoneType' object is
not callable`` despite the registration log line confirming the
handler was wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kshitijk4poor
kshitijk4poor merged commit c574170 into NousResearch:main Jun 12, 2026
28 checks passed
AIalliAI pushed a commit to AIalliAI/Hermes that referenced this pull request Jun 14, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
@kshitijk4poor
kshitijk4poor deleted the salvage/slack-plugin-action-handlers branch August 5, 2026 07:09
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…k-plugin-action-handlers

feat(plugins): expose register_slack_action_handler API (salvage NousResearch#20589)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants