Release v0.51.236 — Release HD (stage-q7) (#3491)

## Release v0.51.236 — Release HD (stage-q7)

First Phase 3 (deep-review) release — picked by the 3-factor framework (contributor × impact × mitigated-risk): high-impact (#1952 native Windows support), backend-only (no screenshots), well-mitigated risk (POSIX path provably unchanged), from a contributor active this session (@rodboev, #3446/#3486 shipped earlier today).

### Added
| PR | Author | Fix |
|----|--------|-----|
| #1952 | @rodboev | Native Windows support for `bootstrap.py` + the embedded terminal: POSIX-only `fcntl`/`termios`/`select` guarded behind `_TERMINAL_SUPPORTED`; terminal entry points raise `NotImplementedError`/no-op on Windows; bootstrap Windows block → warning; auto-install errors clearly on native Windows (WSL unaffected); foreground uses `Popen`+exit on Windows instead of `os.execv`. **POSIX behavior unchanged on every path.** |

### Absorbed on the way in (fix-it-ourselves, reviewed fresh)
- `subprocess.CREATE_NEW_PROCESS_GROUP` → `getattr(subprocess, ..., 0)` — the constant is Windows-only, so a win32-simulating test `AttributeError`'d on Linux. Mirrors the `SO_EXCLUSIVEADDRUSE` getattr guard.
- Fixed 2 over-reaching tests in `test_windows_native_support.py` — one was launching a **real installer subprocess** via an unstubbed `subprocess.run` (now stubbed; harness 2.8s vs 80s); removed unused imports.
- Updated `test_onboarding_static.py` — it asserted the OLD "Native Windows is not supported" hard-block string this PR intentionally replaces; now asserts the new experimental-warning + auto-install guard.
- Help-text accuracy: `--foreground` help now describes the Windows Popen path (Opus nit).

### Gate results
- **Full pytest suite**: 7478 passed, 9 skipped, 3 xpassed, **0 failed**
- **ruff forward gate**: CLEAN
- **browser-smoke gate**: CLEAN (gate hardened mid-release to auto-detect the cached chromium revision)
- **Codex (regression)**: SAFE TO SHIP (simulated `sys.platform=win32`, verified POSIX modules not imported + all terminal guards complete + POSIX foreground still uses execv)
- **Opus (correctness)**: SAFE TO SHIP (POSIX path provably unchanged, all fcntl/termios/select guarded, Popen+exit correct; noted inherent-Windows trade-offs that aren't PR bugs)

Note: the Windows *runtime* path can't be executed on the Linux CI box; it was reviewed statically by both reviewers + the contributor's 209-line test (win32 simulated via monkeypatch). Linux/POSIX no-regression is fully verified.

Closes #1952.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
nesquena-hermesandrodboev authored and GitHub committed 2026-06-03 10:46:06 -07:00
1 parent aae584ba57
commit 6f68fdb6ff
5 files changed
+273 -12

No files matched your search

+5
View File
@@ -3,6 +3,11 @@
## [Unreleased]
## [v0.51.236] — 2026-06-03 — Release HD (stage-q7 — native Windows support for bootstrap and terminal)
### Added
- Native Windows support for `bootstrap.py` and the embedded terminal (#1952). Hermes WebUI already ran on Windows when invoked as `python server.py` directly; this unblocks the supported `python bootstrap.py` path. `api/terminal.py` no longer hard-imports the POSIX-only `fcntl`/`termios`/`select` at module load — they're guarded behind `_TERMINAL_SUPPORTED = sys.platform != "win32"`, and the embedded-terminal entry points raise `NotImplementedError` (or no-op) on Windows, following the existing optional-feature guard pattern (`api/turn_journal.py`, `api/providers.py`). The bootstrap native-Windows block becomes a warning instead of a hard `RuntimeError`; auto-install (which shells out to `/bin/bash`) still errors clearly on native Windows (WSL is unaffected), and the foreground launch path uses `subprocess.Popen` + exit on Windows (where `os.execv` spawns rather than replaces the process, orphaning it from a supervisor) instead of `os.execv`. POSIX behavior is unchanged on every path (#1952, @rodboev).
## [v0.51.235] — 2026-06-03 — Release HC (stage-q5 — no duplicate transcript replay on repeated questions after compression)
### Fixed
+24 -4
View File
@@ -11,20 +11,29 @@ from __future__ import annotations
import errno
import atexit
import codecs
import fcntl
import os
import queue
import select
import shutil
import signal
import struct
import subprocess
import termios
import sys
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
_TERMINAL_SUPPORTED = sys.platform != "win32"
if _TERMINAL_SUPPORTED:
import fcntl
import select
import termios
else:
fcntl = None # type: ignore[assignment]
select = None # type: ignore[assignment]
termios = None # type: ignore[assignment]
def _set_nonblocking(fd: int) -> None:
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
@@ -175,7 +184,8 @@ def _ensure_spawn_supervisor() -> None:
_spawn_supervisor_started = True
_ensure_spawn_supervisor()
if _TERMINAL_SUPPORTED:
_ensure_spawn_supervisor()
# NOTE on parent-death-signal: a previous version of this module set
@@ -257,6 +267,8 @@ def _set_size(term: TerminalSession, rows: int, cols: int) -> None:
def start_terminal(session_id: str, workspace: Path, rows: int = 24, cols: int = 80, restart: bool = False) -> TerminalSession:
"""Start or return the embedded terminal for a WebUI session."""
if not _TERMINAL_SUPPORTED:
raise NotImplementedError("Embedded terminal is not supported on Windows")
sid = str(session_id or "").strip()
if not sid:
raise ValueError("session_id is required")
@@ -346,6 +358,8 @@ def start_terminal(session_id: str, workspace: Path, rows: int = 24, cols: int =
def get_terminal(session_id: str) -> TerminalSession | None:
if not _TERMINAL_SUPPORTED:
return None
with _LOCK:
term = _TERMINALS.get(str(session_id or ""))
if term and term.is_alive():
@@ -354,6 +368,8 @@ def get_terminal(session_id: str) -> TerminalSession | None:
def write_terminal(session_id: str, data: str) -> None:
if not _TERMINAL_SUPPORTED:
raise NotImplementedError("Embedded terminal is not supported on Windows")
term = get_terminal(session_id)
if not term or not term.is_alive():
raise KeyError("terminal not running")
@@ -361,6 +377,8 @@ def write_terminal(session_id: str, data: str) -> None:
def resize_terminal(session_id: str, rows: int, cols: int) -> None:
if not _TERMINAL_SUPPORTED:
raise NotImplementedError("Embedded terminal is not supported on Windows")
term = get_terminal(session_id)
if not term:
raise KeyError("terminal not running")
@@ -368,6 +386,8 @@ def resize_terminal(session_id: str, rows: int, cols: int) -> None:
def close_terminal(session_id: str) -> bool:
if not _TERMINAL_SUPPORTED:
return False
sid = str(session_id or "")
with _LOCK:
term = _TERMINALS.pop(sid, None)
+25 -7
View File
@@ -92,9 +92,9 @@ def is_wsl() -> bool:
def ensure_supported_platform() -> None:
if platform.system() == "Windows" and not is_wsl():
raise RuntimeError(
"Native Windows is not supported for this bootstrap yet. "
"Please run it from Linux, macOS, or inside WSL2."
info(
"Warning: Native Windows bootstrap is experimental. "
"Embedded terminal and auto-install are not supported."
)
@@ -270,6 +270,11 @@ def hermes_command_exists() -> bool:
def install_hermes_agent() -> None:
if platform.system() == "Windows" and not is_wsl():
raise RuntimeError(
"Auto-install is not supported on native Windows. "
"Install hermes-agent manually first."
)
info(f"Hermes Agent not found. Attempting install via {INSTALLER_URL}")
subprocess.run(
["/bin/bash", "-lc", f"curl -fsSL {INSTALLER_URL} | bash"], check=True
@@ -316,8 +321,10 @@ def parse_args() -> argparse.Namespace:
"--foreground",
action="store_true",
help=(
"Run server.py in this process (via os.execv) instead of spawning a "
"child. Use this under launchd / systemd / supervisord so the "
"Run server.py in this process (via os.execv on POSIX; via a "
"Popen child + exit on Windows, where execv can't replace the "
"process image) instead of spawning a detached child. Use this "
"under launchd / systemd / supervisord so the "
"supervisor sees the long-lived server as the original child. "
"Implies --no-browser. Skips the post-launch health probe — the "
"supervisor's own KeepAlive / Restart=on-failure handles liveness."
@@ -444,8 +451,19 @@ def main() -> int:
f"Set HERMES_WEBUI_PYTHON to a working interpreter or fix "
f"the agent venv at {agent_dir}."
)
# os.execv replaces the current process image. Anything after this line
# only runs if execv itself fails (it raises OSError on failure).
# os.execv replaces the current process image. On Windows, execv
# spawns a new process instead of replacing (Python calls CreateProcess),
# orphaning it from any supervisor. Use Popen + exit there instead.
if sys.platform == "win32":
# CREATE_NEW_PROCESS_GROUP only exists in the subprocess module on
# Windows; resolve it defensively (0 = no extra flags) so this line
# can't AttributeError if reached on a non-Windows interpreter
# (e.g. a win32-simulating test) — mirrors the getattr() guard used
# for SO_EXCLUSIVEADDRUSE.
_CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
subprocess.Popen([python_exe, server_path],
creationflags=_CREATE_NEW_PROCESS_GROUP)
sys.exit(0)
os.execv(python_exe, [python_exe, server_path])
# Unreachable — execv either replaces the process or raises.
raise RuntimeError("os.execv returned unexpectedly")
+5 -1
View File
@@ -55,4 +55,8 @@ def test_bootstrap_script_contains_official_installer_and_windows_guard():
"https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh"
in src
)
assert "Native Windows is not supported" in src
# Native Windows is now experimental-supported (#1952), not hard-blocked:
# ensure_supported_platform() warns instead of raising, but auto-install
# (which shells out to /bin/bash) still guards native Windows explicitly.
assert "Native Windows bootstrap is experimental" in src
assert "Auto-install is not supported on native Windows" in src
+214
View File
@@ -0,0 +1,214 @@
"""Tests for native Windows support (terminal.py POSIX guards + bootstrap.py unblock).
terminal.py guards:
- _TERMINAL_SUPPORTED is False on Windows, True on POSIX
- Terminal functions raise NotImplementedError on Windows
- close_terminal returns False (no-op) on Windows
- get_terminal returns None on Windows
- Module imports cleanly on Windows (no fcntl/termios ImportError)
bootstrap.py unblock:
- ensure_supported_platform does not raise on Windows
- install_hermes_agent raises RuntimeError on Windows (calls /bin/bash)
- Foreground path uses Popen + sys.exit on Windows instead of os.execv
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(REPO_ROOT))
# ── terminal.py tests ────────────────────────────────────────────────────────
class TestTerminalPosixGuard:
"""Verify _TERMINAL_SUPPORTED flag and import guards."""
def test_terminal_supported_matches_platform(self):
from api import terminal
if sys.platform == "win32":
assert not terminal._TERMINAL_SUPPORTED
else:
assert terminal._TERMINAL_SUPPORTED
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only check")
def test_posix_modules_are_loaded(self):
from api import terminal
assert terminal.fcntl is not None
assert terminal.select is not None
assert terminal.termios is not None
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only check")
def test_windows_modules_are_none(self):
from api import terminal
assert terminal.fcntl is None
assert terminal.select is None
assert terminal.termios is None
class TestTerminalWindowsFunctions:
"""Terminal functions should fail gracefully on Windows."""
@pytest.fixture(autouse=True)
def _force_unsupported(self, monkeypatch):
"""Force _TERMINAL_SUPPORTED=False regardless of actual platform."""
from api import terminal
monkeypatch.setattr(terminal, "_TERMINAL_SUPPORTED", False)
def test_start_terminal_raises(self, tmp_path):
from api.terminal import start_terminal
with pytest.raises(NotImplementedError, match="not supported on Windows"):
start_terminal("test-session", tmp_path)
def test_write_terminal_raises(self):
from api.terminal import write_terminal
with pytest.raises(NotImplementedError, match="not supported on Windows"):
write_terminal("test-session", "hello")
def test_resize_terminal_raises(self):
from api.terminal import resize_terminal
with pytest.raises(NotImplementedError, match="not supported on Windows"):
resize_terminal("test-session", 24, 80)
def test_close_terminal_returns_false(self):
from api.terminal import close_terminal
assert close_terminal("test-session") is False
def test_get_terminal_returns_none(self):
from api.terminal import get_terminal
assert get_terminal("test-session") is None
# ── bootstrap.py tests ───────────────────────────────────────────────────────
@pytest.fixture
def import_bootstrap():
"""Import bootstrap freshly to avoid module-level state bleed."""
if "bootstrap" in sys.modules:
del sys.modules["bootstrap"]
import bootstrap as bs
return bs
class TestBootstrapWindowsUnblock:
def test_ensure_supported_platform_does_not_raise_on_windows(self, import_bootstrap, monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Windows")
monkeypatch.setattr(import_bootstrap, "is_wsl", lambda: False)
# Should print a warning, not raise
import_bootstrap.ensure_supported_platform()
def test_ensure_supported_platform_still_passes_on_posix(self, import_bootstrap, monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
import_bootstrap.ensure_supported_platform()
def test_install_hermes_agent_raises_on_windows(self, import_bootstrap, monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Windows")
monkeypatch.setattr(import_bootstrap, "is_wsl", lambda: False)
with pytest.raises(RuntimeError, match="not supported on native Windows"):
import_bootstrap.install_hermes_agent()
def test_install_hermes_agent_allowed_in_wsl(self, import_bootstrap, monkeypatch):
"""WSL should still be able to auto-install (it has /bin/bash) — the
Windows guard must NOT fire. Stub subprocess.run so the test proves the
guard was passed without actually launching the real installer."""
monkeypatch.setattr("platform.system", lambda: "Windows")
monkeypatch.setattr(import_bootstrap, "is_wsl", lambda: True)
run_calls = []
monkeypatch.setattr(
import_bootstrap.subprocess, "run",
lambda *a, **kw: run_calls.append((a, kw)),
)
# Must NOT raise the native-Windows guard; instead it reaches the
# (stubbed) install subprocess.
import_bootstrap.install_hermes_agent()
assert len(run_calls) == 1, "WSL install should reach the install subprocess"
class TestBootstrapForegroundWindows:
"""Foreground path uses Popen + sys.exit on Windows instead of os.execv."""
@pytest.fixture
def stub_main_dependencies(self, monkeypatch, tmp_path):
import bootstrap as bs
monkeypatch.setattr(bs, "ensure_supported_platform", lambda: None)
monkeypatch.setattr(bs, "discover_agent_dir", lambda: tmp_path / "agent")
monkeypatch.setattr(bs, "hermes_command_exists", lambda: True)
python_exe = sys.executable
monkeypatch.setattr(bs, "discover_launcher_python", lambda *a: python_exe)
monkeypatch.setattr(bs, "ensure_python_has_webui_deps", lambda *a, **kw: a[0])
monkeypatch.setattr(bs, "wait_for_health", lambda *a, **kw: True)
monkeypatch.setattr(bs, "open_browser", lambda *a, **kw: None)
monkeypatch.setenv("HERMES_WEBUI_STATE_DIR", str(tmp_path / "state"))
(tmp_path / "agent").mkdir(parents=True, exist_ok=True)
return bs
def test_foreground_uses_popen_on_windows(self, stub_main_dependencies, monkeypatch):
"""On win32, foreground mode should use Popen + sys.exit, not os.execv."""
bs = stub_main_dependencies
monkeypatch.setattr(sys, "argv", ["bootstrap.py", "--foreground"])
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(os, "chdir", lambda p: None)
# Strip supervisor env vars that would interfere
for var in ("INVOCATION_ID", "JOURNAL_STREAM", "NOTIFY_SOCKET",
"XPC_SERVICE_NAME", "SUPERVISOR_ENABLED", "HERMES_WEBUI_FOREGROUND"):
monkeypatch.delenv(var, raising=False)
popen_calls = []
execv_calls = []
class FakePopen:
pid = 12345
def __init__(self, *args, **kwargs):
popen_calls.append((args, kwargs))
monkeypatch.setattr(subprocess, "Popen", FakePopen)
monkeypatch.setattr(os, "execv", lambda *a: execv_calls.append(a))
with pytest.raises(SystemExit) as ei:
bs.main()
assert ei.value.code == 0
assert len(popen_calls) == 1, "Windows foreground should use Popen"
assert len(execv_calls) == 0, "Windows foreground must NOT use execv"
def test_foreground_uses_execv_on_posix(self, stub_main_dependencies, monkeypatch):
"""On POSIX, foreground mode should still use os.execv."""
bs = stub_main_dependencies
monkeypatch.setattr(sys, "argv", ["bootstrap.py", "--foreground"])
monkeypatch.setattr(sys, "platform", "linux")
monkeypatch.setattr(os, "chdir", lambda p: None)
for var in ("INVOCATION_ID", "JOURNAL_STREAM", "NOTIFY_SOCKET",
"XPC_SERVICE_NAME", "SUPERVISOR_ENABLED", "HERMES_WEBUI_FOREGROUND"):
monkeypatch.delenv(var, raising=False)
execv_calls = []
popen_calls = []
def fake_execv(path, argv):
execv_calls.append((path, argv))
raise SystemExit(0)
monkeypatch.setattr(os, "execv", fake_execv)
class FakePopen:
pid = 12345
def __init__(self, *args, **kwargs):
popen_calls.append((args, kwargs))
monkeypatch.setattr(subprocess, "Popen", FakePopen)
with pytest.raises(SystemExit) as ei:
bs.main()
assert ei.value.code == 0
assert len(execv_calls) == 1, "POSIX foreground should use execv"
assert len(popen_calls) == 0, "POSIX foreground must NOT use Popen"