feat(gateway): plugin-platform-safe deserialization via resolve_platform_id

Without this, plugin-registered platforms (like molecule-a2a) survive
the runtime hot path but die on daemon restart: SessionSource.from_dict
calls Platform(value) directly, which raises ValueError for any name
not in the closed enum. Restored sessions with plugin-platform origins
either crash the restore loop or silently drop the platform field.

Add resolve_platform_id() — Platform-first, falls back to
PluginPlatformIdentifier ONLY when a currently-loaded plugin actually
claims the name. Bare typos and corrupted state still raise so silent
state-corruption can't slip past restore. Wire it into the three
known from_dict call sites:

- gateway/session.py:SessionSource.from_dict
- gateway/session.py:SessionEntry.from_dict (drops the silent debug-log
  swallow that left platform=None on unknown values)
- gateway/config.py:HomeChannel.from_dict

All existing gateway/test_session.py round-trip tests still pass,
including test_invalid_platform_raises (the narrow fallback preserves
loud-fail semantics for genuinely unknown names).
This commit is contained in:
Hongming Wang 2026-05-02 03:01:51 -07:00
parent 17451dc77a
commit 047de4a668
3 changed files with 32 additions and 6 deletions

View File

@ -90,8 +90,9 @@ class HomeChannel:
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "HomeChannel":
from hermes_cli.plugins import resolve_platform_id
return cls(
platform=Platform(data["platform"]),
platform=resolve_platform_id(data["platform"]),
chat_id=str(data["chat_id"]),
name=data.get("name", "Home"),
)

View File

@ -123,8 +123,9 @@ class SessionSource:
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SessionSource":
from hermes_cli.plugins import resolve_platform_id
return cls(
platform=Platform(data["platform"]),
platform=resolve_platform_id(data["platform"]),
chat_id=str(data["chat_id"]),
chat_name=data.get("chat_name"),
chat_type=data.get("chat_type", "dm"),
@ -409,10 +410,8 @@ class SessionEntry:
platform = None
if data.get("platform"):
try:
platform = Platform(data["platform"])
except ValueError as e:
logger.debug("Unknown platform value %r: %s", data["platform"], e)
from hermes_cli.plugins import resolve_platform_id
platform = resolve_platform_id(data["platform"])
return cls(
session_key=data["session_key"],

View File

@ -883,6 +883,32 @@ class PluginPlatformIdentifier:
return f"PluginPlatformIdentifier({self.value!r})"
def resolve_platform_id(value: str):
"""Resolve a platform-name string to a ``Platform`` enum member or
a ``PluginPlatformIdentifier``.
Used by ``from_dict`` deserializers for ``SessionSource``,
``SessionEntry``, ``HomeChannel`` etc. Without this fallback,
daemon restart would lose every session whose platform was
contributed by a plugin (e.g., ``molecule-a2a``) because the
bare ``Platform(value)`` call raises ``ValueError`` for unknown
enum members.
The fallback is intentionally narrow: an unknown name is only
accepted as a plugin identifier if a currently-loaded plugin has
actually claimed that name via ``register_platform_adapter``. Bare
typos and corrupted state still raise ``ValueError`` as before, so
silent state-corruption doesn't slip past restore.
"""
from gateway.config import Platform # late import to avoid cycle
try:
return Platform(value)
except ValueError:
if get_plugin_platform_adapter(value) is not None:
return PluginPlatformIdentifier(value)
raise
def get_plugin_commands() -> Dict[str, dict]:
"""Return the full plugin commands dict (name → {handler, description, plugin}).