fix(windows): capture is not a no-window boundary; route flashing spawns through chokepoint (#53829)

Follow-up to #53791 addressing review feedback: the footgun checker treated
capture_output=/stdout=/stderr=/check_output as proof a subprocess can't pop a
Windows console. That invariant is false — stream redirection controls where a
child's output goes, not whether a console is allocated. From a console-less
parent (Desktop/Electron, pythonw.exe, detached gateway/cron) a console-subsystem
child still flashes a window even when fully captured.

- check-windows-footguns.py: capture/redirect/check_output is no longer a blanket
  safe-pass. Added _WINDOWS_FLASHING_PROGRAMS (git/gh/npm/node/python/uv/ffmpeg/
  docker/powershell/…); calls to those are flagged even when captured. Non-flashing
  programs keep the capture exemption (no 271-site noise). _subprocess_compat.run/
  popen calls are inherently safe (wrapper injects CREATE_NO_WINDOW).
- Routed the 35 genuine flashing git/gh/npm/uv/ffmpeg/docker spawns through the
  _subprocess_compat.run/popen chokepoint (Brooklyn's wrapper from #53810) — the
  durable fix, not per-site annotations. cmd.exe /c start stays # ok (intentional).
- Updated tests + CONTRIBUTING.md rule #17 to the corrected invariant.
This commit is contained in:
Teknium 2026-06-27 14:49:41 -07:00 committed by GitHub
parent 3ac96d3308
commit 2ecca1e7d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 191 additions and 76 deletions

View file

@ -24,6 +24,7 @@ from hermes_constants import get_hermes_home
from hermes_cli.profiles import _get_default_hermes_home
from plugins.plugin_utils import SingletonSlot
from typing import Any, TYPE_CHECKING
from hermes_cli import _subprocess_compat
if TYPE_CHECKING:
from honcho import Honcho
@ -625,7 +626,7 @@ class HonchoClientConfig:
import subprocess
try:
root = subprocess.run(
root = _subprocess_compat.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, cwd=cwd, timeout=5,
stdin=subprocess.DEVNULL,

View file

@ -22,6 +22,7 @@ from ._oss_providers import (
KNOWN_DIMS,
validate_oss_config,
)
from hermes_cli import _subprocess_compat
def _curses_select(title: str, items: list[tuple[str, str]], default: int = 0) -> int:
@ -405,13 +406,13 @@ def _ensure_pgvector(host: str = "localhost", port: int = 5432) -> dict | None:
# Check if our container already exists but is stopped
if shutil.which("docker"):
try:
result = subprocess.run(
result = _subprocess_compat.run(
["docker", "inspect", _PGVECTOR_CONTAINER, "--format", "{{.State.Status}}"],
capture_output=True, text=True, timeout=10, stdin=subprocess.DEVNULL,
)
if result.returncode == 0 and "exited" in result.stdout:
print(f" Found stopped container '{_PGVECTOR_CONTAINER}', restarting...")
subprocess.run(["docker", "start", _PGVECTOR_CONTAINER],
_subprocess_compat.run(["docker", "start", _PGVECTOR_CONTAINER],
capture_output=True, timeout=15,
stdin=subprocess.DEVNULL)
_wait_for_port(host, port, timeout=15)
@ -438,17 +439,17 @@ def _start_pgvector_docker(host: str, port: int) -> dict | None:
"""Pull and start pgvector Docker container."""
try:
print(f" Pulling {_PGVECTOR_IMAGE}...")
subprocess.run(["docker", "pull", _PGVECTOR_IMAGE],
_subprocess_compat.run(["docker", "pull", _PGVECTOR_IMAGE],
capture_output=True, timeout=120,
stdin=subprocess.DEVNULL)
# Remove existing container if present
subprocess.run(["docker", "rm", "-f", _PGVECTOR_CONTAINER],
_subprocess_compat.run(["docker", "rm", "-f", _PGVECTOR_CONTAINER],
capture_output=True, timeout=10,
stdin=subprocess.DEVNULL)
print(f" Starting container '{_PGVECTOR_CONTAINER}' on port {port}...")
subprocess.run([
_subprocess_compat.run([
"docker", "run", "-d",
"--name", _PGVECTOR_CONTAINER,
"-e", f"POSTGRES_PASSWORD={_PGVECTOR_PASSWORD}",
@ -734,7 +735,7 @@ def _install_provider_deps(llm_id: str, embedder_id: str, vector_id: str) -> Non
for dep in sorted(deps):
try:
print(f" Installing {dep}...")
subprocess.run(
_subprocess_compat.run(
["uv", "pip", "install", "--python", sys.executable, dep],
capture_output=True, timeout=60,
)