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-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-17 01:13:34 -07:00
|
|
|
|
# Env var names written to .env that aren't in OPTIONAL_ENV_VARS
|
|
|
|
|
|
# (managed by setup/provider flows directly).
|
|
|
|
|
|
_EXTRA_ENV_KEYS = frozenset({
|
|
|
|
|
|
"OPENAI_API_KEY", "OPENAI_BASE_URL",
|
|
|
|
|
|
"ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN",
|
|
|
|
|
|
"AUXILIARY_VISION_MODEL",
|
|
|
|
|
|
"DISCORD_HOME_CHANNEL", "TELEGRAM_HOME_CHANNEL",
|
|
|
|
|
|
"SIGNAL_ACCOUNT", "SIGNAL_HTTP_URL",
|
|
|
|
|
|
"SIGNAL_ALLOWED_USERS", "SIGNAL_GROUP_ALLOWED_USERS",
|
2026-03-17 03:04:58 -07:00
|
|
|
|
"DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_SECRET",
|
2026-03-17 01:13:34 -07:00
|
|
|
|
"TERMINAL_ENV", "TERMINAL_SSH_KEY", "TERMINAL_SSH_PORT",
|
|
|
|
|
|
"WHATSAPP_MODE", "WHATSAPP_ENABLED",
|
feat: register Mattermost and Matrix env vars in OPTIONAL_ENV_VARS
Adds both platforms to the config system so hermes setup, hermes doctor,
and hermes config properly discover and manage their env vars.
- MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS
- MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_USER_ID, MATRIX_ALLOWED_USERS
- Extra env keys for .env sanitizer: MATTERMOST_HOME_CHANNEL,
MATTERMOST_REPLY_MODE, MATRIX_PASSWORD, MATRIX_ENCRYPTION, MATRIX_HOME_ROOM
2026-03-17 03:11:54 -07:00
|
|
|
|
"MATTERMOST_HOME_CHANNEL", "MATTERMOST_REPLY_MODE",
|
|
|
|
|
|
"MATRIX_PASSWORD", "MATRIX_ENCRYPTION", "MATRIX_HOME_ROOM",
|
2026-03-17 01:13:34 -07:00
|
|
|
|
})
|
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
|
|
|
|
|
|
|
|
|
|
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Managed mode (NixOS declarative config)
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def is_managed() -> bool:
|
|
|
|
|
|
"""Check if hermes is running in Nix-managed mode.
|
|
|
|
|
|
|
|
|
|
|
|
Two signals: the HERMES_MANAGED env var (set by the systemd service),
|
|
|
|
|
|
or a .managed marker file in HERMES_HOME (set by the NixOS activation
|
|
|
|
|
|
script, so interactive shells also see it).
|
|
|
|
|
|
"""
|
|
|
|
|
|
if os.getenv("HERMES_MANAGED", "").lower() in ("true", "1", "yes"):
|
|
|
|
|
|
return True
|
refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)
Centralizes two widely-duplicated patterns into hermes_constants.py:
1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
- Was copy-pasted inline across 30+ files as:
Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- Now defined once in hermes_constants.py (zero-dependency module)
- hermes_cli/config.py re-exports it for backward compatibility
- Removed local wrapper functions in honcho_integration/client.py,
tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py
2. parse_reasoning_effort() — Reasoning effort string validation
- Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
- Same validation logic: check against (xhigh, high, medium, low, minimal, none)
- Now defined once in hermes_constants.py, called from all 3 locations
- Warning log for unknown values kept at call sites (context-specific)
31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
2026-03-25 15:54:28 -07:00
|
|
|
|
managed_marker = get_hermes_home() / ".managed"
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
return managed_marker.exists()
|
|
|
|
|
|
|
|
|
|
|
|
def managed_error(action: str = "modify configuration"):
|
|
|
|
|
|
"""Print user-friendly error for managed mode."""
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"Cannot {action}: configuration is managed by NixOS (HERMES_MANAGED=true).\n"
|
|
|
|
|
|
"Edit services.hermes-agent.settings in your configuration.nix and run:\n"
|
|
|
|
|
|
" sudo nixos-rebuild switch",
|
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config paths
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)
Centralizes two widely-duplicated patterns into hermes_constants.py:
1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
- Was copy-pasted inline across 30+ files as:
Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- Now defined once in hermes_constants.py (zero-dependency module)
- hermes_cli/config.py re-exports it for backward compatibility
- Removed local wrapper functions in honcho_integration/client.py,
tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py
2. parse_reasoning_effort() — Reasoning effort string validation
- Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
- Same validation logic: check against (xhigh, high, medium, low, minimal, none)
- Now defined once in hermes_constants.py, called from all 3 locations
- Warning log for unknown values kept at call sites (context-specific)
31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
2026-03-25 15:54:28 -07:00
|
|
|
|
# Re-export from hermes_constants — canonical definition lives there.
|
|
|
|
|
|
from hermes_constants import get_hermes_home # noqa: F811,E402
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
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,
|
feat: env var passthrough for skills and user config (#2807)
* feat: env var passthrough for skills and user config
Skills that declare required_environment_variables now have those vars
passed through to sandboxed execution environments (execute_code and
terminal). Previously, execute_code stripped all vars containing KEY,
TOKEN, SECRET, etc. and the terminal blocklist removed Hermes
infrastructure vars — both blocked skill-declared env vars.
Two passthrough sources:
1. Skill-scoped (automatic): when a skill is loaded via skill_view and
declares required_environment_variables, vars that are present in
the environment are registered in a session-scoped passthrough set.
2. Config-based (manual): terminal.env_passthrough in config.yaml lets
users explicitly allowlist vars for non-skill use cases.
Changes:
- New module: tools/env_passthrough.py — shared passthrough registry
- hermes_cli/config.py: add terminal.env_passthrough to DEFAULT_CONFIG
- tools/skills_tool.py: register available skill env vars on load
- tools/code_execution_tool.py: check passthrough before filtering
- tools/environments/local.py: check passthrough in _sanitize_subprocess_env
and _make_run_env
- 19 new tests covering all layers
* docs: add environment variable passthrough documentation
Document the env var passthrough feature across four docs pages:
- security.md: new 'Environment Variable Passthrough' section with
full explanation, comparison table, and security considerations
- code-execution.md: update security section, add passthrough subsection,
fix comparison table
- creating-skills.md: add tip about automatic sandbox passthrough
- skills.md: add note about passthrough after secure setup docs
Live-tested: launched interactive CLI, loaded a skill with
required_environment_variables, verified TEST_SKILL_SECRET_KEY was
accessible inside execute_code sandbox (value: passthrough-test-value-42).
2026-03-24 08:19:34 -07:00
|
|
|
|
# Environment variables to pass through to sandboxed execution
|
|
|
|
|
|
# (terminal and execute_code). Skill-declared required_environment_variables
|
|
|
|
|
|
# are passed through automatically; this list is for non-skill use cases.
|
|
|
|
|
|
"env_passthrough": [],
|
2026-02-02 19:13:41 -08:00
|
|
|
|
"docker_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
2026-03-17 02:34:25 -07:00
|
|
|
|
"docker_forward_env": [],
|
2026-02-02 19:13:41 -08:00
|
|
|
|
"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,
|
2026-03-24 07:21:50 -07:00
|
|
|
|
"command_timeout": 30, # Timeout for browser commands in seconds (screenshot, navigate, etc.)
|
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-24 18:48:04 -07:00
|
|
|
|
"threshold": 0.50, # compress when context usage exceeds this ratio
|
|
|
|
|
|
"target_ratio": 0.20, # fraction of threshold to preserve as recent tail
|
2026-03-24 18:05:43 -07:00
|
|
|
|
"protect_last_n": 20, # minimum recent messages to keep uncompressed
|
|
|
|
|
|
"summary_model": "", # empty = use main configured model
|
2026-03-07 08:52:06 -08:00
|
|
|
|
"summary_provider": "auto",
|
2026-03-17 04:46:15 -07:00
|
|
|
|
"summary_base_url": None,
|
2026-03-07 08:52:06 -08:00
|
|
|
|
},
|
fix: hermes update causes dual gateways on macOS (launchd) (#1567)
* feat: add optional smart model routing
Add a conservative cheap-vs-strong routing option that can send very short/simple turns to a cheaper model across providers while keeping the primary model for complex work. Wire it through CLI, gateway, and cron, and document the config.yaml workflow.
* fix(gateway): remove recursive ExecStop from systemd units, extend TimeoutStopSec to 60s
* fix(gateway): avoid recursive ExecStop in user systemd unit
* fix: extend ExecStop removal and TimeoutStopSec=60 to system unit
The cherry-picked PR #1448 fix only covered the user systemd unit.
The system unit had the same TimeoutStopSec=15 and could benefit
from the same 60s timeout for clean shutdown. Also adds a regression
test for the system unit.
---------
Co-authored-by: Ninja <ninja@local>
* feat(skills): add blender-mcp optional skill for 3D modeling
Control a running Blender instance from Hermes via socket connection
to the blender-mcp addon (port 9876). Supports creating 3D objects,
materials, animations, and running arbitrary bpy code.
Placed in optional-skills/ since it requires Blender 4.3+ desktop
with a third-party addon manually started each session.
* feat(acp): support slash commands in ACP adapter (#1532)
Adds /help, /model, /tools, /context, /reset, /compact, /version
to the ACP adapter (VS Code, Zed, JetBrains). Commands are handled
directly in the server without instantiating the TUI — each command
queries agent/session state and returns plain text.
Unrecognized /commands fall through to the LLM as normal messages.
/model uses detect_provider_for_model() for auto-detection when
switching models, matching the CLI and gateway behavior.
Fixes #1402
* fix(logging): improve error logging in session search tool (#1533)
* fix(gateway): restart on retryable startup failures (#1517)
* feat(email): add skip_attachments option via config.yaml
* feat(email): add skip_attachments option via config.yaml
Adds a config.yaml-driven option to skip email attachments in the
gateway email adapter. Useful for malware protection and bandwidth
savings.
Configure in config.yaml:
platforms:
email:
skip_attachments: true
Based on PR #1521 by @an420eth, changed from env var to config.yaml
(via PlatformConfig.extra) to match the project's config-first pattern.
* docs: document skip_attachments option for email adapter
* fix(telegram): retry on transient TLS failures during connect and send
Add exponential-backoff retry (3 attempts) around initialize() to
handle transient TLS resets during gateway startup. Also catches
TimedOut and OSError in addition to NetworkError.
Add exponential-backoff retry (3 attempts) around send_message() for
NetworkError during message delivery, wrapping the existing Markdown
fallback logic.
Both imports are guarded with try/except ImportError for test
environments where telegram is mocked.
Based on PR #1527 by cmd8. Closes #1526.
* feat: permissive block_anchor thresholds and unicode normalization (#1539)
Salvaged from PR #1528 by an420eth. Closes #517.
Improves _strategy_block_anchor in fuzzy_match.py:
- Add unicode normalization (smart quotes, em/en-dashes, ellipsis,
non-breaking spaces → ASCII) so LLM-produced unicode artifacts
don't break anchor line matching
- Lower thresholds: 0.10 for unique matches (was 0.70), 0.30 for
multiple candidates — if first/last lines match exactly, the
block is almost certainly correct
- Use original (non-normalized) content for offset calculation to
preserve correct character positions
Tested: 3 new scenarios fixed (em-dash anchors, non-breaking space
anchors, very-low-similarity unique matches), zero regressions on
all 9 existing fuzzy match tests.
Co-authored-by: an420eth <an420eth@users.noreply.github.com>
* feat(cli): add file path autocomplete in the input prompt (#1545)
When typing a path-like token (./ ../ ~/ / or containing /),
the CLI now shows filesystem completions in the dropdown menu.
Directories show a trailing slash and 'dir' label; files show
their size. Completions are case-insensitive and capped at 30
entries.
Triggered by tokens like:
edit ./src/ma → shows ./src/main.py, ./src/manifest.json, ...
check ~/doc → shows ~/docs/, ~/documents/, ...
read /etc/hos → shows /etc/hosts, /etc/hostname, ...
open tools/reg → shows tools/registry.py
Slash command autocomplete (/help, /model, etc.) is unaffected —
it still triggers when the input starts with /.
Inspired by OpenCode PR #145 (file path completion menu).
Implementation:
- hermes_cli/commands.py: _extract_path_word() detects path-like
tokens, _path_completions() yields filesystem Completions with
size labels, get_completions() routes to paths vs slash commands
- tests/hermes_cli/test_path_completion.py: 26 tests covering
path extraction, prefix filtering, directory markers, home
expansion, case-insensitivity, integration with slash commands
* feat(privacy): redact PII from LLM context when privacy.redact_pii is enabled
Add privacy.redact_pii config option (boolean, default false). When
enabled, the gateway redacts personally identifiable information from
the system prompt before sending it to the LLM provider:
- Phone numbers (user IDs on WhatsApp/Signal) → hashed to user_<sha256>
- User IDs → hashed to user_<sha256>
- Chat IDs → numeric portion hashed, platform prefix preserved
- Home channel IDs → hashed
- Names/usernames → NOT affected (user-chosen, publicly visible)
Hashes are deterministic (same user → same hash) so the model can
still distinguish users in group chats. Routing and delivery use
the original values internally — redaction only affects LLM context.
Inspired by OpenClaw PR #47959.
* fix(privacy): skip PII redaction on Discord/Slack (mentions need real IDs)
Discord uses <@user_id> for mentions and Slack uses <@U12345> — the LLM
needs the real ID to tag users. Redaction now only applies to WhatsApp,
Signal, and Telegram where IDs are pure routing metadata.
Add 4 platform-specific tests covering Discord, WhatsApp, Signal, Slack.
* 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.
* feat: first-class plugin architecture + hide status bar cost by default (#1544)
The persistent status bar now shows context %, token counts, and
duration but NOT $ cost by default. Cost display is opt-in via:
display:
show_cost: true
in config.yaml, or: hermes config set display.show_cost true
The /usage command still shows full cost breakdown since the user
explicitly asked for it — this only affects the always-visible bar.
Status bar without cost:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ 15m
Status bar with show_cost: true:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ $0.06 │ 15m
* feat: improve memory prioritization + aggressive skill updates (inspired by OpenAI Codex)
* feat: improve memory prioritization — user preferences over procedural knowledge
Inspired by OpenAI Codex's memory prompt improvements (openai/codex#14493)
which focus memory writes on user preferences and recurring patterns
rather than procedural task details.
Key insight: 'Optimize for reducing future user steering — the most
valuable memory prevents the user from having to repeat themselves.'
Changes:
- MEMORY_GUIDANCE (prompt_builder.py): added prioritization hierarchy
and the core principle about reducing user steering
- MEMORY_SCHEMA (memory_tool.py): reordered WHEN TO SAVE list to put
corrections first, added explicit PRIORITY guidance
- Memory nudge (run_agent.py): now asks specifically about preferences,
corrections, and workflow patterns instead of generic 'anything'
- Memory flush (run_agent.py): now instructs to prioritize user
preferences and corrections over task-specific details
* feat: more aggressive skill creation and update prompting
Press harder on skill updates — the agent should proactively patch
skills when it encounters issues during use, not wait to be asked.
Changes:
- SKILLS_GUIDANCE: 'consider saving' → 'save'; added explicit instruction
to patch skills immediately when found outdated/wrong
- Skills header: added instruction to update loaded skills before finishing
if they had missing steps or wrong commands
- Skill nudge: more assertive ('save the approach' not 'consider saving'),
now also prompts for updating existing skills used in the task
- Skill nudge interval: lowered default from 15 to 10 iterations
- skill_manage schema: added 'patch it immediately' to update triggers
* feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
* fix: hermes update causes dual gateways on macOS (launchd)
Three bugs worked together to create the dual-gateway problem:
1. cmd_update only checked systemd for gateway restart, completely
ignoring launchd on macOS. After killing the PID it would print
'Restart it with: hermes gateway run' even when launchd was about
to auto-respawn the process.
2. launchd's KeepAlive.SuccessfulExit=false respawns the gateway
after SIGTERM (non-zero exit), so the user's manual restart
created a second instance.
3. The launchd plist lacked --replace (systemd had it), so the
respawned gateway didn't kill stale instances on startup.
Fixes:
- Add --replace to launchd ProgramArguments (matches systemd)
- Add launchd detection to cmd_update's auto-restart logic
- Print 'auto-restart via launchd' instead of manual restart hint
* fix: add launchd plist auto-refresh + explicit restart in cmd_update
Two integration issues with the initial fix:
1. Existing macOS users with old plist (no --replace) would never
get the fix until manual uninstall/reinstall. Added
refresh_launchd_plist_if_needed() — mirrors the existing
refresh_systemd_unit_if_needed(). Called from launchd_start(),
launchd_restart(), and cmd_update.
2. cmd_update relied on KeepAlive respawn after SIGTERM rather than
explicit launchctl stop/start. This caused races: launchd would
respawn the old process before the PID file was cleaned up.
Now does explicit stop+start (matching how systemd gets an
explicit systemctl restart), with plist refresh first so the
new --replace flag is picked up.
---------
Co-authored-by: Ninja <ninja@local>
Co-authored-by: alireza78a <alireza78a@users.noreply.github.com>
Co-authored-by: Oktay Aydin <113846926+aydnOktay@users.noreply.github.com>
Co-authored-by: JP Lew <polydegen@protonmail.com>
Co-authored-by: an420eth <an420eth@users.noreply.github.com>
2026-03-16 12:36:29 -07:00
|
|
|
|
"smart_model_routing": {
|
|
|
|
|
|
"enabled": False,
|
|
|
|
|
|
"max_simple_chars": 160,
|
|
|
|
|
|
"max_simple_words": 28,
|
|
|
|
|
|
"cheap_model": {},
|
|
|
|
|
|
},
|
2026-03-07 08:52:06 -08:00
|
|
|
|
|
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-22 05:28:24 -07:00
|
|
|
|
"timeout": 30, # seconds — increase for slow local vision models
|
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",
|
2026-03-26 17:58:40 -07:00
|
|
|
|
"busy_input_mode": "interrupt",
|
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
|
|
|
|
"bell_on_complete": False,
|
2026-03-11 05:53:21 -07:00
|
|
|
|
"show_reasoning": False,
|
2026-03-16 07:44:42 -07:00
|
|
|
|
"streaming": False,
|
2026-03-16 06:43:57 -07:00
|
|
|
|
"show_cost": False, # Show $ cost in the status bar (off by default)
|
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-03-26 14:41:04 -07:00
|
|
|
|
"tool_progress_command": False, # Enable /verbose command in messaging gateway
|
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": {
|
feat: add NeuTTS optional skill + local TTS provider backend
* feat(skills): add bundled neutts optional skill
Add NeuTTS optional skill with CLI scaffold, bootstrap helper, and
sample voice profile. Also fixes skills_hub.py to handle binary
assets (WAV files) during skill installation.
Changes:
- optional-skills/mlops/models/neutts/ — skill + CLI scaffold
- tools/skills_hub.py — binary asset support (read_bytes, write_bytes)
- tests/tools/test_skills_hub.py — regression tests for binary assets
* feat(tts): add NeuTTS as local TTS provider backend
Add NeuTTS as a fourth TTS provider option alongside Edge, ElevenLabs,
and OpenAI. NeuTTS runs fully on-device via neutts_cli — no API key
needed.
Provider behavior:
- Explicit: set tts.provider to 'neutts' in config.yaml
- Fallback: when Edge TTS is unavailable and neutts_cli is installed,
automatically falls back to NeuTTS instead of failing
- check_tts_requirements() now includes NeuTTS in availability checks
NeuTTS outputs WAV natively. For Telegram voice bubbles, ffmpeg
converts to Opus (same pattern as Edge TTS).
Changes:
- tools/tts_tool.py — _generate_neutts(), _check_neutts_available(),
provider dispatch, fallback logic, Opus conversion
- hermes_cli/config.py — tts.neutts config defaults
---------
Co-authored-by: unmodeled-tyler <unmodeled.tyler@proton.me>
2026-03-17 02:13:34 -07:00
|
|
|
|
"provider": "edge", # "edge" (free) | "elevenlabs" (premium) | "openai" | "neutts" (local)
|
2026-02-12 10:05:08 -08:00
|
|
|
|
"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
|
|
|
|
|
|
},
|
feat: add NeuTTS optional skill + local TTS provider backend
* feat(skills): add bundled neutts optional skill
Add NeuTTS optional skill with CLI scaffold, bootstrap helper, and
sample voice profile. Also fixes skills_hub.py to handle binary
assets (WAV files) during skill installation.
Changes:
- optional-skills/mlops/models/neutts/ — skill + CLI scaffold
- tools/skills_hub.py — binary asset support (read_bytes, write_bytes)
- tests/tools/test_skills_hub.py — regression tests for binary assets
* feat(tts): add NeuTTS as local TTS provider backend
Add NeuTTS as a fourth TTS provider option alongside Edge, ElevenLabs,
and OpenAI. NeuTTS runs fully on-device via neutts_cli — no API key
needed.
Provider behavior:
- Explicit: set tts.provider to 'neutts' in config.yaml
- Fallback: when Edge TTS is unavailable and neutts_cli is installed,
automatically falls back to NeuTTS instead of failing
- check_tts_requirements() now includes NeuTTS in availability checks
NeuTTS outputs WAV natively. For Telegram voice bubbles, ffmpeg
converts to Opus (same pattern as Edge TTS).
Changes:
- tools/tts_tool.py — _generate_neutts(), _check_neutts_available(),
provider dispatch, fallback logic, Opus conversion
- hermes_cli/config.py — tts.neutts config defaults
---------
Co-authored-by: unmodeled-tyler <unmodeled.tyler@proton.me>
2026-03-17 02:13:34 -07:00
|
|
|
|
"neutts": {
|
2026-03-17 02:33:12 -07:00
|
|
|
|
"ref_audio": "", # Path to reference voice audio (empty = bundled default)
|
|
|
|
|
|
"ref_text": "", # Path to reference voice transcript (empty = bundled default)
|
|
|
|
|
|
"model": "neuphonic/neutts-air-q4-gguf", # HuggingFace model repo
|
|
|
|
|
|
"device": "cpu", # cpu, cuda, or mps
|
feat: add NeuTTS optional skill + local TTS provider backend
* feat(skills): add bundled neutts optional skill
Add NeuTTS optional skill with CLI scaffold, bootstrap helper, and
sample voice profile. Also fixes skills_hub.py to handle binary
assets (WAV files) during skill installation.
Changes:
- optional-skills/mlops/models/neutts/ — skill + CLI scaffold
- tools/skills_hub.py — binary asset support (read_bytes, write_bytes)
- tests/tools/test_skills_hub.py — regression tests for binary assets
* feat(tts): add NeuTTS as local TTS provider backend
Add NeuTTS as a fourth TTS provider option alongside Edge, ElevenLabs,
and OpenAI. NeuTTS runs fully on-device via neutts_cli — no API key
needed.
Provider behavior:
- Explicit: set tts.provider to 'neutts' in config.yaml
- Fallback: when Edge TTS is unavailable and neutts_cli is installed,
automatically falls back to NeuTTS instead of failing
- check_tts_requirements() now includes NeuTTS in availability checks
NeuTTS outputs WAV natively. For Telegram voice bubbles, ffmpeg
converts to Opus (same pattern as Edge TTS).
Changes:
- tools/tts_tool.py — _generate_neutts(), _check_neutts_available(),
provider dispatch, fallback logic, Opus conversion
- hermes_cli/config.py — tts.neutts config defaults
---------
Co-authored-by: unmodeled-tyler <unmodeled.tyler@proton.me>
2026-03-17 02:13:34 -07:00
|
|
|
|
},
|
2026-02-12 10:05:08 -08: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
|
|
|
|
"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)
|
2026-03-25 11:29:49 -07:00
|
|
|
|
"max_iterations": 50, # per-subagent iteration cap (each subagent gets its own budget,
|
|
|
|
|
|
# independent of the parent's max_iterations)
|
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: OpenAI-compatible API server + WhatsApp configurable reply prefix (#1756)
* feat: OpenAI-compatible API server platform adapter
Salvaged from PR #956, updated for current main.
Adds an HTTP API server as a gateway platform adapter that exposes
hermes-agent via the OpenAI Chat Completions and Responses APIs.
Any OpenAI-compatible frontend (Open WebUI, LobeChat, LibreChat,
AnythingLLM, NextChat, ChatBox, etc.) can connect by pointing at
http://localhost:8642/v1.
Endpoints:
- POST /v1/chat/completions — stateless Chat Completions API
- POST /v1/responses — stateful Responses API with chaining
- GET /v1/responses/{id} — retrieve stored response
- DELETE /v1/responses/{id} — delete stored response
- GET /v1/models — list hermes-agent as available model
- GET /health — health check
Features:
- Real SSE streaming via stream_delta_callback (uses main's streaming)
- In-memory LRU response store for Responses API conversation chaining
- Named conversations via 'conversation' parameter
- Bearer token auth (optional, via API_SERVER_KEY)
- CORS support for browser-based frontends
- System prompt layering (frontend system messages on top of core)
- Real token usage tracking in responses
Integration points:
- Platform.API_SERVER in gateway/config.py
- _create_adapter() branch in gateway/run.py
- API_SERVER_* env vars in hermes_cli/config.py
- Env var overrides in gateway/config.py _apply_env_overrides()
Changes vs original PR #956:
- Removed streaming infrastructure (already on main via stream_consumer.py)
- Removed Telegram reply_to_mode (separate feature, not included)
- Updated _resolve_model() -> _resolve_gateway_model()
- Updated stream_callback -> stream_delta_callback
- Updated connect()/disconnect() to use _mark_connected()/_mark_disconnected()
- Adapted to current Platform enum (includes MATTERMOST, MATRIX, DINGTALK)
Tests: 72 new tests, all passing
Docs: API server guide, Open WebUI integration guide, env var reference
* feat(whatsapp): make reply prefix configurable via config.yaml
Reworked from PR #1764 (ifrederico) to use config.yaml instead of .env.
The WhatsApp bridge prepends a header to every outgoing message.
This was hardcoded to '⚕ *Hermes Agent*'. Users can now customize
or disable it via config.yaml:
whatsapp:
reply_prefix: '' # disable header
reply_prefix: '🤖 *My Bot*\n───\n' # custom prefix
How it works:
- load_gateway_config() reads whatsapp.reply_prefix from config.yaml
and stores it in PlatformConfig.extra['reply_prefix']
- WhatsAppAdapter reads it from config.extra at init
- When spawning bridge.js, the adapter passes it as
WHATSAPP_REPLY_PREFIX in the subprocess environment
- bridge.js handles undefined (default), empty (no header),
or custom values with \\n escape support
- Self-chat echo suppression uses the configured prefix
Also fixes _config_version: was 9 but ENV_VARS_BY_VERSION had a
key 10 (TAVILY_API_KEY), so existing users at v9 would never be
prompted for Tavily. Bumped to 10 to close the gap. Added a
regression test to prevent this from happening again.
Credit: ifrederico (PR #1764) for the bridge.js implementation
and the config version gap discovery.
---------
Co-authored-by: Test <test@test.com>
2026-03-17 10:44:37 -07:00
|
|
|
|
# WhatsApp platform settings (gateway mode)
|
|
|
|
|
|
"whatsapp": {
|
|
|
|
|
|
# Reply prefix prepended to every outgoing WhatsApp message.
|
|
|
|
|
|
# Default (None) uses the built-in "⚕ *Hermes Agent*" header.
|
|
|
|
|
|
# Set to "" (empty string) to disable the header entirely.
|
|
|
|
|
|
# Supports \n for newlines, e.g. "🤖 *My Bot*\n──────\n"
|
|
|
|
|
|
},
|
|
|
|
|
|
|
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-03-17 02:59:28 -07:00
|
|
|
|
"website_blocklist": {
|
2026-03-17 03:11:21 -07:00
|
|
|
|
"enabled": False,
|
2026-03-17 02:59:28 -07:00
|
|
|
|
"domains": [],
|
|
|
|
|
|
"shared_files": [],
|
|
|
|
|
|
},
|
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
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# Config schema version - bump this when adding new required fields
|
feat: OpenAI-compatible API server + WhatsApp configurable reply prefix (#1756)
* feat: OpenAI-compatible API server platform adapter
Salvaged from PR #956, updated for current main.
Adds an HTTP API server as a gateway platform adapter that exposes
hermes-agent via the OpenAI Chat Completions and Responses APIs.
Any OpenAI-compatible frontend (Open WebUI, LobeChat, LibreChat,
AnythingLLM, NextChat, ChatBox, etc.) can connect by pointing at
http://localhost:8642/v1.
Endpoints:
- POST /v1/chat/completions — stateless Chat Completions API
- POST /v1/responses — stateful Responses API with chaining
- GET /v1/responses/{id} — retrieve stored response
- DELETE /v1/responses/{id} — delete stored response
- GET /v1/models — list hermes-agent as available model
- GET /health — health check
Features:
- Real SSE streaming via stream_delta_callback (uses main's streaming)
- In-memory LRU response store for Responses API conversation chaining
- Named conversations via 'conversation' parameter
- Bearer token auth (optional, via API_SERVER_KEY)
- CORS support for browser-based frontends
- System prompt layering (frontend system messages on top of core)
- Real token usage tracking in responses
Integration points:
- Platform.API_SERVER in gateway/config.py
- _create_adapter() branch in gateway/run.py
- API_SERVER_* env vars in hermes_cli/config.py
- Env var overrides in gateway/config.py _apply_env_overrides()
Changes vs original PR #956:
- Removed streaming infrastructure (already on main via stream_consumer.py)
- Removed Telegram reply_to_mode (separate feature, not included)
- Updated _resolve_model() -> _resolve_gateway_model()
- Updated stream_callback -> stream_delta_callback
- Updated connect()/disconnect() to use _mark_connected()/_mark_disconnected()
- Adapted to current Platform enum (includes MATTERMOST, MATRIX, DINGTALK)
Tests: 72 new tests, all passing
Docs: API server guide, Open WebUI integration guide, env var reference
* feat(whatsapp): make reply prefix configurable via config.yaml
Reworked from PR #1764 (ifrederico) to use config.yaml instead of .env.
The WhatsApp bridge prepends a header to every outgoing message.
This was hardcoded to '⚕ *Hermes Agent*'. Users can now customize
or disable it via config.yaml:
whatsapp:
reply_prefix: '' # disable header
reply_prefix: '🤖 *My Bot*\n───\n' # custom prefix
How it works:
- load_gateway_config() reads whatsapp.reply_prefix from config.yaml
and stores it in PlatformConfig.extra['reply_prefix']
- WhatsAppAdapter reads it from config.extra at init
- When spawning bridge.js, the adapter passes it as
WHATSAPP_REPLY_PREFIX in the subprocess environment
- bridge.js handles undefined (default), empty (no header),
or custom values with \\n escape support
- Self-chat echo suppression uses the configured prefix
Also fixes _config_version: was 9 but ENV_VARS_BY_VERSION had a
key 10 (TAVILY_API_KEY), so existing users at v9 would never be
prompted for Tavily. Bumped to 10 to close the gap. Added a
regression test to prevent this from happening again.
Credit: ifrederico (PR #1764) for the bridge.js implementation
and the config version gap discovery.
---------
Co-authored-by: Test <test@test.com>
2026-03-17 10:44:37 -07:00
|
|
|
|
"_config_version": 10,
|
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-03-17 04:28:03 -07:00
|
|
|
|
10: ["TAVILY_API_KEY"],
|
2026-03-08 05:55:30 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
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-03-17 02:49:22 -07:00
|
|
|
|
"DASHSCOPE_API_KEY": {
|
|
|
|
|
|
"description": "Alibaba Cloud DashScope API key for Qwen models",
|
|
|
|
|
|
"prompt": "DashScope API Key",
|
|
|
|
|
|
"url": "https://modelstudio.console.alibabacloud.com/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
},
|
|
|
|
|
|
"DASHSCOPE_BASE_URL": {
|
|
|
|
|
|
"description": "Custom DashScope base URL (default: international endpoint)",
|
|
|
|
|
|
"prompt": "DashScope Base URL",
|
|
|
|
|
|
"url": "",
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-03-17 02:02:43 -07:00
|
|
|
|
"OPENCODE_ZEN_API_KEY": {
|
|
|
|
|
|
"description": "OpenCode Zen API key (pay-as-you-go access to curated models)",
|
|
|
|
|
|
"prompt": "OpenCode Zen API key",
|
|
|
|
|
|
"url": "https://opencode.ai/auth",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"OPENCODE_ZEN_BASE_URL": {
|
|
|
|
|
|
"description": "OpenCode Zen base URL override",
|
|
|
|
|
|
"prompt": "OpenCode Zen base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"OPENCODE_GO_API_KEY": {
|
|
|
|
|
|
"description": "OpenCode Go API key ($10/month subscription for open models)",
|
|
|
|
|
|
"prompt": "OpenCode Go API key",
|
|
|
|
|
|
"url": "https://opencode.ai/auth",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"OPENCODE_GO_BASE_URL": {
|
|
|
|
|
|
"description": "OpenCode Go base URL override",
|
|
|
|
|
|
"prompt": "OpenCode Go base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
feat: add Hugging Face as a first-class inference provider (#3419)
Salvage of PR #1747 (original PR #1171 by @davanstrien) onto current main.
Registers Hugging Face Inference Providers (router.huggingface.co/v1) as a named provider:
- hermes chat --provider huggingface (or --provider hf)
- 18 curated open models via hermes model picker
- HF_TOKEN in ~/.hermes/.env
- OpenAI-compatible endpoint with automatic failover (Groq, Together, SambaNova, etc.)
Files: auth.py, models.py, main.py, setup.py, config.py, model_metadata.py, .env.example, 5 docs pages, 17 new tests.
Co-authored-by: Daniel van Strien <davanstrien@gmail.com>
2026-03-27 12:41:59 -07:00
|
|
|
|
"HF_TOKEN": {
|
|
|
|
|
|
"description": "Hugging Face token for Inference Providers (20+ open models via router.huggingface.co)",
|
|
|
|
|
|
"prompt": "Hugging Face Token",
|
|
|
|
|
|
"url": "https://huggingface.co/settings/tokens",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
},
|
|
|
|
|
|
"HF_BASE_URL": {
|
|
|
|
|
|
"description": "Hugging Face Inference Providers base URL override",
|
|
|
|
|
|
"prompt": "HF base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
|
|
|
|
|
|
# ── Tool API keys ──
|
feat(web): add Parallel as alternative web search/extract backend (#1696)
* feat(web): add Parallel as alternative web search/extract backend
Adds Parallel (parallel.ai) as a drop-in alternative to Firecrawl for
web_search and web_extract tools using the official parallel-web SDK.
- Backend selection via WEB_SEARCH_BACKEND env var (auto/parallel/firecrawl)
- Auto mode prefers Firecrawl when both keys present; Parallel when sole backend
- web_crawl remains Firecrawl-only with clear error when unavailable
- Lazy SDK imports, interrupt support, singleton clients
- 16 new unit tests for backend selection and client config
Co-authored-by: s-jag <s-jag@users.noreply.github.com>
* fix: add PARALLEL_API_KEY to config registry and fix web_crawl policy tests
Follow-up for Parallel backend integration:
- Add PARALLEL_API_KEY to OPTIONAL_ENV_VARS (hermes doctor, env blocklist)
- Add to set_config_value api_keys list (hermes config set)
- Add to doctor keys display
- Fix 2 web_crawl policy tests that didn't set FIRECRAWL_API_KEY
(needed now that web_crawl has a Firecrawl availability guard)
* refactor: explicit backend selection via hermes tools, not auto-detect
Replace the auto-detect backend selection with explicit user choice:
- hermes tools saves WEB_SEARCH_BACKEND to .env when user picks a provider
- _get_backend() reads the explicit choice first
- Fallback only for manual/legacy config (uses whichever key is present)
- _is_provider_active() shows [active] for the selected web backend
- Updated tests, docs, and .env.example to remove 'auto' mode language
* refactor: use config.yaml for web backend, not env var
Match the TTS/browser pattern — web.backend is stored in config.yaml
(set by hermes tools), not as a WEB_SEARCH_BACKEND env var.
- _load_web_config() reads web: section from config.yaml
- _get_backend() reads web.backend from config, falls back to key detection
- _configure_provider() saves to config dict (saved to config.yaml)
- _is_provider_active() reads from config dict
- Removed WEB_SEARCH_BACKEND from .env.example, set_config_value, docs
- Updated all tests to mock _load_web_config instead of env vars
---------
Co-authored-by: s-jag <s-jag@users.noreply.github.com>
2026-03-17 04:02:02 -07:00
|
|
|
|
"PARALLEL_API_KEY": {
|
|
|
|
|
|
"description": "Parallel API key for AI-native web search and extract",
|
|
|
|
|
|
"prompt": "Parallel API key",
|
|
|
|
|
|
"url": "https://parallel.ai/",
|
|
|
|
|
|
"tools": ["web_search", "web_extract"],
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "tool",
|
|
|
|
|
|
},
|
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-03-17 04:28:03 -07:00
|
|
|
|
"TAVILY_API_KEY": {
|
|
|
|
|
|
"description": "Tavily API key for AI-native web search, extract, and crawl",
|
|
|
|
|
|
"prompt": "Tavily API key",
|
|
|
|
|
|
"url": "https://app.tavily.com/home",
|
|
|
|
|
|
"tools": ["web_search", "web_extract", "web_crawl"],
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "tool",
|
|
|
|
|
|
},
|
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
|
|
|
|
},
|
2026-03-17 00:16:34 -07:00
|
|
|
|
"BROWSER_USE_API_KEY": {
|
|
|
|
|
|
"description": "Browser Use API key for cloud browser (optional — local browser works without this)",
|
|
|
|
|
|
"prompt": "Browser Use API key",
|
|
|
|
|
|
"url": "https://browser-use.com/",
|
|
|
|
|
|
"tools": ["browser_navigate", "browser_click"],
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"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-03-20 04:36:06 -07:00
|
|
|
|
"HONCHO_BASE_URL": {
|
|
|
|
|
|
"description": "Base URL for self-hosted Honcho instances (no API key needed)",
|
|
|
|
|
|
"prompt": "Honcho base URL (e.g. http://localhost:8000)",
|
|
|
|
|
|
"category": "tool",
|
|
|
|
|
|
},
|
2026-02-25 19:34:25 -05:00
|
|
|
|
|
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",
|
|
|
|
|
|
},
|
feat: register Mattermost and Matrix env vars in OPTIONAL_ENV_VARS
Adds both platforms to the config system so hermes setup, hermes doctor,
and hermes config properly discover and manage their env vars.
- MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS
- MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_USER_ID, MATRIX_ALLOWED_USERS
- Extra env keys for .env sanitizer: MATTERMOST_HOME_CHANNEL,
MATTERMOST_REPLY_MODE, MATRIX_PASSWORD, MATRIX_ENCRYPTION, MATRIX_HOME_ROOM
2026-03-17 03:11:54 -07:00
|
|
|
|
"MATTERMOST_URL": {
|
|
|
|
|
|
"description": "Mattermost server URL (e.g. https://mm.example.com)",
|
|
|
|
|
|
"prompt": "Mattermost server URL",
|
|
|
|
|
|
"url": "https://mattermost.com/deploy/",
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"MATTERMOST_TOKEN": {
|
|
|
|
|
|
"description": "Mattermost bot token or personal access token",
|
|
|
|
|
|
"prompt": "Mattermost bot token",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"MATTERMOST_ALLOWED_USERS": {
|
|
|
|
|
|
"description": "Comma-separated Mattermost user IDs allowed to use the bot",
|
|
|
|
|
|
"prompt": "Allowed Mattermost user IDs (comma-separated)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"MATRIX_HOMESERVER": {
|
|
|
|
|
|
"description": "Matrix homeserver URL (e.g. https://matrix.example.org)",
|
|
|
|
|
|
"prompt": "Matrix homeserver URL",
|
|
|
|
|
|
"url": "https://matrix.org/ecosystem/servers/",
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"MATRIX_ACCESS_TOKEN": {
|
|
|
|
|
|
"description": "Matrix access token (preferred over password login)",
|
|
|
|
|
|
"prompt": "Matrix access token",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"MATRIX_USER_ID": {
|
|
|
|
|
|
"description": "Matrix user ID (e.g. @hermes:example.org)",
|
|
|
|
|
|
"prompt": "Matrix user ID (@user:server)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"MATRIX_ALLOWED_USERS": {
|
|
|
|
|
|
"description": "Comma-separated Matrix user IDs allowed to use the bot (@user:server format)",
|
|
|
|
|
|
"prompt": "Allowed Matrix user IDs (comma-separated)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"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
|
|
|
|
},
|
feat: OpenAI-compatible API server + WhatsApp configurable reply prefix (#1756)
* feat: OpenAI-compatible API server platform adapter
Salvaged from PR #956, updated for current main.
Adds an HTTP API server as a gateway platform adapter that exposes
hermes-agent via the OpenAI Chat Completions and Responses APIs.
Any OpenAI-compatible frontend (Open WebUI, LobeChat, LibreChat,
AnythingLLM, NextChat, ChatBox, etc.) can connect by pointing at
http://localhost:8642/v1.
Endpoints:
- POST /v1/chat/completions — stateless Chat Completions API
- POST /v1/responses — stateful Responses API with chaining
- GET /v1/responses/{id} — retrieve stored response
- DELETE /v1/responses/{id} — delete stored response
- GET /v1/models — list hermes-agent as available model
- GET /health — health check
Features:
- Real SSE streaming via stream_delta_callback (uses main's streaming)
- In-memory LRU response store for Responses API conversation chaining
- Named conversations via 'conversation' parameter
- Bearer token auth (optional, via API_SERVER_KEY)
- CORS support for browser-based frontends
- System prompt layering (frontend system messages on top of core)
- Real token usage tracking in responses
Integration points:
- Platform.API_SERVER in gateway/config.py
- _create_adapter() branch in gateway/run.py
- API_SERVER_* env vars in hermes_cli/config.py
- Env var overrides in gateway/config.py _apply_env_overrides()
Changes vs original PR #956:
- Removed streaming infrastructure (already on main via stream_consumer.py)
- Removed Telegram reply_to_mode (separate feature, not included)
- Updated _resolve_model() -> _resolve_gateway_model()
- Updated stream_callback -> stream_delta_callback
- Updated connect()/disconnect() to use _mark_connected()/_mark_disconnected()
- Adapted to current Platform enum (includes MATTERMOST, MATRIX, DINGTALK)
Tests: 72 new tests, all passing
Docs: API server guide, Open WebUI integration guide, env var reference
* feat(whatsapp): make reply prefix configurable via config.yaml
Reworked from PR #1764 (ifrederico) to use config.yaml instead of .env.
The WhatsApp bridge prepends a header to every outgoing message.
This was hardcoded to '⚕ *Hermes Agent*'. Users can now customize
or disable it via config.yaml:
whatsapp:
reply_prefix: '' # disable header
reply_prefix: '🤖 *My Bot*\n───\n' # custom prefix
How it works:
- load_gateway_config() reads whatsapp.reply_prefix from config.yaml
and stores it in PlatformConfig.extra['reply_prefix']
- WhatsAppAdapter reads it from config.extra at init
- When spawning bridge.js, the adapter passes it as
WHATSAPP_REPLY_PREFIX in the subprocess environment
- bridge.js handles undefined (default), empty (no header),
or custom values with \\n escape support
- Self-chat echo suppression uses the configured prefix
Also fixes _config_version: was 9 but ENV_VARS_BY_VERSION had a
key 10 (TAVILY_API_KEY), so existing users at v9 would never be
prompted for Tavily. Bumped to 10 to close the gap. Added a
regression test to prevent this from happening again.
Credit: ifrederico (PR #1764) for the bridge.js implementation
and the config version gap discovery.
---------
Co-authored-by: Test <test@test.com>
2026-03-17 10:44:37 -07:00
|
|
|
|
"API_SERVER_ENABLED": {
|
|
|
|
|
|
"description": "Enable the OpenAI-compatible API server (true/false). Allows frontends like Open WebUI, LobeChat, etc. to connect.",
|
|
|
|
|
|
"prompt": "Enable API server (true/false)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"API_SERVER_KEY": {
|
|
|
|
|
|
"description": "Bearer token for API server authentication. If empty, all requests are allowed (local use only).",
|
|
|
|
|
|
"prompt": "API server auth key (optional)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"API_SERVER_PORT": {
|
|
|
|
|
|
"description": "Port for the API server (default: 8642).",
|
|
|
|
|
|
"prompt": "API server port",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"API_SERVER_HOST": {
|
|
|
|
|
|
"description": "Host/bind address for the API server (default: 127.0.0.1). Use 0.0.0.0 for network access — requires API_SERVER_KEY for security.",
|
|
|
|
|
|
"prompt": "API server host",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
feat(gateway): add webhook platform adapter for external event triggers
Add a generic webhook platform adapter that receives HTTP POSTs from
external services (GitHub, GitLab, JIRA, Stripe, etc.), validates HMAC
signatures, transforms payloads into agent prompts, and routes responses
back to the source or to another platform.
Features:
- Configurable routes with per-route HMAC secrets, event filters,
prompt templates with dot-notation payload access, skill loading,
and pluggable delivery (github_comment, telegram, discord, log)
- HMAC signature validation (GitHub SHA-256, GitLab token, generic)
- Rate limiting (30 req/min per route, configurable)
- Idempotency cache (1hr TTL, prevents duplicate runs on retries)
- Body size limits (1MB default, checked before reading payload)
- Setup wizard integration with security warnings and docs links
- 33 tests (29 unit + 4 integration), all passing
Security:
- HMAC secret required per route (startup validation)
- Setup wizard warns about internet exposure for webhook/SMS platforms
- Sandboxing (Docker/VM) recommended in docs for public-facing deployments
Files changed:
- gateway/config.py — Platform.WEBHOOK enum + env var overrides
- gateway/platforms/webhook.py — WebhookAdapter (~420 lines)
- gateway/run.py — factory wiring + auth bypass for webhook events
- hermes_cli/config.py — WEBHOOK_* env var definitions
- hermes_cli/setup.py — webhook section in setup_gateway()
- tests/gateway/test_webhook_adapter.py — 29 unit tests
- tests/gateway/test_webhook_integration.py — 4 integration tests
- website/docs/user-guide/messaging/webhooks.md — full user docs
- website/docs/reference/environment-variables.md — WEBHOOK_* vars
- website/sidebars.ts — nav entry
2026-03-20 06:33:36 -07:00
|
|
|
|
"WEBHOOK_ENABLED": {
|
|
|
|
|
|
"description": "Enable the webhook platform adapter for receiving events from GitHub, GitLab, etc.",
|
|
|
|
|
|
"prompt": "Enable webhooks (true/false)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"WEBHOOK_PORT": {
|
|
|
|
|
|
"description": "Port for the webhook HTTP server (default: 8644).",
|
|
|
|
|
|
"prompt": "Webhook port",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"WEBHOOK_SECRET": {
|
|
|
|
|
|
"description": "Global HMAC secret for webhook signature validation (overridable per route in config.yaml).",
|
|
|
|
|
|
"prompt": "Webhook secret",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
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": []}
|
2026-03-17 01:13:34 -07:00
|
|
|
|
|
2026-03-17 01:26:23 -07:00
|
|
|
|
# ── Always: sanitize .env (split concatenated keys) ──
|
2026-03-17 01:13:34 -07:00
|
|
|
|
try:
|
|
|
|
|
|
fixes = sanitize_env_file()
|
|
|
|
|
|
if fixes and not quiet:
|
|
|
|
|
|
print(f" ✓ Repaired .env file ({fixes} corrupted entries fixed)")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass # best-effort; don't block migration on sanitize failure
|
2026-03-17 01:26:23 -07:00
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# 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-03-17 01:31:20 -07:00
|
|
|
|
# ── Version 8 → 9: clear ANTHROPIC_TOKEN from .env ──
|
|
|
|
|
|
# The new Anthropic auth flow no longer uses this env var.
|
2026-03-17 01:28:38 -07:00
|
|
|
|
if current_ver < 9:
|
|
|
|
|
|
try:
|
|
|
|
|
|
old_token = get_env_value("ANTHROPIC_TOKEN")
|
|
|
|
|
|
if old_token:
|
2026-03-17 01:31:20 -07:00
|
|
|
|
save_env_value("ANTHROPIC_TOKEN", "")
|
|
|
|
|
|
if not quiet:
|
|
|
|
|
|
print(" ✓ Cleared ANTHROPIC_TOKEN from .env (no longer used)")
|
2026-03-17 01:28:38 -07:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
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-23 16:02:06 -07:00
|
|
|
|
def _expand_env_vars(obj):
|
|
|
|
|
|
"""Recursively expand ``${VAR}`` references in config values.
|
|
|
|
|
|
|
|
|
|
|
|
Only string values are processed; dict keys, numbers, booleans, and
|
|
|
|
|
|
None are left untouched. Unresolved references (variable not in
|
|
|
|
|
|
``os.environ``) are kept verbatim so callers can detect them.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if isinstance(obj, str):
|
|
|
|
|
|
return re.sub(
|
|
|
|
|
|
r"\${([^}]+)}",
|
|
|
|
|
|
lambda m: os.environ.get(m.group(1), m.group(0)),
|
|
|
|
|
|
obj,
|
|
|
|
|
|
)
|
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
|
return {k: _expand_env_vars(v) for k, v in obj.items()}
|
|
|
|
|
|
if isinstance(obj, list):
|
|
|
|
|
|
return [_expand_env_vars(item) for item in obj]
|
|
|
|
|
|
return obj
|
|
|
|
|
|
|
|
|
|
|
|
|
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-23 16:02:06 -07:00
|
|
|
|
return _expand_env_vars(_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
|
fix: hermes update causes dual gateways on macOS (launchd) (#1567)
* feat: add optional smart model routing
Add a conservative cheap-vs-strong routing option that can send very short/simple turns to a cheaper model across providers while keeping the primary model for complex work. Wire it through CLI, gateway, and cron, and document the config.yaml workflow.
* fix(gateway): remove recursive ExecStop from systemd units, extend TimeoutStopSec to 60s
* fix(gateway): avoid recursive ExecStop in user systemd unit
* fix: extend ExecStop removal and TimeoutStopSec=60 to system unit
The cherry-picked PR #1448 fix only covered the user systemd unit.
The system unit had the same TimeoutStopSec=15 and could benefit
from the same 60s timeout for clean shutdown. Also adds a regression
test for the system unit.
---------
Co-authored-by: Ninja <ninja@local>
* feat(skills): add blender-mcp optional skill for 3D modeling
Control a running Blender instance from Hermes via socket connection
to the blender-mcp addon (port 9876). Supports creating 3D objects,
materials, animations, and running arbitrary bpy code.
Placed in optional-skills/ since it requires Blender 4.3+ desktop
with a third-party addon manually started each session.
* feat(acp): support slash commands in ACP adapter (#1532)
Adds /help, /model, /tools, /context, /reset, /compact, /version
to the ACP adapter (VS Code, Zed, JetBrains). Commands are handled
directly in the server without instantiating the TUI — each command
queries agent/session state and returns plain text.
Unrecognized /commands fall through to the LLM as normal messages.
/model uses detect_provider_for_model() for auto-detection when
switching models, matching the CLI and gateway behavior.
Fixes #1402
* fix(logging): improve error logging in session search tool (#1533)
* fix(gateway): restart on retryable startup failures (#1517)
* feat(email): add skip_attachments option via config.yaml
* feat(email): add skip_attachments option via config.yaml
Adds a config.yaml-driven option to skip email attachments in the
gateway email adapter. Useful for malware protection and bandwidth
savings.
Configure in config.yaml:
platforms:
email:
skip_attachments: true
Based on PR #1521 by @an420eth, changed from env var to config.yaml
(via PlatformConfig.extra) to match the project's config-first pattern.
* docs: document skip_attachments option for email adapter
* fix(telegram): retry on transient TLS failures during connect and send
Add exponential-backoff retry (3 attempts) around initialize() to
handle transient TLS resets during gateway startup. Also catches
TimedOut and OSError in addition to NetworkError.
Add exponential-backoff retry (3 attempts) around send_message() for
NetworkError during message delivery, wrapping the existing Markdown
fallback logic.
Both imports are guarded with try/except ImportError for test
environments where telegram is mocked.
Based on PR #1527 by cmd8. Closes #1526.
* feat: permissive block_anchor thresholds and unicode normalization (#1539)
Salvaged from PR #1528 by an420eth. Closes #517.
Improves _strategy_block_anchor in fuzzy_match.py:
- Add unicode normalization (smart quotes, em/en-dashes, ellipsis,
non-breaking spaces → ASCII) so LLM-produced unicode artifacts
don't break anchor line matching
- Lower thresholds: 0.10 for unique matches (was 0.70), 0.30 for
multiple candidates — if first/last lines match exactly, the
block is almost certainly correct
- Use original (non-normalized) content for offset calculation to
preserve correct character positions
Tested: 3 new scenarios fixed (em-dash anchors, non-breaking space
anchors, very-low-similarity unique matches), zero regressions on
all 9 existing fuzzy match tests.
Co-authored-by: an420eth <an420eth@users.noreply.github.com>
* feat(cli): add file path autocomplete in the input prompt (#1545)
When typing a path-like token (./ ../ ~/ / or containing /),
the CLI now shows filesystem completions in the dropdown menu.
Directories show a trailing slash and 'dir' label; files show
their size. Completions are case-insensitive and capped at 30
entries.
Triggered by tokens like:
edit ./src/ma → shows ./src/main.py, ./src/manifest.json, ...
check ~/doc → shows ~/docs/, ~/documents/, ...
read /etc/hos → shows /etc/hosts, /etc/hostname, ...
open tools/reg → shows tools/registry.py
Slash command autocomplete (/help, /model, etc.) is unaffected —
it still triggers when the input starts with /.
Inspired by OpenCode PR #145 (file path completion menu).
Implementation:
- hermes_cli/commands.py: _extract_path_word() detects path-like
tokens, _path_completions() yields filesystem Completions with
size labels, get_completions() routes to paths vs slash commands
- tests/hermes_cli/test_path_completion.py: 26 tests covering
path extraction, prefix filtering, directory markers, home
expansion, case-insensitivity, integration with slash commands
* feat(privacy): redact PII from LLM context when privacy.redact_pii is enabled
Add privacy.redact_pii config option (boolean, default false). When
enabled, the gateway redacts personally identifiable information from
the system prompt before sending it to the LLM provider:
- Phone numbers (user IDs on WhatsApp/Signal) → hashed to user_<sha256>
- User IDs → hashed to user_<sha256>
- Chat IDs → numeric portion hashed, platform prefix preserved
- Home channel IDs → hashed
- Names/usernames → NOT affected (user-chosen, publicly visible)
Hashes are deterministic (same user → same hash) so the model can
still distinguish users in group chats. Routing and delivery use
the original values internally — redaction only affects LLM context.
Inspired by OpenClaw PR #47959.
* fix(privacy): skip PII redaction on Discord/Slack (mentions need real IDs)
Discord uses <@user_id> for mentions and Slack uses <@U12345> — the LLM
needs the real ID to tag users. Redaction now only applies to WhatsApp,
Signal, and Telegram where IDs are pure routing metadata.
Add 4 platform-specific tests covering Discord, WhatsApp, Signal, Slack.
* 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.
* feat: first-class plugin architecture + hide status bar cost by default (#1544)
The persistent status bar now shows context %, token counts, and
duration but NOT $ cost by default. Cost display is opt-in via:
display:
show_cost: true
in config.yaml, or: hermes config set display.show_cost true
The /usage command still shows full cost breakdown since the user
explicitly asked for it — this only affects the always-visible bar.
Status bar without cost:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ 15m
Status bar with show_cost: true:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ $0.06 │ 15m
* feat: improve memory prioritization + aggressive skill updates (inspired by OpenAI Codex)
* feat: improve memory prioritization — user preferences over procedural knowledge
Inspired by OpenAI Codex's memory prompt improvements (openai/codex#14493)
which focus memory writes on user preferences and recurring patterns
rather than procedural task details.
Key insight: 'Optimize for reducing future user steering — the most
valuable memory prevents the user from having to repeat themselves.'
Changes:
- MEMORY_GUIDANCE (prompt_builder.py): added prioritization hierarchy
and the core principle about reducing user steering
- MEMORY_SCHEMA (memory_tool.py): reordered WHEN TO SAVE list to put
corrections first, added explicit PRIORITY guidance
- Memory nudge (run_agent.py): now asks specifically about preferences,
corrections, and workflow patterns instead of generic 'anything'
- Memory flush (run_agent.py): now instructs to prioritize user
preferences and corrections over task-specific details
* feat: more aggressive skill creation and update prompting
Press harder on skill updates — the agent should proactively patch
skills when it encounters issues during use, not wait to be asked.
Changes:
- SKILLS_GUIDANCE: 'consider saving' → 'save'; added explicit instruction
to patch skills immediately when found outdated/wrong
- Skills header: added instruction to update loaded skills before finishing
if they had missing steps or wrong commands
- Skill nudge: more assertive ('save the approach' not 'consider saving'),
now also prompts for updating existing skills used in the task
- Skill nudge interval: lowered default from 15 to 10 iterations
- skill_manage schema: added 'patch it immediately' to update triggers
* feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
* fix: hermes update causes dual gateways on macOS (launchd)
Three bugs worked together to create the dual-gateway problem:
1. cmd_update only checked systemd for gateway restart, completely
ignoring launchd on macOS. After killing the PID it would print
'Restart it with: hermes gateway run' even when launchd was about
to auto-respawn the process.
2. launchd's KeepAlive.SuccessfulExit=false respawns the gateway
after SIGTERM (non-zero exit), so the user's manual restart
created a second instance.
3. The launchd plist lacked --replace (systemd had it), so the
respawned gateway didn't kill stale instances on startup.
Fixes:
- Add --replace to launchd ProgramArguments (matches systemd)
- Add launchd detection to cmd_update's auto-restart logic
- Print 'auto-restart via launchd' instead of manual restart hint
* fix: add launchd plist auto-refresh + explicit restart in cmd_update
Two integration issues with the initial fix:
1. Existing macOS users with old plist (no --replace) would never
get the fix until manual uninstall/reinstall. Added
refresh_launchd_plist_if_needed() — mirrors the existing
refresh_systemd_unit_if_needed(). Called from launchd_start(),
launchd_restart(), and cmd_update.
2. cmd_update relied on KeepAlive respawn after SIGTERM rather than
explicit launchctl stop/start. This caused races: launchd would
respawn the old process before the PID file was cleaned up.
Now does explicit stop+start (matching how systemd gets an
explicit systemctl restart), with plist refresh first so the
new --replace flag is picked up.
---------
Co-authored-by: Ninja <ninja@local>
Co-authored-by: alireza78a <alireza78a@users.noreply.github.com>
Co-authored-by: Oktay Aydin <113846926+aydnOktay@users.noreply.github.com>
Co-authored-by: JP Lew <polydegen@protonmail.com>
Co-authored-by: an420eth <an420eth@users.noreply.github.com>
2026-03-16 12:36:29 -07:00
|
|
|
|
#
|
|
|
|
|
|
# ── Smart Model Routing ────────────────────────────────────────────────
|
|
|
|
|
|
# Optional cheap-vs-strong routing for simple turns.
|
|
|
|
|
|
# Keeps the primary model for complex work, but can route short/simple
|
|
|
|
|
|
# messages to a cheaper model across providers.
|
|
|
|
|
|
#
|
|
|
|
|
|
# smart_model_routing:
|
|
|
|
|
|
# enabled: true
|
|
|
|
|
|
# max_simple_chars: 160
|
|
|
|
|
|
# max_simple_words: 28
|
|
|
|
|
|
# cheap_model:
|
|
|
|
|
|
# provider: openrouter
|
|
|
|
|
|
# model: google/gemini-2.5-flash
|
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
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
fix: hermes update causes dual gateways on macOS (launchd) (#1567)
* feat: add optional smart model routing
Add a conservative cheap-vs-strong routing option that can send very short/simple turns to a cheaper model across providers while keeping the primary model for complex work. Wire it through CLI, gateway, and cron, and document the config.yaml workflow.
* fix(gateway): remove recursive ExecStop from systemd units, extend TimeoutStopSec to 60s
* fix(gateway): avoid recursive ExecStop in user systemd unit
* fix: extend ExecStop removal and TimeoutStopSec=60 to system unit
The cherry-picked PR #1448 fix only covered the user systemd unit.
The system unit had the same TimeoutStopSec=15 and could benefit
from the same 60s timeout for clean shutdown. Also adds a regression
test for the system unit.
---------
Co-authored-by: Ninja <ninja@local>
* feat(skills): add blender-mcp optional skill for 3D modeling
Control a running Blender instance from Hermes via socket connection
to the blender-mcp addon (port 9876). Supports creating 3D objects,
materials, animations, and running arbitrary bpy code.
Placed in optional-skills/ since it requires Blender 4.3+ desktop
with a third-party addon manually started each session.
* feat(acp): support slash commands in ACP adapter (#1532)
Adds /help, /model, /tools, /context, /reset, /compact, /version
to the ACP adapter (VS Code, Zed, JetBrains). Commands are handled
directly in the server without instantiating the TUI — each command
queries agent/session state and returns plain text.
Unrecognized /commands fall through to the LLM as normal messages.
/model uses detect_provider_for_model() for auto-detection when
switching models, matching the CLI and gateway behavior.
Fixes #1402
* fix(logging): improve error logging in session search tool (#1533)
* fix(gateway): restart on retryable startup failures (#1517)
* feat(email): add skip_attachments option via config.yaml
* feat(email): add skip_attachments option via config.yaml
Adds a config.yaml-driven option to skip email attachments in the
gateway email adapter. Useful for malware protection and bandwidth
savings.
Configure in config.yaml:
platforms:
email:
skip_attachments: true
Based on PR #1521 by @an420eth, changed from env var to config.yaml
(via PlatformConfig.extra) to match the project's config-first pattern.
* docs: document skip_attachments option for email adapter
* fix(telegram): retry on transient TLS failures during connect and send
Add exponential-backoff retry (3 attempts) around initialize() to
handle transient TLS resets during gateway startup. Also catches
TimedOut and OSError in addition to NetworkError.
Add exponential-backoff retry (3 attempts) around send_message() for
NetworkError during message delivery, wrapping the existing Markdown
fallback logic.
Both imports are guarded with try/except ImportError for test
environments where telegram is mocked.
Based on PR #1527 by cmd8. Closes #1526.
* feat: permissive block_anchor thresholds and unicode normalization (#1539)
Salvaged from PR #1528 by an420eth. Closes #517.
Improves _strategy_block_anchor in fuzzy_match.py:
- Add unicode normalization (smart quotes, em/en-dashes, ellipsis,
non-breaking spaces → ASCII) so LLM-produced unicode artifacts
don't break anchor line matching
- Lower thresholds: 0.10 for unique matches (was 0.70), 0.30 for
multiple candidates — if first/last lines match exactly, the
block is almost certainly correct
- Use original (non-normalized) content for offset calculation to
preserve correct character positions
Tested: 3 new scenarios fixed (em-dash anchors, non-breaking space
anchors, very-low-similarity unique matches), zero regressions on
all 9 existing fuzzy match tests.
Co-authored-by: an420eth <an420eth@users.noreply.github.com>
* feat(cli): add file path autocomplete in the input prompt (#1545)
When typing a path-like token (./ ../ ~/ / or containing /),
the CLI now shows filesystem completions in the dropdown menu.
Directories show a trailing slash and 'dir' label; files show
their size. Completions are case-insensitive and capped at 30
entries.
Triggered by tokens like:
edit ./src/ma → shows ./src/main.py, ./src/manifest.json, ...
check ~/doc → shows ~/docs/, ~/documents/, ...
read /etc/hos → shows /etc/hosts, /etc/hostname, ...
open tools/reg → shows tools/registry.py
Slash command autocomplete (/help, /model, etc.) is unaffected —
it still triggers when the input starts with /.
Inspired by OpenCode PR #145 (file path completion menu).
Implementation:
- hermes_cli/commands.py: _extract_path_word() detects path-like
tokens, _path_completions() yields filesystem Completions with
size labels, get_completions() routes to paths vs slash commands
- tests/hermes_cli/test_path_completion.py: 26 tests covering
path extraction, prefix filtering, directory markers, home
expansion, case-insensitivity, integration with slash commands
* feat(privacy): redact PII from LLM context when privacy.redact_pii is enabled
Add privacy.redact_pii config option (boolean, default false). When
enabled, the gateway redacts personally identifiable information from
the system prompt before sending it to the LLM provider:
- Phone numbers (user IDs on WhatsApp/Signal) → hashed to user_<sha256>
- User IDs → hashed to user_<sha256>
- Chat IDs → numeric portion hashed, platform prefix preserved
- Home channel IDs → hashed
- Names/usernames → NOT affected (user-chosen, publicly visible)
Hashes are deterministic (same user → same hash) so the model can
still distinguish users in group chats. Routing and delivery use
the original values internally — redaction only affects LLM context.
Inspired by OpenClaw PR #47959.
* fix(privacy): skip PII redaction on Discord/Slack (mentions need real IDs)
Discord uses <@user_id> for mentions and Slack uses <@U12345> — the LLM
needs the real ID to tag users. Redaction now only applies to WhatsApp,
Signal, and Telegram where IDs are pure routing metadata.
Add 4 platform-specific tests covering Discord, WhatsApp, Signal, Slack.
* 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.
* feat: first-class plugin architecture + hide status bar cost by default (#1544)
The persistent status bar now shows context %, token counts, and
duration but NOT $ cost by default. Cost display is opt-in via:
display:
show_cost: true
in config.yaml, or: hermes config set display.show_cost true
The /usage command still shows full cost breakdown since the user
explicitly asked for it — this only affects the always-visible bar.
Status bar without cost:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ 15m
Status bar with show_cost: true:
⚕ claude-sonnet-4 │ 12K/200K │ 6% │ $0.06 │ 15m
* feat: improve memory prioritization + aggressive skill updates (inspired by OpenAI Codex)
* feat: improve memory prioritization — user preferences over procedural knowledge
Inspired by OpenAI Codex's memory prompt improvements (openai/codex#14493)
which focus memory writes on user preferences and recurring patterns
rather than procedural task details.
Key insight: 'Optimize for reducing future user steering — the most
valuable memory prevents the user from having to repeat themselves.'
Changes:
- MEMORY_GUIDANCE (prompt_builder.py): added prioritization hierarchy
and the core principle about reducing user steering
- MEMORY_SCHEMA (memory_tool.py): reordered WHEN TO SAVE list to put
corrections first, added explicit PRIORITY guidance
- Memory nudge (run_agent.py): now asks specifically about preferences,
corrections, and workflow patterns instead of generic 'anything'
- Memory flush (run_agent.py): now instructs to prioritize user
preferences and corrections over task-specific details
* feat: more aggressive skill creation and update prompting
Press harder on skill updates — the agent should proactively patch
skills when it encounters issues during use, not wait to be asked.
Changes:
- SKILLS_GUIDANCE: 'consider saving' → 'save'; added explicit instruction
to patch skills immediately when found outdated/wrong
- Skills header: added instruction to update loaded skills before finishing
if they had missing steps or wrong commands
- Skill nudge: more assertive ('save the approach' not 'consider saving'),
now also prompts for updating existing skills used in the task
- Skill nudge interval: lowered default from 15 to 10 iterations
- skill_manage schema: added 'patch it immediately' to update triggers
* feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
* fix: hermes update causes dual gateways on macOS (launchd)
Three bugs worked together to create the dual-gateway problem:
1. cmd_update only checked systemd for gateway restart, completely
ignoring launchd on macOS. After killing the PID it would print
'Restart it with: hermes gateway run' even when launchd was about
to auto-respawn the process.
2. launchd's KeepAlive.SuccessfulExit=false respawns the gateway
after SIGTERM (non-zero exit), so the user's manual restart
created a second instance.
3. The launchd plist lacked --replace (systemd had it), so the
respawned gateway didn't kill stale instances on startup.
Fixes:
- Add --replace to launchd ProgramArguments (matches systemd)
- Add launchd detection to cmd_update's auto-restart logic
- Print 'auto-restart via launchd' instead of manual restart hint
* fix: add launchd plist auto-refresh + explicit restart in cmd_update
Two integration issues with the initial fix:
1. Existing macOS users with old plist (no --replace) would never
get the fix until manual uninstall/reinstall. Added
refresh_launchd_plist_if_needed() — mirrors the existing
refresh_systemd_unit_if_needed(). Called from launchd_start(),
launchd_restart(), and cmd_update.
2. cmd_update relied on KeepAlive respawn after SIGTERM rather than
explicit launchctl stop/start. This caused races: launchd would
respawn the old process before the PID file was cleaned up.
Now does explicit stop+start (matching how systemd gets an
explicit systemctl restart), with plist refresh first so the
new --replace flag is picked up.
---------
Co-authored-by: Ninja <ninja@local>
Co-authored-by: alireza78a <alireza78a@users.noreply.github.com>
Co-authored-by: Oktay Aydin <113846926+aydnOktay@users.noreply.github.com>
Co-authored-by: JP Lew <polydegen@protonmail.com>
Co-authored-by: an420eth <an420eth@users.noreply.github.com>
2026-03-16 12:36:29 -07:00
|
|
|
|
#
|
|
|
|
|
|
# ── Smart Model Routing ────────────────────────────────────────────────
|
|
|
|
|
|
# Optional cheap-vs-strong routing for simple turns.
|
|
|
|
|
|
# Keeps the primary model for complex work, but can route short/simple
|
|
|
|
|
|
# messages to a cheaper model across providers.
|
|
|
|
|
|
#
|
|
|
|
|
|
# smart_model_routing:
|
|
|
|
|
|
# enabled: true
|
|
|
|
|
|
# max_simple_chars: 160
|
|
|
|
|
|
# max_simple_words: 28
|
|
|
|
|
|
# cheap_model:
|
|
|
|
|
|
# provider: openrouter
|
|
|
|
|
|
# model: google/gemini-2.5-flash
|
2026-03-08 21:25:58 -07:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def save_config(config: Dict[str, Any]):
|
|
|
|
|
|
"""Save configuration to ~/.hermes/config.yaml."""
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
if is_managed():
|
|
|
|
|
|
managed_error("save configuration")
|
|
|
|
|
|
return
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-17 01:13:34 -07:00
|
|
|
|
def _sanitize_env_lines(lines: list) -> list:
|
|
|
|
|
|
"""Fix corrupted .env lines before writing.
|
|
|
|
|
|
|
|
|
|
|
|
Handles two known corruption patterns:
|
|
|
|
|
|
1. Concatenated KEY=VALUE pairs on a single line (missing newline between
|
|
|
|
|
|
entries, e.g. ``ANTHROPIC_API_KEY=sk-...OPENAI_BASE_URL=https://...``).
|
|
|
|
|
|
2. Stale ``KEY=***`` placeholder entries left by incomplete setup runs.
|
|
|
|
|
|
|
|
|
|
|
|
Uses a known-keys set (OPTIONAL_ENV_VARS + _EXTRA_ENV_KEYS) so we only
|
|
|
|
|
|
split on real Hermes env var names, avoiding false positives from values
|
|
|
|
|
|
that happen to contain uppercase text with ``=``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Build the known keys set lazily from OPTIONAL_ENV_VARS + extras.
|
|
|
|
|
|
# Done inside the function so OPTIONAL_ENV_VARS is guaranteed to be defined.
|
|
|
|
|
|
known_keys = set(OPTIONAL_ENV_VARS.keys()) | _EXTRA_ENV_KEYS
|
|
|
|
|
|
|
|
|
|
|
|
sanitized: list[str] = []
|
|
|
|
|
|
for line in lines:
|
|
|
|
|
|
raw = line.rstrip("\r\n")
|
|
|
|
|
|
stripped = raw.strip()
|
|
|
|
|
|
|
|
|
|
|
|
# Preserve blank lines and comments
|
|
|
|
|
|
if not stripped or stripped.startswith("#"):
|
|
|
|
|
|
sanitized.append(raw + "\n")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Detect concatenated KEY=VALUE pairs on one line.
|
|
|
|
|
|
# Search for known KEY= patterns at any position in the line.
|
|
|
|
|
|
split_positions = []
|
|
|
|
|
|
for key_name in known_keys:
|
|
|
|
|
|
needle = key_name + "="
|
|
|
|
|
|
idx = stripped.find(needle)
|
|
|
|
|
|
while idx >= 0:
|
|
|
|
|
|
split_positions.append(idx)
|
|
|
|
|
|
idx = stripped.find(needle, idx + len(needle))
|
|
|
|
|
|
|
|
|
|
|
|
if len(split_positions) > 1:
|
|
|
|
|
|
split_positions.sort()
|
|
|
|
|
|
# Deduplicate (shouldn't happen, but be safe)
|
|
|
|
|
|
split_positions = sorted(set(split_positions))
|
|
|
|
|
|
for i, pos in enumerate(split_positions):
|
|
|
|
|
|
end = split_positions[i + 1] if i + 1 < len(split_positions) else len(stripped)
|
|
|
|
|
|
part = stripped[pos:end].strip()
|
|
|
|
|
|
if part:
|
|
|
|
|
|
sanitized.append(part + "\n")
|
|
|
|
|
|
else:
|
|
|
|
|
|
sanitized.append(stripped + "\n")
|
|
|
|
|
|
|
|
|
|
|
|
return sanitized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_env_file() -> int:
|
|
|
|
|
|
"""Read, sanitize, and rewrite ~/.hermes/.env in place.
|
|
|
|
|
|
|
|
|
|
|
|
Returns the number of lines that were fixed (concatenation splits +
|
|
|
|
|
|
placeholder removals). Returns 0 when no changes are needed.
|
|
|
|
|
|
"""
|
|
|
|
|
|
env_path = get_env_path()
|
|
|
|
|
|
if not env_path.exists():
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
read_kw = {"encoding": "utf-8", "errors": "replace"} if _IS_WINDOWS else {}
|
|
|
|
|
|
write_kw = {"encoding": "utf-8"} if _IS_WINDOWS else {}
|
|
|
|
|
|
|
|
|
|
|
|
with open(env_path, **read_kw) as f:
|
|
|
|
|
|
original_lines = f.readlines()
|
|
|
|
|
|
|
|
|
|
|
|
sanitized = _sanitize_env_lines(original_lines)
|
|
|
|
|
|
|
|
|
|
|
|
if sanitized == original_lines:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
# Count fixes: difference in line count (from splits) + removed lines
|
|
|
|
|
|
fixes = abs(len(sanitized) - len(original_lines))
|
|
|
|
|
|
if fixes == 0:
|
|
|
|
|
|
# Lines changed content (e.g. *** removal) even if count is same
|
|
|
|
|
|
fixes = sum(1 for a, b in zip(original_lines, sanitized) if a != b)
|
|
|
|
|
|
fixes += abs(len(sanitized) - len(original_lines))
|
|
|
|
|
|
|
|
|
|
|
|
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(sanitized)
|
|
|
|
|
|
f.flush()
|
|
|
|
|
|
os.fsync(f.fileno())
|
|
|
|
|
|
os.replace(tmp_path, env_path)
|
|
|
|
|
|
except BaseException:
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
raise
|
|
|
|
|
|
_secure_file(env_path)
|
|
|
|
|
|
return fixes
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def save_env_value(key: str, value: str):
|
|
|
|
|
|
"""Save or update a value in ~/.hermes/.env."""
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
if is_managed():
|
|
|
|
|
|
managed_error(f"set {key}")
|
|
|
|
|
|
return
|
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()
|
2026-03-17 01:13:34 -07:00
|
|
|
|
# Sanitize on every read: split concatenated keys, drop stale placeholders
|
|
|
|
|
|
lines = _sanitize_env_lines(lines)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
# 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)"),
|
feat(web): add Parallel as alternative web search/extract backend (#1696)
* feat(web): add Parallel as alternative web search/extract backend
Adds Parallel (parallel.ai) as a drop-in alternative to Firecrawl for
web_search and web_extract tools using the official parallel-web SDK.
- Backend selection via WEB_SEARCH_BACKEND env var (auto/parallel/firecrawl)
- Auto mode prefers Firecrawl when both keys present; Parallel when sole backend
- web_crawl remains Firecrawl-only with clear error when unavailable
- Lazy SDK imports, interrupt support, singleton clients
- 16 new unit tests for backend selection and client config
Co-authored-by: s-jag <s-jag@users.noreply.github.com>
* fix: add PARALLEL_API_KEY to config registry and fix web_crawl policy tests
Follow-up for Parallel backend integration:
- Add PARALLEL_API_KEY to OPTIONAL_ENV_VARS (hermes doctor, env blocklist)
- Add to set_config_value api_keys list (hermes config set)
- Add to doctor keys display
- Fix 2 web_crawl policy tests that didn't set FIRECRAWL_API_KEY
(needed now that web_crawl has a Firecrawl availability guard)
* refactor: explicit backend selection via hermes tools, not auto-detect
Replace the auto-detect backend selection with explicit user choice:
- hermes tools saves WEB_SEARCH_BACKEND to .env when user picks a provider
- _get_backend() reads the explicit choice first
- Fallback only for manual/legacy config (uses whichever key is present)
- _is_provider_active() shows [active] for the selected web backend
- Updated tests, docs, and .env.example to remove 'auto' mode language
* refactor: use config.yaml for web backend, not env var
Match the TTS/browser pattern — web.backend is stored in config.yaml
(set by hermes tools), not as a WEB_SEARCH_BACKEND env var.
- _load_web_config() reads web: section from config.yaml
- _get_backend() reads web.backend from config, falls back to key detection
- _configure_provider() saves to config dict (saved to config.yaml)
- _is_provider_active() reads from config dict
- Removed WEB_SEARCH_BACKEND from .env.example, set_config_value, docs
- Updated all tests to mock _load_web_config instead of env vars
---------
Co-authored-by: s-jag <s-jag@users.noreply.github.com>
2026-03-17 04:02:02 -07:00
|
|
|
|
("PARALLEL_API_KEY", "Parallel"),
|
2026-02-02 19:01:51 -08:00
|
|
|
|
("FIRECRAWL_API_KEY", "Firecrawl"),
|
2026-03-17 04:28:03 -07:00
|
|
|
|
("TAVILY_API_KEY", "Tavily"),
|
2026-02-02 19:01:51 -08:00
|
|
|
|
("BROWSERBASE_API_KEY", "Browserbase"),
|
2026-03-17 00:16:34 -07:00
|
|
|
|
("BROWSER_USE_API_KEY", "Browser Use"),
|
2026-02-02 19:01:51 -08:00
|
|
|
|
("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
|
|
|
|
|
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':
|
2026-03-22 04:55:34 -07:00
|
|
|
|
print(f" Docker image: {terminal.get('docker_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
|
2026-02-02 19:13:41 -08:00
|
|
|
|
elif terminal.get('backend') == 'singularity':
|
2026-03-22 04:55:34 -07:00
|
|
|
|
print(f" Image: {terminal.get('singularity_image', 'docker://nikolaik/python-nodejs:python3.11-nodejs20')}")
|
2026-02-02 19:13:41 -08:00
|
|
|
|
elif terminal.get('backend') == 'modal':
|
2026-03-22 04:55:34 -07:00
|
|
|
|
print(f" Modal image: {terminal.get('modal_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
|
2026-02-02 19:13:41 -08:00
|
|
|
|
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-24 18:48:04 -07:00
|
|
|
|
print(f" Threshold: {compression.get('threshold', 0.50) * 100:.0f}%")
|
|
|
|
|
|
print(f" Target ratio: {compression.get('target_ratio', 0.20) * 100:.0f}% of threshold preserved")
|
2026-03-24 18:05:43 -07:00
|
|
|
|
print(f" Protect last: {compression.get('protect_last_n', 20)} messages")
|
2026-03-22 11:20:27 +00:00
|
|
|
|
_sm = compression.get('summary_model', '') or '(main model)'
|
|
|
|
|
|
print(f" Model: {_sm}")
|
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."""
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
if is_managed():
|
|
|
|
|
|
managed_error("edit configuration")
|
|
|
|
|
|
return
|
2026-02-02 19:01:51 -08:00
|
|
|
|
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."""
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
if is_managed():
|
|
|
|
|
|
managed_error("set configuration values")
|
|
|
|
|
|
return
|
2026-02-02 19:01:51 -08:00
|
|
|
|
# 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-17 04:28:03 -07:00
|
|
|
|
'PARALLEL_API_KEY', 'FIRECRAWL_API_KEY', 'FIRECRAWL_API_URL', 'TAVILY_API_KEY',
|
|
|
|
|
|
'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'BROWSER_USE_API_KEY',
|
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)
|