Skip to content

fix(gateway): resolve uv-managed pythonw for Windows scheduled-task wrapper - #30312

Closed
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/gateway-windows-cmd-script-uv-pythonw-30308
Closed

briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/gateway-windows-cmd-script-uv-pythonw-30308

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

What does this PR do?

hermes gateway start on Windows pops up a visible cmd.exe console window when Hermes is installed inside a uv-managed venv. Root cause is in hermes_cli/gateway_windows.py::_build_gateway_cmd_script, which derives the GUI interpreter via _derive_venv_pythonw. That helper only checks for a sibling pythonw.exe under venv/Scripts/ — on uv venvs that sibling is a ~44KB launcher shim that respawns the base python.exe (console subsystem), so the Scheduled Task ends up running console Python despite asking for pythonw.exe.

The direct-spawn path (_build_gateway_argv) already handles this correctly via _resolve_detached_python, 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. This PR switches the Scheduled-Task .cmd generator to use the same resolver so both code paths mirror each other.

Mirrors _resolve_detached_python precedence used by _build_gateway_argv: pyvenv.cfg.uv + home= → base pythonw.exe + PYTHONPATH injection; non-uv venvs fall through to the previous _derive_venv_pythonw behavior.

Related Issue

Fixes #30308

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/gateway_windows.py_build_gateway_cmd_script now calls _resolve_detached_python(python_path) instead of _derive_venv_pythonw(python_path), and emits a set "PYTHONPATH=<working_dir>;<site-packages>" line when the resolver returns extra PYTHONPATH entries (uv-venv case). VIRTUAL_ENV continues to point at the venv dir.
  • tests/hermes_cli/test_gateway_windows.py — new test_build_gateway_cmd_script_uses_base_pythonw_for_uv_venv_launcher mirrors the existing _build_gateway_argv test: arranges a uv-shaped venv (pyvenv.cfg with uv = 0.11.14 + home = <base>), calls _build_gateway_cmd_script directly, and asserts the emitted script (a) references the base pythonw.exe, not the venv shim, (b) sets VIRTUAL_ENV to the resolved venv dir, and (c) sets PYTHONPATH to include both the project working dir and the venv site-packages.

How to Test

  1. On a Windows host with Hermes installed in a uv-managed venv: hermes gateway install then trigger the Scheduled Task. Before this PR a visible cmd.exe window appears; after this PR the task runs hidden.
  2. uv run --with pytest --with pytest-xdist --with pytest-asyncio --with pytest-timeout python3 -m pytest tests/hermes_cli/test_gateway_windows.py -v — all 22 tests pass, including the new regression test (reverting the production change alone makes only the new test fail).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run focused tests for the touched code and all pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15.x (Windows behavior verified via regression test)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — Windows-specific path; the resolver already gates on pyvenv.cfg.uv so non-uv venvs and non-Windows hosts are unaffected.
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Sibling code paths

Sibling code paths that may need the same fix: _launch_elevated_gateway_command at line 156-174 also calls _derive_venv_pythonw(sys.executable) for the UAC-elevated handoff. That call site uses ShellExecuteW with SW_HIDE, so the visible-window symptom does not reproduce there in the same way, but on a uv venv the elevated child would still respawn through the launcher shim. Intentionally left out of this PR's scope to keep the diff small and the test surface focused; happy to widen if preferred.

Screenshots / Logs

The issue includes a screenshot of the rogue cmd.exe window on Windows; after this PR the Scheduled Task action completes without surfacing a console window because the base pythonw.exe is genuinely a GUI-subsystem binary.

Copilot AI review requested due to automatic review settings May 22, 2026 08:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates the Windows Scheduled Task .cmd wrapper generation to match the detached-spawn behavior for uv-managed virtual environments, ensuring the base pythonw.exe is used and required paths are injected for imports.

Changes:

  • Update _build_gateway_cmd_script to use _resolve_detached_python() and conditionally set PYTHONPATH for uv venvs.
  • Add a unit test to verify the .cmd wrapper uses base pythonw.exe (not the venv shim) and includes expected env vars/paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
tests/hermes_cli/test_gateway_windows.py Adds coverage for uv venv .cmd generation behavior (base pythonw.exe + PYTHONPATH).
hermes_cli/gateway_windows.py Aligns Windows .cmd wrapper with uv venv detached interpreter resolution and import path injection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/gateway_windows.py Outdated
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])
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)
Comment on lines +322 to +326
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}"')
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery labels May 22, 2026
@briandevans

Copy link
Copy Markdown
Contributor Author

CI audit — the test job failure on this PR is 16 pre-existing baseline failures in tests/tools/test_transcription*.py, none in this PR's touched code (hermes_cli/gateway_windows.py). All 16 reproduce on clean origin/main@d617858896 with identical error text:

Test Symptom Root cause on main
test_transcription.py::TestGetProvider::test_* (×2) AssertionError: assert 'local' == 'none' _try_lazy_install_stt() (added in b5c6d9ac0 ~3h ago) calls importlib.util.find_spec('faster_whisper'), which returns truthy in CI even when the test patches _HAS_FASTER_WHISPER=False
test_transcription.py::TestTranscribeLocal::test_not_installed 'not installed' in 'Local transcription failed: [Errno 2] No such file or directory: /tmp/test.ogg' same — lazy-install probe re-imports the real faster_whisper past the static patch
test_transcription_tools.py::Test* (×12) same family of 'local' == '<expected>' mismatches same
test_transcription_dotenv_fallback.py::test_auto_detect_sees_dotenv_groq 'local' == 'groq' same

Fix is in flight in #30334 (mine, opened ~1h after the regression landed) — adds an opt-in disable_lazy_stt_install fixture in tests/tools/conftest.py that disarms the lazy-install probe so the existing _HAS_FASTER_WHISPER patches still simulate "not installed" as intended. Zero failures in this PR's touched code.

@briandevans

Copy link
Copy Markdown
Contributor Author

@copilot All three findings addressed in commit 8b9975c71:

  • hermes_cli/gateway_windows.py line 325 — replaced os.pathsep with literal ";" when building the PYTHONPATH baked into the .cmd. Added a short comment explaining the cmd.exe target.
  • tests/hermes_cli/test_gateway_windows.py line 111 — same change on the assertion side; the test now splits on literal ";" regardless of host OS.
  • _build_gateway_cmd_script docstring updated to note PYTHONPATH is conditionally exported on uv-managed venvs.

tests/hermes_cli/test_gateway_windows.py (22 tests) re-run locally, all pass.

@briandevans
briandevans force-pushed the fix/gateway-windows-cmd-script-uv-pythonw-30308 branch from 8b9975c to 7fb962d Compare May 24, 2026 08:17
@briandevans
briandevans force-pushed the fix/gateway-windows-cmd-script-uv-pythonw-30308 branch from 7fb962d to 617bd23 Compare May 26, 2026 16:16
@briandevans
briandevans force-pushed the fix/gateway-windows-cmd-script-uv-pythonw-30308 branch from 617bd23 to b1ef4a4 Compare May 28, 2026 21:15
@briandevans
briandevans force-pushed the fix/gateway-windows-cmd-script-uv-pythonw-30308 branch from b1ef4a4 to 42e0c12 Compare May 30, 2026 01:11
@briandevans

Copy link
Copy Markdown
Contributor Author

@copilot Re-anchoring on the current head (42e0c129d) — all three findings are already addressed on this commit:

  1. _build_gateway_cmd_script PATH separator (hermes_cli/gateway_windows.py): PYTHONPATH is built with the literal Windows separator ";".join([working_dir, *extra_pythonpath]), not os.pathsep, so the emitted .cmd stays valid even when the script is generated on a non-Windows host (CI / Linux / macOS). An inline comment documents the rationale. This generator deliberately does not inline a PATH override at all (it inherits the per-user PATH of the Scheduled Task), so there is no PATH-join to harden. The remaining os.pathsep in _prepend_pythonpath belongs to the native _build_gateway_argv subprocess path, which only runs on the Windows host itself where os.pathsep is correctly ;.

  2. Test separator (tests/hermes_cli/test_gateway_windows.py): the .cmd-generation test parses PYTHONPATH with pythonpath_value.split(";") (literal Windows separator), with a comment noting the generated script is Windows-targeted regardless of host OS. The two os.pathsep splits that remain are in test_build_gateway_argv_*, which exercises the runtime native-env path whose value is built with os.pathsep — consistent with the code under test.

  3. Docstring var list (_build_gateway_cmd_script): the docstring lists HERMES_HOME, PYTHONIOENCODING, VIRTUAL_ENV and explicitly notes that on uv-managed venvs it also exports PYTHONPATH (with the reason). It matches the env set the function actually emits.

No code change required this cycle — the fixes were folded into the current head.

…rapper

`_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 NousResearch#30308.
Address Copilot review on NousResearch#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.
@briandevans
briandevans force-pushed the fix/gateway-windows-cmd-script-uv-pythonw-30308 branch from 42e0c12 to 6da8bd0 Compare May 30, 2026 20:12
@briandevans

Copy link
Copy Markdown
Contributor Author

Closing to focus the queue on security/file-safety work where civilian merges are landing. Happy to reopen if maintainers want this picked up.

@briandevans briandevans closed this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hermes gateway start pops up cmd.exe console window on Windows (uv venv + pythonw shim issue)

3 participants