Skip to content

feat(security): add tirith pre-exec command scanning#904

Closed
sheeki03 wants to merge 1 commit into
NousResearch:mainfrom
sheeki03:feat/tirith-pre-exec-security
Closed

feat(security): add tirith pre-exec command scanning#904
sheeki03 wants to merge 1 commit into
NousResearch:mainfrom
sheeki03:feat/tirith-pre-exec-security

Conversation

@sheeki03

@sheeki03 sheeki03 commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Integrates tirith as an optional pre-execution security scanner that detects content-level threats the existing 50-pattern dangerous command detector does not cover: homograph URLs, pipe-to-interpreter variants, ANSI terminal injection, zero-width Unicode, and environment variable manipulation (66 rules across 11 categories).

The existing approval.py dangerous command detector catches destructive ops (rm -rf, DROP TABLE, fork bombs) but cannot inspect command content — URL payloads, Unicode tricks, or escape sequences. tirith fills that gap with a Rust binary that runs as a subprocess, adding sub-millisecond scanning before each command execution.

Architecture: gather-then-decide. Both tirith and the dangerous command detector run before any approval prompt, and their findings are combined into a single approval request. This prevents the gateway force=True replay from bypassing one check when only the other was shown to the user.

Auto-install with mandatory provenance verification. When tirith is not found on PATH, the wrapper automatically downloads the latest release from GitHub and verifies it via cosign (Sigstore) before installation. SHA-256 alone only proves self-consistency; cosign verifies the release was produced by the expected GitHub Actions workflow. If cosign is unavailable, auto-install is skipped (users can install tirith manually).

Non-blocking startup. The network download runs in a background daemon thread so CLI/gateway startup is never blocked. Commands fail-open until tirith becomes available.

Failure recovery. Failed installs are cached to disk with a reason tag. The cosign_missing reason auto-clears when cosign later appears on PATH — both in-memory and on disk — so a long-lived gateway picks up the change without restart.

Related Issue

N/A — new security feature

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

New files

  • tools/tirith_security.py (~665 lines) — Subprocess wrapper + auto-installer

    • Exit code semantics: 0=allow, 1=block, 2=warn (exit code is source of truth; JSON enriches findings)
    • check_command_security(): main API, returns {action, findings, summary}
    • ensure_installed(): non-blocking startup hook
    • Auto-installer with mandatory cosign provenance verification (_verify_cosign, _install_tirith)
    • Disk-persistent failure markers with retryable-cause tracking (_mark_install_failed, _read_failure_reason)
    • HERMES_HOME-aware paths matching codebase convention (_get_hermes_home)
    • Operational failures (OSError, TimeoutExpired) respect fail_open config; programming errors propagate
  • tests/tools/test_tirith_security.py (~958 lines, 62 tests) — Full coverage of the subprocess wrapper

    • TestExitCodeMapping, TestJsonParseFailure, TestOSErrorFailOpen, TestTimeoutFailOpen, TestUnknownExitCode
    • TestDisabled, TestPathExpansion, TestCaps, TestProgrammingErrors
    • TestEnsureInstalled, TestFailedDownloadCaching, TestExplicitPathNoAutoDownload
    • TestCosignVerification (pass, rejection, missing, exec-fail, artifacts missing, identity pinning)
    • TestBackgroundInstall, TestDiskFailureMarker (mark/check, expiry, cosign recovery, reason preservation)
    • TestHermesHomeIsolation
  • tests/tools/test_command_guards.py (~312 lines, 21 tests) — Integration tests for combined guard orchestration

    • Container skip, tirith allow+safe, tirith block, tirith allow+dangerous
    • tirith warn+safe (CLI prompt, session approved, non-interactive auto-allow)
    • Combined warnings (gateway, CLI deny, CLI session approves both)
    • Always visibility (dangerous-only shows permanent option)
    • ImportError fallback, empty findings, gateway pattern_keys persistence
    • Programming error propagation

Modified files

  • tools/approval.py — New check_all_command_guards() orchestrator (~80 lines added)

    • Gathers findings from both tirith and dangerous command detection, then combines into single approval request
    • allow_permanent: bool parameter on prompt_dangerous_approval() — hides [a]lways when any tirith warning present (tirith keys are session-scoped, too broad for permanent allowlisting)
    • pattern_keys list in pending approval dict for gateway replay
  • tools/terminal_tool.py — Replace direct _check_dangerous_command call with check_all_command_guards

  • cli.py — Update _approval_callback signature for allow_permanent kwarg; call ensure_installed() at startup

  • gateway/run.py — Iterate pattern_keys list on replay approval (backward compatible via pattern_key fallback); call ensure_installed() at startup

  • hermes_cli/config.py — Add security config defaults to DEFAULT_CONFIG (tirith_enabled, tirith_path, tirith_timeout, tirith_fail_open); split _COMMENTED_SECTIONS into independent blocks so unrelated defaults don't clobber each other

  • cli-config.yaml.example — Document tirith security config section with install instructions

How to Test

  1. Run the new test suites:

    pytest tests/tools/test_tirith_security.py -v   # 62 tests
    pytest tests/tools/test_command_guards.py -v     # 21 tests
  2. Verify no regression in existing approval tests:

    pytest tests/tools/test_approval.py -v           # 53 tests
  3. Full suite:

    pytest tests/ -q
  4. Manual testing (requires tirith installed — brew install sheeki03/tap/tirith):

    • Commands with homograph URLs should trigger a warn/block
    • Safe commands like echo hello should pass through
    • The [a]lways approval option should be hidden when tirith warnings are present

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(security): add tirith pre-exec command scanning)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (83 new tests across 2 new test files)
  • I've tested on my platform: macOS 15.4 (Darwin 25.3.0, arm64)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

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
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #1256. Your substantive work was cherry-picked onto current main with authorship preserved, then adapted for current-main compatibility (approval UX, yolo/noninteractive semantics, and gateway replay integration). Thank you for the contribution.

@teknium1 teknium1 closed this Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants