From d6029d372fdb25196809e82d5697c7491a01bd13 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:15:59 -0700 Subject: [PATCH] =?UTF-8?q?refactor(hermes=5Fcli):=20doctor=20=E2=80=94=20?= =?UTF-8?q?extract=20=5Fapikey=5Frequest/=5Fbits/=5Flink=5Fvenv,=20collaps?= =?UTF-8?q?e=20journal-mode=20&=20certifi=20branches,=20drop=20blank-after?= =?UTF-8?q?-import=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hermes_cli/doctor.py | 1 - hermes_cli/doctor_config.py | 36 +++++-------- hermes_cli/doctor_connectivity.py | 53 ++++++++++--------- hermes_cli/doctor_live.py | 12 +---- hermes_cli/doctor_platform.py | 88 ++++++++++++------------------- hermes_cli/doctor_report.py | 16 ++---- hermes_cli/doctor_state.py | 45 ++++++---------- hermes_cli/doctor_tools.py | 12 ++--- hermes_cli/dump.py | 3 -- 9 files changed, 99 insertions(+), 167 deletions(-) diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 9f561c84f5..c4a30dabb2 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -81,7 +81,6 @@ def _check_auth_providers(should_fix: bool) -> Finding: f = Finding() try: from hermes_cli.auth import get_nous_auth_status_local, get_codex_auth_status, get_minimax_oauth_auth_status - # Read-only display: refresh-free snapshot — doctor must never trigger an OAuth refresh. _login_row("Nous Portal auth", get_nous_auth_status_local()) codex_status = get_codex_auth_status() diff --git a/hermes_cli/doctor_config.py b/hermes_cli/doctor_config.py index 3b795e8602..4ecc939f84 100644 --- a/hermes_cli/doctor_config.py +++ b/hermes_cli/doctor_config.py @@ -68,12 +68,8 @@ def collect_deprecated_env_vars(env_map: dict | None) -> list[tuple[str, str]]: def collect_relay_plugin_cutover_findings(raw_config: dict | None, env_map: dict | None) -> list[tuple[str, str]]: """Return actionable findings for the removed Hermes Relay plugin.""" from hermes_cli.relay_plugin_cutover import ( - LEGACY_RELAY_EXPORT_ENV_VARS, - RELAY_PLUGINS_CONFIG_ENV, - configured_legacy_relay_env_vars, - legacy_relay_plugin_keys, + LEGACY_RELAY_EXPORT_ENV_VARS, RELAY_PLUGINS_CONFIG_ENV, configured_legacy_relay_env_vars, legacy_relay_plugin_keys, ) - findings: list[tuple[str, str]] = [] plugins = raw_config.get("plugins") if isinstance(raw_config, dict) else None if isinstance(plugins, dict): @@ -135,7 +131,6 @@ def _check_mcp_security(should_fix: bool, f: Finding) -> None: """Flag mcp_servers entries with suspicious stdio commands.""" from hermes_cli.config import load_config from hermes_cli.mcp_security import validate_mcp_server_entry - servers = load_config().get("mcp_servers") or {} suspicious = 0 if isinstance(servers, dict): @@ -219,11 +214,10 @@ def _known_provider_ids(cfg: dict) -> tuple[set, list, object, object, object]: from hermes_cli.config import is_provider_enabled known.update(str(name).strip().lower() for name, prov_cfg in user_providers.items() if str(name).strip() and is_provider_enabled(prov_cfg)) - if aliases is not None: - for entry in custom_providers: - name = str(entry.get("name") or "").strip() if isinstance(entry, dict) else "" - if name: - known.update(aliases(name, str(entry.get("provider_key") or "").strip())) + for entry in custom_providers if aliases is not None else (): + name = str(entry.get("name") or "").strip() if isinstance(entry, dict) else "" + if name: + known.update(aliases(name, str(entry.get("provider_key") or "").strip())) return known, custom_providers, resolve_auth, normalize, resolve_full @@ -240,11 +234,8 @@ def _provider_has_credentials(runtime_provider: str) -> bool: checks elsewhere, and get_auth_status() returns a bare {logged_in: False} for anything it doesn't dispatch.""" if runtime_provider == "openrouter": from hermes_cli.config import get_env_value - - return bool(str(get_env_value("OPENROUTER_API_KEY") or "").strip() - or str(get_env_value("OPENAI_API_KEY") or "").strip()) + return any(str(get_env_value(k) or "").strip() for k in ("OPENROUTER_API_KEY", "OPENAI_API_KEY")) from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status - pconfig = PROVIDER_REGISTRY.get(runtime_provider) if pconfig and getattr(pconfig, "auth_type", "") == "api_key": status = get_auth_status(runtime_provider) or {} @@ -264,12 +255,11 @@ def _validate_model_config(config_path, issues: list) -> None: known_providers, custom_providers, resolve_auth, normalize, resolve_full = _known_provider_ids(cfg) valid_provider_ids = set(known_providers) accept = {provider} if provider else set() - if normalize is not None: - for known_provider in known_providers: - try: - valid_provider_ids.add(normalize(known_provider)) - except Exception: - continue + for known_provider in known_providers if normalize is not None else (): + try: + valid_provider_ids.add(normalize(known_provider)) + except Exception: + continue runtime_provider = catalog_provider = provider if provider and provider not in {"auto", "custom"}: @@ -340,11 +330,10 @@ def _check_config_file(should_fix: bool) -> Finding: example_config = PROJECT_ROOT / 'cli-config.yaml.example' if example_config.exists(): shutil.copy2(str(example_config), str(config_path)) - check_ok(f"Created {_DHH}/config.yaml from cli-config.yaml.example") else: from hermes_cli.config import DEFAULT_CONFIG, save_config save_config(DEFAULT_CONFIG) - check_ok(f"Created {_DHH}/config.yaml from defaults") + check_ok(f"Created {_DHH}/config.yaml from {'cli-config.yaml.example' if example_config.exists() else 'defaults'}") f.fixed += 1 else: check_warn("config.yaml not found", "(using defaults)") @@ -481,7 +470,6 @@ def _check_config_drift(should_fix: bool) -> Finding: def _check_xai_retirement(should_fix: bool, f: Finding) -> None: from hermes_cli.config import load_config from hermes_cli.xai_retirement import MIGRATION_GUIDE_URL, find_retired_xai_refs, format_issue - retired_refs = find_retired_xai_refs(load_config()) if not retired_refs: check_ok("No retired xAI models in config") diff --git a/hermes_cli/doctor_connectivity.py b/hermes_cli/doctor_connectivity.py index c04eb2634d..1c1295d8e4 100644 --- a/hermes_cli/doctor_connectivity.py +++ b/hermes_cli/doctor_connectivity.py @@ -189,26 +189,7 @@ def _probe_apikey_provider(pname, env_vars, default_url, base_env, supports_heal return _row(pname, "ok", "(key configured)", label=label) try: import httpx - base = os.getenv(base_env, "") if base_env else "" - # Kimi Code keys (sk-kimi-) → api.kimi.com/coding/v1 (OpenAI-compat surface exposing /models). - if not base and key.startswith("sk-kimi-"): - base = "https://api.kimi.com/coding/v1" - # Anthropic-compat endpoints (/anthropic, api.kimi.com/coding with no /v1) don't support - # /models — rewrite to the OpenAI-compat /v1 surface for health checks. - if base and base.rstrip("/").endswith("/anthropic"): - from agent.auxiliary_client import _to_openai_base_url - base = _to_openai_base_url(base) - if base_url_host_matches(base, "api.kimi.com") and base.rstrip("/").endswith("/coding"): - base = base.rstrip("/") + "/v1" - url = (base.rstrip("/") + "/models") if base else default_url - headers = {"Authorization": f"Bearer {key}", "User-Agent": _HERMES_USER_AGENT} - if base_url_host_matches(base, "api.kimi.com"): - headers["User-Agent"] = "claude-code/0.1.0" - # Google's Generative Language API rejects ``Authorization: Bearer `` with 401 - # ACCESS_TOKEN_TYPE_UNSUPPORTED (reserved for OAuth 2 tokens); plain keys use ``x-goog-api-key``. - if url and base_url_host_matches(url, "generativelanguage.googleapis.com"): - headers.pop("Authorization", None) - headers["x-goog-api-key"] = key + base, url, headers = _apikey_request(key, base_env, default_url) r = httpx.get(url, headers=headers, timeout=10) if pname == "Alibaba/DashScope" and not base and r.status_code == 401: r = httpx.get("https://dashscope.aliyuncs.com/compatible-mode/v1/models", headers=headers, timeout=10) @@ -221,6 +202,31 @@ def _probe_apikey_provider(pname, env_vars, default_url, base_env, supports_heal return _row(pname, "warn", f"(HTTP {r.status_code})", label=label) +def _apikey_request(key: str, base_env, default_url) -> tuple: + """(effective base, models URL, headers) for a generic Bearer-auth probe, with the per-vendor rewrites.""" + base = os.getenv(base_env, "") if base_env else "" + # Kimi Code keys (sk-kimi-) → api.kimi.com/coding/v1 (OpenAI-compat surface exposing /models). + if not base and key.startswith("sk-kimi-"): + base = "https://api.kimi.com/coding/v1" + # Anthropic-compat endpoints (/anthropic, api.kimi.com/coding with no /v1) don't support + # /models — rewrite to the OpenAI-compat /v1 surface for health checks. + if base and base.rstrip("/").endswith("/anthropic"): + from agent.auxiliary_client import _to_openai_base_url + base = _to_openai_base_url(base) + if base_url_host_matches(base, "api.kimi.com") and base.rstrip("/").endswith("/coding"): + base = base.rstrip("/") + "/v1" + url = (base.rstrip("/") + "/models") if base else default_url + headers = {"Authorization": f"Bearer {key}", "User-Agent": _HERMES_USER_AGENT} + if base_url_host_matches(base, "api.kimi.com"): + headers["User-Agent"] = "claude-code/0.1.0" + # Google's Generative Language API rejects ``Authorization: Bearer `` with 401 + # ACCESS_TOKEN_TYPE_UNSUPPORTED (reserved for OAuth 2 tokens); plain keys use ``x-goog-api-key``. + if url and base_url_host_matches(url, "generativelanguage.googleapis.com"): + headers.pop("Authorization", None) + headers["x-goog-api-key"] = key + return base, url, headers + + def _probe_bedrock() -> ProbeResult: name = "AWS Bedrock" try: @@ -263,8 +269,7 @@ def _probe_azure_entra() -> ProbeResult: model_cfg = cfg.get("model") if isinstance(cfg, dict) else {} if not isinstance(model_cfg, dict): return _skip(name) - cfg_provider = str(model_cfg.get("provider") or "").strip().lower() - auth_mode = str(model_cfg.get("auth_mode") or "").strip().lower() + cfg_provider, auth_mode = (str(model_cfg.get(k) or "").strip().lower() for k in ("provider", "auth_mode")) if cfg_provider != "azure-foundry" or auth_mode != "entra_id": return _skip(name) except Exception: @@ -278,9 +283,7 @@ def _probe_azure_entra() -> ProbeResult: return _row(name, "warn", f"(adapter import failed: {exc})", [f"Azure Foundry adapter import failed: {exc}"], label=label) if not has_azure_identity_installed(): - return _row(name, "warn", "(azure-identity not installed)", - [f"Install azure-identity: {sys.executable} -m pip install azure-identity"], label=label) - + return _row(name, "warn", "(azure-identity not installed)", [f"Install azure-identity: {sys.executable} -m pip install azure-identity"], label=label) entra_cfg = model_cfg.get("entra") or {} scope = (str(entra_cfg.get("scope") or "").strip() if isinstance(entra_cfg, dict) else "") or SCOPE_AI_AZURE_DEFAULT info = describe_active_credential(config=EntraIdentityConfig(scope=scope), timeout_seconds=10.0) diff --git a/hermes_cli/doctor_live.py b/hermes_cli/doctor_live.py index 330fb9648f..275848311b 100644 --- a/hermes_cli/doctor_live.py +++ b/hermes_cli/doctor_live.py @@ -42,7 +42,6 @@ class ProbeResult: def _load_config() -> dict: try: from hermes_cli.config import load_config - return load_config() or {} except Exception: return {} @@ -51,19 +50,16 @@ def _load_config() -> dict: def _http_get(url: str, headers: Optional[dict] = None, timeout: Optional[float] = None): """Single HTTP GET seam for all metadata probes.""" import httpx - return httpx.get(url, headers=headers or {}, timeout=timeout) def _browser_available() -> bool: """Is the local browser automation backend (agent-browser) installed?""" import shutil - if shutil.which("agent-browser"): return True try: from hermes_cli.doctor import HERMES_HOME, PROJECT_ROOT - if (PROJECT_ROOT / "node_modules" / "agent-browser").exists(): return True for candidate in (HERMES_HOME / "node" / "bin", HERMES_HOME / "node", HERMES_HOME / "node_modules" / ".bin"): @@ -75,11 +71,7 @@ def _browser_available() -> bool: # probes above. Mirror the rung hermes_cli.doctor uses so this probe can't diverge from it, including # the Termux carve-out (bare npx is too fragile to advertise as ready there). try: - from tools.browser_tool import ( - _find_agent_browser, - _is_npx_agent_browser_sentinel, - _requires_real_termux_browser_install, - ) + from tools.browser_tool import _find_agent_browser, _is_npx_agent_browser_sentinel, _requires_real_termux_browser_install browser_cmd = _find_agent_browser(validate=False) except Exception: return False @@ -93,7 +85,6 @@ def _launch_browser_probe(timeout: float) -> tuple: from playwright.sync_api import sync_playwright except ImportError: return (False, "playwright not installed") - with sync_playwright() as p: browser = p.chromium.launch(headless=True, timeout=timeout * 1000) try: @@ -106,7 +97,6 @@ def _launch_browser_probe(timeout: float) -> tuple: def _probe_mcp_server(name: str, config: dict, timeout: float): """initialize + tools/list against one configured MCP server.""" from hermes_cli.mcp_config import _probe_single_server - return _probe_single_server(name, config, connect_timeout=timeout) diff --git a/hermes_cli/doctor_platform.py b/hermes_cli/doctor_platform.py index 62e42f77dd..1f02ade5cb 100644 --- a/hermes_cli/doctor_platform.py +++ b/hermes_cli/doctor_platform.py @@ -32,14 +32,11 @@ def _sqlite_upgrade_hint(install_method: str | None = None) -> str: """Return an actionable SQLite upgrade hint for this install layout.""" from hermes_cli.doctor import PROJECT_ROOT, detect_install_method method = install_method or detect_install_method(PROJECT_ROOT) - if method == "docker": - action = f"run `{recommended_update_command_for_method(method)}`, then recreate all Hermes containers" - elif is_nix_install_method(method): - action = recommended_update_command_for_method(method) # prose guidance, not a shell command - elif method == "apt": - action = f"run `{recommended_update_command_for_method(method)}`" + cmd = recommended_update_command_for_method(method) + if is_nix_install_method(method): + action = cmd # prose guidance, not a shell command else: - action = "run `hermes update`" + action = {"docker": f"run `{cmd}`, then recreate all Hermes containers", "apt": f"run `{cmd}`"}.get(method, "run `hermes update`") return f"({action}; fixed versions: 3.51.3+ / 3.50.7 / 3.44.6 — see https://sqlite.org/wal.html#walresetbug)" @@ -116,19 +113,17 @@ def _report_database_journal_modes(hermes_home: Path | None = None, version_info continue mode, error = _read_journal_mode(path) size = _format_db_size(path) - if error is not None: - if vulnerable: - check_warn(f"{name}: journal mode could not be read", f"({error}; cannot rule out WAL exposure)") - else: - check_info(f"{name}: journal mode could not be read ({error})") + if error is not None and vulnerable: + check_warn(f"{name}: journal mode could not be read", f"({error}; cannot rule out WAL exposure)") + elif error is not None: + check_info(f"{name}: journal mode could not be read ({error})") + elif mode == "wal" and vulnerable: + exposed.append(name) + check_warn(f"{name} is in WAL mode ({size})", "(exposed to the WAL-reset bug until SQLite is upgraded)") elif mode == "wal": - if vulnerable: - exposed.append(name) - check_warn(f"{name} is in WAL mode ({size})", "(exposed to the WAL-reset bug until SQLite is upgraded)") - else: - check_info(f"{name}: WAL journal mode ({size})") + check_info(f"{name}: WAL journal mode ({size})") else: - check_info(f"{name}: rollback journal mode ({size}, not exposed)" if vulnerable else f"{name}: rollback journal mode ({size})") + check_info(f"{name}: rollback journal mode ({size}{', not exposed' if vulnerable else ''})") if exposed: check_info(f"To clear the exposure: {_wal_reset_repair_hint()}") @@ -188,8 +183,7 @@ def _check_s6_supervision(issues: list[str]) -> None: profiles = mgr.list_profile_gateways() if not profiles: - check_info("No per-profile gateways registered yet — create one with `hermes profile create `") - return + return check_info("No per-profile gateways registered yet — create one with `hermes profile create `") up_count = sum(1 for p in profiles if mgr.is_running(f"gateway-{p}")) check_ok(f"Per-profile gateways: {up_count}/{len(profiles)} supervised up" + (f" ({', '.join(sorted(profiles))})" if len(profiles) <= 8 else "")) @@ -207,11 +201,8 @@ def check_certificates(should_fix: bool = False, issues: "list | None" = None) - except Exception as e: check_warn("SSL certificate check skipped", str(e)) return - - def add_issue(msg: str) -> None: - if issues is not None: - issues.append(msg) - + if issues is None: + issues = [] try: verify_ca_bundle_with_fallback() check_ok("SSL CA certificate bundle is valid") @@ -225,20 +216,19 @@ def check_certificates(should_fix: bool = False, issues: "list | None" = None) - check_fail("SSL CA certificate bundle is broken", first_error) pip_cmd = f"{sys.executable} -m pip install --force-reinstall certifi" if not should_fix: - add_issue(f"Repair the CA bundle: run `hermes doctor --fix`, or `{pip_cmd}`") + issues.append(f"Repair the CA bundle: run `hermes doctor --fix`, or `{pip_cmd}`") return print(" → Repairing: force-reinstalling certifi...") try: result = subprocess.run([sys.executable, "-m", "pip", "install", "--force-reinstall", "certifi"], capture_output=True, text=True, timeout=300) + failure = ("certifi reinstall failed", (result.stderr or result.stdout or "")[-500:]) if result.returncode != 0 else None except Exception as exc: - check_fail("certifi repair could not run pip", str(exc)) - add_issue(f"Reinstall certifi manually: {pip_cmd}") - return - if result.returncode != 0: - check_fail("certifi reinstall failed", (result.stderr or result.stdout or "")[-500:]) - add_issue(f"Reinstall certifi manually: {pip_cmd}") + failure = ("certifi repair could not run pip", str(exc)) + if failure: + check_fail(*failure) + issues.append(f"Reinstall certifi manually: {pip_cmd}") return # Drop cached certifi modules so where() resolves the fresh install without a restart. @@ -252,11 +242,8 @@ def check_certificates(should_fix: bool = False, issues: "list | None" = None) - check_ok("SSL CA certificate bundle repaired (certifi reinstalled)") except SSLConfigurationError as e: check_fail("SSL CA certificate bundle still broken after reinstall", str(e)) - add_issue( - "certifi reinstall did not restore the CA bundle — check for a " - "custom CA env var (SSL_CERT_FILE/REQUESTS_CA_BUNDLE) pointing " - "at a missing file, or recreate the venv." - ) + issues.append("certifi reinstall did not restore the CA bundle — check for a custom CA env var " + "(SSL_CERT_FILE/REQUESTS_CA_BUNDLE) pointing at a missing file, or recreate the venv.") def _check_gateway_service_linger(issues: list[str]) -> None: @@ -291,9 +278,7 @@ def check_macos_tcc_grants() -> None: Disk Access, so the DR string is the only readable signal (a proxy for the signing class, not DR wording). """ from hermes_cli.doctor import _desktop_app_bundle, _macos_desktop_dr - if sys.platform != "darwin": - return - app = _desktop_app_bundle() + app = _desktop_app_bundle() if sys.platform == "darwin" else None if app is None: return dr = _macos_desktop_dr(app) @@ -335,13 +320,11 @@ def _desktop_app_bundle() -> Path | None: def _macos_desktop_dr(app: Path) -> str | None: """Return the bundle's designated requirement string, or None on failure (a hanging codesign must never abort doctor).""" codesign = shutil.which("codesign") - if not codesign: - return None try: - proc = subprocess.run([codesign, "-d", "--requirements", "-", str(app)], capture_output=True, text=True, timeout=15) + proc = subprocess.run([codesign, "-d", "--requirements", "-", str(app)], capture_output=True, text=True, timeout=15) if codesign else None except (FileNotFoundError, subprocess.TimeoutExpired): return None - return None if proc.returncode != 0 else (proc.stdout or "") + (proc.stderr or "") + return None if proc is None or proc.returncode != 0 else (proc.stdout or "") + (proc.stderr or "") def check_macos_tcc_anchor(should_fix: bool = False) -> None: @@ -354,13 +337,10 @@ def check_macos_tcc_anchor(should_fix: bool = False) -> None: if status == "skip": return if status == "active": - check_ok("macOS TCC anchor active", f"({detail})") - return - if should_fix: - anchored = tcc.ensure_tcc_anchor() - if anchored is not None: - check_ok("macOS TCC anchor installed", f"({anchored})") - return + return check_ok("macOS TCC anchor active", f"({detail})") + anchored = tcc.ensure_tcc_anchor() if should_fix else None + if anchored is not None: + return check_ok("macOS TCC anchor installed", f"({anchored})") check_warn("macOS TCC anchor missing" if status == "missing" else "macOS TCC anchor stale", f"({detail})") except Exception as e: check_warn("macOS TCC anchor check failed", f"({e})") @@ -377,7 +357,9 @@ def check_macos_full_disk_access() -> None: tcc_dir = Path.home() / "Library" / "Application Support" / "com.apple.TCC" try: os.listdir(tcc_dir) - except PermissionError: + except OSError as e: + if not isinstance(e, PermissionError): + return check_info( "One switch silences all macOS folder prompts: grant your terminal " "app Full Disk Access and Hermes will never trip per-folder dialogs " @@ -389,8 +371,6 @@ def check_macos_full_disk_access() -> None: "and restart them once. With Hermes' stable signing identities the " "grant survives every update." ) - except OSError: - return else: check_ok("macOS Full Disk Access granted", "(no per-folder permission prompts will occur)") diff --git a/hermes_cli/doctor_report.py b/hermes_cli/doctor_report.py index 93e08175d3..6a5190dbca 100644 --- a/hermes_cli/doctor_report.py +++ b/hermes_cli/doctor_report.py @@ -15,7 +15,6 @@ def _mark(glyph: str, col: str): check_ok, check_warn, check_fail = _mark("✓", Colors.GREEN), _mark("⚠", Colors.YELLOW), _mark("✗", Colors.RED) -CHECK_ROW = {"ok": check_ok, "warn": check_warn, "fail": check_fail} def check_info(text: str): @@ -24,13 +23,9 @@ def check_info(text: str): def check_bool(cond, ok, bad, *, fail: bool = False): """``check_ok(*ok)`` when *cond* else ``check_warn(*bad)`` (``check_fail`` with fail=True); returns bool(cond). - - *ok* / *bad* are a text string or a ``(text, detail)`` tuple. - """ + *ok* / *bad* are a text string or a ``(text, detail)`` tuple.""" args = ok if cond else bad - if isinstance(args, str): - args = (args,) - (check_ok if cond else (check_fail if fail else check_warn))(*args) + (check_ok if cond else (check_fail if fail else check_warn))(*((args,) if isinstance(args, str) else args)) return bool(cond) @@ -62,11 +57,8 @@ class Finding: def doctor_check(on_error: str | None = None, detail: str = ""): """Turn ``fn(should_fix, f: Finding)`` into a ``(should_fix) -> Finding`` doctor check. - - Creates the Finding, and if *fn* raises prints ``check_warn(on_error.format(e=e), detail.format(e=e))`` - (nothing when *on_error* is None) — the partial Finding is still returned, so issues recorded - before the crash survive. - """ + If *fn* raises, prints ``check_warn(on_error.format(e=e), detail.format(e=e))`` (nothing when *on_error* + is None); the partial Finding is still returned, so issues recorded before the crash survive.""" def deco(fn): @functools.wraps(fn) def check(should_fix: bool) -> Finding: diff --git a/hermes_cli/doctor_state.py b/hermes_cli/doctor_state.py index 54e7922480..5b2fa255c7 100644 --- a/hermes_cli/doctor_state.py +++ b/hermes_cli/doctor_state.py @@ -18,7 +18,6 @@ def _honcho_is_configured_for_doctor() -> bool: """Return True when Honcho is configured, even if this process has no active session.""" try: from plugins.memory.honcho.client import HonchoClientConfig - cfg = HonchoClientConfig.from_global_config() return bool(cfg.enabled and (cfg.api_key or cfg.base_url)) except Exception: @@ -30,14 +29,12 @@ def _doctor_memory_config(hermes_home: Path | None = None) -> dict: from hermes_cli.doctor import HERMES_HOME try: from hermes_cli.config import _expand_env_vars, read_user_config_raw - config_path = (hermes_home if hermes_home is not None else HERMES_HOME) / "config.yaml" if not config_path.exists(): return {} config = _expand_env_vars(read_user_config_raw(config_path)) try: from hermes_cli import managed_scope - config = managed_scope.apply_managed_overlay(config) except Exception: pass @@ -52,6 +49,11 @@ def _doctor_memory_config(hermes_home: Path | None = None) -> dict: STATE_DB_SIZE_WARN_BYTES = 1 * 1024 * 1024 * 1024 # 1 GiB logical size +def _bits(*pairs) -> list: + """``[fmt() for value, fmt in pairs if value is not None]`` — present-only stat fragments.""" + return [fmt() for value, fmt in pairs if value is not None] + + def _render_state_db_stats(stats: dict, holders=None) -> list: """Turn a collect_state_db_stats() dict into ``(kind, text, detail)`` rows, kind 'info' / 'warn'. @@ -64,27 +66,20 @@ def _render_state_db_stats(stats: dict, holders=None) -> list: wal = stats.get("wal_size_bytes") freelist = stats.get("freelist_count") - size_bits = [] - if logical is not None: - size_bits.append(f"logical size {_human_bytes(logical)}") - if stats.get("page_count") is not None: - size_bits.append(f"{stats['page_count']:,} pages") - if freelist is not None: - size_bits.append(f"{freelist:,} free") - if wal is not None: - size_bits.append(f"WAL {_human_bytes(wal)}") + size_bits = _bits( + (logical, lambda: f"logical size {_human_bytes(logical)}"), + (stats.get("page_count"), lambda: f"{stats['page_count']:,} pages"), + (freelist, lambda: f"{freelist:,} free"), + (wal, lambda: f"WAL {_human_bytes(wal)}"), + ) if size_bits: lines.append(("info", "state.db " + ", ".join(size_bits), "")) - - row_bits = [] - if stats.get("messages") is not None: - row_bits.append(f"{stats['messages']:,} messages") - if stats.get("sessions") is not None: - row_bits.append(f"{stats['sessions']:,} sessions") - if stats.get("journal_mode"): - row_bits.append(f"journal_mode={stats['journal_mode']}") - if holders is not None: - row_bits.append(f"{holders} process(es) holding the DB open") + row_bits = _bits( + (stats.get("messages"), lambda: f"{stats['messages']:,} messages"), + (stats.get("sessions"), lambda: f"{stats['sessions']:,} sessions"), + (stats.get("journal_mode") or None, lambda: f"journal_mode={stats['journal_mode']}"), + (holders, lambda: f"{holders} process(es) holding the DB open"), + ) if row_bits: lines.append(("info", ", ".join(row_bits), "")) @@ -120,7 +115,6 @@ def _render_state_db_stats(stats: dict, holders=None) -> list: def _memory_store_flags(hermes_home: Path) -> tuple: from tools.memory_tool import get_builtin_memory_store_flags - return get_builtin_memory_store_flags({"memory": _doctor_memory_config(hermes_home)}) @@ -226,7 +220,6 @@ def _state_db_health(f: Finding, should_fix: bool, state_db_path: Path, _DHH: st # `SELECT COUNT(*)` succeeds even when the FTS index is corrupt and every message write # fails through the triggers; _db_opens_cleanly drives a rolled-back write to surface that. from hermes_state import _db_opens_cleanly - _write_reason = _db_opens_cleanly(state_db_path) if _write_reason is not None: check_warn(f"{_DHH}/state.db fails a write-health probe (FTS index may be corrupt)", f"({_write_reason})") @@ -241,7 +234,6 @@ def _state_db_health(f: Finding, should_fix: bool, state_db_path: Path, _DHH: st ) except Exception as e: from hermes_state import is_malformed_db_error - if not is_malformed_db_error(e): check_warn(f"{_DHH}/state.db exists but has issues: {e}") return @@ -264,7 +256,6 @@ def _state_db_stats(issues: list, state_db_path: Path) -> None: the gateway; any failure degrades to one info line rather than failing doctor.""" try: from hermes_state import collect_state_db_stats, count_db_holders - rows = _render_state_db_stats(collect_state_db_stats(state_db_path), holders=count_db_holders(state_db_path)) for _kind, _text, _detail in rows: if _kind != "warn": @@ -351,7 +342,6 @@ def _check_skills_hub(should_fix: bool) -> Finding: check_warn(f"{q_count} skill(s) in quarantine", "(pending review)") from hermes_cli.config import get_env_value - if get_env_value("GITHUB_TOKEN") or get_env_value("GH_TOKEN"): check_ok("GitHub token configured (authenticated API access)") else: @@ -440,7 +430,6 @@ def _check_memory_provider(should_fix: bool) -> Finding: def _check_profiles(should_fix: bool, f: Finding) -> None: from hermes_cli.profiles import list_profiles, _get_wrapper_dir, profile_exists import re as _re - named_profiles = [p for p in list_profiles() if not p.is_default] if not named_profiles: return diff --git a/hermes_cli/doctor_tools.py b/hermes_cli/doctor_tools.py index 5734daacde..6785627a27 100644 --- a/hermes_cli/doctor_tools.py +++ b/hermes_cli/doctor_tools.py @@ -11,7 +11,7 @@ import shutil import subprocess import sys from hermes_cli.doctor_platform import _system_package_install_cmd -from hermes_cli.doctor_report import CHECK_ROW, Finding, _fail_and_issue, check_bool, check_info, check_ok, check_warn, doctor_check +from hermes_cli.doctor_report import Finding, _fail_and_issue, check_bool, check_info, check_ok, check_warn, doctor_check from hermes_cli.vercel_auth import describe_vercel_auth from hermes_constants import agent_browser_runnable, is_termux as _is_termux @@ -71,12 +71,10 @@ def _doctor_web_capability_rows() -> list[tuple[str, str, str]]: try: from agent.web_search_registry import get_active_extract_provider, get_active_search_provider from tools.web_tools import _ensure_web_plugins_loaded, _provider_is_ready - # Fresh process: bundled web providers only register during plugin discovery (idempotent, cheap). _ensure_web_plugins_loaded() except Exception: return rows - for capability, getter in (("web search", get_active_search_provider), ("web extract", get_active_extract_provider)): try: provider = getter() @@ -94,8 +92,7 @@ def _doctor_web_capability_rows() -> list[tuple[str, str, str]]: def _apply_doctor_tool_availability_overrides(available: list[str], unavailable: list[dict]) -> tuple[list[str], list[dict]]: """Adjust runtime-gated tool availability for doctor diagnostics.""" from hermes_cli.doctor import _honcho_is_configured_for_doctor - updated_available = list(available) - updated_unavailable = [] + updated_available, updated_unavailable = list(available), [] for item in unavailable: if _is_kanban_worker_env_gate(item): gated = "kanban" @@ -220,10 +217,8 @@ def _check_vercel_backend(issues: list[str]) -> None: def _check_plugin_backend(terminal_env: str, issues: list[str]) -> None: try: from hermes_cli.plugins import discover_plugins - discover_plugins() from agent.terminal_env_registry import get_provider - provider = get_provider(terminal_env) except Exception: provider = None @@ -247,7 +242,6 @@ def _check_terminal_backend(should_fix: bool) -> Finding: running_in_container = _is_container() except Exception: running_in_container = False - # Inside our container docker-in-docker isn't set up, so the local backend is the intended one: skip the # noisy "docker not found" warning. An explicit TERMINAL_ENV=docker (mounted docker.sock) still gets checked. if running_in_container and terminal_env != "docker": @@ -460,7 +454,7 @@ def _check_tool_availability(should_fix: bool, f: Finding) -> None: for tid in available: check_ok(TOOLSET_REQUIREMENTS.get(tid, {}).get("name", tid), _doctor_tool_availability_detail(tid)) for status, label, detail in web_rows: - CHECK_ROW[status](label, detail) + (check_ok if status == "ok" else check_warn)(label, detail) for item in unavailable: env_vars = item.get("missing_vars") or item.get("env_vars") or [] check_warn(item["name"], f"(missing {', '.join(env_vars)})" if env_vars else "(system dependency not met)") diff --git a/hermes_cli/dump.py b/hermes_cli/dump.py index 2070e507e7..c56b8836d9 100644 --- a/hermes_cli/dump.py +++ b/hermes_cli/dump.py @@ -76,7 +76,6 @@ def _gateway_status() -> str: """Return a short gateway status string.""" try: from hermes_cli.gateway import get_gateway_runtime_snapshot - snapshot = get_gateway_runtime_snapshot() if snapshot.running: mode = "manual" if snapshot.has_process_service_mismatch else snapshot.manager @@ -142,7 +141,6 @@ _INTERESTING_PATHS = ( def _config_overrides(config: dict) -> dict[str, str]: """Find non-default config values worth reporting.""" from hermes_cli.config import DEFAULT_CONFIG - overrides = {} for section, key in _INTERESTING_PATHS: default_section = DEFAULT_CONFIG.get(section, {}) @@ -214,7 +212,6 @@ def _api_key_lines(show_keys: bool) -> list[str]: if not val and label == "openrouter": try: from agent.credential_pool import load_pool as _load_pool - if _load_pool("openrouter").has_credentials(): display = "set (auth pool)" except Exception: