refactor(hermes_cli): tools_config group — dense docstring reflow (word-identical, AST-verified)
This commit is contained in:
@@ -49,11 +49,9 @@ def _run_text(cmd: list, *, timeout, capture_output: bool = True, **kwargs) -> s
|
||||
|
||||
def _post_setup_no_window_flags(*, streams_to_console: bool = False) -> int:
|
||||
"""Win32 creationflags that stop post-setup children flashing a console.
|
||||
|
||||
CREATE_NO_WINDOW hides console grandchildren (npm, pip, powershell) while keeping stdio inheritable
|
||||
(unlike DETACHED_PROCESS). Returns 0 on POSIX. ``streams_to_console`` children are only hidden when
|
||||
our own stdout is not a console, so live installer output is never swallowed.
|
||||
"""
|
||||
our own stdout is not a console, so live installer output is never swallowed."""
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
flags = windows_hide_flags()
|
||||
@@ -136,11 +134,10 @@ def _cua_driver_install_ready() -> bool:
|
||||
|
||||
def _pip_install(args: List[str], *, timeout: int = 300, capture_output: bool = True):
|
||||
"""Install Python packages from a post-setup hook.
|
||||
|
||||
Order: ``uv pip install`` (fast, needs no pip in the venv), then ``python -m pip install``, then
|
||||
``python -m ensurepip --upgrade`` and retry pip. The last tier exists because the Windows installer
|
||||
creates the venv via ``uv venv``, which does NOT seed pip, so bare ``-m pip`` failed on fresh installs.
|
||||
"""
|
||||
creates the venv via ``uv venv``, which does NOT seed pip, so bare ``-m pip`` failed on fresh
|
||||
installs."""
|
||||
venv_root = Path(sys.executable).parent.parent
|
||||
uv_env = {**os.environ, "VIRTUAL_ENV": str(venv_root)}
|
||||
install_flags = _post_setup_no_window_flags(streams_to_console=not capture_output)
|
||||
@@ -207,12 +204,10 @@ def _cua_driver_version(binary: str) -> Optional[str]:
|
||||
|
||||
def _confirmed_update_check(driver_cmd: str, require_confirmed_update: bool) -> tuple:
|
||||
"""Ask the installed driver whether a newer release exists; returns ``(proceed, pin_version)``.
|
||||
|
||||
``proceed=False`` means stop with success (already latest, or indeterminate under
|
||||
``require_confirmed_update``). An old driver (no check-update verb) or offline check yields None:
|
||||
`hermes update` then keeps the installed version — an indeterminate check must never cost a
|
||||
multi-minute silent reinstall on every update — while explicit `install --upgrade` falls through.
|
||||
"""
|
||||
multi-minute silent reinstall on every update — while explicit `install --upgrade` falls through."""
|
||||
try:
|
||||
from tools.computer_use.cua_backend import cua_driver_update_check
|
||||
_state = cua_driver_update_check()
|
||||
@@ -248,11 +243,9 @@ def install_cua_driver(
|
||||
upgrade: bool = False, require_confirmed_update: bool = False, show_installer_progress: bool = True,
|
||||
) -> bool:
|
||||
"""Install or refresh the cua-driver binary used by Computer Use.
|
||||
|
||||
The upstream installer always pulls the latest release tag, so re-running it is the canonical way to
|
||||
upgrade. ``upgrade=False`` (toolset enable flow) keeps a compatible installation, repairs an
|
||||
old/incomplete one and installs when missing; ``upgrade=True`` always refreshes.
|
||||
"""
|
||||
old/incomplete one and installs when missing; ``upgrade=True`` always refreshes."""
|
||||
import platform as _plat
|
||||
|
||||
system = _plat.system()
|
||||
@@ -385,11 +378,9 @@ def _cua_windows_install_lock_file() -> "Path":
|
||||
|
||||
def _clear_stale_windows_cua_install_lock() -> None:
|
||||
"""Delete install.ps1's lock file only when no process still holds it.
|
||||
|
||||
install.ps1 locks with ``FileShare::None``; mirror it with a zero-share ``CreateFileW`` probe and
|
||||
``FILE_FLAG_DELETE_ON_CLOSE`` so an unlocked leftover is removed atomically, with no window in which
|
||||
a new installer could acquire the file between probe and delete.
|
||||
"""
|
||||
a new installer could acquire the file between probe and delete."""
|
||||
lock_file = _cua_windows_install_lock_file()
|
||||
try:
|
||||
if not lock_file.is_file():
|
||||
@@ -437,11 +428,9 @@ def _clear_stale_windows_cua_install_lock() -> None:
|
||||
|
||||
def _clear_stale_cua_install_lock() -> None:
|
||||
"""Best-effort: remove a stale installer lock left by a dead holder.
|
||||
|
||||
POSIX stamps the holder pid into ``~/.cua-driver/packages/.install.lock.d/info``; Windows holds
|
||||
``~/.cua-driver/install.lock`` open with ``FileShare::None``. Clear either artifact up front only
|
||||
when its platform-specific liveness check proves that no install still holds it.
|
||||
"""
|
||||
when its platform-specific liveness check proves that no install still holds it."""
|
||||
if sys.platform == "win32":
|
||||
_clear_stale_windows_cua_install_lock()
|
||||
return
|
||||
@@ -485,10 +474,8 @@ def _clear_stale_cua_install_lock() -> None:
|
||||
|
||||
def _cua_install_lock_held() -> bool:
|
||||
"""True when the upstream installer's lock is held by a LIVE process.
|
||||
|
||||
Called after ``_clear_stale_cua_install_lock()``: anything provably stale is already gone, so a
|
||||
surviving lock artifact means a concurrent (or orphaned-but-alive) install owns it.
|
||||
"""
|
||||
surviving lock artifact means a concurrent (or orphaned-but-alive) install owns it."""
|
||||
try:
|
||||
if sys.platform != "win32":
|
||||
return _cua_install_lock_dir().is_dir()
|
||||
@@ -509,11 +496,9 @@ def _cua_install_lock_held() -> bool:
|
||||
|
||||
def _cua_release_endpoint_reachable(timeout: float = 5.0) -> bool:
|
||||
"""Fast probe: can we reach GitHub's release download host at all?
|
||||
|
||||
When github.com is down the installer dies slowly inside its own retries and eats the whole unattended
|
||||
ceiling; a 5s HEAD decides in seconds. Only a connection-level failure counts as unreachable — any
|
||||
HTTP response (even 4xx/5xx) proves the path works.
|
||||
"""
|
||||
When github.com is down the installer dies slowly inside its own retries and eats the whole
|
||||
unattended ceiling; a 5s HEAD decides in seconds. Only a connection-level failure counts as
|
||||
unreachable — any HTTP response (even 4xx/5xx) proves the path works."""
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
@@ -548,11 +533,9 @@ def _cua_driver_autostart_registered_windows() -> bool:
|
||||
|
||||
def _repair_cua_driver_autostart_windows(driver_cmd: str, *, verbose: bool) -> bool:
|
||||
"""Best-effort repair for Windows installer autostart quoting failures.
|
||||
|
||||
Older install.ps1 builds interpolated the binary path into a PowerShell command string, which split at
|
||||
the first space. If the scheduled task is missing, retry via Start-Process's structured ``-FilePath`` /
|
||||
``-ArgumentList`` parameters instead.
|
||||
"""
|
||||
Older install.ps1 builds interpolated the binary path into a PowerShell command string, which split
|
||||
at the first space. If the scheduled task is missing, retry via Start-Process's structured
|
||||
``-FilePath`` / ``-ArgumentList`` parameters instead."""
|
||||
if sys.platform != "win32" or _cua_driver_autostart_registered_windows():
|
||||
return True
|
||||
|
||||
@@ -654,10 +637,8 @@ def _kill_installer_tree(proc, *, is_windows: bool) -> None:
|
||||
|
||||
def _reap_after_timeout(proc, *, is_windows: bool) -> None:
|
||||
"""Kill the installer tree, then drain its pipes under a deadline.
|
||||
|
||||
An unbounded drain blocks on an EOF that only arrives when someone kills a surviving descendant by
|
||||
hand, so ``_CUA_INSTALLER_TIMEOUT`` would stop bounding anything.
|
||||
"""
|
||||
hand, so ``_CUA_INSTALLER_TIMEOUT`` would stop bounding anything."""
|
||||
_kill_installer_tree(proc, is_windows=is_windows)
|
||||
try:
|
||||
drained_out, _ = proc.communicate(timeout=_CUA_INSTALLER_DRAIN_GRACE)
|
||||
@@ -705,13 +686,11 @@ def _cua_installer_command(is_windows: bool):
|
||||
|
||||
def _unattended_installer_preflight(install_cmd: list, is_windows: bool):
|
||||
"""Fail FAST on the two conditions that otherwise consume the whole unattended ceiling.
|
||||
|
||||
1. Install lock held by a live process — upstream would poll it for up to LOCK_STALE_AFTER_SECONDS=600
|
||||
before probing the holder (the 11-minute silent hang class).
|
||||
2. Release host unreachable — the installer would die slowly inside its own retries; a 5s HEAD answers.
|
||||
Returns the (possibly rewritten) install command, or None to skip this refresh. Explicit
|
||||
`computer-use install --upgrade` runs never come here and keep upstream's full lock-recovery.
|
||||
"""
|
||||
1. Install lock held by a live process — upstream would poll it for up to
|
||||
LOCK_STALE_AFTER_SECONDS=600 before probing the holder (the 11-minute silent hang class).
|
||||
2. Release host unreachable — the installer would die slowly inside its own retries; a 5s HEAD
|
||||
answers. Returns the (possibly rewritten) install command, or None to skip this refresh. Explicit
|
||||
`computer-use install --upgrade` runs never come here and keep upstream's full lock-recovery."""
|
||||
if _cua_install_lock_held():
|
||||
_print_info(" Another cua-driver install is in progress (upstream install lock is held) — skipping this refresh.")
|
||||
_print_info(f" If no install is really running, retry with: {_UPGRADE_CMD}")
|
||||
@@ -734,11 +713,9 @@ def _run_cua_driver_installer(
|
||||
label: str = "Installing", verbose: bool = True, pin_version: Optional[str] = None, show_progress: bool = True,
|
||||
installer_timeout: Optional[float] = None) -> bool:
|
||||
"""Run the upstream cua-driver installer for this platform.
|
||||
|
||||
The scripts are idempotent: they always download the latest release, so re-running on an
|
||||
already-installed system performs an upgrade. ``installer_timeout`` lets quiet callers use a shorter
|
||||
ceiling without weakening the explicit install path's stale-lock recovery window.
|
||||
"""
|
||||
ceiling without weakening the explicit install path's stale-lock recovery window."""
|
||||
import platform as _plat
|
||||
|
||||
system = _plat.system()
|
||||
|
||||
@@ -14,11 +14,9 @@ from hermes_cli.toolset_scope import (
|
||||
|
||||
def _mcp_match_filter():
|
||||
"""Runtime name-filter matcher (exact names or fnmatch globs), with a literal fallback.
|
||||
|
||||
Must use the SAME semantics as tools/mcp_tool.py registration — a literal ``in`` check renders
|
||||
glob excludes (e.g. ``*team_member*`` from catalog default_excluded manifests) as if nothing
|
||||
were excluded.
|
||||
"""
|
||||
Must use the SAME semantics as tools/mcp_tool.py registration — a literal ``in`` check renders glob
|
||||
excludes (e.g. ``*team_member*`` from catalog default_excluded manifests) as if nothing were
|
||||
excluded."""
|
||||
try:
|
||||
from tools.mcp_tool import matches_name_filter
|
||||
return matches_name_filter
|
||||
|
||||
@@ -25,11 +25,9 @@ PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||
|
||||
def _ensure_browser_use_cli(*, verbose_hints: bool = False) -> None:
|
||||
"""Install the Browser Use CLI if it isn't already runnable.
|
||||
|
||||
Primary driver engine for EVERY browser backend except Camofox (Firefox-based, no CDP surface).
|
||||
MANAGED-FIRST: a browser-use on the user's PATH does NOT satisfy this check — only the
|
||||
Hermes-managed ``$HERMES_HOME/bin`` copy does.
|
||||
"""
|
||||
Hermes-managed ``$HERMES_HOME/bin`` copy does."""
|
||||
_print_info(" Ensuring browser-use CLI (managed install)...")
|
||||
try:
|
||||
from tools.browser_use_cli import install_cli
|
||||
@@ -86,10 +84,8 @@ def _install_chromium(install_cmd: list[str]) -> None:
|
||||
|
||||
def _post_setup_agent_browser(post_setup_key: str) -> None:
|
||||
"""``agent_browser`` (local Chromium) and ``browserbase`` (cloud rows) hooks.
|
||||
|
||||
agent-browser is not a root package.json dependency — it resolves lazily via npx (or a
|
||||
global/Hermes-managed install), so there is no ``npm install`` step here.
|
||||
"""
|
||||
global/Hermes-managed install), so there is no ``npm install`` step here."""
|
||||
# Every non-Camofox backend drives through the Browser Use CLI — install it here too.
|
||||
_ensure_browser_use_cli()
|
||||
try:
|
||||
@@ -286,10 +282,9 @@ def _post_setup_langfuse() -> None:
|
||||
|
||||
|
||||
def _post_setup_xai_grok() -> None:
|
||||
"""Shared xAI credential bootstrap for any picker row that talks to xAI (TTS, STT, Video Gen,
|
||||
x_search …). Accepts a SuperGrok-tier OAuth token (preferred — billed to the existing
|
||||
subscription) or a raw XAI_API_KEY; the rows declare empty env_vars so the auth UX lives here.
|
||||
"""
|
||||
"""Shared xAI credential bootstrap for any picker row that talks to xAI (TTS, STT, Video Gen, x_search
|
||||
…). Accepts a SuperGrok-tier OAuth token (preferred — billed to the existing subscription) or a raw
|
||||
XAI_API_KEY; the rows declare empty env_vars so the auth UX lives here."""
|
||||
try:
|
||||
from hermes_cli.auth import get_xai_oauth_auth_status
|
||||
oauth_logged_in = bool(get_xai_oauth_auth_status().get("logged_in"))
|
||||
@@ -361,8 +356,7 @@ def _run_post_setup(post_setup_key: str):
|
||||
def valid_post_setup_keys() -> Set[str]:
|
||||
"""Return the set of post-setup keys declared by any visible provider (``TOOL_CATEGORIES`` plus
|
||||
plugin-registered providers). This is the allowlist ``post-setup`` and the dashboard endpoint
|
||||
validate against, so a caller cannot drive ``_run_post_setup`` with an arbitrary key.
|
||||
"""
|
||||
validate against, so a caller cannot drive ``_run_post_setup`` with an arbitrary key."""
|
||||
from hermes_cli.tools_config import (
|
||||
TOOL_CATEGORIES,
|
||||
_plugin_browser_providers,
|
||||
@@ -384,9 +378,8 @@ def valid_post_setup_keys() -> Set[str]:
|
||||
|
||||
|
||||
def run_post_setup_command(args) -> int:
|
||||
"""``hermes tools post-setup <key>`` — non-interactive runner the dashboard spawns so the GUI can
|
||||
drive backend setup without re-implementing install logic. Exit code: 0 ok, 2 unknown key.
|
||||
"""
|
||||
"""``hermes tools post-setup <key>`` — non-interactive runner the dashboard spawns so the GUI can drive
|
||||
backend setup without re-implementing install logic. Exit code: 0 ok, 2 unknown key."""
|
||||
key = getattr(args, "post_setup_key", None)
|
||||
if not key:
|
||||
_print_error("Usage: hermes tools post-setup <key>")
|
||||
@@ -458,10 +451,9 @@ def restorable_python_tool_dependency(name: str) -> tuple[str, tuple[str, ...]]
|
||||
|
||||
|
||||
def _agent_browser_installed() -> bool:
|
||||
"""True when everything ``_run_post_setup("agent_browser")`` installs is present: the agent-browser
|
||||
CLI *and* the Chromium build it drives (or the Lightpanda engine, which needs no Chromium), so
|
||||
"Run setup" flips to installed only when re-running it would be a no-op.
|
||||
"""
|
||||
"""True when everything ``_run_post_setup("agent_browser")`` installs is present: the agent-browser CLI
|
||||
*and* the Chromium build it drives (or the Lightpanda engine, which needs no Chromium), so "Run
|
||||
setup" flips to installed only when re-running it would be a no-op."""
|
||||
from hermes_cli.nous_subscription import _local_browser_runnable
|
||||
|
||||
# The hook runs in a spawned process; this probe runs in the long-lived web-server/CLI process whose
|
||||
|
||||
@@ -39,11 +39,10 @@ def _plugin_provider_rows(
|
||||
registry_module: str, marker_keys: tuple[str, ...], *, require_name: bool = True, skip_builtin: bool = False,
|
||||
flatten_variants: bool = False) -> list[dict]:
|
||||
"""Picker-row dicts (TOOL_CATEGORIES-shaped) for a plugin registry's providers.
|
||||
|
||||
``marker_keys`` are all set to the registry name so downstream config/model-picker code routes through
|
||||
the registry. ``skip_builtin`` drops names shadowing ``_BUILTIN_NAMES``; ``flatten_variants`` expands a
|
||||
schema's tier ``variants`` into separate rows sharing one backend name, distinguished by ``web_tier``.
|
||||
"""
|
||||
``marker_keys`` are all set to the registry name so downstream config/model-picker code routes
|
||||
through the registry. ``skip_builtin`` drops names shadowing ``_BUILTIN_NAMES``;
|
||||
``flatten_variants`` expands a schema's tier ``variants`` into separate rows sharing one backend
|
||||
name, distinguished by ``web_tier``."""
|
||||
registry = _plugin_registry(registry_module)
|
||||
if registry is None:
|
||||
return []
|
||||
@@ -109,10 +108,8 @@ _plugin_tts_providers = partial(_plugin_rows_for, "tts")
|
||||
|
||||
def web_provider_capabilities(backend: str) -> list:
|
||||
"""Capabilities (``search`` / ``extract``) a web backend supports, per the registry provider instance.
|
||||
|
||||
Lets the Capabilities GUI offer ``web.search_backend`` / ``web.extract_backend`` only where it makes sense
|
||||
(ddgs and brave-free are search-only). Unknown backend or registry failure -> both.
|
||||
"""
|
||||
Lets the Capabilities GUI offer ``web.search_backend`` / ``web.extract_backend`` only where it makes
|
||||
sense (ddgs and brave-free are search-only). Unknown backend or registry failure -> both."""
|
||||
try:
|
||||
from agent.web_search_registry import get_provider
|
||||
|
||||
@@ -138,10 +135,8 @@ def _visible_providers(
|
||||
cat: dict, config: dict, *, force_fresh: bool = False, features: Optional[NousSubscriptionFeatures] = None,
|
||||
) -> list[dict]:
|
||||
"""Provider entries visible for the current auth/config state.
|
||||
|
||||
Nous-managed rows (``managed_nous_feature``) are always shown, even logged-out/unentitled, to advertise
|
||||
the capability.
|
||||
"""
|
||||
Nous-managed rows (``managed_nous_feature``) are always shown, even logged-out/unentitled, to
|
||||
advertise the capability."""
|
||||
from hermes_cli.tools_config import get_nous_subscription_features
|
||||
|
||||
if features is None:
|
||||
@@ -170,10 +165,8 @@ def _visible_providers(
|
||||
|
||||
def provider_readiness_status(provider: dict, config: dict, *, features=None, is_active: Optional[bool] = None) -> str:
|
||||
"""Honest readiness state for a provider picker row.
|
||||
|
||||
``features`` avoids re-fetching portal state per row. ``is_active`` is the completed-setup fallback for
|
||||
post_setup hooks with no registered installed-check (selecting a row runs its hook).
|
||||
"""
|
||||
``features`` avoids re-fetching portal state per row. ``is_active`` is the completed-setup fallback
|
||||
for post_setup hooks with no registered installed-check (selecting a row runs its hook)."""
|
||||
from hermes_cli.tools_config import (
|
||||
_POST_SETUP_READY, _provider_env_ready, _xai_credentials_present, get_nous_subscription_features,
|
||||
)
|
||||
@@ -262,10 +255,8 @@ def _any_plugin_provider_available(registry_module: str) -> bool:
|
||||
|
||||
def _configure_tool_category(ts_key: str, cat: dict, config: dict, *, force_fresh: bool = True, reconfigure: bool = False):
|
||||
"""Provider selection for a tool category, then API-key setup for the chosen row.
|
||||
|
||||
``reconfigure`` ("Reconfigure an existing tool"): no setup note / skip row / Nous marker, and the chosen
|
||||
provider goes through the key-update prompts instead of the new-enable prompts.
|
||||
"""
|
||||
``reconfigure`` ("Reconfigure an existing tool"): no setup note / skip row / Nous marker, and the
|
||||
chosen provider goes through the key-update prompts instead of the new-enable prompts."""
|
||||
from hermes_cli.tools_config import _prompt_choice, _provider_env_ready, get_nous_subscription_features
|
||||
|
||||
icon = cat.get("icon", "")
|
||||
@@ -329,11 +320,10 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict, *, force_fres
|
||||
|
||||
def _web_tier_matches(provider: dict, config: dict) -> bool:
|
||||
"""True when a web picker row's tier matches the configured tier (``web.provider_tier.<backend>``).
|
||||
|
||||
Tiered rows (Exa/Parallel Free vs Paid) share one ``web_backend`` and differ only in ``web_tier``. No
|
||||
``web_tier`` on the row → matches; configured tier set → must equal the row's tier; unset → "auto":
|
||||
paid when the row's key is present, free otherwise (highlight the row the runtime would actually use).
|
||||
"""
|
||||
Tiered rows (Exa/Parallel Free vs Paid) share one ``web_backend`` and differ only in ``web_tier``.
|
||||
No ``web_tier`` on the row → matches; configured tier set → must equal the row's tier; unset →
|
||||
"auto": paid when the row's key is present, free otherwise (highlight the row the runtime would
|
||||
actually use)."""
|
||||
row_tier = provider.get("web_tier")
|
||||
if not row_tier:
|
||||
return True
|
||||
@@ -547,11 +537,10 @@ def _pick_model_from_catalog(
|
||||
catalog: dict, default_model, cfg_key: str, display: str, config: dict, *, row_indent: str = "",
|
||||
) -> None:
|
||||
"""Column-aligned model picker shared by the FAL, plugin image gen and video gen flows.
|
||||
|
||||
Writes the choice to ``config[cfg_key]["model"]``. The current model is listed first so the cursor lands
|
||||
on it; a saved model belonging to another provider (shared config key) or a drifted catalog default never
|
||||
indexes the catalog. Safe when stdin is not a TTY — curses_radiolist keeps the current selection.
|
||||
"""
|
||||
Writes the choice to ``config[cfg_key]["model"]``. The current model is listed first so the cursor
|
||||
lands on it; a saved model belonging to another provider (shared config key) or a drifted catalog
|
||||
default never indexes the catalog. Safe when stdin is not a TTY — curses_radiolist keeps the current
|
||||
selection."""
|
||||
from hermes_cli.tools_config import _cfg_section, _prompt_choice
|
||||
|
||||
if not catalog:
|
||||
@@ -633,10 +622,9 @@ def _configure_xai_imagine_storage(section_name: str, config: dict) -> None:
|
||||
|
||||
def _select_plugin_gen_provider(section: str, plugin_name: str, config: dict, *, use_gateway: bool = False) -> None:
|
||||
"""Persist a plugin-backed image/video gen provider selection and run its model picker.
|
||||
|
||||
``use_gateway=True`` (Nous-managed pick) stores ``<section>.provider: nous``; BYOK picks store the plugin
|
||||
name. Any legacy ``use_gateway`` key is removed so old read-time shims cannot override the selection.
|
||||
"""
|
||||
``use_gateway=True`` (Nous-managed pick) stores ``<section>.provider: nous``; BYOK picks store the
|
||||
plugin name. Any legacy ``use_gateway`` key is removed so old read-time shims cannot override the
|
||||
selection."""
|
||||
from hermes_cli.tools_config import _cfg_section
|
||||
|
||||
cfg = _cfg_section(config, section)
|
||||
@@ -698,12 +686,10 @@ def _drop_use_gateway(section) -> None:
|
||||
|
||||
def _write_provider_config(provider: dict, config: dict, *, managed_feature) -> None:
|
||||
"""Persist the provider/backend config keys for a selected provider.
|
||||
|
||||
Pure, non-interactive core of :func:`_configure_provider` (no env prompts, post-setup hooks, Nous auth
|
||||
gating or model pickers) shared by the CLI and the GUI ``PUT .../provider`` endpoint. Each pick writes
|
||||
exactly ONE provider string per category (``nous`` for managed rows) and removes any legacy
|
||||
``use_gateway`` key so the read-time shim cannot override the new choice.
|
||||
"""
|
||||
Pure, non-interactive core of :func:`_configure_provider` (no env prompts, post-setup hooks, Nous
|
||||
auth gating or model pickers) shared by the CLI and the GUI ``PUT .../provider`` endpoint. Each pick
|
||||
writes exactly ONE provider string per category (``nous`` for managed rows) and removes any legacy
|
||||
``use_gateway`` key so the read-time shim cannot override the new choice."""
|
||||
from hermes_cli.tools_config import TOOL_CATEGORIES, _cfg_section
|
||||
|
||||
def _set_selection(section_key: str, name_key: str, vendor_value) -> None:
|
||||
@@ -797,11 +783,10 @@ def apply_provider_selection(ts_key: str, provider_name: str, config: dict) -> N
|
||||
|
||||
def _nous_provider_gate(provider: dict, config: dict, managed_feature, *, force_fresh: bool) -> bool:
|
||||
"""Return False (after printing why) when a Nous-gated row cannot be selected.
|
||||
|
||||
Managed Tool Gateway rows are always listed but only *activate* with paid Nous Portal access — selecting
|
||||
one runs an inline Portal login (auth + entitlement only, no inference-provider switch). Pure pre-auth UX
|
||||
rows (``requires_nous_auth`` without a managed feature) keep the older logged-in + entitled gate.
|
||||
"""
|
||||
Managed Tool Gateway rows are always listed but only *activate* with paid Nous Portal access —
|
||||
selecting one runs an inline Portal login (auth + entitlement only, no inference-provider switch).
|
||||
Pure pre-auth UX rows (``requires_nous_auth`` without a managed feature) keep the older logged-in +
|
||||
entitled gate."""
|
||||
from hermes_cli.tools_config import get_nous_subscription_features
|
||||
|
||||
if managed_feature:
|
||||
@@ -896,11 +881,9 @@ def _prompt_secret(
|
||||
key: str, label: str, url: str, default_val: str, *, reconfigure: bool, url_label: str, strip: bool = False,
|
||||
) -> bool:
|
||||
"""One env-var prompt; True unless the new-enable flow skipped the key.
|
||||
|
||||
Reconfigure mode shows the current value and re-prompts ("Enter to keep current"); the new-enable flow
|
||||
prompts with ``default_val`` visible when one exists, else as a password, and ``strip`` decides whether
|
||||
whitespace is trimmed (and whitespace-only counts as skipped).
|
||||
"""
|
||||
Reconfigure mode shows the current value and re-prompts ("Enter to keep current"); the new-enable
|
||||
flow prompts with ``default_val`` visible when one exists, else as a password, and ``strip`` decides
|
||||
whether whitespace is trimmed (and whitespace-only counts as skipped)."""
|
||||
if reconfigure:
|
||||
existing = get_env_value(key)
|
||||
if existing:
|
||||
@@ -928,10 +911,8 @@ def _prompt_secret(
|
||||
|
||||
def _prompt_env_vars(env_vars: list, *, reconfigure: bool) -> bool:
|
||||
"""Prompt for a provider's env vars; True when every key ended up configured.
|
||||
|
||||
Reconfigure mode re-prompts every key and always returns True; the new-enable flow keeps already-set
|
||||
keys without asking and reports False on any skipped key.
|
||||
"""
|
||||
keys without asking and reports False on any skipped key."""
|
||||
all_configured = True
|
||||
for var in env_vars:
|
||||
if not reconfigure and get_env_value(var["key"]):
|
||||
@@ -945,10 +926,9 @@ def _prompt_env_vars(env_vars: list, *, reconfigure: bool) -> bool:
|
||||
|
||||
def _configure_provider(provider: dict, config: dict, *, force_fresh: bool = True, reconfigure: bool = False):
|
||||
"""Configure a single provider - prompt for API keys and set config.
|
||||
|
||||
``reconfigure=False`` (new-enable): already-set keys are kept without asking and the post-setup hook only
|
||||
runs when every key was provided. ``reconfigure=True`` re-prompts every key and always runs the hook.
|
||||
"""
|
||||
``reconfigure=False`` (new-enable): already-set keys are kept without asking and the post-setup hook
|
||||
only runs when every key was provided. ``reconfigure=True`` re-prompts every key and always runs the
|
||||
hook."""
|
||||
from hermes_cli.tools_config import _run_post_setup
|
||||
|
||||
env_vars = provider.get("env_vars", [])
|
||||
@@ -989,10 +969,9 @@ def _reconfigure_provider(provider: dict, config: dict, *, force_fresh: bool = T
|
||||
|
||||
def _configure_vision_backend() -> None:
|
||||
"""Interactive vision-backend configuration (``auxiliary.vision.{provider,model,base_url}``).
|
||||
|
||||
Offers any authenticated provider + model (same surface as ``hermes model``) or a custom endpoint rather
|
||||
than forcing OpenRouter. "Auto" leaves the keys empty so the resolver uses the main-model fallback chain.
|
||||
"""
|
||||
Offers any authenticated provider + model (same surface as ``hermes model``) or a custom endpoint
|
||||
rather than forcing OpenRouter. "Auto" leaves the keys empty so the resolver uses the main-model
|
||||
fallback chain."""
|
||||
from hermes_cli.tools_config import _cfg_section, _prompt_choice
|
||||
|
||||
print()
|
||||
@@ -1045,11 +1024,9 @@ def _configure_vision_backend() -> None:
|
||||
|
||||
def _configure_vision_provider_model(config: dict, vision_cfg: dict) -> None:
|
||||
"""Provider + model picker for vision, mirroring the ``/model`` surface.
|
||||
|
||||
Rows come from ``build_aux_picker_rows()`` so this lists exactly what the ``hermes model`` aux-task picker
|
||||
lists, including user-defined ``providers:`` / ``custom_providers:`` endpoints. Persists
|
||||
``auxiliary.vision.provider`` + ``.model``.
|
||||
"""
|
||||
Rows come from ``build_aux_picker_rows()`` so this lists exactly what the ``hermes model`` aux-task
|
||||
picker lists, including user-defined ``providers:`` / ``custom_providers:`` endpoints. Persists
|
||||
``auxiliary.vision.provider`` + ``.model``."""
|
||||
from hermes_cli.tools_config import _prompt_choice
|
||||
|
||||
try:
|
||||
@@ -1108,10 +1085,8 @@ def _configure_vision_provider_model(config: dict, vision_cfg: dict) -> None:
|
||||
|
||||
def _configure_simple_requirements(ts_key: str, *, reconfigure: bool = False):
|
||||
"""Fallback for toolsets that just need env vars (no provider selection).
|
||||
|
||||
Vision has its own provider/model picker — run it directly so neither flow falls back to the generic
|
||||
single-key prompt (which would re-ask for OPENROUTER_API_KEY).
|
||||
"""
|
||||
single-key prompt (which would re-ask for OPENROUTER_API_KEY)."""
|
||||
from hermes_cli.tools_config import TOOLSET_ENV_REQUIREMENTS, _toolset_has_keys, _toolset_label
|
||||
|
||||
if ts_key == "vision":
|
||||
|
||||
@@ -33,14 +33,11 @@ def validate_platform_toolsets(
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user