fix: preserve current approval semantics for tirith guard

Restore gateway/run.py to current main behavior while keeping tirith startup
and pattern_keys replay, preserve yolo and non-interactive bypass semantics in
the combined guard, and add regression tests for yolo and view-full flows.
This commit is contained in:
teknium1 2026-03-14 00:17:04 -07:00
parent 375ce8a881
commit 6f1889b0fa
5 changed files with 1959 additions and 13 deletions

View file

@ -377,6 +377,18 @@ class TestViewFullCommand:
result = prompt_dangerous_approval(long_cmd, "recursive delete")
assert result == "always"
def test_view_then_session_when_permanent_hidden(self):
"""The view-full flow still works when allow_permanent=False."""
long_cmd = "rm -rf " + "d" * 200
inputs = iter(["v", "s"])
with mock_patch("builtins.input", side_effect=inputs):
result = prompt_dangerous_approval(
long_cmd,
"recursive delete",
allow_permanent=False,
)
assert result == "session"
def test_view_not_shown_for_short_command(self):
"""Short commands don't offer the view option; 'v' falls through to deny."""
short_cmd = "rm -rf /tmp"

View file

@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock
import pytest
import tools.approval as approval_module
from tools.approval import (
approve_session,
check_all_command_guards,
@ -35,15 +36,17 @@ def _clean_state():
"""Clear approval state and relevant env vars between tests."""
key = os.getenv("HERMES_SESSION_KEY", "default")
clear_session(key)
approval_module._permanent_approved.clear()
saved = {}
for k in ("HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK"):
for k in ("HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK", "HERMES_YOLO_MODE"):
if k in os.environ:
saved[k] = os.environ.pop(k)
yield
clear_session(key)
approval_module._permanent_approved.clear()
for k, v in saved.items():
os.environ[k] = v
for k in ("HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK"):
for k in ("HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK", "HERMES_YOLO_MODE"):
os.environ.pop(k, None)
@ -76,9 +79,16 @@ class TestContainerSkip:
class TestTirithAllowSafeCommand:
@patch(_TIRITH_PATCH, return_value=_tirith_result("allow"))
def test_both_allow(self, mock_tirith):
os.environ["HERMES_INTERACTIVE"] = "1"
result = check_all_command_guards("echo hello", "local")
assert result["approved"] is True
@patch(_TIRITH_PATCH, return_value=_tirith_result("allow"))
def test_noninteractive_skips_external_scan(self, mock_tirith):
result = check_all_command_guards("echo hello", "local")
assert result["approved"] is True
mock_tirith.assert_not_called()
# ---------------------------------------------------------------------------
# tirith block
@ -88,6 +98,7 @@ class TestTirithBlock:
@patch(_TIRITH_PATCH,
return_value=_tirith_result("block", summary="homograph detected"))
def test_tirith_block_safe_command(self, mock_tirith):
os.environ["HERMES_INTERACTIVE"] = "1"
result = check_all_command_guards("curl http://gооgle.com", "local")
assert result["approved"] is False
assert "BLOCKED" in result["message"]
@ -97,6 +108,7 @@ class TestTirithBlock:
return_value=_tirith_result("block", summary="terminal injection"))
def test_tirith_block_plus_dangerous(self, mock_tirith):
"""tirith block takes precedence even if command is also dangerous."""
os.environ["HERMES_INTERACTIVE"] = "1"
result = check_all_command_guards("rm -rf / | curl http://evil", "local")
assert result["approved"] is False
assert "BLOCKED" in result["message"]
@ -308,5 +320,6 @@ class TestProgrammingErrorsPropagateFromWrapper:
@patch(_TIRITH_PATCH, side_effect=AttributeError("bug in wrapper"))
def test_attribute_error_propagates(self, mock_tirith):
"""Non-ImportError exceptions from tirith wrapper should propagate."""
os.environ["HERMES_INTERACTIVE"] = "1"
with pytest.raises(AttributeError, match="bug in wrapper"):
check_all_command_guards("echo hello", "local")

View file

@ -3,7 +3,25 @@
import os
import pytest
from tools.approval import check_dangerous_command, detect_dangerous_command
import tools.approval as approval_module
import tools.tirith_security
from tools.approval import (
check_all_command_guards,
check_dangerous_command,
detect_dangerous_command,
)
@pytest.fixture(autouse=True)
def _clear_approval_state():
approval_module._permanent_approved.clear()
approval_module.clear_session("default")
approval_module.clear_session("test-session")
yield
approval_module._permanent_approved.clear()
approval_module.clear_session("default")
approval_module.clear_session("test-session")
class TestYoloMode:
@ -54,6 +72,24 @@ class TestYoloMode:
result = check_dangerous_command(cmd, "local")
assert result["approved"], f"Command should be approved in yolo mode: {cmd}"
def test_combined_guard_bypasses_yolo_mode(self, monkeypatch):
"""The new combined guard should preserve yolo bypass semantics."""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
monkeypatch.setenv("HERMES_INTERACTIVE", "1")
called = {"value": False}
def fake_check(command):
called["value"] = True
return {"action": "block", "findings": [], "summary": "should never run"}
monkeypatch.setattr(tools.tirith_security, "check_command_security", fake_check)
result = check_all_command_guards("rm -rf /", "local")
assert result["approved"]
assert result["message"] is None
assert called["value"] is False
def test_yolo_mode_not_set_by_default(self):
"""HERMES_YOLO_MODE should not be set by default."""
# Clean env check — if it happens to be set in test env, that's fine,