`ctx.inject_message` could only reach an existing gateway session (`session_key`), so an orchestrator plugin that opens a chat/thread itself (a bot-authored Discord forum post never comes back as an inbound message) had no way to get an agent turn there. `origin` takes the SessionSource.to_dict() shape. The gateway creates (or continues) that chat's session through the adapter's normal message path, in the profile whose plugin manager loaded the plugin, and the reply goes back to the origin chat: - Profile: resolved from the manager's immutable home, never an ambient one. The origin may not name another profile, the gateway's own routing for that chat through that profile's adapter must land in the plugin's profile, and the profile needs its own connected adapter for the platform (another profile's bot is never borrowed). Refusals are synchronous False + one log line. - Authorization: same current-allowlist check as the session_key path (no bypass for host code). - Multiplex: handle_message runs inside the profile's runtime scope so the spawned turn AND the reply send after it inherit that profile's home and secrets (applies to the session_key path too, which used to send from the default loop context). - allow_gateway_injection is read from the plugin's own profile config. - Plugin/system turns (event.internal) skip the human first-contact notes (onboarding intro and the "no home channel" nudge), which were posted into the plugin's fresh thread. Requested by canonicle-desk in #132609. Closes #132609
63 lines
3.0 KiB
Python
63 lines
3.0 KiB
Python
"""Host routing for ``PluginContext.inject_message``: classic CLI REPL, Ink TUI / desktop, messaging
|
|
gateway (existing ``session_key`` or a new session at ``origin``)."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Mapping
|
|
from typing import Any, Optional
|
|
|
|
logger = logging.getLogger("hermes_cli.plugins")
|
|
|
|
|
|
def inject_plugin_message(
|
|
ctx: Any, content: str, role: str, *, session_key: Optional[str],
|
|
origin: Optional[Mapping[str, Any]],
|
|
) -> bool:
|
|
manager = ctx._manager
|
|
msg = content if role == "user" else f"[{role}] {content}"
|
|
cli = manager._cli_ref
|
|
# An origin names a messaging chat; the local REPL is never that chat.
|
|
if cli is not None and origin is None:
|
|
queue_ = cli._interrupt_queue if getattr(cli, "_agent_running", False) else cli._pending_input
|
|
queue_.put(msg)
|
|
return True
|
|
if session_key and origin is not None:
|
|
logger.warning("inject_message: pass session_key or origin, not both")
|
|
return False
|
|
if not session_key and origin is None:
|
|
logger.warning("inject_message: gateway mode requires an existing session_key or an origin")
|
|
return False
|
|
if origin is not None and not isinstance(origin, Mapping):
|
|
logger.warning("inject_message: origin must be a mapping in the SessionSource.to_dict() shape")
|
|
return False
|
|
if not ctx._gateway_injection_allowed():
|
|
logger.warning("inject_message: gateway injection denied for plugin %s; set "
|
|
"plugins.entries.%s.allow_gateway_injection: true to allow it",
|
|
ctx.plugin_id, ctx.plugin_id)
|
|
return False
|
|
# TUI/desktop host is a different slot. It accepts only when it owns this
|
|
# session_key; a miss falls through so a co-resident messaging gateway
|
|
# still receives its own keys. An exception fails closed — do not also
|
|
# hand the same text to the gateway.
|
|
if session_key and manager.has_tui_message_injector:
|
|
try:
|
|
if manager.inject_tui_message(session_key=session_key, content=msg, plugin_id=ctx.plugin_id):
|
|
return True
|
|
except Exception:
|
|
logger.warning("inject_message: TUI scheduling failed for plugin %s", ctx.plugin_id, exc_info=True)
|
|
return False
|
|
if not manager.has_gateway_message_injector:
|
|
logger.warning("inject_message: no live gateway is available")
|
|
return False
|
|
if origin is None:
|
|
target: dict[str, Any] = {"session_key": session_key}
|
|
else:
|
|
# The manager's immutable home is the plugin's profile: the gateway creates the session
|
|
# there or nowhere (never the ambient HERMES_HOME of whatever thread called us).
|
|
target = {"origin": dict(origin), "plugin_home": manager.home_path}
|
|
try:
|
|
return bool(manager.inject_gateway_message(**target, content=msg, plugin_id=ctx.plugin_id))
|
|
except Exception:
|
|
logger.warning("inject_message: gateway scheduling failed for plugin %s", ctx.plugin_id, exc_info=True)
|
|
return False
|