#116691 taught the runtime (`_get_platform_tools`) to read the `'["browser", "terminal"]'` string an older `hermes config set` stored as the user's real toolset selection, but the two other readers of the section kept the old `isinstance(raw, list)` gate: - `validate_platform_toolsets` (hermes doctor / startup warnings) reported "invalid toolset value ... falling back to 'hermes-cli'" for a value the runtime resolves to browser+terminal — a false warning. - `plugins_cmd._toggle_plugin_toolset` skipped the string entry, so `hermes plugins enable <p>` never reached that platform. - `nous_subscription._toolset_enabled` treated it as an empty list. Move the list-literal parse into one function, `toolset_validation.parse_platform_toolsets_value`, and route all four readers through it; the plugin toggle re-saves the entry as a real list so the string never persists past the next write. `_coerce_platform_toolsets_value` keeps its once-per-platform warning for values that do not parse. Why one parser: three readers with three notions of "configured" is exactly how #115866 went unnoticed. Follow-up to #116691 (independent review finding).
114 lines
5.2 KiB
Python
114 lines
5.2 KiB
Python
"""Validation for the ``platform_toolsets`` config section."""
|
|
|
|
import ast
|
|
from typing import Callable, List, Optional
|
|
|
|
from hermes_cli.platforms import PLATFORMS
|
|
from hermes_cli.toolset_scope import toolset_allowed_for_platform
|
|
|
|
_NO_TOOLS = "the agent will have no tools on this platform. Run `hermes tools` to reconfigure."
|
|
|
|
|
|
def parse_platform_toolsets_value(value: object) -> Optional[List[str]]:
|
|
"""The toolset list a saved ``platform_toolsets.<platform>`` value encodes, or None.
|
|
|
|
Older ``hermes config set`` builds stored a bare ``[...]`` argument as a plain string, so an
|
|
explicit selection like ``'["browser", "terminal"]'`` parses as str, not list (#115866).
|
|
Every reader and writer of the section goes through this one parser so the runtime,
|
|
``hermes doctor`` and ``hermes plugins enable`` agree on what the user configured. Any other
|
|
shape (null, scalar, unparseable string) is None: the caller decides how to report it.
|
|
"""
|
|
if isinstance(value, list):
|
|
return value
|
|
if isinstance(value, str) and value.strip().startswith("["):
|
|
try:
|
|
parsed = ast.literal_eval(value.strip())
|
|
except (ValueError, SyntaxError):
|
|
return None
|
|
if isinstance(parsed, list):
|
|
return [str(item) for item in parsed]
|
|
return None
|
|
|
|
|
|
def _platform_default_toolset(platform: object) -> str:
|
|
info = PLATFORMS.get(platform)
|
|
return info.default_toolset if info is not None else f"hermes-{platform}"
|
|
|
|
|
|
def _platform_default_is_valid(
|
|
platform: object, default_toolset: str, is_valid_toolset: Callable[[str], bool],
|
|
is_allowed_for_platform: Callable[[str, str], bool]) -> bool:
|
|
if is_valid_toolset(default_toolset) and is_allowed_for_platform(default_toolset, str(platform)):
|
|
return True
|
|
# Dynamic plugin platforms are resolved by toolsets.resolve_toolset() even though their synthesized
|
|
# hermes-<platform> name is not in TOOLSETS.
|
|
try:
|
|
from gateway.platform_registry import platform_registry
|
|
|
|
return platform_registry.is_registered(platform)
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def validate_platform_toolsets(
|
|
platform_toolsets: object, is_valid_toolset: Callable[[str], bool],
|
|
is_allowed_for_platform: Callable[[str, str], bool] = toolset_allowed_for_platform,
|
|
) -> List[str]:
|
|
"""Return human-readable warnings for a ``platform_toolsets`` mapping.
|
|
Reports: a toolset name ``is_valid_toolset`` rejects (suggesting ``hermes-<platform>`` when that
|
|
would have been valid); a non-empty mapping resolving to zero valid toolsets (agent would start with
|
|
no tools); a platform with no valid toolsets, checked per-platform because the global net is
|
|
suppressed once any platform is valid; and non-list platform values, which fall back to the platform
|
|
default. ``is_valid_toolset`` is injected so this does no registry imports or I/O."""
|
|
warnings: List[str] = []
|
|
if not isinstance(platform_toolsets, dict) or not platform_toolsets:
|
|
return warnings
|
|
|
|
valid_count = 0
|
|
for platform, raw in platform_toolsets.items():
|
|
default = _platform_default_toolset(platform)
|
|
default_valid = _platform_default_is_valid(platform, default, is_valid_toolset, is_allowed_for_platform)
|
|
platform_valid_count = 0
|
|
toolsets = parse_platform_toolsets_value(raw)
|
|
if toolsets is None:
|
|
if default_valid:
|
|
valid_count += 1
|
|
platform_valid_count += 1
|
|
fallback_detail = f"falling back to '{default}'" if default_valid else f"falling back to unknown default '{default}'"
|
|
if raw is None:
|
|
value_detail = "a null toolset value"
|
|
elif isinstance(raw, str):
|
|
value_detail = f"invalid toolset value '{raw}'"
|
|
else:
|
|
value_detail = f"invalid {type(raw).__name__} toolset value"
|
|
warnings.append(
|
|
f"platform '{platform}' has {value_detail} — "
|
|
f"{fallback_detail}. Run `hermes tools` to configure explicitly.")
|
|
if platform_valid_count == 0:
|
|
warnings.append(f"platform '{platform}' has no valid toolsets configured — {_NO_TOOLS}")
|
|
continue
|
|
|
|
for name in toolsets:
|
|
if not isinstance(name, str) or not name:
|
|
continue
|
|
if not is_valid_toolset(name):
|
|
hint = f" — did you mean '{default}'?" if default_valid else ""
|
|
warnings.append(f"platform '{platform}' references unknown toolset '{name}'{hint}")
|
|
elif is_allowed_for_platform(name, str(platform)):
|
|
valid_count += 1
|
|
platform_valid_count += 1
|
|
else:
|
|
warnings.append(
|
|
f"platform '{platform}' references toolset '{name}' which is not available on this platform"
|
|
)
|
|
|
|
if platform_valid_count == 0:
|
|
reason = "is configured with an empty toolset list" if not toolsets else "has no valid toolsets configured"
|
|
warnings.append(f"platform '{platform}' {reason} — {_NO_TOOLS}")
|
|
|
|
if valid_count == 0:
|
|
warnings.append(
|
|
"platform_toolsets resolves to zero valid toolsets — the agent will "
|
|
"have no tools. Run `hermes tools` to reconfigure.")
|
|
return warnings
|