fix(approval): guard env and config overwrites

This commit is contained in:
helix4u 2026-04-23 12:35:16 -06:00 committed by Teknium
parent 1cc0bdd5f3
commit 1dfcda4e3c
4 changed files with 68 additions and 1 deletions

View file

@ -262,6 +262,7 @@ _MAX_TOOL_WORKERS = 8
_DESTRUCTIVE_PATTERNS = re.compile( _DESTRUCTIVE_PATTERNS = re.compile(
r"""(?:^|\s|&&|\|\||;|`)(?: r"""(?:^|\s|&&|\|\||;|`)(?:
rm\s|rmdir\s| rm\s|rmdir\s|
cp\s|install\s|
mv\s| mv\s|
sed\s+-i| sed\s+-i|
truncate\s| truncate\s|

View file

@ -44,6 +44,14 @@ def _make_tool_defs(*names: str) -> list:
] ]
def test_is_destructive_command_treats_cp_as_mutating():
assert run_agent._is_destructive_command("cp .env.local .env") is True
def test_is_destructive_command_treats_install_as_mutating():
assert run_agent._is_destructive_command("install template.env .env") is True
@pytest.fixture() @pytest.fixture()
def agent(): def agent():
"""Minimal AIAgent with mocked OpenAI client and tool loading.""" """Minimal AIAgent with mocked OpenAI client and tool loading."""

View file

@ -434,6 +434,58 @@ class TestSensitiveRedirectPattern:
assert dangerous is False assert dangerous is False
assert key is None assert key is None
def test_redirect_to_local_dotenv_requires_approval(self):
dangerous, key, desc = detect_dangerous_command("echo TOKEN=x > .env")
assert dangerous is True
assert key is not None
assert "project env/config" in desc.lower()
def test_redirect_to_nested_config_yaml_requires_approval(self):
dangerous, key, desc = detect_dangerous_command("echo mode: prod > deploy/config.yaml")
assert dangerous is True
assert key is not None
assert "project env/config" in desc.lower()
def test_redirect_from_local_dotenv_source_is_safe(self):
dangerous, key, desc = detect_dangerous_command("cat .env > backup.txt")
assert dangerous is False
assert key is None
assert desc is None
class TestProjectSensitiveCopyPattern:
def test_cp_to_local_dotenv_requires_approval(self):
dangerous, key, desc = detect_dangerous_command("cp .env.local .env")
assert dangerous is True
assert key is not None
assert "project env/config" in desc.lower()
def test_mv_to_nested_config_yaml_requires_approval(self):
dangerous, key, desc = detect_dangerous_command("mv tmp/generated.yaml config/config.yaml")
assert dangerous is True
assert key is not None
assert "project env/config" in desc.lower()
def test_install_to_dotenv_requires_approval(self):
dangerous, key, desc = detect_dangerous_command("install -m 600 template.env .env.production")
assert dangerous is True
assert key is not None
assert "project env/config" in desc.lower()
def test_cp_from_config_yaml_source_is_safe(self):
dangerous, key, desc = detect_dangerous_command("cp config.yaml backup.yaml")
assert dangerous is False
assert key is None
assert desc is None
class TestProjectSensitiveTeePattern:
def test_tee_to_local_dotenv_requires_approval(self):
dangerous, key, desc = detect_dangerous_command("printenv | tee .env.local")
assert dangerous is True
assert key is not None
assert "project env/config" in desc.lower()
class TestPatternKeyUniqueness: class TestPatternKeyUniqueness:
"""Bug: pattern_key is derived by splitting on \\b and taking [1], so """Bug: pattern_key is derived by splitting on \\b and taking [1], so
@ -836,4 +888,3 @@ class TestChmodExecuteCombo:
cmd = "chmod +x script.sh" cmd = "chmod +x script.sh"
dangerous, _, _ = detect_dangerous_command(cmd) dangerous, _, _ = detect_dangerous_command(cmd)
assert dangerous is False assert dangerous is False

View file

@ -63,11 +63,15 @@ _HERMES_ENV_PATH = (
r'(?:\$hermes_home|\$\{hermes_home\})/)' r'(?:\$hermes_home|\$\{hermes_home\})/)'
r'\.env\b' r'\.env\b'
) )
_PROJECT_ENV_PATH = r'(?:(?:\.{1,2}/)?(?:[^\s/"\'`]+/)*\.env(?:\.[^/\s"\'`]+)*)'
_PROJECT_CONFIG_PATH = r'(?:(?:\.{1,2}/)?(?:[^\s/"\'`]+/)*config\.yaml)'
_SENSITIVE_WRITE_TARGET = ( _SENSITIVE_WRITE_TARGET = (
r'(?:/etc/|/dev/sd|' r'(?:/etc/|/dev/sd|'
rf'{_SSH_SENSITIVE_PATH}|' rf'{_SSH_SENSITIVE_PATH}|'
rf'{_HERMES_ENV_PATH})' rf'{_HERMES_ENV_PATH})'
) )
_PROJECT_SENSITIVE_WRITE_TARGET = rf'(?:{_PROJECT_ENV_PATH}|{_PROJECT_CONFIG_PATH})'
_COMMAND_TAIL = r'(?:\s*(?:&&|\|\||;).*)?$'
# ========================================================================= # =========================================================================
# Dangerous command patterns # Dangerous command patterns
@ -99,6 +103,8 @@ DANGEROUS_PATTERNS = [
(r'\b(bash|sh|zsh|ksh)\s+<\s*<?\s*\(\s*(curl|wget)\b', "execute remote script via process substitution"), (r'\b(bash|sh|zsh|ksh)\s+<\s*<?\s*\(\s*(curl|wget)\b', "execute remote script via process substitution"),
(rf'\btee\b.*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via tee"), (rf'\btee\b.*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via tee"),
(rf'>>?\s*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via redirection"), (rf'>>?\s*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via redirection"),
(rf'\btee\b.*["\']?{_PROJECT_SENSITIVE_WRITE_TARGET}["\']?{_COMMAND_TAIL}', "overwrite project env/config via tee"),
(rf'>>?\s*["\']?{_PROJECT_SENSITIVE_WRITE_TARGET}["\']?{_COMMAND_TAIL}', "overwrite project env/config via redirection"),
(r'\bxargs\s+.*\brm\b', "xargs with rm"), (r'\bxargs\s+.*\brm\b', "xargs with rm"),
(r'\bfind\b.*-exec\s+(/\S*/)?rm\b', "find -exec rm"), (r'\bfind\b.*-exec\s+(/\S*/)?rm\b', "find -exec rm"),
(r'\bfind\b.*-delete\b', "find -delete"), (r'\bfind\b.*-delete\b', "find -delete"),
@ -120,6 +126,7 @@ DANGEROUS_PATTERNS = [
(r'\bkill\b.*`\s*pgrep\b', "kill process via backtick pgrep expansion (self-termination)"), (r'\bkill\b.*`\s*pgrep\b', "kill process via backtick pgrep expansion (self-termination)"),
# File copy/move/edit into sensitive system paths # File copy/move/edit into sensitive system paths
(r'\b(cp|mv|install)\b.*\s/etc/', "copy/move file into /etc/"), (r'\b(cp|mv|install)\b.*\s/etc/', "copy/move file into /etc/"),
(rf'\b(cp|mv|install)\b.*\s["\']?{_PROJECT_SENSITIVE_WRITE_TARGET}["\']?{_COMMAND_TAIL}', "overwrite project env/config file"),
(r'\bsed\s+-[^\s]*i.*\s/etc/', "in-place edit of system config"), (r'\bsed\s+-[^\s]*i.*\s/etc/', "in-place edit of system config"),
(r'\bsed\s+--in-place\b.*\s/etc/', "in-place edit of system config (long flag)"), (r'\bsed\s+--in-place\b.*\s/etc/', "in-place edit of system config (long flag)"),
# Script execution via heredoc — bypasses the -e/-c flag patterns above. # Script execution via heredoc — bypasses the -e/-c flag patterns above.