# NousResearch/hermes-agent · Windows installer Python detection and venv fixes

## Open now

- Nothing open on GitHub.

These issues and fixes address Windows-specific installer failures related to Python detection, including skipping Microsoft Store stub executables, re-resolving the Python fallback at venv creation time, and handling missing Python installations.

## TL;DR

- 8 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #26586. Verify it addresses your case.
- **Recommended:** read PR #26586 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-51602`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.23`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #24424: \[Bug\]: Windows native installation fails if python not installed
- #50769: \[Bug\]: Installer detects Python 3.12 fallback but still uses 3.11 for venv creation (fails)

## Prior & competing PRs

- **#26586: fix(install.ps1): restore EAP=Continue around uv python install, skip Store stub**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `scripts/install.ps1`
  - fix hunks:
  - `scripts/install.ps1`

    ```diff
    @@ -145,19 +145,39 @@ function Test-Python {
         # Python not found — use uv to install it (no admin needed!)
         Write-Info "Python $PythonVersion not found, installing via uv..."
         try {
    +        # Temporarily relax ErrorActionPreference: uv writes download progress
    +        # ("Downloading cpython-3.11.15-windows-x86_64-none (24.5MiB)") to
    +        # stderr.  With $ErrorActionPreference = "Stop" (set at the top of this
    +        # script) PowerShell wraps stderr lines from native commands as
    +        # ErrorRecord objects when captured via 2>&1, then throws a terminating
    +        # exception on the first one — even though uv exits 0 and Python was
    +        # installed successfully.  Verify success via `uv python find`
    +        # afterwards, which is the reliable signal regardless of exit-code
    +        # semantics or stderr noise.  This fix was previously landed as
    +        # commit ec1714e71 and then lost in a release squash; reapplied here.
    +        $prevEAP = $ErrorActionPreference
    +        $ErrorActionPreference = "Continue"
             $uvOutput = & $UvCmd python install $PythonVersion 2>&1
    -        if ($LASTEXITCODE -eq 0) {
    -            $pythonPath = & $UvCmd python find $PythonVersion 2>$null
    -            if ($pythonPath) {
    -                $ver = & $pythonPath --version 2>$null
    -                Write-Success "Python installed: $ver"
    -                return $true
    -            }
    -        } else {
    +        $uvExitCode = $LASTEXITCODE
    +        $ErrorActionPreference = $prevEAP
    +
    +        # Check if Python is now available (more reliable than exit code
    +        # since uv may return non-zero due to "already installed" etc.)
    +        $pythonPath = & $UvCmd python find $PythonVersion 2>$null
    +        if ($pythonPath) {
    +            $ver = & $pythonPath --version 2>$null
    +            Write-Success "Python installed: $ver"
    +            return $true
    +        }
    +
    +        # uv ran but Python still not findable — show what happened
    +        if ($uvExitCode -ne 0) {
                 Write-Warn "uv python install output:"
                 Write-Host $uvOutput -ForegroundColor DarkGray
             }
         } catch {
    +        # Restore EAP in case the try block threw before the assignment
    +        if ($prevEAP) { $ErrorActionPreference = $prevEAP }
             Write-Warn "uv python install error: $_"
         }
     
    ```

    ```diff
    @@ -175,15 +195,42 @@ function Test-Python {
             } catch { }
         }
     
    -    # Fallback: try system python
    -    if (Get-Command python -ErrorAction SilentlyContinue) {
    -        $sysVer = python --version 2>$null
    -        if ($sysVer -match "3\.(1[0-9]|[1-9][0-9])") {
    -            Write-Success "Using system Python: $sysVer"
    -            return $true
    +    # Fallback: try system python — but skip the Microsoft Store stub.
    +    # On Windows, %LOCALAPPDATA%\Microsoft\WindowsApps\python.exe is a 0-byte
    +    # reparse-point stub that prints "Python was not found; run without
    +    # arguments to install from the Microsoft Store..." to stdout and exits
    +    # non-zero.  Get-Command finds it; invoking it produces a confusing error
    +    # that the user sees as our installer crashing.
    +    $pythonCmd = Get-Command python -ErrorAction SilentlyContinue
    +    if ($pythonCmd) {
    +        $isStoreStub = $false
    +        try {
    +            $pythonSource = $pythonCmd.Source
    +            if ($pythonSource -and $pythonSource -like "*\WindowsApps\*") {
    +                $isStoreStub = $true
    +            } else {
    +                # Even outside WindowsApps, a 0-byte file is the stub
    +                $item = Get-Item $pythonSource -ErrorAction SilentlyContinue
    +                if ($item -and $item.Length -eq 0) { $isStoreStub = $true }
    +            }
    +        } catch { }
    +
    +        if (-not $isStoreStub) {
    +            try {
    +                $prevEAP2 = $ErrorActionPreference
    +                $ErrorActionPreference = "Continue"
    +                $sysVer = & python --version 2>&1
    +                $ErrorActionPreference = $prevEAP2
    +                if ($sysVer -match "Python 3\.(1[0-9]|[1-9][0-9])") {
    +                    Write-Success "Using system Python: $sysVer"
    +                    return $true
    +                }
    +            } catch {
    +                if ($prevEAP2) { $ErrorActionPreference = $prevEAP2 }
    +            }
             }
         }
    -    
    +
         Write-Err "Failed to install Python $PythonVersion"
         Write-Info "Install Python 3.11 manually, then re-run this script:"
         Write-Info "  https://www.python.org/downloads/"
    ```
- **#51602: fix(installer): re-resolve Python fallback at venv stage on Windows (#50769)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `scripts/install.ps1`, `tests/test_install_ps1_python_fallback_venv.py`
  - fix hunks:
  - `scripts/install.ps1`

    ```diff
    @@ -139,6 +139,11 @@ foreach ($tmpVar in @('TEMP', 'TMP')) {
     $RepoUrlSsh = "[redacted:email]:NousResearch/hermes-agent.git"
     $RepoUrlHttps = "https://github.com/NousResearch/hermes-agent.git"
     $PythonVersion = "3.11"
    +# Minor versions the installer accepts when the requested $PythonVersion isn't
    +# available, in preference order.  uv discovers both uv-managed and system
    +# interpreters, so this list also matches a pre-existing system Python.  Single
    +# source of truth shared by Test-Python's fallback and Resolve-AvailablePythonVersion.
    +$PythonFallbackVersions = @("3.12", "3.13", "3.10")
     $NodeVersion = "22"
     
     # Stage-protocol version.  Bumped only for genuinely breaking changes to the
    ```

    ```diff
    @@ -510,6 +515,31 @@ function Resolve-UvCmd {
         throw "uv is not installed. Run install.ps1 -Stage uv first."
     }
     
    +function Resolve-AvailablePythonVersion {
    +    # Return the first Python minor version uv can actually find, preferring the
    +    # requested $PythonVersion and then $PythonFallbackVersions.  Returns $null
    +    # when none are available.
    +    #
    +    # This is the cross-process-safe counterpart to Test-Python's in-memory
    +    # ``$script:PythonVersion = $fallbackVer`` mutation.  Under Hermes-Setup.exe
    +    # each ``-Stage NAME`` runs in a *fresh* powershell.exe, so the fallback the
    +    # ``python`` stage settled on (e.g. 3.12 when 3.11 is absent) does NOT
    +    # survive into the ``venv`` stage's process -- there $PythonVersion is back
    +    # at its "3.11" default.  Consumers re-resolve here instead of trusting that
    +    # default, which is exactly the propagation gap behind issue #50769.
    +    $candidates = @($PythonVersion) + $PythonFallbackVersions
    +    $seen = @{}
    +    foreach ($ver in $candidates) {
    +        if (-not $ver -or $seen.ContainsKey($ver)) { continue }
    +        $seen[$ver] = $true
    +        try {
    +            $found = & $UvCmd python find $ver 2>$null
    +            if ($found) { return $ver }
    +        } catch { }
    +    }
    +    return $null
    +}
    +
     function Test-Python {
         Write-Info "Checking Python $PythonVersion..."
         
    ```

    ```diff
    @@ -566,7 +596,7 @@ function Test-Python {
     
         # Fallback: check if ANY Python 3.10+ is already available on the system
         Write-Info "Trying to find any existing Python 3.10+..."
    -    foreach ($fallbackVer in @("3.12", "3.13", "3.10")) {
    +    foreach ($fallbackVer in $PythonFallbackVersions) {
             try {
                 $pythonPath = & $UvCmd python find $fallbackVer 2>$null
                 if ($pythonPath) {
    ```

    ```diff
    @@ -1513,7 +1543,19 @@ function Install-Venv {
             Write-Info "Skipping virtual environment (-NoVenv)"
             return
         }
    -    
    +
    +    # Re-resolve the interpreter before creating the venv.  Under Hermes-Setup.exe
    +    # each stage runs in its own powershell.exe, so the fallback the `python`
    +    # stage picked (e.g. 3.12 when 3.11 is absent) did NOT propagate into this
    +    # fresh process -- $PythonVersion is back at its "3.11" default.  Trusting it
    +    # here made `uv venv venv --python 3.11` fail with exit 2 on machines without
    +    # 3.11 even though the `python` stage reported success (issue #50769).
    +    $resolved = Resolve-AvailablePythonVersion
    +    if ($resolved -and $resolved -ne $PythonVersion) {
    +        Write-Info "Python $PythonVersion not available; using detected Python $resolved"
    +        $script:PythonVersion = $resolved
    +    }
    +
         Write-Info "Creating virtual environment with Python $PythonVersion..."
         
         Push-Location $InstallDir
    ```
  - `tests/test_install_ps1_python_fallback_venv.py`

    ```diff
    @@ -0,0 +1,113 @@
    +"""Regression: the Windows installer must honor its Python fallback at venv time.
    +
    +A user on Windows 11 reported (#50769) that the installer correctly detected a
    +Python 3.12 fallback when 3.11 was absent::
    +
    +    [OK] Found fallback: Python 3.12.8
    +    ...
    +    -> Creating virtual environment with Python 3.11...
    +
    +and then failed with ``Failed to create virtual environment (uv venv exited
    +with 2)``.
    +
    +Root cause: ``Test-Python`` records the fallback via an in-memory
    +``$script:PythonVersion = $fallbackVer`` mutation, but under Hermes-Setup.exe
    +each ``-Stage NAME`` runs in its *own* fresh ``powershell.exe`` process.  The
    +``venv`` stage therefore starts with ``$PythonVersion`` back at its ``"3.11"``
    +default, so ``uv venv venv --python 3.11`` runs on a machine that has no 3.11.
    +
    +The fix re-resolves the interpreter inside ``Install-Venv`` (via the
    +cross-process-safe ``Resolve-AvailablePythonVersion`` helper) before creating
    +the venv, so the venv stage uses whatever interpreter is actually present.
    +These tests lock that contract at the source level (the script only runs on
    +Windows, so there's no runner to execute it on Linux CI).
    +"""
    +
    +import re
    +from pathlib import Path
    +
    +import pytest
    +
    +_INSTALL_PS1 = Path(__file__).resolve().parents[1] / "scripts" / "install.ps1"
    +
    +
    [redacted:email](scope="module")
    +def source() -> str:
    +    return _INSTALL_PS1.read_text(encoding="utf-8")
    +
    +
    +def _function_body(source: str, name: str) -> str:
    +    """Return the text of a PowerShell ``function <name> { ... }`` block."""
    +    start = source.index(f"function {name}")
    +    brace = source.index("{", start)
    +    depth = 0
    +    for i in range(brace, len(source)):
    +        if source[i] == "{":
    +            depth += 1
    +        elif source[i] == "}":
    +            depth -= 1
    +            if depth == 0:
    +                return source[brace : i + 1]
    +    raise AssertionError(f"unterminated function body for {name}")
    +
    +
    +def test_resolver_helper_is_defined(source: str):
    +    """A cross-process-safe Python-version resolver must exist."""
    +    assert "function Resolve-AvailablePythonVersion" in source, (
    +        "expected a Resolve-AvailablePythonVersion helper that re-resolves the "
    +        "interpreter independently of the in-memory $script:PythonVersion mutation"
    +    )
    +
    +
    +def test_install_venv_reresolves_before_creating_venv(source: str):
    +    """Install-Venv must re-resolve the interpreter BEFORE `uv venv`.
    +
    +    Otherwise the venv stage's fresh process trusts the stale "3.11" default
    +    and fails on machines where only the fallback (e.g. 3.12) is installed.
    +    """
    +    body = _function_body(source, "Install-Venv")
    +    resolve_at = body.find("Resolve-AvailablePythonVersion")
    +    assert resolve_at != -1, (
    +        "Install-Venv must call Resolve-AvailablePythonVersion so the venv "
    +        "stage doesn't trust the stale $PythonVersion default across processes"
    +    )
    +    create_at = body.find("Creating virtual environment with Python")
    +    assert create_at != -1, "expected the venv-creation log line in Install-Venv"
    +    assert resolve_at < create_at, (
    +        "the interpreter must be re-resolved BEFORE the 'Creating virtual "
    +        "environment' step (and before `uv venv` runs)"
    +    )
    +
    +
    +def test_fallback_list_is_single_source_of_truth(source: str):
    +    """The fallback versions live in one shared constant, used by both paths.
    +
    +    A drifting second copy of the list is how detection and venv creation
    +    disagree in the first place.
    +    """
    +    assert re.search(r"\$PythonFallbackVersions\s*=", source), (
    +        "expected a shared $PythonFallbackVersions constant"
    +    )
    +    # Test-Python's fallback loop must iterate the shared constant, not an
    +    # inline literal list.
    +    test_python = _function_body(source, "Test-Python")
    +    assert "foreach ($fallbackVer in $PythonFallbackVersions)" in test_python, (
    +        "Test-Python must iterate the shared $PythonFallbackVersions constant"
    +    )
    +    # The resolver must seed its candidate list from the same constant.
    +    resolver = _function_body(source, "Resolve-AvailablePythonVersion")
    +    assert "$PythonFallbackVersions" in resolver, (
    +        "Resolve-AvailablePythonVersion must reuse the shared fallback constant"
    +    )
    +
    +
    +def test_resolver_prefers_requested_version_then_fallbacks(source: str):
    +    """The resolver tries the requested version first, then the fallbacks."""
    +    resolver = _function_body(source, "Resolve-AvailablePythonVersion")
    +    assert "@($PythonVersion) + $PythonFallbackVersions" in resolver, (
    +        "resolver candidate order must be: requested $PythonVersion first, "
    +        "then the shared fallbacks"
    +    )
    +    assert "uv" in resolver and "python find" in resolver, (
    +        "resolver must probe availability via `uv python find`"
    +    )
    ```
- **#24543: fix(install): skip Microsoft Store python.exe stub in Test-Python fallback (#24424)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `622c27e55`, `#26586`
- **#50790: fix(installer): re-resolve Python fallback at venv stage on Windows (#50769)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `23683c335`, `34bd6a0db`, `#51602`
- **#50840: fix(installer): use detected Python fallback for venv creation**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#50850: fix(installer): use detected Python fallback for venv creation**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#50790`

## Fix status

- Merged to main in PR #26586 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-24`.
- Subsystem: `hermes_cli`
