Skip to content

feat(plugins): add pre_gateway_dispatch hook for gateway-level message interception - #13445

Closed
keiravoss94 wants to merge 2 commits into
NousResearch:mainfrom
pebble-tech:feature/pre-gateway-dispatch
Closed

feat(plugins): add pre_gateway_dispatch hook for gateway-level message interception#13445
keiravoss94 wants to merge 2 commits into
NousResearch:mainfrom
pebble-tech:feature/pre-gateway-dispatch

Conversation

@keiravoss94

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a new plugin hook pre_gateway_dispatch, fired once per incoming MessageEvent in GatewayRunner._handle_message — after the internal-event guard but before the auth / pairing chain. Plugins may return a dict to influence flow:

{"action": "skip",    "reason": "..."}  # drop (no reply; plugin handled)
{"action": "rewrite", "text":   "..."}  # replace event.text, continue normally
{"action": "allow"}   /  None           # normal dispatch

Motivation. Several commercially relevant message-flow patterns don't fit cleanly into any single platform adapter:

  • Listen-only group chats — ambient-record a group conversation but only reply when the bot is @mentioned, and open a short follow-up window where replies continue without re-tagging.
  • Human handover — when a customer hits a condition the owner wants to handle personally (design discussions, refunds, complaints), silently ingest subsequent customer messages into the transcript without the bot replying, until the owner issues a takeback command.
  • Profile-specific auth / rate-limit / routing policies that today require forking core.

Today these patterns require modifying _handle_message directly. With this hook they can live entirely in plugins (e.g. a gateway-policy plugin we maintain out-of-tree at https://github.com/pebble-tech/hermes-plugin-gateway-policy), making them profile-agnostic and installable via hermes plugins install.

Design choices.

  • Action taxonomy is deliberately minimal (skip | rewrite | allow). Any "side effect" (silent-ingest, handover state, owner notification, etc.) is the plugin's responsibility and results in skip for the core.
  • Hook runs before auth so plugins can handle unauthorized senders (e.g. customer DMs during an active handover) without triggering the pairing-code flow.
  • Exceptions from plugin callbacks are caught and logged; the gateway always falls through to the normal auth chain on error. First non-None action dict wins; remaining results are ignored.
  • Internal events (event.internal == True) bypass the hook entirely — they're system-generated (background-process completions etc.) and must not be gate-kept by user-facing policy.

Related Issue

No existing issue; happy to file one if preferred.

Fixes #

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • hermes_cli/plugins.py — add "pre_gateway_dispatch" to VALID_HOOKS with inline documentation of the action contract.
  • gateway/run.py — add import dataclasses; invoke the hook in _handle_message after the is_internal check and before the source.user_id is None auth chain; handle skip / rewrite / allow actions and log a structured message on skip.
  • tests/gateway/test_pre_gateway_dispatch.pynew file, 5 tests covering skip, rewrite, allow, exception safety, and internal-event bypass.
  • tests/hermes_cli/test_plugins.py — 2 new tests confirming the hook is registered and that action dicts are collected across multiple plugin callbacks.
  • website/docs/user-guide/features/plugins.md — new row in the "Available hooks" table.

Total: ~260 additions, 1 deletion across 5 files. No existing behavior changes when no plugin registers the hook.

How to Test

  1. pip install -e .[dev] (or your usual dev setup).

  2. pytest tests/gateway/test_pre_gateway_dispatch.py tests/hermes_cli/test_plugins.py -q — 59 tests pass locally.

  3. Register a quick plugin to see it in action:

    def register(ctx):
        def _hook(event, gateway, session_store):
            if "ignore me" in (event.text or ""):
                return {"action": "skip", "reason": "demo"}
            return None
        ctx.register_hook("pre_gateway_dispatch", _hook)

    Send a message containing ignore me via any connected platform — the gateway logs the skip and returns without replying.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs — no duplicate hook proposal found
  • My PR contains only changes related to this feature
  • I've run pytest tests/gateway/test_pre_gateway_dispatch.py tests/hermes_cli/test_plugins.py -q and all tests pass (59 passed)
  • I've added tests for my changes
  • I've tested on my platform: macOS 15 (Darwin 24.6.0), Python 3.11

Documentation & Housekeeping

  • I've updated relevant documentation (website/docs/user-guide/features/plugins.md)
  • No new config keys (hook is purely an extension point) — N/A for cli-config.yaml.example
  • No architectural docs needed beyond the plugins doc table update — N/A for CONTRIBUTING.md / AGENTS.md
  • Hook is Python-only and lives in platform-agnostic gateway code — N/A for cross-platform concerns
  • No tool changes — N/A for tool descriptions/schemas

Screenshots / Logs

Example log line when a plugin returns skip:

INFO gateway.run: pre_gateway_dispatch skip: reason=listen-only-buffer platform=whatsapp chat=1234567890@g.us

Made with Cursor

@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 labels Apr 22, 2026
@keiravoss94
keiravoss94 force-pushed the feature/pre-gateway-dispatch branch from 62a8d84 to 81f98d5 Compare April 22, 2026 13:57
@github-actions
github-actions Bot force-pushed the feature/pre-gateway-dispatch branch from 81f98d5 to 539b5e4 Compare April 23, 2026 05:35
@keiravoss94
keiravoss94 force-pushed the feature/pre-gateway-dispatch branch from 539b5e4 to aff2d25 Compare April 24, 2026 03:58
Introduces a new plugin hook `pre_gateway_dispatch` fired once per
incoming MessageEvent in `_handle_message`, after the internal-event
guard but before the auth / pairing chain. Plugins may return a dict
to influence flow:

    {"action": "skip",    "reason": "..."}  -> drop (no reply)
    {"action": "rewrite", "text":   "..."}  -> replace event.text
    {"action": "allow"}  /  None             -> normal dispatch

Motivation: gateway-level message-flow patterns that don't fit cleanly
into any single adapter — e.g. listen-only group-chat windows (buffer
ambient messages, collapse on @mention), or human-handover silent
ingest (record messages while an owner handles the chat manually).
Today these require forking core; with this hook they can live in a
single profile-agnostic plugin.

Hook runs BEFORE auth so plugins can handle unauthorized senders
(e.g. customer-service handover ingest) without triggering the
pairing-code flow. Exceptions in plugin callbacks are caught and
logged; the first non-None action dict wins, remaining results are
ignored.

Includes:
- `VALID_HOOKS` entry + inline doc in `hermes_cli/plugins.py`
- Invocation block in `gateway/run.py::_handle_message`
- 5 new tests in `tests/gateway/test_pre_gateway_dispatch.py`
  (skip, rewrite, allow, exception safety, internal-event bypass)
- 2 additional tests in `tests/hermes_cli/test_plugins.py`
- Table entry in `website/docs/user-guide/features/plugins.md`

Made-with: Cursor
… section

Follow-up to aeff6dfe:

- Fix semantic error in VALID_HOOKS inline comment ("after core auth" ->
  "before auth"). Hook intentionally runs BEFORE auth so plugins can
  handle unauthorized senders without triggering the pairing flow.
- Fix wrong class name in the same comment (HermesGateway ->
  GatewayRunner, matching gateway/run.py).
- Add a full ### pre_gateway_dispatch section in
  website/docs/user-guide/features/hooks.md (matches the pattern of
  every other plugin hook: signature, params table, fires-where,
  return-value table, use cases, two worked examples) plus a row in
  the quick-reference table.
- Add the anchor link on the plugins.md table row so it matches the
  other hook entries.

No code behavior change.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #15050 — your commits were cherry-picked onto current main with authorship preserved in git log (f33415a, 2f0ccb8). Thanks @keiravoss94! Full PR: #15050

@teknium1 teknium1 closed this Apr 24, 2026
@keiravoss94
keiravoss94 deleted the feature/pre-gateway-dispatch branch April 26, 2026 09:25
github-actions Bot pushed a commit to pebble-tech/hermes-agent that referenced this pull request Apr 27, 2026
This is the sole commit on ops-overlay. It carries:

  - .github/workflows/sync-upstream.yml: daily rebase + rebuild of main
    on top of upstream/main + ops-overlay + each feature branch.
  - FORK.md: branch layout, sync model, recovery procedure.

Steady state (2026-04-26 onward): both feature branches we used to
carry have merged upstream (PRs NousResearch#13445 and NousResearch#14904 via NousResearch#15050 and
NousResearch#15191). FEATURE_BRANCHES is now empty; main collapses to
upstream/main + ops-overlay. The fork stays alive so customer VPSes
have a stable deploy target rebuilt on our schedule.
github-actions Bot pushed a commit to pebble-tech/hermes-agent that referenced this pull request Apr 30, 2026
This is the sole commit on ops-overlay. It carries:

  - .github/workflows/sync-upstream.yml: daily rebase + rebuild of main
    on top of upstream/main + ops-overlay + each feature branch.
  - FORK.md: branch layout, sync model, recovery procedure.

Steady state (2026-04-26 onward): both feature branches we used to
carry have merged upstream (PRs NousResearch#13445 and NousResearch#14904 via NousResearch#15050 and
NousResearch#15191). FEATURE_BRANCHES is now empty; main collapses to
upstream/main + ops-overlay. The fork stays alive so customer VPSes
have a stable deploy target rebuilt on our schedule.
github-actions Bot pushed a commit to pebble-tech/hermes-agent that referenced this pull request May 6, 2026
This is the sole commit on ops-overlay. It carries:

  - .github/workflows/sync-upstream.yml: daily rebase + rebuild of main
    on top of upstream/main + ops-overlay + each feature branch.
  - FORK.md: branch layout, sync model, recovery procedure.

Steady state (2026-04-26 onward): both feature branches we used to
carry have merged upstream (PRs NousResearch#13445 and NousResearch#14904 via NousResearch#15050 and
NousResearch#15191). FEATURE_BRANCHES is now empty; main collapses to
upstream/main + ops-overlay. The fork stays alive so customer VPSes
have a stable deploy target rebuilt on our schedule.
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 type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants