Files
hermes-agent/hermes_cli/toolset_validation.py
kshitijk4poor 16c59d0e78 perf(cli): scan platform manifests only for names the cheap checks cannot place
saved_toolset_resolver built the hermes-<platform> bundle set eagerly, so
every migrate_config run (hermes update, config migrate, doctor) and every
config check paid a full bundled+user manifest scan even with no saved
platform_toolsets. The scan is now cached and lazy, like the plugin lookup.
The stale-toolset test stubs the plugin lookups so host-installed plugins
cannot change its verdict.
2026-09-29 20:34:45 +05:30

144 lines
6.6 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 saved_toolset_resolver(config: dict) -> Callable[[str], bool]:
"""``is_valid_toolset`` for a saved ``platform_toolsets`` list, mirroring what
``tools_config._get_platform_tools`` lets through: registered toolsets, any configured MCP server
(a disabled one is inactive, not a typo), ``hermes-<platform>`` plugin-platform bundles and the
``no_mcp`` sentinel. The manifest scan and the plugin lookup (which may run plugin discovery)
happen only for a name the cheaper checks cannot place."""
from functools import cache
from toolsets import validate_toolset
mcp_servers = config.get("mcp_servers")
known = {str(name) for name in mcp_servers} if isinstance(mcp_servers, dict) else set()
known.add("no_mcp")
@cache
def platform_bundles() -> frozenset:
from hermes_cli.config import _platform_plugin_manifests
return frozenset(f"hermes-{name}" for name, _manifest in _platform_plugin_manifests())
@cache
def plugin_names() -> frozenset:
from hermes_cli.plugins import get_plugin_toolset_keys_nowait, get_portable_mcp_server_names_nowait
return frozenset(get_plugin_toolset_keys_nowait() | get_portable_mcp_server_names_nowait())
return lambda name: (validate_toolset(name) or name in known
or name in platform_bundles() or name in plugin_names())
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