mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
auto populate flux3 in tools for nous portal users
This commit is contained in:
parent
524ab53994
commit
97c6a183af
3 changed files with 180 additions and 0 deletions
|
|
@ -1853,6 +1853,7 @@ _EXTRA_KNOWN_ROOT_KEYS = {
|
|||
"smart_model_routing", # written by the setup wizard (hermes_cli/setup.py)
|
||||
"platform_toolsets", # written by the setup wizard (hermes_cli/setup.py)
|
||||
"known_plugin_toolsets", # written/read by hermes_cli/tools_config.py toolset-save flow
|
||||
"known_builtin_toolsets", # ditto — which builtin toolsets a platform's checklist has offered
|
||||
"session_reset", # top-level form read by gateway/config.py + setup
|
||||
"group_sessions_per_user", # top-level form bridged by gateway/config.py
|
||||
"thread_sessions_per_user", # top-level form bridged by gateway/config.py
|
||||
|
|
|
|||
|
|
@ -2132,6 +2132,66 @@ def _exempt_explicit_platform_native(
|
|||
default_off.discard(ts)
|
||||
|
||||
|
||||
#: Toolsets young enough that absence from a saved ``platform_toolsets`` list
|
||||
#: means "never offered" rather than "declined".
|
||||
#:
|
||||
#: Saving ``hermes tools`` (or one toggle in the desktop Toolsets UI) replaces
|
||||
#: a platform's composite with a frozen explicit list, and nothing ever adds to
|
||||
#: that list — so a toolset shipped afterwards stays off forever for anyone who
|
||||
#: has touched the picker, while everyone still on ``[hermes-cli]`` inherits it
|
||||
#: on upgrade. Listing it here restores that parity.
|
||||
#:
|
||||
#: MUST ship in the same release as the toolset it names, and be emptied in the
|
||||
#: next one. The inference only holds while no released build has put the
|
||||
#: toolset on a checklist: once one has, a user who unchecks it writes a config
|
||||
#: byte-identical to one saved before the toolset existed (the record below is
|
||||
#: only written from that point on), and this rule turns their opt-out back on.
|
||||
#: Landing late — or leaving an entry here for a second release — converts a
|
||||
#: back-fill into a stuck checkbox.
|
||||
#:
|
||||
#: Not gated on a Nous subscription here: the six ``bfl_flux3_*`` tools carry
|
||||
#: ``check_fn=check_bfl_requirements`` (logged in AND paid), so an enabled
|
||||
#: toolset still ships zero schemas to a user without paid portal access — the
|
||||
#: same split Home Assistant uses. Probing the portal from this path would put
|
||||
#: a network call on every CLI start, gateway session and cron tick.
|
||||
_RECENTLY_SHIPPED_TOOLSETS = frozenset({"bfl"})
|
||||
|
||||
|
||||
def _enable_recently_shipped_toolsets(
|
||||
enabled_toolsets: Set[str], config: dict, platform: str
|
||||
) -> None:
|
||||
"""Turn on toolsets that shipped after this platform's saved list.
|
||||
|
||||
Either way of saying no outlives this: unchecking in ``hermes tools``
|
||||
records the toolset in ``known_builtin_toolsets`` so it reads as declined
|
||||
from then on, and ``agent.disabled_toolsets`` is subtracted after every
|
||||
rule in :func:`_get_platform_tools`. Mutates ``enabled_toolsets`` in place.
|
||||
"""
|
||||
from toolsets import resolve_toolset
|
||||
|
||||
offered = (config.get("known_builtin_toolsets") or {}).get(platform)
|
||||
declined = {str(ts) for ts in offered} if isinstance(offered, list) else set()
|
||||
|
||||
plat_info = PLATFORMS.get(platform)
|
||||
default_ts = plat_info["default_toolset"] if plat_info else f"hermes-{platform}"
|
||||
composite_tools = None
|
||||
|
||||
for ts_key in sorted(_RECENTLY_SHIPPED_TOOLSETS):
|
||||
if ts_key in enabled_toolsets or ts_key in declined:
|
||||
continue
|
||||
if not _toolset_allowed_for_platform(ts_key, platform):
|
||||
continue
|
||||
# Parity is the whole justification, so only enable the toolset where
|
||||
# staying on the composite would have enabled it anyway. Deliberately
|
||||
# narrow composites (hermes-acp, hermes-webhook) stay narrow.
|
||||
ts_tools = set(resolve_toolset(ts_key, include_registry=False))
|
||||
if composite_tools is None:
|
||||
composite_tools = set(resolve_toolset(default_ts))
|
||||
if not ts_tools or not ts_tools.issubset(composite_tools):
|
||||
continue
|
||||
enabled_toolsets.add(ts_key)
|
||||
|
||||
|
||||
def _get_platform_tools(
|
||||
config: dict,
|
||||
platform: str,
|
||||
|
|
@ -2217,6 +2277,8 @@ def _get_platform_tools(
|
|||
expanded -= default_off
|
||||
|
||||
enabled_toolsets |= expanded
|
||||
|
||||
_enable_recently_shipped_toolsets(enabled_toolsets, config, platform)
|
||||
else:
|
||||
# No explicit config — fall back to resolving composite toolset names
|
||||
# (e.g. "hermes-cli") to individual tool names and reverse-mapping.
|
||||
|
|
@ -2484,6 +2546,17 @@ def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[
|
|||
config["known_plugin_toolsets"] = {}
|
||||
config["known_plugin_toolsets"][platform] = sorted(plugin_keys)
|
||||
|
||||
# Same record for builtin toolsets: which ones this platform's checklist
|
||||
# has actually put in front of the user. Without it, a toolset the user
|
||||
# unchecks here is indistinguishable from one that shipped after they
|
||||
# saved, and _enable_recently_shipped_toolsets would turn it straight back
|
||||
# on. Recorded from the full catalog, since that is what the picker showed.
|
||||
if not isinstance(config.get("known_builtin_toolsets"), dict):
|
||||
config["known_builtin_toolsets"] = {}
|
||||
config["known_builtin_toolsets"][platform] = sorted(
|
||||
ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS
|
||||
)
|
||||
|
||||
# Reconcile with agent.disabled_toolsets. _get_platform_tools() applies
|
||||
# that list as a final override AFTER reading platform_toolsets.<platform>,
|
||||
# so a toolset listed there stays permanently OFF no matter what this
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import pytest
|
|||
from hermes_cli.nous_account import NousPortalAccountInfo
|
||||
from hermes_cli.tools_config import (
|
||||
_DEFAULT_OFF_TOOLSETS,
|
||||
_RECENTLY_SHIPPED_TOOLSETS,
|
||||
_apply_toolset_change,
|
||||
_checklist_toolset_keys,
|
||||
_configure_provider,
|
||||
|
|
@ -553,3 +554,108 @@ def _fake_features(*, logged_in: bool, paid: bool = True):
|
|||
# ("browserbase") only the CLI, and camofox its npm package.
|
||||
|
||||
|
||||
# ── Toolsets that shipped after a platform's last `hermes tools` save ────────
|
||||
#
|
||||
# Saving the picker (or one toggle in the desktop Toolsets UI) replaces a
|
||||
# platform's composite (``[hermes-cli]``) with a frozen explicit list, and
|
||||
# nothing ever adds to that list — so a toolset shipped later stays off
|
||||
# forever, while everyone still on the composite inherits it on upgrade.
|
||||
# ``_RECENTLY_SHIPPED_TOOLSETS`` closes that gap for toolsets new enough that
|
||||
# absence from a saved list cannot mean the user declined them.
|
||||
#
|
||||
# Every assertion here is a subset test against that set, which passes
|
||||
# vacuously once it empties out — and empty is the steady state between
|
||||
# releases. Skip loudly rather than going quietly green.
|
||||
_requires_recently_shipped = pytest.mark.skipif(
|
||||
not _RECENTLY_SHIPPED_TOOLSETS,
|
||||
reason="no toolset is currently inside its first release",
|
||||
)
|
||||
|
||||
|
||||
def _saved_list_from_before(platform="cli"):
|
||||
"""A saved explicit list as it looked before the new toolsets existed."""
|
||||
from hermes_cli.tools_config import (
|
||||
_CONFIG_ONLY_TOOLSETS,
|
||||
_toolset_allowed_for_platform,
|
||||
)
|
||||
|
||||
return {
|
||||
"platform_toolsets": {
|
||||
platform: sorted(
|
||||
ts_key
|
||||
for ts_key, _, _ in CONFIGURABLE_TOOLSETS
|
||||
if ts_key not in _RECENTLY_SHIPPED_TOOLSETS
|
||||
and ts_key not in _DEFAULT_OFF_TOOLSETS
|
||||
and ts_key not in _CONFIG_ONLY_TOOLSETS
|
||||
and _toolset_allowed_for_platform(ts_key, platform)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@_requires_recently_shipped
|
||||
def test_saved_list_gains_toolsets_that_shipped_after_it_was_written():
|
||||
"""The bug: a frozen list never gained bfl, so composite users got Nous
|
||||
Portal video generation on upgrade and picker users silently did not."""
|
||||
on_composite = _get_platform_tools(
|
||||
{"platform_toolsets": {"cli": ["hermes-cli"]}},
|
||||
"cli",
|
||||
include_default_mcp_servers=False,
|
||||
)
|
||||
on_saved_list = _get_platform_tools(
|
||||
_saved_list_from_before(), "cli", include_default_mcp_servers=False
|
||||
)
|
||||
|
||||
assert _RECENTLY_SHIPPED_TOOLSETS <= (on_composite & on_saved_list)
|
||||
|
||||
|
||||
@_requires_recently_shipped
|
||||
def test_unchecking_the_new_toolset_sticks():
|
||||
"""Saving records it as offered, so the next read reads absence as a
|
||||
decline instead of turning it back on."""
|
||||
config = {"platform_toolsets": {"cli": ["hermes-cli"]}}
|
||||
enabled = _get_platform_tools(config, "cli", include_default_mcp_servers=False)
|
||||
with patch("hermes_cli.tools_config.save_config"):
|
||||
_save_platform_tools(config, "cli", enabled - _RECENTLY_SHIPPED_TOOLSETS)
|
||||
|
||||
reread = _get_platform_tools(config, "cli", include_default_mcp_servers=False)
|
||||
|
||||
assert not (_RECENTLY_SHIPPED_TOOLSETS & reread)
|
||||
|
||||
|
||||
@_requires_recently_shipped
|
||||
def test_agent_disabled_toolsets_still_wins():
|
||||
"""The other way to say no — a global suppression list applied last."""
|
||||
config = _saved_list_from_before()
|
||||
config["agent"] = {"disabled_toolsets": sorted(_RECENTLY_SHIPPED_TOOLSETS)}
|
||||
|
||||
enabled = _get_platform_tools(config, "cli", include_default_mcp_servers=False)
|
||||
|
||||
assert not (_RECENTLY_SHIPPED_TOOLSETS & enabled)
|
||||
|
||||
|
||||
@_requires_recently_shipped
|
||||
def test_platforms_whose_composite_excludes_it_are_left_narrow():
|
||||
"""Parity is the justification, so don't widen a deliberately small
|
||||
composite (hermes-acp, hermes-webhook) that never carried the toolset."""
|
||||
from toolsets import TOOLSETS, resolve_toolset
|
||||
|
||||
narrow = [
|
||||
platform
|
||||
for platform in ("acp", "webhook")
|
||||
if f"hermes-{platform}" in TOOLSETS
|
||||
and not any(
|
||||
set(resolve_toolset(ts, include_registry=False))
|
||||
<= set(resolve_toolset(f"hermes-{platform}"))
|
||||
for ts in _RECENTLY_SHIPPED_TOOLSETS
|
||||
)
|
||||
]
|
||||
assert narrow, "expected a composite that excludes the new toolset"
|
||||
|
||||
for platform in narrow:
|
||||
enabled = _get_platform_tools(
|
||||
_saved_list_from_before(platform),
|
||||
platform,
|
||||
include_default_mcp_servers=False,
|
||||
)
|
||||
assert not (_RECENTLY_SHIPPED_TOOLSETS & enabled), platform
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue