Skip to content

feat: pluggable gateway platform interface — any messaging platform as a drop-in plugin#7942

Closed
teknium1 wants to merge 6 commits into
mainfrom
hermes/hermes-1f7bfa9e
Closed

feat: pluggable gateway platform interface — any messaging platform as a drop-in plugin#7942
teknium1 wants to merge 6 commits into
mainfrom
hermes/hermes-1f7bfa9e

Conversation

@teknium1

@teknium1 teknium1 commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a complete plugin interface for gateway platform adapters. Anyone can now add a new messaging platform (IRC, Viber, Line, custom bridges, etc.) by dropping a plugin directory into ~/.hermes/plugins/ — zero core code changes needed. The plugin uses the same PluginContext as existing plugins, so a single plugin can register a platform adapter AND custom tools, hooks, and CLI commands.

Includes a full IRC adapter as the reference implementation to prove the interface end-to-end.

If all 19 built-in platforms were migrated to plugins, ~48,000 lines would leave the core codebase (~24,500 adapter code + ~21,300 tests + ~2,600 hardcoded glue).

Architecture

Plugin calls ctx.register_platform()
         ↓
PlatformRegistry (gateway/platform_registry.py)
         ↓
Gateway startup checks registry BEFORE the built-in if/elif chain
         ↓
All downstream systems (auth, delivery, toolsets, hints, status)
fall back to the registry for unknown platform names

A plugin platform is just a regular plugin that also calls ctx.register_platform():

def register(ctx):
    # Platform adapter
    ctx.register_platform(name="irc", label="IRC", ...)
    # Optional: platform-specific tools
    ctx.register_tool(name="irc_whois", toolset="irc", ...)
    # Optional: lifecycle hooks
    ctx.register_hook("on_session_start", my_hook)

What Changed (25 files, +2080 / -105)

Core Infrastructure

File Change
gateway/platform_registry.py NEW — PlatformEntry dataclass + PlatformRegistry singleton
gateway/config.py Platform.missing() for dynamic enum members; get_connected_platforms() registry fallback
gateway/run.py _create_adapter() checks registry first; _is_user_authorized() registry fallback; _UPDATE_ALLOWED_PLATFORMS registry check; orphaned config warning; _gateway_runner_ref weakref
hermes_cli/plugins.py PluginContext.register_platform()
toolsets.py Auto-generates hermes-<name> toolsets for plugin platforms with _HERMES_CORE_TOOLS + plugin tools
hermes_cli/tools_config.py _get_platform_tools() handles unknown platforms via derived toolset name

Integration Points (18 total — full parity with built-ins)

Integration point How it works
Gateway adapter creation Registry checked before built-in if/elif chain
Config parsing Platform.missing() accepts any platform name
Connected platform validation Registry validate_config() called
User authorization PlatformEntry.allowed_users_env / allow_all_env
Cron delivery Platform() resolves any registered name
send_message tool _send_via_adapter() routes through live gateway adapter
Webhook cross-platform delivery Registry checked for known platforms
/update command access PlatformEntry.allow_update_command flag
Channel directory Plugin platforms included in enumeration
System prompt hints PlatformEntry.platform_hint injected into LLM context
Message chunking PlatformEntry.max_message_length for smart splitting
PII redaction PlatformEntry.pii_safe flag
Toolsets Auto-generated hermes-<name> with core tools + plugin tools
hermes status Shows plugin platforms with (plugin) tag
hermes gateway setup Plugin platforms appear in setup menu
hermes tools / hermes skills Plugin platforms in per-platform config via get_all_platforms()
Token lock (multi-profile) acquire_scoped_lock() pattern documented + implemented in IRC
Orphaned config warning Descriptive log when plugin is enabled in config but missing

IRC Reference Plugin (plugins/platforms/irc/)

  • Full async IRC adapter — stdlib asyncio, zero external dependencies
  • TLS, PING/PONG, nick collision recovery, NickServ auth
  • Channel addressing (nick: msg), DM dispatch, CTCP ACTION
  • Markdown stripping, 512-byte message splitting
  • Scoped token lock, platform hint, auth env vars
  • Config via config.yaml extra dict or IRC_* env vars

Documentation

  • website/docs/developer-guide/adding-platform-adapters.md — full "Plugin Path (Recommended)" section with code template, config examples, and 18-row integration table
  • gateway/platforms/ADDING_A_PLATFORM.md — plugin path section with pointer to reference implementation

Tests

  • 71 new tests across platform registry, IRC adapter, config integration, toolset resolution
  • Updated 5 existing tests for new Platform()-based resolution (source-inspection tests that checked hardcoded dicts)
  • E2E validation: 36-point integration test covering the full plugin lifecycle
  • Zero new failures (24 pre-existing in worktree environment)

How to Use (Plugin Author)

mkdir -p ~/.hermes/plugins/my-platform

Create PLUGIN.yaml + adapter.py (see plugins/platforms/irc/ or the developer guide).

Configure in config.yaml:

gateway:
  platforms:
    my_platform:
      enabled: true
      extra:
        token: "..."

That's it. The gateway discovers the plugin, creates the adapter, and routes messages through it.

Design Decisions

  • Registry-first, fallthrough to builtins: Plugin platforms are checked before the if/elif chain. Built-in adapters are completely untouched.
  • Dynamic Platform enum: _missing_() creates cached pseudo-members so Platform("irc") is Platform("irc") holds true. No enum modification needed.
  • Same PluginContext: A platform plugin can also register tools, hooks, and CLI commands — it's a regular plugin that happens to call register_platform().
  • Auto-generated toolsets: resolve_toolset("hermes-irc") returns _HERMES_CORE_TOOLS + any tools registered with toolset="irc". No toolsets.py entry needed.
  • No migration: All 19 built-in adapters stay as-is. This is purely additive.

Adds a platform adapter plugin interface so anyone can create new gateway
platforms (IRC, Viber, Line, etc.) as drop-in plugins without modifying
core gateway code.

## Platform Registry (gateway/platform_registry.py)
- PlatformEntry dataclass: name, label, adapter_factory, check_fn,
  validate_config, required_env, install_hint, source
- PlatformRegistry singleton with register/unregister/create_adapter
- _create_adapter() in gateway/run.py checks registry first, falls
  through to existing if/elif chain for built-in platforms

## Dynamic Platform Enum (gateway/config.py)
- Platform._missing_() accepts unknown string values, creating cached
  pseudo-members so Platform('irc') is Platform('irc') holds true
- GatewayConfig.from_dict() now parses plugin platform names from
  config.yaml without rejecting them
- get_connected_platforms() delegates to registry for unknown platforms

## Plugin Registration (hermes_cli/plugins.py)
- PluginContext.register_platform() for plugin authors
- Mirrors the existing register_tool() / register_hook() pattern

## IRC Reference Plugin (plugins/platforms/irc/)
- Full async IRC adapter using stdlib asyncio (zero external deps)
- Connects via TLS, handles PING/PONG, nick collision, NickServ auth
- Channel messages require addressing (nick: msg), DMs always dispatch
- Markdown stripping for IRC-clean output, message splitting for
  512-byte line limit
- Config via config.yaml extra dict or IRC_* env vars

## Tests (55 new tests)
- Platform enum dynamic members (identity stability, case normalization)
- PlatformRegistry (register, unregister, create, validation, factory)
- GatewayConfig integration (from_dict parsing, get_connected_platforms)
- IRC adapter (init, send, protocol parsing, markdown, requirements)

No existing platform adapters were migrated — the if/elif chain is
untouched. This is Phase 1: prove the interface with a real plugin.
Extends the platform plugin interface from Phase 1 to cover every
touchpoint where built-in platforms have hardcoded behavior.

## PlatformEntry extended fields
- allowed_users_env / allow_all_env: per-platform auth env vars
- max_message_length: smart-chunking for send_message tool
- pii_safe: session PII redaction flag
- emoji: CLI/gateway display
- allow_update_command: /update access control

## Functional fixes (Tier 1)

send_message tool (tools/send_message_tool.py):
- Replaced hardcoded platform_map dict with Platform() call
- Added _send_via_adapter() for plugin platforms — routes through
  live gateway adapter when available
- Registry-aware max message length for smart chunking

Cron delivery (cron/scheduler.py):
- Replaced hardcoded 15-entry platform_map with Platform() call
- Plugin platforms now work as cron delivery targets

User authorization (gateway/run.py _is_user_authorized):
- Registry fallback: checks PlatformEntry.allowed_users_env and
  allow_all_env when platform not in hardcoded maps
- Plugin platforms get per-platform auth support

## Integration fixes (Tier 2)

_UPDATE_ALLOWED_PLATFORMS: checks registry allow_update_command flag
Channel directory: includes plugin platforms in session enumeration
Orphaned config warning: descriptive message when plugin platform is
  in config but no plugin registered it
Gateway weakref: _gateway_runner_ref for cross-module adapter access

## UX completeness

hermes status: shows plugin platforms with (plugin) tag
hermes gateway setup: plugin platforms appear in menu with setup hints
hermes_cli/platforms.py: get_all_platforms() merges with registry,
  platform_label() falls back to registry for plugin names

## Tests
- 8 new tests (extended fields, cron resolution, platforms merge)
- Updated 3 tests for new Platform() based resolution
- 2829 passed, 24 pre-existing failures, zero new failures
PII redaction: build_session_context_prompt() now checks the plugin
registry's pii_safe flag in addition to the hardcoded _PII_SAFE_PLATFORMS
frozenset. Plugin platforms that set pii_safe=True (e.g. phone-based
messaging bridges) get their user IDs redacted before LLM context.

Token empty warnings: the empty-token diagnostic at config load now
checks the plugin registry's required_env when a platform isn't in the
hardcoded _token_env_names dict. Catches 'enabled but empty' for
plugin platforms too.
# Conflicts:
#	cron/scheduler.py
#	tools/send_message_tool.py
…, docs

Closes remaining functional gaps and adds documentation.

## Functional fixes

webhook.py: Cross-platform delivery now checks the plugin registry
  for unknown platform names instead of hardcoding 15 names in a tuple.
  Plugin platforms can receive webhook-routed deliveries.

prompt_builder: Platform hints (system prompt LLM guidance) now fall
  back to the plugin registry's platform_hint field. Plugin platforms
  can tell the LLM 'you're on IRC, no markdown.'

PlatformEntry: Added platform_hint field for LLM guidance injection.

IRC adapter: Added acquire_scoped_lock/release_scoped_lock in
  connect/disconnect to prevent two profiles from using the same IRC
  identity. Added platform_hint for IRC-specific LLM guidance.

Removed dead token-empty-warning extension for plugin platforms
  (plugin adapters handle their own env vars via check_fn).

## Documentation

website/docs/developer-guide/adding-platform-adapters.md:
  - Added 'Plugin Path (Recommended)' section with full code examples,
    PLUGIN.yaml template, config.yaml examples, and a table showing all
    18 integration points the plugin system handles automatically
  - Renamed built-in checklist to clarify it's for core contributors

gateway/platforms/ADDING_A_PLATFORM.md:
  - Added Plugin Path section pointing to the reference implementation
    and full docs guide
  - Clarified built-in path is for core contributors only
Plugin platforms now get full toolset support without any entries in
toolsets.py.

tools_config._get_platform_tools(): Falls back to 'hermes-<name>'
  when the platform isn't in the static PLATFORMS dict. No more
  KeyError for plugin platforms.

toolsets.resolve_toolset(): Auto-generates a toolset for plugin
  platforms (hermes-<name>) containing _HERMES_CORE_TOOLS plus any
  tools the plugin registered into a matching toolset name. This means
  a plugin can call ctx.register_tool(toolset='irc', ...) and those
  tools will be included in the hermes-irc toolset automatically.

webhook.py: Registry-aware cross-platform delivery.
run_agent.py: Platform hints from plugin registry.
IRC adapter: Token lock + platform hint.
Removed dead token-empty-warning extension.
Updated docs.
@teknium1 teknium1 changed the title feat: pluggable platform adapter registry + IRC reference implementation feat: pluggable gateway platform interface — any messaging platform as a drop-in plugin Apr 12, 2026
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins labels Apr 28, 2026
@ethernet8023

Copy link
Copy Markdown
Collaborator

superseded by #17664

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