Skip to content
Closed
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
35 changes: 31 additions & 4 deletions hermes_cli/_scan_venv_blockers.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,29 @@ def _is_pausable_gateway(cmdline: str) -> bool:
return looks_like_gateway_command_line(cmdline)


def _is_pausable_gateway_process(pid: int, captured_cmdline: str) -> bool:
"""Classify a holder using its live argv when the capture is incomplete.

``_detect_venv_python_processes`` caps captured command lines at 120
characters for display. Managed-runtime interpreter paths can exceed that
limit before the trailing ``gateway run`` tokens, so the captured prefix
alone is not sufficient for the Desktop preflight exemption. Re-read the
argv only for classification and keep the captured prefix for output.

If the process exits or cannot be inspected, fail closed by retaining the
captured-prefix verdict: the holder remains a blocker.
"""
if _is_pausable_gateway(captured_cmdline):
return True
try:
import psutil # noqa: PLC0415

live_cmdline = " ".join(psutil.Process(int(pid)).cmdline())
except Exception:
return False
return bool(live_cmdline) and _is_pausable_gateway(live_cmdline)


def main() -> None:
"""Entry point. Prints one JSON doc to stdout. Exits 0 for valid scan."""
try:
Expand All @@ -140,27 +163,31 @@ def main() -> None:
except Exception as exc:
_emit_probe_fail(f"scan aborted: {exc}")

pausable_gateway_pids = {
pid
for pid, _name, cmdline in matches
if _is_pausable_gateway_process(pid, cmdline)
}
processes = [
{
"pid": pid,
"name": name,
"cmdline": _redact_sensitive_cmdline(cmdline),
}
for pid, name, cmdline in matches
if not _is_pausable_gateway(cmdline)
if pid not in pausable_gateway_pids
]
exempted = sum(1 for _pid, _name, cmdline in matches if _is_pausable_gateway(cmdline))
data = {
"ok": True,
"blocked": bool(processes),
"processes": processes,
# Diagnostic only: gateway processes present but not counted as
# blockers because the downstream updater pauses them itself.
"pausable_gateways": exempted,
"pausable_gateways": len(pausable_gateway_pids),
}
print(json.dumps(data))
sys.exit(0)


if __name__ == "__main__":
main()
main()
37 changes: 36 additions & 1 deletion tests/hermes_cli/test_scan_venv_blockers.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,39 @@ def test_main_desktop_serve_backend_still_blocks(monkeypatch, capsys):
assert code == 0
assert data["blocked"] is True
assert [p["pid"] for p in data["processes"]] == [78]
assert data["pausable_gateways"] == 0
assert data["pausable_gateways"] == 0


def test_main_classifies_gateway_from_live_cmdline_when_capture_is_truncated(
monkeypatch, capsys
):
"""Long managed-runtime paths must not hide the trailing gateway command."""
executable = (
"C:/Users/u/.hermes/hermes-agent/.hermes-runtime/python/"
"generation-1785179197-21284-2ba3099e/"
"cpython-3.11-windows-x86_64-none/python.exe"
)
argv = [executable, "-m", "hermes_cli.main", "gateway", "run"]
full_cmdline = " ".join(argv)
captured = full_cmdline[:120]
assert "gateway" not in captured

process = types.SimpleNamespace(cmdline=lambda: argv)
fake_psutil = types.SimpleNamespace(Process=lambda pid: process)
monkeypatch.setitem(sys.modules, "psutil", fake_psutil)
import hermes_cli.main as cli_main

monkeypatch.setattr(
cli_main,
"_detect_venv_python_processes",
lambda: [(42, "python.exe", captured)],
)

with pytest.raises(SystemExit) as excinfo:
main()
data = json.loads(capsys.readouterr().out)

assert excinfo.value.code == 0
assert data["blocked"] is False
assert data["processes"] == []
assert data["pausable_gateways"] == 1