Skip to content

Add optional Tirith pre-exec scanning for sandbox bash tool - #14

Open
sheeki03 wants to merge 2 commits into
10xapp:mainfrom
sheeki03:feat/tirith-sandbox-bash-scan
Open

Add optional Tirith pre-exec scanning for sandbox bash tool#14
sheeki03 wants to merge 2 commits into
10xapp:mainfrom
sheeki03:feat/tirith-sandbox-bash-scan

Conversation

@sheeki03

@sheeki03 sheeki03 commented Mar 31, 2026

Copy link
Copy Markdown

What this does

The AI agent sandbox runs bash commands with subprocess.run(shell=True) and no pre-execution checks. The E2B sandbox provides process isolation, but the LLM inside can still curl | bash from a homograph URL, exfiltrate credentials, or pipe through a decode chain — and nothing gates that today.

This adds Tirith as an optional pre-exec scanner for the sandbox bash tool. When enabled, every bash command the LLM requests is scanned before execution. If Tirith flags the command, it's blocked and structured findings are returned to the model so it can reformulate — the LLM gets told what was wrong and why, which is more useful than a generic rejection.

Tirith has already been integrated into Hermes Agent where it gates all terminal command execution.

What Tirith catches

80+ detection rules across 15 categories. The ones most relevant to sandbox bash:

  • Pipe-to-shellcurl | bash, wget | sh, every source-to-sink pattern
  • Homograph attacks — Cyrillic/Greek lookalike characters in URLs that resolve to attacker servers
  • Base64 decode-executebase64 -d | bash, python -c "exec(b64decode(...))", PowerShell encoded commands
  • Data exfiltrationcurl -d @/etc/passwd, env var uploads, command substitution exfil
  • Credential exposure — AWS keys, GitHub PATs, private key blocks, high-entropy secrets
  • Insecure transport — plain HTTP piped to shell, disabled TLS verification

Sub-millisecond on clean commands. The LLM never notices it's there until something is actually wrong.

How it works

  1. LLM requests a bash tool call
  2. If TIRITH_ENABLED=true, the runtime runs tirith check --json --non-interactive -- <command>
  3. Exit code decides: 0 = allow, 1 = block, 2 = warn (exit code is authoritative — JSON enriches but never overrides)
  4. On block/warn, the command is not executed and findings are returned to the model
  5. On allow, the command runs normally — zero overhead path is identical to today

If Tirith is not installed, times out, or crashes, the default behavior is fail-open (configurable to fail-closed via TIRITH_FAIL_MODE).

What the model sees when a command is blocked

{
  "status": "error",
  "error_type": "security_blocked",
  "tirith_action": "block",
  "message": "Command blocked by security scan: [CRITICAL] Curl pipe to shell. Review the findings and reformulate the command.",
  "findings": [
    {
      "rule_id": "curl_pipe_shell",
      "severity": "CRITICAL",
      "title": "Curl pipe to shell",
      "description": "Piping curl output to a shell interpreter allows arbitrary code execution."
    }
  ]
}

The model gets enough context to reformulate (e.g., download first, then inspect, then execute) rather than retrying the same dangerous command.

Configuration

Four env vars, all optional:

Variable Default Description
TIRITH_ENABLED false Set to true to enable scanning
TIRITH_PATH (auto-detect) Explicit path to tirith binary inside the sandbox
TIRITH_TIMEOUT 10 Seconds to wait for tirith check
TIRITH_FAIL_MODE open open = allow if scanner fails; closed = block

For production, prebake the tirith binary into the E2B template at /usr/local/bin/tirith. For development, install tirith in the sandbox and set TIRITH_PATH.

Files changed

File What changed
core-api/api/config.py 4 new settings with sensible defaults
core-api/.env.example Documented under [OPTIONAL] Tirith section
core-api/api/services/agents/dispatch.py Passes tirith env vars to sandbox
core-api/api/services/agents/runtime_bundle.py Embedded config + scanner helper + bash gate
core-api/tests/test_tirith_sandbox.py 19 tests: runtime compilation, scanner behavior matrix, integration gate tests

Tests

19 tests covering:

  • Embedded runtime string constants compile as valid Python
  • Scanner verdict handling: allow, block, warn, block+bad JSON, warn+bad JSON, non-dict JSON, non-list findings
  • Fail-open/fail-closed behavior: missing binary, timeout, unknown exit codes
  • Binary resolution: explicit path, PATH lookup, tilde expansion
  • Config safety: TIRITH_TIMEOUT=abc doesn't crash import
  • Integration gate: disabled skips scan (verified via marker files), enabled+allow executes, enabled+block prevents execution

All tests are self-contained — they load runtime_bundle.py by file path to avoid api.services import side effects and work without Supabase env vars.

Scope

This PR is intentionally narrow:

  • Bash tool only — no scanning of read_file, write_file, or edit_file
  • No auto-installer — tirith must be pre-installed in the sandbox image
  • No approval UX — blocked commands are returned to the model, not prompted to the user
  • No retry caps — that's a follow-up concern for the agent loop, not the scanner

How to try it

# Install tirith
brew install sheeki03/tap/tirith  # or: cargo install tirith / npm i -g tirith

# Set env and boot a sandbox
TIRITH_ENABLED=true TIRITH_PATH=/usr/local/bin/tirith

# The agent will see blocks for dangerous commands and reformulate

Summary by CodeRabbit

  • New Features

    • Optional pre-execution security scanning that can allow, warn, or block tool execution, with configurable timeout, fail-open/closed behavior, and scanner path auto-detection.
    • Configurable sandbox working directory for tool runs.
  • Tests

    • Comprehensive unit and integration tests covering scanning behavior, configuration validation, and sandbox gating.

The embedded agent runtime executes bash commands via subprocess.run()
with shell=True and no pre-execution security checks. The sandbox
provides containment but no intent validation — the LLM can run
curl|bash, exfiltrate data, or hit homograph URLs without any gate.

This adds an optional Tirith scanner integration that intercepts bash
commands before execution. When enabled, tirith check runs against
the command and returns structured findings to the model so it can
reformulate, rather than silently executing something dangerous.

Key design decisions:
- Opt-in via TIRITH_ENABLED (default: false, zero impact when off)
- Exit code is source of truth (0=allow, 1=block, 2=warn)
- JSON enriches findings but never overrides the exit code verdict
- Fail-open by default (configurable to fail-closed)
- Distinguishes real verdicts from scanner failures in the response
- One-time warnings for operational failures (no log spam)
- Bash only — other tools are out of scope for this change
@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a9033c8-930b-4a74-a901-85421ee489b6

📥 Commits

Reviewing files that changed from the base of the PR and between 30c51f3 and aef3509.

📒 Files selected for processing (2)
  • core-api/api/config.py
  • core-api/tests/unit/test_config_tirith_settings.py
✅ Files skipped from review due to trivial changes (1)
  • core-api/tests/unit/test_config_tirith_settings.py

📝 Walkthrough

Walkthrough

Adds optional Tirith pre-exec security scanning: new env vars and Settings fields, sandbox env plumbing, runtime scanner integration that can allow/warn/block tool execution, and comprehensive unit/integration tests validating behaviors.

Changes

Cohort / File(s) Summary
Configuration
core-api/.env.example, core-api/api/config.py
Added Tirith config variables (TIRITH_ENABLED, TIRITH_PATH, TIRITH_TIMEOUT, TIRITH_FAIL_MODE) to env example and new Settings fields plus a post-validator enforcing timeout > 0 and fail-mode ∈ {open, closed}.
Sandbox plumbing
core-api/api/services/agents/dispatch.py
create_sandbox now injects Tirith-related env vars (TIRITH_ENABLED, TIRITH_PATH, TIRITH_TIMEOUT, TIRITH_FAIL_MODE) into sandbox envs.
Runtime tooling
core-api/api/services/agents/runtime_bundle.py
Added Tirith runtime helpers: integer parsing, binary lookup, JSON findings parser with limits, rate-limited warnings, tirith_check_command() that returns None or structured error, and integration of Tirith gating into bash tool execution; also made sandbox CWD configurable via SANDBOX_CWD.
Tests — integration/unit
core-api/tests/test_tirith_sandbox.py, core-api/tests/unit/test_config_tirith_settings.py
New comprehensive tests: runtime materialization and compile checks, unit tests for tirith_check_command using fake tirith binaries (exit codes, JSON shapes, timeouts, missing binary), integration tests for execute_tool("bash", ...) gating, and unit tests validating Settings normalization and validation.

Sequence Diagram

sequenceDiagram
    participant Client as Bash Tool Caller
    participant Gate as Execution Gate
    participant Tirith as Tirith Scanner
    participant Sandbox as Sandbox Environment

    Client->>Gate: execute_tool("bash", command)
    alt TIRITH_ENABLED = true
        Gate->>Tirith: tirith_check_command(command)
        Tirith->>Tirith: run binary, parse JSON findings
        alt exit code == 0 (allow)
            Tirith-->>Gate: None
            Gate->>Sandbox: Execute command
        else exit code != 0 (block/warn)
            Tirith-->>Gate: {error_type, tirith_action, message, findings}
            alt tirith_action == "block"
                Gate-->>Client: Return error (status="error", error_type="security_blocked")
            else tirith_action == "warn"
                Gate->>Sandbox: Execute command, emit warning
            end
        end
    else TIRITH_ENABLED = false
        Gate->>Sandbox: Execute command directly
    end
    Sandbox-->>Client: Command result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through code to add a guard so bold,

Tirith whispers warnings, blocks when rules are told.
In sandbox burrows commands now safely play,
A rabbit's nod—secure paths light the way! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding Tirith pre-exec scanning as an optional feature for the sandbox bash tool, which aligns with the changeset's core objective.
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core-api/api/services/agents/dispatch.py (1)

141-144: Document or enforce a sandbox recycle when Tirith config changes.

These vars are only applied in create_sandbox(). The reconnect paths reuse existing sandboxes unchanged, so toggling TIRITH_ENABLED or TIRITH_FAIL_MODE only affects newly created sandboxes. If live policy changes are expected, compare a config hash on reconnect and recreate on drift; otherwise make the recycle requirement explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/api/services/agents/dispatch.py` around lines 141 - 144, The current
Tirith-related env vars (TIRITH_ENABLED, TIRITH_PATH, TIRITH_TIMEOUT,
TIRITH_FAIL_MODE) are only applied when create_sandbox() runs, so reconnect
logic reuses old sandboxes and ignores config changes; to fix, compute a stable
config hash from the Tirith-relevant settings (e.g., tirith_enabled,
tirith_path, tirith_timeout, tirith_fail_mode) and store it on the sandbox
metadata when create_sandbox() makes the sandbox, then update the reconnect flow
(the code path that reuses existing sandboxes on agent reconnect) to compare the
stored sandbox config hash with the current settings hash and force a sandbox
recycle/recreate when they differ (or alternatively throw/require explicit
recycle); ensure you reference create_sandbox(), the reconnect handler, and the
Tirith env names (TIRITH_ENABLED/TIRITH_FAIL_MODE) so maintainers can locate and
apply the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core-api/api/config.py`:
- Around line 186-190: Validate Tirith config right after loading the settings:
ensure tirith_timeout is a positive integer (>0) and tirith_fail_mode is one of
the allowed values ("open" or "closed"); if tirith_enabled is true reject
invalid timeout or fail_mode values with a clear error/exception instead of
silently defaulting; also treat an explicitly provided TIRITH_FAIL_MODE (or
tirith_path) that is not recognized as invalid and fail fast. Update the config
initialization/validation code around the tirith_* fields (tirith_enabled,
tirith_path, tirith_timeout, tirith_fail_mode) to enforce these checks and
raise/exit on invalid values so unsafe or mistyped settings are not injected
into the sandbox.

---

Nitpick comments:
In `@core-api/api/services/agents/dispatch.py`:
- Around line 141-144: The current Tirith-related env vars (TIRITH_ENABLED,
TIRITH_PATH, TIRITH_TIMEOUT, TIRITH_FAIL_MODE) are only applied when
create_sandbox() runs, so reconnect logic reuses old sandboxes and ignores
config changes; to fix, compute a stable config hash from the Tirith-relevant
settings (e.g., tirith_enabled, tirith_path, tirith_timeout, tirith_fail_mode)
and store it on the sandbox metadata when create_sandbox() makes the sandbox,
then update the reconnect flow (the code path that reuses existing sandboxes on
agent reconnect) to compare the stored sandbox config hash with the current
settings hash and force a sandbox recycle/recreate when they differ (or
alternatively throw/require explicit recycle); ensure you reference
create_sandbox(), the reconnect handler, and the Tirith env names
(TIRITH_ENABLED/TIRITH_FAIL_MODE) so maintainers can locate and apply the
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 771f7506-f836-439e-aeb4-17b606a0c1fd

📥 Commits

Reviewing files that changed from the base of the PR and between 06ad084 and 30c51f3.

📒 Files selected for processing (5)
  • core-api/.env.example
  • core-api/api/config.py
  • core-api/api/services/agents/dispatch.py
  • core-api/api/services/agents/runtime_bundle.py
  • core-api/tests/test_tirith_sandbox.py

Comment thread core-api/api/config.py
Reject TIRITH_TIMEOUT <= 0 and unknown TIRITH_FAIL_MODE values during
settings initialization rather than letting them silently degrade
scanning behavior inside the sandbox. Normalize fail_mode and trim
tirith_path on load. The embedded runtime normalization stays as a
second layer of defense for direct env var injection.
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant