From 308d12e1fdf622520bb2c70664f6ad2c3fedd66e Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:21:03 -0700 Subject: [PATCH] fix(tests): live-system guard lets a gateway start inside a container exec #106623 (ca16cafee40) blocks any test subprocess whose argv resolves to `hermes gateway run|start|restart`, so the harness cannot spawn a runtime that outlives the worker and restarts the developer's gateway. The matcher strips the `-p ` selector and reads through `sh -c`, so it also fired on `docker exec -u hermes sh -c 'hermes -p x gateway start'` and broke tests/docker/test_profile_gateway.py on every Docker build since. A gateway launched inside a container cannot reach the host unit or webhook port; the guard now skips commands whose argv[0] is a container runtime (docker, podman, nerdctl). Host-side spawns stay blocked. --- tests/conftest.py | 17 ++++++++++++++++- tests/test_live_system_guard.py | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index bac6334869..acaca8d8c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1428,6 +1428,14 @@ def _live_system_guard(request, monkeypatch): "daemon-reload", "try-restart", "reload-or-restart", ) _PROCESS_KILLERS = ("pkill", "killall", "taskkill", "skill", "fuser") + _CONTAINER_RUNTIMES = ("docker", "podman", "nerdctl") + + def _first_token_basename(cmd_str: str) -> str: + try: + tokens = _shlex.split(cmd_str) + except ValueError: + tokens = cmd_str.split() + return tokens[0].rsplit("/", 1)[-1].lower() if tokens else "" # Shell/launcher executables whose arguments are themselves commands — # argv[0]-only scanning must not exempt what they wrap. _WRAPPER_COMMANDS = ( @@ -1563,7 +1571,14 @@ def _live_system_guard(request, monkeypatch): # sibling refactor moved the spawn seam and left tests patching the # facade. The canonical matcher, never an argv substring. from gateway.status import _gateway_command_subcommand - if not lookalike_ok and _gateway_command_subcommand(cmd_str) in ("run", "start", "restart"): + # A gateway launched INSIDE a container (`docker exec … hermes gateway start`) cannot + # reach the host's systemd unit or webhook port; tests/docker/ exists to exercise it. + in_container = _first_token_basename(cmd_str) in _CONTAINER_RUNTIMES + if ( + not lookalike_ok + and not in_container + and _gateway_command_subcommand(cmd_str) in ("run", "start", "restart") + ): raise RuntimeError( f"tests/conftest.py live-system guard: blocked " f"subprocess.{name}({cmd!r}) — this would spawn a REAL " diff --git a/tests/test_live_system_guard.py b/tests/test_live_system_guard.py index 83e98565f9..ce51a08cce 100644 --- a/tests/test_live_system_guard.py +++ b/tests/test_live_system_guard.py @@ -37,3 +37,29 @@ def test_wrapped_killer_command_is_still_blocked(): def test_env_wrapped_killer_command_is_still_blocked(): with pytest.raises(RuntimeError, match="live-system guard"): subprocess.run(["env", "GUARD_TEST=1", "pkill", "-f", "hermes-guard-regression-nomatch"]) + + +def test_gateway_start_inside_a_container_exec_is_not_blocked(): + """``docker exec hermes gateway start`` launches the gateway INSIDE the container, + where it cannot reach the host's systemd unit or webhook port; tests/docker/ depends on it. + The binary is a stub so the argv stays exact without needing a Docker daemon.""" + import os + import stat + + stub_dir = os.path.join(os.environ["HERMES_HOME"], "stub-bin") + os.makedirs(stub_dir, exist_ok=True) + stub = os.path.join(stub_dir, "docker") + with open(stub, "w") as fh: + fh.write("#!/bin/sh\nexit 0\n") + os.chmod(stub, os.stat(stub).st_mode | stat.S_IXUSR) + result = subprocess.run( + [stub, "exec", "-u", "hermes", "ctr", "sh", "-c", "hermes -p prof gateway start"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_gateway_start_on_the_host_is_still_blocked(): + with pytest.raises(RuntimeError, match="REAL.*gateway runtime"): + subprocess.run(["python", "-m", "hermes_cli.main", "gateway", "start"])