Skip to content

refactor(plugins): add apply_yaml_config_fn registry hook for YAML→env config bridges#24849

Closed
kshitijk4poor wants to merge 1 commit into
NousResearch:mainfrom
kshitijk4poor:feat/apply-yaml-config-fn-24836
Closed

refactor(plugins): add apply_yaml_config_fn registry hook for YAML→env config bridges#24849
kshitijk4poor wants to merge 1 commit into
NousResearch:mainfrom
kshitijk4poor:feat/apply-yaml-config-fn-24836

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Adds apply_yaml_config_fn to PlatformEntry so platform plugins can own their YAML→env config bridges instead of forcing core gateway/config.py to know every platform's schema. Pure addition — no behavior change for existing platforms.

Closes #24836. Refs #3823, #24356.

Why

gateway/config.py currently has hardcoded YAML→env bridges for 8 platforms (discord, telegram, whatsapp, slack, dingtalk, mattermost, matrix, feishu, ~252 LOC). When a platform migrates to the bundled plugin system (e.g. Discord in #24356), its bridge stays stuck in core. Third-party plugin platforms can't bridge YAML keys to env at all without monkey-patching load_gateway_config. The existing env_enablement_fn hook (#21306, #21331) handles env→extras seeding but not YAML→env, which is the symmetric case this hook covers.

What changed

File Change
gateway/platform_registry.py New optional apply_yaml_config_fn: Optional[Callable[[dict, dict], Optional[dict]]] = None field on PlatformEntry.
gateway/config.py Dispatch loop in load_gateway_config() immediately after the generic shared-key loop. Iterates registry entries, calls hook with (yaml_cfg, platform_cfg), swallows exceptions to debug log (matches env_enablement_fn precedent), merges any returned dict into the platform's extra via platforms_data so it survives from_dict.
tests/gateway/test_platform_registry.py 10 new tests: field default, callable acceptance, env mutation, extras merge, both signature args received, exception handling (loop continues after a bad hook), missing/non-dict YAML sections skipped, env > YAML precedence preserved when hook uses not os.getenv(...) guards.
gateway/platforms/ADDING_A_PLATFORM.md Document the new hook alongside env_enablement_fn.
website/docs/developer-guide/adding-platform-adapters.md New "YAML→env Config Bridge" section + entry in the integration-points table.

hermes_cli/plugins.py::register_platform() already forwards **entry_kwargs to PlatformEntry, so plugins can pass apply_yaml_config_fn= immediately with no plugin-side core change.

Order of operations

load_gateway_config():
  1. Generic shared-key loop (unchanged)         — handles unauthorized_dm_behavior,
                                                    notice_delivery, reply_prefix,
                                                    require_mention, etc. across all
                                                    Platform enum members.
  2. apply_yaml_config_fn dispatch (NEW)         — platform-specific YAML keys.
  3. Legacy per-platform hardcoded blocks        — still run; their `not os.getenv(...)`
                                                    guards make them no-ops for any env
                                                    var the hook already set, so env
                                                    > YAML precedence is preserved.
  4. GatewayConfig.from_dict + _apply_env_overrides

This order means each of the 8 hardcoded platforms can migrate independently in follow-up PRs without breaking anything, and migration is net-negative LOC per platform.

Example usage (for a future plugin migration PR)

import os

def _apply_yaml_config(yaml_cfg: dict, platform_cfg: dict) -> dict | None:
    if "require_mention" in platform_cfg and not os.getenv("MY_PLATFORM_REQUIRE_MENTION"):
        os.environ["MY_PLATFORM_REQUIRE_MENTION"] = str(platform_cfg["require_mention"]).lower()
    allowed = platform_cfg.get("allowed_channels")
    if allowed is not None and not os.getenv("MY_PLATFORM_ALLOWED_CHANNELS"):
        if isinstance(allowed, list):
            allowed = ",".join(str(v) for v in allowed)
        os.environ["MY_PLATFORM_ALLOWED_CHANNELS"] = str(allowed)
    return None  # nothing extra to seed into PlatformConfig.extra

def register(ctx):
    ctx.register_platform(
        name="my_platform",
        ...,
        apply_yaml_config_fn=_apply_yaml_config,
    )

Test plan

  • bash scripts/run_tests.sh tests/gateway/test_platform_registry.py42 passed (32 existing + 10 new).
  • bash scripts/run_tests.sh tests/gateway/test_discord_reply_mode.py tests/gateway/test_slack_mention.py tests/gateway/test_whatsapp_reply_prefix.py tests/gateway/test_whatsapp_group_gating.py118 passed (every YAML-loading test in the gateway suite).
  • Full tests/gateway/ → 5367 passed, 9 failures all pre-existing on origin/main (Discord document-handling + Matrix E2EE — verified by stashing this PR and re-running on stock main).
  • E2E with real load_gateway_config(): registered a temporary plugin platform with a YAML→env hook in a tmp HERMES_HOME, wrote a config.yaml exercising both the new hook AND the existing Discord hardcoded block, and verified:
    • Discord legacy block still bridges discord.require_mention and discord.allowed_channels to env vars.
    • Plugin hook successfully sets its own env vars and seeds PlatformConfig.extra.
    • Both coexist cleanly.

Out of scope (future PRs)

@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins comp/gateway Gateway runner, session dispatch, delivery labels May 13, 2026
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 (NousResearch#21306, NousResearch#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 NousResearch#3823, NousResearch#24356.
Closes NousResearch#24836.
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged onto current main via PR #25443 — cherry-pick was clean, your authorship is preserved in git log. Thanks @kshitijk4poor!

#25443

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(plugins): add apply_yaml_config_fn registry hook to let plugins own their YAML→env config bridges

3 participants