diff --git a/pyproject.toml b/pyproject.toml index 790dab81c33..225c0a46779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -367,6 +367,9 @@ markers = [ "real_concurrent_gate: opt out of the autouse stub that disables _detect_concurrent_hermes_instances", "real_agent_prewarm: opt out of the autouse stub that disables the tui_gateway deferred agent pre-warm timer", "requires_wal: needs the runtime to actually enable SQLite WAL mode (skipped where Hermes falls back to journal_mode=DELETE)", + "no_isolate: opt out of per-file subprocess isolation (tests share mutable module-level state)", + "ssh: marks tests requiring a reachable SSH server (skipped in normal CI)", + "live_system_guard_bypass: opt out of the os.kill monkeypatch guard for tests that need real signal delivery", ] # integration tests take way too long to run in the normal CI environments addopts = "-m 'not integration'" diff --git a/tests/conftest.py b/tests/conftest.py index ad2a337fb93..672723a6d91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1139,6 +1139,12 @@ def _live_system_guard(request, monkeypatch): "daemon-reload", "try-restart", "reload-or-restart", ) _PROCESS_KILLERS = ("pkill", "killall", "taskkill", "skill", "fuser") + # Shell/launcher executables whose arguments are themselves commands — + # argv[0]-only scanning must not exempt what they wrap. + _WRAPPER_COMMANDS = ( + "sh", "bash", "zsh", "dash", "env", "nohup", "setsid", + "timeout", "sudo", "xargs", "nice", "ionice", "stdbuf", "flock", + ) def _cmd_to_string(cmd) -> str: if cmd is None: @@ -1181,7 +1187,17 @@ def _live_system_guard(request, monkeypatch): tokens = cmd_str.split() if not tokens: return False - for tok in tokens: + + # For argv-style calls only argv[0] is the executable; scanning every + # argument blocked innocent commands like ``cat /tmp/.../skill`` + # ("skill" is in _PROCESS_KILLERS). Wrapper executables still get + # full-token scanning so ``["bash", "-c", "pkill ..."]`` stays caught. + if isinstance(cmd, (list, tuple)): + head0 = tokens[0].rsplit("/", 1)[-1].rsplit("\\", 1)[-1] + killer_tokens = tokens if head0 in _WRAPPER_COMMANDS else tokens[:1] + else: + killer_tokens = tokens + for tok in killer_tokens: head = tok.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] if head in _PROCESS_KILLERS: low = cmd_str.lower() diff --git a/tests/test_live_system_guard.py b/tests/test_live_system_guard.py new file mode 100644 index 00000000000..83e98565f95 --- /dev/null +++ b/tests/test_live_system_guard.py @@ -0,0 +1,39 @@ +"""Regression tests for the conftest live-system guard's argv handling. + +The guard must treat only argv[0] of a list/tuple command as the executable +(arguments are data: a file named ``skill`` is not the ``skill`` binary), +while still scanning every token of wrapper invocations like ``bash -c``. +All blocked-case commands use patterns that match no real process, so a +guard regression cannot kill anything. +""" + +import subprocess + +import pytest + + +def test_argv_arguments_are_not_treated_as_executables(tmp_path): + """A file argument whose basename is a killer name must not trip the + guard (the path contains "hermes" via the pytest tmp root).""" + target = tmp_path / "skill" + target.write_text("just a filename\n") + result = subprocess.run(["cat", str(target)], capture_output=True, text=True) + assert result.returncode == 0 + assert "just a filename" in result.stdout + + +def test_direct_killer_argv_is_still_blocked(): + with pytest.raises(RuntimeError, match="live-system guard"): + subprocess.run(["pkill", "-f", "hermes-guard-regression-nomatch"]) + + +def test_wrapped_killer_command_is_still_blocked(): + """argv[0]-only scanning must not exempt commands hidden behind a + shell wrapper.""" + with pytest.raises(RuntimeError, match="live-system guard"): + subprocess.run(["bash", "-c", "pkill -f hermes-guard-regression-nomatch"]) + + +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"]) diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index 5e9150cd98e..23f28552310 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -88,7 +88,6 @@ def test_shared_fixture_cleanup_uses_full_session_teardown(server, monkeypatch): assert server._sessions == {} assert closed == {"worker": 1, "agent": 1, "lease": 1} ->>>>>>> theirs @pytest.fixture()