Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,16 @@ def _node_bin(bin: str) -> str:
tui_dir,
include_child_workspaces=True,
)
# CREATE_NO_WINDOW: this call is spawned from the windowless
# pythonw.exe dashboard/gateway backend (e.g. a Windows Scheduled
# Task), but without this flag a console-subsystem child (npm.cmd)
# gets its own new console — visibly, if the user's default
# terminal handler is Windows Terminal (Settings > For developers >
# Terminal delegation) — even though the parent has no window of
# its own. Same pattern as the wmic scan above; see
# windows_hide_flags()'s docstring.
from hermes_cli._subprocess_compat import windows_hide_flags

result = subprocess.run(
[
npm,
Expand All @@ -2046,6 +2056,7 @@ def _node_bin(bin: str) -> str:
encoding="utf-8",
errors="replace",
env={**os.environ, "CI": "1"},
creationflags=windows_hide_flags(),
)
if result.returncode != 0:
combined = f"{result.stdout or ''}\n{result.stderr or ''}".strip()
Expand All @@ -2071,6 +2082,7 @@ def _node_bin(bin: str) -> str:
text=True,
encoding="utf-8",
errors="replace",
creationflags=windows_hide_flags(),
)
Comment on lines 2083 to 2086
if result.returncode != 0:
combined = f"{result.stdout or ''}{result.stderr or ''}".strip()
Expand Down Expand Up @@ -2101,6 +2113,7 @@ def _node_bin(bin: str) -> str:
text=True,
encoding="utf-8",
errors="replace",
creationflags=windows_hide_flags(),
)
if result.returncode != 0:
combined = f"{result.stdout or ''}{result.stderr or ''}".strip()
Expand Down
29 changes: 27 additions & 2 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14350,8 +14350,33 @@ def _ws_host_origin_reason(ws: "WebSocket") -> Optional[str]:
if not bound_host:
return None

# When the dashboard is loopback-bound but fronted by a reverse proxy
# (e.g. cloudflared + Cloudflare Access) that rewrites the Host header
# to ``localhost``, the browser still sends Origin: https://public.host
# because cloudflared does not rewrite Origin. Operators opt into this
# topology by setting ``dashboard.public_url`` — when present, accept
# requests whose Host/Origin matches either the bound host (local dev,
# SSH/Tailscale tunnels) OR the public URL's host.
public_host: str = ""
if bound_host in _LOOPBACK_HOSTS:
try:
from hermes_cli.dashboard_auth.prefix import resolve_public_url
import urllib.parse as _up
_purl = resolve_public_url()
if _purl:
public_host = (_up.urlparse(_purl).netloc or "").lower()
except Exception: # noqa: BLE001 — best-effort; never fail-closed on config lookup
public_host = ""

def _host_accepted(value: str) -> bool:
if _is_accepted_host(value, bound_host):
return True
if public_host and value and value.split(":", 1)[0].lower() == public_host:
return True
return False
Comment on lines +14360 to +14376

host_header = ws.headers.get("host", "")
if not _is_accepted_host(host_header, bound_host):
if not _host_accepted(host_header):
return f"host_mismatch host={host_header or '?'} bound={bound_host}"

origin = ws.headers.get("origin", "")
Expand All @@ -14368,7 +14393,7 @@ def _ws_host_origin_reason(ws: "WebSocket") -> Optional[str]:
if not parsed.netloc:
return f"origin_mismatch origin={origin} bound={bound_host}"

if not _is_accepted_host(parsed.netloc, bound_host):
if not _host_accepted(parsed.netloc):
return f"origin_mismatch origin={origin} bound={bound_host}"
return None

Expand Down
37 changes: 37 additions & 0 deletions tests/test_windows_subprocess_no_window_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,43 @@ def fake_run(cmd, **kwargs):



def test_tui_dependency_install_hides_npm_window(tmp_path, monkeypatch):
"""The TUI npm-install call (main.py, gated by _tui_need_npm_install) was
spawned with no creationflags at all — not even the weaker
windows_hide_flags() used elsewhere in this same file. On a system where
Windows Terminal is the default terminal-delegation handler, an
unflagged console-subsystem child (npm.cmd) gets its own new, VISIBLE
console even though the parent is a windowless pythonw.exe Scheduled
Task — confirmed empirically on a live Windows install 2026-07-16."""
from hermes_cli import main as main_mod
from hermes_cli import _subprocess_compat

tui_dir = tmp_path / "ui-tui"
tui_dir.mkdir()
(tui_dir / "package.json").write_text("{}")
(tui_dir / "dist" / "entry.js").parent.mkdir(parents=True)
(tui_dir / "dist" / "entry.js").write_text("console.log('tui')")
(tmp_path / "package-lock.json").write_text("{}")

monkeypatch.setattr(main_mod, "_tui_need_npm_install", lambda _root: True)
monkeypatch.setattr(main_mod, "_tui_need_rebuild", lambda _root: False)
monkeypatch.setattr(main_mod.shutil, "which", lambda name: f"C:/bin/{name}.cmd")
monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)
monkeypatch.setattr(_subprocess_compat, "windows_hide_flags", lambda: _CREATE_NO_WINDOW)

captured = []

def fake_run(cmd, **kwargs):
captured.append((cmd, kwargs))
return _Completed(stdout="", returncode=0)

monkeypatch.setattr(main_mod.subprocess, "run", fake_run)

main_mod._make_tui_argv(tui_dir, tui_dev=False)

npm_calls = _spawns(captured, "install", "--workspace", "ui-tui")
assert len(npm_calls) == 1, captured
assert npm_calls[0][1].get("creationflags") == _CREATE_NO_WINDOW



Expand Down
Loading