mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(tui): prevent RPC stdin inheritance
This commit is contained in:
1 parent
7ad51527d4
commit
ec1f26f476
2 files changed
+73
No files matched your search
@@ -0,0 +1,51 @@
|
||||
"""Regression coverage for the stdio gateway's child-process boundary."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from tui_gateway import entry
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "posix", reason="POSIX close-on-exec semantics")
|
||||
def test_rpc_stdin_is_closed_before_a_bare_child_executes(monkeypatch):
|
||||
"""A child with no explicit stdin must not be able to consume RPC bytes."""
|
||||
original_stdin = os.dup(0)
|
||||
original_inheritable = os.get_inheritable(0)
|
||||
reader, writer = os.pipe()
|
||||
try:
|
||||
os.dup2(reader, 0)
|
||||
os.write(writer, b'{"method":"prompt.submit"}\n')
|
||||
monkeypatch.setattr(entry.sys, "stdin", type("RpcStdin", (), {"fileno": lambda _: 0})())
|
||||
|
||||
entry._close_rpc_stdin_on_exec()
|
||||
|
||||
output_reader, output_writer = os.pipe()
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
os.dup2(output_writer, 1)
|
||||
os.close(output_reader)
|
||||
os.close(output_writer)
|
||||
os.execv(
|
||||
sys.executable,
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import os; "
|
||||
"\ntry: os.read(0, 1)\n"
|
||||
"except OSError: print('stdin-closed')\n"
|
||||
"else: print('stdin-inherited')",
|
||||
],
|
||||
)
|
||||
|
||||
os.close(output_writer)
|
||||
_, status = os.waitpid(pid, 0)
|
||||
assert os.waitstatus_to_exitcode(status) == 0
|
||||
assert os.read(output_reader, 100) == b"stdin-closed\n"
|
||||
os.close(output_reader)
|
||||
finally:
|
||||
os.dup2(original_stdin, 0, inheritable=original_inheritable)
|
||||
os.close(original_stdin)
|
||||
os.close(reader)
|
||||
os.close(writer)
|
||||
@@ -33,6 +33,27 @@ _mcp_discovery_thread = None
|
||||
_mcp_discovery_enabled = False
|
||||
|
||||
|
||||
def _close_rpc_stdin_on_exec() -> None:
|
||||
"""Keep stdio RPC requests out of children launched by the gateway.
|
||||
|
||||
The TUI's stdin is the Node-to-Python JSON-RPC socketpair. Standard
|
||||
descriptors survive subprocess execution unless they are explicitly
|
||||
close-on-exec, so a dependency that launches a child without ``stdin=``
|
||||
could otherwise consume a request before this process reads it.
|
||||
|
||||
Windows does not provide the POSIX FD_CLOEXEC contract used here; retain
|
||||
its existing descriptor behavior rather than changing its launch paths.
|
||||
"""
|
||||
if os.name != "posix":
|
||||
return
|
||||
try:
|
||||
os.set_inheritable(sys.stdin.fileno(), False)
|
||||
except (OSError, ValueError):
|
||||
# Embedded launchers can supply a stream without an inheritable file
|
||||
# descriptor. Keep gateway startup available when no guard is possible.
|
||||
logger.debug("could not mark TUI RPC stdin close-on-exec", exc_info=True)
|
||||
|
||||
|
||||
def _install_sidecar_publisher() -> None:
|
||||
"""Mirror every dispatcher emit to the dashboard sidebar via WS when set (best-effort)."""
|
||||
url = os.environ.get("HERMES_TUI_SIDECAR_URL")
|
||||
@@ -237,6 +258,7 @@ def _write_or_exit(payload: dict, reason: str) -> None:
|
||||
|
||||
|
||||
def main():
|
||||
_close_rpc_stdin_on_exec()
|
||||
_install_sidecar_publisher()
|
||||
|
||||
# The heartbeat row lets the orphan sweep tell "live but idle" from "truly orphaned",
|
||||
|
||||
Reference in new issue
Block a user