mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-24 05:41:40 +00:00
fix(cli): harden skin yaml parsing for invalid section types
This commit is contained in:
parent
8f19078c6a
commit
5f234d4057
2 changed files with 57 additions and 5 deletions
|
|
@ -666,25 +666,46 @@ def _load_skin_from_yaml(path: Path) -> Optional[Dict[str, Any]]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _mapping_or_empty(value: Any, *, section: str, skin_name: str) -> Dict[str, Any]:
|
||||||
|
"""Return a mapping value or an empty dict when the section type is invalid."""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
if value is None:
|
||||||
|
return {}
|
||||||
|
logger.warning(
|
||||||
|
"Skin '%s' has invalid '%s' section type (%s); ignoring section",
|
||||||
|
skin_name,
|
||||||
|
section,
|
||||||
|
type(value).__name__,
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _build_skin_config(data: Dict[str, Any]) -> SkinConfig:
|
def _build_skin_config(data: Dict[str, Any]) -> SkinConfig:
|
||||||
"""Build a SkinConfig from a raw dict (built-in or loaded from YAML)."""
|
"""Build a SkinConfig from a raw dict (built-in or loaded from YAML)."""
|
||||||
# Start with default values as base for missing keys
|
# Start with default values as base for missing keys
|
||||||
default = _BUILTIN_SKINS["default"]
|
default = _BUILTIN_SKINS["default"]
|
||||||
|
skin_name = str(data.get("name", "unknown"))
|
||||||
|
color_overrides = _mapping_or_empty(data.get("colors"), section="colors", skin_name=skin_name)
|
||||||
|
spinner_overrides = _mapping_or_empty(data.get("spinner"), section="spinner", skin_name=skin_name)
|
||||||
|
branding_overrides = _mapping_or_empty(data.get("branding"), section="branding", skin_name=skin_name)
|
||||||
|
emoji_overrides = _mapping_or_empty(data.get("tool_emojis"), section="tool_emojis", skin_name=skin_name)
|
||||||
|
|
||||||
colors = dict(default.get("colors", {}))
|
colors = dict(default.get("colors", {}))
|
||||||
colors.update(data.get("colors", {}))
|
colors.update(color_overrides)
|
||||||
spinner = dict(default.get("spinner", {}))
|
spinner = dict(default.get("spinner", {}))
|
||||||
spinner.update(data.get("spinner", {}))
|
spinner.update(spinner_overrides)
|
||||||
branding = dict(default.get("branding", {}))
|
branding = dict(default.get("branding", {}))
|
||||||
branding.update(data.get("branding", {}))
|
branding.update(branding_overrides)
|
||||||
|
|
||||||
return SkinConfig(
|
return SkinConfig(
|
||||||
name=data.get("name", "unknown"),
|
name=skin_name,
|
||||||
description=data.get("description", ""),
|
description=data.get("description", ""),
|
||||||
colors=colors,
|
colors=colors,
|
||||||
spinner=spinner,
|
spinner=spinner,
|
||||||
branding=branding,
|
branding=branding,
|
||||||
tool_prefix=data.get("tool_prefix", default.get("tool_prefix", "┊")),
|
tool_prefix=data.get("tool_prefix", default.get("tool_prefix", "┊")),
|
||||||
tool_emojis=data.get("tool_emojis", {}),
|
tool_emojis=emoji_overrides,
|
||||||
banner_logo=data.get("banner_logo", ""),
|
banner_logo=data.get("banner_logo", ""),
|
||||||
banner_hero=data.get("banner_hero", ""),
|
banner_hero=data.get("banner_hero", ""),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,37 @@ class TestUserSkins:
|
||||||
# Should inherit defaults for unspecified colors
|
# Should inherit defaults for unspecified colors
|
||||||
assert skin.get_color("banner_border") == "#CD7F32" # from default
|
assert skin.get_color("banner_border") == "#CD7F32" # from default
|
||||||
|
|
||||||
|
def test_load_user_skin_invalid_section_types_fall_back_to_defaults(self, tmp_path, monkeypatch):
|
||||||
|
from hermes_cli.skin_engine import load_skin
|
||||||
|
|
||||||
|
skins_dir = tmp_path / "skins"
|
||||||
|
skins_dir.mkdir()
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
(skins_dir / "broken.yaml").write_text(
|
||||||
|
yaml.dump(
|
||||||
|
{
|
||||||
|
"name": "broken",
|
||||||
|
"colors": ["not", "a", "mapping"],
|
||||||
|
"spinner": "invalid",
|
||||||
|
"branding": ["also", "invalid"],
|
||||||
|
"tool_emojis": ["invalid"],
|
||||||
|
"tool_prefix": "!",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("hermes_cli.skin_engine._skins_dir", lambda: skins_dir)
|
||||||
|
|
||||||
|
skin = load_skin("broken")
|
||||||
|
|
||||||
|
assert skin.name == "broken"
|
||||||
|
assert skin.get_color("banner_title") == "#FFD700"
|
||||||
|
assert skin.get_branding("agent_name") == "Hermes Agent"
|
||||||
|
assert skin.get_spinner_list("waiting_faces") == []
|
||||||
|
assert skin.tool_emojis == {}
|
||||||
|
assert skin.tool_prefix == "!"
|
||||||
|
|
||||||
def test_list_skins_includes_user_skins(self, tmp_path, monkeypatch):
|
def test_list_skins_includes_user_skins(self, tmp_path, monkeypatch):
|
||||||
from hermes_cli.skin_engine import list_skins
|
from hermes_cli.skin_engine import list_skins
|
||||||
skins_dir = tmp_path / "skins"
|
skins_dir = tmp_path / "skins"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue