{"repo":"NousResearch/hermes-agent","public_id":"24836","display_label":"Plugin registry hook for YAML-to-env config bridges","summary":"All three members share near-identical titles describing a refactor that adds an `apply_yaml_config_fn` registry hook so plugins can own their YAML→env configuration bridges. They appear to be duplicate or closely related submissions; titles alone don't indicate their status or outcome.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":25443,"merged_at":"2026-05-14T05:20:30+00:00"},"resolution":{"state":"resolved","by":"pr-25443"},"freshness":{"state":"verified","last_verified_at":"2026-05-14T05:20:30+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":24836,"pr_number":25443,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-14T05:20:30+00:00"}],"members":[{"kind":"issue","number":24836,"title":"refactor(plugins): add apply_yaml_config_fn registry hook to let plugins own their YAML→env config bridges","url":null,"state":"closed"},{"kind":"pr","number":24849,"title":"refactor(plugins): add apply_yaml_config_fn registry hook for YAML→env config bridges","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#25443","number":25443}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":25443,"title":"refactor(plugins): add apply_yaml_config_fn registry hook for YAML→env config bridges","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/config.py","gateway/platform_registry.py","gateway/platforms/ADDING_A_PLATFORM.md","tests/gateway/test_platform_registry.py","website/docs/developer-guide/adding-platform-adapters.md"],"fix_hunks":[{"file":"gateway/config.py","hunks":["@@ -74,6 +74,24 @@ def _normalize_notice_delivery(value: Any, default: str = \"public\") -> str:\n     return default\n \n \n+def _ensure_platform_extra_dict(platforms_data: dict, name: str) -> tuple[dict, dict]:\n+    \"\"\"Get-or-create ``platforms_data[name]`` and its nested ``extra`` dict.\n+\n+    Both slots are coerced to ``{}`` if a non-dict value is encountered, so\n+    callers can safely write keys without type-checking.  Returns\n+    ``(plat_data, extra)`` for in-place mutation.\n+    \"\"\"\n+    plat_data = platforms_data.setdefault(name, {})\n+    if not isinstance(plat_data, dict):\n+        plat_data = {}\n+        platforms_data[name] = plat_data\n+    extra = plat_data.setdefault(\"extra\", {})\n+    if not isinstance(extra, dict):\n+        extra = {}\n+        plat_data[\"extra\"] = extra\n+    return plat_data, extra\n+\n+\n # Module-level cache for bundled platform plugin names (lives outside the\n # enum so it doesn't become an accidental enum member).\n _Platform__bundled_plugin_names: Optional[set] = None","@@ -755,7 +773,27 @@ def load_gateway_config() -> GatewayConfig:\n                         merged[\"extra\"] = merged_extra\n                     platforms_data[plat_name] = merged\n                 gw_data[\"platforms\"] = platforms_data\n-            for plat in Platform:\n+            # Iterate built-in platforms plus any registered plugin platforms\n+            # so plugin authors get the same shared-key bridging (#24836).\n+            try:\n+                from hermes_cli.plugins import discover_plugins\n+                discover_plugins()  # idempotent\n+                from gateway.platform_registry import platform_registry as _pr\n+            except Exception as e:\n+                logger.debug(\"plugin discovery skipped: %s\", e)\n+                _pr = None\n+\n+            _shared_loop_targets: list = list(Platform)\n+            if _pr is not None:\n+                for _entry in _pr.plugin_entries():\n+                    try:\n+                        _plat = Platform(_entry.name)\n+                    except (ValueError, KeyError):\n+                        continue\n+                    if _plat not in _shared_loop_targets:\n+                        _shared_loop_targets.append(_plat)\n+\n+            for plat in _shared_loop_targets:\n                 if plat == Platform.LOCAL:\n                     continue\n                 platform_cfg = yaml_cfg.get(plat.value)","@@ -810,20 +848,38 @@ def load_gateway_config() -> GatewayConfig:\n                 enabled_was_explicit = \"enabled\" in platform_cfg\n                 if not bridged and not enabled_was_explicit:\n                     continue\n-                plat_data = platforms_data.setdefault(plat.value, {})\n-                if not isinstance(plat_data, dict):\n-                    plat_data = {}\n-                    platforms_data[plat.value] = plat_data\n+                plat_data, extra = _ensure_platform_extra_dict(platforms_data, plat.value)\n                 if enabled_was_explicit:\n                     plat_data[\"enabled\"] = platform_cfg[\"enabled\"]\n-                extra = plat_data.setdefault(\"extra\", {})\n-                if not isinstance(extra, dict):\n-                    extra = {}\n-                    plat_data[\"extra\"] = extra\n                 if plat == Platform.SLACK and enabled_was_explicit:\n                     extra[\"_enabled_explicit\"] = True\n                 extra.update(bridged)\n \n+            # Plugin-owned YAML→env config bridges (#24836).  See\n+            # ``PlatformEntry.apply_yaml_config_fn`` for the hook contract.\n+            # Order: shared-key loop (above) → this dispatch → legacy hardcoded\n+            # blocks (below; no-op when a hook already set their env var) →\n+            # ``_apply_env_overrides()`` after ``GatewayConfig.from_dict``.\n+            if _pr is not None:\n+                for entry in _pr.all_entries():\n+                    if entry.apply_yaml_config_fn is None:\n+                        continue\n+                    platform_cfg = yaml_cfg.get(entry.name)\n+                    if not isinstance(platform_cfg, dict):\n+                        continue\n+                    try:\n+                        seeded = entry.apply_yaml_config_fn(yaml_cfg, platform_cfg)\n+                    except Exception as e:\n+                        logger.debug(\n+                            \"apply_yaml_config_fn for %s raised: %s\",\n+                            entry.name, e,\n+                        )\n+                        continue\n+                    if not isinstance(seeded, dict) or not seeded:\n+                        continue\n+                    _, extra = _ensure_platform_extra_dict(platforms_data, entry.name)\n+                    extra.update(seeded)\n+\n             # Slack settings → env vars (env vars take precedence)\n             slack_cfg = yaml_cfg.get(\"slack\", {})\n             if isinstance(slack_cfg, dict):"]},{"file":"gateway/platform_registry.py","hunks":["@@ -119,6 +119,22 @@ class PlatformEntry:\n     # Signature: () -> Optional[dict[str, Any]]\n     env_enablement_fn: Optional[Callable[[], Optional[dict]]] = None\n \n+    # ── YAML→env config bridge ──\n+    # Optional: translate this platform's ``config.yaml`` keys into env vars\n+    # and/or seed ``PlatformConfig.extra`` directly.  Lets a plugin own its\n+    # YAML config translation instead of forcing core ``gateway/config.py``\n+    # to know every platform's schema.\n+    #\n+    # Signature: (yaml_cfg: dict, platform_cfg: dict) -> Optional[dict]\n+    # Called from ``load_gateway_config()`` after the generic shared-key loop\n+    # and before ``_apply_env_overrides``.  Mutating ``os.environ`` is allowed\n+    # (use ``not os.getenv(...)`` guards to preserve env > YAML precedence);\n+    # any returned dict is merged into ``PlatformConfig.extra``.  Exceptions\n+    # are caught and logged at debug level.\n+    # See website/docs/developer-guide/adding-platform-adapters.md for the\n+    # full contract and a worked example.\n+    apply_yaml_config_fn: Optional[Callable[[dict, dict], Optional[dict]]] = None\n+\n     # Optional: home-channel env var name for cron/notification delivery\n     # (e.g. ``\"IRC_HOME_CHANNEL\"``).  When set, ``cron.scheduler`` treats this\n     # platform as a valid ``deliver=<name>`` target and reads the env var to"]},{"file":"gateway/platforms/ADDING_A_PLATFORM.md","hunks":["@@ -21,6 +21,14 @@ status display, gateway setup, and more.\n   constructed.  Without this, env-only setups don't surface in\n   `hermes gateway status` or `get_connected_platforms()` until the SDK\n   instantiates.\n+- `apply_yaml_config_fn: (yaml_cfg, platform_cfg) -> Optional[dict]` —\n+  translate this platform's `config.yaml` keys into env vars and/or seed\n+  `PlatformConfig.extra` directly.  Lets a plugin own its YAML schema\n+  instead of growing core `gateway/config.py` boilerplate per platform.\n+  Mutating `os.environ` is allowed (use `not os.getenv(...)` guards to\n+  preserve env > YAML precedence); the returned dict is merged into\n+  `PlatformConfig.extra`.  Called during `load_gateway_config()` after\n+  the generic shared-key loop and before `_apply_env_overrides()`.\n - `cron_deliver_env_var: str` — name of the `*_HOME_CHANNEL` env var.  When\n   set, `deliver=<name>` cron jobs route to this var without editing\n   `cron/scheduler.py`'s hardcoded sets."]},{"file":"tests/gateway/test_platform_registry.py","hunks":["@@ -394,3 +394,317 @@ def test_platform_label_plugin_fallback(self):\n             assert \"LabelTest\" in label\n         finally:\n             _reg.unregister(\"labeltest\")\n+\n+\n+# ── apply_yaml_config_fn (PlatformEntry field + load_gateway_config dispatch) ──\n+\n+\n+class TestApplyYamlConfigFnField:\n+    \"\"\"The hook field itself — defaults, custom values, signature.\"\"\"\n+\n+    def test_default_is_none(self):\n+        entry = PlatformEntry(\n+            name=\"test\",\n+            label=\"Test\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+        )\n+        assert entry.apply_yaml_config_fn is None\n+\n+    def test_accepts_callable(self):\n+        def _hook(yaml_cfg, platform_cfg):\n+            return None\n+\n+        entry = PlatformEntry(\n+            name=\"test\",\n+            label=\"Test\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            apply_yaml_config_fn=_hook,\n+        )\n+        assert entry.apply_yaml_config_fn is _hook\n+        # Sanity-check the signature contract.\n+        assert entry.apply_yaml_config_fn({\"x\": 1}, {\"y\": 2}) is None\n+\n+\n+class TestApplyYamlConfigFnDispatch:\n+    \"\"\"End-to-end dispatch through load_gateway_config().\n+\n+    Each test registers a temporary PlatformEntry, writes a config.yaml in\n+    a tmp HERMES_HOME, calls load_gateway_config(), and asserts the hook\n+    was invoked correctly.  Cleanup unregisters the entry.\n+    \"\"\"\n+\n+    def _write_config(self, tmp_path, content: str):\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        (hermes_home / \"config.yaml\").write_text(content, encoding=\"utf-8\")\n+        return hermes_home\n+\n+    def _register_hook(self, name, hook_fn):\n+        from gateway.platform_registry import platform_registry as _reg\n+\n+        entry = PlatformEntry(\n+            name=name,\n+            label=name.title(),\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            source=\"plugin\",\n+            apply_yaml_config_fn=hook_fn,\n+        )\n+        _reg.register(entry)\n+        return _reg\n+\n+    def test_hook_can_mutate_environ(self, tmp_path, monkeypatch):\n+        \"\"\"A hook that mutates os.environ has its env vars set after load.\"\"\"\n+        env_var = \"MYHOOKPLAT_FLAG\"\n+        monkeypatch.delenv(env_var, raising=False)\n+\n+        def _hook(yaml_cfg, platform_cfg):\n+            if \"flag\" in platform_cfg and not os.getenv(env_var):\n+                os.environ[env_var] = str(platform_cfg[\"flag\"]).lower()\n+            return None\n+\n+        reg = self._register_hook(\"myhookplat\", _hook)\n+        try:\n+            home = self._write_config(\n+                tmp_path, \"myhookplat:\\n  flag: true\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config\n+            load_gateway_config()\n+\n+            assert os.environ.get(env_var) == \"true\"\n+        finally:\n+            reg.unregister(\"myhookplat\")\n+            os.environ.pop(env_var, None)\n+\n+    def test_hook_returned_dict_merges_into_extra(self, tmp_path, monkeypatch):\n+        \"\"\"A hook that returns a dict has it merged into PlatformConfig.extra.\"\"\"\n+\n+        def _hook(yaml_cfg, platform_cfg):\n+            return {\"seeded_key\": \"seeded_value\", \"flag\": platform_cfg.get(\"flag\")}\n+\n+        reg = self._register_hook(\"myextraplat\", _hook)\n+        try:\n+            home = self._write_config(\n+                tmp_path, \"myextraplat:\\n  flag: yes\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config\n+            cfg = load_gateway_config()\n+\n+            plat = Platform(\"myextraplat\")\n+            assert plat in cfg.platforms\n+            extra = cfg.platforms[plat].extra\n+            assert extra.get(\"seeded_key\") == \"seeded_value\"\n+            # flag value carried through from yaml_cfg arg.\n+            assert extra.get(\"flag\") is True\n+        finally:\n+            reg.unregister(\"myextraplat\")\n+\n+    def test_hook_receives_full_yaml_and_platform_subdict(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"Hook receives both the full yaml_cfg and its own platform sub-dict.\"\"\"\n+        captured: dict = {}\n+\n+        def _hook(yaml_cfg, platform_cfg):\n+            captured[\"yaml_cfg\"] = yaml_cfg\n+            captured[\"platform_cfg\"] = platform_cfg\n+            return None\n+\n+        reg = self._register_hook(\"mycaptureplat\", _hook)\n+        try:\n+            home = self._write_config(\n+                tmp_path,\n+                \"top_level_key: 1\\n\"\n+                \"mycaptureplat:\\n\"\n+                \"  inner_key: deep\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config\n+            load_gateway_config()\n+\n+            assert captured[\"yaml_cfg\"].get(\"top_level_key\") == 1\n+            assert captured[\"platform_cfg\"] == {\"inner_key\": \"deep\"}\n+        finally:\n+            reg.unregister(\"mycaptureplat\")\n+\n+    def test_hook_exception_swallowed(self, tmp_path, monkeypatch):\n+        \"\"\"A misbehaving hook never aborts load_gateway_config().\"\"\"\n+\n+        def _bad_hook(yaml_cfg, platform_cfg):\n+            raise RuntimeError(\"plugin author bug\")\n+\n+        # Also register a well-behaved hook to ensure dispatch continues\n+        # iterating after a bad one.\n+        good_called = {\"count\": 0}\n+\n+        def _good_hook(yaml_cfg, platform_cfg):\n+            good_called[\"count\"] += 1\n+            return None\n+\n+        from gateway.platform_registry import platform_registry as _reg\n+        _reg.register(PlatformEntry(\n+            name=\"mybadplat\",\n+            label=\"MyBad\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            source=\"plugin\",\n+            apply_yaml_config_fn=_bad_hook,\n+        ))\n+        _reg.register(PlatformEntry(\n+            name=\"mygoodplat\",\n+            label=\"MyGood\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            source=\"plugin\",\n+            apply_yaml_config_fn=_good_hook,\n+        ))\n+        try:\n+            home = self._write_config(\n+                tmp_path,\n+                \"mybadplat:\\n  k: v\\n\"\n+                \"mygoodplat:\\n  k: v\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            # Must not raise.\n+            from gateway.config import load_gateway_config\n+            load_gateway_config()\n+\n+            assert good_called[\"count\"] == 1\n+        finally:\n+            _reg.unregister(\"mybadplat\")\n+            _reg.unregister(\"mygoodplat\")\n+\n+    def test_hook_skipped_when_platform_section_missing(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"Hook is NOT called when the platform's YAML section is absent.\"\"\"\n+        called = {\"count\": 0}\n+\n+        def _hook(yaml_cfg, platform_cfg):\n+            called[\"count\"] += 1\n+            return None\n+\n+        reg = self._register_hook(\"myabsentplat\", _hook)\n+        try:\n+            home = self._write_config(tmp_path, \"telegram:\\n  k: v\\n\")\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config\n+            load_gateway_config()\n+\n+            assert called[\"count\"] == 0\n+        finally:\n+            reg.unregister(\"myabsentplat\")\n+\n+    def test_hook_skipped_when_platform_section_not_dict(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"Hook is NOT called when the platform's YAML section isn't a dict.\"\"\"\n+        called = {\"count\": 0}\n+\n+        def _hook(yaml_cfg, platform_cfg):\n+            called[\"count\"] += 1\n+            return None\n+\n+        reg = self._register_hook(\"mybadshapeplat\", _hook)\n+        try:\n+            home = self._write_config(\n+                tmp_path, \"mybadshapeplat: just-a-string\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config\n+            load_gateway_config()\n+\n+            assert called[\"count\"] == 0\n+        finally:\n+            reg.unregister(\"mybadshapeplat\")\n+\n+    def test_env_var_takes_precedence_when_hook_uses_getenv_guard(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"The standard `not os.getenv(...)` guard preserves env > YAML.\"\"\"\n+        env_var = \"MYPRECPLAT_FLAG\"\n+        monkeypatch.setenv(env_var, \"preexisting\")\n+\n+        def _hook(yaml_cfg, platform_cfg):\n+            if \"flag\" in platform_cfg and not os.getenv(env_var):\n+                os.environ[env_var] = str(platform_cfg[\"flag\"]).lower()\n+            return None\n+\n+        reg = self._register_hook(\"myprecplat\", _hook)\n+        try:\n+            home = self._write_config(\n+                tmp_path, \"myprecplat:\\n  flag: yaml-value\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config\n+            load_gateway_config()\n+\n+            # Pre-existing env var was NOT clobbered by the hook.\n+            assert os.environ.get(env_var) == \"preexisting\"\n+        finally:\n+            reg.unregister(\"myprecplat\")\n+            os.environ.pop(env_var, None)\n+\n+\n+class TestPluginPlatformSharedKeyBridge:\n+    \"\"\"Plugin-registered platforms get the same shared-key bridging as built-ins.\n+\n+    Without this, plugin authors using ``apply_yaml_config_fn`` would have to\n+    re-implement bridging for every common key (``unauthorized_dm_behavior``,\n+    ``notice_delivery``, ``reply_prefix``, ``require_mention``, ``dm_policy``,\n+    ``allow_from``, etc.) — defeating the hook's whole point of letting\n+    plugins focus on their *platform-specific* keys.\n+    \"\"\"\n+\n+    def _write_config(self, tmp_path, content: str):\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        (hermes_home / \"config.yaml\").write_text(content, encoding=\"utf-8\")\n+        return hermes_home\n+\n+    def test_shared_keys_bridged_for_plugin_platform(self, tmp_path, monkeypatch):\n+        \"\"\"A plugin platform's ``require_mention``/``dm_policy``/etc. flow into\n+        ``PlatformConfig.extra`` without the plugin needing its own bridge.\"\"\"\n+        from gateway.platform_registry import platform_registry as _reg\n+\n+        _reg.register(PlatformEntry(\n+            name=\"mysharedplat\",\n+            label=\"MySharedPlat\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            source=\"plugin\",\n+        ))\n+        try:\n+            home = self._write_config(\n+                tmp_path,\n+                \"mysharedplat:\\n\"\n+                \"  require_mention: true\\n\"\n+                \"  dm_policy: allow\\n\"\n+                \"  reply_prefix: \\\"→ \\\"\\n\"\n+                \"  allow_from: [\\\"alice\\\", \\\"bob\\\"]\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config, Platform\n+            cfg = load_gateway_config()\n+\n+            plat = Platform(\"mysharedplat\")\n+            assert plat in cfg.platforms\n+            extra = cfg.platforms[plat].extra\n+            assert extra.get(\"require_mention\") is True\n+            assert extra.get(\"dm_policy\") == \"allow\"\n+            assert extra.get(\"reply_prefix\") == \"→ \"\n+            assert extra.get(\"allow_from\") == [\"alice\", \"bob\"]\n+        finally:\n+            _reg.unregister(\"mysharedplat\")"]},{"file":"website/docs/developer-guide/adding-platform-adapters.md","hunks":["@@ -182,6 +182,7 @@ When you call `ctx.register_platform()`, the following integration points are ha\n | Connected platform validation | Registry `validate_config()` called |\n | User authorization | `allowed_users_env` / `allow_all_env` checked |\n | Env-only auto-enable | `env_enablement_fn` seeds `PlatformConfig.extra` + `home_channel` |\n+| YAML config bridge | `apply_yaml_config_fn` translates `config.yaml` keys into env vars / extras |\n | Cron delivery | `cron_deliver_env_var` makes `deliver=<name>` work |\n | `hermes config` UI entries | `requires_env` / `optional_env` in `plugin.yaml` auto-populate |\n | send_message tool | Routes through live gateway adapter |","@@ -239,6 +240,46 @@ def register(ctx):\n     )\n ```\n \n+\n+## YAML→env Config Bridge\n+\n+Some users prefer setting `config.yaml` keys (`my_platform.require_mention`, `my_platform.allowed_channels`, etc.) over env vars. The `apply_yaml_config_fn` hook lets your plugin own this translation instead of forcing core `gateway/config.py` to know your platform's YAML schema.\n+\n+```python\n+import os\n+\n+def _apply_yaml_config(yaml_cfg: dict, platform_cfg: dict) -> dict | None:\n+    \"\"\"Translate config.yaml `my_platform:` keys into env vars / extras.\n+\n+    yaml_cfg     — the full top-level parsed config.yaml dict\n+    platform_cfg — the platform's own sub-dict (yaml_cfg.get(\"my_platform\", {}))\n+\n+    May mutate os.environ directly (use `not os.getenv(...)` guards to\n+    preserve env > YAML precedence) and/or return a dict to merge into\n+    PlatformConfig.extra. Return None or {} for no extras.\n+    \"\"\"\n+    if \"require_mention\" in platform_cfg and not os.getenv(\"MY_PLATFORM_REQUIRE_MENTION\"):\n+        os.environ[\"MY_PLATFORM_REQUIRE_MENTION\"] = str(platform_cfg[\"require_mention\"]).lower()\n+    allowed = platform_cfg.get(\"allowed_channels\")\n+    if allowed is not None and not os.getenv(\"MY_PLATFORM_ALLOWED_CHANNELS\"):\n+        if isinstance(allowed, list):\n+            allowed = \",\".join(str(v) for v in allowed)\n+        os.environ[\"MY_PLATFORM_ALLOWED_CHANNELS\"] = str(allowed)\n+    return None  # nothing extra to merge into PlatformConfig.extra\n+\n+def register(ctx):\n+    ctx.register_platform(\n+        name=\"my_platform\",\n+        ...,\n+        apply_yaml_config_fn=_apply_yaml_config,\n+    )\n+```\n+\n+The hook is invoked during `load_gateway_config()` after the generic shared-key loop (which handles common keys like `unauthorized_dm_behavior`, `notice_delivery`, `reply_prefix`, `require_mention`, etc.) and before `_apply_env_overrides()`, so your plugin only needs to bridge **platform-specific** keys.\n+\n+Exceptions raised by the hook are swallowed and logged at debug level — a misbehaving plugin never aborts gateway config load.\n+\n+\n ## Cron Delivery\n \n To let `deliver=my_platform` cron jobs route to a configured home channel, set `cron_deliver_env_var` to the env var name that holds the default chat/room/channel ID:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}