diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 69ae0ce567..92fad1ecbd 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2640,6 +2640,28 @@ def _dashboard_sanitize_desktop_env(headless_backend) -> None: os.environ.pop("HERMES_SERVE_HEADLESS", None) +def _require_dashboard_web_deps() -> None: + """Exit with the right message when the dashboard's web-server packages can't import. + + A plain missing-package ImportError gets the standard repair guidance; the + ``DLL load failed ... _ssl`` signature of Windows Smart App Control blocking the + embedded runtime gets the policy guidance instead, so users stop looping on + repair for a block repair can never lift (#63796).""" + try: + import fastapi # noqa: F401 + import uvicorn # noqa: F401 + except ImportError as e: + from hermes_cli.main_dep_hints import ( + missing_optional_deps_message, + smart_app_control_block_message, + ) + + print(smart_app_control_block_message(e) or missing_optional_deps_message( + "dashboard", "its web-server packages (fastapi, uvicorn)", "all")) + print(f"Details: {e}") + sys.exit(1) + + def _dashboard_prepare_runtime(args, headless_backend) -> bool: """Deps check, skills seed, terminal env bridge, plugins, MCP discovery. @@ -2653,15 +2675,7 @@ def _dashboard_prepare_runtime(args, headless_backend) -> bool: except Exception: pass - try: - import fastapi # noqa: F401 - import uvicorn # noqa: F401 - except ImportError as e: - from hermes_cli.main_dep_hints import missing_optional_deps_message - - print(missing_optional_deps_message("dashboard", "its web-server packages (fastapi, uvicorn)", "all")) - print(f"Details: {e}") - sys.exit(1) + _require_dashboard_web_deps() # Seed bundled skills on first dashboard launch so the desktop GUI's # skills picker / agent skill discovery sees the bundled library. diff --git a/hermes_cli/main_dep_hints.py b/hermes_cli/main_dep_hints.py index 70fbb5381c..e2f5134468 100644 --- a/hermes_cli/main_dep_hints.py +++ b/hermes_cli/main_dep_hints.py @@ -7,3 +7,38 @@ def missing_optional_deps_message(surface: str, what: str, extra: str) -> str: "Run `hermes pm install` to prepare the declared dependencies.\n" "If an installed dependency is damaged, run `hermes pm repair`, then restart Hermes." ) + + +def smart_app_control_block_message(error: BaseException) -> "str | None": + """Guidance for Windows Smart App Control / Application Control blocking the embedded + Python runtime's ``_ssl`` module, or ``None`` when *error* is not that case. + + fastapi/uvicorn import ``ssl``; when the policy blocks the ``_ssl`` DLL, the import + fails with the signature ``DLL load failed ... _ssl`` — and the generic + missing-deps repair loop above can never fix it, so users looped on repair + (#63796). The desktop's embedded runtime is the usual victim; the CLI and the + gateway run on the system Python and stay unaffected. + """ + message = str(error) + if "DLL load failed" not in message or "_ssl" not in message: + return None + return "\n".join([ + "✗ The embedded Python runtime is blocked by Windows security policy.", + "", + "Root cause: Python's SSL module (_ssl) could not be loaded:", + f" {message}", + "", + "This happens when Windows Smart App Control or an Application Control", + "policy blocks the embedded Python runtime that ships with Hermes Desktop.", + "A repair / reinstall loop cannot fix this — the packages are not missing,", + "the runtime's DLL is being blocked.", + "", + "Recovery options:", + " 1. Use a trusted system Python installation instead of the embedded", + " runtime (if your organization allows it).", + " 2. Ask your IT administrator for an exemption for the Hermes Desktop", + " application (Smart App Control / Application Control).", + " 3. Use the CLI or gateway instead (they use your system Python).", + "", + "See https://aka.ms/smartappcontrol for Windows Smart App Control details.", + ]) diff --git a/tests/hermes_cli/test_smart_app_control_block.py b/tests/hermes_cli/test_smart_app_control_block.py new file mode 100644 index 0000000000..0ab10095ad --- /dev/null +++ b/tests/hermes_cli/test_smart_app_control_block.py @@ -0,0 +1,70 @@ +"""Windows Smart App Control detection in the dashboard deps check (#63796). + +When the policy blocks the embedded Python runtime's ``_ssl`` DLL, the +fastapi/uvicorn import fails with the ``DLL load failed ... _ssl`` signature. +The dashboard must say so instead of the generic missing-deps repair guidance +— repair can never lift a policy block, and users looped on it. +""" +from __future__ import annotations + +import builtins + +import pytest + +from hermes_cli import main +from hermes_cli.main_dep_hints import smart_app_control_block_message + +SAC_ERROR = ImportError( + "DLL load failed while importing _ssl: " + "An Application Control policy has blocked this file." +) + + +def _raise_for(names, error): + """``__import__`` stand-in that raises *error* for the given module names.""" + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name in names: + raise error + return real_import(name, *args, **kwargs) + + return fake_import + + +def test_smart_app_control_block_message_matches_the_dll_ssl_signature(): + message = smart_app_control_block_message(SAC_ERROR) + assert message is not None + assert "Smart App Control" in message + assert "embedded Python runtime" in message + assert "_ssl" in message + assert "repair" in message # must say the repair loop cannot fix this + assert "https://aka.ms/smartappcontrol" in message + + +def test_smart_app_control_block_message_ignores_other_errors(): + assert smart_app_control_block_message(ImportError("No module named 'fastapi'")) is None + assert smart_app_control_block_message(ImportError("cannot import name '_ssl' from 'ssl'")) is None + + +def test_dashboard_deps_check_names_the_policy_block(monkeypatch, capsys): + monkeypatch.setattr(builtins, "__import__", _raise_for({"fastapi"}, SAC_ERROR)) + with pytest.raises(SystemExit) as exc: + main._require_dashboard_web_deps() + assert exc.value.code == 1 + output = capsys.readouterr().out + assert "Smart App Control" in output + assert "embedded Python runtime" in output + assert "hermes pm install" not in output # the repair hint must not appear + + +def test_dashboard_deps_check_keeps_repair_guidance_for_plain_missing_deps( + monkeypatch, capsys): + monkeypatch.setattr( + builtins, "__import__", _raise_for({"fastapi"}, ImportError("No module named 'fastapi'"))) + with pytest.raises(SystemExit) as exc: + main._require_dashboard_web_deps() + assert exc.value.code == 1 + output = capsys.readouterr().out + assert "hermes pm install" in output + assert "Smart App Control" not in output