From 780c32d569d63223ed18f928b7f8dda37eeaa88a Mon Sep 17 00:00:00 2001 From: briandevans <252620095+briandevans@users.noreply.github.com> Date: Fri, 22 May 2026 01:12:37 -0700 Subject: [PATCH 1/2] fix(gateway): resolve uv-managed pythonw for Windows scheduled-task wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_build_gateway_cmd_script` derived the GUI interpreter via `_derive_venv_pythonw`, which only looks for a sibling `pythonw.exe` in `venv/Scripts/`. On uv-managed venvs that sibling is a ~44KB shim launcher that internally respawns the base `python.exe` (console subsystem), so the Scheduled Task surfaces a visible `cmd.exe` window even though we asked for `pythonw.exe`. Switch the .cmd generator to `_resolve_detached_python` — the same resolver `_build_gateway_argv` already uses for direct spawns — which inspects `pyvenv.cfg`, detects the `uv = ...` marker, and returns the base `pythonw.exe` plus the site-packages path that has to be injected on `PYTHONPATH` so imports still resolve without the venv launcher in the loop. Refs #30308. --- hermes_cli/gateway_windows.py | 15 +++++--- tests/hermes_cli/test_gateway_windows.py | 47 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/hermes_cli/gateway_windows.py b/hermes_cli/gateway_windows.py index a7f4b983dcbfd..8e23ac2bbe7b8 100644 --- a/hermes_cli/gateway_windows.py +++ b/hermes_cli/gateway_windows.py @@ -314,12 +314,17 @@ def _build_gateway_cmd_script( lines.append(f'set "HERMES_HOME={hermes_home}"') lines.append('set "PYTHONIOENCODING=utf-8"') lines.append('set "HERMES_GATEWAY_DETACHED=1"') - # VIRTUAL_ENV lets the gateway's own python detection find the venv - # if someone imports hermes_constants-based logic during startup. - venv_dir = str(Path(python_path).resolve().parent.parent) - lines.append(f'set "VIRTUAL_ENV={venv_dir}"') + # Mirror _build_gateway_argv: uv-managed venvs ship a shim pythonw.exe + # that respawns the base console python.exe, which surfaces as a visible + # cmd.exe window when launched from the Scheduled Task. Resolve to the + # base interpreter and inject site-packages on PYTHONPATH the same way + # the direct-spawn path does. + pythonw_path, venv_dir, extra_pythonpath = _resolve_detached_python(python_path) + lines.append(f'set "VIRTUAL_ENV={venv_dir.resolve()}"') + if extra_pythonpath: + pythonpath_value = os.pathsep.join([working_dir, *extra_pythonpath]) + lines.append(f'set "PYTHONPATH={pythonpath_value}"') - pythonw_path = _derive_venv_pythonw(python_path) prog_args = [pythonw_path, "-m", "hermes_cli.main"] if profile_arg: prog_args.extend(profile_arg.split()) diff --git a/tests/hermes_cli/test_gateway_windows.py b/tests/hermes_cli/test_gateway_windows.py index e613021982828..eeeb981862502 100644 --- a/tests/hermes_cli/test_gateway_windows.py +++ b/tests/hermes_cli/test_gateway_windows.py @@ -67,6 +67,53 @@ def test_build_gateway_argv_uses_base_pythonw_for_uv_venv_launcher(monkeypatch, assert str(site_packages) in env_overlay["PYTHONPATH"].split(gateway_windows.os.pathsep) +def test_build_gateway_cmd_script_uses_base_pythonw_for_uv_venv_launcher(monkeypatch, tmp_path): + """Scheduled-Task .cmd wrapper must mirror _build_gateway_argv on uv venvs. + + The venv/Scripts/pythonw.exe shipped by uv is a launcher shim that + respawns the base console python.exe, which renders as a visible cmd.exe + window when started by the Scheduled Task. The .cmd generator must + resolve to the base pythonw.exe and inject the venv site-packages on + PYTHONPATH the same way the direct-spawn path does. + """ + project = tmp_path / "project" + scripts = project / "venv" / "Scripts" + site_packages = project / "venv" / "Lib" / "site-packages" + base = tmp_path / "uv" / "python" / "cpython-3.11-windows-x86_64-none" + scripts.mkdir(parents=True) + site_packages.mkdir(parents=True) + base.mkdir(parents=True) + + venv_python = scripts / "python.exe" + venv_pythonw = scripts / "pythonw.exe" + base_pythonw = base / "pythonw.exe" + for exe in (venv_python, venv_pythonw, base_pythonw): + exe.write_text("", encoding="utf-8") + (project / "venv" / "pyvenv.cfg").write_text( + f"home = {base}\nimplementation = CPython\nuv = 0.11.14\nversion_info = 3.11.15\n", + encoding="utf-8", + ) + + content = gateway_windows._build_gateway_cmd_script( + str(venv_python), + str(project), + str(tmp_path / "hermes-home"), + "--profile alice", + ) + + assert str(base_pythonw) in content + assert str(venv_pythonw) not in content + assert f'set "VIRTUAL_ENV={(project / "venv").resolve()}"' in content + pythonpath_line = next( + line for line in content.splitlines() if line.startswith('set "PYTHONPATH=') + ) + pythonpath_value = pythonpath_line.split("=", 1)[1].rstrip('"') + pythonpath_entries = pythonpath_value.split(gateway_windows.os.pathsep) + assert str(project) in pythonpath_entries + assert str(site_packages) in pythonpath_entries + assert "gateway run" in content + + def _arrange_startup_fallback(monkeypatch, tmp_path, running_pids): script_path = tmp_path / "Hermes_Gateway_alice.cmd" startup_entry = tmp_path / "Startup" / "Hermes_Gateway_alice.cmd" From 6da8bd000986ea98fc22f72855308da1f4449913 Mon Sep 17 00:00:00 2001 From: briandevans <252620095+briandevans@users.noreply.github.com> Date: Fri, 22 May 2026 05:08:22 -0700 Subject: [PATCH 2/2] fixup(gateway): use literal Windows PYTHONPATH separator + docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review on #30312: - `_build_gateway_cmd_script` and its test both used `os.pathsep` to build/split the PYTHONPATH baked into the generated .cmd. Since that script is consumed by cmd.exe on Windows, the separator must be `;` literally — `os.pathsep` would yield `:` if the generator runs on Linux/macOS (cross-platform build tooling or unit tests on a non- Windows host). - Update the `_build_gateway_cmd_script` docstring to note PYTHONPATH is now conditionally exported when the resolver returns extra PYTHONPATH entries (uv venv case). No production behavior change on Windows; only the cross-platform robustness of the generator and its test. --- hermes_cli/gateway_windows.py | 9 ++++++++- tests/hermes_cli/test_gateway_windows.py | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/hermes_cli/gateway_windows.py b/hermes_cli/gateway_windows.py index 8e23ac2bbe7b8..9a20772000216 100644 --- a/hermes_cli/gateway_windows.py +++ b/hermes_cli/gateway_windows.py @@ -302,6 +302,9 @@ def _build_gateway_cmd_script( The script: - cd's into the project directory - exports HERMES_HOME, PYTHONIOENCODING, VIRTUAL_ENV + - on uv-managed venvs (per ``_resolve_detached_python``), also exports + PYTHONPATH so the base ``pythonw.exe`` can import the venv's + ``site-packages`` - invokes ``pythonw -m hermes_cli.main [--profile X] gateway run`` directly so the wrapper cmd.exe exits without a visible gateway console @@ -322,7 +325,11 @@ def _build_gateway_cmd_script( pythonw_path, venv_dir, extra_pythonpath = _resolve_detached_python(python_path) lines.append(f'set "VIRTUAL_ENV={venv_dir.resolve()}"') if extra_pythonpath: - pythonpath_value = os.pathsep.join([working_dir, *extra_pythonpath]) + # The emitted ``.cmd`` is consumed by cmd.exe on Windows, so use the + # Windows path separator literally rather than ``os.pathsep`` — the + # latter would yield ``:`` if this script were ever generated off + # Windows (e.g. cross-platform build tooling or unit tests). + pythonpath_value = ";".join([working_dir, *extra_pythonpath]) lines.append(f'set "PYTHONPATH={pythonpath_value}"') prog_args = [pythonw_path, "-m", "hermes_cli.main"] diff --git a/tests/hermes_cli/test_gateway_windows.py b/tests/hermes_cli/test_gateway_windows.py index eeeb981862502..a4d22513fcd13 100644 --- a/tests/hermes_cli/test_gateway_windows.py +++ b/tests/hermes_cli/test_gateway_windows.py @@ -108,7 +108,10 @@ def test_build_gateway_cmd_script_uses_base_pythonw_for_uv_venv_launcher(monkeyp line for line in content.splitlines() if line.startswith('set "PYTHONPATH=') ) pythonpath_value = pythonpath_line.split("=", 1)[1].rstrip('"') - pythonpath_entries = pythonpath_value.split(gateway_windows.os.pathsep) + # The generated script is Windows-targeted, so its PYTHONPATH always + # uses ``;`` as the separator regardless of the host OS running this + # test. Use the literal Windows separator instead of os.pathsep. + pythonpath_entries = pythonpath_value.split(";") assert str(project) in pythonpath_entries assert str(site_packages) in pythonpath_entries assert "gateway run" in content