From 4c03032a240a297ce6603bf352cf76d88f59d75f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:57:54 -0700 Subject: [PATCH] fix(cli): normalize malformed skills config in get_disabled_skills (#61797) skills: null crashed with AttributeError, and a bare scalar disabled: my-skill was split into a set of characters. Both now normalize the same way agent.skill_utils._normalize_string_set does: null -> empty set, scalar -> single-item set. Non-dict skills sections are ignored. Closes #13026. --- hermes_cli/skills_config.py | 25 ++++++++++++++++++++--- tests/hermes_cli/test_skills_config.py | 28 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/hermes_cli/skills_config.py b/hermes_cli/skills_config.py index 840aca3880f..720b9eb3f04 100644 --- a/hermes_cli/skills_config.py +++ b/hermes_cli/skills_config.py @@ -24,6 +24,23 @@ PLATFORMS = {k: info.label for k, info in _PLATFORMS.items() if k != "api_server # ─── Config Helpers ─────────────────────────────────────────────────────────── +def _normalize_skill_names(values) -> Set[str]: + """Normalize a config value into a set of skill names. + + Mirrors ``agent.skill_utils._normalize_string_set``: ``None`` (YAML null) + means empty, a bare scalar (``disabled: my-skill``) means a single-item + list — NOT a set of its characters (#13026). + """ + if values is None: + return set() + if isinstance(values, str): + values = [values] + try: + return {str(v).strip() for v in values if str(v).strip()} + except TypeError: + return set() + + def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str]: """Return disabled skill names: the global list unioned with the platform-specific list when a platform is given. @@ -32,14 +49,16 @@ def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str platform list adds to the global list rather than replacing it. This mirrors ``agent.skill_utils.get_disabled_skill_names``. """ - skills_cfg = config.get("skills", {}) - global_disabled = set(skills_cfg.get("disabled", [])) + skills_cfg = config.get("skills") or {} + if not isinstance(skills_cfg, dict): + return set() + global_disabled = _normalize_skill_names(skills_cfg.get("disabled")) if platform is None: return global_disabled platform_disabled = cfg_get(skills_cfg, "platform_disabled", platform) if platform_disabled is None: return global_disabled - return global_disabled | set(platform_disabled) + return global_disabled | _normalize_skill_names(platform_disabled) def save_disabled_skills(config: dict, disabled: Set[str], platform: Optional[str] = None): diff --git a/tests/hermes_cli/test_skills_config.py b/tests/hermes_cli/test_skills_config.py index 8fbb063f620..31365fcf227 100644 --- a/tests/hermes_cli/test_skills_config.py +++ b/tests/hermes_cli/test_skills_config.py @@ -46,6 +46,34 @@ class TestGetDisabledSkills: from hermes_cli.skills_config import get_disabled_skills assert get_disabled_skills({"other": "value"}) == set() + def test_null_skills_section(self): + """``skills:`` with no value (YAML null) must not crash (#13026).""" + from hermes_cli.skills_config import get_disabled_skills + assert get_disabled_skills({"skills": None}) == set() + assert get_disabled_skills({"skills": None}, platform="telegram") == set() + + def test_null_disabled_key(self): + from hermes_cli.skills_config import get_disabled_skills + assert get_disabled_skills({"skills": {"disabled": None}}) == set() + + def test_scalar_disabled_is_single_skill_not_characters(self): + """``disabled: my-skill`` (bare scalar) is one skill name, not a + set of its characters (#13026).""" + from hermes_cli.skills_config import get_disabled_skills + assert get_disabled_skills({"skills": {"disabled": "my-skill"}}) == {"my-skill"} + + def test_scalar_platform_disabled(self): + from hermes_cli.skills_config import get_disabled_skills + config = {"skills": { + "disabled": ["global-skill"], + "platform_disabled": {"telegram": "tg-skill"}, + }} + assert get_disabled_skills(config, platform="telegram") == {"global-skill", "tg-skill"} + + def test_non_dict_skills_section(self): + from hermes_cli.skills_config import get_disabled_skills + assert get_disabled_skills({"skills": "oops"}) == set() + def test_empty_disabled_list(self): from hermes_cli.skills_config import get_disabled_skills assert get_disabled_skills({"skills": {"disabled": []}}) == set()