refactor(hermes_cli): platform_actions — one gate/dispatch/audit runner with a verb×platform table

This commit is contained in:
Teknium
2026-09-02 20:14:54 -07:00
parent 82430db2a0
commit 17551141cf

View File

@@ -1,7 +1,7 @@
"""Capability-gated platform action facade for plugins (#64176, action half).
Both return a structured result dict — ``{"ok": True, ...}`` on success, ``{"ok": False, "error":
<code>, "detail": <str>}`` on failure — and never raise into hook dispatch.
Every verb returns a structured result dict — ``{"ok": True, ...}`` on success, ``{"ok": False,
"error": <code>, "detail": <str>}`` on failure — and never raises into hook dispatch.
"""
from __future__ import annotations
@@ -24,9 +24,50 @@ def _err(code: str, detail: str = "") -> Dict[str, Any]:
def _ok(**fields: Any) -> Dict[str, Any]:
result: Dict[str, Any] = {"ok": True}
result.update(fields)
return result
return {"ok": True, **fields}
# -- per-platform verb implementations (adapter, *args) -> result ------------
async def _telegram_add_reaction(adapter, chat_id, message_id, emoji):
if await adapter._set_reaction(chat_id, message_id, emoji):
return _ok(action="add_reaction")
return _err("action_failed", "telegram set_message_reaction failed")
async def _discord_add_reaction(adapter: Any, chat_id: str, message_id: str, emoji: str) -> Dict[str, Any]:
client = getattr(adapter, "_client", None)
if client is None:
return _err("adapter_disconnected", "discord client unavailable")
try:
channel_id = int(str(chat_id))
msg_id = int(str(message_id))
except (TypeError, ValueError):
return _err("invalid_argument", "discord ids must be numeric")
channel = client.get_channel(channel_id)
if channel is None:
channel = await client.fetch_channel(channel_id)
message = await channel.fetch_message(msg_id)
await message.add_reaction(emoji)
return _ok(action="add_reaction")
async def _telegram_set_thread_title(adapter, chat_id, thread_id, title):
await adapter.rename_dm_topic(chat_id, int(thread_id), title)
return _ok(action="set_thread_title")
async def _discord_set_thread_title(adapter, chat_id, thread_id, title):
if await adapter.rename_thread(thread_id, title):
return _ok(action="set_thread_title")
return _err("action_failed", "discord thread rename failed")
_VERBS = {
"add_reaction": {"telegram": _telegram_add_reaction, "discord": _discord_add_reaction},
"set_thread_title": {"telegram": _telegram_set_thread_title, "discord": _discord_set_thread_title},
}
class PlatformActions:
@@ -64,23 +105,18 @@ class PlatformActions:
except Exception:
runner = None
if runner is None:
return None, _err(
"gateway_unavailable", "no gateway runner is active in this process"
)
return None, _err("gateway_unavailable", "no gateway runner is active in this process")
try:
from gateway.config import Platform
platform_enum = Platform(str(platform).strip().lower())
except Exception:
return None, _err("unknown_platform", f"unknown platform {platform!r}")
# Multiplex/Team-Gateway: a secondary profile's adapters live in
# runner._profile_adapters[profile], not runner.adapters (the default
# profile's registry) — every other adapter-resolution path in this
# codebase (_authorization_adapter, plugin message-injection) goes
# through this same profile-aware, fail-closed lookup so a plugin
# scoped to one profile can never act through another profile's bot
# identity. Falls back to the bare default-profile lookup only when
# the gateway runner predates this method (defensive, not expected).
# Multiplex/Team-Gateway: a secondary profile's adapters live in runner._profile_adapters,
# not runner.adapters. Every adapter-resolution path goes through the same profile-aware,
# fail-closed lookup so a plugin scoped to one profile can never act through another
# profile's bot identity. The bare default-profile lookup is only for a runner predating
# _authorization_adapter (defensive, not expected).
resolve_fn = getattr(runner, "_authorization_adapter", None)
if callable(resolve_fn):
try:
@@ -88,9 +124,7 @@ class PlatformActions:
profile_name = get_active_profile_name()
except Exception:
# Fail closed: an unresolvable profile must not degrade to the
# default profile's bot (the same rule _authorization_adapter
# applies to a stamped profile with no registry entry).
# Fail closed: an unresolvable profile must not degrade to the default profile's bot.
logger.debug(
"platform_actions: profile resolution failed for %s",
self._plugin_id, exc_info=True,
@@ -104,19 +138,13 @@ class PlatformActions:
else:
adapter = getattr(runner, "adapters", {}).get(platform_enum)
if adapter is None:
return None, _err(
"adapter_not_registered",
f"no {platform_enum.value} adapter is registered",
)
return None, _err("adapter_not_registered", f"no {platform_enum.value} adapter is registered")
try:
connected = bool(adapter.is_connected)
except Exception:
connected = False
if not connected:
return None, _err(
"adapter_disconnected",
f"the {platform_enum.value} adapter is not connected",
)
return None, _err("adapter_disconnected", f"the {platform_enum.value} adapter is not connected")
return adapter, None
def _gate(self, platform: str, **required: Any):
@@ -130,96 +158,45 @@ class PlatformActions:
)
for name, value in required.items():
if not isinstance(value, str) or not value.strip():
return None, _err(
"invalid_argument", f"{name} must be a non-empty string"
)
return None, _err("invalid_argument", f"{name} must be a non-empty string")
return self._resolve_adapter(platform)
async def _run(self, verb: str, platform: str, *args: str, **required: Any) -> Dict[str, Any]:
"""Gate, dispatch *verb* to the adapter's platform implementation, audit, return."""
adapter, error = self._gate(platform, **required)
if error is None and adapter is not None:
try:
impl = _VERBS[verb].get(getattr(adapter.platform, "value", None))
if impl is None:
result = _err("unsupported_platform_action", f"{verb} is not implemented for {platform}")
else:
result = await impl(adapter, *args)
except Exception as exc:
result = _err("action_failed", str(exc)[:512])
else:
result = error or _err("gateway_unavailable")
self._audit(verb, platform, result)
return result
# -- v1 verbs -----------------------------------------------------------
async def add_reaction(
self, platform: str, chat_id: str, message_id: str, emoji: str
) -> Dict[str, Any]:
"""Add/set an emoji reaction on a platform message."""
adapter, error = self._gate(
platform, chat_id=chat_id, message_id=message_id, emoji=emoji
return await self._run(
"add_reaction", platform, chat_id, message_id, emoji,
chat_id=chat_id, message_id=message_id, emoji=emoji,
)
if error is not None or adapter is None:
self._audit("add_reaction", platform, error or _err("gateway_unavailable"))
return error or _err("gateway_unavailable")
try:
if getattr(adapter.platform, "value", None) == "telegram":
done = await adapter._set_reaction(chat_id, message_id, emoji)
result = (
_ok(action="add_reaction")
if done
else _err("action_failed", "telegram set_message_reaction failed")
)
elif getattr(adapter.platform, "value", None) == "discord":
result = await self._discord_add_reaction(
adapter, chat_id, message_id, emoji
)
else:
result = _err(
"unsupported_platform_action",
f"add_reaction is not implemented for {platform}",
)
except Exception as exc:
result = _err("action_failed", str(exc)[:512])
self._audit("add_reaction", platform, result)
return result
async def set_thread_title(
self, platform: str, chat_id: str, thread_id: str, title: str
) -> Dict[str, Any]:
"""Rename a thread / forum topic."""
adapter, error = self._gate(
platform, chat_id=chat_id, thread_id=thread_id, title=title
return await self._run(
"set_thread_title", platform, chat_id, thread_id, title,
chat_id=chat_id, thread_id=thread_id, title=title,
)
if error is not None or adapter is None:
self._audit("set_thread_title", platform, error or _err("gateway_unavailable"))
return error or _err("gateway_unavailable")
try:
if getattr(adapter.platform, "value", None) == "telegram":
await adapter.rename_dm_topic(chat_id, int(thread_id), title)
result = _ok(action="set_thread_title")
elif getattr(adapter.platform, "value", None) == "discord":
done = await adapter.rename_thread(thread_id, title)
result = (
_ok(action="set_thread_title")
if done
else _err("action_failed", "discord thread rename failed")
)
else:
result = _err(
"unsupported_platform_action",
f"set_thread_title is not implemented for {platform}",
)
except Exception as exc:
result = _err("action_failed", str(exc)[:512])
self._audit("set_thread_title", platform, result)
return result
# -- per-platform helpers -------------------------------------------------
@staticmethod
async def _discord_add_reaction(
adapter: Any, chat_id: str, message_id: str, emoji: str
) -> Dict[str, Any]:
client = getattr(adapter, "_client", None)
if client is None:
return _err("adapter_disconnected", "discord client unavailable")
try:
channel_id = int(str(chat_id))
msg_id = int(str(message_id))
except (TypeError, ValueError):
return _err("invalid_argument", "discord ids must be numeric")
channel = client.get_channel(channel_id)
if channel is None:
channel = await client.fetch_channel(channel_id)
message = await channel.fetch_message(msg_id)
await message.add_reaction(emoji)
return _ok(action="add_reaction")
def _audit(self, verb: str, platform: str, result: Dict[str, Any]) -> None:
"""Every platform action is logged (the #64176 'all actions logged' rule)."""