2026-02-02 19:01:51 -08:00
|
|
|
|
"""
|
|
|
|
|
|
Configuration management for Hermes Agent.
|
|
|
|
|
|
|
|
|
|
|
|
Config files are stored in ~/.hermes/ for easy access:
|
|
|
|
|
|
- ~/.hermes/config.yaml - All settings (model, toolsets, terminal, etc.)
|
|
|
|
|
|
- ~/.hermes/.env - API keys and secrets
|
|
|
|
|
|
|
|
|
|
|
|
This module provides:
|
|
|
|
|
|
- hermes config - Show current configuration
|
|
|
|
|
|
- hermes config edit - Open config in editor
|
|
|
|
|
|
- hermes config set - Set a specific value
|
|
|
|
|
|
- hermes config wizard - Re-run setup wizard
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
2026-03-02 22:26:21 -08:00
|
|
|
|
import platform
|
2026-03-13 03:14:04 -07:00
|
|
|
|
import re
|
2026-03-06 15:14:26 +03:00
|
|
|
|
import stat
|
2026-03-13 03:14:04 -07:00
|
|
|
|
import sys
|
2026-02-02 19:01:51 -08:00
|
|
|
|
import subprocess
|
2026-03-06 15:14:26 +03:00
|
|
|
|
import sys
|
2026-03-11 08:58:33 -07:00
|
|
|
|
import tempfile
|
2026-02-02 19:01:51 -08:00
|
|
|
|
from pathlib import Path
|
2026-02-02 19:39:23 -08:00
|
|
|
|
from typing import Dict, Any, Optional, List, Tuple
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-03-02 22:26:21 -08:00
|
|
|
|
_IS_WINDOWS = platform.system() == "Windows"
|
2026-03-13 03:14:04 -07:00
|
|
|
|
_ENV_VAR_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
2026-03-02 22:26:21 -08:00
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
import yaml
|
|
|
|
|
|
|
2026-02-20 23:23:32 -08:00
|
|
|
|
from hermes_cli.colors import Colors, color
|
2026-03-14 08:05:30 -07:00
|
|
|
|
from hermes_cli.default_soul import DEFAULT_SOUL_MD
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config paths
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def get_hermes_home() -> Path:
|
|
|
|
|
|
"""Get the Hermes home directory (~/.hermes)."""
|
|
|
|
|
|
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
|
|
|
|
|
|
|
|
|
|
|
def get_config_path() -> Path:
|
|
|
|
|
|
"""Get the main config file path."""
|
|
|
|
|
|
return get_hermes_home() / "config.yaml"
|
|
|
|
|
|
|
|
|
|
|
|
def get_env_path() -> Path:
|
|
|
|
|
|
"""Get the .env file path (for API keys)."""
|
|
|
|
|
|
return get_hermes_home() / ".env"
|
|
|
|
|
|
|
|
|
|
|
|
def get_project_root() -> Path:
|
|
|
|
|
|
"""Get the project installation directory."""
|
|
|
|
|
|
return Path(__file__).parent.parent.resolve()
|
|
|
|
|
|
|
2026-03-09 02:19:32 -07:00
|
|
|
|
def _secure_dir(path):
|
|
|
|
|
|
"""Set directory to owner-only access (0700). No-op on Windows."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.chmod(path, 0o700)
|
|
|
|
|
|
except (OSError, NotImplementedError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _secure_file(path):
|
|
|
|
|
|
"""Set file to owner-only read/write (0600). No-op on Windows."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if os.path.exists(str(path)):
|
|
|
|
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
except (OSError, NotImplementedError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-14 08:05:30 -07:00
|
|
|
|
def _ensure_default_soul_md(home: Path) -> None:
|
|
|
|
|
|
"""Seed a default SOUL.md into HERMES_HOME if the user doesn't have one yet."""
|
|
|
|
|
|
soul_path = home / "SOUL.md"
|
|
|
|
|
|
if soul_path.exists():
|
|
|
|
|
|
return
|
|
|
|
|
|
soul_path.write_text(DEFAULT_SOUL_MD, encoding="utf-8")
|
|
|
|
|
|
_secure_file(soul_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def ensure_hermes_home():
|
2026-03-09 02:19:32 -07:00
|
|
|
|
"""Ensure ~/.hermes directory structure exists with secure permissions."""
|
2026-02-02 19:01:51 -08:00
|
|
|
|
home = get_hermes_home()
|
2026-03-09 02:19:32 -07:00
|
|
|
|
home.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
_secure_dir(home)
|
|
|
|
|
|
for subdir in ("cron", "sessions", "logs", "memories"):
|
|
|
|
|
|
d = home / subdir
|
|
|
|
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
_secure_dir(d)
|
2026-03-14 08:05:30 -07:00
|
|
|
|
_ensure_default_soul_md(home)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config loading/saving
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_CONFIG = {
|
2026-02-08 10:49:24 +00:00
|
|
|
|
"model": "anthropic/claude-opus-4.6",
|
2026-02-02 19:01:51 -08:00
|
|
|
|
"toolsets": ["hermes-cli"],
|
2026-03-07 21:01:23 -08:00
|
|
|
|
"agent": {
|
|
|
|
|
|
"max_turns": 90,
|
|
|
|
|
|
},
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
"terminal": {
|
|
|
|
|
|
"backend": "local",
|
|
|
|
|
|
"cwd": ".", # Use current directory
|
|
|
|
|
|
"timeout": 180,
|
2026-02-02 19:13:41 -08:00
|
|
|
|
"docker_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
|
|
|
|
|
"singularity_image": "docker://nikolaik/python-nodejs:python3.11-nodejs20",
|
|
|
|
|
|
"modal_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
2026-03-05 11:12:50 -08:00
|
|
|
|
"daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
|
|
|
|
|
# Container resource limits (docker, singularity, modal, daytona — ignored for local/ssh)
|
2026-03-04 03:29:05 -08:00
|
|
|
|
"container_cpu": 1,
|
|
|
|
|
|
"container_memory": 5120, # MB (default 5GB)
|
|
|
|
|
|
"container_disk": 51200, # MB (default 50GB)
|
|
|
|
|
|
"container_persistent": True, # Persist filesystem across sessions
|
2026-03-09 15:29:34 -07:00
|
|
|
|
# Docker volume mounts — share host directories with the container.
|
|
|
|
|
|
# Each entry is "host_path:container_path" (standard Docker -v syntax).
|
|
|
|
|
|
# Example: ["/home/user/projects:/workspace/projects", "/data:/data"]
|
|
|
|
|
|
"docker_volumes": [],
|
2026-03-16 05:19:43 -07:00
|
|
|
|
# Explicit opt-in: mount the host cwd into /workspace for Docker sessions.
|
|
|
|
|
|
# Default off because passing host directories into a sandbox weakens isolation.
|
|
|
|
|
|
"docker_mount_cwd_to_workspace": False,
|
2026-03-15 20:17:13 -07:00
|
|
|
|
# Persistent shell — keep a long-lived bash shell across execute() calls
|
|
|
|
|
|
# so cwd/env vars/shell variables survive between commands.
|
|
|
|
|
|
# Enabled by default for non-local backends (SSH); local is always opt-in
|
|
|
|
|
|
# via TERMINAL_LOCAL_PERSISTENT env var.
|
|
|
|
|
|
"persistent_shell": True,
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"browser": {
|
|
|
|
|
|
"inactivity_timeout": 120,
|
feat: browser console/errors tool, annotated screenshots, auto-recording, and dogfood QA skill
New browser capabilities and a built-in skill for agent-driven web QA.
## New tool: browser_console
Returns console messages (log/warn/error/info) AND uncaught JavaScript
exceptions in a single call. Uses agent-browser's 'console' and 'errors'
commands through the existing session plumbing. Supports --clear to reset
buffers. Verified working in both local and Browserbase cloud modes.
## Enhanced tool: browser_vision(annotate=True)
New boolean parameter on browser_vision. When true, agent-browser overlays
numbered [N] labels on interactive elements — each [N] maps to ref @eN.
Annotation data (element name, role, bounding box) returned alongside the
vision analysis. Useful for QA reports and spatial reasoning.
## Config: browser.record_sessions
Auto-record browser sessions as WebM video files when enabled:
- Starts recording on first browser_navigate
- Stops and saves on browser_close
- Saves to ~/.hermes/browser_recordings/
- Works in both local and cloud modes (verified)
- Disabled by default
## Built-in skill: dogfood
Systematic exploratory QA testing for web applications. Teaches the agent
a 5-phase workflow:
1. Plan — accept URL, create output dirs, set scope
2. Explore — systematic crawl with annotated screenshots
3. Collect Evidence — screenshots, console errors, JS exceptions
4. Categorize — severity (Critical/High/Medium/Low) and category
(Functional/Visual/Accessibility/Console/UX/Content)
5. Report — structured markdown with per-issue evidence
Includes:
- skills/dogfood/SKILL.md — full workflow instructions
- skills/dogfood/references/issue-taxonomy.md — severity/category defs
- skills/dogfood/templates/dogfood-report-template.md — report template
## Tests
21 new tests covering:
- browser_console message/error parsing, clear flag, empty/failed states
- browser_console schema registration
- browser_vision annotate schema and flag passing
- record_sessions config defaults and recording lifecycle
- Dogfood skill file existence and content validation
Addresses #315.
2026-03-08 21:02:14 -07:00
|
|
|
|
"record_sessions": False, # Auto-record browser sessions as WebM videos
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
|
# Filesystem checkpoints — automatic snapshots before destructive file ops.
|
|
|
|
|
|
# When enabled, the agent takes a snapshot of the working directory once per
|
|
|
|
|
|
# conversation turn (on first write_file/patch call). Use /rollback to restore.
|
|
|
|
|
|
"checkpoints": {
|
feat: major /rollback improvements — enabled by default, diff preview, file-level restore, conversation undo, terminal checkpoints
Checkpoint & rollback upgrades:
1. Enabled by default — checkpoints are now on for all new sessions.
Zero cost when no file-mutating tools fire. Disable with
checkpoints.enabled: false in config.yaml.
2. Diff preview — /rollback diff <N> shows a git diff between the
checkpoint and current working tree before committing to a restore.
3. File-level restore — /rollback <N> <file> restores a single file
from a checkpoint instead of the entire directory.
4. Conversation undo on rollback — when restoring files, the last
chat turn is automatically undone so the agent's context matches
the restored filesystem state.
5. Terminal command checkpoints — destructive terminal commands (rm,
mv, sed -i, truncate, git reset/clean, output redirects) now
trigger automatic checkpoints before execution. Previously only
write_file and patch were covered.
6. Change summary in listing — /rollback now shows file count and
+insertions/-deletions for each checkpoint.
7. Fixed dead code — removed duplicate _run_git call in
list_checkpoints with nonsensical --all if False condition.
8. Updated help text — /rollback with no args now shows available
subcommands (diff, file-level restore).
2026-03-16 04:43:37 -07:00
|
|
|
|
"enabled": True,
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
|
"max_snapshots": 50, # Max checkpoints to keep per directory
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
"compression": {
|
|
|
|
|
|
"enabled": True,
|
2026-03-12 15:51:50 -07:00
|
|
|
|
"threshold": 0.50,
|
2026-02-08 10:49:24 +00:00
|
|
|
|
"summary_model": "google/gemini-3-flash-preview",
|
2026-03-07 08:52:06 -08:00
|
|
|
|
"summary_provider": "auto",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-11 20:52:19 -07:00
|
|
|
|
# Auxiliary model config — provider:model for each side task.
|
|
|
|
|
|
# Format: provider is the provider name, model is the model slug.
|
|
|
|
|
|
# "auto" for provider = auto-detect best available provider.
|
|
|
|
|
|
# Empty model = use provider's default auxiliary model.
|
|
|
|
|
|
# All tasks fall back to openrouter:google/gemini-3-flash-preview if
|
|
|
|
|
|
# the configured provider is unavailable.
|
2026-03-07 08:52:06 -08:00
|
|
|
|
"auxiliary": {
|
|
|
|
|
|
"vision": {
|
2026-03-11 20:52:19 -07:00
|
|
|
|
"provider": "auto", # auto | openrouter | nous | codex | custom
|
2026-03-07 08:52:06 -08:00
|
|
|
|
"model": "", # e.g. "google/gemini-2.5-flash", "gpt-4o"
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "", # direct OpenAI-compatible endpoint (takes precedence over provider)
|
|
|
|
|
|
"api_key": "", # API key for base_url (falls back to OPENAI_API_KEY)
|
2026-03-07 08:52:06 -08:00
|
|
|
|
},
|
|
|
|
|
|
"web_extract": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "",
|
|
|
|
|
|
"api_key": "",
|
2026-03-07 08:52:06 -08:00
|
|
|
|
},
|
2026-03-11 20:52:19 -07:00
|
|
|
|
"compression": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "",
|
|
|
|
|
|
"api_key": "",
|
2026-03-11 20:52:19 -07:00
|
|
|
|
},
|
|
|
|
|
|
"session_search": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "",
|
|
|
|
|
|
"api_key": "",
|
2026-03-11 20:52:19 -07:00
|
|
|
|
},
|
|
|
|
|
|
"skills_hub": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "",
|
|
|
|
|
|
"api_key": "",
|
2026-03-11 20:52:19 -07:00
|
|
|
|
},
|
feat: smart approvals + /stop command (inspired by OpenAI Codex)
* feat: smart approvals — LLM-based risk assessment for dangerous commands
Adds a 'smart' approval mode that uses the auxiliary LLM to assess
whether a flagged command is genuinely dangerous or a false positive,
auto-approving low-risk commands without prompting the user.
Inspired by OpenAI Codex's Smart Approvals guardian subagent
(openai/codex#13860).
Config (config.yaml):
approvals:
mode: manual # manual (default), smart, off
Modes:
- manual — current behavior, always prompt the user
- smart — aux LLM evaluates risk: APPROVE (auto-allow), DENY (block),
or ESCALATE (fall through to manual prompt)
- off — skip all approval prompts (equivalent to --yolo)
When smart mode auto-approves, the pattern gets session-level approval
so subsequent uses of the same pattern don't trigger another LLM call.
When it denies, the command is blocked without user prompt. When
uncertain, it escalates to the normal manual approval flow.
The LLM prompt is carefully scoped: it sees only the command text and
the flagged reason, assesses actual risk vs false positive, and returns
a single-word verdict.
* feat: make smart approval model configurable via config.yaml
Adds auxiliary.approval section to config.yaml with the same
provider/model/base_url/api_key pattern as other aux tasks (vision,
web_extract, compression, etc.).
Config:
auxiliary:
approval:
provider: auto
model: '' # fast/cheap model recommended
base_url: ''
api_key: ''
Bridged to env vars in both CLI and gateway paths so the aux client
picks them up automatically.
* feat: add /stop command to kill all background processes
Adds a /stop slash command that kills all running background processes
at once. Currently users have to process(list) then process(kill) for
each one individually.
Inspired by OpenAI Codex's separation of interrupt (Ctrl+C stops current
turn) from /stop (cleans up background processes). See openai/codex#14602.
Ctrl+C continues to only interrupt the active agent turn — background
dev servers, watchers, etc. are preserved. /stop is the explicit way
to clean them all up.
2026-03-16 06:20:11 -07:00
|
|
|
|
"approval": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "", # fast/cheap model recommended (e.g. gemini-flash, haiku)
|
|
|
|
|
|
"base_url": "",
|
|
|
|
|
|
"api_key": "",
|
|
|
|
|
|
},
|
2026-03-11 20:52:19 -07:00
|
|
|
|
"mcp": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "",
|
|
|
|
|
|
"api_key": "",
|
2026-03-11 20:52:19 -07:00
|
|
|
|
},
|
|
|
|
|
|
"flush_memories": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "",
|
|
|
|
|
|
"api_key": "",
|
2026-03-11 20:52:19 -07:00
|
|
|
|
},
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"display": {
|
|
|
|
|
|
"compact": False,
|
|
|
|
|
|
"personality": "kawaii",
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
|
"resume_display": "full",
|
|
|
|
|
|
"bell_on_complete": False,
|
2026-03-11 05:53:21 -07:00
|
|
|
|
"show_reasoning": False,
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
|
"skin": "default",
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
2026-03-16 05:48:45 -07:00
|
|
|
|
|
|
|
|
|
|
# Privacy settings
|
|
|
|
|
|
"privacy": {
|
|
|
|
|
|
"redact_pii": False, # When True, hash user IDs and strip phone numbers from LLM context
|
|
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
2026-02-12 10:05:08 -08:00
|
|
|
|
# Text-to-speech configuration
|
|
|
|
|
|
"tts": {
|
|
|
|
|
|
"provider": "edge", # "edge" (free) | "elevenlabs" (premium) | "openai"
|
|
|
|
|
|
"edge": {
|
|
|
|
|
|
"voice": "en-US-AriaNeural",
|
|
|
|
|
|
# Popular: AriaNeural, JennyNeural, AndrewNeural, BrianNeural, SoniaNeural
|
|
|
|
|
|
},
|
|
|
|
|
|
"elevenlabs": {
|
|
|
|
|
|
"voice_id": "pNInz6obpgDQGcFmaJgB", # Adam
|
|
|
|
|
|
"model_id": "eleven_multilingual_v2",
|
|
|
|
|
|
},
|
|
|
|
|
|
"openai": {
|
|
|
|
|
|
"model": "gpt-4o-mini-tts",
|
|
|
|
|
|
"voice": "alloy",
|
|
|
|
|
|
# Voices: alloy, echo, fable, onyx, nova, shimmer
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
"stt": {
|
2026-03-14 22:09:59 -07:00
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"provider": "local", # "local" (free, faster-whisper) | "groq" | "openai" (Whisper API)
|
feat(stt): add free local whisper transcription via faster-whisper (#1185)
* fix: Home Assistant event filtering now closed by default
Previously, when no watch_domains or watch_entities were configured,
ALL state_changed events passed through to the agent, causing users
to be flooded with notifications for every HA entity change.
Now events are dropped by default unless the user explicitly configures:
- watch_domains: list of domains to monitor (e.g. climate, light)
- watch_entities: list of specific entity IDs to monitor
- watch_all: true (new option — opt-in to receive all events)
A warning is logged at connect time if no filters are configured,
guiding users to set up their HA platform config.
All 49 gateway HA tests + 52 HA tool tests pass.
* docs: update Home Assistant integration documentation
- homeassistant.md: Fix event filtering docs to reflect closed-by-default
behavior. Add watch_all option. Replace Python dict config example with
YAML. Fix defaults table (was incorrectly showing 'all'). Add required
configuration warning admonition.
- environment-variables.md: Add HASS_TOKEN and HASS_URL to Messaging section.
- messaging/index.md: Add Home Assistant to description, architecture
diagram, platform toolsets table, and Next Steps links.
* fix(terminal): strip provider env vars from background and PTY subprocesses
Extends the env var blocklist from #1157 to also cover the two remaining
leaky paths in process_registry.py:
- spawn_local() PTY path (line 156)
- spawn_local() background Popen path (line 197)
Both were still using raw os.environ, leaking provider vars to background
processes and interactive PTY sessions. Now uses the same dynamic
_HERMES_PROVIDER_ENV_BLOCKLIST from local.py.
Explicit env_vars passed to spawn_local() still override the blocklist,
matching the existing behavior for callers that intentionally need these.
Gap identified by PR #1004 (@PeterFile).
* feat(delegate): add observability metadata to subagent results
Enrich delegate_task results with metadata from the child AIAgent:
- model: which model the child used
- exit_reason: completed | interrupted | max_iterations
- tokens.input / tokens.output: token counts
- tool_trace: per-tool-call trace with byte sizes and ok/error status
Tool trace uses tool_call_id matching to correctly pair parallel tool
calls with their results, with a fallback for messages without IDs.
Cherry-picked from PR #872 by @omerkaz, with fixes:
- Fixed parallel tool call trace pairing (was always updating last entry)
- Removed redundant 'iterations' field (identical to existing 'api_calls')
- Added test for parallel tool call trace correctness
Co-authored-by: omerkaz <omerkaz@users.noreply.github.com>
* feat(stt): add free local whisper transcription via faster-whisper
Replace OpenAI-only STT with a dual-provider system mirroring the TTS
architecture (Edge TTS free / ElevenLabs paid):
STT: faster-whisper local (free, default) / OpenAI Whisper API (paid)
Changes:
- tools/transcription_tools.py: Full rewrite with provider dispatch,
config loading, local faster-whisper backend, and OpenAI API backend.
Auto-downloads model (~150MB for 'base') on first voice message.
Singleton model instance reused across calls.
- pyproject.toml: Add faster-whisper>=1.0.0 as core dependency
- hermes_cli/config.py: Expand stt config to match TTS pattern with
provider selection and per-provider model settings
- agent/context_compressor.py: Fix .strip() crash when LLM returns
non-string content (dict from llama.cpp, None). Fixes #1100 partially.
- tests/: 23 new tests for STT providers + 2 for compressor fix
- docs/: Updated Voice & TTS page with STT provider table, model sizes,
config examples, and fallback behavior
Fallback behavior:
- Local not installed → OpenAI API (if key set)
- OpenAI key not set → local whisper (if installed)
- Neither → graceful error message to user
Co-authored-by: Jah-yee <Jah-yee@users.noreply.github.com>
---------
Co-authored-by: omerkaz <omerkaz@users.noreply.github.com>
Co-authored-by: Jah-yee <Jah-yee@users.noreply.github.com>
2026-03-13 11:11:05 -07:00
|
|
|
|
"local": {
|
|
|
|
|
|
"model": "base", # tiny, base, small, medium, large-v3
|
|
|
|
|
|
},
|
|
|
|
|
|
"openai": {
|
|
|
|
|
|
"model": "whisper-1", # whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe
|
|
|
|
|
|
},
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
},
|
2026-03-03 16:17:05 +03:00
|
|
|
|
|
|
|
|
|
|
"voice": {
|
2026-03-09 13:00:08 +03:00
|
|
|
|
"record_key": "ctrl+b",
|
2026-03-03 16:17:05 +03:00
|
|
|
|
"max_recording_seconds": 120,
|
|
|
|
|
|
"auto_tts": False,
|
2026-03-03 20:43:22 +03:00
|
|
|
|
"silence_threshold": 200, # RMS below this = silence (0-32767)
|
|
|
|
|
|
"silence_duration": 3.0, # Seconds of silence before auto-stop
|
2026-03-03 16:17:05 +03:00
|
|
|
|
},
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
|
|
|
|
|
|
"human_delay": {
|
|
|
|
|
|
"mode": "off",
|
|
|
|
|
|
"min_ms": 800,
|
|
|
|
|
|
"max_ms": 2500,
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-19 00:57:31 -08:00
|
|
|
|
# Persistent memory -- bounded curated memory injected into system prompt
|
|
|
|
|
|
"memory": {
|
|
|
|
|
|
"memory_enabled": True,
|
|
|
|
|
|
"user_profile_enabled": True,
|
|
|
|
|
|
"memory_char_limit": 2200, # ~800 tokens at 2.75 chars/token
|
|
|
|
|
|
"user_char_limit": 1375, # ~500 tokens at 2.75 chars/token
|
|
|
|
|
|
},
|
feat: configurable subagent provider:model with full credential resolution
Adds delegation.model and delegation.provider config fields so subagents
can run on a completely different provider:model pair than the parent agent.
When delegation.provider is set, the system resolves the full credential
bundle (base_url, api_key, api_mode) via resolve_runtime_provider() —
the same path used by CLI/gateway startup. This means all configured
providers work out of the box: openrouter, nous, zai, kimi-coding,
minimax, minimax-cn.
Key design decisions:
- Provider resolution uses hermes_cli.runtime_provider (single source of
truth for credential resolution across CLI, gateway, cron, and now
delegation)
- When only delegation.model is set (no provider), the model name changes
but parent credentials are inherited (for switching models within the
same provider like OpenRouter)
- When delegation.provider is set, full credentials are resolved
independently — enabling cross-provider delegation (e.g. parent on
Nous Portal, subagents on OpenRouter)
- Clear error messages if provider resolution fails (missing API key,
unknown provider name)
- _load_config() now falls back to hermes_cli.config.load_config() for
gateway/cron contexts where CLI_CONFIG is unavailable
Based on PR #791 by 0xbyt4 (closes #609), reworked to use proper
provider credential resolution instead of passing provider as metadata.
Co-authored-by: 0xbyt4 <0xbyt4@users.noreply.github.com>
2026-03-11 06:12:21 -07:00
|
|
|
|
|
|
|
|
|
|
# Subagent delegation — override the provider:model used by delegate_task
|
|
|
|
|
|
# so child agents can run on a different (cheaper/faster) provider and model.
|
|
|
|
|
|
# Uses the same runtime provider resolution as CLI/gateway startup, so all
|
|
|
|
|
|
# configured providers (OpenRouter, Nous, Z.ai, Kimi, etc.) are supported.
|
|
|
|
|
|
"delegation": {
|
|
|
|
|
|
"model": "", # e.g. "google/gemini-3-flash-preview" (empty = inherit parent model)
|
|
|
|
|
|
"provider": "", # e.g. "openrouter" (empty = inherit parent provider + credentials)
|
2026-03-14 20:48:29 -07:00
|
|
|
|
"base_url": "", # direct OpenAI-compatible endpoint for subagents
|
|
|
|
|
|
"api_key": "", # API key for delegation.base_url (falls back to OPENAI_API_KEY)
|
feat: configurable subagent provider:model with full credential resolution
Adds delegation.model and delegation.provider config fields so subagents
can run on a completely different provider:model pair than the parent agent.
When delegation.provider is set, the system resolves the full credential
bundle (base_url, api_key, api_mode) via resolve_runtime_provider() —
the same path used by CLI/gateway startup. This means all configured
providers work out of the box: openrouter, nous, zai, kimi-coding,
minimax, minimax-cn.
Key design decisions:
- Provider resolution uses hermes_cli.runtime_provider (single source of
truth for credential resolution across CLI, gateway, cron, and now
delegation)
- When only delegation.model is set (no provider), the model name changes
but parent credentials are inherited (for switching models within the
same provider like OpenRouter)
- When delegation.provider is set, full credentials are resolved
independently — enabling cross-provider delegation (e.g. parent on
Nous Portal, subagents on OpenRouter)
- Clear error messages if provider resolution fails (missing API key,
unknown provider name)
- _load_config() now falls back to hermes_cli.config.load_config() for
gateway/cron contexts where CLI_CONFIG is unavailable
Based on PR #791 by 0xbyt4 (closes #609), reworked to use proper
provider credential resolution instead of passing provider as metadata.
Co-authored-by: 0xbyt4 <0xbyt4@users.noreply.github.com>
2026-03-11 06:12:21 -07:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-23 23:55:42 -08:00
|
|
|
|
# Ephemeral prefill messages file — JSON list of {role, content} dicts
|
|
|
|
|
|
# injected at the start of every API call for few-shot priming.
|
|
|
|
|
|
# Never saved to sessions, logs, or trajectories.
|
|
|
|
|
|
"prefill_messages_file": "",
|
|
|
|
|
|
|
2026-02-25 19:34:25 -05:00
|
|
|
|
# Honcho AI-native memory -- reads ~/.honcho/config.json as single source of truth.
|
|
|
|
|
|
# This section is only needed for hermes-specific overrides; everything else
|
|
|
|
|
|
# (apiKey, workspace, peerName, sessions, enabled) comes from the global config.
|
|
|
|
|
|
"honcho": {},
|
|
|
|
|
|
|
2026-03-03 11:57:18 +05:30
|
|
|
|
# IANA timezone (e.g. "Asia/Kolkata", "America/New_York").
|
|
|
|
|
|
# Empty string means use server-local time.
|
|
|
|
|
|
"timezone": "",
|
|
|
|
|
|
|
2026-03-11 09:15:31 -07:00
|
|
|
|
# Discord platform settings (gateway mode)
|
|
|
|
|
|
"discord": {
|
|
|
|
|
|
"require_mention": True, # Require @mention to respond in server channels
|
|
|
|
|
|
"free_response_channels": "", # Comma-separated channel IDs where bot responds without mention
|
2026-03-15 07:59:55 -07:00
|
|
|
|
"auto_thread": True, # Auto-create threads on @mention in channels (like Slack)
|
2026-03-11 09:15:31 -07:00
|
|
|
|
},
|
|
|
|
|
|
|
feat: smart approvals + /stop command (inspired by OpenAI Codex)
* feat: smart approvals — LLM-based risk assessment for dangerous commands
Adds a 'smart' approval mode that uses the auxiliary LLM to assess
whether a flagged command is genuinely dangerous or a false positive,
auto-approving low-risk commands without prompting the user.
Inspired by OpenAI Codex's Smart Approvals guardian subagent
(openai/codex#13860).
Config (config.yaml):
approvals:
mode: manual # manual (default), smart, off
Modes:
- manual — current behavior, always prompt the user
- smart — aux LLM evaluates risk: APPROVE (auto-allow), DENY (block),
or ESCALATE (fall through to manual prompt)
- off — skip all approval prompts (equivalent to --yolo)
When smart mode auto-approves, the pattern gets session-level approval
so subsequent uses of the same pattern don't trigger another LLM call.
When it denies, the command is blocked without user prompt. When
uncertain, it escalates to the normal manual approval flow.
The LLM prompt is carefully scoped: it sees only the command text and
the flagged reason, assesses actual risk vs false positive, and returns
a single-word verdict.
* feat: make smart approval model configurable via config.yaml
Adds auxiliary.approval section to config.yaml with the same
provider/model/base_url/api_key pattern as other aux tasks (vision,
web_extract, compression, etc.).
Config:
auxiliary:
approval:
provider: auto
model: '' # fast/cheap model recommended
base_url: ''
api_key: ''
Bridged to env vars in both CLI and gateway paths so the aux client
picks them up automatically.
* feat: add /stop command to kill all background processes
Adds a /stop slash command that kills all running background processes
at once. Currently users have to process(list) then process(kill) for
each one individually.
Inspired by OpenAI Codex's separation of interrupt (Ctrl+C stops current
turn) from /stop (cleans up background processes). See openai/codex#14602.
Ctrl+C continues to only interrupt the active agent turn — background
dev servers, watchers, etc. are preserved. /stop is the explicit way
to clean them all up.
2026-03-16 06:20:11 -07:00
|
|
|
|
# Approval mode for dangerous commands:
|
|
|
|
|
|
# manual — always prompt the user (default)
|
|
|
|
|
|
# smart — use auxiliary LLM to auto-approve low-risk commands, prompt for high-risk
|
|
|
|
|
|
# off — skip all approval prompts (equivalent to --yolo)
|
|
|
|
|
|
"approvals": {
|
|
|
|
|
|
"mode": "manual",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 23:35:18 -08:00
|
|
|
|
# Permanently allowed dangerous command patterns (added via "always" approval)
|
|
|
|
|
|
"command_allowlist": [],
|
2026-03-09 07:38:06 +03:00
|
|
|
|
# User-defined quick commands that bypass the agent loop (type: exec only)
|
|
|
|
|
|
"quick_commands": {},
|
2026-03-09 17:18:09 +03:00
|
|
|
|
# Custom personalities — add your own entries here
|
|
|
|
|
|
# Supports string format: {"name": "system prompt"}
|
|
|
|
|
|
# Or dict format: {"name": {"description": "...", "system_prompt": "...", "tone": "...", "style": "..."}}
|
|
|
|
|
|
"personalities": {},
|
2026-03-03 11:57:18 +05:30
|
|
|
|
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
# Pre-exec security scanning via tirith
|
|
|
|
|
|
"security": {
|
|
|
|
|
|
"redact_secrets": True,
|
|
|
|
|
|
"tirith_enabled": True,
|
|
|
|
|
|
"tirith_path": "tirith",
|
|
|
|
|
|
"tirith_timeout": 5,
|
|
|
|
|
|
"tirith_fail_open": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# Config schema version - bump this when adding new required fields
|
2026-03-14 22:09:59 -07:00
|
|
|
|
"_config_version": 8,
|
2026-02-02 19:01:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config Migration System
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
2026-03-08 05:55:30 -07:00
|
|
|
|
# Track which env vars were introduced in each config version.
|
|
|
|
|
|
# Migration only mentions vars new since the user's previous version.
|
|
|
|
|
|
ENV_VARS_BY_VERSION: Dict[int, List[str]] = {
|
|
|
|
|
|
3: ["FIRECRAWL_API_KEY", "BROWSERBASE_API_KEY", "BROWSERBASE_PROJECT_ID", "FAL_KEY"],
|
|
|
|
|
|
4: ["VOICE_TOOLS_OPENAI_KEY", "ELEVENLABS_API_KEY"],
|
|
|
|
|
|
5: ["WHATSAPP_ENABLED", "WHATSAPP_MODE", "WHATSAPP_ALLOWED_USERS",
|
|
|
|
|
|
"SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_ALLOWED_USERS"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 23:06:47 +00:00
|
|
|
|
# Required environment variables with metadata for migration prompts.
|
|
|
|
|
|
# LLM provider is required but handled in the setup wizard's provider
|
|
|
|
|
|
# selection step (Nous Portal / OpenRouter / Custom endpoint), so this
|
|
|
|
|
|
# dict is intentionally empty — no single env var is universally required.
|
|
|
|
|
|
REQUIRED_ENV_VARS = {}
|
|
|
|
|
|
|
|
|
|
|
|
# Optional environment variables that enhance functionality
|
|
|
|
|
|
OPTIONAL_ENV_VARS = {
|
2026-02-23 23:25:38 +00:00
|
|
|
|
# ── Provider (handled in provider selection, not shown in checklists) ──
|
2026-03-08 18:40:50 +10:00
|
|
|
|
"NOUS_BASE_URL": {
|
|
|
|
|
|
"description": "Nous Portal base URL override",
|
|
|
|
|
|
"prompt": "Nous Portal base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"OPENROUTER_API_KEY": {
|
2026-02-23 23:06:47 +00:00
|
|
|
|
"description": "OpenRouter API key (for vision, web scraping helpers, and MoA)",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"prompt": "OpenRouter API key",
|
|
|
|
|
|
"url": "https://openrouter.ai/keys",
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:06:47 +00:00
|
|
|
|
"tools": ["vision_analyze", "mixture_of_agents"],
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
feat: add z.ai/GLM, Kimi/Moonshot, MiniMax as first-class providers
Adds 4 new direct API-key providers (zai, kimi-coding, minimax, minimax-cn)
to the inference provider system. All use standard OpenAI-compatible
chat/completions endpoints with Bearer token auth.
Core changes:
- auth.py: Extended ProviderConfig with api_key_env_vars and base_url_env_var
fields. Added providers to PROVIDER_REGISTRY. Added provider aliases
(glm, z-ai, zhipu, kimi, moonshot). Added auto-detection of API-key
providers in resolve_provider(). Added resolve_api_key_provider_credentials()
and get_api_key_provider_status() helpers.
- runtime_provider.py: Added generic API-key provider branch in
resolve_runtime_provider() — any provider with auth_type='api_key'
is automatically handled.
- main.py: Added providers to hermes model menu with generic
_model_flow_api_key_provider() flow. Updated _has_any_provider_configured()
to check all provider env vars. Updated argparse --provider choices.
- setup.py: Added providers to setup wizard with API key prompts and
curated model lists.
- config.py: Added env vars (GLM_API_KEY, KIMI_API_KEY, MINIMAX_API_KEY,
etc.) to OPTIONAL_ENV_VARS.
- status.py: Added API key display and provider status section.
- doctor.py: Added connectivity checks for each provider endpoint.
- cli.py: Updated provider docstrings.
Docs: Updated README.md, .env.example, cli-config.yaml.example,
cli-commands.md, environment-variables.md, configuration.md.
Tests: 50 new tests covering registry, aliases, resolution, auto-detection,
credential resolution, and runtime provider dispatch.
Inspired by PR #33 (numman-ali) which proposed a provider registry approach.
Credit to tars90percent (PR #473) and manuelschipper (PR #420) for related
provider improvements merged earlier in this changeset.
2026-03-06 18:55:12 -08:00
|
|
|
|
"GLM_API_KEY": {
|
|
|
|
|
|
"description": "Z.AI / GLM API key (also recognized as ZAI_API_KEY / Z_AI_API_KEY)",
|
|
|
|
|
|
"prompt": "Z.AI / GLM API key",
|
|
|
|
|
|
"url": "https://z.ai/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"ZAI_API_KEY": {
|
|
|
|
|
|
"description": "Z.AI API key (alias for GLM_API_KEY)",
|
|
|
|
|
|
"prompt": "Z.AI API key",
|
|
|
|
|
|
"url": "https://z.ai/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"Z_AI_API_KEY": {
|
|
|
|
|
|
"description": "Z.AI API key (alias for GLM_API_KEY)",
|
|
|
|
|
|
"prompt": "Z.AI API key",
|
|
|
|
|
|
"url": "https://z.ai/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"GLM_BASE_URL": {
|
|
|
|
|
|
"description": "Z.AI / GLM base URL override",
|
|
|
|
|
|
"prompt": "Z.AI / GLM base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"KIMI_API_KEY": {
|
|
|
|
|
|
"description": "Kimi / Moonshot API key",
|
|
|
|
|
|
"prompt": "Kimi API key",
|
|
|
|
|
|
"url": "https://platform.moonshot.cn/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"KIMI_BASE_URL": {
|
|
|
|
|
|
"description": "Kimi / Moonshot base URL override",
|
|
|
|
|
|
"prompt": "Kimi base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_API_KEY": {
|
|
|
|
|
|
"description": "MiniMax API key (international)",
|
|
|
|
|
|
"prompt": "MiniMax API key",
|
|
|
|
|
|
"url": "https://www.minimax.io/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_BASE_URL": {
|
|
|
|
|
|
"description": "MiniMax base URL override",
|
|
|
|
|
|
"prompt": "MiniMax base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_CN_API_KEY": {
|
|
|
|
|
|
"description": "MiniMax API key (China endpoint)",
|
|
|
|
|
|
"prompt": "MiniMax (China) API key",
|
|
|
|
|
|
"url": "https://www.minimaxi.com/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_CN_BASE_URL": {
|
|
|
|
|
|
"description": "MiniMax (China) base URL override",
|
|
|
|
|
|
"prompt": "MiniMax (China) base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-03-16 04:34:45 -07:00
|
|
|
|
"DEEPSEEK_API_KEY": {
|
|
|
|
|
|
"description": "DeepSeek API key for direct DeepSeek access",
|
|
|
|
|
|
"prompt": "DeepSeek API Key",
|
|
|
|
|
|
"url": "https://platform.deepseek.com/api_keys",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
},
|
|
|
|
|
|
"DEEPSEEK_BASE_URL": {
|
|
|
|
|
|
"description": "Custom DeepSeek API base URL (advanced)",
|
|
|
|
|
|
"prompt": "DeepSeek Base URL",
|
|
|
|
|
|
"url": "",
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
|
|
|
|
|
|
# ── Tool API keys ──
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"FIRECRAWL_API_KEY": {
|
|
|
|
|
|
"description": "Firecrawl API key for web search and scraping",
|
|
|
|
|
|
"prompt": "Firecrawl API key",
|
|
|
|
|
|
"url": "https://firecrawl.dev/",
|
|
|
|
|
|
"tools": ["web_search", "web_extract"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
2026-03-05 16:16:18 -06:00
|
|
|
|
"FIRECRAWL_API_URL": {
|
|
|
|
|
|
"description": "Firecrawl API URL for self-hosted instances (optional)",
|
|
|
|
|
|
"prompt": "Firecrawl API URL (leave empty for cloud)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "tool",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"BROWSERBASE_API_KEY": {
|
2026-03-07 01:23:27 -08:00
|
|
|
|
"description": "Browserbase API key for cloud browser (optional — local browser works without this)",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"prompt": "Browserbase API key",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"url": "https://browserbase.com/",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"tools": ["browser_navigate", "browser_click"],
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"BROWSERBASE_PROJECT_ID": {
|
2026-03-07 01:23:27 -08:00
|
|
|
|
"description": "Browserbase project ID (optional — only needed for cloud browser)",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"prompt": "Browserbase project ID",
|
|
|
|
|
|
"url": "https://browserbase.com/",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"tools": ["browser_navigate", "browser_click"],
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"FAL_KEY": {
|
|
|
|
|
|
"description": "FAL API key for image generation",
|
|
|
|
|
|
"prompt": "FAL API key",
|
|
|
|
|
|
"url": "https://fal.ai/",
|
|
|
|
|
|
"tools": ["image_generate"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
2026-02-04 09:36:51 -08:00
|
|
|
|
"TINKER_API_KEY": {
|
|
|
|
|
|
"description": "Tinker API key for RL training",
|
|
|
|
|
|
"prompt": "Tinker API key",
|
|
|
|
|
|
"url": "https://tinker-console.thinkingmachines.ai/keys",
|
|
|
|
|
|
"tools": ["rl_start_training", "rl_check_status", "rl_stop_training"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-04 09:36:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
"WANDB_API_KEY": {
|
|
|
|
|
|
"description": "Weights & Biases API key for experiment tracking",
|
|
|
|
|
|
"prompt": "WandB API key",
|
|
|
|
|
|
"url": "https://wandb.ai/authorize",
|
|
|
|
|
|
"tools": ["rl_get_results", "rl_check_status"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-04 09:36:51 -08:00
|
|
|
|
},
|
2026-02-23 23:21:33 +00:00
|
|
|
|
"VOICE_TOOLS_OPENAI_KEY": {
|
2026-02-17 03:11:17 -08:00
|
|
|
|
"description": "OpenAI API key for voice transcription (Whisper) and OpenAI TTS",
|
|
|
|
|
|
"prompt": "OpenAI API Key (for Whisper STT + TTS)",
|
2026-02-15 21:48:07 -08:00
|
|
|
|
"url": "https://platform.openai.com/api-keys",
|
2026-02-17 03:11:17 -08:00
|
|
|
|
"tools": ["voice_transcription", "openai_tts"],
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"ELEVENLABS_API_KEY": {
|
|
|
|
|
|
"description": "ElevenLabs API key for premium text-to-speech voices",
|
|
|
|
|
|
"prompt": "ElevenLabs API key",
|
|
|
|
|
|
"url": "https://elevenlabs.io/",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"GITHUB_TOKEN": {
|
|
|
|
|
|
"description": "GitHub token for Skills Hub (higher API rate limits, skill publish)",
|
|
|
|
|
|
"prompt": "GitHub Token",
|
|
|
|
|
|
"url": "https://github.com/settings/tokens",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
|
2026-02-25 19:34:25 -05:00
|
|
|
|
# ── Honcho ──
|
|
|
|
|
|
"HONCHO_API_KEY": {
|
|
|
|
|
|
"description": "Honcho API key for AI-native persistent memory",
|
|
|
|
|
|
"prompt": "Honcho API key",
|
|
|
|
|
|
"url": "https://app.honcho.dev",
|
2026-03-09 17:59:30 -04:00
|
|
|
|
"tools": ["honcho_context"],
|
2026-02-25 19:34:25 -05:00
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "tool",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-23 23:25:38 +00:00
|
|
|
|
# ── Messaging platforms ──
|
2026-02-03 10:46:23 -08:00
|
|
|
|
"TELEGRAM_BOT_TOKEN": {
|
|
|
|
|
|
"description": "Telegram bot token from @BotFather",
|
|
|
|
|
|
"prompt": "Telegram bot token",
|
|
|
|
|
|
"url": "https://t.me/BotFather",
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"TELEGRAM_ALLOWED_USERS": {
|
|
|
|
|
|
"description": "Comma-separated Telegram user IDs allowed to use the bot (get ID from @userinfobot)",
|
|
|
|
|
|
"prompt": "Allowed Telegram user IDs (comma-separated)",
|
|
|
|
|
|
"url": "https://t.me/userinfobot",
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"DISCORD_BOT_TOKEN": {
|
|
|
|
|
|
"description": "Discord bot token from Developer Portal",
|
|
|
|
|
|
"prompt": "Discord bot token",
|
|
|
|
|
|
"url": "https://discord.com/developers/applications",
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"DISCORD_ALLOWED_USERS": {
|
|
|
|
|
|
"description": "Comma-separated Discord user IDs allowed to use the bot",
|
|
|
|
|
|
"prompt": "Allowed Discord user IDs (comma-separated)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"SLACK_BOT_TOKEN": {
|
2026-03-09 14:00:11 -07:00
|
|
|
|
"description": "Slack bot token (xoxb-). Get from OAuth & Permissions after installing your app. "
|
|
|
|
|
|
"Required scopes: chat:write, app_mentions:read, channels:history, groups:history, "
|
|
|
|
|
|
"im:history, im:read, im:write, users:read, files:write",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"prompt": "Slack Bot Token (xoxb-...)",
|
|
|
|
|
|
"url": "https://api.slack.com/apps",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"SLACK_APP_TOKEN": {
|
2026-03-09 14:00:11 -07:00
|
|
|
|
"description": "Slack app-level token (xapp-) for Socket Mode. Get from Basic Information → "
|
|
|
|
|
|
"App-Level Tokens. Also ensure Event Subscriptions include: message.im, "
|
|
|
|
|
|
"message.channels, message.groups, app_mention",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"prompt": "Slack App Token (xapp-...)",
|
|
|
|
|
|
"url": "https://api.slack.com/apps",
|
2026-02-12 10:05:08 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"GATEWAY_ALLOW_ALL_USERS": {
|
|
|
|
|
|
"description": "Allow all users to interact with messaging bots (true/false). Default: false.",
|
|
|
|
|
|
"prompt": "Allow all users (true/false)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
"advanced": True,
|
2026-02-12 10:05:08 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
|
|
|
|
|
|
# ── Agent settings ──
|
2026-02-03 10:46:23 -08:00
|
|
|
|
"MESSAGING_CWD": {
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"description": "Working directory for terminal commands via messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
"prompt": "Messaging working directory (default: home)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"SUDO_PASSWORD": {
|
|
|
|
|
|
"description": "Sudo password for terminal commands requiring root access",
|
|
|
|
|
|
"prompt": "Sudo password",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
2026-02-03 14:48:19 -08:00
|
|
|
|
"HERMES_MAX_ITERATIONS": {
|
docs: comprehensive AGENTS.md audit and corrections
Major fixes:
- Default model: claude-sonnet-4.6 → claude-opus-4.6
- max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description)
- chat() signature: chat(user_message, task_id) → chat(message)
- Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation()
- Removed async/await references (agent is entirely synchronous)
- KawaiiSpinner location: run_agent.py → agent/display.py
- NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY
- OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY
- check_for_missing_config() → check_config_version() + get_missing_env_vars()
- Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py)
- Venv path: venv/ → .venv/
- Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl
- process_command() location clarified (HermesCLI in cli.py, not commands.py)
- REQUIRED_ENV_VARS noted as intentionally empty
- _config_version noted as currently at version 5
New content:
- Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/
- Full gateway/ directory listing with all modules and platforms/
- Added honcho_integration/, scripts/, tests/ directories
- Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py
- CLI commands table: added 25+ missing commands (model, login, logout, whatsapp,
skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall,
sessions export/delete/prune/stats, config path/env-path/show)
- Gateway slash commands section with all 20+ commands
- Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway
- Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
|
|
|
|
"description": "Maximum tool-calling iterations per conversation (default: 90)",
|
2026-02-03 14:48:19 -08:00
|
|
|
|
"prompt": "Max iterations",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 14:48:19 -08:00
|
|
|
|
},
|
2026-02-28 00:05:58 -08:00
|
|
|
|
# HERMES_TOOL_PROGRESS and HERMES_TOOL_PROGRESS_MODE are deprecated —
|
|
|
|
|
|
# now configured via display.tool_progress in config.yaml (off|new|all|verbose).
|
|
|
|
|
|
# Gateway falls back to these env vars for backward compatibility.
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"HERMES_TOOL_PROGRESS": {
|
2026-02-28 00:05:58 -08:00
|
|
|
|
"description": "(deprecated) Use display.tool_progress in config.yaml instead",
|
|
|
|
|
|
"prompt": "Tool progress (deprecated — use config.yaml)",
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 14:54:43 -08:00
|
|
|
|
},
|
|
|
|
|
|
"HERMES_TOOL_PROGRESS_MODE": {
|
2026-02-28 00:05:58 -08:00
|
|
|
|
"description": "(deprecated) Use display.tool_progress in config.yaml instead",
|
|
|
|
|
|
"prompt": "Progress mode (deprecated — use config.yaml)",
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
},
|
2026-02-23 23:55:42 -08:00
|
|
|
|
"HERMES_PREFILL_MESSAGES_FILE": {
|
|
|
|
|
|
"description": "Path to JSON file with ephemeral prefill messages for few-shot priming",
|
|
|
|
|
|
"prompt": "Prefill messages file path",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "setting",
|
|
|
|
|
|
},
|
|
|
|
|
|
"HERMES_EPHEMERAL_SYSTEM_PROMPT": {
|
|
|
|
|
|
"description": "Ephemeral system prompt injected at API-call time (never persisted to sessions)",
|
|
|
|
|
|
"prompt": "Ephemeral system prompt",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "setting",
|
|
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_missing_env_vars(required_only: bool = False) -> List[Dict[str, Any]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Check which environment variables are missing.
|
|
|
|
|
|
|
|
|
|
|
|
Returns list of dicts with var info for missing variables.
|
|
|
|
|
|
"""
|
|
|
|
|
|
missing = []
|
|
|
|
|
|
|
|
|
|
|
|
# Check required vars
|
|
|
|
|
|
for var_name, info in REQUIRED_ENV_VARS.items():
|
|
|
|
|
|
if not get_env_value(var_name):
|
|
|
|
|
|
missing.append({"name": var_name, **info, "is_required": True})
|
|
|
|
|
|
|
|
|
|
|
|
# Check optional vars (if not required_only)
|
|
|
|
|
|
if not required_only:
|
|
|
|
|
|
for var_name, info in OPTIONAL_ENV_VARS.items():
|
|
|
|
|
|
if not get_env_value(var_name):
|
|
|
|
|
|
missing.append({"name": var_name, **info, "is_required": False})
|
|
|
|
|
|
|
|
|
|
|
|
return missing
|
|
|
|
|
|
|
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
def _set_nested(config: dict, dotted_key: str, value):
|
|
|
|
|
|
"""Set a value at an arbitrarily nested dotted key path.
|
|
|
|
|
|
|
|
|
|
|
|
Creates intermediate dicts as needed, e.g. ``_set_nested(c, "a.b.c", 1)``
|
|
|
|
|
|
ensures ``c["a"]["b"]["c"] == 1``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
parts = dotted_key.split(".")
|
|
|
|
|
|
current = config
|
|
|
|
|
|
for part in parts[:-1]:
|
|
|
|
|
|
if part not in current or not isinstance(current.get(part), dict):
|
|
|
|
|
|
current[part] = {}
|
|
|
|
|
|
current = current[part]
|
|
|
|
|
|
current[parts[-1]] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
def get_missing_config_fields() -> List[Dict[str, Any]]:
|
|
|
|
|
|
"""
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
Check which config fields are missing or outdated (recursive).
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
Walks the DEFAULT_CONFIG tree at arbitrary depth and reports any keys
|
|
|
|
|
|
present in defaults but absent from the user's loaded config.
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"""
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
missing = []
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
|
|
|
|
|
|
def _check(defaults: dict, current: dict, prefix: str = ""):
|
|
|
|
|
|
for key, default_value in defaults.items():
|
|
|
|
|
|
if key.startswith('_'):
|
|
|
|
|
|
continue
|
|
|
|
|
|
full_key = key if not prefix else f"{prefix}.{key}"
|
|
|
|
|
|
if key not in current:
|
|
|
|
|
|
missing.append({
|
|
|
|
|
|
"key": full_key,
|
|
|
|
|
|
"default": default_value,
|
|
|
|
|
|
"description": f"New config option: {full_key}",
|
|
|
|
|
|
})
|
|
|
|
|
|
elif isinstance(default_value, dict) and isinstance(current.get(key), dict):
|
|
|
|
|
|
_check(default_value, current[key], full_key)
|
|
|
|
|
|
|
|
|
|
|
|
_check(DEFAULT_CONFIG, config)
|
2026-02-02 19:39:23 -08:00
|
|
|
|
return missing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_config_version() -> Tuple[int, int]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Check config version.
|
|
|
|
|
|
|
|
|
|
|
|
Returns (current_version, latest_version).
|
|
|
|
|
|
"""
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
current = config.get("_config_version", 0)
|
|
|
|
|
|
latest = DEFAULT_CONFIG.get("_config_version", 1)
|
|
|
|
|
|
return current, latest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, Any]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Migrate config to latest version, prompting for new required fields.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
interactive: If True, prompt user for missing values
|
|
|
|
|
|
quiet: If True, suppress output
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict with migration results: {"env_added": [...], "config_added": [...], "warnings": [...]}
|
|
|
|
|
|
"""
|
|
|
|
|
|
results = {"env_added": [], "config_added": [], "warnings": []}
|
|
|
|
|
|
|
|
|
|
|
|
# Check config version
|
|
|
|
|
|
current_ver, latest_ver = check_config_version()
|
|
|
|
|
|
|
2026-02-28 00:05:58 -08:00
|
|
|
|
# ── Version 3 → 4: migrate tool progress from .env to config.yaml ──
|
|
|
|
|
|
if current_ver < 4:
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
display = config.get("display", {})
|
|
|
|
|
|
if not isinstance(display, dict):
|
|
|
|
|
|
display = {}
|
|
|
|
|
|
if "tool_progress" not in display:
|
|
|
|
|
|
old_enabled = get_env_value("HERMES_TOOL_PROGRESS")
|
|
|
|
|
|
old_mode = get_env_value("HERMES_TOOL_PROGRESS_MODE")
|
|
|
|
|
|
if old_enabled and old_enabled.lower() in ("false", "0", "no"):
|
|
|
|
|
|
display["tool_progress"] = "off"
|
|
|
|
|
|
results["config_added"].append("display.tool_progress=off (from HERMES_TOOL_PROGRESS=false)")
|
|
|
|
|
|
elif old_mode and old_mode.lower() in ("new", "all"):
|
|
|
|
|
|
display["tool_progress"] = old_mode.lower()
|
|
|
|
|
|
results["config_added"].append(f"display.tool_progress={old_mode.lower()} (from HERMES_TOOL_PROGRESS_MODE)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
display["tool_progress"] = "all"
|
|
|
|
|
|
results["config_added"].append("display.tool_progress=all (default)")
|
|
|
|
|
|
config["display"] = display
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
if not quiet:
|
|
|
|
|
|
print(f" ✓ Migrated tool progress to config.yaml: {display['tool_progress']}")
|
|
|
|
|
|
|
2026-03-03 11:57:18 +05:30
|
|
|
|
# ── Version 4 → 5: add timezone field ──
|
|
|
|
|
|
if current_ver < 5:
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
if "timezone" not in config:
|
2026-03-07 00:05:05 -08:00
|
|
|
|
old_tz = os.getenv("HERMES_TIMEZONE", "")
|
2026-03-03 11:57:18 +05:30
|
|
|
|
if old_tz and old_tz.strip():
|
|
|
|
|
|
config["timezone"] = old_tz.strip()
|
|
|
|
|
|
results["config_added"].append(f"timezone={old_tz.strip()} (from HERMES_TIMEZONE)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
config["timezone"] = ""
|
|
|
|
|
|
results["config_added"].append("timezone= (empty, uses server-local)")
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
if not quiet:
|
|
|
|
|
|
tz_display = config["timezone"] or "(server-local)"
|
|
|
|
|
|
print(f" ✓ Added timezone to config.yaml: {tz_display}")
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
if current_ver < latest_ver and not quiet:
|
|
|
|
|
|
print(f"Config version: {current_ver} → {latest_ver}")
|
|
|
|
|
|
|
|
|
|
|
|
# Check for missing required env vars
|
|
|
|
|
|
missing_env = get_missing_env_vars(required_only=True)
|
|
|
|
|
|
|
|
|
|
|
|
if missing_env and not quiet:
|
|
|
|
|
|
print("\n⚠️ Missing required environment variables:")
|
|
|
|
|
|
for var in missing_env:
|
|
|
|
|
|
print(f" • {var['name']}: {var['description']}")
|
|
|
|
|
|
|
|
|
|
|
|
if interactive and missing_env:
|
|
|
|
|
|
print("\nLet's configure them now:\n")
|
|
|
|
|
|
for var in missing_env:
|
|
|
|
|
|
if var.get("url"):
|
|
|
|
|
|
print(f" Get your key at: {var['url']}")
|
|
|
|
|
|
|
|
|
|
|
|
if var.get("password"):
|
|
|
|
|
|
import getpass
|
|
|
|
|
|
value = getpass.getpass(f" {var['prompt']}: ")
|
|
|
|
|
|
else:
|
|
|
|
|
|
value = input(f" {var['prompt']}: ").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if value:
|
|
|
|
|
|
save_env_value(var["name"], value)
|
|
|
|
|
|
results["env_added"].append(var["name"])
|
|
|
|
|
|
print(f" ✓ Saved {var['name']}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
results["warnings"].append(f"Skipped {var['name']} - some features may not work")
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
2026-02-15 21:53:59 -08:00
|
|
|
|
# Check for missing optional env vars and offer to configure interactively
|
|
|
|
|
|
# Skip "advanced" vars (like OPENAI_BASE_URL) -- those are for power users
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
missing_optional = get_missing_env_vars(required_only=False)
|
|
|
|
|
|
required_names = {v["name"] for v in missing_env} if missing_env else set()
|
2026-02-15 21:53:59 -08:00
|
|
|
|
missing_optional = [
|
|
|
|
|
|
v for v in missing_optional
|
|
|
|
|
|
if v["name"] not in required_names and not v.get("advanced")
|
|
|
|
|
|
]
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
|
2026-03-08 05:55:30 -07:00
|
|
|
|
# Only offer to configure env vars that are NEW since the user's previous version
|
|
|
|
|
|
new_var_names = set()
|
|
|
|
|
|
for ver in range(current_ver + 1, latest_ver + 1):
|
|
|
|
|
|
new_var_names.update(ENV_VARS_BY_VERSION.get(ver, []))
|
|
|
|
|
|
|
|
|
|
|
|
if new_var_names and interactive and not quiet:
|
|
|
|
|
|
new_and_unset = [
|
|
|
|
|
|
(name, OPTIONAL_ENV_VARS[name])
|
|
|
|
|
|
for name in sorted(new_var_names)
|
|
|
|
|
|
if not get_env_value(name) and name in OPTIONAL_ENV_VARS
|
|
|
|
|
|
]
|
|
|
|
|
|
if new_and_unset:
|
|
|
|
|
|
print(f"\n {len(new_and_unset)} new optional key(s) in this update:")
|
|
|
|
|
|
for name, info in new_and_unset:
|
|
|
|
|
|
print(f" • {name} — {info.get('description', '')}")
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
print()
|
2026-03-08 05:55:30 -07:00
|
|
|
|
try:
|
|
|
|
|
|
answer = input(" Configure new keys? [y/N]: ").strip().lower()
|
|
|
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
|
|
|
|
answer = "n"
|
|
|
|
|
|
|
|
|
|
|
|
if answer in ("y", "yes"):
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
print()
|
2026-03-08 05:55:30 -07:00
|
|
|
|
for name, info in new_and_unset:
|
|
|
|
|
|
if info.get("url"):
|
|
|
|
|
|
print(f" {info.get('description', name)}")
|
|
|
|
|
|
print(f" Get your key at: {info['url']}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" {info.get('description', name)}")
|
|
|
|
|
|
if info.get("password"):
|
|
|
|
|
|
import getpass
|
|
|
|
|
|
value = getpass.getpass(f" {info.get('prompt', name)} (Enter to skip): ")
|
|
|
|
|
|
else:
|
|
|
|
|
|
value = input(f" {info.get('prompt', name)} (Enter to skip): ").strip()
|
|
|
|
|
|
if value:
|
|
|
|
|
|
save_env_value(name, value)
|
|
|
|
|
|
results["env_added"].append(name)
|
|
|
|
|
|
print(f" ✓ Saved {name}")
|
|
|
|
|
|
print()
|
|
|
|
|
|
else:
|
2026-03-11 09:07:30 -07:00
|
|
|
|
print(" Set later with: hermes config set <key> <value>")
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# Check for missing config fields
|
|
|
|
|
|
missing_config = get_missing_config_fields()
|
|
|
|
|
|
|
|
|
|
|
|
if missing_config:
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
|
|
|
|
|
|
for field in missing_config:
|
|
|
|
|
|
key = field["key"]
|
|
|
|
|
|
default = field["default"]
|
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
_set_nested(config, key, default)
|
2026-02-02 19:39:23 -08:00
|
|
|
|
results["config_added"].append(key)
|
|
|
|
|
|
if not quiet:
|
|
|
|
|
|
print(f" ✓ Added {key} = {default}")
|
|
|
|
|
|
|
|
|
|
|
|
# Update version and save
|
|
|
|
|
|
config["_config_version"] = latest_ver
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
elif current_ver < latest_ver:
|
|
|
|
|
|
# Just update version
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
config["_config_version"] = latest_ver
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
def _deep_merge(base: dict, override: dict) -> dict:
|
|
|
|
|
|
"""Recursively merge *override* into *base*, preserving nested defaults.
|
|
|
|
|
|
|
|
|
|
|
|
Keys in *override* take precedence. If both values are dicts the merge
|
|
|
|
|
|
recurses, so a user who overrides only ``tts.elevenlabs.voice_id`` will
|
|
|
|
|
|
keep the default ``tts.elevenlabs.model_id`` intact.
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = base.copy()
|
|
|
|
|
|
for key, value in override.items():
|
|
|
|
|
|
if (
|
|
|
|
|
|
key in result
|
|
|
|
|
|
and isinstance(result[key], dict)
|
|
|
|
|
|
and isinstance(value, dict)
|
|
|
|
|
|
):
|
|
|
|
|
|
result[key] = _deep_merge(result[key], value)
|
|
|
|
|
|
else:
|
|
|
|
|
|
result[key] = value
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-07 21:01:23 -08:00
|
|
|
|
def _normalize_max_turns_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
|
"""Normalize legacy root-level max_turns into agent.max_turns."""
|
|
|
|
|
|
config = dict(config)
|
|
|
|
|
|
agent_config = dict(config.get("agent") or {})
|
|
|
|
|
|
|
|
|
|
|
|
if "max_turns" in config and "max_turns" not in agent_config:
|
|
|
|
|
|
agent_config["max_turns"] = config["max_turns"]
|
|
|
|
|
|
|
|
|
|
|
|
if "max_turns" not in agent_config:
|
|
|
|
|
|
agent_config["max_turns"] = DEFAULT_CONFIG["agent"]["max_turns"]
|
|
|
|
|
|
|
|
|
|
|
|
config["agent"] = agent_config
|
|
|
|
|
|
config.pop("max_turns", None)
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def load_config() -> Dict[str, Any]:
|
|
|
|
|
|
"""Load configuration from ~/.hermes/config.yaml."""
|
2026-02-16 00:33:45 -08:00
|
|
|
|
import copy
|
2026-03-14 08:05:30 -07:00
|
|
|
|
ensure_hermes_home()
|
2026-02-02 19:01:51 -08:00
|
|
|
|
config_path = get_config_path()
|
|
|
|
|
|
|
2026-02-16 00:33:45 -08:00
|
|
|
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
if config_path.exists():
|
|
|
|
|
|
try:
|
2026-03-05 17:04:33 -05:00
|
|
|
|
with open(config_path, encoding="utf-8") as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
|
user_config = yaml.safe_load(f) or {}
|
2026-03-05 17:04:33 -05:00
|
|
|
|
|
2026-03-07 21:01:23 -08:00
|
|
|
|
if "max_turns" in user_config:
|
|
|
|
|
|
agent_user_config = dict(user_config.get("agent") or {})
|
|
|
|
|
|
if agent_user_config.get("max_turns") is None:
|
|
|
|
|
|
agent_user_config["max_turns"] = user_config["max_turns"]
|
|
|
|
|
|
user_config["agent"] = agent_user_config
|
|
|
|
|
|
user_config.pop("max_turns", None)
|
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
config = _deep_merge(config, user_config)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Warning: Failed to load config: {e}")
|
|
|
|
|
|
|
2026-03-07 21:01:23 -08:00
|
|
|
|
return _normalize_max_turns_config(config)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
_SECURITY_COMMENT = """
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
# ── Security ──────────────────────────────────────────────────────────
|
|
|
|
|
|
# API keys, tokens, and passwords are redacted from tool output by default.
|
|
|
|
|
|
# Set to false to see full values (useful for debugging auth issues).
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
# tirith pre-exec scanning is enabled by default when the tirith binary
|
|
|
|
|
|
# is available. Configure via security.tirith_* keys or env vars
|
|
|
|
|
|
# (TIRITH_ENABLED, TIRITH_BIN, TIRITH_TIMEOUT, TIRITH_FAIL_OPEN).
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
#
|
|
|
|
|
|
# security:
|
|
|
|
|
|
# redact_secrets: false
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
# tirith_enabled: true
|
|
|
|
|
|
# tirith_path: "tirith"
|
|
|
|
|
|
# tirith_timeout: 5
|
|
|
|
|
|
# tirith_fail_open: true
|
|
|
|
|
|
"""
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
_FALLBACK_COMMENT = """
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
# ── Fallback Model ────────────────────────────────────────────────────
|
|
|
|
|
|
# Automatic provider failover when primary is unavailable.
|
|
|
|
|
|
# Uncomment and configure to enable. Triggers on rate limits (429),
|
|
|
|
|
|
# overload (529), service errors (503), or connection failures.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Supported providers:
|
|
|
|
|
|
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
|
|
|
|
|
# openai-codex (OAuth — hermes login) — OpenAI Codex
|
|
|
|
|
|
# nous (OAuth — hermes login) — Nous Portal
|
|
|
|
|
|
# zai (ZAI_API_KEY) — Z.AI / GLM
|
|
|
|
|
|
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
|
|
|
|
|
# minimax (MINIMAX_API_KEY) — MiniMax
|
|
|
|
|
|
# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China)
|
|
|
|
|
|
#
|
|
|
|
|
|
# For custom OpenAI-compatible endpoints, add base_url and api_key_env.
|
|
|
|
|
|
#
|
|
|
|
|
|
# fallback_model:
|
|
|
|
|
|
# provider: openrouter
|
|
|
|
|
|
# model: anthropic/claude-sonnet-4
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 01:12:49 -07:00
|
|
|
|
_COMMENTED_SECTIONS = """
|
|
|
|
|
|
# ── Security ──────────────────────────────────────────────────────────
|
|
|
|
|
|
# API keys, tokens, and passwords are redacted from tool output by default.
|
|
|
|
|
|
# Set to false to see full values (useful for debugging auth issues).
|
|
|
|
|
|
#
|
|
|
|
|
|
# security:
|
|
|
|
|
|
# redact_secrets: false
|
|
|
|
|
|
|
|
|
|
|
|
# ── Fallback Model ────────────────────────────────────────────────────
|
|
|
|
|
|
# Automatic provider failover when primary is unavailable.
|
2026-03-08 21:25:58 -07:00
|
|
|
|
# Uncomment and configure to enable. Triggers on rate limits (429),
|
|
|
|
|
|
# overload (529), service errors (503), or connection failures.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Supported providers:
|
|
|
|
|
|
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
2026-03-08 21:34:15 -07:00
|
|
|
|
# openai-codex (OAuth — hermes login) — OpenAI Codex
|
refactor: unified OAuth/API-key credential resolution for fallback
Split fallback provider handling into two clean registries:
_FALLBACK_API_KEY_PROVIDERS — env-var-based (openrouter, zai, kimi, minimax)
_FALLBACK_OAUTH_PROVIDERS — OAuth-based (openai-codex, nous)
New _resolve_fallback_credentials() method handles all three cases
(OAuth, API key, custom endpoint) and returns a uniform (key, url, mode)
tuple. _try_activate_fallback() is now just validation + client build.
Adds Nous Portal as a fallback provider — uses the same OAuth flow
as the primary provider (hermes login), returns chat_completions mode.
OAuth providers get credential refresh for free: the existing 401
retry handlers (_try_refresh_codex/nous_client_credentials) check
self.provider, which is set correctly after fallback activation.
4 new tests (nous activation, nous no-login, codex retained).
27 total fallback tests passing, 2548 full suite.
2026-03-08 21:44:48 -07:00
|
|
|
|
# nous (OAuth — hermes login) — Nous Portal
|
2026-03-08 21:25:58 -07:00
|
|
|
|
# zai (ZAI_API_KEY) — Z.AI / GLM
|
|
|
|
|
|
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
|
|
|
|
|
# minimax (MINIMAX_API_KEY) — MiniMax
|
|
|
|
|
|
# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China)
|
|
|
|
|
|
#
|
|
|
|
|
|
# For custom OpenAI-compatible endpoints, add base_url and api_key_env.
|
|
|
|
|
|
#
|
|
|
|
|
|
# fallback_model:
|
|
|
|
|
|
# provider: openrouter
|
|
|
|
|
|
# model: anthropic/claude-sonnet-4
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def save_config(config: Dict[str, Any]):
|
|
|
|
|
|
"""Save configuration to ~/.hermes/config.yaml."""
|
2026-03-08 18:55:09 +03:30
|
|
|
|
from utils import atomic_yaml_write
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
ensure_hermes_home()
|
|
|
|
|
|
config_path = get_config_path()
|
2026-03-07 21:01:23 -08:00
|
|
|
|
normalized = _normalize_max_turns_config(config)
|
2026-03-08 18:55:09 +03:30
|
|
|
|
|
|
|
|
|
|
# Build optional commented-out sections for features that are off by
|
|
|
|
|
|
# default or only relevant when explicitly configured.
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
parts = []
|
2026-03-08 18:55:09 +03:30
|
|
|
|
sec = normalized.get("security", {})
|
|
|
|
|
|
if not sec or sec.get("redact_secrets") is None:
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
parts.append(_SECURITY_COMMENT)
|
2026-03-08 18:55:09 +03:30
|
|
|
|
fb = normalized.get("fallback_model", {})
|
|
|
|
|
|
if not fb or not (fb.get("provider") and fb.get("model")):
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
parts.append(_FALLBACK_COMMENT)
|
2026-03-08 18:55:09 +03:30
|
|
|
|
|
|
|
|
|
|
atomic_yaml_write(
|
|
|
|
|
|
config_path,
|
|
|
|
|
|
normalized,
|
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
2026-03-11 14:20:32 +05:30
|
|
|
|
extra_content="".join(parts) if parts else None,
|
2026-03-08 18:55:09 +03:30
|
|
|
|
)
|
2026-03-09 02:19:32 -07:00
|
|
|
|
_secure_file(config_path)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_env() -> Dict[str, str]:
|
|
|
|
|
|
"""Load environment variables from ~/.hermes/.env."""
|
|
|
|
|
|
env_path = get_env_path()
|
|
|
|
|
|
env_vars = {}
|
|
|
|
|
|
|
|
|
|
|
|
if env_path.exists():
|
2026-03-02 22:26:21 -08:00
|
|
|
|
# On Windows, open() defaults to the system locale (cp1252) which can
|
|
|
|
|
|
# fail on UTF-8 .env files. Use explicit UTF-8 only on Windows.
|
|
|
|
|
|
open_kw = {"encoding": "utf-8", "errors": "replace"} if _IS_WINDOWS else {}
|
|
|
|
|
|
with open(env_path, **open_kw) as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
|
for line in f:
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if line and not line.startswith('#') and '=' in line:
|
|
|
|
|
|
key, _, value = line.partition('=')
|
|
|
|
|
|
env_vars[key.strip()] = value.strip().strip('"\'')
|
|
|
|
|
|
|
|
|
|
|
|
return env_vars
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_env_value(key: str, value: str):
|
|
|
|
|
|
"""Save or update a value in ~/.hermes/.env."""
|
2026-03-13 03:14:04 -07:00
|
|
|
|
if not _ENV_VAR_NAME_RE.match(key):
|
|
|
|
|
|
raise ValueError(f"Invalid environment variable name: {key!r}")
|
|
|
|
|
|
value = value.replace("\n", "").replace("\r", "")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
ensure_hermes_home()
|
|
|
|
|
|
env_path = get_env_path()
|
|
|
|
|
|
|
2026-03-02 22:26:21 -08:00
|
|
|
|
# On Windows, open() defaults to the system locale (cp1252) which can
|
|
|
|
|
|
# cause OSError errno 22 on UTF-8 .env files.
|
|
|
|
|
|
read_kw = {"encoding": "utf-8", "errors": "replace"} if _IS_WINDOWS else {}
|
|
|
|
|
|
write_kw = {"encoding": "utf-8"} if _IS_WINDOWS else {}
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
lines = []
|
|
|
|
|
|
if env_path.exists():
|
2026-03-02 22:26:21 -08:00
|
|
|
|
with open(env_path, **read_kw) as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
|
lines = f.readlines()
|
|
|
|
|
|
|
|
|
|
|
|
# Find and update or append
|
|
|
|
|
|
found = False
|
|
|
|
|
|
for i, line in enumerate(lines):
|
|
|
|
|
|
if line.strip().startswith(f"{key}="):
|
|
|
|
|
|
lines[i] = f"{key}={value}\n"
|
|
|
|
|
|
found = True
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not found:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
# Ensure there's a newline at the end of the file before appending
|
|
|
|
|
|
if lines and not lines[-1].endswith("\n"):
|
|
|
|
|
|
lines[-1] += "\n"
|
2026-02-02 19:01:51 -08:00
|
|
|
|
lines.append(f"{key}={value}\n")
|
|
|
|
|
|
|
2026-03-11 08:58:33 -07:00
|
|
|
|
fd, tmp_path = tempfile.mkstemp(dir=str(env_path.parent), suffix='.tmp', prefix='.env_')
|
|
|
|
|
|
try:
|
|
|
|
|
|
with os.fdopen(fd, 'w', **write_kw) as f:
|
|
|
|
|
|
f.writelines(lines)
|
|
|
|
|
|
f.flush()
|
|
|
|
|
|
os.fsync(f.fileno())
|
|
|
|
|
|
os.replace(tmp_path, env_path)
|
|
|
|
|
|
except BaseException:
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
raise
|
2026-03-09 02:19:32 -07:00
|
|
|
|
_secure_file(env_path)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-03-13 03:14:04 -07:00
|
|
|
|
os.environ[key] = value
|
|
|
|
|
|
|
2026-03-06 15:14:26 +03:00
|
|
|
|
# Restrict .env permissions to owner-only (contains API keys)
|
|
|
|
|
|
if not _IS_WINDOWS:
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-03-13 02:09:52 -07:00
|
|
|
|
def save_anthropic_oauth_token(value: str, save_fn=None):
|
|
|
|
|
|
"""Persist an Anthropic OAuth/setup token and clear the API-key slot."""
|
|
|
|
|
|
writer = save_fn or save_env_value
|
|
|
|
|
|
writer("ANTHROPIC_TOKEN", value)
|
|
|
|
|
|
writer("ANTHROPIC_API_KEY", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-14 19:38:55 -07:00
|
|
|
|
def use_anthropic_claude_code_credentials(save_fn=None):
|
|
|
|
|
|
"""Use Claude Code's own credential files instead of persisting env tokens."""
|
|
|
|
|
|
writer = save_fn or save_env_value
|
|
|
|
|
|
writer("ANTHROPIC_TOKEN", "")
|
|
|
|
|
|
writer("ANTHROPIC_API_KEY", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 02:09:52 -07:00
|
|
|
|
def save_anthropic_api_key(value: str, save_fn=None):
|
|
|
|
|
|
"""Persist an Anthropic API key and clear the OAuth/setup-token slot."""
|
|
|
|
|
|
writer = save_fn or save_env_value
|
|
|
|
|
|
writer("ANTHROPIC_API_KEY", value)
|
|
|
|
|
|
writer("ANTHROPIC_TOKEN", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 03:14:04 -07:00
|
|
|
|
def save_env_value_secure(key: str, value: str) -> Dict[str, Any]:
|
|
|
|
|
|
save_env_value(key, value)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"stored_as": key,
|
|
|
|
|
|
"validated": False,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def get_env_value(key: str) -> Optional[str]:
|
|
|
|
|
|
"""Get a value from ~/.hermes/.env or environment."""
|
|
|
|
|
|
# Check environment first
|
|
|
|
|
|
if key in os.environ:
|
|
|
|
|
|
return os.environ[key]
|
|
|
|
|
|
|
|
|
|
|
|
# Then check .env file
|
|
|
|
|
|
env_vars = load_env()
|
|
|
|
|
|
return env_vars.get(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config display
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def redact_key(key: str) -> str:
|
|
|
|
|
|
"""Redact an API key for display."""
|
|
|
|
|
|
if not key:
|
|
|
|
|
|
return color("(not set)", Colors.DIM)
|
|
|
|
|
|
if len(key) < 12:
|
|
|
|
|
|
return "***"
|
|
|
|
|
|
return key[:4] + "..." + key[-4:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def show_config():
|
|
|
|
|
|
"""Display current configuration."""
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.CYAN))
|
2026-02-20 21:25:04 -08:00
|
|
|
|
print(color("│ ⚕ Hermes Configuration │", Colors.CYAN))
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print(color("└─────────────────────────────────────────────────────────┘", Colors.CYAN))
|
|
|
|
|
|
|
|
|
|
|
|
# Paths
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Paths", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print(f" Config: {get_config_path()}")
|
|
|
|
|
|
print(f" Secrets: {get_env_path()}")
|
|
|
|
|
|
print(f" Install: {get_project_root()}")
|
|
|
|
|
|
|
|
|
|
|
|
# API Keys
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ API Keys", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
|
|
keys = [
|
|
|
|
|
|
("OPENROUTER_API_KEY", "OpenRouter"),
|
2026-02-23 23:21:33 +00:00
|
|
|
|
("VOICE_TOOLS_OPENAI_KEY", "OpenAI (STT/TTS)"),
|
2026-02-02 19:01:51 -08:00
|
|
|
|
("FIRECRAWL_API_KEY", "Firecrawl"),
|
|
|
|
|
|
("BROWSERBASE_API_KEY", "Browserbase"),
|
|
|
|
|
|
("FAL_KEY", "FAL"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for env_key, name in keys:
|
|
|
|
|
|
value = get_env_value(env_key)
|
|
|
|
|
|
print(f" {name:<14} {redact_key(value)}")
|
2026-03-13 02:09:52 -07:00
|
|
|
|
anthropic_value = get_env_value("ANTHROPIC_TOKEN") or get_env_value("ANTHROPIC_API_KEY")
|
|
|
|
|
|
print(f" {'Anthropic':<14} {redact_key(anthropic_value)}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
# Model settings
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Model", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print(f" Model: {config.get('model', 'not set')}")
|
2026-03-07 21:01:23 -08:00
|
|
|
|
print(f" Max turns: {config.get('agent', {}).get('max_turns', DEFAULT_CONFIG['agent']['max_turns'])}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print(f" Toolsets: {', '.join(config.get('toolsets', ['all']))}")
|
|
|
|
|
|
|
2026-03-11 05:53:21 -07:00
|
|
|
|
# Display
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Display", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
display = config.get('display', {})
|
|
|
|
|
|
print(f" Personality: {display.get('personality', 'kawaii')}")
|
|
|
|
|
|
print(f" Reasoning: {'on' if display.get('show_reasoning', False) else 'off'}")
|
|
|
|
|
|
print(f" Bell: {'on' if display.get('bell_on_complete', False) else 'off'}")
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
# Terminal
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Terminal", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
terminal = config.get('terminal', {})
|
|
|
|
|
|
print(f" Backend: {terminal.get('backend', 'local')}")
|
|
|
|
|
|
print(f" Working dir: {terminal.get('cwd', '.')}")
|
|
|
|
|
|
print(f" Timeout: {terminal.get('timeout', 60)}s")
|
|
|
|
|
|
|
|
|
|
|
|
if terminal.get('backend') == 'docker':
|
|
|
|
|
|
print(f" Docker image: {terminal.get('docker_image', 'python:3.11-slim')}")
|
2026-02-02 19:13:41 -08:00
|
|
|
|
elif terminal.get('backend') == 'singularity':
|
|
|
|
|
|
print(f" Image: {terminal.get('singularity_image', 'docker://python:3.11')}")
|
|
|
|
|
|
elif terminal.get('backend') == 'modal':
|
|
|
|
|
|
print(f" Modal image: {terminal.get('modal_image', 'python:3.11')}")
|
|
|
|
|
|
modal_token = get_env_value('MODAL_TOKEN_ID')
|
|
|
|
|
|
print(f" Modal token: {'configured' if modal_token else '(not set)'}")
|
2026-03-05 11:12:50 -08:00
|
|
|
|
elif terminal.get('backend') == 'daytona':
|
|
|
|
|
|
print(f" Daytona image: {terminal.get('daytona_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
|
|
|
|
|
|
daytona_key = get_env_value('DAYTONA_API_KEY')
|
|
|
|
|
|
print(f" API key: {'configured' if daytona_key else '(not set)'}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
elif terminal.get('backend') == 'ssh':
|
|
|
|
|
|
ssh_host = get_env_value('TERMINAL_SSH_HOST')
|
|
|
|
|
|
ssh_user = get_env_value('TERMINAL_SSH_USER')
|
|
|
|
|
|
print(f" SSH host: {ssh_host or '(not set)'}")
|
|
|
|
|
|
print(f" SSH user: {ssh_user or '(not set)'}")
|
|
|
|
|
|
|
2026-03-03 11:57:18 +05:30
|
|
|
|
# Timezone
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Timezone", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
tz = config.get('timezone', '')
|
|
|
|
|
|
if tz:
|
|
|
|
|
|
print(f" Timezone: {tz}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" Timezone: {color('(server-local)', Colors.DIM)}")
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
# Compression
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Context Compression", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
compression = config.get('compression', {})
|
|
|
|
|
|
enabled = compression.get('enabled', True)
|
|
|
|
|
|
print(f" Enabled: {'yes' if enabled else 'no'}")
|
|
|
|
|
|
if enabled:
|
2026-03-12 15:51:50 -07:00
|
|
|
|
print(f" Threshold: {compression.get('threshold', 0.50) * 100:.0f}%")
|
2026-02-08 10:49:24 +00:00
|
|
|
|
print(f" Model: {compression.get('summary_model', 'google/gemini-3-flash-preview')}")
|
2026-03-07 08:52:06 -08:00
|
|
|
|
comp_provider = compression.get('summary_provider', 'auto')
|
|
|
|
|
|
if comp_provider != 'auto':
|
|
|
|
|
|
print(f" Provider: {comp_provider}")
|
|
|
|
|
|
|
|
|
|
|
|
# Auxiliary models
|
|
|
|
|
|
auxiliary = config.get('auxiliary', {})
|
|
|
|
|
|
aux_tasks = {
|
|
|
|
|
|
"Vision": auxiliary.get('vision', {}),
|
|
|
|
|
|
"Web extract": auxiliary.get('web_extract', {}),
|
|
|
|
|
|
}
|
|
|
|
|
|
has_overrides = any(
|
|
|
|
|
|
t.get('provider', 'auto') != 'auto' or t.get('model', '')
|
|
|
|
|
|
for t in aux_tasks.values()
|
|
|
|
|
|
)
|
|
|
|
|
|
if has_overrides:
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Auxiliary Models (overrides)", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
for label, task_cfg in aux_tasks.items():
|
|
|
|
|
|
prov = task_cfg.get('provider', 'auto')
|
|
|
|
|
|
mdl = task_cfg.get('model', '')
|
|
|
|
|
|
if prov != 'auto' or mdl:
|
|
|
|
|
|
parts = [f"provider={prov}"]
|
|
|
|
|
|
if mdl:
|
|
|
|
|
|
parts.append(f"model={mdl}")
|
|
|
|
|
|
print(f" {label:12s} {', '.join(parts)}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
# Messaging
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Messaging Platforms", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
|
|
telegram_token = get_env_value('TELEGRAM_BOT_TOKEN')
|
|
|
|
|
|
discord_token = get_env_value('DISCORD_BOT_TOKEN')
|
|
|
|
|
|
|
|
|
|
|
|
print(f" Telegram: {'configured' if telegram_token else color('not configured', Colors.DIM)}")
|
|
|
|
|
|
print(f" Discord: {'configured' if discord_token else color('not configured', Colors.DIM)}")
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("─" * 60, Colors.DIM))
|
|
|
|
|
|
print(color(" hermes config edit # Edit config file", Colors.DIM))
|
2026-03-11 09:07:30 -07:00
|
|
|
|
print(color(" hermes config set <key> <value>", Colors.DIM))
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print(color(" hermes setup # Run setup wizard", Colors.DIM))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def edit_config():
|
|
|
|
|
|
"""Open config file in user's editor."""
|
|
|
|
|
|
config_path = get_config_path()
|
|
|
|
|
|
|
|
|
|
|
|
# Ensure config exists
|
|
|
|
|
|
if not config_path.exists():
|
|
|
|
|
|
save_config(DEFAULT_CONFIG)
|
|
|
|
|
|
print(f"Created {config_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Find editor
|
|
|
|
|
|
editor = os.getenv('EDITOR') or os.getenv('VISUAL')
|
|
|
|
|
|
|
|
|
|
|
|
if not editor:
|
|
|
|
|
|
# Try common editors
|
|
|
|
|
|
for cmd in ['nano', 'vim', 'vi', 'code', 'notepad']:
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
if shutil.which(cmd):
|
|
|
|
|
|
editor = cmd
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not editor:
|
2026-03-13 03:14:04 -07:00
|
|
|
|
print("No editor found. Config file is at:")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print(f" {config_path}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Opening {config_path} in {editor}...")
|
|
|
|
|
|
subprocess.run([editor, str(config_path)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_config_value(key: str, value: str):
|
|
|
|
|
|
"""Set a configuration value."""
|
|
|
|
|
|
# Check if it's an API key (goes to .env)
|
|
|
|
|
|
api_keys = [
|
2026-03-06 08:45:35 +01:00
|
|
|
|
'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'VOICE_TOOLS_OPENAI_KEY',
|
2026-03-05 16:16:18 -06:00
|
|
|
|
'FIRECRAWL_API_KEY', 'FIRECRAWL_API_URL', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID',
|
2026-02-02 19:01:51 -08:00
|
|
|
|
'FAL_KEY', 'TELEGRAM_BOT_TOKEN', 'DISCORD_BOT_TOKEN',
|
|
|
|
|
|
'TERMINAL_SSH_HOST', 'TERMINAL_SSH_USER', 'TERMINAL_SSH_KEY',
|
2026-02-16 00:33:45 -08:00
|
|
|
|
'SUDO_PASSWORD', 'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN',
|
2026-03-08 17:45:38 -07:00
|
|
|
|
'GITHUB_TOKEN', 'HONCHO_API_KEY', 'WANDB_API_KEY',
|
2026-03-06 08:45:35 +01:00
|
|
|
|
'TINKER_API_KEY',
|
2026-02-02 19:01:51 -08:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-03-06 08:45:35 +01:00
|
|
|
|
if key.upper() in api_keys or key.upper().endswith('_API_KEY') or key.upper().endswith('_TOKEN') or key.upper().startswith('TERMINAL_SSH'):
|
2026-02-02 19:01:51 -08:00
|
|
|
|
save_env_value(key.upper(), value)
|
|
|
|
|
|
print(f"✓ Set {key} in {get_env_path()}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Otherwise it goes to config.yaml
|
2026-02-16 00:33:45 -08:00
|
|
|
|
# Read the raw user config (not merged with defaults) to avoid
|
|
|
|
|
|
# dumping all default values back to the file
|
|
|
|
|
|
config_path = get_config_path()
|
|
|
|
|
|
user_config = {}
|
|
|
|
|
|
if config_path.exists():
|
|
|
|
|
|
try:
|
2026-03-05 17:04:33 -05:00
|
|
|
|
with open(config_path, encoding="utf-8") as f:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
user_config = yaml.safe_load(f) or {}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
user_config = {}
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-02-16 00:33:45 -08:00
|
|
|
|
# Handle nested keys (e.g., "tts.provider")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
parts = key.split('.')
|
2026-02-16 00:33:45 -08:00
|
|
|
|
current = user_config
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
for part in parts[:-1]:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
if part not in current or not isinstance(current.get(part), dict):
|
2026-02-02 19:01:51 -08:00
|
|
|
|
current[part] = {}
|
|
|
|
|
|
current = current[part]
|
|
|
|
|
|
|
|
|
|
|
|
# Convert value to appropriate type
|
|
|
|
|
|
if value.lower() in ('true', 'yes', 'on'):
|
|
|
|
|
|
value = True
|
|
|
|
|
|
elif value.lower() in ('false', 'no', 'off'):
|
|
|
|
|
|
value = False
|
|
|
|
|
|
elif value.isdigit():
|
|
|
|
|
|
value = int(value)
|
|
|
|
|
|
elif value.replace('.', '', 1).isdigit():
|
|
|
|
|
|
value = float(value)
|
|
|
|
|
|
|
|
|
|
|
|
current[parts[-1]] = value
|
2026-02-16 00:33:45 -08:00
|
|
|
|
|
|
|
|
|
|
# Write only user config back (not the full merged defaults)
|
|
|
|
|
|
ensure_hermes_home()
|
2026-03-05 17:04:33 -05:00
|
|
|
|
with open(config_path, 'w', encoding="utf-8") as f:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
yaml.dump(user_config, f, default_flow_style=False, sort_keys=False)
|
|
|
|
|
|
|
2026-02-26 20:02:46 -08:00
|
|
|
|
# Keep .env in sync for keys that terminal_tool reads directly from env vars.
|
|
|
|
|
|
# config.yaml is authoritative, but terminal_tool only reads TERMINAL_ENV etc.
|
|
|
|
|
|
_config_to_env_sync = {
|
|
|
|
|
|
"terminal.backend": "TERMINAL_ENV",
|
|
|
|
|
|
"terminal.docker_image": "TERMINAL_DOCKER_IMAGE",
|
|
|
|
|
|
"terminal.singularity_image": "TERMINAL_SINGULARITY_IMAGE",
|
|
|
|
|
|
"terminal.modal_image": "TERMINAL_MODAL_IMAGE",
|
2026-03-05 00:42:05 -08:00
|
|
|
|
"terminal.daytona_image": "TERMINAL_DAYTONA_IMAGE",
|
2026-03-16 05:19:43 -07:00
|
|
|
|
"terminal.docker_mount_cwd_to_workspace": "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE",
|
2026-02-26 20:02:46 -08:00
|
|
|
|
"terminal.cwd": "TERMINAL_CWD",
|
|
|
|
|
|
"terminal.timeout": "TERMINAL_TIMEOUT",
|
2026-03-08 01:33:46 -08:00
|
|
|
|
"terminal.sandbox_dir": "TERMINAL_SANDBOX_DIR",
|
2026-03-15 20:17:13 -07:00
|
|
|
|
"terminal.persistent_shell": "TERMINAL_PERSISTENT_SHELL",
|
2026-02-26 20:02:46 -08:00
|
|
|
|
}
|
|
|
|
|
|
if key in _config_to_env_sync:
|
|
|
|
|
|
save_env_value(_config_to_env_sync[key], str(value))
|
|
|
|
|
|
|
2026-02-16 00:33:45 -08:00
|
|
|
|
print(f"✓ Set {key} = {value} in {config_path}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Command handler
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def config_command(args):
|
|
|
|
|
|
"""Handle config subcommands."""
|
|
|
|
|
|
subcmd = getattr(args, 'config_command', None)
|
|
|
|
|
|
|
|
|
|
|
|
if subcmd is None or subcmd == "show":
|
|
|
|
|
|
show_config()
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "edit":
|
|
|
|
|
|
edit_config()
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "set":
|
|
|
|
|
|
key = getattr(args, 'key', None)
|
|
|
|
|
|
value = getattr(args, 'value', None)
|
|
|
|
|
|
if not key or not value:
|
2026-03-11 09:07:30 -07:00
|
|
|
|
print("Usage: hermes config set <key> <value>")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print()
|
|
|
|
|
|
print("Examples:")
|
|
|
|
|
|
print(" hermes config set model anthropic/claude-sonnet-4")
|
|
|
|
|
|
print(" hermes config set terminal.backend docker")
|
|
|
|
|
|
print(" hermes config set OPENROUTER_API_KEY sk-or-...")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
set_config_value(key, value)
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "path":
|
|
|
|
|
|
print(get_config_path())
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "env-path":
|
|
|
|
|
|
print(get_env_path())
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
elif subcmd == "migrate":
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("🔄 Checking configuration for updates...", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Check what's missing
|
|
|
|
|
|
missing_env = get_missing_env_vars(required_only=False)
|
|
|
|
|
|
missing_config = get_missing_config_fields()
|
|
|
|
|
|
current_ver, latest_ver = check_config_version()
|
|
|
|
|
|
|
|
|
|
|
|
if not missing_env and not missing_config and current_ver >= latest_ver:
|
|
|
|
|
|
print(color("✓ Configuration is up to date!", Colors.GREEN))
|
|
|
|
|
|
print()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Show what needs to be updated
|
|
|
|
|
|
if current_ver < latest_ver:
|
|
|
|
|
|
print(f" Config version: {current_ver} → {latest_ver}")
|
|
|
|
|
|
|
|
|
|
|
|
if missing_config:
|
|
|
|
|
|
print(f"\n {len(missing_config)} new config option(s) will be added with defaults")
|
|
|
|
|
|
|
|
|
|
|
|
required_missing = [v for v in missing_env if v.get("is_required")]
|
2026-02-15 21:53:59 -08:00
|
|
|
|
optional_missing = [
|
|
|
|
|
|
v for v in missing_env
|
|
|
|
|
|
if not v.get("is_required") and not v.get("advanced")
|
|
|
|
|
|
]
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
|
|
|
|
|
if required_missing:
|
|
|
|
|
|
print(f"\n ⚠️ {len(required_missing)} required API key(s) missing:")
|
|
|
|
|
|
for var in required_missing:
|
|
|
|
|
|
print(f" • {var['name']}")
|
|
|
|
|
|
|
|
|
|
|
|
if optional_missing:
|
|
|
|
|
|
print(f"\n ℹ️ {len(optional_missing)} optional API key(s) not configured:")
|
|
|
|
|
|
for var in optional_missing:
|
|
|
|
|
|
tools = var.get("tools", [])
|
|
|
|
|
|
tools_str = f" (enables: {', '.join(tools[:2])})" if tools else ""
|
|
|
|
|
|
print(f" • {var['name']}{tools_str}")
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Run migration
|
|
|
|
|
|
results = migrate_config(interactive=True, quiet=False)
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
if results["env_added"] or results["config_added"]:
|
|
|
|
|
|
print(color("✓ Configuration updated!", Colors.GREEN))
|
|
|
|
|
|
|
|
|
|
|
|
if results["warnings"]:
|
|
|
|
|
|
print()
|
|
|
|
|
|
for warning in results["warnings"]:
|
|
|
|
|
|
print(color(f" ⚠️ {warning}", Colors.YELLOW))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "check":
|
|
|
|
|
|
# Non-interactive check for what's missing
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("📋 Configuration Status", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
current_ver, latest_ver = check_config_version()
|
|
|
|
|
|
if current_ver >= latest_ver:
|
|
|
|
|
|
print(f" Config version: {current_ver} ✓")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(color(f" Config version: {current_ver} → {latest_ver} (update available)", Colors.YELLOW))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color(" Required:", Colors.BOLD))
|
|
|
|
|
|
for var_name in REQUIRED_ENV_VARS:
|
|
|
|
|
|
if get_env_value(var_name):
|
|
|
|
|
|
print(f" ✓ {var_name}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(color(f" ✗ {var_name} (missing)", Colors.RED))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color(" Optional:", Colors.BOLD))
|
|
|
|
|
|
for var_name, info in OPTIONAL_ENV_VARS.items():
|
|
|
|
|
|
if get_env_value(var_name):
|
|
|
|
|
|
print(f" ✓ {var_name}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
tools = info.get("tools", [])
|
|
|
|
|
|
tools_str = f" → {', '.join(tools[:2])}" if tools else ""
|
|
|
|
|
|
print(color(f" ○ {var_name}{tools_str}", Colors.DIM))
|
|
|
|
|
|
|
|
|
|
|
|
missing_config = get_missing_config_fields()
|
|
|
|
|
|
if missing_config:
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color(f" {len(missing_config)} new config option(s) available", Colors.YELLOW))
|
2026-03-13 03:14:04 -07:00
|
|
|
|
print(" Run 'hermes config migrate' to add them")
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
else:
|
|
|
|
|
|
print(f"Unknown config command: {subcmd}")
|
2026-02-02 19:39:23 -08:00
|
|
|
|
print()
|
|
|
|
|
|
print("Available commands:")
|
|
|
|
|
|
print(" hermes config Show current configuration")
|
|
|
|
|
|
print(" hermes config edit Open config in editor")
|
2026-03-14 10:35:14 -07:00
|
|
|
|
print(" hermes config set <key> <value> Set a config value")
|
2026-02-02 19:39:23 -08:00
|
|
|
|
print(" hermes config check Check for missing/outdated config")
|
|
|
|
|
|
print(" hermes config migrate Update config with new options")
|
|
|
|
|
|
print(" hermes config path Show config file path")
|
|
|
|
|
|
print(" hermes config env-path Show .env file path")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
sys.exit(1)
|