fix: honor JSON-array string forms for skills.disabled and agent.disabled_toolsets

`hermes config set` and JSON-mode editor saves store lists as quoted
strings (e.g. '["skill-a","skill-b"]' or "['memory']"). Both disable
filters treated such a string as a single name, so curated disable
lists silently filtered nothing with zero diagnostics.

Add parse_config_string_list() in agent.skill_utils and use it in
_normalize_string_set (skills.disabled / platform_disabled) and at
every agent.disabled_toolsets read site: tools_config resolve +
reconcile, CLI, gateway agent construction (both sites), cron
scheduler, and prompt_size. A scalar string still names a single
entry (#13026); malformed JSON falls back to the single-name
behavior instead of raising.

Fixes #86661
This commit is contained in:
Merge_Conflict - Pasi
2026-08-15 09:23:30 +00:00
committed by Teknium
parent bd0586e062
commit 309cf2c5e2
8 changed files with 161 additions and 16 deletions
+28 -5
View File
@@ -5,6 +5,7 @@ heavy dependency chain. It is safe to import at module level without triggering
tool registration or provider resolution.
"""
import ast
import logging
import os
import re
@@ -471,12 +472,34 @@ def get_disabled_skill_names(platform: str | None = None) -> Set[str]:
return global_disabled
def parse_config_string_list(value) -> List[str]:
"""Normalize a config value that may hold a JSON-array string into a list.
``hermes config set`` and JSON-mode editor saves store lists as quoted
JSON strings (``'["a","b"]'`` or the Python-literal ``"['a']"``). Treating
such a string as a single name makes a curated disabled list silently
filter nothing (#86661); parsing it restores the intended list. A scalar
string still means one name (#13026).
"""
if value is None:
return []
if isinstance(value, str):
stripped = value.strip()
if stripped.startswith("["):
try:
parsed = ast.literal_eval(stripped)
except (ValueError, SyntaxError):
parsed = None
if isinstance(parsed, list):
return [str(item) for item in parsed]
return [value]
if isinstance(value, (list, tuple, set, frozenset)):
return [str(item) for item in value]
return []
def _normalize_string_set(values) -> Set[str]:
if values is None:
return set()
if isinstance(values, str):
values = [values]
return {str(v).strip() for v in values if str(v).strip()}
return {name.strip() for name in parse_config_string_list(values) if name.strip()}
# ── External skills directories ──────────────────────────────────────────