Teknium ruled the post-admission consumer hook (#129958) must fail OPEN, so reshape the salvaged implementation into one small seam: - `{"action": "handled", "reply": str|None}` is the only decisive result; None, any other value, a raising callback or a timeout runs the ordinary agent turn. One buggy plugin can no longer blackhole every inbound message on a profile (the cherry-picked version failed closed on any exception or malformed return from ANY registered callback). - Fire under the message's ROUTED profile scope. The receiving bot's handler binds the transport home so auth can read its .env; when that bot serves a chat for another profile, the ambient scope at the hook was the wrong profile and that profile's consumers never ran. - Snapshot-only payload (session_key, platform, source dict, message_id, text): no live gateway runner / session store handles, so the plugin isolation host needs no HOST_DEGRADED_HOOKS entry. - Bounded by plugins.hook_callback_timeout (fail-open) instead of the fail-closed policy-hook set; the required-but-discarded receipt, alias table, destination snapshot/restore machinery and hard-coded English failure notice are gone. - Tests trimmed to three invariants through the real _handle_message and real plugin dispatch: consume path, raise path proceeds, routed profile scope bound.
59 lines
2.6 KiB
Python
59 lines
2.6 KiB
Python
"""``post_gateway_admission``: let a plugin consume an admitted inbound message (#129958).
|
|
|
|
``GatewayInboundMixin._handle_message`` fires it once per non-internal message that survived
|
|
authorization, bot admission, pause/drain, pending-reply intercepts, the running-session lane and
|
|
idle slash-command dispatch, inside the claimed session slot. It runs under the message's ROUTED
|
|
profile scope: the receiving bot's handler binds the transport home (auth reads its ``.env``), which
|
|
differs from the runtime home when that bot serves a chat for another profile.
|
|
|
|
Fail-open: a callback that raises, times out or returns anything other than
|
|
``{"action": "handled"}`` is logged/ignored and the message proceeds to the agent, so one buggy
|
|
plugin can never blackhole a profile's traffic. The payload is a plain snapshot, never live
|
|
runner/session-store handles.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import logging
|
|
from typing import Any, Optional, Tuple
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
POST_ADMISSION_HOOK = "post_gateway_admission"
|
|
|
|
|
|
def _routed_scope(runner: Any, source: Any):
|
|
if not getattr(getattr(runner, "config", None), "multiplex_profiles", False):
|
|
return contextlib.nullcontext()
|
|
from gateway.run import _async_profile_runtime_scope
|
|
return _async_profile_runtime_scope(runner._resolve_profile_home_for_source(source))
|
|
|
|
|
|
async def run_post_admission_hook(
|
|
runner: Any, event: Any, source: Any, session_key: str
|
|
) -> Tuple[bool, Optional[str]]:
|
|
"""Return ``(handled, reply)``; ``handled=False`` means run the ordinary agent turn."""
|
|
try:
|
|
async with _routed_scope(runner, source):
|
|
from hermes_cli.lifecycle import ainvoke_hook, has_hook
|
|
if not has_hook(POST_ADMISSION_HOOK):
|
|
return False, None
|
|
results = await ainvoke_hook(
|
|
POST_ADMISSION_HOOK, session_key=session_key,
|
|
platform=source.platform.value if source.platform else "",
|
|
source=source.to_dict(), message_id=event.message_id, text=event.text or "",
|
|
)
|
|
except asyncio.CancelledError:
|
|
raise
|
|
except Exception:
|
|
logger.warning("%s failed; continuing with the agent turn", POST_ADMISSION_HOOK, exc_info=True)
|
|
return False, None
|
|
for result in results:
|
|
if isinstance(result, dict) and result.get("action") == "handled":
|
|
reply = result.get("reply")
|
|
logger.info("%s: a plugin handled the message for session %s", POST_ADMISSION_HOOK, session_key)
|
|
return True, reply if isinstance(reply, str) and reply.strip() else None
|
|
return False, None
|