mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
refactor(plugins): add apply_yaml_config_fn registry hook
Lets platform plugins own their YAML→env config bridge instead of forcing core gateway/config.py to know every platform's schema. The hook receives the full parsed config.yaml and the platform's own sub-dict, may mutate os.environ (env > YAML precedence preserved via the standard `not os.getenv(...)` guards), and may return a dict to merge into PlatformConfig.extra. It runs during load_gateway_config() after the existing generic shared-key loop and before _apply_env_overrides(), mirroring the env_enablement_fn dispatch pattern (#21306, #21331). Pure addition — no behavior change for existing platforms. Each of the eight platforms with hardcoded YAML→env blocks today (discord, telegram, whatsapp, slack, dingtalk, mattermost, matrix, feishu, ~252 LOC in gateway/config.py) can migrate in independent follow-up PRs; the hardcoded blocks remain functional in the meantime, and their `not os.getenv(...)` guards make them no-ops for any env var the hook already set. Test coverage: 10 new tests in tests/gateway/test_platform_registry.py covering field default, callable acceptance, env mutation, extras merge, both signature args, exception swallowing, missing/non-dict sections, and env > YAML precedence. Refs #3823, #24356. Closes #24836.
This commit is contained in:
parent
d5775fe988
commit
3633c8690b
5 changed files with 444 additions and 9 deletions
|
|
@ -74,6 +74,24 @@ def _normalize_notice_delivery(value: Any, default: str = "public") -> str:
|
|||
return default
|
||||
|
||||
|
||||
def _ensure_platform_extra_dict(platforms_data: dict, name: str) -> tuple[dict, dict]:
|
||||
"""Get-or-create ``platforms_data[name]`` and its nested ``extra`` dict.
|
||||
|
||||
Both slots are coerced to ``{}`` if a non-dict value is encountered, so
|
||||
callers can safely write keys without type-checking. Returns
|
||||
``(plat_data, extra)`` for in-place mutation.
|
||||
"""
|
||||
plat_data = platforms_data.setdefault(name, {})
|
||||
if not isinstance(plat_data, dict):
|
||||
plat_data = {}
|
||||
platforms_data[name] = plat_data
|
||||
extra = plat_data.setdefault("extra", {})
|
||||
if not isinstance(extra, dict):
|
||||
extra = {}
|
||||
plat_data["extra"] = extra
|
||||
return plat_data, extra
|
||||
|
||||
|
||||
# Module-level cache for bundled platform plugin names (lives outside the
|
||||
# enum so it doesn't become an accidental enum member).
|
||||
_Platform__bundled_plugin_names: Optional[set] = None
|
||||
|
|
@ -755,7 +773,27 @@ def load_gateway_config() -> GatewayConfig:
|
|||
merged["extra"] = merged_extra
|
||||
platforms_data[plat_name] = merged
|
||||
gw_data["platforms"] = platforms_data
|
||||
for plat in Platform:
|
||||
# Iterate built-in platforms plus any registered plugin platforms
|
||||
# so plugin authors get the same shared-key bridging (#24836).
|
||||
try:
|
||||
from hermes_cli.plugins import discover_plugins
|
||||
discover_plugins() # idempotent
|
||||
from gateway.platform_registry import platform_registry as _pr
|
||||
except Exception as e:
|
||||
logger.debug("plugin discovery skipped: %s", e)
|
||||
_pr = None
|
||||
|
||||
_shared_loop_targets: list = list(Platform)
|
||||
if _pr is not None:
|
||||
for _entry in _pr.plugin_entries():
|
||||
try:
|
||||
_plat = Platform(_entry.name)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
if _plat not in _shared_loop_targets:
|
||||
_shared_loop_targets.append(_plat)
|
||||
|
||||
for plat in _shared_loop_targets:
|
||||
if plat == Platform.LOCAL:
|
||||
continue
|
||||
platform_cfg = yaml_cfg.get(plat.value)
|
||||
|
|
@ -810,20 +848,38 @@ def load_gateway_config() -> GatewayConfig:
|
|||
enabled_was_explicit = "enabled" in platform_cfg
|
||||
if not bridged and not enabled_was_explicit:
|
||||
continue
|
||||
plat_data = platforms_data.setdefault(plat.value, {})
|
||||
if not isinstance(plat_data, dict):
|
||||
plat_data = {}
|
||||
platforms_data[plat.value] = plat_data
|
||||
plat_data, extra = _ensure_platform_extra_dict(platforms_data, plat.value)
|
||||
if enabled_was_explicit:
|
||||
plat_data["enabled"] = platform_cfg["enabled"]
|
||||
extra = plat_data.setdefault("extra", {})
|
||||
if not isinstance(extra, dict):
|
||||
extra = {}
|
||||
plat_data["extra"] = extra
|
||||
if plat == Platform.SLACK and enabled_was_explicit:
|
||||
extra["_enabled_explicit"] = True
|
||||
extra.update(bridged)
|
||||
|
||||
# Plugin-owned YAML→env config bridges (#24836). See
|
||||
# ``PlatformEntry.apply_yaml_config_fn`` for the hook contract.
|
||||
# Order: shared-key loop (above) → this dispatch → legacy hardcoded
|
||||
# blocks (below; no-op when a hook already set their env var) →
|
||||
# ``_apply_env_overrides()`` after ``GatewayConfig.from_dict``.
|
||||
if _pr is not None:
|
||||
for entry in _pr.all_entries():
|
||||
if entry.apply_yaml_config_fn is None:
|
||||
continue
|
||||
platform_cfg = yaml_cfg.get(entry.name)
|
||||
if not isinstance(platform_cfg, dict):
|
||||
continue
|
||||
try:
|
||||
seeded = entry.apply_yaml_config_fn(yaml_cfg, platform_cfg)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
"apply_yaml_config_fn for %s raised: %s",
|
||||
entry.name, e,
|
||||
)
|
||||
continue
|
||||
if not isinstance(seeded, dict) or not seeded:
|
||||
continue
|
||||
_, extra = _ensure_platform_extra_dict(platforms_data, entry.name)
|
||||
extra.update(seeded)
|
||||
|
||||
# Slack settings → env vars (env vars take precedence)
|
||||
slack_cfg = yaml_cfg.get("slack", {})
|
||||
if isinstance(slack_cfg, dict):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue