auto populate flux3 in tools for nous portal users

This commit is contained in:
rob-maron 2026-07-30 18:46:55 -04:00 committed by Teknium
parent 524ab53994
commit 97c6a183af
3 changed files with 180 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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