feat: complete plugin platform parity — all 12 integration points
Extends the platform plugin interface from Phase 1 to cover every touchpoint where built-in platforms have hardcoded behavior. - 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 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 _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 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 - 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
This commit is contained in:
@@ -86,6 +86,16 @@ async def build_channel_directory(adapters: Dict[Any, Any]) -> Dict[str, Any]:
|
||||
continue
|
||||
platforms[plat_name] = _build_from_sessions(plat_name)
|
||||
|
||||
# Include plugin-registered platforms (dynamic enum members aren't in
|
||||
# Platform.__members__, so the loop above misses them).
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
for entry in platform_registry.plugin_entries():
|
||||
if entry.name not in _SKIP_SESSION_DISCOVERY and entry.name not in platforms:
|
||||
platforms[entry.name] = _build_from_sessions(entry.name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
directory = {
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"platforms": platforms,
|
||||
|
||||
@@ -67,6 +67,28 @@ class PlatformEntry:
|
||||
# "builtin" or "plugin"
|
||||
source: str = "plugin"
|
||||
|
||||
# ── Auth env var names (for _is_user_authorized integration) ──
|
||||
# E.g. "IRC_ALLOWED_USERS" — checked for comma-separated user IDs.
|
||||
allowed_users_env: str = ""
|
||||
# E.g. "IRC_ALLOW_ALL_USERS" — if truthy, all users authorized.
|
||||
allow_all_env: str = ""
|
||||
|
||||
# ── Message limits ──
|
||||
# Max message length for smart-chunking. 0 = no limit.
|
||||
max_message_length: int = 0
|
||||
|
||||
# ── Privacy ──
|
||||
# If True, session descriptions redact PII (phone numbers, etc.)
|
||||
pii_safe: bool = False
|
||||
|
||||
# ── Display ──
|
||||
# Emoji for CLI/gateway display (e.g. "💬")
|
||||
emoji: str = "🔌"
|
||||
|
||||
# Whether this platform should appear in _UPDATE_ALLOWED_PLATFORMS
|
||||
# (allows /update command from this platform).
|
||||
allow_update_command: bool = True
|
||||
|
||||
|
||||
class PlatformRegistry:
|
||||
"""Central registry of platform adapters.
|
||||
|
||||
@@ -782,6 +782,13 @@ def _format_gateway_process_notification(evt: dict) -> "str | None":
|
||||
return None
|
||||
|
||||
|
||||
# Module-level weak reference to the active GatewayRunner instance.
|
||||
# Used by tools (e.g. send_message) that need to route through a live
|
||||
# adapter for plugin platforms. Set in GatewayRunner.__init__().
|
||||
import weakref as _weakref
|
||||
_gateway_runner_ref: _weakref.ref = lambda: None
|
||||
|
||||
|
||||
class GatewayRunner:
|
||||
"""
|
||||
Main gateway controller.
|
||||
@@ -806,9 +813,11 @@ class GatewayRunner:
|
||||
_session_reasoning_overrides: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def __init__(self, config: Optional[GatewayConfig] = None):
|
||||
global _gateway_runner_ref
|
||||
self.config = config or load_gateway_config()
|
||||
self.adapters: Dict[Platform, BasePlatformAdapter] = {}
|
||||
self._warn_if_docker_media_delivery_is_risky()
|
||||
_gateway_runner_ref = _weakref.ref(self)
|
||||
|
||||
# Load ephemeral config from config.yaml / env vars.
|
||||
# Both are injected at API-call time only and never persisted.
|
||||
@@ -2483,7 +2492,17 @@ class GatewayRunner:
|
||||
|
||||
adapter = self._create_adapter(platform, platform_config)
|
||||
if not adapter:
|
||||
logger.warning("No adapter available for %s", platform.value)
|
||||
# Distinguish between missing builtin deps and missing plugin
|
||||
_pval = platform.value
|
||||
_builtin_names = {m.value for m in Platform.__members__.values()}
|
||||
if _pval not in _builtin_names:
|
||||
logger.warning(
|
||||
"No adapter for '%s' — is the plugin installed? "
|
||||
"(platform is enabled in config.yaml but no plugin registered it)",
|
||||
_pval,
|
||||
)
|
||||
else:
|
||||
logger.warning("No adapter available for %s", _pval)
|
||||
continue
|
||||
|
||||
# Set up message + fatal error handlers
|
||||
@@ -3462,6 +3481,19 @@ class GatewayRunner:
|
||||
Platform.YUANBAO: "YUANBAO_ALLOW_ALL_USERS",
|
||||
}
|
||||
|
||||
# Plugin platforms: check the registry for auth env var names
|
||||
if source.platform not in platform_env_map:
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
entry = platform_registry.get(source.platform.value)
|
||||
if entry:
|
||||
if entry.allowed_users_env:
|
||||
platform_env_map[source.platform] = entry.allowed_users_env
|
||||
if entry.allow_all_env:
|
||||
platform_allow_all_map[source.platform] = entry.allow_all_env
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
|
||||
platform_allow_all_var = platform_allow_all_map.get(source.platform, "")
|
||||
if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in ("true", "1", "yes"):
|
||||
@@ -8761,8 +8793,16 @@ class GatewayRunner:
|
||||
|
||||
# Block non-messaging platforms (API server, webhooks, ACP)
|
||||
platform = event.source.platform
|
||||
if platform not in self._UPDATE_ALLOWED_PLATFORMS:
|
||||
return "✗ /update is only available from messaging platforms. Run `hermes update` from the terminal."
|
||||
_allowed = self._UPDATE_ALLOWED_PLATFORMS
|
||||
# Plugin platforms with allow_update_command=True are also allowed
|
||||
if platform not in _allowed:
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
entry = platform_registry.get(platform.value)
|
||||
if not entry or not entry.allow_update_command:
|
||||
return "✗ /update is only available from messaging platforms. Run `hermes update` from the terminal."
|
||||
except Exception:
|
||||
return "✗ /update is only available from messaging platforms. Run `hermes update` from the terminal."
|
||||
|
||||
if is_managed():
|
||||
return f"✗ {format_managed_message('update Hermes Agent')}"
|
||||
|
||||
Reference in New Issue
Block a user