feat: add documentation website (Docusaurus)
- 25 documentation pages covering Getting Started, User Guide, Developer Guide, and Reference - Docusaurus with custom amber/gold theme matching the landing page branding - GitHub Actions workflow to deploy landing page + docs to GitHub Pages - Landing page at root, docs at /docs/ on hermes-agent.nousresearch.com - Content extracted and restructured from existing repo docs (README, AGENTS.md, CONTRIBUTING.md, docs/) - Auto-deploy on push to main when website/ or landingpage/ changes
This commit is contained in:
8
website/docs/developer-guide/_category_.json
Normal file
8
website/docs/developer-guide/_category_.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Developer Guide",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Contribute to Hermes Agent — architecture, tools, skills, and more."
|
||||
}
|
||||
}
|
||||
208
website/docs/developer-guide/adding-tools.md
Normal file
208
website/docs/developer-guide/adding-tools.md
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: "Adding Tools"
|
||||
description: "How to add a new tool to Hermes Agent — schemas, handlers, registration, and toolsets"
|
||||
---
|
||||
|
||||
# Adding Tools
|
||||
|
||||
Before writing a tool, ask yourself: **should this be a [skill](creating-skills.md) instead?**
|
||||
|
||||
Make it a **Skill** when the capability can be expressed as instructions + shell commands + existing tools (arXiv search, git workflows, Docker management, PDF processing).
|
||||
|
||||
Make it a **Tool** when it requires end-to-end integration with API keys, custom processing logic, binary data handling, or streaming (browser automation, TTS, vision analysis).
|
||||
|
||||
## Overview
|
||||
|
||||
Adding a tool touches **3 files**:
|
||||
|
||||
1. **`tools/your_tool.py`** — handler, schema, check function, `registry.register()` call
|
||||
2. **`toolsets.py`** — add tool name to `_HERMES_CORE_TOOLS` (or a specific toolset)
|
||||
3. **`model_tools.py`** — add `"tools.your_tool"` to the `_discover_tools()` list
|
||||
|
||||
## Step 1: Create the Tool File
|
||||
|
||||
Every tool file follows the same structure:
|
||||
|
||||
```python
|
||||
# tools/weather_tool.py
|
||||
"""Weather Tool -- look up current weather for a location."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- Availability check ---
|
||||
|
||||
def check_weather_requirements() -> bool:
|
||||
"""Return True if the tool's dependencies are available."""
|
||||
return bool(os.getenv("WEATHER_API_KEY"))
|
||||
|
||||
|
||||
# --- Handler ---
|
||||
|
||||
def weather_tool(location: str, units: str = "metric") -> str:
|
||||
"""Fetch weather for a location. Returns JSON string."""
|
||||
api_key = os.getenv("WEATHER_API_KEY")
|
||||
if not api_key:
|
||||
return json.dumps({"error": "WEATHER_API_KEY not configured"})
|
||||
try:
|
||||
# ... call weather API ...
|
||||
return json.dumps({"location": location, "temp": 22, "units": units})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
# --- Schema ---
|
||||
|
||||
WEATHER_SCHEMA = {
|
||||
"name": "weather",
|
||||
"description": "Get current weather for a location.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "City name or coordinates (e.g. 'London' or '51.5,-0.1')"
|
||||
},
|
||||
"units": {
|
||||
"type": "string",
|
||||
"enum": ["metric", "imperial"],
|
||||
"description": "Temperature units (default: metric)",
|
||||
"default": "metric"
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# --- Registration ---
|
||||
|
||||
from tools.registry import registry
|
||||
|
||||
registry.register(
|
||||
name="weather",
|
||||
toolset="weather",
|
||||
schema=WEATHER_SCHEMA,
|
||||
handler=lambda args, **kw: weather_tool(
|
||||
location=args.get("location", ""),
|
||||
units=args.get("units", "metric")),
|
||||
check_fn=check_weather_requirements,
|
||||
requires_env=["WEATHER_API_KEY"],
|
||||
)
|
||||
```
|
||||
|
||||
### Key Rules
|
||||
|
||||
:::danger Important
|
||||
- Handlers **MUST** return a JSON string (via `json.dumps()`), never raw dicts
|
||||
- Errors **MUST** be returned as `{"error": "message"}`, never raised as exceptions
|
||||
- The `check_fn` is called when building tool definitions — if it returns `False`, the tool is silently excluded
|
||||
- The `handler` receives `(args: dict, **kwargs)` where `args` is the LLM's tool call arguments
|
||||
:::
|
||||
|
||||
## Step 2: Add to a Toolset
|
||||
|
||||
In `toolsets.py`, add the tool name:
|
||||
|
||||
```python
|
||||
# If it should be available on all platforms (CLI + messaging):
|
||||
_HERMES_CORE_TOOLS = [
|
||||
...
|
||||
"weather", # <-- add here
|
||||
]
|
||||
|
||||
# Or create a new standalone toolset:
|
||||
"weather": {
|
||||
"description": "Weather lookup tools",
|
||||
"tools": ["weather"],
|
||||
"includes": []
|
||||
},
|
||||
```
|
||||
|
||||
## Step 3: Add Discovery Import
|
||||
|
||||
In `model_tools.py`, add the module to the `_discover_tools()` list:
|
||||
|
||||
```python
|
||||
def _discover_tools():
|
||||
_modules = [
|
||||
...
|
||||
"tools.weather_tool", # <-- add here
|
||||
]
|
||||
```
|
||||
|
||||
This import triggers the `registry.register()` call at the bottom of your tool file.
|
||||
|
||||
## Async Handlers
|
||||
|
||||
If your handler needs async code, mark it with `is_async=True`:
|
||||
|
||||
```python
|
||||
async def weather_tool_async(location: str) -> str:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
...
|
||||
return json.dumps(result)
|
||||
|
||||
registry.register(
|
||||
name="weather",
|
||||
toolset="weather",
|
||||
schema=WEATHER_SCHEMA,
|
||||
handler=lambda args, **kw: weather_tool_async(args.get("location", "")),
|
||||
check_fn=check_weather_requirements,
|
||||
is_async=True, # registry calls _run_async() automatically
|
||||
)
|
||||
```
|
||||
|
||||
The registry handles async bridging transparently — you never call `asyncio.run()` yourself.
|
||||
|
||||
## Handlers That Need task_id
|
||||
|
||||
Tools that manage per-session state receive `task_id` via `**kwargs`:
|
||||
|
||||
```python
|
||||
def _handle_weather(args, **kw):
|
||||
task_id = kw.get("task_id")
|
||||
return weather_tool(args.get("location", ""), task_id=task_id)
|
||||
|
||||
registry.register(
|
||||
name="weather",
|
||||
...
|
||||
handler=_handle_weather,
|
||||
)
|
||||
```
|
||||
|
||||
## Agent-Loop Intercepted Tools
|
||||
|
||||
Some tools (`todo`, `memory`, `session_search`, `delegate_task`) need access to per-session agent state. These are intercepted by `run_agent.py` before reaching the registry. The registry still holds their schemas, but `dispatch()` returns a fallback error if the intercept is bypassed.
|
||||
|
||||
## Optional: Setup Wizard Integration
|
||||
|
||||
If your tool requires an API key, add it to `hermes_cli/config.py`:
|
||||
|
||||
```python
|
||||
OPTIONAL_ENV_VARS = {
|
||||
...
|
||||
"WEATHER_API_KEY": {
|
||||
"description": "Weather API key for weather lookup",
|
||||
"prompt": "Weather API key",
|
||||
"url": "https://weatherapi.com/",
|
||||
"tools": ["weather"],
|
||||
"password": True,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tool file created with handler, schema, check function, and registration
|
||||
- [ ] Added to appropriate toolset in `toolsets.py`
|
||||
- [ ] Discovery import added to `model_tools.py`
|
||||
- [ ] Handler returns JSON strings, errors returned as `{"error": "..."}`
|
||||
- [ ] Optional: API key added to `OPTIONAL_ENV_VARS` in `hermes_cli/config.py`
|
||||
- [ ] Optional: Added to `toolset_distributions.py` for batch processing
|
||||
- [ ] Tested with `hermes chat -q "Use the weather tool for London"`
|
||||
215
website/docs/developer-guide/architecture.md
Normal file
215
website/docs/developer-guide/architecture.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: "Architecture"
|
||||
description: "Hermes Agent internals — project structure, agent loop, key classes, and design patterns"
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
This guide covers the internal architecture of Hermes Agent for developers contributing to the project.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
hermes-agent/
|
||||
├── run_agent.py # AIAgent class — core conversation loop, tool dispatch
|
||||
├── cli.py # HermesCLI class — interactive TUI, prompt_toolkit
|
||||
├── model_tools.py # Tool orchestration (thin layer over tools/registry.py)
|
||||
├── toolsets.py # Tool groupings and presets
|
||||
├── hermes_state.py # SQLite session database with FTS5 full-text search
|
||||
├── batch_runner.py # Parallel batch processing for trajectory generation
|
||||
│
|
||||
├── agent/ # Agent internals (extracted modules)
|
||||
│ ├── prompt_builder.py # System prompt assembly (identity, skills, memory)
|
||||
│ ├── context_compressor.py # Auto-summarization when approaching context limits
|
||||
│ ├── auxiliary_client.py # Resolves auxiliary OpenAI clients (summarization, vision)
|
||||
│ ├── display.py # KawaiiSpinner, tool progress formatting
|
||||
│ ├── model_metadata.py # Model context lengths, token estimation
|
||||
│ └── trajectory.py # Trajectory saving helpers
|
||||
│
|
||||
├── hermes_cli/ # CLI command implementations
|
||||
│ ├── main.py # Entry point, argument parsing, command dispatch
|
||||
│ ├── config.py # Config management, migration, env var definitions
|
||||
│ ├── setup.py # Interactive setup wizard
|
||||
│ ├── auth.py # Provider resolution, OAuth, Nous Portal
|
||||
│ ├── models.py # OpenRouter model selection lists
|
||||
│ ├── banner.py # Welcome banner, ASCII art
|
||||
│ ├── commands.py # Slash command definitions + autocomplete
|
||||
│ ├── callbacks.py # Interactive callbacks (clarify, sudo, approval)
|
||||
│ ├── doctor.py # Diagnostics
|
||||
│ └── skills_hub.py # Skills Hub CLI + /skills slash command handler
|
||||
│
|
||||
├── tools/ # Tool implementations (self-registering)
|
||||
│ ├── registry.py # Central tool registry (schemas, handlers, dispatch)
|
||||
│ ├── approval.py # Dangerous command detection + per-session approval
|
||||
│ ├── terminal_tool.py # Terminal orchestration (sudo, env lifecycle, backends)
|
||||
│ ├── file_operations.py # read_file, write_file, search, patch
|
||||
│ ├── web_tools.py # web_search, web_extract
|
||||
│ ├── vision_tools.py # Image analysis via multimodal models
|
||||
│ ├── delegate_tool.py # Subagent spawning and parallel task execution
|
||||
│ ├── code_execution_tool.py # Sandboxed Python with RPC tool access
|
||||
│ ├── session_search_tool.py # Search past conversations
|
||||
│ ├── cronjob_tools.py # Scheduled task management
|
||||
│ ├── skill_tools.py # Skill search, load, manage
|
||||
│ └── environments/ # Terminal execution backends
|
||||
│ ├── base.py # BaseEnvironment ABC
|
||||
│ ├── local.py, docker.py, ssh.py, singularity.py, modal.py
|
||||
│
|
||||
├── gateway/ # Messaging gateway
|
||||
│ ├── run.py # GatewayRunner — platform lifecycle, message routing
|
||||
│ ├── config.py # Platform configuration resolution
|
||||
│ ├── session.py # Session store, context prompts, reset policies
|
||||
│ └── platforms/ # Platform adapters
|
||||
│ ├── telegram.py, discord_adapter.py, slack.py, whatsapp.py
|
||||
│
|
||||
├── scripts/ # Installer and bridge scripts
|
||||
│ ├── install.sh # Linux/macOS installer
|
||||
│ ├── install.ps1 # Windows PowerShell installer
|
||||
│ └── whatsapp-bridge/ # Node.js WhatsApp bridge (Baileys)
|
||||
│
|
||||
├── skills/ # Bundled skills (copied to ~/.hermes/skills/)
|
||||
├── environments/ # RL training environments (Atropos integration)
|
||||
└── tests/ # Test suite
|
||||
```
|
||||
|
||||
## Core Loop
|
||||
|
||||
The main agent loop lives in `run_agent.py`:
|
||||
|
||||
```
|
||||
User message → AIAgent._run_agent_loop()
|
||||
├── Build system prompt (prompt_builder.py)
|
||||
├── Build API kwargs (model, messages, tools, reasoning config)
|
||||
├── Call LLM (OpenAI-compatible API)
|
||||
├── If tool_calls in response:
|
||||
│ ├── Execute each tool via registry dispatch
|
||||
│ ├── Add tool results to conversation
|
||||
│ └── Loop back to LLM call
|
||||
├── If text response:
|
||||
│ ├── Persist session to DB
|
||||
│ └── Return final_response
|
||||
└── Context compression if approaching token limit
|
||||
```
|
||||
|
||||
```python
|
||||
while turns < max_turns:
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=tool_schemas,
|
||||
)
|
||||
|
||||
if response.tool_calls:
|
||||
for tool_call in response.tool_calls:
|
||||
result = await execute_tool(tool_call)
|
||||
messages.append(tool_result_message(result))
|
||||
turns += 1
|
||||
else:
|
||||
return response.content
|
||||
```
|
||||
|
||||
## AIAgent Class
|
||||
|
||||
```python
|
||||
class AIAgent:
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "anthropic/claude-sonnet-4",
|
||||
api_key: str = None,
|
||||
base_url: str = "https://openrouter.ai/api/v1",
|
||||
max_iterations: int = 60,
|
||||
enabled_toolsets: list = None,
|
||||
disabled_toolsets: list = None,
|
||||
verbose_logging: bool = False,
|
||||
quiet_mode: bool = False,
|
||||
tool_progress_callback: callable = None,
|
||||
):
|
||||
...
|
||||
|
||||
def chat(self, user_message: str, task_id: str = None) -> str:
|
||||
# Main entry point - runs the agent loop
|
||||
...
|
||||
```
|
||||
|
||||
## File Dependency Chain
|
||||
|
||||
```
|
||||
tools/registry.py (no deps — imported by all tool files)
|
||||
↑
|
||||
tools/*.py (each calls registry.register() at import time)
|
||||
↑
|
||||
model_tools.py (imports tools/registry + triggers tool discovery)
|
||||
↑
|
||||
run_agent.py, cli.py, batch_runner.py, environments/
|
||||
```
|
||||
|
||||
Each tool file co-locates its schema, handler, and registration. `model_tools.py` is a thin orchestration layer.
|
||||
|
||||
## Key Design Patterns
|
||||
|
||||
### Self-Registering Tools
|
||||
|
||||
Each tool file calls `registry.register()` at import time. `model_tools.py` triggers discovery by importing all tool modules.
|
||||
|
||||
### Toolset Grouping
|
||||
|
||||
Tools are grouped into toolsets (`web`, `terminal`, `file`, `browser`, etc.) that can be enabled/disabled per platform.
|
||||
|
||||
### Session Persistence
|
||||
|
||||
All conversations are stored in SQLite (`hermes_state.py`) with full-text search. JSON logs go to `~/.hermes/sessions/`.
|
||||
|
||||
### Ephemeral Injection
|
||||
|
||||
System prompts and prefill messages are injected at API call time, never persisted to the database or logs.
|
||||
|
||||
### Provider Abstraction
|
||||
|
||||
The agent works with any OpenAI-compatible API. Provider resolution happens at init time (Nous Portal OAuth, OpenRouter API key, or custom endpoint).
|
||||
|
||||
### Conversation Format
|
||||
|
||||
Messages follow the OpenAI format:
|
||||
|
||||
```python
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant..."},
|
||||
{"role": "user", "content": "Search for Python tutorials"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [...]},
|
||||
{"role": "tool", "tool_call_id": "...", "content": "..."},
|
||||
{"role": "assistant", "content": "Here's what I found..."},
|
||||
]
|
||||
```
|
||||
|
||||
## CLI Architecture
|
||||
|
||||
The interactive CLI (`cli.py`) uses:
|
||||
|
||||
- **Rich** — Welcome banner and styled panels
|
||||
- **prompt_toolkit** — Fixed input area with history, `patch_stdout`, slash command autocomplete
|
||||
- **KawaiiSpinner** — Animated kawaii faces during API calls; clean activity feed for tool results
|
||||
|
||||
Key UX behaviors:
|
||||
|
||||
- Thinking spinner shows animated kawaii face + verb (`(⌐■_■) deliberating...`)
|
||||
- Tool execution results appear as `┊ {emoji} {verb} {detail} {duration}`
|
||||
- Prompt shows `⚕ ❯` when working, `❯` when idle
|
||||
- Pasting 5+ lines auto-saves to `~/.hermes/pastes/` and collapses
|
||||
|
||||
## Messaging Gateway Architecture
|
||||
|
||||
The gateway (`gateway/run.py`) uses `GatewayRunner` to:
|
||||
|
||||
1. Connect to all configured platforms
|
||||
2. Route messages through per-chat session stores
|
||||
3. Dispatch to AIAgent instances
|
||||
4. Run the cron scheduler (ticks every 60s)
|
||||
5. Handle interrupts and tool progress notifications
|
||||
|
||||
Each platform adapter conforms to `BasePlatformAdapter`.
|
||||
|
||||
## Configuration System
|
||||
|
||||
- `~/.hermes/config.yaml` — All settings
|
||||
- `~/.hermes/.env` — API keys and secrets
|
||||
- `_config_version` in `DEFAULT_CONFIG` — Bumped when required fields are added, triggers migration prompts
|
||||
231
website/docs/developer-guide/contributing.md
Normal file
231
website/docs/developer-guide/contributing.md
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: "Contributing"
|
||||
description: "How to contribute to Hermes Agent — dev setup, code style, PR process"
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
Thank you for contributing to Hermes Agent! This guide covers setting up your dev environment, understanding the codebase, and getting your PR merged.
|
||||
|
||||
## Contribution Priorities
|
||||
|
||||
We value contributions in this order:
|
||||
|
||||
1. **Bug fixes** — crashes, incorrect behavior, data loss
|
||||
2. **Cross-platform compatibility** — Windows, macOS, different Linux distros
|
||||
3. **Security hardening** — shell injection, prompt injection, path traversal
|
||||
4. **Performance and robustness** — retry logic, error handling, graceful degradation
|
||||
5. **New skills** — broadly useful ones (see [Creating Skills](creating-skills.md))
|
||||
6. **New tools** — rarely needed; most capabilities should be skills
|
||||
7. **Documentation** — fixes, clarifications, new examples
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
| Requirement | Notes |
|
||||
|-------------|-------|
|
||||
| **Git** | With `--recurse-submodules` support |
|
||||
| **Python 3.11+** | uv will install it if missing |
|
||||
| **uv** | Fast Python package manager ([install](https://docs.astral.sh/uv/)) |
|
||||
| **Node.js 18+** | Optional — needed for browser tools and WhatsApp bridge |
|
||||
|
||||
### Clone and Install
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/NousResearch/hermes-agent.git
|
||||
cd hermes-agent
|
||||
|
||||
# Create venv with Python 3.11
|
||||
uv venv venv --python 3.11
|
||||
export VIRTUAL_ENV="$(pwd)/venv"
|
||||
|
||||
# Install with all extras (messaging, cron, CLI menus, dev tools)
|
||||
uv pip install -e ".[all,dev]"
|
||||
uv pip install -e "./mini-swe-agent"
|
||||
uv pip install -e "./tinker-atropos"
|
||||
|
||||
# Optional: browser tools
|
||||
npm install
|
||||
```
|
||||
|
||||
### Configure for Development
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.hermes/{cron,sessions,logs,memories,skills}
|
||||
cp cli-config.yaml.example ~/.hermes/config.yaml
|
||||
touch ~/.hermes/.env
|
||||
|
||||
# Add at minimum an LLM provider key:
|
||||
echo 'OPENROUTER_API_KEY=sk-or-v1-your-key' >> ~/.hermes/.env
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
# Symlink for global access
|
||||
mkdir -p ~/.local/bin
|
||||
ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes
|
||||
|
||||
# Verify
|
||||
hermes doctor
|
||||
hermes chat -q "Hello"
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
- **PEP 8** with practical exceptions (no strict line length enforcement)
|
||||
- **Comments**: Only when explaining non-obvious intent, trade-offs, or API quirks
|
||||
- **Error handling**: Catch specific exceptions. Use `logger.warning()`/`logger.error()` with `exc_info=True` for unexpected errors
|
||||
- **Cross-platform**: Never assume Unix (see below)
|
||||
|
||||
## Cross-Platform Compatibility
|
||||
|
||||
Hermes runs on Linux, macOS, and Windows. Critical rules:
|
||||
|
||||
### 1. `termios` and `fcntl` are Unix-only
|
||||
|
||||
Always catch both `ImportError` and `NotImplementedError`:
|
||||
|
||||
```python
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
menu = TerminalMenu(options)
|
||||
idx = menu.show()
|
||||
except (ImportError, NotImplementedError):
|
||||
# Fallback: numbered menu for Windows
|
||||
for i, opt in enumerate(options):
|
||||
print(f" {i+1}. {opt}")
|
||||
idx = int(input("Choice: ")) - 1
|
||||
```
|
||||
|
||||
### 2. File encoding
|
||||
|
||||
Windows may save `.env` files in `cp1252`:
|
||||
|
||||
```python
|
||||
try:
|
||||
load_dotenv(env_path)
|
||||
except UnicodeDecodeError:
|
||||
load_dotenv(env_path, encoding="latin-1")
|
||||
```
|
||||
|
||||
### 3. Process management
|
||||
|
||||
`os.setsid()`, `os.killpg()`, and signal handling differ on Windows:
|
||||
|
||||
```python
|
||||
import platform
|
||||
if platform.system() != "Windows":
|
||||
kwargs["preexec_fn"] = os.setsid
|
||||
```
|
||||
|
||||
### 4. Path separators
|
||||
|
||||
Use `pathlib.Path` instead of string concatenation with `/`.
|
||||
|
||||
### 5. Shell commands in installers
|
||||
|
||||
If you change `scripts/install.sh`, check if the equivalent change is needed in `scripts/install.ps1`.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Hermes has terminal access. Security matters.
|
||||
|
||||
### Existing Protections
|
||||
|
||||
| Layer | Implementation |
|
||||
|-------|---------------|
|
||||
| **Sudo password piping** | Uses `shlex.quote()` to prevent shell injection |
|
||||
| **Dangerous command detection** | Regex patterns in `tools/approval.py` with user approval flow |
|
||||
| **Cron prompt injection** | Scanner blocks instruction-override patterns |
|
||||
| **Write deny list** | Protected paths resolved via `os.path.realpath()` to prevent symlink bypass |
|
||||
| **Skills guard** | Security scanner for hub-installed skills |
|
||||
| **Code execution sandbox** | Child process runs with API keys stripped |
|
||||
| **Container hardening** | Docker: all capabilities dropped, no privilege escalation, PID limits |
|
||||
|
||||
### Contributing Security-Sensitive Code
|
||||
|
||||
- Always use `shlex.quote()` when interpolating user input into shell commands
|
||||
- Resolve symlinks with `os.path.realpath()` before access control checks
|
||||
- Don't log secrets
|
||||
- Catch broad exceptions around tool execution
|
||||
- Test on all platforms if your change touches file paths or processes
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Branch Naming
|
||||
|
||||
```
|
||||
fix/description # Bug fixes
|
||||
feat/description # New features
|
||||
docs/description # Documentation
|
||||
test/description # Tests
|
||||
refactor/description # Code restructuring
|
||||
```
|
||||
|
||||
### Before Submitting
|
||||
|
||||
1. **Run tests**: `pytest tests/ -v`
|
||||
2. **Test manually**: Run `hermes` and exercise the code path you changed
|
||||
3. **Check cross-platform impact**: Consider Windows and macOS
|
||||
4. **Keep PRs focused**: One logical change per PR
|
||||
|
||||
### PR Description
|
||||
|
||||
Include:
|
||||
- **What** changed and **why**
|
||||
- **How to test** it
|
||||
- **What platforms** you tested on
|
||||
- Reference any related issues
|
||||
|
||||
### Commit Messages
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
| Type | Use for |
|
||||
|------|---------|
|
||||
| `fix` | Bug fixes |
|
||||
| `feat` | New features |
|
||||
| `docs` | Documentation |
|
||||
| `test` | Tests |
|
||||
| `refactor` | Code restructuring |
|
||||
| `chore` | Build, CI, dependency updates |
|
||||
|
||||
Scopes: `cli`, `gateway`, `tools`, `skills`, `agent`, `install`, `whatsapp`, `security`
|
||||
|
||||
Examples:
|
||||
```
|
||||
fix(cli): prevent crash in save_config_value when model is a string
|
||||
feat(gateway): add WhatsApp multi-user session isolation
|
||||
fix(security): prevent shell injection in sudo password piping
|
||||
```
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- Use [GitHub Issues](https://github.com/NousResearch/hermes-agent/issues)
|
||||
- Include: OS, Python version, Hermes version (`hermes version`), full error traceback
|
||||
- Include steps to reproduce
|
||||
- Check existing issues before creating duplicates
|
||||
- For security vulnerabilities, please report privately
|
||||
|
||||
## Community
|
||||
|
||||
- **Discord**: [discord.gg/NousResearch](https://discord.gg/NousResearch)
|
||||
- **GitHub Discussions**: For design proposals and architecture discussions
|
||||
- **Skills Hub**: Upload specialized skills and share with the community
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the [MIT License](https://github.com/NousResearch/hermes-agent/blob/main/LICENSE).
|
||||
140
website/docs/developer-guide/creating-skills.md
Normal file
140
website/docs/developer-guide/creating-skills.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: "Creating Skills"
|
||||
description: "How to create skills for Hermes Agent — SKILL.md format, guidelines, and publishing"
|
||||
---
|
||||
|
||||
# Creating Skills
|
||||
|
||||
Skills are the preferred way to add new capabilities to Hermes Agent. They're easier to create than tools, require no code changes to the agent, and can be shared with the community.
|
||||
|
||||
## Should it be a Skill or a Tool?
|
||||
|
||||
Make it a **Skill** when:
|
||||
- The capability can be expressed as instructions + shell commands + existing tools
|
||||
- It wraps an external CLI or API that the agent can call via `terminal` or `web_extract`
|
||||
- It doesn't need custom Python integration or API key management baked into the agent
|
||||
- Examples: arXiv search, git workflows, Docker management, PDF processing, email via CLI tools
|
||||
|
||||
Make it a **Tool** when:
|
||||
- It requires end-to-end integration with API keys, auth flows, or multi-component configuration
|
||||
- It needs custom processing logic that must execute precisely every time
|
||||
- It handles binary data, streaming, or real-time events
|
||||
- Examples: browser automation, TTS, vision analysis
|
||||
|
||||
## Skill Directory Structure
|
||||
|
||||
Bundled skills live in `skills/` organized by category:
|
||||
|
||||
```
|
||||
skills/
|
||||
├── research/
|
||||
│ └── arxiv/
|
||||
│ ├── SKILL.md # Required: main instructions
|
||||
│ └── scripts/ # Optional: helper scripts
|
||||
│ └── search_arxiv.py
|
||||
├── productivity/
|
||||
│ └── ocr-and-documents/
|
||||
│ ├── SKILL.md
|
||||
│ ├── scripts/
|
||||
│ └── references/
|
||||
└── ...
|
||||
```
|
||||
|
||||
## SKILL.md Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: Brief description (shown in skill search results)
|
||||
version: 1.0.0
|
||||
author: Your Name
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Category, Subcategory, Keywords]
|
||||
related_skills: [other-skill-name]
|
||||
---
|
||||
|
||||
# Skill Title
|
||||
|
||||
Brief intro.
|
||||
|
||||
## When to Use
|
||||
Trigger conditions — when should the agent load this skill?
|
||||
|
||||
## Quick Reference
|
||||
Table of common commands or API calls.
|
||||
|
||||
## Procedure
|
||||
Step-by-step instructions the agent follows.
|
||||
|
||||
## Pitfalls
|
||||
Known failure modes and how to handle them.
|
||||
|
||||
## Verification
|
||||
How the agent confirms it worked.
|
||||
```
|
||||
|
||||
## Skill Guidelines
|
||||
|
||||
### No External Dependencies
|
||||
|
||||
Prefer stdlib Python, curl, and existing Hermes tools (`web_extract`, `terminal`, `read_file`). If a dependency is needed, document installation steps in the skill.
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
Put the most common workflow first. Edge cases and advanced usage go at the bottom. This keeps token usage low for common tasks.
|
||||
|
||||
### Include Helper Scripts
|
||||
|
||||
For XML/JSON parsing or complex logic, include helper scripts in `scripts/` — don't expect the LLM to write parsers inline every time.
|
||||
|
||||
### Test It
|
||||
|
||||
Run the skill and verify the agent follows the instructions correctly:
|
||||
|
||||
```bash
|
||||
hermes --toolsets skills -q "Use the X skill to do Y"
|
||||
```
|
||||
|
||||
## Should the Skill Be Bundled?
|
||||
|
||||
Bundled skills (in `skills/`) ship with every Hermes install. They should be **broadly useful to most users**:
|
||||
|
||||
- Document handling, web research, common dev workflows, system administration
|
||||
- Used regularly by a wide range of people
|
||||
|
||||
If your skill is specialized (a niche engineering tool, a specific SaaS integration, a game), it's better suited for a **Skills Hub** — upload it to a registry and share it via `hermes skills install`.
|
||||
|
||||
## Publishing Skills
|
||||
|
||||
### To the Skills Hub
|
||||
|
||||
```bash
|
||||
hermes skills publish skills/my-skill --to github --repo owner/repo
|
||||
```
|
||||
|
||||
### To a Custom Repository
|
||||
|
||||
Add your repo as a tap:
|
||||
|
||||
```bash
|
||||
hermes skills tap add owner/repo
|
||||
```
|
||||
|
||||
Users can then search and install from your repository.
|
||||
|
||||
## Security Scanning
|
||||
|
||||
All hub-installed skills go through a security scanner that checks for:
|
||||
|
||||
- Data exfiltration patterns
|
||||
- Prompt injection attempts
|
||||
- Destructive commands
|
||||
- Shell injection
|
||||
|
||||
Trust levels:
|
||||
- `builtin` — ships with Hermes (always trusted)
|
||||
- `trusted` — from openai/skills, anthropics/skills
|
||||
- `community` — any findings = blocked unless `--force`
|
||||
Reference in New Issue
Block a user