fix: Windows guard, dedup recovery, profile-safe paths, clear SO_RCVTIMEO

Follow-up fixes for salvaged PR #67686 (issue #67639):

1. Windows regression: bare 'import fcntl' at module level in entry.py and
   slash_worker.py would crash on Windows (fcntl is POSIX-only). entry.py
   explicitly aims to 'import cleanly on Windows'. Extracted all fcntl/socket
   logic to tui_gateway/_stdin_recovery.py with try/except import guards.

2. fd leak: socket.fromfd() dups fd 0; s.detach() returned the fd number
   without closing it, leaking one fd per recovery call. Changed to s.close()
   (safe — fromfd duped the fd, closing won't close stdin).

3. Code duplication: ~80 lines of recovery loop + diagnostic were copy-pasted
   between entry.py and slash_worker.py. Extracted to shared
   tui_gateway/_stdin_recovery.py (handle_spurious_eof + diagnose_stdin_state).

4. Profile-unsafe path: scanner used Path.home() / '.hermes' / 'plugins' but
   canonical plugin discovery uses get_hermes_home() / 'plugins'. With
   HERMES_HOME pointing elsewhere, scanner missed the actual plugin dir.
   Also gated project plugins behind HERMES_ENABLE_PROJECT_PLUGINS to match
   hermes_cli/plugins.py.

5. SO_RCVTIMEO not cleared: recovery only called os.set_blocking(0, True)
   but a child that set SO_RCVTIMEO would cause the next readline to time
   out and loop. Now clears SO_RCVTIMEO alongside O_NONBLOCK.
This commit is contained in:
kshitijk4poor 2026-07-20 12:44:10 +05:30 committed by kshitij
parent 01c02c6f8f
commit 113d9f63b5
4 changed files with 182 additions and 126 deletions

View file

@ -38,13 +38,15 @@ TUI_CONTEXT_DIRS = [
]
# User plugin roots — scanned at runtime if they exist. Plugins load from
# ~/.hermes/plugins/ and ./.hermes/plugins/ (hermes_cli/plugins.py:10-12),
# but the guard only checked the bundled plugins/ dir, missing user-installed
# code that spawns subprocesses (gap reported in #67639).
USER_PLUGIN_DIRS = [
Path.home() / ".hermes" / "plugins",
Path.cwd() / ".hermes" / "plugins",
]
# ``get_hermes_home() / "plugins"`` (user) and ``./.hermes/plugins/`` (project,
# gated behind ``HERMES_ENABLE_PROJECT_PLUGINS``) — see
# ``hermes_cli/plugins.py:10-12``. The guard only checked the bundled
# ``plugins/`` dir, missing user-installed code that spawns subprocesses
# (gap reported in #67639).
#
# Import is deferred to ``main()`` (after ``os.chdir(repo_root)``) because
# this script runs as a standalone subprocess — ``hermes_constants`` isn't
# on ``sys.path`` until the repo root is added.
# subprocess and os APIs that inherit stdin by default when called without
# an explicit stdin= argument. The original regex only covered run/Popen
@ -155,6 +157,11 @@ def main() -> int:
repo_root = Path(__file__).resolve().parent.parent
os.chdir(repo_root)
# Add repo root to sys.path so we can import hermes_constants (this script
# runs as a standalone subprocess, not as a module).
sys.path.insert(0, str(repo_root))
from hermes_constants import get_hermes_home
all_violations = []
for tui_dir in TUI_CONTEXT_DIRS:
@ -178,11 +185,15 @@ def main() -> int:
violations = find_subprocess_calls(content, rel)
all_violations.extend(violations)
# Scan user plugin directories (Gap 1: guard missed ~/.hermes/plugins/
# and ./.hermes/plugins/, where user-installed plugins like ori/hooks.py
# can spawn subprocesses with inherited stdin — #67639).
# Scan user plugin directories (Gap 1: guard missed user-installed
# plugins in get_hermes_home()/plugins/ and project plugins in
# ./.hermes/plugins/, where code like ori/hooks.py can spawn
# subprocesses with inherited stdin — #67639).
plugin_roots: list[Path] = [get_hermes_home() / "plugins"]
if os.environ.get("HERMES_ENABLE_PROJECT_PLUGINS"):
plugin_roots.append(Path.cwd() / ".hermes" / "plugins")
seen_roots: set[Path] = set()
for plugin_root in USER_PLUGIN_DIRS:
for plugin_root in plugin_roots:
resolved = plugin_root.resolve()
if resolved in seen_roots or not resolved.is_dir():
continue