mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 04:08:28 +08:00
Compare commits
3 Commits
bb/coding-
...
bb/install
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a12b0070 | ||
|
|
859fd752f4 | ||
|
|
1e7ccaa2b6 |
@@ -288,18 +288,25 @@ function Install-AgentBrowser {
|
||||
# Dependency checks
|
||||
# ============================================================================
|
||||
|
||||
# A uv that lives inside a conda/Anaconda environment carries its own
|
||||
# environment assumptions; pointed at the Hermes venv via VIRTUAL_ENV it breaks
|
||||
# dependency installs (reported on Windows with a conda uv first on PATH).
|
||||
# Treat such a uv as untrusted so resolution falls back to a standalone uv.
|
||||
# Mirrors hermes_cli/managed_uv.py's trust ordering for `hermes update`.
|
||||
function Test-UvUntrusted {
|
||||
param([string]$UvSource)
|
||||
if (-not $UvSource) { return $false }
|
||||
return ($UvSource -match '(?i)(anaconda|miniconda|miniforge|mambaforge|conda)')
|
||||
}
|
||||
|
||||
function Install-Uv {
|
||||
Write-Info "Checking for uv package manager..."
|
||||
|
||||
# Check if uv is already available
|
||||
if (Get-Command uv -ErrorAction SilentlyContinue) {
|
||||
$version = uv --version
|
||||
$script:UvCmd = "uv"
|
||||
Write-Success "uv found ($version)"
|
||||
return $true
|
||||
}
|
||||
|
||||
# Check common install locations
|
||||
|
||||
# Prefer a standalone uv in the known managed locations over whatever is
|
||||
# first on PATH. A bare `Get-Command uv` frequently resolves to a
|
||||
# conda/Anaconda-shipped uv; pointed at the Hermes venv via VIRTUAL_ENV its
|
||||
# environment assumptions collide and the install breaks. Mirrors the trust
|
||||
# ordering hermes_cli/managed_uv.py uses for `hermes update` (PR #37605).
|
||||
$uvPaths = @(
|
||||
"$env:USERPROFILE\.local\bin\uv.exe",
|
||||
"$env:USERPROFILE\.cargo\bin\uv.exe"
|
||||
@@ -312,6 +319,18 @@ function Install-Uv {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
# Fall back to a PATH uv only when it isn't a conda/Anaconda-managed one.
|
||||
$pathUv = Get-Command uv -ErrorAction SilentlyContinue
|
||||
if ($pathUv -and -not (Test-UvUntrusted $pathUv.Source)) {
|
||||
$version = & $pathUv.Source --version
|
||||
$script:UvCmd = "uv"
|
||||
Write-Success "uv found ($version)"
|
||||
return $true
|
||||
}
|
||||
if ($pathUv) {
|
||||
Write-Info "Ignoring conda-managed uv on PATH ($($pathUv.Source)); installing a standalone uv instead"
|
||||
}
|
||||
|
||||
# Install uv
|
||||
Write-Info "Installing uv (fast Python package manager)..."
|
||||
@@ -404,24 +423,11 @@ function Resolve-UvCmd {
|
||||
# Stale; fall through to re-discover.
|
||||
}
|
||||
|
||||
# Try PATH first (covers `winget install astral.uv`, manual installs,
|
||||
# and the post-Install-Uv state where uv.exe lives in
|
||||
# %USERPROFILE%\.local\bin which the installer added to PATH).
|
||||
if (Get-Command uv -ErrorAction SilentlyContinue) {
|
||||
$script:UvCmd = "uv"
|
||||
return
|
||||
}
|
||||
|
||||
# Refresh PATH from registry in case the current process started before
|
||||
# Install-Uv updated User PATH.
|
||||
$env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
if (Get-Command uv -ErrorAction SilentlyContinue) {
|
||||
$script:UvCmd = "uv"
|
||||
return
|
||||
}
|
||||
|
||||
# Check the well-known install locations the astral.sh installer drops
|
||||
# uv into. Mirrors the probe order Install-Uv uses.
|
||||
# Prefer the well-known managed locations the astral.sh installer drops uv
|
||||
# into over a bare PATH uv, which may be a conda/Anaconda uv that breaks the
|
||||
# Hermes venv install. Mirrors hermes_cli/managed_uv.py's trust ordering for
|
||||
# `hermes update` (PR #37605). Test-Path doesn't depend on PATH, so this is
|
||||
# also robust to the stale-PATH-in-a-fresh-stage-process case.
|
||||
foreach ($uvPath in @("$env:USERPROFILE\.local\bin\uv.exe", "$env:USERPROFILE\.cargo\bin\uv.exe")) {
|
||||
if (Test-Path $uvPath) {
|
||||
$script:UvCmd = $uvPath
|
||||
@@ -429,6 +435,31 @@ function Resolve-UvCmd {
|
||||
}
|
||||
}
|
||||
|
||||
# Try the current process PATH *before* mutating it, so a session-only
|
||||
# trusted uv (prepended for this shell but not persisted to the registry)
|
||||
# is still honored. Accept it only when it isn't conda/Anaconda-managed.
|
||||
$pathUv = Get-Command uv -ErrorAction SilentlyContinue
|
||||
if ($pathUv -and -not (Test-UvUntrusted $pathUv.Source)) {
|
||||
$script:UvCmd = "uv"
|
||||
return
|
||||
}
|
||||
|
||||
# Nothing trusted on the current PATH -- refresh from the registry in case
|
||||
# this process started before Install-Uv updated User PATH (each stage runs
|
||||
# in its own process), then re-check for a trusted uv.
|
||||
$env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
$registryUv = Get-Command uv -ErrorAction SilentlyContinue
|
||||
if ($registryUv -and -not (Test-UvUntrusted $registryUv.Source)) {
|
||||
$script:UvCmd = "uv"
|
||||
return
|
||||
}
|
||||
|
||||
# Be explicit about *why* resolution failed: a uv that exists but was
|
||||
# rejected as conda/Anaconda-managed is a different fix than no uv at all.
|
||||
$rejected = if ($registryUv) { $registryUv.Source } elseif ($pathUv) { $pathUv.Source } else { $null }
|
||||
if ($rejected) {
|
||||
throw "Only a conda/Anaconda-managed uv was found ($rejected); refusing to use it for the Hermes venv. Run install.ps1 -Stage uv to install a standalone uv."
|
||||
}
|
||||
throw "uv is not installed or not on PATH. Run install.ps1 -Stage uv first."
|
||||
}
|
||||
|
||||
|
||||
@@ -468,6 +468,22 @@ detect_os() {
|
||||
# Dependency checks
|
||||
# ============================================================================
|
||||
|
||||
# A uv that lives inside a conda/Anaconda environment carries its own
|
||||
# environment assumptions; pointed at the Hermes venv via VIRTUAL_ENV it breaks
|
||||
# dependency installs. Treat such a uv as untrusted so resolution falls back to
|
||||
# a standalone uv. Mirrors hermes_cli/managed_uv.py's trust ordering.
|
||||
uv_path_is_trusted() {
|
||||
# Normalize to lowercase first (bash-3.2-safe via tr, no ${var,,}) so
|
||||
# capitalized install dirs like "Miniconda3" / "Anaconda3" / "Miniforge3"
|
||||
# don't slip past the guard and reintroduce the PATH hijack.
|
||||
local _uv_lc
|
||||
_uv_lc="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
|
||||
case "$_uv_lc" in
|
||||
*conda*|*miniforge*|*mambaforge*) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_uv() {
|
||||
if [ "$DISTRO" = "termux" ]; then
|
||||
log_info "Termux detected — using Python's stdlib venv + pip instead of uv"
|
||||
@@ -477,15 +493,11 @@ install_uv() {
|
||||
|
||||
log_info "Checking for uv package manager..."
|
||||
|
||||
# Check common locations for uv
|
||||
if command -v uv &> /dev/null; then
|
||||
UV_CMD="uv"
|
||||
UV_VERSION=$($UV_CMD --version 2>/dev/null)
|
||||
log_success "uv found ($UV_VERSION)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check ~/.local/bin (default uv install location) even if not on PATH yet
|
||||
# Prefer a standalone uv in the known managed locations over whatever is
|
||||
# first on PATH. A bare `command -v uv` frequently resolves to a
|
||||
# conda/Anaconda-shipped uv; pointed at the Hermes venv via VIRTUAL_ENV its
|
||||
# environment assumptions collide and the install breaks. Mirrors the trust
|
||||
# ordering hermes_cli/managed_uv.py uses for `hermes update` (PR #37605).
|
||||
if [ -x "$HOME/.local/bin/uv" ]; then
|
||||
UV_CMD="$HOME/.local/bin/uv"
|
||||
UV_VERSION=$($UV_CMD --version 2>/dev/null)
|
||||
@@ -501,6 +513,19 @@ install_uv() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fall back to a PATH uv only when it isn't conda/Anaconda-managed.
|
||||
if command -v uv &> /dev/null; then
|
||||
local _path_uv
|
||||
_path_uv="$(command -v uv)"
|
||||
if uv_path_is_trusted "$_path_uv"; then
|
||||
UV_CMD="uv"
|
||||
UV_VERSION=$($UV_CMD --version 2>/dev/null)
|
||||
log_success "uv found ($UV_VERSION)"
|
||||
return 0
|
||||
fi
|
||||
log_info "Ignoring conda-managed uv on PATH ($_path_uv); installing a standalone uv instead"
|
||||
fi
|
||||
|
||||
# Install uv
|
||||
log_info "Installing uv (fast Python package manager)..."
|
||||
# Capture installer output so a failure shows the user WHY (network,
|
||||
|
||||
96
tests/test_install_trusted_uv_resolution.py
Normal file
96
tests/test_install_trusted_uv_resolution.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Regression test: installers prefer a managed/standalone uv over a bare PATH uv.
|
||||
|
||||
A bare ``command -v uv`` / ``Get-Command uv`` frequently resolves to a
|
||||
conda/Anaconda-shipped uv. Pointed at the Hermes venv via ``VIRTUAL_ENV`` that
|
||||
uv's own environment assumptions collide and the dependency install breaks
|
||||
(reported on Windows with a conda uv first on PATH). ``hermes update`` was fixed
|
||||
the same way in PR #37605 (``hermes_cli/managed_uv.py``); this guards the
|
||||
bootstrap installers so resolution checks the managed locations
|
||||
(``~/.local/bin`` / ``~/.cargo/bin``) before falling back to PATH, and skips a
|
||||
conda-managed PATH uv entirely.
|
||||
|
||||
These are ordering-contract assertions (managed-before-PATH), not data
|
||||
snapshots, so routine edits won't churn them.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
|
||||
INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1"
|
||||
|
||||
|
||||
def _body(text: str, signature: str) -> str:
|
||||
"""Return the body of a shell/PowerShell function, bounded by its opening
|
||||
signature and the next top-level ``}`` (a brace alone on its own line)."""
|
||||
_, _, rest = text.partition(signature)
|
||||
assert rest, f"Could not find {signature!r}"
|
||||
body, _, _ = rest.partition("\n}\n")
|
||||
assert body, f"Could not find closing brace for {signature!r}"
|
||||
return body
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# install.sh
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_install_sh_has_conda_trust_helper() -> None:
|
||||
text = INSTALL_SH.read_text(encoding="utf-8")
|
||||
helper = _body(text, "uv_path_is_trusted() {")
|
||||
assert "conda" in helper, "uv_path_is_trusted must reject conda-managed uv"
|
||||
|
||||
|
||||
def test_install_sh_prefers_managed_uv_over_path() -> None:
|
||||
text = INSTALL_SH.read_text(encoding="utf-8")
|
||||
body = _body(text, "install_uv() {")
|
||||
|
||||
local_idx = body.find('if [ -x "$HOME/.local/bin/uv" ]')
|
||||
path_idx = body.find("if command -v uv &> /dev/null")
|
||||
assert local_idx != -1, "install_uv must probe ~/.local/bin/uv"
|
||||
assert path_idx != -1, "install_uv must still fall back to a PATH uv"
|
||||
assert local_idx < path_idx, (
|
||||
"managed ~/.local/bin/uv must be preferred over a bare PATH uv"
|
||||
)
|
||||
assert "uv_path_is_trusted" in body, (
|
||||
"install_uv must gate the PATH fallback through uv_path_is_trusted"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# install.ps1
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_install_ps1_has_conda_trust_helper() -> None:
|
||||
text = INSTALL_PS1.read_text(encoding="utf-8")
|
||||
helper = _body(text, "function Test-UvUntrusted {")
|
||||
assert "conda" in helper.lower(), "Test-UvUntrusted must reject conda-managed uv"
|
||||
|
||||
|
||||
def test_install_ps1_install_uv_prefers_managed_over_path() -> None:
|
||||
text = INSTALL_PS1.read_text(encoding="utf-8")
|
||||
body = _body(text, "function Install-Uv {")
|
||||
|
||||
managed_idx = body.find(r"$env:USERPROFILE\.local\bin\uv.exe")
|
||||
path_idx = body.find("$pathUv = Get-Command uv")
|
||||
assert managed_idx != -1, "Install-Uv must probe the managed uv locations"
|
||||
assert path_idx != -1, "Install-Uv must still fall back to a PATH uv"
|
||||
assert managed_idx < path_idx, (
|
||||
"managed uv locations must be preferred over a bare PATH uv"
|
||||
)
|
||||
assert "Test-UvUntrusted" in body, (
|
||||
"Install-Uv must gate the PATH fallback through Test-UvUntrusted"
|
||||
)
|
||||
|
||||
|
||||
def test_install_ps1_resolve_uvcmd_prefers_managed_over_path() -> None:
|
||||
text = INSTALL_PS1.read_text(encoding="utf-8")
|
||||
body = _body(text, "function Resolve-UvCmd {")
|
||||
|
||||
managed_idx = body.find(r"$env:USERPROFILE\.local\bin\uv.exe")
|
||||
refresh_idx = body.find('GetEnvironmentVariable("Path", "User")')
|
||||
assert managed_idx != -1, "Resolve-UvCmd must probe the managed uv locations"
|
||||
assert refresh_idx != -1, "Resolve-UvCmd must still refresh PATH as a fallback"
|
||||
assert managed_idx < refresh_idx, (
|
||||
"managed uv locations must be checked before the PATH-refresh fallback"
|
||||
)
|
||||
assert "Test-UvUntrusted" in body, (
|
||||
"Resolve-UvCmd must gate the PATH fallback through Test-UvUntrusted"
|
||||
)
|
||||
Reference in New Issue
Block a user