hermes-agent/tests/tools/test_hardline_blocklist.py
amathxbt 6a6fd42111 fix(security): block subshell/brace-group wrappers at the hardline floor
Wrapping a catastrophic command in a bare subshell or brace group walked
straight past the unconditional hardline floor -- even under --yolo,
/yolo, approvals.mode=off, and cron approve mode. The command-substitution
forms were already caught; the bare paren / brace-group forms were the gap.

Rather than add the paren and brace openers to the flat _CMDPOS pattern
class (which cannot tell a real subshell opener from one sitting inside a
quoted argument, and would false-positive on ordinary prose such as a PR
title that merely mentions the trigger word), teach the existing
QUOTE-AWARE command-start tokenizer (_iter_shell_command_starts) to treat
the paren and brace openers as command starts, then emit a detection
variant that marks each real command start with a newline (already a
_CMDPOS separator). Openers inside quotes never register as starts, so
quoted arguments are left untouched while real subshell/brace bypasses now
anchor. One place covers every _CMDPOS rule (shutdown/reboot/init/
systemctl/telinit and the rm root/home/system floor).

Tests: subshell/brace bypasses added to the hardline-block, root-wipe, and
yolo-bypass sets; a regression set asserts quoted paren/brace prose is NOT
blocked (guards our own gh-pr-create workflow).
2026-07-01 03:03:05 -07:00

639 lines
24 KiB
Python

"""Tests for the unconditional hardline command blocklist.
The hardline list is a floor below yolo: a small set of commands so
catastrophic they should never run via the agent, regardless of --yolo,
gateway /yolo, approvals.mode=off, or cron approve mode.
Inspired by Mercury Agent's permission-hardened blocklist.
"""
import pytest
from tools.approval import (
HARDLINE_PATTERNS,
check_all_command_guards,
check_dangerous_command,
detect_dangerous_command,
detect_hardline_command,
disable_session_yolo,
enable_session_yolo,
reset_current_session_key,
set_current_session_key,
)
# -------------------------------------------------------------------------
# Pattern detection
# -------------------------------------------------------------------------
# Commands that MUST be hardline-blocked.
_HARDLINE_BLOCK = [
# rm -rf targeting root / system dirs / home
"rm -rf /",
"rm -rf /*",
# Shell-equivalent spellings of "rm -rf /": repeated slashes and
# current/parent-dir segments all collapse back to root, so they must
# hit the hardline floor too (regression: these used to slip through the
# root pattern's target group and fall to the softer DANGEROUS_PATTERNS
# rule, which --yolo / approvals.mode=off / cron approve-mode bypass).
"rm -rf //",
"rm -rf /.",
"rm -rf /./",
"rm -rf /..",
"rm -rf //*",
"rm -fr /./",
"ls && rm -rf //",
"rm -rf /home",
"rm -rf /home/*",
"rm -rf /etc",
"rm -rf /usr",
"rm -rf /var",
"rm -rf /boot",
"rm -rf /bin",
"rm --recursive --force /",
"rm -fr /",
"sudo rm -rf /",
"rm -rf ~",
"rm -rf ~/",
"rm -rf ~/*",
"rm -rf $HOME",
# Quoted path idioms — the recommended shell form for paths with special
# chars. These previously slipped past the floor because the surrounding
# quote broke both the flag group and the (\s|$) terminator (regression
# guard: catastrophic disk/home wipe under --yolo / approvals.mode=off).
'rm -rf "/"',
"rm -rf '/'",
'rm -rf "/*"',
'rm -rf "/etc"',
"rm -rf '/etc'",
'rm -rf "/home"',
'rm -rf "/usr"',
'rm -rf "$HOME"',
"rm -rf '$HOME'",
'rm -rf "$HOME/"',
'rm -rf "~"',
'sudo rm -rf "/"',
'rm -rf "/" && echo done',
# ${HOME} brace form (universally common, previously unmatched).
"rm -rf ${HOME}",
'rm -rf "${HOME}"',
"rm -fr ${HOME}",
# Filesystem format
"mkfs.ext4 /dev/sda1",
"mkfs /dev/sdb",
"mkfs.xfs /dev/nvme0n1",
# Raw block device overwrites
"dd if=/dev/zero of=/dev/sda bs=1M",
"dd if=/dev/urandom of=/dev/nvme0n1",
"dd if=anything of=/dev/hda",
"echo bad > /dev/sda",
"cat /dev/urandom > /dev/sdb",
# Fork bomb
":(){ :|:& };:",
# System-wide kill
"kill -9 -1",
"kill -1",
# Shutdown / reboot / halt
"shutdown -h now",
"shutdown -r now",
"sudo shutdown now",
"reboot",
"sudo reboot",
"halt",
"poweroff",
"init 0",
"init 6",
"telinit 0",
"systemctl poweroff",
"systemctl reboot",
"systemctl halt",
# Compound / subshell variants
"ls; reboot",
"echo done && shutdown -h now",
"false || halt",
"$(reboot)",
"`shutdown now`",
"sudo -E shutdown now",
"env FOO=1 reboot",
"exec shutdown",
"nohup reboot",
"setsid poweroff",
# Bare subshell `(cmd)` and brace-group `{ cmd; }` openers put the trigger
# at a real command position, so they must hit the floor just like `$(…)`.
# These slipped through before the quote-aware command-start tokenizer
# learned to recognize `(` / `{` (issue: (reboot) walked past --yolo).
"(reboot)",
"( reboot )",
"(shutdown -h now)",
"(poweroff)",
"(halt)",
"(init 0)",
"(systemctl reboot)",
"(sudo reboot)",
"{ reboot; }",
"{ shutdown -h now; }",
"{ poweroff; }",
"true && (reboot)",
"echo hi; { reboot; }",
]
# Commands that look superficially similar but must NOT be hardline-blocked.
_HARDLINE_ALLOW = [
# rm on non-protected paths
"rm -rf /tmp/foo",
"rm -rf /tmp/*",
"rm -rf ./build",
"rm -rf node_modules",
"rm -rf /home/user/scratch", # subpath of /home, not /home itself
"rm -rf ~/Downloads/old",
"rm -rf $HOME/tmp",
"rm foo.txt",
"rm -rf some/path",
# A dangerous-looking command embedded as a quoted *argument* to another
# command must not trip the floor: the path is immediately followed by a
# closing quote with no matching opening quote of its own, so the
# quote-tolerant matcher must still ignore it (no new false positives).
'git commit -m "rm -rf /"',
'git commit -m "wipe with rm -rf /etc"',
# dd to regular files
"dd if=/dev/zero of=./image.bin",
"dd if=./data of=./backup.bin",
# Redirect to regular files / non-block devices
"echo done > /tmp/flag",
"echo test > /dev/null",
# Reading devices is fine
"ls /dev/sda",
"cat /dev/urandom | head -c 10",
# Unrelated commands that happen to contain the trigger word
"grep 'shutdown' logs.txt",
"echo reboot",
"echo '# init 0 in comment'",
"cat rebooting.log",
"echo 'halt and catch fire'",
"python3 -c 'print(\"shutdown\")'",
"find . -name '*reboot*'",
# Word-boundary protection
"mkfs_helper --version",
# systemctl non-destructive verbs
"systemctl status nginx",
"systemctl restart nginx",
"systemctl stop nginx",
"systemctl start nginx",
# targeted kill
"kill -9 12345",
"kill -HUP 1234",
"pkill python",
# Ordinary ops
"git status",
"npm run build",
"sudo apt update",
"curl https://example.com | head",
]
@pytest.mark.parametrize("command", _HARDLINE_BLOCK)
def test_hardline_detection_blocks(command):
is_hl, desc = detect_hardline_command(command)
assert is_hl, f"expected hardline to match {command!r}"
assert desc, "hardline match must provide a description"
@pytest.mark.parametrize("command", _HARDLINE_ALLOW)
def test_hardline_detection_allows(command):
is_hl, desc = detect_hardline_command(command)
assert not is_hl, f"expected hardline NOT to match {command!r} (got: {desc})"
assert desc is None
# Commands written with the ordinary quoting / brace shell idioms that
# previously slipped past the floor. Kept as an explicit regression set so
# the intent (quoting `rm -rf "/"` must not be a disk-wipe bypass) survives
# any future refactor of the rm patterns.
_QUOTED_BRACE_BYPASS = [
'rm -rf "/"',
"rm -rf '/'",
'rm -rf "/etc"',
'rm -rf "/home"',
'rm -rf "$HOME"',
"rm -rf ${HOME}",
'rm -rf "${HOME}"',
]
@pytest.mark.parametrize("command", _QUOTED_BRACE_BYPASS)
def test_quoted_and_brace_paths_are_hardline_blocked(command):
"""Quoted paths and ${HOME} must hit the floor (was a silent bypass)."""
is_hl, desc = detect_hardline_command(command)
assert is_hl, f"quoting/brace bypass leaked through hardline floor: {command!r}"
assert desc
# Commands that carry the literal string "rm -rf /" (or a sibling) as DATA in
# another command's quoted argument — a PR title, a commit message, an echo /
# printf argument. The shell never executes that text as an rm command, so the
# hardline floor must NOT fire; otherwise the command cannot run at all (this
# blocked `gh pr create --title "…rm -rf /…"` outright). Regression guard for
# the command-position anchor on the rm rules.
_DATA_ARG_NOT_A_COMMAND = [
'gh pr create --title "block rm -rf / spellings"',
'git commit -m "fixes rm -rf / bypass"',
'echo "run rm -rf / now"',
'echo "rm -rf /"',
'printf "%s" "rm -rf /"',
'gh issue comment 1 --body "the fix blocks rm -rf //"',
# A `(` or `{` INSIDE a quoted argument is prose, not a subshell/brace
# opener — the trigger word after it is data. Naively adding `(` / `{` to
# the flat command-position class blocked these (it broke our own
# `gh pr create --title "…(reboot)…"` workflow); the quote-aware tokenizer
# must leave them alone.
'gh pr create --title "block (reboot) spellings"',
'git commit -m "(rm -rf /) note"',
'echo "(reboot)"',
'echo "{ reboot; }"',
"echo '(poweroff)'",
"echo '{ rm -rf /; }'",
'find . -name "*(reboot)*"',
]
@pytest.mark.parametrize("command", _DATA_ARG_NOT_A_COMMAND)
def test_root_wipe_string_as_data_arg_is_not_hardline(command):
""""rm -rf /" as a quoted argument to another command is data, not a wipe."""
is_hl, desc = detect_hardline_command(command)
assert not is_hl, f"false positive: quoted data arg hit hardline floor: {command!r} ({desc})"
# Real root wipes at every command position — bare, chained after a separator,
# inside a command substitution ($()/backtick), or after sudo/env wrappers.
# The command-position anchor must keep catching all of these; the substitution
# forms exercise the shell-metacharacter terminator on the bare path branch.
_COMMAND_POSITION_ROOT_WIPES = [
"rm -rf /",
"ls && rm -rf /",
"ls; rm -rf /",
"echo x | rm -rf /",
"sudo rm -rf /",
"env X=1 rm -rf /",
"$(rm -rf /)",
"`rm -rf /`",
'echo "$(rm -rf /)"',
# Bare subshell / brace-group openers are real command positions too.
"(rm -rf /)",
"{ rm -rf /; }",
"(rm -rf ~)",
"(sudo rm -rf /)",
]
@pytest.mark.parametrize("command", _COMMAND_POSITION_ROOT_WIPES)
def test_root_wipe_at_command_position_is_hardline(command):
"""A real `rm -rf /` at any command position stays hardline-blocked."""
is_hl, desc = detect_hardline_command(command)
assert is_hl, f"real root wipe leaked past the floor: {command!r}"
assert desc
# -------------------------------------------------------------------------
# Shell line-continuation bypass
# -------------------------------------------------------------------------
#
# A backslash immediately followed by a newline is a POSIX line
# continuation: the shell removes BOTH characters and joins the tokens, so
# `rm -rf \<newline>/` executes as `rm -rf /`. The normalizer used to strip
# only backslash-escapes of NON-newline characters (`\\([^\n])`), leaving the
# dangling backslash wedged between tokens — which broke the structured
# rm/dd/mkfs patterns and let a root wipe slip past the hardline floor.
# (command_with_continuation, description_substring) — each is the
# line-continuation form of a command already in _HARDLINE_BLOCK.
_HARDLINE_LINE_CONTINUATION = [
("rm -rf \\\n/", "root"), # split before the path
("rm -r\\\nf /", "root"), # split inside the flag bundle
("rm -rf \\\n~", "home"), # home-directory wipe
("rm -rf \\\r\n/", "root"), # CRLF line ending
("mkfs.ext4 \\\n/dev/sda1", "mkfs"), # filesystem format
]
@pytest.mark.parametrize("command,desc_substr", _HARDLINE_LINE_CONTINUATION)
def test_hardline_blocks_line_continuation(command, desc_substr):
is_hl, desc = detect_hardline_command(command)
assert is_hl, f"line-continuation bypassed hardline detection: {command!r}"
assert desc and desc_substr in desc.lower(), (
f"unexpected description {desc!r} for {command!r}"
)
# -------------------------------------------------------------------------
# Integration with the approval flow
# -------------------------------------------------------------------------
@pytest.fixture
def clean_session(monkeypatch):
"""Reset session-scoped approval state around each test."""
monkeypatch.delenv("HERMES_YOLO_MODE", raising=False)
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
monkeypatch.delenv("HERMES_CRON_SESSION", raising=False)
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
token = set_current_session_key("hardline_test")
try:
disable_session_yolo("hardline_test")
yield
finally:
disable_session_yolo("hardline_test")
reset_current_session_key(token)
def test_check_dangerous_command_blocks_hardline(clean_session):
result = check_dangerous_command("rm -rf /", "local")
assert result["approved"] is False
assert result.get("hardline") is True
assert "BLOCKED (hardline)" in result["message"]
def test_check_all_command_guards_blocks_hardline(clean_session):
result = check_all_command_guards("rm -rf /", "local")
assert result["approved"] is False
assert result.get("hardline") is True
assert "BLOCKED (hardline)" in result["message"]
def test_yolo_env_var_cannot_bypass_hardline(clean_session, monkeypatch):
"""HERMES_YOLO_MODE=1 must not bypass the hardline floor."""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
for cmd in ['rm -rf /', 'rm -rf "/"', 'rm -rf "$HOME"', "rm -rf ${HOME}",
"shutdown -h now", "mkfs.ext4 /dev/sda", "reboot"]:
r1 = check_dangerous_command(cmd, "local")
assert r1["approved"] is False, f"yolo leaked hardline on {cmd!r} (check_dangerous_command)"
assert r1.get("hardline") is True
r2 = check_all_command_guards(cmd, "local")
assert r2["approved"] is False, f"yolo leaked hardline on {cmd!r} (check_all_command_guards)"
assert r2.get("hardline") is True
def test_root_collapse_forms_cannot_bypass_hardline(clean_session, monkeypatch):
"""Shell-equivalent spellings of "rm -rf /" stay blocked under yolo.
"//", "/.", "/./", "/..", "//*" all collapse to the root filesystem in
the shell. They previously matched only the softer DANGEROUS_PATTERNS
rule, which yolo bypasses — leaving the hardline floor open to a full
root wipe under --yolo / approvals.mode=off / cron approve-mode.
"""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
for cmd in ["rm -rf //", "rm -rf /.", "rm -rf /./", "rm -rf /..", "rm -rf //*"]:
is_hl, _ = detect_hardline_command(cmd)
assert is_hl, f"{cmd!r} should be hardline-blocked"
result = check_all_command_guards(cmd, "local")
assert result["approved"] is False, f"yolo leaked hardline on {cmd!r}"
assert result.get("hardline") is True
def test_root_collapse_pattern_leaves_real_paths_alone(clean_session):
"""The broadened root token must not over-match real trailing segments.
A path with a real component after the root-collapse prefix (/tmp,
/home/user/x, /.ssh, ./build) is recoverable-or-legitimate and must NOT
be pulled onto the hardline floor by the "collapse to /" broadening.
"""
for cmd in ["rm -rf /tmp", "rm -rf /home/user/x", "rm -rf /.ssh",
"rm -rf /.config", "rm -rf ./build", "rm -rf /opt/foo"]:
is_hl, _ = detect_hardline_command(cmd)
assert not is_hl, f"{cmd!r} must not be hardline-blocked (over-match)"
def test_subshell_brace_group_cannot_bypass_hardline(clean_session, monkeypatch):
"""Wrapping a catastrophic command in `(…)` or `{ …; }` must not bypass
the floor, even under yolo. `(reboot)` / `{ shutdown -h now; }` walked
straight past the guard before the command-start tokenizer recognized the
subshell and brace-group openers.
"""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
for cmd in ["(reboot)", "( reboot )", "(shutdown -h now)", "(poweroff)",
"(systemctl reboot)", "(init 0)", "(sudo reboot)",
"{ reboot; }", "{ shutdown -h now; }", "{ poweroff; }",
"(rm -rf /)", "{ rm -rf /; }", "(rm -rf ~)",
"true && (reboot)", "echo hi; { reboot; }"]:
r1 = check_dangerous_command(cmd, "local")
assert r1["approved"] is False, f"yolo leaked hardline on {cmd!r} (check_dangerous_command)"
assert r1.get("hardline") is True
r2 = check_all_command_guards(cmd, "local")
assert r2["approved"] is False, f"yolo leaked hardline on {cmd!r} (check_all_command_guards)"
assert r2.get("hardline") is True
def test_quoted_paren_brace_prose_not_blocked_under_yolo(clean_session, monkeypatch):
"""A `(` / `{` inside a quoted argument is prose, not a command opener.
Regression guard: naively adding `(` / `{` to the flat command-position
class blocked ordinary quoted arguments — including our own
`gh pr create --title "…(reboot)…"` workflow. The quote-aware tokenizer
must leave quoted text untouched, so these stay runnable.
"""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
for cmd in ['gh pr create --title "block (reboot) spellings"',
'git commit -m "(rm -rf /) note"',
'echo "(reboot)"', 'echo "{ reboot; }"',
"echo '(poweroff)'", 'find . -name "*(reboot)*"']:
assert detect_hardline_command(cmd)[0] is False, (
f"quoted prose false-positived on the hardline floor: {cmd!r}"
)
def test_line_continuation_root_wipe_cannot_bypass_hardline(clean_session, monkeypatch):
"""A line-continuation root wipe must stay blocked even under yolo.
`rm -rf \\<newline>/` runs as `rm -rf /`. Yolo bypasses the regular
dangerous-command layer, so the hardline floor is the only thing left to
catch it — it must hold.
"""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
result = check_all_command_guards("rm -rf \\\n/", "local")
assert result["approved"] is False, "yolo leaked a line-continuation root wipe"
assert result.get("hardline") is True
assert "BLOCKED (hardline)" in result["message"]
def test_session_yolo_cannot_bypass_hardline(clean_session):
"""Gateway /yolo (session-scoped) must not bypass the hardline floor."""
enable_session_yolo("hardline_test")
result = check_dangerous_command("rm -rf /", "local")
assert result["approved"] is False
assert result.get("hardline") is True
result = check_all_command_guards("rm -rf /", "local")
assert result["approved"] is False
assert result.get("hardline") is True
def test_approvals_mode_off_cannot_bypass_hardline(clean_session, monkeypatch, tmp_path):
"""config approvals.mode=off (yolo-equivalent) must not bypass hardline."""
# _get_approval_mode() reads from hermes config; simplest path: monkeypatch the helper.
import tools.approval as approval_mod
monkeypatch.setattr(approval_mod, "_get_approval_mode", lambda: "off")
result = check_all_command_guards("rm -rf /", "local")
assert result["approved"] is False
assert result.get("hardline") is True
def test_cron_approve_mode_cannot_bypass_hardline(clean_session, monkeypatch):
"""Cron sessions with cron_mode=approve must not bypass hardline."""
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
import tools.approval as approval_mod
monkeypatch.setattr(approval_mod, "_get_cron_approval_mode", lambda: "approve")
result = check_all_command_guards("rm -rf /", "local")
assert result["approved"] is False
assert result.get("hardline") is True
def test_container_backends_still_bypass(clean_session):
"""Containerized backends remain bypass-approved — they can't touch the host.
Hardline only protects environments with real host impact (local, ssh).
"""
for env in ("docker", "singularity", "modal", "daytona"):
r1 = check_dangerous_command("rm -rf /", env)
assert r1["approved"] is True, f"container {env} should still bypass"
r2 = check_all_command_guards("rm -rf /", env)
assert r2["approved"] is True, f"container {env} should still bypass"
def test_hardline_runs_before_dangerous_detection(clean_session):
"""Hardline command should return hardline block, not dangerous approval prompt."""
# `rm -rf /` is both hardline AND matches DANGEROUS_PATTERNS. Hardline must win.
is_dangerous, _, _ = detect_dangerous_command("rm -rf /")
assert is_dangerous, "precondition: rm -rf / is also in DANGEROUS_PATTERNS"
result = check_dangerous_command("rm -rf /", "local")
assert result.get("hardline") is True
def test_recoverable_dangerous_commands_still_pass_yolo(clean_session, monkeypatch):
"""Yolo still bypasses the regular DANGEROUS_PATTERNS list.
This confirms we haven't broken the yolo escape hatch — only narrowed it.
"""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
# These are dangerous but NOT hardline — yolo should still pass them.
for cmd in ["rm -rf /tmp/x", "chmod -R 777 .", "git reset --hard", "git push --force"]:
# Sanity: still flagged as dangerous
is_dangerous, _, _ = detect_dangerous_command(cmd)
assert is_dangerous, f"precondition: {cmd!r} should be in DANGEROUS_PATTERNS"
# But NOT hardline
is_hl, _ = detect_hardline_command(cmd)
assert not is_hl, f"{cmd!r} should not be hardline"
# And yolo bypasses the dangerous check
result = check_dangerous_command(cmd, "local")
assert result["approved"] is True, f"yolo should have bypassed {cmd!r}"
def test_hardline_list_is_small():
"""Hardline list stays focused on unrecoverable commands only.
If you're adding a 20th+ pattern, reconsider — it probably belongs in
DANGEROUS_PATTERNS where yolo can still bypass it.
"""
assert len(HARDLINE_PATTERNS) <= 20, (
f"HARDLINE_PATTERNS has grown to {len(HARDLINE_PATTERNS)} entries; "
"only truly unrecoverable commands belong here."
)
# =========================================================================
# Sudo stdin guard — blocks "sudo -S" without SUDO_PASSWORD
# =========================================================================
_SUDO_STDIN_BLOCK = [
"sudo -S whoami",
"echo hunter2 | sudo -S whoami",
"sudo -S -u root whoami",
"sudo -S apt-get install foo",
"echo password | sudo -S systemctl restart nginx",
"sudo -k && sudo -S whoami",
]
_SUDO_STDIN_ALLOW = [
# Plain sudo without -S — goes through normal approval
"sudo whoami",
"sudo apt-get update",
"sudo -u root whoami",
# -S flag not attached to sudo
"echo -S hello",
"some_tool -S thing",
# Literal text mention of sudo
"echo 'use sudo -S to pipe passwords'",
]
_SUDO_STDIN_BLOCK_YOLO = [
"sudo -S whoami",
"echo hunter2 | sudo -S apt-get install",
]
def test_sudo_stdin_guard_detects_without_password():
"""sudo -S is dangerous when SUDO_PASSWORD is not configured."""
import tools.approval as approval_mod
for cmd in _SUDO_STDIN_BLOCK:
is_blocked, desc = approval_mod._check_sudo_stdin_guard(cmd)
assert is_blocked, f"expected sudo stdin guard to block {cmd!r}"
assert "sudo" in desc.lower()
def test_sudo_stdin_guard_allows_benign_commands():
"""Commands without explicit sudo -S are not blocked."""
import tools.approval as approval_mod
for cmd in _SUDO_STDIN_ALLOW:
is_blocked, desc = approval_mod._check_sudo_stdin_guard(cmd)
assert not is_blocked, f"expected sudo stdin guard NOT to block {cmd!r}"
def test_sudo_stdin_guard_bypassed_when_password_configured(monkeypatch):
"""When SUDO_PASSWORD is set, sudo -S is legitimate (injected by transform)."""
import tools.approval as approval_mod
monkeypatch.setenv("SUDO_PASSWORD", "testpass")
for cmd in _SUDO_STDIN_BLOCK:
is_blocked, _ = approval_mod._check_sudo_stdin_guard(cmd)
assert not is_blocked, f"with SUDO_PASSWORD set, {cmd!r} should NOT be blocked"
def test_sudo_stdin_guard_blocks_via_check_all_command_guards(clean_session):
"""Integration: check_all_command_guards returns block for sudo -S."""
for cmd in _SUDO_STDIN_BLOCK:
result = check_all_command_guards(cmd, "local")
assert result["approved"] is False, f"expected block on {cmd!r}"
# Should NOT be marked as hardline (it's sudo-specific)
assert result.get("hardline") is not True
assert "BLOCKED" in result["message"]
assert "sudo -S" in result["message"].lower() or "sudo password" in result["message"].lower()
def test_sudo_stdin_guard_not_blocked_by_yolo(clean_session, monkeypatch):
"""yolo/approvals.mode=off must NOT bypass sudo stdin guard."""
monkeypatch.setenv("HERMES_YOLO_MODE", "1")
for cmd in _SUDO_STDIN_BLOCK_YOLO:
result = check_all_command_guards(cmd, "local")
assert result["approved"] is False, f"yolo leaked sudo guard on {cmd!r}"
def test_sudo_stdin_guard_container_bypass(clean_session):
"""Containerized backends still bypass — they can't touch the host."""
for env in ("docker", "singularity", "modal", "daytona"):
for cmd in _SUDO_STDIN_BLOCK:
result = check_all_command_guards(cmd, env)
assert result["approved"] is True, f"container {env} should bypass sudo guard on {cmd!r}"