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.
This commit is contained in:
Teknium 2026-07-09 19:57:54 -07:00 committed by GitHub
parent 881a9520e3
commit 4c03032a24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 3 deletions

View file

@ -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):

View file

@ -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()