diff --git a/tests/tools/test_approval.py b/tests/tools/test_approval.py index dc9eace274c..b7598380708 100644 --- a/tests/tools/test_approval.py +++ b/tests/tools/test_approval.py @@ -9,6 +9,7 @@ from types import SimpleNamespace from unittest.mock import patch as mock_patch import tools.approval as approval_module +from hermes_constants import get_hermes_home from tools.approval import ( _get_approval_mode, _smart_approve, @@ -424,6 +425,22 @@ class TestHermesConfigWriteProtection: dangerous, key, desc = detect_dangerous_command("sed --in-place 's/manual/off/' ~/.hermes/config.yaml") assert dangerous is True + def test_sed_in_place_absolute_hermes_home_config(self): + config_path = get_hermes_home() / "config.yaml" + dangerous, key, desc = detect_dangerous_command( + f"sed -i 's/manual/off/' {config_path}" + ) + assert dangerous is True + assert "hermes config" in desc.lower() or "in-place" in desc.lower() + + def test_sed_in_place_absolute_hermes_home_env(self): + env_path = get_hermes_home() / ".env" + dangerous, key, desc = detect_dangerous_command( + f"sed -i 's/API_KEY=.*/API_KEY=x/' {env_path}" + ) + assert dangerous is True + assert "hermes config" in desc.lower() or "in-place" in desc.lower() + def test_custom_hermes_home(self): dangerous, key, desc = detect_dangerous_command("echo x | tee $HERMES_HOME/config.yaml") assert dangerous is True @@ -437,12 +454,33 @@ class TestHermesConfigWriteProtection: assert dangerous is True assert "in-place" in desc.lower() or "perl" in desc.lower() + def test_perl_in_place_absolute_hermes_home_config(self): + config_path = get_hermes_home() / "config.yaml" + dangerous, key, desc = detect_dangerous_command( + f"perl -i -pe 's/approvals.mode: on/approvals.mode: off/' {config_path}" + ) + assert dangerous is True + assert "in-place" in desc.lower() or "perl" in desc.lower() + def test_ruby_in_place_config(self): dangerous, key, desc = detect_dangerous_command( "ruby -i -pe 'gsub(/manual/, \"off\")' ~/.hermes/config.yaml" ) assert dangerous is True + def test_ruby_in_place_absolute_hermes_home_env(self): + env_path = get_hermes_home() / ".env" + dangerous, key, desc = detect_dangerous_command( + f"ruby -i -pe 'gsub(/API_KEY=.*/, \"API_KEY=x\")' {env_path}" + ) + assert dangerous is True + + def test_regular_absolute_config_path_still_uses_project_rule(self): + dangerous, key, desc = detect_dangerous_command( + "sed -i 's/a/b/' /srv/app/config.yaml" + ) + assert dangerous is False + def test_perl_in_place_env(self): dangerous, key, desc = detect_dangerous_command( "perl -i -pe 's/SECRET=old/SECRET=new/' ~/.hermes/.env" diff --git a/tools/approval.py b/tools/approval.py index 85ae2b9d7f6..92bbe592131 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -151,10 +151,31 @@ def _is_gateway_approval_context() -> bool: return bool(_get_session_platform()) # Sensitive write targets that should trigger approval even when referenced -# via shell expansions like $HOME or $HERMES_HOME. +# via shell expansions like $HOME or $HERMES_HOME, or by the resolved absolute +# active profile home path such as /home/hermes/.hermes/config.yaml. _SSH_SENSITIVE_PATH = r'(?:~|\$home|\$\{home\})/\.ssh(?:/|$)' + + +def _resolved_hermes_home_path_pattern() -> str: + try: + from hermes_constants import get_hermes_home + home = get_hermes_home().expanduser() + candidates = [ + str(home).rstrip("/"), + str(home.resolve(strict=False)).rstrip("/"), + ] + except Exception: + candidates = [] + escaped = [re.escape(path) for path in dict.fromkeys(candidates) if path] + if not escaped: + return r"(?!)" + return r"(?:" + "|".join(escaped) + r")/" + + +_RESOLVED_HERMES_HOME_PATH = _resolved_hermes_home_path_pattern() _HERMES_ENV_PATH = ( r'(?:~\/\.hermes/|' + rf'{_RESOLVED_HERMES_HOME_PATH}|' r'(?:\$home|\$\{home\})/\.hermes/|' r'(?:\$hermes_home|\$\{hermes_home\})/)' r'\.env\b' @@ -169,6 +190,7 @@ _HERMES_ENV_PATH = ( # well as ~/.hermes/. _HERMES_CONFIG_PATH = ( r'(?:~\/\.hermes/|' + rf'{_RESOLVED_HERMES_HOME_PATH}|' r'(?:\$home|\$\{home\})/\.hermes/|' r'(?:\$hermes_home|\$\{hermes_home\})/)' r'config\.yaml\b'