{"repo":"NousResearch/hermes-agent","public_id":"24424","display_label":"Windows installer Python detection and venv fixes","summary":"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.","subsystem":"hermes_cli","counts":{"members":8,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":2,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":26586},"resolution":{"state":"partially_resolved","by":"pr-51602"},"freshness":{"state":"verified","last_verified_at":"2026-06-24T04:33:08+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.22714285714285712},"fix_edges":[{"issue_number":24424,"pr_number":26586,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":50769,"pr_number":51602,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":24424,"title":"[Bug]: Windows native installation fails if python not installed","url":null,"state":"closed"},{"kind":"issue","number":50769,"title":"[Bug]: Installer detects Python 3.12 fallback but still uses 3.11 for venv creation (fails)","url":null,"state":"closed"},{"kind":"pr","number":24543,"title":"fix(install): skip Microsoft Store python.exe stub in Test-Python fallback (#24424)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"622c27e55","number":0},{"kind":"pr","value":"#26586","number":26586}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":26586,"title":"fix(install.ps1): restore EAP=Continue around uv python install, skip Store stub","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/install.ps1"],"fix_hunks":[{"file":"scripts/install.ps1","hunks":["@@ -145,19 +145,39 @@ function Test-Python {\n     # Python not found — use uv to install it (no admin needed!)\n     Write-Info \"Python $PythonVersion not found, installing via uv...\"\n     try {\n+        # Temporarily relax ErrorActionPreference: uv writes download progress\n+        # (\"Downloading cpython-3.11.15-windows-x86_64-none (24.5MiB)\") to\n+        # stderr.  With $ErrorActionPreference = \"Stop\" (set at the top of this\n+        # script) PowerShell wraps stderr lines from native commands as\n+        # ErrorRecord objects when captured via 2>&1, then throws a terminating\n+        # exception on the first one — even though uv exits 0 and Python was\n+        # installed successfully.  Verify success via `uv python find`\n+        # afterwards, which is the reliable signal regardless of exit-code\n+        # semantics or stderr noise.  This fix was previously landed as\n+        # commit ec1714e71 and then lost in a release squash; reapplied here.\n+        $prevEAP = $ErrorActionPreference\n+        $ErrorActionPreference = \"Continue\"\n         $uvOutput = & $UvCmd python install $PythonVersion 2>&1\n-        if ($LASTEXITCODE -eq 0) {\n-            $pythonPath = & $UvCmd python find $PythonVersion 2>$null\n-            if ($pythonPath) {\n-                $ver = & $pythonPath --version 2>$null\n-                Write-Success \"Python installed: $ver\"\n-                return $true\n-            }\n-        } else {\n+        $uvExitCode = $LASTEXITCODE\n+        $ErrorActionPreference = $prevEAP\n+\n+        # Check if Python is now available (more reliable than exit code\n+        # since uv may return non-zero due to \"already installed\" etc.)\n+        $pythonPath = & $UvCmd python find $PythonVersion 2>$null\n+        if ($pythonPath) {\n+            $ver = & $pythonPath --version 2>$null\n+            Write-Success \"Python installed: $ver\"\n+            return $true\n+        }\n+\n+        # uv ran but Python still not findable — show what happened\n+        if ($uvExitCode -ne 0) {\n             Write-Warn \"uv python install output:\"\n             Write-Host $uvOutput -ForegroundColor DarkGray\n         }\n     } catch {\n+        # Restore EAP in case the try block threw before the assignment\n+        if ($prevEAP) { $ErrorActionPreference = $prevEAP }\n         Write-Warn \"uv python install error: $_\"\n     }\n ","@@ -175,15 +195,42 @@ function Test-Python {\n         } catch { }\n     }\n \n-    # Fallback: try system python\n-    if (Get-Command python -ErrorAction SilentlyContinue) {\n-        $sysVer = python --version 2>$null\n-        if ($sysVer -match \"3\\.(1[0-9]|[1-9][0-9])\") {\n-            Write-Success \"Using system Python: $sysVer\"\n-            return $true\n+    # Fallback: try system python — but skip the Microsoft Store stub.\n+    # On Windows, %LOCALAPPDATA%\\Microsoft\\WindowsApps\\python.exe is a 0-byte\n+    # reparse-point stub that prints \"Python was not found; run without\n+    # arguments to install from the Microsoft Store...\" to stdout and exits\n+    # non-zero.  Get-Command finds it; invoking it produces a confusing error\n+    # that the user sees as our installer crashing.\n+    $pythonCmd = Get-Command python -ErrorAction SilentlyContinue\n+    if ($pythonCmd) {\n+        $isStoreStub = $false\n+        try {\n+            $pythonSource = $pythonCmd.Source\n+            if ($pythonSource -and $pythonSource -like \"*\\WindowsApps\\*\") {\n+                $isStoreStub = $true\n+            } else {\n+                # Even outside WindowsApps, a 0-byte file is the stub\n+                $item = Get-Item $pythonSource -ErrorAction SilentlyContinue\n+                if ($item -and $item.Length -eq 0) { $isStoreStub = $true }\n+            }\n+        } catch { }\n+\n+        if (-not $isStoreStub) {\n+            try {\n+                $prevEAP2 = $ErrorActionPreference\n+                $ErrorActionPreference = \"Continue\"\n+                $sysVer = & python --version 2>&1\n+                $ErrorActionPreference = $prevEAP2\n+                if ($sysVer -match \"Python 3\\.(1[0-9]|[1-9][0-9])\") {\n+                    Write-Success \"Using system Python: $sysVer\"\n+                    return $true\n+                }\n+            } catch {\n+                if ($prevEAP2) { $ErrorActionPreference = $prevEAP2 }\n+            }\n         }\n     }\n-    \n+\n     Write-Err \"Failed to install Python $PythonVersion\"\n     Write-Info \"Install Python 3.11 manually, then re-run this script:\"\n     Write-Info \"  https://www.python.org/downloads/\"\n"]}]},{"kind":"pr","number":50790,"title":"fix(installer): re-resolve Python fallback at venv stage on Windows (#50769)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"23683c335","number":0},{"kind":"commit","value":"34bd6a0db","number":0},{"kind":"pr","value":"#51602","number":51602}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":50840,"title":"fix(installer): use detected Python fallback for venv creation","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":50850,"title":"fix(installer): use detected Python fallback for venv creation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50790","number":50790}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":51602,"title":"fix(installer): re-resolve Python fallback at venv stage on Windows (#50769)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/install.ps1","tests/test_install_ps1_python_fallback_venv.py"],"fix_hunks":[{"file":"scripts/install.ps1","hunks":["@@ -139,6 +139,11 @@ foreach ($tmpVar in @('TEMP', 'TMP')) {\n $RepoUrlSsh = \"[redacted:email]:NousResearch/hermes-agent.git\"\n $RepoUrlHttps = \"https://github.com/NousResearch/hermes-agent.git\"\n $PythonVersion = \"3.11\"\n+# Minor versions the installer accepts when the requested $PythonVersion isn't\n+# available, in preference order.  uv discovers both uv-managed and system\n+# interpreters, so this list also matches a pre-existing system Python.  Single\n+# source of truth shared by Test-Python's fallback and Resolve-AvailablePythonVersion.\n+$PythonFallbackVersions = @(\"3.12\", \"3.13\", \"3.10\")\n $NodeVersion = \"22\"\n \n # Stage-protocol version.  Bumped only for genuinely breaking changes to the","@@ -510,6 +515,31 @@ function Resolve-UvCmd {\n     throw \"uv is not installed. Run install.ps1 -Stage uv first.\"\n }\n \n+function Resolve-AvailablePythonVersion {\n+    # Return the first Python minor version uv can actually find, preferring the\n+    # requested $PythonVersion and then $PythonFallbackVersions.  Returns $null\n+    # when none are available.\n+    #\n+    # This is the cross-process-safe counterpart to Test-Python's in-memory\n+    # ``$script:PythonVersion = $fallbackVer`` mutation.  Under Hermes-Setup.exe\n+    # each ``-Stage NAME`` runs in a *fresh* powershell.exe, so the fallback the\n+    # ``python`` stage settled on (e.g. 3.12 when 3.11 is absent) does NOT\n+    # survive into the ``venv`` stage's process -- there $PythonVersion is back\n+    # at its \"3.11\" default.  Consumers re-resolve here instead of trusting that\n+    # default, which is exactly the propagation gap behind issue #50769.\n+    $candidates = @($PythonVersion) + $PythonFallbackVersions\n+    $seen = @{}\n+    foreach ($ver in $candidates) {\n+        if (-not $ver -or $seen.ContainsKey($ver)) { continue }\n+        $seen[$ver] = $true\n+        try {\n+            $found = & $UvCmd python find $ver 2>$null\n+            if ($found) { return $ver }\n+        } catch { }\n+    }\n+    return $null\n+}\n+\n function Test-Python {\n     Write-Info \"Checking Python $PythonVersion...\"\n     ","@@ -566,7 +596,7 @@ function Test-Python {\n \n     # Fallback: check if ANY Python 3.10+ is already available on the system\n     Write-Info \"Trying to find any existing Python 3.10+...\"\n-    foreach ($fallbackVer in @(\"3.12\", \"3.13\", \"3.10\")) {\n+    foreach ($fallbackVer in $PythonFallbackVersions) {\n         try {\n             $pythonPath = & $UvCmd python find $fallbackVer 2>$null\n             if ($pythonPath) {","@@ -1513,7 +1543,19 @@ function Install-Venv {\n         Write-Info \"Skipping virtual environment (-NoVenv)\"\n         return\n     }\n-    \n+\n+    # Re-resolve the interpreter before creating the venv.  Under Hermes-Setup.exe\n+    # each stage runs in its own powershell.exe, so the fallback the `python`\n+    # stage picked (e.g. 3.12 when 3.11 is absent) did NOT propagate into this\n+    # fresh process -- $PythonVersion is back at its \"3.11\" default.  Trusting it\n+    # here made `uv venv venv --python 3.11` fail with exit 2 on machines without\n+    # 3.11 even though the `python` stage reported success (issue #50769).\n+    $resolved = Resolve-AvailablePythonVersion\n+    if ($resolved -and $resolved -ne $PythonVersion) {\n+        Write-Info \"Python $PythonVersion not available; using detected Python $resolved\"\n+        $script:PythonVersion = $resolved\n+    }\n+\n     Write-Info \"Creating virtual environment with Python $PythonVersion...\"\n     \n     Push-Location $InstallDir"]},{"file":"tests/test_install_ps1_python_fallback_venv.py","hunks":["@@ -0,0 +1,113 @@\n+\"\"\"Regression: the Windows installer must honor its Python fallback at venv time.\n+\n+A user on Windows 11 reported (#50769) that the installer correctly detected a\n+Python 3.12 fallback when 3.11 was absent::\n+\n+    [OK] Found fallback: Python 3.12.8\n+    ...\n+    -> Creating virtual environment with Python 3.11...\n+\n+and then failed with ``Failed to create virtual environment (uv venv exited\n+with 2)``.\n+\n+Root cause: ``Test-Python`` records the fallback via an in-memory\n+``$script:PythonVersion = $fallbackVer`` mutation, but under Hermes-Setup.exe\n+each ``-Stage NAME`` runs in its *own* fresh ``powershell.exe`` process.  The\n+``venv`` stage therefore starts with ``$PythonVersion`` back at its ``\"3.11\"``\n+default, so ``uv venv venv --python 3.11`` runs on a machine that has no 3.11.\n+\n+The fix re-resolves the interpreter inside ``Install-Venv`` (via the\n+cross-process-safe ``Resolve-AvailablePythonVersion`` helper) before creating\n+the venv, so the venv stage uses whatever interpreter is actually present.\n+These tests lock that contract at the source level (the script only runs on\n+Windows, so there's no runner to execute it on Linux CI).\n+\"\"\"\n+\n+import re\n+from pathlib import Path\n+\n+import pytest\n+\n+_INSTALL_PS1 = Path(__file__).resolve().parents[1] / \"scripts\" / \"install.ps1\"\n+\n+\n[redacted:email](scope=\"module\")\n+def source() -> str:\n+    return _INSTALL_PS1.read_text(encoding=\"utf-8\")\n+\n+\n+def _function_body(source: str, name: str) -> str:\n+    \"\"\"Return the text of a PowerShell ``function <name> { ... }`` block.\"\"\"\n+    start = source.index(f\"function {name}\")\n+    brace = source.index(\"{\", start)\n+    depth = 0\n+    for i in range(brace, len(source)):\n+        if source[i] == \"{\":\n+            depth += 1\n+        elif source[i] == \"}\":\n+            depth -= 1\n+            if depth == 0:\n+                return source[brace : i + 1]\n+    raise AssertionError(f\"unterminated function body for {name}\")\n+\n+\n+def test_resolver_helper_is_defined(source: str):\n+    \"\"\"A cross-process-safe Python-version resolver must exist.\"\"\"\n+    assert \"function Resolve-AvailablePythonVersion\" in source, (\n+        \"expected a Resolve-AvailablePythonVersion helper that re-resolves the \"\n+        \"interpreter independently of the in-memory $script:PythonVersion mutation\"\n+    )\n+\n+\n+def test_install_venv_reresolves_before_creating_venv(source: str):\n+    \"\"\"Install-Venv must re-resolve the interpreter BEFORE `uv venv`.\n+\n+    Otherwise the venv stage's fresh process trusts the stale \"3.11\" default\n+    and fails on machines where only the fallback (e.g. 3.12) is installed.\n+    \"\"\"\n+    body = _function_body(source, \"Install-Venv\")\n+    resolve_at = body.find(\"Resolve-AvailablePythonVersion\")\n+    assert resolve_at != -1, (\n+        \"Install-Venv must call Resolve-AvailablePythonVersion so the venv \"\n+        \"stage doesn't trust the stale $PythonVersion default across processes\"\n+    )\n+    create_at = body.find(\"Creating virtual environment with Python\")\n+    assert create_at != -1, \"expected the venv-creation log line in Install-Venv\"\n+    assert resolve_at < create_at, (\n+        \"the interpreter must be re-resolved BEFORE the 'Creating virtual \"\n+        \"environment' step (and before `uv venv` runs)\"\n+    )\n+\n+\n+def test_fallback_list_is_single_source_of_truth(source: str):\n+    \"\"\"The fallback versions live in one shared constant, used by both paths.\n+\n+    A drifting second copy of the list is how detection and venv creation\n+    disagree in the first place.\n+    \"\"\"\n+    assert re.search(r\"\\$PythonFallbackVersions\\s*=\", source), (\n+        \"expected a shared $PythonFallbackVersions constant\"\n+    )\n+    # Test-Python's fallback loop must iterate the shared constant, not an\n+    # inline literal list.\n+    test_python = _function_body(source, \"Test-Python\")\n+    assert \"foreach ($fallbackVer in $PythonFallbackVersions)\" in test_python, (\n+        \"Test-Python must iterate the shared $PythonFallbackVersions constant\"\n+    )\n+    # The resolver must seed its candidate list from the same constant.\n+    resolver = _function_body(source, \"Resolve-AvailablePythonVersion\")\n+    assert \"$PythonFallbackVersions\" in resolver, (\n+        \"Resolve-AvailablePythonVersion must reuse the shared fallback constant\"\n+    )\n+\n+\n+def test_resolver_prefers_requested_version_then_fallbacks(source: str):\n+    \"\"\"The resolver tries the requested version first, then the fallbacks.\"\"\"\n+    resolver = _function_body(source, \"Resolve-AvailablePythonVersion\")\n+    assert \"@($PythonVersion) + $PythonFallbackVersions\" in resolver, (\n+        \"resolver candidate order must be: requested $PythonVersion first, \"\n+        \"then the shared fallbacks\"\n+    )\n+    assert \"uv\" in resolver and \"python find\" in resolver, (\n+        \"resolver must probe availability via `uv python find`\"\n+    )\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}