hermes-agent/gateway/platforms/ADDING_A_PLATFORM.md
Teknium 50f9fee988
feat(gateway): add LINE Messaging API platform plugin (#23197)
* feat(gateway): add LINE Messaging API platform plugin

Adds LINE as a bundled platform plugin under `plugins/platforms/line/`,
synthesized from the strongest pieces of seven open community PRs. The
adapter requires zero core edits — `Platform("line")` is auto-discovered
via the bundled-plugin scan in `gateway/config.py`, and all hooks
(setup, env-enablement, cron delivery, standalone send) are wired
through `register_platform()` kwargs the way IRC and Teams do it.

Highlights merged into one plugin:

- **Reply token preferred, Push fallback.** Try the free reply token
  first (single-use, ~60s TTL); fall back to metered Push when the
  token is absent, expired, or rejected. (PR #21023)
- **Slow-LLM Template Buttons postback.** When the LLM is still running
  past `LINE_SLOW_RESPONSE_THRESHOLD` (default 45s), the adapter burns
  the original reply token to send a "Get answer" button bubble. The
  user taps it to fetch the cached answer via a fresh reply token —
  also free. State machine: PENDING → READY → DELIVERED, ERROR for
  cancelled runs (orphan resolves to `LINE_INTERRUPTED_TEXT` after
  /stop). Set threshold to 0 to disable. (PR #18153)
- **Three-allowlist gating** — separate user / group / room allowlists
  with `LINE_ALLOW_ALL_USERS=true` dev-only escape hatch. (PR #18153)
- **Markdown URL preservation.** Strip bold/italic/code-fence/heading
  markers (LINE renders them literally) but keep `[label](url)` →
  `label (url)` so URLs stay tappable. (PR #18153)
- **System-message bypass** for ` Interrupting`, ` Queued`, etc. —
  busy-acks reach the user as visible bubbles instead of being
  swallowed into the postback cache. (PR #18153)
- **Media via public HTTPS URLs.** LINE doesn't accept binary uploads;
  images/audio/video must be HTTPS-reachable. The adapter serves
  registered tempfiles under `/line/media/<token>/<filename>` from the
  same aiohttp app. Allowed-roots traversal guard covers
  `tempfile.gettempdir()`, `/tmp` (→ `/private/tmp` on macOS), and
  `HERMES_HOME`. `LINE_PUBLIC_URL` overrides URL construction for
  setups behind tunnels/proxies. (PR #8398)
- **5-message-per-call batching.** LINE rejects >5 messages per
  Reply/Push; smart-chunker caps text at 4500 chars per bubble.
- **Inbound dedup** via `webhookEventId` LRU. (PR #21023)
- **Self-message filter** via `/v2/bot/info` userId lookup. (PR #21023)
- **Loading-animation indicator** wired to LINE's `chat/loading/start`
  endpoint, DM-only (LINE rejects it for groups/rooms). (PR #21023)
- **Out-of-process cron delivery** via `_standalone_send`, so
  `deliver: line` cron jobs work even when cron runs detached from
  the gateway.
- **Webhook hardening** — 1 MiB body cap, constant-time HMAC-SHA256
  signature verification, dedup, scoped lock so two profiles can't
  bind the same channel.

Validation
----------

- `scripts/run_tests.sh tests/gateway/test_line_plugin.py` →
  73 passed in 1.05s
- `scripts/run_tests.sh tests/gateway/test_line_plugin.py
  tests/gateway/test_irc_adapter.py
  tests/gateway/test_plugin_platform_interface.py
  tests/gateway/test_platform_registry.py
  tests/gateway/test_config.py` → 193 passed, 7 skipped
- E2E import + register + signature roundtrip + `Platform("line")`
  bundled-plugin discovery verified against current `origin/main`.

Closes the seven open LINE PRs (#18153, #16832, #6676, #21023, #14942,
#14988, #8398) by superseding them with a single plugin-form
implementation that takes the best idea from each.

Co-authored-by: pwlee <32443648+leepoweii@users.noreply.github.com>
Co-authored-by: Jetha Chan <jetha@google.com>
Co-authored-by: Cattia <openclaw@liyangchen.me>
Co-authored-by: perng <charles@perng.com>
Co-authored-by: Soichiro Yoshimura <soichiro0111.dev@gmail.com>
Co-authored-by: David Zhou <77736378+David-0x221Eight@users.noreply.github.com>
Co-authored-by: Yu-ga <74749461+yuga-hashimoto@users.noreply.github.com>

* docs(platforms): document platform-specific slow-LLM UX pattern

Add a 'Platform-Specific Slow-LLM UX' section to the platform-adapter
developer guide covering the _keep_typing override pattern that LINE
uses for its Template Buttons postback flow.

Three subsections:
- Pattern: subclass _keep_typing to layer mid-flight UX (with code)
- Pattern: subclass send to route through a cache instead of sending
- When this pattern is appropriate (vs. always-Push fallback)

Plus a short pointer in gateway/platforms/ADDING_A_PLATFORM.md so
tree-readers find the prose walkthrough on the docsite.

Filed because the LINE plugin (PR #23197) was the first bundled
adapter to need this pattern — every prior plugin (irc, teams,
google_chat) handles slow responses with the default typing-loop and
a regular send_text. Documenting now while the rationale is fresh.

---------

Co-authored-by: pwlee <32443648+leepoweii@users.noreply.github.com>
Co-authored-by: Jetha Chan <jetha@google.com>
Co-authored-by: Cattia <openclaw@liyangchen.me>
Co-authored-by: perng <charles@perng.com>
Co-authored-by: Soichiro Yoshimura <soichiro0111.dev@gmail.com>
Co-authored-by: David Zhou <77736378+David-0x221Eight@users.noreply.github.com>
Co-authored-by: Yu-ga <74749461+yuga-hashimoto@users.noreply.github.com>
2026-05-10 06:40:46 -07:00

11 KiB

Adding a New Messaging Platform

There are two ways to add a platform to the Hermes gateway:

Create a plugin directory in ~/.hermes/plugins/ (or under plugins/platforms/ for bundled plugins) with a plugin.yaml and adapter.py. The adapter inherits from BasePlatformAdapter and registers via ctx.register_platform() in the register(ctx) entry point. This requires zero changes to core Hermes code.

The plugin system automatically handles: adapter creation, config parsing, user authorization, cron delivery, send_message routing, system prompt hints, status display, gateway setup, and more.

Optional hooks cover the edges most adapters need:

  • env_enablement_fn: () -> Optional[dict] — seeds PlatformConfig.extra (and an optional home_channel dict) from env vars BEFORE the adapter is constructed. Without this, env-only setups don't surface in hermes gateway status or get_connected_platforms() until the SDK instantiates.
  • cron_deliver_env_var: str — name of the *_HOME_CHANNEL env var. When set, deliver=<name> cron jobs route to this var without editing cron/scheduler.py's hardcoded sets.
  • standalone_sender_fn: async (...) -> dict: out-of-process delivery for cron jobs that run separately from the gateway. Without this, a deliver=<name> job fires correctly but the actual send returns No live adapter for platform '<name>'. Pair with cron_deliver_env_var for end-to-end cron support. See the docsite for the signature.
  • plugin.yaml requires_env / optional_env rich-dict entries — auto-populate OPTIONAL_ENV_VARS in hermes_cli/config.py so the setup wizard surfaces proper descriptions, prompts, password flags, and URLs.

Subclassing for platform-specific UX. When a platform has a hard time-window constraint that the base adapter can't anticipate (LINE's 60s single-use reply token, WhatsApp's 24h session window, etc.), an adapter can override _keep_typing to layer a mid-flight bubble at a threshold without expanding the kwarg surface. Always await super()._keep_typing(...) so the typing heartbeat keeps running, and tear down your side task in finally. See plugins/platforms/line/ for the full pattern (Template Buttons postback at 45s, RequestCache state machine, interrupt_session_activity override for /stop orphans) and the developer-guide page for the prose walkthrough.

See plugins/platforms/irc/, plugins/platforms/teams/, and plugins/platforms/google_chat/ for complete working examples, and website/docs/developer-guide/adding-platform-adapters.md for the full plugin guide with code examples and hook documentation.


Built-in Path (Core Contributors Only)

Checklist for integrating a platform directly into the Hermes core. Use this as a reference when building a built-in adapter — every item here is a real integration point. Missing any of them will cause broken functionality, missing features, or inconsistent behavior.


1. Core Adapter (gateway/platforms/<platform>.py)

The adapter is a subclass of BasePlatformAdapter from gateway/platforms/base.py.

Required methods

Method Purpose
__init__(self, config) Parse config, init state. Call super().__init__(config, Platform.YOUR_PLATFORM)
connect() -> bool Connect to the platform, start listeners. Return True on success
disconnect() Stop listeners, close connections, cancel tasks
send(chat_id, text, ...) -> SendResult Send a text message
send_typing(chat_id) Send typing indicator
send_image(chat_id, image_url, caption) -> SendResult Send an image
get_chat_info(chat_id) -> dict Return {name, type, chat_id} for a chat

Optional methods (have default stubs in base)

Method Purpose
send_document(chat_id, path, caption) Send a file attachment
send_voice(chat_id, path) Send a voice message
send_video(chat_id, path, caption) Send a video
send_animation(chat_id, path, caption) Send a GIF/animation
send_image_file(chat_id, path, caption) Send image from local file

Required function

def check_<platform>_requirements() -> bool:
    """Check if this platform's dependencies are available."""

Key patterns to follow

  • Use self.build_source(...) to construct SessionSource objects
  • Call self.handle_message(event) to dispatch inbound messages to the gateway
  • Use MessageEvent, MessageType, SendResult from base
  • Use cache_image_from_bytes, cache_audio_from_bytes, cache_document_from_bytes for attachments
  • Filter self-messages (prevent reply loops)
  • Filter sync/echo messages if the platform has them
  • Redact sensitive identifiers (phone numbers, tokens) in all log output
  • Implement reconnection with exponential backoff + jitter for streaming connections
  • Set MAX_MESSAGE_LENGTH if the platform has message size limits

2. Platform Enum (gateway/config.py)

Add the platform to the Platform enum:

class Platform(Enum):
    ...
    YOUR_PLATFORM = "your_platform"

Add env var loading in _apply_env_overrides():

# Your Platform
your_token = os.getenv("YOUR_PLATFORM_TOKEN")
if your_token:
    if Platform.YOUR_PLATFORM not in config.platforms:
        config.platforms[Platform.YOUR_PLATFORM] = PlatformConfig()
    config.platforms[Platform.YOUR_PLATFORM].enabled = True
    config.platforms[Platform.YOUR_PLATFORM].token = your_token

Update get_connected_platforms() if your platform doesn't use token/api_key (e.g., WhatsApp uses enabled flag, Signal uses extra dict).


3. Adapter Factory (gateway/run.py)

Add to _create_adapter():

elif platform == Platform.YOUR_PLATFORM:
    from gateway.platforms.your_platform import YourAdapter, check_your_requirements
    if not check_your_requirements():
        logger.warning("Your Platform: dependencies not met")
        return None
    return YourAdapter(config)

4. Authorization Maps (gateway/run.py)

Add to BOTH dicts in _is_user_authorized():

platform_env_map = {
    ...
    Platform.YOUR_PLATFORM: "YOUR_PLATFORM_ALLOWED_USERS",
}
platform_allow_all_map = {
    ...
    Platform.YOUR_PLATFORM: "YOUR_PLATFORM_ALLOW_ALL_USERS",
}

5. Session Source (gateway/session.py)

If your platform needs extra identity fields (e.g., Signal's UUID alongside phone number), add them to the SessionSource dataclass with Optional defaults, and update to_dict(), from_dict(), and build_source() in base.py.


6. System Prompt Hints (agent/prompt_builder.py)

Add a PLATFORM_HINTS entry so the agent knows what platform it's on:

PLATFORM_HINTS = {
    ...
    "your_platform": (
        "You are on Your Platform. "
        "Describe formatting capabilities, media support, etc."
    ),
}

Without this, the agent won't know it's on your platform and may use inappropriate formatting (e.g., markdown on platforms that don't render it).


7. Toolset (toolsets.py)

Add a named toolset for your platform:

"hermes-your-platform": {
    "description": "Your Platform bot toolset",
    "tools": _HERMES_CORE_TOOLS,
    "includes": []
},

And add it to the hermes-gateway composite:

"hermes-gateway": {
    "includes": [..., "hermes-your-platform"]
}

8. Cron Delivery (cron/scheduler.py)

Add to platform_map in _deliver_result():

platform_map = {
    ...
    "your_platform": Platform.YOUR_PLATFORM,
}

Without this, cronjob(action="create", deliver="your_platform", ...) silently fails.


9. Send Message Tool (tools/send_message_tool.py)

Add to platform_map in send_message_tool():

platform_map = {
    ...
    "your_platform": Platform.YOUR_PLATFORM,
}

Add routing in _send_to_platform():

elif platform == Platform.YOUR_PLATFORM:
    return await _send_your_platform(pconfig, chat_id, message)

Implement _send_your_platform() — a standalone async function that sends a single message without requiring the full adapter (for use by cron jobs and the send_message tool outside the gateway process).

Update the tool schema target description to include your platform example.


10. Cronjob Tool Schema (tools/cronjob_tools.py)

Update the deliver parameter description and docstring to mention your platform as a delivery option.


11. Channel Directory (gateway/channel_directory.py)

If your platform can't enumerate chats (most can't), add it to the session-based discovery list:

for plat_name in ("telegram", "whatsapp", "signal", "your_platform"):

12. Status Display (hermes_cli/status.py)

Add to the platforms dict in the Messaging Platforms section:

platforms = {
    ...
    "Your Platform": ("YOUR_PLATFORM_TOKEN", "YOUR_PLATFORM_HOME_CHANNEL"),
}

13. Gateway Setup Wizard (hermes_cli/gateway.py)

Add to the _PLATFORMS list:

{
    "key": "your_platform",
    "label": "Your Platform",
    "emoji": "📱",
    "token_var": "YOUR_PLATFORM_TOKEN",
    "setup_instructions": [...],
    "vars": [...],
}

If your platform needs custom setup logic (connectivity testing, QR codes, policy choices), add a _setup_your_platform() function and route to it in the platform selection switch.

Update _platform_status() if your platform's "configured" check differs from the standard bool(get_env_value(token_var)).


14. Phone/ID Redaction (agent/redact.py)

If your platform uses sensitive identifiers (phone numbers, etc.), add a regex pattern and redaction function to agent/redact.py. This ensures identifiers are masked in ALL log output, not just your adapter's logs.


15. Documentation

File What to update
README.md Platform list in feature table + documentation table
AGENTS.md Gateway description + env var config section
website/docs/user-guide/messaging/<platform>.md NEW — Full setup guide (see existing platform docs for template)
website/docs/user-guide/messaging/index.md Architecture diagram, toolset table, security examples, Next Steps links
website/docs/reference/environment-variables.md All env vars for the platform

16. Tests (tests/gateway/test_<platform>.py)

Recommended test coverage:

  • Platform enum exists with correct value
  • Config loading from env vars via _apply_env_overrides
  • Adapter init (config parsing, allowlist handling, default values)
  • Helper functions (redaction, parsing, file type detection)
  • Session source round-trip (to_dict → from_dict)
  • Authorization integration (platform in allowlist maps)
  • Send message tool routing (platform in platform_map)

Optional but valuable:

  • Async tests for message handling flow (mock the platform API)
  • SSE/WebSocket reconnection logic
  • Attachment processing
  • Group message filtering

Quick Verification

After implementing everything, verify with:

# All tests pass
python -m pytest tests/ -q

# Grep for your platform name to find any missed integration points
grep -r "telegram\|discord\|whatsapp\|slack" gateway/ tools/ agent/ cron/ hermes_cli/ toolsets.py \
  --include="*.py" -l | sort -u
# Check each file in the output — if it mentions other platforms but not yours, you missed it