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:
Hermes Agent
2026-09-25 12:27:19 -05:00
committed by brooklyn!
parent 3bbe5c593a
commit 954d94c3b0
3 changed files with 128 additions and 9 deletions

View File

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

View File

@@ -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.",
])

View 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