fix(cli): name Windows Smart App Control when it blocks the dashboard runtime
When Windows Smart App Control or an Application Control policy blocks the embedded Python runtime's _ssl module, the fastapi/uvicorn import in the dashboard startup fails with the "DLL load failed ... _ssl" signature, but the handler printed the generic missing-deps message — so users looped on `hermes pm install` / `hermes pm repair`, which can never lift a policy block. Detect the signature and say so instead: the runtime's DLL is blocked, repair cannot fix it, and the recovery options are a trusted system Python, an IT exemption, or the CLI/gateway (which use the system Python). Fixes #63796 Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
])
|
||||
|
||||
70
tests/hermes_cli/test_smart_app_control_block.py
Normal file
70
tests/hermes_cli/test_smart_app_control_block.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user