Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@

## [Unreleased]

## [v0.51.124] — 2026-05-24 — Release CV (stage-batch6 — 3-PR Windows-only stack — agent paths / docs / port hardening)

### Added

- **PR #2805** by @Koraji95-coder — `start.ps1`: expand hermes-agent candidate paths for Windows installers. The launcher now searches `$env:USERPROFILE\.hermes\hermes-agent`, the dev-checkout sibling, and the Windows installer roots (`$env:LOCALAPPDATA\hermes\hermes-agent`, `${env:ProgramW6432}\hermes\hermes-agent`, `${env:ProgramFiles}\hermes\hermes-agent`, `${env:ProgramFiles(x86)}\hermes\hermes-agent`) with `Select-Object -Unique` to collapse WOW64 ProgramFiles redirection collisions on 32-bit PowerShell processes. Adds `-PathType Container` to the `HERMES_WEBUI_AGENT_DIR` guard so a file named `hermes_cli` doesn't false-positive. Null-guards `${env:ProgramFiles(x86)}` for constrained environments where it's missing. Zero impact on Linux/macOS — file is `start.ps1`, never loaded by `start.sh` or `bootstrap.py`.

### Documentation

- **PR #2806** by @Koraji95-coder — Native Windows venv path corrected in `start.ps1` doc-comment and `README.md`. The previous text suggested "run bootstrap.py inside WSL2 once to create the venv, then this script can use that venv" — but a WSL2-created venv is `venv/bin/python` (ELF) and cannot be invoked by native Windows Python. The corrected guidance is to create a Windows venv natively (`python -m venv venv` from PowerShell), then `start.ps1` auto-discovers `venv\Scripts\python.exe`. WSL2 remains useful as a parallel install for the full `bootstrap.py` + Linux runtime path.

### Hardened

- **PR #2807** by @Koraji95-coder — `start.ps1`: `HERMES_WEBUI_PORT` env-var parsing uses `[int]::TryParse` + range guard (1-65535) instead of a bare `[int]` cast that threw `InvalidCastException` with no context on typos or accidental shell expansion. Server-process exit code is captured into `$script:serverExitCode` and emitted via `exit` AFTER the `try/finally` cleanup, so `Pop-Location` always runs (avoids leaving the caller stuck at `$RepoRoot` in interactive or dot-sourced sessions). Also drops a non-functional `@args` splat that PowerShell doesn't populate under `[CmdletBinding()]` — the launcher's existing use case is env-var-driven, no pass-through args needed.

## [v0.51.123] — 2026-05-24 — Release CU (stage-batch5 — 2-PR low-risk batch — gzip+ETag static caching / Open in VS Code)

### Performance
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ A community-maintained native Windows setup is documented at [@markwang2658/herm
- **Memory:** community-measured ~330 MB native vs ~1080 MB with WSL2+Docker (varies by configuration).
- **What works:** chat, workspace browser, session management, all themes.
- **Known limitations:** some POSIX-style file paths surface in the workspace browser; bash-assuming agent tools may not work natively.
- **WSL2 relationship:** WSL2 is recommended *once* for first-time venv creation (since `bootstrap.py` currently refuses on native Windows). After the venv exists, `start.ps1` at the repo root runs the WebUI natively by invoking `server.py` directly — no WSL2 needed for day-to-day use.
- **Native Windows setup:** install Python 3.11+, then from the hermes-agent root in PowerShell: `python -m venv venv` → `pip install -r requirements.txt` → `pwsh .\start.ps1` (it auto-discovers `venv\Scripts\python.exe`).
- **WSL2 relationship:** not a prerequisite — a WSL2-built venv (`venv/bin/python`, ELF) isn't invokable by native Windows Python, so use the native setup above. WSL2 stays useful as a parallel install if you want the full `bootstrap.py` + Linux runtime.

If provider setup is still incomplete after install, the onboarding wizard will point you to finish it with `hermes model` instead of trying to replicate the full CLI setup in-browser.
For a step-by-step walkthrough of the wizard, provider choices, local model server Base URLs, and safe re-runs, see [`docs/onboarding.md`](docs/onboarding.md).
Expand Down
80 changes: 65 additions & 15 deletions start.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@
server.py itself runs cleanly on native Windows.

Assumes Python + hermes-agent + the WebUI Python deps are already
installed - same assumption start.sh makes when invoked outside
a fresh bootstrap. For first-time setup, run bootstrap.py inside
WSL2 once to create the venv, then this script can use that venv.
installed natively on Windows - same assumption start.sh makes
when invoked outside a fresh bootstrap. For first-time setup, the
native Windows path is to install Python 3.11+, then create a
Windows venv (`python -m venv venv`) and `pip install -r
requirements.txt` from the hermes-agent root in PowerShell - this
script then finds `venv\Scripts\python.exe` automatically. A venv
created inside WSL2 is a Linux virtual environment (`venv/bin/python`)
and cannot be used by native Windows Python, so the bootstrap.py-
inside-WSL2 path produces a venv `start.ps1` can't invoke.

.PARAMETER Port
TCP port the WebUI binds to. Overrides HERMES_WEBUI_PORT env.
Expand Down Expand Up @@ -91,23 +97,35 @@ if (-not $Python) {
# that's about to crash on missing imports. Smoke-test feedback on
# PR #2783: nesquena/hermes-webui requested this guard.
$AgentDir = $env:HERMES_WEBUI_AGENT_DIR
if ($AgentDir -and -not (Test-Path (Join-Path $AgentDir 'hermes_cli'))) {
if ($AgentDir -and -not (Test-Path (Join-Path $AgentDir 'hermes_cli') -PathType Container)) {
Write-Error "HERMES_WEBUI_AGENT_DIR is set to '$AgentDir' but no hermes_cli/ folder exists there. Unset the variable to fall back to auto-discovery, or fix the path."
exit 1
}
if (-not $AgentDir) {
$candidates = @(
(Join-Path $env:USERPROFILE '.hermes\hermes-agent'),
(Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
)
# Build candidate list incrementally — ${env:ProgramFiles(x86)} is null on
# 32-bit Windows and in some constrained environments, and Join-Path throws
# on a null Path. Skip any system-wide root that isn't set so the launcher
# stays robust across Windows variants. USERPROFILE is always set so it
# stays unguarded; the dev-checkout sibling is path-derived, not env-based.
$candidates = @()
$candidates += (Join-Path $env:USERPROFILE '.hermes\hermes-agent')
foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramW6432}, ${env:ProgramFiles}, ${env:ProgramFiles(x86)})) {
if ($root) { $candidates += (Join-Path $root 'hermes\hermes-agent') }
}
$candidates += (Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
# De-dup: when running in a WOW64 (32-bit-on-64-bit) PowerShell process,
# $env:ProgramFiles is redirected to C:\Program Files (x86), so without
# $env:ProgramW6432 (the canonical 64-bit override) we'd miss the real
# C:\Program Files\hermes\hermes-agent AND duplicate the x86 entry.
# Select-Object -Unique collapses any collisions regardless of cause.
$candidates = $candidates | Select-Object -Unique
foreach ($c in $candidates) {
if (Test-Path (Join-Path $c 'hermes_cli')) { $AgentDir = $c; break }
if (Test-Path (Join-Path $c 'hermes_cli') -PathType Container) { $AgentDir = $c; break }
}
}
if (-not $AgentDir) {
$expectedPrimary = Join-Path $env:USERPROFILE '.hermes\hermes-agent'
$expectedSibling = Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent'
Write-Error "hermes-agent not found at $expectedPrimary or $expectedSibling. Set HERMES_WEBUI_AGENT_DIR explicitly."
$searched = $candidates -join ', '
Write-Error "hermes-agent not found. Searched: $searched. Set HERMES_WEBUI_AGENT_DIR explicitly to override."
exit 1
}

Expand All @@ -119,7 +137,26 @@ if (Test-Path $agentVenvPython) {

# === Resolve bind + state defaults =====================================
$BindHostFinal = if ($BindHost) { $BindHost } elseif ($env:HERMES_WEBUI_HOST) { $env:HERMES_WEBUI_HOST } else { '127.0.0.1' }
$PortFinal = if ($Port) { $Port } elseif ($env:HERMES_WEBUI_PORT) { [int]$env:HERMES_WEBUI_PORT } else { 8787 }
$PortFinal = if ($Port) {
$Port
} elseif ($env:HERMES_WEBUI_PORT) {
# TryParse + range guard on the env var. A plain [int] cast on the
# env var throws InvalidCastException with no actionable context when
# the env var is set to a non-integer (typo, accidental shell
# expansion, etc.) — surface a targeted error message instead.
$parsedPort = 0
if (-not [int]::TryParse($env:HERMES_WEBUI_PORT, [ref]$parsedPort)) {
Write-Error "HERMES_WEBUI_PORT='$($env:HERMES_WEBUI_PORT)' is not a valid integer port. Unset the variable to use the default (8787), or set it to a number 1-65535."
exit 1
}
if ($parsedPort -lt 1 -or $parsedPort -gt 65535) {
Write-Error "HERMES_WEBUI_PORT=$parsedPort is out of TCP-port range. Must be 1-65535."
exit 1
}
$parsedPort
} else {
8787
}
$env:HERMES_WEBUI_HOST = $BindHostFinal
$env:HERMES_WEBUI_PORT = "$PortFinal"
if (-not $env:HERMES_WEBUI_STATE_DIR) {
Expand Down Expand Up @@ -147,10 +184,23 @@ if (-not (Test-Path $serverPath)) {
exit 1
}

# Capture exit code, let finally{} run Pop-Location, exit AFTER the try.
# Plain `exit $LASTEXITCODE` inside the try block can prevent the finally
# from running in some termination paths (especially when dot-sourced or
# in interactive sessions), leaving the caller's working directory stuck
# at $RepoRoot.
$script:serverExitCode = 0
Push-Location $RepoRoot
try {
& $Python $serverPath @args
exit $LASTEXITCODE
# @args was non-functional here — PowerShell does NOT populate $args when the
# script declares [CmdletBinding()] with an explicit param() block (Copilot's
# finding on PR #2807). Dropped rather than added a ValueFromRemainingArguments
# parameter, because the existing tracked use case is the launcher running
# server.py with the env-var-driven config — no pass-through args are needed.
# If pass-through becomes a requirement later, add a [Parameter(ValueFromRemainingArguments=$true)] [string[]]$ServerArgs and splat that.
& $Python $serverPath
$script:serverExitCode = $LASTEXITCODE
} finally {
Pop-Location
}
exit $script:serverExitCode
Loading