mcp-<server> is the registered toolset name and the bare server name its
alias, not the other way round. The stale-toolset test now stubs one
portable MCP server so the prefixed-name rule for plugin-provided servers
is covered too (removing it turns the test red).
MCP tools register under the toolset name mcp-<server>, an alias that only
exists after MCP discovery. `hermes config migrate`, `hermes update` and
`hermes config check` run without discovery, so a platform_toolsets entry
such as mcp-codegraph for a configured server was reported as an unknown
toolset with a 'hermes-cli' hint (#78102).
saved_toolset_resolver now accepts the prefixed form of every configured
and plugin-provided (portable) MCP server. An mcp-<name> with no such
server is still reported.
Rule from #106715 by @a692570, adapted onto the shared resolver that
landed in #127782.
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.
Follow-up to the config check diagnostics from #127392.
- Saved platform_toolsets are validated by one resolver,
toolset_validation.saved_toolset_resolver, shared with
`hermes config migrate`. The two commands previously disagreed: migrate
flagged MCP server names and plugin-platform bundles (even suggesting
'hermes-teams' for 'hermes-teams'), while config check accepted them.
- A configured MCP server is known even when disabled (enabled: false);
it is inactive, not a typo.
- known_plugin_toolsets is no longer treated as proof that a toolset
exists. It records what a platform was offered at save time, so it hid
toolsets of uninstalled plugins, the stale case this check is for.
- Plugin-provided names are consulted only for names nothing else
explains, so a clean config no longer runs plugin discovery.
- A malformed mcp_servers (a list) no longer crashes the command.
- The top-level `toolsets` check is dropped: the CLI resolves its
toolsets from --toolsets or platform_toolsets, and reads the top-level
key only for the legacy kanban opt-in.
- Disabled platforms are read with the runtime's plugins.disabled reader,
so a non-list value (which the runtime ignores) is no longer reported
as disabling a platform. Required credentials come from
_platform_manifest_env_entries, which now takes optional=False.
- Tests use a synthetic platform manifest instead of the real Telegram
one, and assert config check and migrate report the same names.
#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).
validate_platform_toolsets() accumulated a single valid_count across every
platform, so the "zero valid toolsets" safety net was suppressed as soon as any
one platform carried a valid toolset. A platform wiped to [] — the active one,
typically cli — therefore produced no warning at all.
resolve_enabled_toolsets() honours that empty list verbatim ([] is a list, so
the platform-default fallback is skipped), leaving the agent with zero tool
schemas. The model then has nothing to call and emits the tool call as
assistant text with finish_reason=stop: no error, no warning, no log entry.
That is the silent-failure mode this module was written to prevent (#38798).
Note the asymmetry this leaves intact: a malformed *string* value is not a list,
so it falls back to the platform default and fails open (#78103); an empty list
fails closed. The fail-closed resolution is deliberate (the explicit_empty_
selection contract in tools_config.py, and #82010 wants it persistable), so this
only adds the missing warning and does not change resolution semantics.
Fixes#89050
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A config migration (or hand-edit) that leaves an invalid toolset name in
`platform_toolsets` — e.g. the #38798 corruption that rewrote `hermes-cli` to
the non-existent `hermes` — silently disabled all affected tools:
resolve_toolset() returns [] for an unknown name, so the agent quietly lost its
tools with no error, warning, or log entry and degraded to text-only replies.
Surface it loudly at two points:
- After migration (migrate_config): validate platform_toolsets and record/print
a warning per unknown name, with a `hermes-<platform>` suggestion when that
would have been valid (the exact #38798 shape).
- At runtime (_get_platform_tools): if a platform was explicitly configured but
every toolset name is invalid, log a warning when tools are resolved for a
session — so an ALREADY-corrupted config is caught at startup, not only on the
next `hermes update`.
Logic lives in a new pure, side-effect-free helper (toolset_validation.py) with
validate_toolset injected, so it is unit-testable without the tool registry.
Note: the original v25→v26 migration that caused the corruption no longer
exists (config format is now v30; no migration step rewrites toolset names).
This change is the durable defense against the silent-failure mode regardless
of cause, matching the issue's "Expected: log a warning".
Salvaged from #39207 by @lEWFkRAD (authorship preserved via cherry-pick).
Tests: 9 helper cases (incl. the #38798 corruption shape, mixed valid/invalid,
zero-tools state, non-dict/scalar/non-string) + a runtime caplog test — both the
helper warning and the runtime guard mutation-verified to fail without the fix.
Closes#38798. Supersedes #39581 (prevent-in-v25→v26 — that path is gone),
#41006 / #40208 (repair-migration for already-corrupted configs).