tools — Agent Tool Implementations
Agent Tool Implementations
Central orchestration hub for all agent capabilities — 40+ production tools with pluggable execution backends, dynamic MCP integration, multi-layer security, and a thread-safe registry powering terminal execution, browser automation, file operations, web search, code sandboxing, delegation, memory, skills, vision, TTS/STT, and more.
🎯 Why This Module Exists
Without the tools/ module, every component in Hermes Agent — the CLI, the gateway, the batch runner — would need to independently implement command execution, file I/O, web scraping, and dozens of other capabilities. Each would maintain its own subprocess management, its own security checks, its own error handling. The result would be a fragmented, inconsistent, and deeply unsafe system where adding a single new capability means touching every integration point.
The tools module solves this by providing a single, unified registry where every agent capability is declared with its schema, handler, security gates, and resource limits. A thread-safe dispatch mechanism routes LLM tool calls to the correct handler, manages async/sync bridging, enforces per-tool and per-turn output budgets to prevent context-window overflow, and applies security policies (write deny lists, dangerous command approval, skill scanning) before any side-effecting operation executes. The pluggable environment system means the same terminal() call works identically whether the agent is running locally, inside Docker, over SSH, on Modal cloud, in a Singularity container, or through a Daytona workspace — all without the LLM or higher layers knowing the difference.
💡 Real-world analogy
Think of the tools module as a hospital switchboard. When a doctor (the LLM) needs an X-ray, a blood test, or a prescription filled, they don't walk to each department — they call the switchboard. The switchboard knows which department handles each request, checks that the doctor has authorization, routes the call to the right specialist, and delivers results back in a standard format. If the radiology department moves to a new building (a Docker container instead of localhost), the switchboard updates the routing — the doctor's workflow doesn't change. And if someone tries to order a dangerous procedure, the switchboard flags it for approval before it goes through.
🔵 40+ Tools 🟢 6 Backends 🟡 MCP Support 🔴 Security First
Key Components
| File | Purpose | Key Details |
|---|---|---|
registry.py | Thread-safe tool registry & dispatch hub | RWLock-protected _tools dict; register/deregister/dispatch; snapshot isolation for readers during mutations |
terminal_tool.py | Terminal command execution | Routes commands through environment backends; CWD tracking; timeout enforcement; interrupt-safe polling |
file_tools.py | File read/search/stat operations | Blocked device paths; large-file hints; offset+limit pagination; 100K char default cap |
file_operations.py | File write/create/patch/move | Write deny list for sensitive paths (~/.ssh, /etc/sudoers); ShellFileOperations unified API |
patch_parser.py | Unified diff & search-replace patch parser | Parses both unified diff format and search/replace blocks; applies patches atomically |
browser_tool.py | Browser automation via Playwright | Headless Chrome; screenshot capture; DOM interaction; configurable viewport |
web_tools.py | Web search & content extraction | Pluggable backends: Exa, Firecrawl, Tavily, Parallel; LLM-based intelligent extraction |
code_execution_tool.py | Sandboxed code execution | Isolated Python/script execution; configurable timeout (default 300s); max 50 tool calls within sandbox |
delegate_tool.py | Sub-agent delegation | Spawns child AIAgent instances with restricted toolsets; max 3 concurrent children |
skill_manager_tool.py | Skill CRUD operations | Create, read, update, delete skills in ~/.hermes/skills/; format validation |
skills_hub.py | Community skill registry | Adapter pattern via SkillSource ABC; GitHub, bundled, and hub sources; quarantine + audit |
mcp_tool.py | MCP server integration | Dynamic tool discovery via list_changed notifications; stdio/HTTP transport; OAuth PKCE flow |
approval.py | Dangerous command approval | Regex-based pattern matching; LLM auto-approval for low-risk; per-session allowlist |
checkpoint_manager.py | Filesystem snapshots via shadow git | Transparent git-based checkpoints for rollback; configurable timeout [10–60s] |
tool_result_storage.py | 3-layer result persistence | Per-tool threshold → per-result persistence → per-turn budget (200K chars); preview + disk spill |
Architecture
flowchart LR
subgraph Core["Core Infrastructure"]
REG["registry.py\nTool Registry"]
APPROV["approval.py\nSecurity Gate"]
STORAGE["tool_result_storage.py\nResult Persistence"]
BUDGET["budget_config.py\nResource Limits"]
end
subgraph Terminal["Terminal & Environments"]
TERM["terminal_tool.py"]
LOCAL["LocalEnvironment"]
DOCKER["DockerEnvironment"]
SSH_E["SSHEnvironment"]
MODAL["ModalEnvironment"]
DAYTONA["DaytonaEnvironment"]
SING["SingularityEnvironment"]
end
subgraph FileOps["File Operations"]
FTOOLS["file_tools.py\nRead / Search"]
FOPS["file_operations.py\nWrite / Patch"]
PATCH["patch_parser.py"]
end
subgraph Web["Web & Browser"]
BROWSER["browser_tool.py"]
WEBTOOLS["web_tools.py"]
end
subgraph Skills["Skills & Memory"]
SKILLMGR["skill_manager_tool.py"]
HUB["skills_hub.py"]
GUARD["skills_guard.py"]
MEMORY["memory_tool.py"]
end
subgraph Integration["External Integration"]
MCP["mcp_tool.py\nMCP Protocol"]
DELEGATE["delegate_tool.py\nSub-Agents"]
CODE["code_execution_tool.py\nSandbox"]
end
REG --> TERM
REG --> FTOOLS
REG --> BROWSER
REG --> SKILLMGR
REG --> MCP
REG --> DELEGATE
REG --> CODE
APPROV --> TERM
APPROV --> FOPS
TERM --> LOCAL
TERM --> DOCKER
TERM --> SSH_E
TERM --> MODAL
TERM --> DAYTONA
TERM --> SING
FTOOLS --> FOPS
FOPS --> PATCH
SKILLMGR --> HUB
HUB --> GUARD
STORAGE --> BUDGET
Data Flow
flowchart TD
LLM["LLM Tool Call\n{name, args}"]
DISPATCH["registry.dispatch()"]
CHECK["check_fn\nToolset Available?"]
APPROVE["approval.py\nDangerous?"]
HANDLER["Tool Handler\nexecute()"]
ASYNC{"Async\nHandler?"}
BRIDGE["_run_async()\nCoroutine Bridge"]
RESULT["JSON Result String"]
PERSIST{"Size >\nThreshold?"}
DISK["Persist to Disk\nReturn Preview"]
INLINE["Return Inline"]
TURNBUDGET{"Turn Budget\nExceeded?"}
SPILL["Spill Largest\nto Disk"]
FINAL["Final Response\nto LLM"]
LLM --> DISPATCH
DISPATCH --> CHECK
CHECK -->|Pass| APPROVE
CHECK -->|Fail| RESULT
APPROVE -->|Approved| HANDLER
APPROVE -->|Blocked| RESULT
HANDLER --> ASYNC
ASYNC -->|Yes| BRIDGE --> RESULT
ASYNC -->|No| RESULT
RESULT --> PERSIST
PERSIST -->|Yes| DISK --> TURNBUDGET
PERSIST -->|No| INLINE --> TURNBUDGET
TURNBUDGET -->|Yes| SPILL --> FINAL
TURNBUDGET -->|No| FINAL
🚶 Step-by-Step Narrated Walkthrough
Follow a single terminal("ls -la /workspace") command as it flows through the entire tools pipeline:
- LLM Generates Tool Call — The model emits a function call:
{"name": "terminal", "args": {"command": "ls -la /workspace"}}. The agent runtime receives this as a structured tool-use block. - Registry Dispatch —
model_tools.pycallsregistry.dispatch("terminal", args). The registry looks up theToolEntryfor "terminal", verifying the toolset'scheck_fnreturnstrue(terminal is always available). - Approval Check —
approval.pyscans the command againstDANGEROUS_PATTERNS(regex list forrm -rf /,chmod 777,sudo, etc.).ls -lamatches no patterns, so execution proceeds without prompting. - Environment Resolution —
terminal_tool.pyresolves the execution backend: check theenvparameter, thenTERMINAL_ENVenv var, thenconfig.yaml, then fall back to"local". For this walkthrough, we getLocalEnvironment. - Session Snapshot Sourcing —
BaseEnvironment._wrap_command()prepends the session snapshot (captured atinit_session()) — sourcing env vars, functions, and aliases. It thencds to the tracked CWD. - Subprocess Spawn —
LocalEnvironment._run_bash()spawnssubprocess.Popen(["bash", "-c", wrapped_cmd])with stdout/stderr piped. This is the spawn-per-call pattern — a fresh bash for every command. - Interrupt-Safe Polling —
_wait_for_process()polls every 0.5 seconds, checking both process completion (proc.poll()) and user interrupt (is_interrupted()). If the 600-second timeout hits, it sends SIGTERM → 5s grace → SIGKILL. - Output Parsing — The wrapper emits CWD markers (
__HERMES_CWD_{session_id}__) and exit code markers. The parser extracts the actual command output, the new CWD, and the return code. - CWD Persistence — The new CWD is written to
/tmp/hermes-cwd-{session_id}.txtso the next command starts in the right directory. - Result Sizing —
tool_result_storage.pychecks the output size against the per-tool threshold (terminal defaults to 100K chars). Ourls -laoutput is small, so it returns inline. - JSON Serialization — The handler returns
{"returncode": 0, "stdout": "total 42\ndrwxr-xr-x ...", "cwd": "/workspace"}as a JSON string. - Response to LLM — The serialized result is sent back to the model as a tool-result content block, completing the round-trip in typically <100ms for local commands.
🏗️ Design Decisions
| Decision | Choice Made | Alternatives Considered | Why This Approach |
|---|---|---|---|
| Command execution model | Spawn-per-call: fresh bash process per command | Persistent shell session (PTY), shared subprocess | Eliminates shell state pollution between commands. Session snapshot captures env vars/functions cheaply. Remote backends (Docker, SSH, Modal) naturally align with stateless calls. Easier to enforce timeouts and interrupts per-call. |
| Result size management | 3-layer defense: per-tool threshold, per-result persistence, per-turn aggregate budget | Single global output cap, no persistence | Per-tool allows search_files (inherently huge) to have a higher cap than read_file. Per-turn budget catches cascading medium-sized results. Preview (1.5 KB) balances readability + token efficiency. Model can always read_file() persisted content — no information loss. |
| Tool discovery mechanism | Thread-safe registry with dynamic register/deregister | Hardcoded tool list, config-file enumeration | MCP servers need to add/remove tools at runtime without agent restart. Tools scattered across 50+ modules — centralized registry avoids maintaining parallel lists. RWLock + snapshots ensure readers see stable state during concurrent mutations. |
| Skill installation security | Regex-based static analysis with trust-level-aware policy | Full sandboxed execution, no scanning | Externally-sourced code is inherently risky (data exfiltration, persistence, destruction). Regex patterns catch obvious red flags without requiring expensive sandboxed execution. Trust levels (builtin > trusted > community) reduce false positives. |
| Skill sync strategy | Manifest-based: track origin hash, only update if user hasn't customized | Overwrite on every update, git-merge based sync | User customizations must not be lost on agent update. Manifest detects modifications via hash mismatch. Respects user deletions (won't re-add deleted skills). Supports clean v1→v2 format migration. |
| Environment abstraction | Polymorphism via BaseEnvironment with _run_bash() override |
Strategy pattern, configuration-driven dispatch | All 6 backends share snapshot/CWD/timeout logic. Each subclass overrides only the backend-specific _run_bash() method. Adding a new backend requires implementing a single method — no changes to the rest of the pipeline. |
🔬 Algorithm Spotlight
Tool Approval Flow & Security Pipeline
The Problem: An autonomous AI agent that can execute arbitrary shell commands is inherently dangerous. It could rm -rf /, exfiltrate credentials via curl, or escalate privileges with sudo. The system needs to detect dangerous operations and gate them behind approval — without blocking legitimate work or creating excessive friction.
The Approach: A multi-stage security pipeline evaluates every side-effecting operation. First, regex-based pattern matching scans commands against a curated list of dangerous patterns (recursive deletion, world-writable permissions, credential access). If a match is found, the system attempts LLM-based auto-approval: a cheap model evaluates whether the command is actually low-risk in context. If auto-approval fails, the user is prompted interactively (CLI) or the command is logged and rejected (gateway). Approved commands are added to a per-session allowlist so repeated similar operations don't trigger re-approval. For file writes, a separate deny list blocks writes to sensitive paths (~/.ssh/, /etc/sudoers, ~/.hermes/.env) unconditionally.
Why This Algorithm: Pure regex matching would generate too many false positives (blocking harmless rm on temp files). Pure LLM-based analysis would be too slow and expensive for every command. The layered approach — fast regex filter → smart LLM analysis → user approval fallback — balances security, speed, and usability. Per-session allowlists prevent approval fatigue during iterative development.
graph TD
CMD["Command Input"]
REGEX{"Regex Match\nDANGEROUS_PATTERNS?"}
SAFE["Execute Immediately"]
WDENY{"Write Deny\nList Check?"}
BLOCK["Block: Protected Path"]
AUTO{"LLM Auto-Approve\nLow-risk in context?"}
ALLOW["Add to Session Allowlist"]
PROMPT{"User Approval\nCLI / Gateway?"}
EXEC["Execute Command"]
REJECT["Reject + Log"]
CMD --> WDENY
WDENY -->|"Denied Path"| BLOCK
WDENY -->|"Allowed Path"| REGEX
REGEX -->|"No Match"| SAFE
REGEX -->|"Match Found"| AUTO
AUTO -->|"Low Risk"| ALLOW --> EXEC
AUTO -->|"High Risk"| PROMPT
PROMPT -->|"Approved"| ALLOW
PROMPT -->|"Denied"| REJECT
SAFE --> EXEC
Trace through an example
Given: Command = rm -rf /tmp/hermes-build-cache/*
Step 1 (Write Deny): Path /tmp/hermes-build-cache/ is not in the deny list — proceed.
Step 2 (Regex): Pattern \brm\s+(-[^\s]*\s+)*\/ matches because rm -rf / prefix is detected. Flag as dangerous.
Step 3 (Auto-Approve): LLM evaluates context: "Deleting a build cache directory under /tmp, not system files." Verdict: low risk. Auto-approved.
Step 4 (Allowlist): Command pattern added to session allowlist. Future rm -rf /tmp/hermes-* commands skip approval entirely.
Result: Command executes. If it had been rm -rf /etc/*, the LLM would have flagged it as high-risk, prompting the user. A naive regex-only system would have blocked the harmless cache cleanup.
⚠️ What Could Go Wrong
| Failure Mode | Trigger | Detection | Recovery / Mitigation |
|---|---|---|---|
| Context window overflow | Tool returns massive output (e.g., find / or cat on a large file) |
Per-tool threshold check + per-turn aggregate budget monitor | 3-layer persistence: large outputs spill to disk with a 1.5 KB preview. Model can read_file() the persisted content. Turn budget catches cascading medium results. |
| Command hangs indefinitely | Process reads from stdin, waits on network, or enters infinite loop | _wait_for_process() polls every 0.5s with hard timeout (default 600s) |
SIGTERM → 5-second grace period → SIGKILL. Blocked device paths (/dev/stdin, /dev/tty) are pre-emptively denied. |
| MCP server connection failure | MCP server process crashes, network timeout, or misconfigured transport | Exponential backoff with up to 5 retries; connection error exceptions | Graceful degradation: MCP toolset becomes unavailable but all built-in tools continue working. Error returned to model so it can adapt. |
| Credential exfiltration attempt | Agent tries to curl sensitive files or write to ~/.ssh/authorized_keys |
Write deny list checks path prefixes (~/.ssh/, ~/.aws/, ~/.docker/); dangerous command regex |
Operation blocked before execution with clear error message. Approval system catches curl-based exfiltration patterns. |
| OAuth token expiration | Managed gateway or MCP OAuth token expires mid-session | read_nous_access_token() checks expiry with 120s skew |
Automatic refresh via hermes_cli.auth; fall back to cached token; if both fail, OAuth-dependent tools degrade gracefully. |
| Sandbox subprocess timeout | Code execution tool runs a script that exceeds the 300s timeout | Timeout enforcement in code_execution_tool.py + environment-level _wait_for_process |
Subprocess killed; error result returned to model with timeout message. Max tool-call limit (50) prevents runaway tool loops within sandbox. |
⚡ Performance Profile
Hot Paths
The most performance-sensitive code paths in this module:
- Registry Dispatch: O(1) dict lookup per tool call — the critical path for every agent action. RWLock ensures readers never block each other; writes (MCP tool registration) acquire exclusive lock briefly.
- Session Snapshot Sourcing: Captured once at
init_session(), sourced before each command via_wrap_command(). Avoidsbash -loverhead (~200ms per call). Snapshot is typically <5 KB of env vars + function definitions. - Result Persistence Check: O(1) size comparison per tool result. The 3-layer budget enforcement sorts results by size (O(n log n) for n results per turn) but n is typically <10.
- Approval Regex Scan: ~15 regex patterns evaluated per command — typically completes in <1ms. Compiled regex objects are cached at module import.
Resource Usage
- Memory: Registry holds ~40
ToolEntryobjects in-memory (~50 KB). MCP tools are added dynamically but follow the same lightweight pattern. Environment objects are per-session singletons. Lazy imports (TTS, voice, browser) avoid loading heavy libraries until first use. - I/O: Local commands spawn subprocesses (fork cost ~1-5ms). Docker/SSH/Modal commands add network round-trip (~10-100ms). Result persistence writes to
/tmp/hermes-results/only when thresholds are exceeded. Checkpoint manager does git operations with configurable timeout [10-60s]. - Concurrency: Registry supports concurrent reads via RWLock snapshots. Delegate tool caps concurrent sub-agents at 3 (configurable). Gateway sessions maintain isolated approval state via
session_contextcontext vars.
Sub-Modules
Terminal & Environments
terminal_tool.py
Primary command execution interface for the agent. Routes commands through the configured environment backend.
- CWD tracking via markers
- Timeout enforcement (default 600s)
- Disk usage warnings
- Interrupt-safe polling
environments/
Six pluggable execution backends behind the BaseEnvironment abstraction.
local.py— subprocess.Popendocker.py— docker execssh.py— remote SSHmodal.py— Modal clouddaytona.py— HTTP APIsingularity.py— HPC containers
File Operations
file_tools.py
Read-side file operations: read_file, search_files, list_directory, file_stat.
- Blocked device path protection
- Large file hints (>512 KB)
- Offset + limit pagination
file_operations.py
Write-side operations: create, write, move, delete files via ShellFileOperations.
- Write deny list for sensitive paths
- Prefix-walk directory escape detection
- Unified API across all backends
patch_parser.py
Parses unified diffs and search-replace blocks for atomic file patching.
- Supports both diff formats
- Atomic apply-or-fail semantics
Browser & Web
browser_tool.py
Headless browser automation via Playwright for web interaction.
- Screenshot capture
- DOM interaction & element queries
- Configurable viewport
web_tools.py
Web search and content extraction with pluggable backends.
- Exa, Firecrawl, Tavily, Parallel
- LLM-based intelligent extraction
- Configurable via
config.yaml
Skills & Memory
skill_manager_tool.py
CRUD operations for agent skills stored in ~/.hermes/skills/.
- Create / read / update / delete
- Format validation
skills_hub.py & skills_tool.py
Community skill registry with adapter pattern (SkillSource ABC).
- GitHub, bundled, and hub sources
- Quarantine + audit logging
- Manifest-based sync
skills_guard.py
Trust-level-aware security scanning for skill installation.
- Regex-based static analysis
- Trust levels: builtin > trusted > community
- Install policy matrix
memory_tool.py
Persistent memory via Markdown files injected into system prompt.
- Stored in
HERMES_HOME/memories/ - Auto-loaded at session start
Code Execution & Delegation
code_execution_tool.py
Sandboxed script execution with resource limits.
- Configurable timeout (default 300s)
- Max 50 tool calls within sandbox
- RPC back to parent for tool dispatch
delegate_tool.py
Spawns child AIAgent instances with restricted toolsets.
- Max 3 concurrent children
- Isolated context per sub-agent
- Task-specific toolset restriction
Communication & Media
send_message_tool.py
Agent-to-user messaging for status updates and notifications.
- Structured message delivery
tts_tool.py & voice_mode.py
Text-to-speech and voice interaction with 6 provider backends.
- Edge, ElevenLabs, OpenAI, Mistral, MiniMax, NeuTTS
- Lazy imports for headless compatibility
vision_tools.py & image_generation_tool.py
Vision analysis and image generation capabilities.
- Vision: auxiliary model routing
- Image gen: FAL.ai (FLUX + Clarity Upscaler)
- 30s download timeout
Security & Infrastructure
approval.py
Multi-stage dangerous command detection and approval pipeline.
- Regex pattern matching
- LLM auto-approve for low-risk
- Per-session allowlist
path_security.py & url_safety.py
Path traversal prevention and URL safety validation.
- Canonical path resolution
- URL scheme validation
checkpoint_manager.py
Transparent filesystem snapshots via shadow git repositories.
- Configurable timeout [10–60s]
- Rollback support
registry.py & process_registry.py
Thread-safe tool registry and background process tracking.
- RWLock for concurrent reads
- Snapshot isolation during mutations
- Dynamic MCP tool registration
Directory Map
Source tree for tools
tools/
├── environments/ ← Pluggable execution backends
│ ├── base.py ← BaseEnvironment ABC (snapshot, CWD, timeout)
│ ├── local.py ← Local subprocess backend
│ ├── docker.py ← Docker container backend
│ ├── ssh.py ← Remote SSH backend
│ ├── modal.py ← Modal cloud backend
│ ├── daytona.py ← Daytona HTTP API backend
│ └── singularity.py ← Singularity/HPC container backend
├── browser_providers/ ← Browser automation backends
├── registry.py ← Thread-safe tool registry & dispatch
├── process_registry.py ← Background process tracking
├── terminal_tool.py ← Terminal command execution
├── file_tools.py ← File read / search / stat
├── file_operations.py ← File write / create / patch / move
├── patch_parser.py ← Unified diff & search-replace parser
├── browser_tool.py ← Headless browser automation
├── web_tools.py ← Web search & content extraction
├── code_execution_tool.py ← Sandboxed script execution
├── delegate_tool.py ← Sub-agent delegation
├── skill_manager_tool.py ← Skill CRUD operations
├── skills_tool.py ← Skill discovery & execution
├── skills_hub.py ← Community skill registry (SkillSource)
├── skills_guard.py ← Trust-aware security scanning
├── skills_sync.py ← Manifest-based skill sync
├── mcp_tool.py ← MCP server integration
├── mcp_oauth.py ← OAuth PKCE for MCP
├── memory_tool.py ← Persistent memory files
├── send_message_tool.py ← Agent-to-user messaging
├── tts_tool.py ← Text-to-speech (6 providers)
├── voice_mode.py ← Voice interaction mode
├── vision_tools.py ← Vision model routing
├── image_generation_tool.py ← Image generation (FAL.ai)
├── transcription_tools.py ← Audio transcription
├── approval.py ← Dangerous command approval
├── path_security.py ← Path traversal prevention
├── tirith_security.py ← Extended security policies
├── url_safety.py ← URL scheme validation
├── tool_result_storage.py ← 3-layer result persistence
├── budget_config.py ← Per-tool/turn resource limits
├── checkpoint_manager.py ← Shadow git filesystem snapshots
├── managed_tool_gateway.py ← Nous hosted tool gateway client
├── todo_tool.py ← Todo/task tracking
└── auxiliary_client.py ← Cheap LLM calls for tool support
⚙️ Configuration Reference
| Parameter | Source | Default | Purpose |
|---|---|---|---|
TERMINAL_ENV | env var | local | Backend selection: local, docker, modal, ssh, singularity, daytona |
TERMINAL_MAX_FOREGROUND_TIMEOUT | env var | 600 | Hard cap on foreground command timeout (seconds) |
TERMINAL_DISK_WARNING_GB | env var | 500 | Disk usage warning threshold in GB |
file_read_max_chars | config.yaml | 100000 | Max characters per read_file call (~25-35K tokens) |
web.backend | config.yaml | — | Web backend: exa, firecrawl, parallel, tavily |
mcp_servers | config.yaml | {} | Dict of MCP server configs (stdio/HTTP, env vars, timeouts, OAuth) |
code_execution.timeout | config.yaml | 300 | Sandbox script timeout in seconds |
delegation.max_concurrent_children | config.yaml | 3 | Max parallel sub-agents |
tts.provider | config.yaml | — | TTS backend: edge, elevenlabs, openai, mistral, minimax, neutts |
HERMES_CHECKPOINT_TIMEOUT | env var | 30 | Git subprocess timeout for snapshots (clamped to [10, 60]) |