Skip to content

refactor(gateway): platform registry — self-registering adapters to eliminate per-platform boilerplate #3823

@teknium1

Description

@teknium1

Problem

Adding a new messaging platform adapter requires touching 17+ files with manual boilerplate. Every platform (Telegram, Discord, Slack, WhatsApp, Signal, Mattermost, Matrix, DingTalk, Feishu, etc.) repeats the same pattern — enum values, factory branches, allowlist maps, toolset definitions, setup wizard entries, cron maps, send_message dispatch branches, and label strings scattered across the codebase.

This was highlighted during the Feishu salvage (PR #3817): the adapter itself is a single file, but wiring it in required surgical edits to 16 other files. Miss one spot and something silently breaks. The tool system already solved this exact problem with tools/registry.py — platform adapters should follow the same pattern.

Current State (per platform, 17 touchpoints)

File What's added Why it exists
gateway/config.py Platform.FEISHU = "feishu" enum Platform identity
gateway/config.py _apply_env_overrides() block Env → config bridging
gateway/config.py get_connected_platforms() check Connection validation
gateway/run.py _create_adapter() factory branch Adapter instantiation
gateway/run.py Allowlist env var map Auth gating
gateway/run.py Allow-all env var map Auth gating
toolsets.py hermes-feishu toolset definition Tool schema selection
toolsets.py hermes-gateway includes list Gateway union toolset
tools/send_message_tool.py Platform map entry Cross-platform send
tools/send_message_tool.py Target ref parser ID format detection
tools/send_message_tool.py Dispatch branch + send function Standalone send
hermes_cli/config.py SENSITIVE_VARS entries Credential masking
hermes_cli/gateway.py _PLATFORMS setup wizard entry Interactive setup
hermes_cli/tools_config.py Platform label/toolset map hermes tools UI
hermes_cli/skills_config.py Platform label map hermes skills UI
cron/scheduler.py Platform map entry Cron delivery routing
tools/cronjob_tools.py Description string Schema docs

Plus 3+ test files and 3+ doc files.

Proposed: Platform Registry

A gateway/platforms/registry.py that mirrors the tool registry pattern. Each adapter self-registers with all its metadata at import time.

Adapter Registration

# gateway/platforms/feishu.py
from gateway.platforms.registry import platform_registry

platform_registry.register(
    name="feishu",
    label="Feishu / Lark",
    emoji="🪽",
    adapter_class=FeishuAdapter,
    check_fn=check_feishu_requirements,
    # Auth
    env_vars={
        "FEISHU_APP_ID": {"prompt": "App ID", "password": False, "required": True},
        "FEISHU_APP_SECRET": {"prompt": "App Secret", "password": True, "required": True},
        "FEISHU_DOMAIN": {"prompt": "Domain (feishu or lark)", "password": False},
        "FEISHU_CONNECTION_MODE": {"prompt": "Connection mode", "password": False},
    },
    sensitive_vars=["FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_ENCRYPT_KEY"],
    allowlist_var="FEISHU_ALLOWED_USERS",
    allow_all_var="FEISHU_ALLOW_ALL_USERS",
    # Toolset
    tools=_HERMES_CORE_TOOLS,  # or could just be "core" to mean the standard set
    # Setup
    setup_instructions=[
        "1. Go to https://open.feishu.cn/ → Create app",
        "2. Copy App ID and App Secret",
        ...
    ],
    # Connection check (for gateway config)
    is_connected=lambda config: bool(config.extra.get("app_id")),
    # Env → config bridge
    env_to_config=_apply_feishu_env,  # function that reads env vars into PlatformConfig
    # Send message support (for cross-platform send_message tool)
    send_fn=_send_feishu,  # async function for standalone sending
    target_re=re.compile(r"..."),  # regex for parsing target IDs
    max_message_length=8000,
)

Consumer Changes

Each file that currently hardcodes platform names would read from the registry:

# gateway/run.py — adapter factory
from gateway.platforms.registry import platform_registry

def _create_adapter(platform, config):
    entry = platform_registry.get(platform.value)
    if not entry:
        return None
    if entry.check_fn and not entry.check_fn():
        logger.warning("%s: requirements not met", platform.value)
        return None
    return entry.adapter_class(config)
# toolsets.py — auto-generate platform toolsets
for name, entry in platform_registry.items():
    TOOLSETS[f"hermes-{name}"] = {
        "description": f"{entry.label} bot toolset",
        "tools": entry.tools,
        "includes": [],
    }
TOOLSETS["hermes-gateway"]["includes"] = [f"hermes-{n}" for n in platform_registry]
# cron/scheduler.py, send_message_tool.py — platform maps
PLATFORM_MAP = {name: Platform(name) for name in platform_registry}

Discovery

Two options (not mutually exclusive):

Option A: Import-time registration (like tools/registry.py)
A _discover_platforms() function in gateway/run.py imports each adapter module, triggering platform_registry.register() calls at module level. Simple, proven pattern.

Option B: Entry-point plugins (like hermes_cli/plugins.py)
Third-party platform adapters register via pyproject.toml entry points:

[project.entry-points."hermes.platforms"]
feishu = "gateway.platforms.feishu"

This enables community-contributed platforms as pip-installable packages without forking the repo.

Recommend starting with Option A for built-in platforms, with Option B as a future extension for community platforms.

Platform Enum

The Platform enum in gateway/config.py can be auto-generated from the registry, or kept as a static enum with validation that registered platforms have matching enum values. The enum is used as dict keys throughout the codebase so removing it entirely may not be worth the churn — but new platforms wouldn't need to manually add enum values if the registry handles it.

Migration Plan

  1. Create gateway/platforms/registry.py with PlatformEntry dataclass and PlatformRegistry class
  2. Add register() calls to each existing adapter (Telegram, Discord, Slack, etc.) — purely additive, no behavior change
  3. Migrate consumers one at a time — each consumer (gateway/run.py factory, toolsets.py, send_message_tool.py, etc.) switches from hardcoded maps to registry lookups
  4. Remove the hardcoded entries once all consumers use the registry
  5. Verify with full test suite at each step

Each step is independently mergeable. The total diff should be net-negative lines (removing ~200 lines of boilerplate across consumers, adding ~50 lines of registry infrastructure).

Success Criteria

Adding a new platform adapter requires:

  • 1 new file: gateway/platforms/new_platform.py (with register() call)
  • 1 test file: tests/gateway/test_new_platform.py
  • 1 doc file: website/docs/user-guide/messaging/new_platform.md
  • 1 line in pyproject.toml (optional dep)
  • 0 changes to gateway/run.py, toolsets.py, config.py, send_message_tool.py, etc.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions