feat: final platform plugin parity — webhook delivery, platform hints, docs
Closes remaining functional gaps and adds documentation.
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).
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
This commit is contained in:
@@ -891,15 +891,6 @@ def _validate_gateway_config(config: "GatewayConfig") -> None:
|
|||||||
if not pconfig.enabled:
|
if not pconfig.enabled:
|
||||||
continue
|
continue
|
||||||
env_name = _token_env_names.get(platform)
|
env_name = _token_env_names.get(platform)
|
||||||
if not env_name:
|
|
||||||
# Check plugin registry for required_env
|
|
||||||
try:
|
|
||||||
from gateway.platform_registry import platform_registry
|
|
||||||
entry = platform_registry.get(platform.value)
|
|
||||||
if entry and entry.required_env:
|
|
||||||
env_name = entry.required_env[0] # primary env var
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if env_name and pconfig.token is not None and not pconfig.token.strip():
|
if env_name and pconfig.token is not None and not pconfig.token.strip():
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"%s is enabled but %s is empty. "
|
"%s is enabled but %s is empty. "
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ class PlatformEntry:
|
|||||||
# (allows /update command from this platform).
|
# (allows /update command from this platform).
|
||||||
allow_update_command: bool = True
|
allow_update_command: bool = True
|
||||||
|
|
||||||
|
# ── LLM guidance ──
|
||||||
|
# Platform hint injected into the system prompt (e.g. "You are on IRC.
|
||||||
|
# Do not use markdown."). Empty string = no hint.
|
||||||
|
platform_hint: str = ""
|
||||||
|
|
||||||
|
|
||||||
class PlatformRegistry:
|
class PlatformRegistry:
|
||||||
"""Central registry of platform adapters.
|
"""Central registry of platform adapters.
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
# Adding a New Messaging Platform
|
# Adding a New Messaging Platform
|
||||||
|
|
||||||
Checklist for integrating a new messaging platform into the Hermes gateway.
|
There are two ways to add a platform to the Hermes gateway:
|
||||||
Use this as a reference when building a new adapter — every item here is a
|
|
||||||
real integration point that exists in the codebase. Missing any of them will
|
## Plugin Path (Recommended for Community/Third-Party)
|
||||||
cause broken functionality, missing features, or inconsistent behavior.
|
|
||||||
|
Create a plugin directory in `~/.hermes/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.
|
||||||
|
|
||||||
|
See `plugins/platforms/irc/` for a complete reference implementation, and
|
||||||
|
`website/docs/developer-guide/adding-platform-adapters.md` for the full
|
||||||
|
plugin guide with code examples.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -202,26 +202,22 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||||||
if deliver_type == "github_comment":
|
if deliver_type == "github_comment":
|
||||||
return await self._deliver_github_comment(content, delivery)
|
return await self._deliver_github_comment(content, delivery)
|
||||||
|
|
||||||
# Cross-platform delivery — any platform with a gateway adapter
|
# Cross-platform delivery — any platform with a gateway adapter.
|
||||||
if self.gateway_runner and deliver_type in (
|
# Check both built-in names and plugin-registered platforms.
|
||||||
"telegram",
|
_BUILTIN_DELIVER_PLATFORMS = {
|
||||||
"discord",
|
"telegram", "discord", "slack", "signal", "sms", "whatsapp",
|
||||||
"slack",
|
"matrix", "mattermost", "homeassistant", "email", "dingtalk",
|
||||||
"signal",
|
"feishu", "wecom", "wecom_callback", "weixin", "bluebubbles",
|
||||||
"sms",
|
"qqbot", "yuanbao",
|
||||||
"whatsapp",
|
}
|
||||||
"matrix",
|
_is_known_platform = deliver_type in _BUILTIN_DELIVER_PLATFORMS
|
||||||
"mattermost",
|
if not _is_known_platform:
|
||||||
"homeassistant",
|
try:
|
||||||
"email",
|
from gateway.platform_registry import platform_registry
|
||||||
"dingtalk",
|
_is_known_platform = platform_registry.is_registered(deliver_type)
|
||||||
"feishu",
|
except Exception:
|
||||||
"wecom",
|
pass
|
||||||
"wecom_callback",
|
if self.gateway_runner and _is_known_platform:
|
||||||
"weixin",
|
|
||||||
"bluebubbles",
|
|
||||||
"qqbot",
|
|
||||||
):
|
|
||||||
return await self._deliver_cross_platform(
|
return await self._deliver_cross_platform(
|
||||||
deliver_type, content, delivery
|
deliver_type, content, delivery
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -151,6 +151,18 @@ class IRCAdapter(BasePlatformAdapter):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Prevent two profiles from using the same IRC identity
|
||||||
|
try:
|
||||||
|
from gateway.status import acquire_scoped_lock, release_scoped_lock
|
||||||
|
lock_key = f"{self.server}:{self.nickname}"
|
||||||
|
if not acquire_scoped_lock("irc", lock_key):
|
||||||
|
logger.error("IRC: %s@%s already in use by another profile", self.nickname, self.server)
|
||||||
|
self._set_fatal_error("lock_conflict", "IRC identity in use by another profile", retryable=False)
|
||||||
|
return False
|
||||||
|
self._lock_key = lock_key
|
||||||
|
except ImportError:
|
||||||
|
self._lock_key = None # status module not available (e.g. tests)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ssl_ctx = None
|
ssl_ctx = None
|
||||||
if self.use_tls:
|
if self.use_tls:
|
||||||
@@ -197,6 +209,13 @@ class IRCAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
async def disconnect(self) -> None:
|
async def disconnect(self) -> None:
|
||||||
"""Quit and close the connection."""
|
"""Quit and close the connection."""
|
||||||
|
# Release the scoped lock so another profile can use this identity
|
||||||
|
if getattr(self, "_lock_key", None):
|
||||||
|
try:
|
||||||
|
from gateway.status import release_scoped_lock
|
||||||
|
release_scoped_lock("irc", self._lock_key)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self._mark_disconnected()
|
self._mark_disconnected()
|
||||||
if self._writer and not self._writer.is_closing():
|
if self._writer and not self._writer.is_closing():
|
||||||
try:
|
try:
|
||||||
@@ -500,4 +519,12 @@ def register(ctx):
|
|||||||
# IRC doesn't have phone numbers to redact
|
# IRC doesn't have phone numbers to redact
|
||||||
pii_safe=False,
|
pii_safe=False,
|
||||||
allow_update_command=True,
|
allow_update_command=True,
|
||||||
|
# LLM guidance
|
||||||
|
platform_hint=(
|
||||||
|
"You are chatting via IRC. IRC does not support markdown formatting "
|
||||||
|
"— use plain text only. Messages are limited to ~450 characters per "
|
||||||
|
"line (long messages are automatically split). In channels, users "
|
||||||
|
"address you by prefixing your nick. Keep responses concise and "
|
||||||
|
"conversational."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
33
run_agent.py
33
run_agent.py
@@ -2151,7 +2151,7 @@ class AIAgent:
|
|||||||
# Context engine reset (works for both built-in compressor and plugins)
|
# Context engine reset (works for both built-in compressor and plugins)
|
||||||
if hasattr(self, "context_compressor") and self.context_compressor:
|
if hasattr(self, "context_compressor") and self.context_compressor:
|
||||||
self.context_compressor.on_session_reset()
|
self.context_compressor.on_session_reset()
|
||||||
|
|
||||||
def _ensure_lmstudio_runtime_loaded(self, config_context_length: Optional[int] = None) -> None:
|
def _ensure_lmstudio_runtime_loaded(self, config_context_length: Optional[int] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Preload the LM Studio model with at least Hermes' minimum context.
|
Preload the LM Studio model with at least Hermes' minimum context.
|
||||||
@@ -2935,7 +2935,7 @@ class AIAgent:
|
|||||||
|
|
||||||
# Check if there's any non-whitespace content remaining
|
# Check if there's any non-whitespace content remaining
|
||||||
return bool(cleaned.strip())
|
return bool(cleaned.strip())
|
||||||
|
|
||||||
def _strip_think_blocks(self, content: str) -> str:
|
def _strip_think_blocks(self, content: str) -> str:
|
||||||
"""Remove reasoning/thinking blocks from content, returning only visible text.
|
"""Remove reasoning/thinking blocks from content, returning only visible text.
|
||||||
|
|
||||||
@@ -3153,8 +3153,8 @@ class AIAgent:
|
|||||||
marker in assistant_text for marker in workspace_markers
|
marker in assistant_text for marker in workspace_markers
|
||||||
)
|
)
|
||||||
return (user_targets_workspace or assistant_targets_workspace) and assistant_mentions_action
|
return (user_targets_workspace or assistant_targets_workspace) and assistant_mentions_action
|
||||||
|
|
||||||
|
|
||||||
def _extract_reasoning(self, assistant_message) -> Optional[str]:
|
def _extract_reasoning(self, assistant_message) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Extract reasoning/thinking content from an assistant message.
|
Extract reasoning/thinking content from an assistant message.
|
||||||
@@ -3727,7 +3727,7 @@ class AIAgent:
|
|||||||
|
|
||||||
# Return everything up to (not including) the last assistant message
|
# Return everything up to (not including) the last assistant message
|
||||||
return messages[:last_assistant_idx]
|
return messages[:last_assistant_idx]
|
||||||
|
|
||||||
def _format_tools_for_system_message(self) -> str:
|
def _format_tools_for_system_message(self) -> str:
|
||||||
"""
|
"""
|
||||||
Format tool definitions for the system message in the trajectory format.
|
Format tool definitions for the system message in the trajectory format.
|
||||||
@@ -3751,7 +3751,7 @@ class AIAgent:
|
|||||||
formatted_tools.append(formatted_tool)
|
formatted_tools.append(formatted_tool)
|
||||||
|
|
||||||
return json.dumps(formatted_tools, ensure_ascii=False)
|
return json.dumps(formatted_tools, ensure_ascii=False)
|
||||||
|
|
||||||
def _convert_to_trajectory_format(self, messages: List[Dict[str, Any]], user_query: str, completed: bool) -> List[Dict[str, Any]]:
|
def _convert_to_trajectory_format(self, messages: List[Dict[str, Any]], user_query: str, completed: bool) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Convert internal message format to trajectory format for saving.
|
Convert internal message format to trajectory format for saving.
|
||||||
@@ -3916,7 +3916,7 @@ class AIAgent:
|
|||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return trajectory
|
return trajectory
|
||||||
|
|
||||||
def _save_trajectory(self, messages: List[Dict[str, Any]], user_query: str, completed: bool):
|
def _save_trajectory(self, messages: List[Dict[str, Any]], user_query: str, completed: bool):
|
||||||
"""
|
"""
|
||||||
Save conversation trajectory to JSONL file.
|
Save conversation trajectory to JSONL file.
|
||||||
@@ -3931,7 +3931,7 @@ class AIAgent:
|
|||||||
|
|
||||||
trajectory = self._convert_to_trajectory_format(messages, user_query, completed)
|
trajectory = self._convert_to_trajectory_format(messages, user_query, completed)
|
||||||
_save_trajectory_to_file(trajectory, self.model, completed)
|
_save_trajectory_to_file(trajectory, self.model, completed)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _summarize_api_error(error: Exception) -> str:
|
def _summarize_api_error(error: Exception) -> str:
|
||||||
"""Extract a human-readable one-liner from an API error.
|
"""Extract a human-readable one-liner from an API error.
|
||||||
@@ -4243,7 +4243,7 @@ class AIAgent:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.verbose_logging:
|
if self.verbose_logging:
|
||||||
logging.warning(f"Failed to save session log: {e}")
|
logging.warning(f"Failed to save session log: {e}")
|
||||||
|
|
||||||
def interrupt(self, message: str = None) -> None:
|
def interrupt(self, message: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
Request the agent to interrupt its current tool-calling loop.
|
Request the agent to interrupt its current tool-calling loop.
|
||||||
@@ -4311,7 +4311,7 @@ class AIAgent:
|
|||||||
logger.debug("Failed to propagate interrupt to child agent: %s", e)
|
logger.debug("Failed to propagate interrupt to child agent: %s", e)
|
||||||
if not self.quiet_mode:
|
if not self.quiet_mode:
|
||||||
print("\n⚡ Interrupt requested" + (f": '{message[:40]}...'" if message and len(message) > 40 else f": '{message}'" if message else ""))
|
print("\n⚡ Interrupt requested" + (f": '{message[:40]}...'" if message and len(message) > 40 else f": '{message}'" if message else ""))
|
||||||
|
|
||||||
def clear_interrupt(self) -> None:
|
def clear_interrupt(self) -> None:
|
||||||
"""Clear any pending interrupt request and the per-thread tool interrupt signal."""
|
"""Clear any pending interrupt request and the per-thread tool interrupt signal."""
|
||||||
self._interrupt_requested = False
|
self._interrupt_requested = False
|
||||||
@@ -4532,7 +4532,7 @@ class AIAgent:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def commit_memory_session(self, messages: list = None) -> None:
|
def commit_memory_session(self, messages: list = None) -> None:
|
||||||
"""Trigger end-of-session extraction without tearing providers down.
|
"""Trigger end-of-session extraction without tearing providers down.
|
||||||
Called when session_id rotates (e.g. /new, context compression);
|
Called when session_id rotates (e.g. /new, context compression);
|
||||||
@@ -4728,7 +4728,7 @@ class AIAgent:
|
|||||||
if not self.quiet_mode:
|
if not self.quiet_mode:
|
||||||
self._vprint(f"{self.log_prefix}📋 Restored {len(last_todo_response)} todo item(s) from history")
|
self._vprint(f"{self.log_prefix}📋 Restored {len(last_todo_response)} todo item(s) from history")
|
||||||
_set_interrupt(False)
|
_set_interrupt(False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_interrupted(self) -> bool:
|
def is_interrupted(self) -> bool:
|
||||||
"""Check if an interrupt has been requested."""
|
"""Check if an interrupt has been requested."""
|
||||||
@@ -4912,6 +4912,15 @@ class AIAgent:
|
|||||||
platform_key = (self.platform or "").lower().strip()
|
platform_key = (self.platform or "").lower().strip()
|
||||||
if platform_key in PLATFORM_HINTS:
|
if platform_key in PLATFORM_HINTS:
|
||||||
prompt_parts.append(PLATFORM_HINTS[platform_key])
|
prompt_parts.append(PLATFORM_HINTS[platform_key])
|
||||||
|
elif platform_key:
|
||||||
|
# Check plugin registry for platform-specific LLM guidance
|
||||||
|
try:
|
||||||
|
from gateway.platform_registry import platform_registry
|
||||||
|
_entry = platform_registry.get(platform_key)
|
||||||
|
if _entry and _entry.platform_hint:
|
||||||
|
prompt_parts.append(_entry.platform_hint)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return "\n\n".join(p.strip() for p in prompt_parts if p.strip())
|
return "\n\n".join(p.strip() for p in prompt_parts if p.strip())
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ sidebar_position: 9
|
|||||||
This guide covers adding a new messaging platform to the Hermes gateway. A platform adapter connects Hermes to an external messaging service (Telegram, Discord, WeCom, etc.) so users can interact with the agent through that service.
|
This guide covers adding a new messaging platform to the Hermes gateway. A platform adapter connects Hermes to an external messaging service (Telegram, Discord, WeCom, etc.) so users can interact with the agent through that service.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
Adding a platform adapter touches 20+ files across code, config, and docs. Use this guide as a checklist — the adapter file itself is typically only 40% of the work.
|
There are two ways to add a platform:
|
||||||
|
- **Plugin** (recommended for community/third-party): Drop a plugin directory into `~/.hermes/plugins/` — zero core code changes needed. See [Plugin Path](#plugin-path-recommended) below.
|
||||||
|
- **Built-in**: Modify 20+ files across code, config, and docs. Use the [Built-in Checklist](#step-by-step-checklist) below.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
@@ -26,7 +28,152 @@ Every adapter extends `BasePlatformAdapter` from `gateway/platforms/base.py` and
|
|||||||
|
|
||||||
Inbound messages are received by the adapter and forwarded via `self.handle_message(event)`, which the base class routes to the gateway runner.
|
Inbound messages are received by the adapter and forwarded via `self.handle_message(event)`, which the base class routes to the gateway runner.
|
||||||
|
|
||||||
## Step-by-Step Checklist
|
## Plugin Path (Recommended)
|
||||||
|
|
||||||
|
The plugin system lets you add a platform adapter without modifying any core Hermes code. Your plugin is a directory with two files:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.hermes/plugins/my-platform/
|
||||||
|
PLUGIN.yaml # Plugin metadata
|
||||||
|
adapter.py # Adapter class + register() entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
### PLUGIN.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: my-platform
|
||||||
|
version: 1.0.0
|
||||||
|
description: My custom messaging platform adapter
|
||||||
|
requires_env:
|
||||||
|
- MY_PLATFORM_TOKEN
|
||||||
|
- MY_PLATFORM_CHANNEL
|
||||||
|
```
|
||||||
|
|
||||||
|
### adapter.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from gateway.platforms.base import (
|
||||||
|
BasePlatformAdapter, SendResult, MessageEvent, MessageType,
|
||||||
|
)
|
||||||
|
from gateway.config import Platform, PlatformConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MyPlatformAdapter(BasePlatformAdapter):
|
||||||
|
def __init__(self, config: PlatformConfig):
|
||||||
|
super().__init__(config, Platform("my_platform"))
|
||||||
|
extra = config.extra or {}
|
||||||
|
self.token = os.getenv("MY_PLATFORM_TOKEN") or extra.get("token", "")
|
||||||
|
|
||||||
|
async def connect(self) -> bool:
|
||||||
|
# Connect to the platform API, start listeners
|
||||||
|
self._mark_connected()
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
self._mark_disconnected()
|
||||||
|
|
||||||
|
async def send(self, chat_id, content, reply_to=None, metadata=None):
|
||||||
|
# Send message via platform API
|
||||||
|
return SendResult(success=True, message_id="...")
|
||||||
|
|
||||||
|
async def get_chat_info(self, chat_id):
|
||||||
|
return {"name": chat_id, "type": "dm"}
|
||||||
|
|
||||||
|
|
||||||
|
def check_requirements() -> bool:
|
||||||
|
return bool(os.getenv("MY_PLATFORM_TOKEN"))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config(config) -> bool:
|
||||||
|
extra = getattr(config, "extra", {}) or {}
|
||||||
|
return bool(os.getenv("MY_PLATFORM_TOKEN") or extra.get("token"))
|
||||||
|
|
||||||
|
|
||||||
|
def register(ctx):
|
||||||
|
"""Plugin entry point — called by the Hermes plugin system."""
|
||||||
|
ctx.register_platform(
|
||||||
|
name="my_platform",
|
||||||
|
label="My Platform",
|
||||||
|
adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
|
||||||
|
check_fn=check_requirements,
|
||||||
|
validate_config=validate_config,
|
||||||
|
required_env=["MY_PLATFORM_TOKEN"],
|
||||||
|
install_hint="pip install my-platform-sdk",
|
||||||
|
# Per-platform user authorization env vars
|
||||||
|
allowed_users_env="MY_PLATFORM_ALLOWED_USERS",
|
||||||
|
allow_all_env="MY_PLATFORM_ALLOW_ALL_USERS",
|
||||||
|
# Message length limit for smart chunking (0 = no limit)
|
||||||
|
max_message_length=4000,
|
||||||
|
# LLM guidance injected into system prompt
|
||||||
|
platform_hint=(
|
||||||
|
"You are chatting via My Platform. "
|
||||||
|
"It supports markdown formatting."
|
||||||
|
),
|
||||||
|
# Display
|
||||||
|
emoji="💬",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional: register platform-specific tools
|
||||||
|
ctx.register_tool(
|
||||||
|
name="my_platform_search",
|
||||||
|
toolset="my_platform",
|
||||||
|
schema={...},
|
||||||
|
handler=my_search_handler,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Users configure the platform in `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
gateway:
|
||||||
|
platforms:
|
||||||
|
my_platform:
|
||||||
|
enabled: true
|
||||||
|
extra:
|
||||||
|
token: "..."
|
||||||
|
channel: "#general"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via environment variables (which the adapter reads in `__init__`).
|
||||||
|
|
||||||
|
### What the Plugin System Handles Automatically
|
||||||
|
|
||||||
|
When you call `ctx.register_platform()`, the following integration points are handled for you — no core code changes needed:
|
||||||
|
|
||||||
|
| 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 | `allowed_users_env` / `allow_all_env` checked |
|
||||||
|
| Cron delivery | `Platform()` resolves any registered name |
|
||||||
|
| send_message tool | Routes through live gateway adapter |
|
||||||
|
| Webhook cross-platform delivery | Registry checked for known platforms |
|
||||||
|
| `/update` command access | `allow_update_command` flag |
|
||||||
|
| Channel directory | Plugin platforms included in enumeration |
|
||||||
|
| System prompt hints | `platform_hint` injected into LLM context |
|
||||||
|
| Message chunking | `max_message_length` for smart splitting |
|
||||||
|
| PII redaction | `pii_safe` flag |
|
||||||
|
| `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 |
|
||||||
|
| Token lock (multi-profile) | Use `acquire_scoped_lock()` in your `connect()` |
|
||||||
|
| Orphaned config warning | Descriptive log when plugin is missing |
|
||||||
|
|
||||||
|
### Reference Implementation
|
||||||
|
|
||||||
|
See `plugins/platforms/irc/` in the repo for a complete working example — a full async IRC adapter with zero external dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Checklist (Built-in Path)
|
||||||
|
|
||||||
|
:::note
|
||||||
|
This checklist is for adding a platform directly to the Hermes core codebase — typically done by core contributors for officially supported platforms. Community/third-party platforms should use the [Plugin Path](#plugin-path-recommended) above.
|
||||||
|
:::
|
||||||
|
|
||||||
### 1. Platform Enum
|
### 1. Platform Enum
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user