Skip to content

feat(plugins): expose register_slack_action_handler API - #20589

Closed
bcsmith528 wants to merge 2 commits into
NousResearch:mainfrom
bcsmith528:feat/plugin-slack-action-handlers
Closed

bcsmith528 wants to merge 2 commits into
NousResearch:mainfrom
bcsmith528:feat/plugin-slack-action-handlers

Conversation

@bcsmith528

Copy link
Copy Markdown
Contributor

What

Adds a public way for Hermes plugins to register Slack Block Kit action handlers (button clicks, overflow menus, datepickers, etc.) without monkey-patching SlackAdapter.connect.

New API on PluginContext:

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 for matching multiple ids, or a constraint dict like {\"action_id\": \"...\", \"block_id\": \"...\"}.

Why

The plugin API already exposes register_tool, register_hook, register_command, register_platform, and register_context_engine, but nothing for slack_bolt action handlers. Plugins that post Block Kit messages with interactive elements had no documented hook for the resulting click events.

The only previous workaround was for the plugin to monkey-patch SlackAdapter.connect from inside register() — fragile, fights with the official API, and breaks on every Hermes update. Block Kit interactivity is core to the Slack experience, so plugins should have first-class access to it.

Before / after

Before — plugin had to reach into gateway.platforms.slack, wrap SlackAdapter.connect, and register handlers post-connect on self._app:

def register(ctx):
    import gateway.platforms.slack as slack_mod
    orig = slack_mod.SlackAdapter.connect
    async def patched(self, *a, **kw):
        result = await orig(self, *a, **kw)
        self._app.action(\"my_action\")(my_handler)
        return result
    slack_mod.SlackAdapter.connect = patched

After:

def register(ctx):
    ctx.register_slack_action_handler(\"my_action\", my_handler)

How it works

  1. PluginContext.register_slack_action_handler(action_id, callback) validates inputs and queues (action_id, callback, plugin_name) on PluginManager._slack_action_handlers.
  2. PluginManager.get_slack_action_handlers() exposes the queue to consumers.
  3. SlackAdapter.connect, after wiring its built-in approval and slash-confirm buttons, iterates the queue and registers each via self._app.action(matcher)(callback).
  4. Each callback is wrapped defensively — exceptions inside a plugin handler are logged with the plugin name and a best-effort ack() is still issued so Slack stops retrying the click.
  5. If get_plugin_manager() itself raises (e.g. plugin layer unhealthy), the gateway logs and proceeds with built-ins only rather than failing to start.

Files changed

  • hermes_cli/plugins.py — new register_slack_action_handler method on PluginContext, new get_slack_action_handlers() accessor on PluginManager, queue cleared on force-rediscover.
  • gateway/platforms/slack.pyconnect() consumes the queue after the existing built-in app.action() blocks.
  • tests/gateway/test_slack_plugin_action_handlers.py — new file, 13 tests covering input validation, multi-plugin registration, the connect-time wiring, defensive exception handling, and the plugin-loader-failure fallback path.
  • website/docs/guides/build-a-hermes-plugin.md — new "Handle Slack Block Kit button clicks" section alongside the existing slash-command + dispatch_tool docs.

How to test

# new tests
pytest tests/gateway/test_slack_plugin_action_handlers.py -v

# all plugin + Slack tests still pass
pytest tests/hermes_cli/test_plugins.py \\
       tests/hermes_cli/test_plugins_cmd.py \\
       tests/hermes_cli/test_plugin_cli_registration.py \\
       tests/hermes_cli/test_plugin_scanner_recursion.py \\
       tests/test_plugin_skills.py \\
       tests/gateway/test_slack.py \\
       tests/gateway/test_slack_approval_buttons.py \\
       tests/gateway/test_plugin_platform_interface.py \\
       tests/gateway/test_platform_registry.py -q
# 416 passed, 7 skipped

ruff check hermes_cli/plugins.py gateway/platforms/slack.py \\
           tests/gateway/test_slack_plugin_action_handlers.py
# All checks passed!

Manual smoke test — wrote a tiny test plugin that calls ctx.register_slack_action_handler(\"foo\", cb), posted a Block Kit message with that action_id, clicked the button, callback fired with the expected (ack, body, action) and the plugin name appeared in the gateway log under [Slack] Wired N plugin action handler(s).

Compatibility

  • No behavioural change for plugins that don't call the new method — get_slack_action_handlers() returns [] and the new for loop in connect() is a no-op.
  • No change to built-in approval/confirm handlers — the new block runs after them.
  • Forward-compatible with slack_bolt matcher types: literal string, compiled regex, and constraint dict are all accepted unchanged.

Tested on

  • macOS (Python 3.11.1)

Checklist

  • Branch named feat/...
  • Conventional Commits
  • Tests added (13 new, all passing)
  • Lint clean (ruff)
  • Adjacent test suites still pass (416 plugin + Slack tests, 0 failures)
  • Docs updated (build-a-hermes-plugin.md)

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>
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins comp/gateway Gateway runner, session dispatch, delivery platform/slack Slack app adapter labels May 6, 2026
…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 added a commit that referenced this pull request Jun 12, 2026
…ion-handlers

feat(plugins): expose register_slack_action_handler API (salvage #20589)
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)
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

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/slack Slack app adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants