feat(plugins): expose register_slack_action_handler API (salvage #20589) - #44664
Merged
kshitijk4poor merged 3 commits intoJun 12, 2026
Merged
Conversation
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>
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)
1 task
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)
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-patchingSlackAdapter.connect.action_idaccepts whateverslack_bolt.App.action()accepts — a literal string, a compiledre.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 currentmainand resolves the conflicts:hermes_cli/plugins.py— additive (_aux_tasksfrom main +_slack_action_handlersfrom the PR coexist).gateway/platforms/slack.py— the plugin-handler registration block now runs before main'sself._start_socket_mode_handler()/ watchdog path (main replaced the old inlineAsyncSocketModeHandler(...)+create_taskblock since the PR was cut), so handlers are wired before Socket Mode starts dispatching.Authorship is preserved: both commits are
Author: Brad Smith, committerkshitijk4poor.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 injectsNonefor any unrecognized param name, so_extisNoneat dispatch andNone.handlerraisesAttributeErroron the first real event. That PR's tests pass only because they bypass theconnect()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-effortack()'d).Files changed
gateway/platforms/slack.py—connect()wires plugin action handlers after built-in approval/confirm buttons, before Socket Mode start.hermes_cli/plugins.py—register_slack_action_handleronPluginContext,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
Verified the wrapper dispatches correctly against the real
slack_bolt.kwargs_injection.utils.build_required_kwargs+ a liveBoltRequest(the closure-factory captures survive; default-arg capture would have resolved toNone).Closes #20589. Supersedes #20936 (broken wiring, see above).