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
This commit is contained in:
sheeki003
2026-03-11 14:20:32 +05:30
committed by teknium1
parent 2fe853bcc9
commit 375ce8a881
9 changed files with 2153 additions and 1902 deletions

19
cli.py
View File

@@ -3565,13 +3565,15 @@ class HermesCLI:
_cprint(f"\n{_DIM} ⏱ Timeout — continuing without sudo{_RST}")
return ""
def _approval_callback(self, command: str, description: str) -> str:
def _approval_callback(self, command: str, description: str,
*, allow_permanent: bool = True) -> str:
"""
Prompt for dangerous command approval through the prompt_toolkit UI.
Called from the agent thread. Shows a selection UI similar to clarify
with choices: once / session / always / deny.
with choices: once / session / always / deny. When allow_permanent
is False (tirith warnings present), the 'always' option is hidden.
Uses _approval_lock to serialize concurrent requests (e.g. from
parallel delegation subtasks) so each prompt gets its own turn
and the shared _approval_state / _approval_deadline aren't clobbered.
@@ -3581,7 +3583,7 @@ class HermesCLI:
with self._approval_lock:
timeout = 60
response_queue = queue.Queue()
choices = ["once", "session", "always", "deny"]
choices = ["once", "session", "always", "deny"] if allow_permanent else ["once", "session", "deny"]
self._approval_state = {
"command": command,
@@ -3941,6 +3943,13 @@ class HermesCLI:
set_sudo_password_callback(self._sudo_password_callback)
set_approval_callback(self._approval_callback)
set_secret_capture_callback(self._secret_capture_callback)
# Ensure tirith security scanner is available (downloads if needed)
try:
from tools.tirith_security import ensure_installed
ensure_installed()
except Exception:
pass # Non-fatal — fail-open at scan time if unavailable
# Key bindings for the input area
kb = KeyBindings()