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-04-05 23:31:20 -07:00
from dataclasses import dataclass
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-30 13:28:10 +09:00
from tools . tool_backend_helpers import managed_nous_tools_enabled as _managed_nous_tools_enabled
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 " ,
feat(gateway): add Feishu/Lark platform support (#3817)
Adds Feishu (ByteDance's enterprise messaging platform) as a gateway
platform adapter with full feature parity: WebSocket + webhook transports,
message batching, dedup, rate limiting, rich post/card content parsing,
media handling (images/audio/files/video), group @mention gating,
reaction routing, and interactive card button support.
Cherry-picked from PR #1793 by penwyp with:
- Moved to current main (PR was 458 commits behind)
- Fixed _send_with_retry shadowing BasePlatformAdapter method (renamed to
_feishu_send_with_retry to avoid signature mismatch crash)
- Fixed import structure: aiohttp/websockets imported independently of
lark_oapi so they remain available when SDK is missing
- Fixed get_hermes_home import (hermes_constants, not hermes_cli.config)
- Added skip decorators for tests requiring lark_oapi SDK
- All 16 integration points added surgically to current main
New dependency: lark-oapi>=1.5.3,<2 (optional, pip install hermes-agent[feishu])
Fixes #1788
Co-authored-by: penwyp <penwyp@users.noreply.github.com>
2026-03-29 18:17:42 -07:00
" FEISHU_APP_ID " , " FEISHU_APP_SECRET " , " FEISHU_ENCRYPT_KEY " , " FEISHU_VERIFICATION_TOKEN " ,
2026-03-29 21:29:13 -07:00
" WECOM_BOT_ID " , " WECOM_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 " ,
2026-04-06 17:07:10 +05:30
" MATRIX_PASSWORD " , " MATRIX_ENCRYPTION " , " MATRIX_DEVICE_ID " , " MATRIX_HOME_ROOM " ,
2026-04-04 12:43:20 -05:00
" MATRIX_REQUIRE_MENTION " , " MATRIX_FREE_RESPONSE_ROOMS " , " MATRIX_AUTO_THREAD " ,
2026-03-17 01:13:34 -07: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)
# =============================================================================
2026-03-30 17:34:43 -07:00
_MANAGED_TRUE_VALUES = ( " true " , " 1 " , " yes " )
_MANAGED_SYSTEM_NAMES = {
" brew " : " Homebrew " ,
" homebrew " : " Homebrew " ,
" nix " : " NixOS " ,
" nixos " : " NixOS " ,
}
def get_managed_system ( ) - > Optional [ str ] :
""" Return the package manager owning this install, if any. """
raw = os . getenv ( " HERMES_MANAGED " , " " ) . strip ( )
if raw :
normalized = raw . lower ( )
if normalized in _MANAGED_TRUE_VALUES :
return " NixOS "
return _MANAGED_SYSTEM_NAMES . get ( normalized , raw )
managed_marker = get_hermes_home ( ) / " .managed "
if managed_marker . exists ( ) :
return " NixOS "
return None
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
def is_managed ( ) - > bool :
2026-03-30 17:34:43 -07:00
""" Check if Hermes is running in package-manager-managed mode.
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
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 ) .
"""
2026-03-30 17:34:43 -07:00
return get_managed_system ( ) is not None
def get_managed_update_command ( ) - > Optional [ str ] :
""" Return the preferred upgrade command for a managed install. """
managed_system = get_managed_system ( )
if managed_system == " Homebrew " :
return " brew upgrade hermes-agent "
if managed_system == " NixOS " :
return " sudo nixos-rebuild switch "
return None
def recommended_update_command ( ) - > str :
""" Return the best update command for the current installation. """
return get_managed_update_command ( ) or " hermes update "
def format_managed_message ( action : str = " modify this Hermes installation " ) - > str :
""" Build a user-facing error for managed installs. """
managed_system = get_managed_system ( ) or " a package manager "
raw = os . getenv ( " HERMES_MANAGED " , " " ) . strip ( ) . lower ( )
if managed_system == " NixOS " :
env_hint = " true " if raw in _MANAGED_TRUE_VALUES else raw or " true "
return (
f " Cannot { action } : this Hermes installation is managed by NixOS "
f " (HERMES_MANAGED= { env_hint } ). \n "
" Edit services.hermes-agent.settings in your configuration.nix and run: \n "
" sudo nixos-rebuild switch "
)
if managed_system == " Homebrew " :
env_hint = raw or " homebrew "
return (
f " Cannot { action } : this Hermes installation is managed by Homebrew "
f " (HERMES_MANAGED= { env_hint } ). \n "
" Use: \n "
" brew upgrade hermes-agent "
)
return (
f " Cannot { action } : this Hermes installation is managed by { managed_system } . \n "
" Use your package manager to upgrade or reinstall Hermes. "
)
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
def managed_error ( action : str = " modify configuration " ) :
""" Print user-friendly error for managed mode. """
2026-03-30 17:34:43 -07:00
print ( format_managed_message ( action ) , file = sys . stderr )
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
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-04-01 15:22:05 -07:00
" model " : " " ,
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
" providers " : { } ,
2026-03-29 16:04:53 -07:00
" fallback_providers " : [ ] ,
feat(auth): same-provider credential pools with rotation, custom endpoint support, and interactive CLI (#2647)
* feat(auth): add same-provider credential pools and rotation UX
Add same-provider credential pooling so Hermes can rotate across
multiple credentials for a single provider, recover from exhausted
credentials without jumping providers immediately, and configure
that behavior directly in hermes setup.
- agent/credential_pool.py: persisted per-provider credential pools
- hermes auth add/list/remove/reset CLI commands
- 429/402/401 recovery with pool rotation in run_agent.py
- Setup wizard integration for pool strategy configuration
- Auto-seeding from env vars and existing OAuth state
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
Salvaged from PR #2647
* fix(tests): prevent pool auto-seeding from host env in credential pool tests
Tests for non-pool Anthropic paths and auth remove were failing when
host env vars (ANTHROPIC_API_KEY) or file-backed OAuth credentials
were present. The pool auto-seeding picked these up, causing unexpected
pool entries in tests.
- Mock _select_pool_entry in auxiliary_client OAuth flag tests
- Clear Anthropic env vars and mock _seed_from_singletons in auth remove test
* feat(auth): add thread safety, least_used strategy, and request counting
- Add threading.Lock to CredentialPool for gateway thread safety
(concurrent requests from multiple gateway sessions could race on
pool state mutations without this)
- Add 'least_used' rotation strategy that selects the credential
with the lowest request_count, distributing load more evenly
- Add request_count field to PooledCredential for usage tracking
- Add mark_used() method to increment per-credential request counts
- Wrap select(), mark_exhausted_and_rotate(), and try_refresh_current()
with lock acquisition
- Add tests: least_used selection, mark_used counting, concurrent
thread safety (4 threads × 20 selects with no corruption)
* feat(auth): add interactive mode for bare 'hermes auth' command
When 'hermes auth' is called without a subcommand, it now launches an
interactive wizard that:
1. Shows full credential pool status across all providers
2. Offers a menu: add, remove, reset cooldowns, set strategy
3. For OAuth-capable providers (anthropic, nous, openai-codex), the
add flow explicitly asks 'API key or OAuth login?' — making it
clear that both auth types are supported for the same provider
4. Strategy picker shows all 4 options (fill_first, round_robin,
least_used, random) with the current selection marked
5. Remove flow shows entries with indices for easy selection
The subcommand paths (hermes auth add/list/remove/reset) still work
exactly as before for scripted/non-interactive use.
* fix(tests): update runtime_provider tests for config.yaml source of truth (#4165)
Tests were using OPENAI_BASE_URL env var which is no longer consulted
after #4165. Updated to use model config (provider, base_url, api_key)
which is the new single source of truth for custom endpoint URLs.
* feat(auth): support custom endpoint credential pools keyed by provider name
Custom OpenAI-compatible endpoints all share provider='custom', making
the provider-keyed pool useless. Now pools for custom endpoints are
keyed by 'custom:<normalized_name>' where the name comes from the
custom_providers config list (auto-generated from URL hostname).
- Pool key format: 'custom:together.ai', 'custom:local-(localhost:8080)'
- load_pool('custom:name') seeds from custom_providers api_key AND
model.api_key when base_url matches
- hermes auth add/list now shows custom endpoints alongside registry
providers
- _resolve_openrouter_runtime and _resolve_named_custom_runtime check
pool before falling back to single config key
- 6 new tests covering custom pool keying, seeding, and listing
* docs: add Excalidraw diagram of full credential pool flow
Comprehensive architecture diagram showing:
- Credential sources (env vars, auth.json OAuth, config.yaml, CLI)
- Pool storage and auto-seeding
- Runtime resolution paths (registry, custom, OpenRouter)
- Error recovery (429 retry-then-rotate, 402 immediate, 401 refresh)
- CLI management commands and strategy configuration
Open at: https://excalidraw.com/#json=2Ycqhqpi6f12E_3ITyiwh,c7u9jSt5BwrmiVzHGbm87g
* fix(tests): update setup wizard pool tests for unified select_provider_and_model flow
The setup wizard now delegates to select_provider_and_model() instead
of using its own prompt_choice-based provider picker. Tests needed:
- Mock select_provider_and_model as no-op (provider pre-written to config)
- Call _stub_tts BEFORE custom prompt_choice mock (it overwrites it)
- Pre-write model.provider to config so the pool step is reached
* docs: add comprehensive credential pool documentation
- New page: website/docs/user-guide/features/credential-pools.md
Full guide covering quick start, CLI commands, rotation strategies,
error recovery, custom endpoint pools, auto-discovery, thread safety,
architecture, and storage format.
- Updated fallback-providers.md to reference credential pools as the
first layer of resilience (same-provider rotation before cross-provider)
- Added hermes auth to CLI commands reference with usage examples
- Added credential_pool_strategies to configuration guide
* chore: remove excalidraw diagram from repo (external link only)
* refactor: simplify credential pool code — extract helpers, collapse extras, dedup patterns
- _load_config_safe(): replace 4 identical try/except/import blocks
- _iter_custom_providers(): shared generator for custom provider iteration
- PooledCredential.extra dict: collapse 11 round-trip-only fields
(token_type, scope, client_id, portal_base_url, obtained_at,
expires_in, agent_key_id, agent_key_expires_in, agent_key_reused,
agent_key_obtained_at, tls) into a single extra dict with
__getattr__ for backward-compatible access
- _available_entries(): shared exhaustion-check between select and peek
- Dedup anthropic OAuth seeding (hermes_pkce + claude_code identical)
- SimpleNamespace replaces class _Args boilerplate in auth_commands
- _try_resolve_from_custom_pool(): shared pool-check in runtime_provider
Net -17 lines. All 383 targeted tests pass.
---------
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
2026-03-31 03:10:01 -07:00
" credential_pool_strategies " : { } ,
2026-02-02 19:01:51 -08:00
" toolsets " : [ " hermes-cli " ] ,
2026-03-07 21:01:23 -08:00
" agent " : {
" max_turns " : 90 ,
2026-04-05 19:38:21 -07:00
# Inactivity timeout for gateway agent execution (seconds).
# The agent can run indefinitely as long as it's actively calling
# tools or receiving API responses. Only fires when the agent has
# been completely idle for this duration. 0 = unlimited.
" gateway_timeout " : 1800 ,
2026-03-28 12:31:22 -07:00
# Tool-use enforcement: injects system prompt guidance that tells the
# model to actually call tools instead of describing intended actions.
# Values: "auto" (default — applies to gpt/codex models), true/false
# (force on/off for all models), or a list of model-name substrings
# to match (e.g. ["gpt", "codex", "gemini", "qwen"]).
" tool_use_enforcement " : " auto " ,
2026-03-07 21:01:23 -08:00
} ,
2026-02-02 19:01:51 -08:00
" terminal " : {
" backend " : " local " ,
2026-03-26 15:27:27 -07:00
" modal_mode " : " auto " ,
2026-02-02 19:01:51 -08:00
" 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-04-03 23:30:12 -07:00
# Explicit environment variables to set inside Docker containers.
# Unlike docker_forward_env (which reads values from the host process),
# docker_env lets you specify exact key-value pairs — useful when Hermes
# runs as a systemd service without access to the user's shell environment.
# Example: {"SSH_AUTH_SOCK": "/run/user/1000/ssh-agent.sock"}
" docker_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-03-31 11:11:55 +02:00
" allow_private_urls " : False , # Allow navigating to private/internal IPs (localhost, 192.168.x.x, etc.)
2026-04-01 04:18:50 -07:00
" camofox " : {
# When true, Hermes sends a stable profile-scoped userId to Camofox
# so the server can map it to a persistent browser profile directory.
# Requires Camofox server to be configured with CAMOFOX_PROFILE_DIR.
# When false (default), each session gets a random userId (ephemeral).
" managed_persistence " : False ,
} ,
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
} ,
feat(file_tools): harden read_file with size guard, dedup, and device blocking (#4315)
* feat(file_tools): harden read_file with size guard, dedup, and device blocking
Three improvements to read_file_tool to reduce wasted context tokens and
prevent process hangs:
1. Character-count guard: reads that produce more than 100K characters
(≈25-35K tokens across tokenisers) are rejected with an error that
tells the model to use offset+limit for a smaller range. The
effective cap is min(file_size, 100K) so small files that happen to
have long lines aren't over-penalised. Large truncated files also
get a hint nudging toward targeted reads.
2. File-read deduplication: when the same (path, offset, limit) is read
a second time and the file hasn't been modified (mtime unchanged),
return a lightweight stub instead of re-sending the full content.
Writes and patches naturally change mtime, so post-edit reads always
return fresh content. The dedup cache is cleared on context
compression — after compression the original read content is
summarised away, so the model needs the full content again.
3. Device path blocking: paths like /dev/zero, /dev/random, /dev/stdin
etc. are rejected before any I/O to prevent process hangs from
infinite-output or blocking-input devices.
Tests: 17 new tests covering all three features plus the dedup-reset-
on-compression integration. All 52 file-read tests pass (35 existing +
17 new). Full tool suite (2124 tests) passes with 0 failures.
* feat: make file_read_max_chars configurable, add docs
Add file_read_max_chars to DEFAULT_CONFIG (default 100K). read_file_tool
reads this on first call and caches for the process lifetime. Users on
large-context models can raise it; users on small local models can lower it.
Also adds a 'File Read Safety' section to the configuration docs
explaining the char limit, dedup behavior, and example values.
2026-03-31 12:53:19 -07:00
# Maximum characters returned by a single read_file call. Reads that
# exceed this are rejected with guidance to use offset+limit.
# 100K chars ≈ 25– 35K tokens across typical tokenisers.
" file_read_max_chars " : 100_000 ,
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
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-30 02:59:39 -07:00
" timeout " : 30 , # seconds — LLM API call timeout; increase for slow local vision models
" download_timeout " : 30 , # seconds — image HTTP download timeout; increase for slow connections
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-04-05 11:16:33 -07:00
" timeout " : 360 , # seconds (6min) — per-attempt LLM summarization timeout; increase for slow local models
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-28 14:35:28 -07:00
" timeout " : 120 , # seconds — compression summarises large contexts; increase for local models
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-28 14:35:28 -07:00
" timeout " : 30 ,
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-28 14:35:28 -07:00
" timeout " : 30 ,
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-28 14:35:28 -07:00
" timeout " : 30 ,
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
} ,
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-28 14:35:28 -07:00
" timeout " : 30 ,
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-28 14:35:28 -07:00
" timeout " : 30 ,
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-04-01 01:50:11 -07:00
" inline_diffs " : True , # Show inline diff previews for write actions (write_file, patch, skill_manage)
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-03-29 18:02:42 -07:00
" tool_preview_length " : 0 , # Max chars for tool call previews (0 = no limit, show full paths/commands)
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(memory): pluggable memory provider interface with profile isolation, review fixes, and honcho CLI restoration (#4623)
* feat(memory): add pluggable memory provider interface with profile isolation
Introduces a pluggable MemoryProvider ABC so external memory backends can
integrate with Hermes without modifying core files. Each backend becomes a
plugin implementing a standard interface, orchestrated by MemoryManager.
Key architecture:
- agent/memory_provider.py — ABC with core + optional lifecycle hooks
- agent/memory_manager.py — single integration point in the agent loop
- agent/builtin_memory_provider.py — wraps existing MEMORY.md/USER.md
Profile isolation fixes applied to all 6 shipped plugins:
- Cognitive Memory: use get_hermes_home() instead of raw env var
- Hindsight Memory: check $HERMES_HOME/hindsight/config.json first,
fall back to legacy ~/.hindsight/ for backward compat
- Hermes Memory Store: replace hardcoded ~/.hermes paths with
get_hermes_home() for config loading and DB path defaults
- Mem0 Memory: use get_hermes_home() instead of raw env var
- RetainDB Memory: auto-derive profile-scoped project name from
hermes_home path (hermes-<profile>), explicit env var overrides
- OpenViking Memory: read-only, no local state, isolation via .env
MemoryManager.initialize_all() now injects hermes_home into kwargs so
every provider can resolve profile-scoped storage without importing
get_hermes_home() themselves.
Plugin system: adds register_memory_provider() to PluginContext and
get_plugin_memory_providers() accessor.
Based on PR #3825. 46 tests (37 unit + 5 E2E + 4 plugin registration).
* refactor(memory): drop cognitive plugin, rewrite OpenViking as full provider
Remove cognitive-memory plugin (#727) — core mechanics are broken:
decay runs 24x too fast (hourly not daily), prefetch uses row ID as
timestamp, search limited by importance not similarity.
Rewrite openviking-memory plugin from a read-only search wrapper into
a full bidirectional memory provider using the complete OpenViking
session lifecycle API:
- sync_turn: records user/assistant messages to OpenViking session
(threaded, non-blocking)
- on_session_end: commits session to trigger automatic memory extraction
into 6 categories (profile, preferences, entities, events, cases,
patterns)
- prefetch: background semantic search via find() endpoint
- on_memory_write: mirrors built-in memory writes to the session
- is_available: checks env var only, no network calls (ABC compliance)
Tools expanded from 3 to 5:
- viking_search: semantic search with mode/scope/limit
- viking_read: tiered content (abstract ~100tok / overview ~2k / full)
- viking_browse: filesystem-style navigation (list/tree/stat)
- viking_remember: explicit memory storage via session
- viking_add_resource: ingest URLs/docs into knowledge base
Uses direct HTTP via httpx (no openviking SDK dependency needed).
Response truncation on viking_read to prevent context flooding.
* fix(memory): harden Mem0 plugin — thread safety, non-blocking sync, circuit breaker
- Remove redundant mem0_context tool (identical to mem0_search with
rerank=true, top_k=5 — wastes a tool slot and confuses the model)
- Thread sync_turn so it's non-blocking — Mem0's server-side LLM
extraction can take 5-10s, was stalling the agent after every turn
- Add threading.Lock around _get_client() for thread-safe lazy init
(prefetch and sync threads could race on first client creation)
- Add circuit breaker: after 5 consecutive API failures, pause calls
for 120s instead of hammering a down server every turn. Auto-resets
after cooldown. Logs a warning when tripped.
- Track success/failure in prefetch, sync_turn, and all tool calls
- Wait for previous sync to finish before starting a new one (prevents
unbounded thread accumulation on rapid turns)
- Clean up shutdown to join both prefetch and sync threads
* fix(memory): enforce single external memory provider limit
MemoryManager now rejects a second non-builtin provider with a warning.
Built-in memory (MEMORY.md/USER.md) is always accepted. Only ONE
external plugin provider is allowed at a time. This prevents tool
schema bloat (some providers add 3-5 tools each) and conflicting
memory backends.
The warning message directs users to configure memory.provider in
config.yaml to select which provider to activate.
Updated all 47 tests to use builtin + one external pattern instead
of multiple externals. Added test_second_external_rejected to verify
the enforcement.
* feat(memory): add ByteRover memory provider plugin
Implements the ByteRover integration (from PR #3499 by hieuntg81) as a
MemoryProvider plugin instead of direct run_agent.py modifications.
ByteRover provides persistent memory via the brv CLI — a hierarchical
knowledge tree with tiered retrieval (fuzzy text then LLM-driven search).
Local-first with optional cloud sync.
Plugin capabilities:
- prefetch: background brv query for relevant context
- sync_turn: curate conversation turns (threaded, non-blocking)
- on_memory_write: mirror built-in memory writes to brv
- on_pre_compress: extract insights before context compression
Tools (3):
- brv_query: search the knowledge tree
- brv_curate: store facts/decisions/patterns
- brv_status: check CLI version and context tree state
Profile isolation: working directory at $HERMES_HOME/byterover/ (scoped
per profile). Binary resolution cached with thread-safe double-checked
locking. All write operations threaded to avoid blocking the agent
(curate can take 120s with LLM processing).
* fix(memory): thread remaining sync_turns, fix holographic, add config key
Plugin fixes:
- Hindsight: thread sync_turn (was blocking up to 30s via _run_in_thread)
- RetainDB: thread sync_turn (was blocking on HTTP POST)
- Both: shutdown now joins sync threads alongside prefetch threads
Holographic retrieval fixes:
- reason(): removed dead intersection_key computation (bundled but never
used in scoring). Now reuses pre-computed entity_residuals directly,
moved role_content encoding outside the inner loop.
- contradict(): added _MAX_CONTRADICT_FACTS=500 scaling guard. Above
500 facts, only checks the most recently updated ones to avoid O(n^2)
explosion (~125K comparisons at 500 is acceptable).
Config:
- Added memory.provider key to DEFAULT_CONFIG ("" = builtin only).
No version bump needed (deep_merge handles new keys automatically).
* feat(memory): extract Honcho as a MemoryProvider plugin
Creates plugins/honcho-memory/ as a thin adapter over the existing
honcho_integration/ package. All 4 Honcho tools (profile, search,
context, conclude) move from the normal tool registry to the
MemoryProvider interface.
The plugin delegates all work to HonchoSessionManager — no Honcho
logic is reimplemented. It uses the existing config chain:
$HERMES_HOME/honcho.json -> ~/.honcho/config.json -> env vars.
Lifecycle hooks:
- initialize: creates HonchoSessionManager via existing client factory
- prefetch: background dialectic query
- sync_turn: records messages + flushes to API (threaded)
- on_memory_write: mirrors user profile writes as conclusions
- on_session_end: flushes all pending messages
This is a prerequisite for the MemoryManager wiring in run_agent.py.
Once wired, Honcho goes through the same provider interface as all
other memory plugins, and the scattered Honcho code in run_agent.py
can be consolidated into the single MemoryManager integration point.
* feat(memory): wire MemoryManager into run_agent.py
Adds 8 integration points for the external memory provider plugin,
all purely additive (zero existing code modified):
1. Init (~L1130): Create MemoryManager, find matching plugin provider
from memory.provider config, initialize with session context
2. Tool injection (~L1160): Append provider tool schemas to self.tools
and self.valid_tool_names after memory_manager init
3. System prompt (~L2705): Add external provider's system_prompt_block
alongside existing MEMORY.md/USER.md blocks
4. Tool routing (~L5362): Route provider tool calls through
memory_manager.handle_tool_call() before the catchall handler
5. Memory write bridge (~L5353): Notify external provider via
on_memory_write() when the built-in memory tool writes
6. Pre-compress (~L5233): Call on_pre_compress() before context
compression discards messages
7. Prefetch (~L6421): Inject provider prefetch results into the
current-turn user message (same pattern as Honcho turn context)
8. Turn sync + session end (~L8161, ~L8172): sync_all() after each
completed turn, queue_prefetch_all() for next turn, on_session_end()
+ shutdown_all() at conversation end
All hooks are wrapped in try/except — a failing provider never breaks
the agent. The existing memory system, Honcho integration, and all
other code paths are completely untouched.
Full suite: 7222 passed, 4 pre-existing failures.
* refactor(memory): remove legacy Honcho integration from core
Extracts all Honcho-specific code from run_agent.py, model_tools.py,
toolsets.py, and gateway/run.py. Honcho is now exclusively available
as a memory provider plugin (plugins/honcho-memory/).
Removed from run_agent.py (-457 lines):
- Honcho init block (session manager creation, activation, config)
- 8 Honcho methods: _honcho_should_activate, _strip_honcho_tools,
_activate_honcho, _register_honcho_exit_hook, _queue_honcho_prefetch,
_honcho_prefetch, _honcho_save_user_observation, _honcho_sync
- _inject_honcho_turn_context module-level function
- Honcho system prompt block (tool descriptions, CLI commands)
- Honcho context injection in api_messages building
- Honcho params from __init__ (honcho_session_key, honcho_manager,
honcho_config)
- HONCHO_TOOL_NAMES constant
- All honcho-specific tool dispatch forwarding
Removed from other files:
- model_tools.py: honcho_tools import, honcho params from handle_function_call
- toolsets.py: honcho toolset definition, honcho tools from core tools list
- gateway/run.py: honcho params from AIAgent constructor calls
Removed tests (-339 lines):
- 9 Honcho-specific test methods from test_run_agent.py
- TestHonchoAtexitFlush class from test_exit_cleanup_interrupt.py
Restored two regex constants (_SURROGATE_RE, _BUDGET_WARNING_RE) that
were accidentally removed during the honcho function extraction.
The honcho_integration/ package is kept intact — the plugin delegates
to it. tools/honcho_tools.py registry entries are now dead code (import
commented out in model_tools.py) but the file is preserved for reference.
Full suite: 7207 passed, 4 pre-existing failures. Zero regressions.
* refactor(memory): restructure plugins, add CLI, clean gateway, migration notice
Plugin restructure:
- Move all memory plugins from plugins/<name>-memory/ to plugins/memory/<name>/
(byterover, hindsight, holographic, honcho, mem0, openviking, retaindb)
- New plugins/memory/__init__.py discovery module that scans the directory
directly, loading providers by name without the general plugin system
- run_agent.py uses load_memory_provider() instead of get_plugin_memory_providers()
CLI wiring:
- hermes memory setup — interactive curses picker + config wizard
- hermes memory status — show active provider, config, availability
- hermes memory off — disable external provider (built-in only)
- hermes honcho — now shows migration notice pointing to hermes memory setup
Gateway cleanup:
- Remove _get_or_create_gateway_honcho (already removed in prev commit)
- Remove _shutdown_gateway_honcho and _shutdown_all_gateway_honcho methods
- Remove all calls to shutdown methods (4 call sites)
- Remove _honcho_managers/_honcho_configs dict references
Dead code removal:
- Delete tools/honcho_tools.py (279 lines, import was already commented out)
- Delete tests/gateway/test_honcho_lifecycle.py (131 lines, tested removed methods)
- Remove if False placeholder from run_agent.py
Migration:
- Honcho migration notice on startup: detects existing honcho.json or
~/.honcho/config.json, prints guidance to run hermes memory setup.
Only fires when memory.provider is not set and not in quiet mode.
Full suite: 7203 passed, 4 pre-existing failures. Zero regressions.
* feat(memory): standardize plugin config + add per-plugin documentation
Config architecture:
- Add save_config(values, hermes_home) to MemoryProvider ABC
- Honcho: writes to $HERMES_HOME/honcho.json (SDK native)
- Mem0: writes to $HERMES_HOME/mem0.json
- Hindsight: writes to $HERMES_HOME/hindsight/config.json
- Holographic: writes to config.yaml under plugins.hermes-memory-store
- OpenViking/RetainDB/ByteRover: env-var only (default no-op)
Setup wizard (hermes memory setup):
- Now calls provider.save_config() for non-secret config
- Secrets still go to .env via env vars
- Only memory.provider activation key goes to config.yaml
Documentation:
- README.md for each of the 7 providers in plugins/memory/<name>/
- Requirements, setup (wizard + manual), config reference, tools table
- Consistent format across all providers
The contract for new memory plugins:
- get_config_schema() declares all fields (REQUIRED)
- save_config() writes native config (REQUIRED if not env-var-only)
- Secrets use env_var field in schema, written to .env by wizard
- README.md in the plugin directory
* docs: add memory providers user guide + developer guide
New pages:
- user-guide/features/memory-providers.md — comprehensive guide covering
all 7 shipped providers (Honcho, OpenViking, Mem0, Hindsight,
Holographic, RetainDB, ByteRover). Each with setup, config, tools,
cost, and unique features. Includes comparison table and profile
isolation notes.
- developer-guide/memory-provider-plugin.md — how to build a new memory
provider plugin. Covers ABC, required methods, config schema,
save_config, threading contract, profile isolation, testing.
Updated pages:
- user-guide/features/memory.md — replaced Honcho section with link to
new Memory Providers page
- user-guide/features/honcho.md — replaced with migration redirect to
the new Memory Providers page
- sidebars.ts — added both new pages to navigation
* fix(memory): auto-migrate Honcho users to memory provider plugin
When honcho.json or ~/.honcho/config.json exists but memory.provider
is not set, automatically set memory.provider: honcho in config.yaml
and activate the plugin. The plugin reads the same config files, so
all data and credentials are preserved. Zero user action needed.
Persists the migration to config.yaml so it only fires once. Prints
a one-line confirmation in non-quiet mode.
* fix(memory): only auto-migrate Honcho when enabled + credentialed
Check HonchoClientConfig.enabled AND (api_key OR base_url) before
auto-migrating — not just file existence. Prevents false activation
for users who disabled Honcho, stopped using it (config lingers),
or have ~/.honcho/ from a different tool.
* feat(memory): auto-install pip dependencies during hermes memory setup
Reads pip_dependencies from plugin.yaml, checks which are missing,
installs them via pip before config walkthrough. Also shows install
guidance for external_dependencies (e.g. brv CLI for ByteRover).
Updated all 7 plugin.yaml files with pip_dependencies:
- honcho: honcho-ai
- mem0: mem0ai
- openviking: httpx
- hindsight: hindsight-client
- holographic: (none)
- retaindb: requests
- byterover: (external_dependencies for brv CLI)
* fix: remove remaining Honcho crash risks from cli.py and gateway
cli.py: removed Honcho session re-mapping block (would crash importing
deleted tools/honcho_tools.py), Honcho flush on compress, Honcho
session display on startup, Honcho shutdown on exit, honcho_session_key
AIAgent param.
gateway/run.py: removed honcho_session_key params from helper methods,
sync_honcho param, _honcho.shutdown() block.
tests: fixed test_cron_session_with_honcho_key_skipped (was passing
removed honcho_key param to _flush_memories_for_session).
* fix: include plugins/ in pyproject.toml package list
Without this, plugins/memory/ wouldn't be included in non-editable
installs. Hermes always runs from the repo checkout so this is belt-
and-suspenders, but prevents breakage if the install method changes.
* fix(memory): correct pip-to-import name mapping for dep checks
The heuristic dep.replace('-', '_') fails for packages where the pip
name differs from the import name: honcho-ai→honcho, mem0ai→mem0,
hindsight-client→hindsight_client. Added explicit mapping table so
hermes memory setup doesn't try to reinstall already-installed packages.
* chore: remove dead code from old plugin memory registration path
- hermes_cli/plugins.py: removed register_memory_provider(),
_memory_providers list, get_plugin_memory_providers() — memory
providers now use plugins/memory/ discovery, not the general plugin system
- hermes_cli/main.py: stripped 74 lines of dead honcho argparse
subparsers (setup, status, sessions, map, peer, mode, tokens,
identity, migrate) — kept only the migration redirect
- agent/memory_provider.py: updated docstring to reflect new
registration path
- tests: replaced TestPluginMemoryProviderRegistration with
TestPluginMemoryDiscovery that tests the actual plugins/memory/
discovery system. Added 3 new tests (discover, load, nonexistent).
* chore: delete dead honcho_integration/cli.py and its tests
cli.py (794 lines) was the old 'hermes honcho' command handler — nobody
calls it since cmd_honcho was replaced with a migration redirect.
Deleted tests that imported from removed code:
- tests/honcho_integration/test_cli.py (tested _resolve_api_key)
- tests/honcho_integration/test_config_isolation.py (tested CLI config paths)
- tests/tools/test_honcho_tools.py (tested the deleted tools/honcho_tools.py)
Remaining honcho_integration/ files (actively used by the plugin):
- client.py (445 lines) — config loading, SDK client creation
- session.py (991 lines) — session management, queries, flush
* refactor: move honcho_integration/ into the honcho plugin
Moves client.py (445 lines) and session.py (991 lines) from the
top-level honcho_integration/ package into plugins/memory/honcho/.
No Honcho code remains in the main codebase.
- plugins/memory/honcho/client.py — config loading, SDK client creation
- plugins/memory/honcho/session.py — session management, queries, flush
- Updated all imports: run_agent.py (auto-migration), hermes_cli/doctor.py,
plugin __init__.py, session.py cross-import, all tests
- Removed honcho_integration/ package and pyproject.toml entry
- Renamed tests/honcho_integration/ → tests/honcho_plugin/
* docs: update architecture + gateway-internals for memory provider system
- architecture.md: replaced honcho_integration/ with plugins/memory/
- gateway-internals.md: replaced Honcho-specific session routing and
flush lifecycle docs with generic memory provider interface docs
* fix: update stale mock path for resolve_active_host after honcho plugin migration
* fix(memory): address review feedback — P0 lifecycle, ABC contract, honcho CLI restore
Review feedback from Honcho devs (erosika):
P0 — Provider lifecycle:
- Remove on_session_end() + shutdown_all() from run_conversation() tail
(was killing providers after every turn in multi-turn sessions)
- Add shutdown_memory_provider() method on AIAgent for callers
- Wire shutdown into CLI atexit, reset_conversation, gateway stop/expiry
Bug fixes:
- Remove sync_honcho=False kwarg from /btw callsites (TypeError crash)
- Fix doctor.py references to dead 'hermes honcho setup' command
- Cache prefetch_all() before tool loop (was re-calling every iteration)
ABC contract hardening (all backwards-compatible):
- Add session_id kwarg to prefetch/sync_turn/queue_prefetch
- Make on_pre_compress() return str (provider insights in compression)
- Add **kwargs to on_turn_start() for runtime context
- Add on_delegation() hook for parent-side subagent observation
- Document agent_context/agent_identity/agent_workspace kwargs on
initialize() (prevents cron corruption, enables profile scoping)
- Fix docstring: single external provider, not multiple
Honcho CLI restoration:
- Add plugins/memory/honcho/cli.py (from main's honcho_integration/cli.py
with imports adapted to plugin path)
- Restore full hermes honcho command with all subcommands (status, peer,
mode, tokens, identity, enable/disable, sync, peers, --target-profile)
- Restore auto-clone on profile creation + sync on hermes update
- hermes honcho setup now redirects to hermes memory setup
* fix(memory): wire on_delegation, skip_memory for cron/flush, fix ByteRover return type
- Wire on_delegation() in delegate_tool.py — parent's memory provider
is notified with task+result after each subagent completes
- Add skip_memory=True to cron scheduler (prevents cron system prompts
from corrupting user representations — closes #4052)
- Add skip_memory=True to gateway flush agent (throwaway agent shouldn't
activate memory provider)
- Fix ByteRover on_pre_compress() return type: None -> str
* fix(honcho): port profile isolation fixes from PR #4632
Ports 5 bug fixes found during profile testing (erosika's PR #4632):
1. 3-tier config resolution — resolve_config_path() now checks
$HERMES_HOME/honcho.json → ~/.hermes/honcho.json → ~/.honcho/config.json
(non-default profiles couldn't find shared host blocks)
2. Thread host=_host_key() through from_global_config() in cmd_setup,
cmd_status, cmd_identity (--target-profile was being ignored)
3. Use bare profile name as aiPeer (not host key with dots) — Honcho's
peer ID pattern is ^[a-zA-Z0-9_-]+$, dots are invalid
4. Wrap add_peers() in try/except — was fatal on new AI peers, killed
all message uploads for the session
5. Gate Honcho clone behind --clone/--clone-all on profile create
(bare create should be blank-slate)
Also: sanitize assistant_peer_id via _sanitize_id()
* fix(tests): add module cleanup fixture to test_cli_provider_resolution
test_cli_provider_resolution._import_cli() wipes tools.*, cli, and
run_agent from sys.modules to force fresh imports, but had no cleanup.
This poisoned all subsequent tests on the same xdist worker — mocks
targeting tools.file_tools, tools.send_message_tool, etc. patched the
NEW module object while already-imported functions still referenced
the OLD one. Caused ~25 cascade failures: send_message KeyError,
process_registry FileNotFoundError, file_read_guards timeouts,
read_loop_detection file-not-found, mcp_oauth None port, and
provider_parity/codex_execution stale tool lists.
Fix: autouse fixture saves all affected modules before each test and
restores them after, matching the pattern in
test_managed_browserbase_and_modal.py.
2026-04-02 15:33:51 -07:00
# External memory provider plugin (empty = built-in only).
# Set to a provider name to activate: "openviking", "mem0",
# "hindsight", "holographic", "retaindb", "byterover".
# Only ONE external provider is allowed at a time.
" provider " : " " ,
2026-02-19 00:57:31 -08:00
} ,
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-03-29 00:33:30 -07:00
# Skills — external skill directories for sharing skills across tools/agents.
# Each path is expanded (~, ${VAR}) and resolved. Read-only — skill creation
# always goes to ~/.hermes/skills/.
" skills " : {
" external_dirs " : [ ] , # e.g. ["~/.agents/skills", "/shared/team-skills"]
} ,
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-31 01:24:48 -07:00
" reactions " : True , # Add 👀/✅/❌ reactions to messages during processing
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-03-30 00:02:02 -07:00
" timeout " : 60 ,
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
} ,
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-03-29 16:31:01 -07:00
" cron " : {
# Wrap delivered cron responses with a header (task name) and footer
# ("The agent cannot see this message"). Set to false for clean output.
" wrap_response " : True ,
} ,
feat: centralized logging, instrumentation, hermes logs CLI, gateway noise fix (#5430)
Adds comprehensive logging infrastructure to Hermes Agent across 4 phases:
**Phase 1 — Centralized logging**
- New hermes_logging.py with idempotent setup_logging() used by CLI, gateway, and cron
- agent.log (INFO+) and errors.log (WARNING+) with RotatingFileHandler + RedactingFormatter
- config.yaml logging: section (level, max_size_mb, backup_count)
- All entry points wired (cli.py, main.py, gateway/run.py, run_agent.py)
- Fixed debug_helpers.py writing to ./logs/ instead of ~/.hermes/logs/
**Phase 2 — Event instrumentation**
- API calls: model, provider, tokens, latency, cache hit %
- Tool execution: name, duration, result size (both sequential + concurrent)
- Session lifecycle: turn start (session/model/provider/platform), compression (before/after)
- Credential pool: rotation events, exhaustion tracking
**Phase 3 — hermes logs CLI command**
- hermes logs / hermes logs -f / hermes logs errors / hermes logs gateway
- --level, --session, --since filters
- hermes logs list (file sizes + ages)
**Phase 4 — Gateway bug fix + noise reduction**
- fix: _async_flush_memories() called with wrong arg count — sessions never flushed
- Batched session expiry logs: 6 lines/cycle → 2 summary lines
- Added inbound message + response time logging
75 new tests, zero regressions on the full suite.
2026-04-06 00:08:20 -07:00
# Logging — controls file logging to ~/.hermes/logs/.
# agent.log captures INFO+ (all agent activity); errors.log captures WARNING+.
" logging " : {
" level " : " INFO " , # Minimum level for agent.log: DEBUG, INFO, WARNING
" max_size_mb " : 5 , # Max size per log file before rotation
" backup_count " : 3 , # Number of rotated backup files to keep
} ,
2026-02-02 19:39:23 -08:00
# Config schema version - bump this when adding new required fields
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
" _config_version " : 12 ,
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-26 15:27:27 -07:00
11 : [ " TERMINAL_MODAL_MODE " ] ,
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
} ,
2026-04-06 10:14:01 -07:00
" GOOGLE_API_KEY " : {
" description " : " Google AI Studio API key (also recognized as GEMINI_API_KEY) " ,
" prompt " : " Google AI Studio API key " ,
" url " : " https://aistudio.google.com/app/apikey " ,
" password " : True ,
" category " : " provider " ,
" advanced " : True ,
} ,
" GEMINI_API_KEY " : {
" description " : " Google AI Studio API key (alias for GOOGLE_API_KEY) " ,
" prompt " : " Gemini API key " ,
" url " : " https://aistudio.google.com/app/apikey " ,
" password " : True ,
" category " : " provider " ,
" advanced " : True ,
} ,
" GEMINI_BASE_URL " : {
" description " : " Google AI Studio base URL override " ,
" prompt " : " Gemini base URL (leave empty for default) " ,
" url " : None ,
" password " : False ,
" category " : " provider " ,
" advanced " : True ,
} ,
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 " : {
2026-03-27 22:10:10 -07:00
" description " : " Alibaba Cloud DashScope API key (Qwen + multi-provider models) " ,
2026-03-17 02:49:22 -07:00
" prompt " : " DashScope API Key " ,
" url " : " https://modelstudio.console.alibabacloud.com/ " ,
" password " : True ,
" category " : " provider " ,
} ,
" DASHSCOPE_BASE_URL " : {
2026-03-27 22:10:10 -07:00
" description " : " Custom DashScope base URL (default: coding-intl OpenAI-compat endpoint) " ,
2026-03-17 02:49:22 -07:00
" 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 ──
2026-03-28 17:35:53 -07:00
" EXA_API_KEY " : {
" description " : " Exa API key for AI-native web search and contents " ,
" prompt " : " Exa API key " ,
" url " : " https://exa.ai/ " ,
" tools " : [ " web_search " , " web_extract " ] ,
" password " : True ,
" category " : " tool " ,
} ,
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-26 15:27:27 -07:00
" FIRECRAWL_GATEWAY_URL " : {
" description " : " Exact Firecrawl tool-gateway origin override for Nous Subscribers only (optional) " ,
" prompt " : " Firecrawl gateway URL (leave empty to derive from domain) " ,
" url " : None ,
" password " : False ,
" category " : " tool " ,
" advanced " : True ,
} ,
" TOOL_GATEWAY_DOMAIN " : {
" description " : " Shared tool-gateway domain suffix for Nous Subscribers only, used to derive vendor hosts, e.g. nousresearch.com -> firecrawl-gateway.nousresearch.com " ,
" prompt " : " Tool-gateway domain suffix " ,
" url " : None ,
" password " : False ,
" category " : " tool " ,
" advanced " : True ,
} ,
" TOOL_GATEWAY_SCHEME " : {
" description " : " Shared tool-gateway URL scheme for Nous Subscribers only, used to derive vendor hosts (`https` by default, set `http` for local gateway testing) " ,
" prompt " : " Tool-gateway URL scheme " ,
" url " : None ,
" password " : False ,
" category " : " tool " ,
" advanced " : True ,
} ,
" TOOL_GATEWAY_USER_TOKEN " : {
" description " : " Explicit Nous Subscriber access token for tool-gateway requests (optional; otherwise read from the Hermes auth store) " ,
" prompt " : " Tool-gateway user token " ,
" url " : None ,
" password " : True ,
" 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-04-06 14:05:26 -07:00
" FIRECRAWL_BROWSER_TTL " : {
" description " : " Firecrawl browser session TTL in seconds (optional, default 300) " ,
" prompt " : " Browser session TTL (seconds) " ,
" tools " : [ " browser_navigate " , " browser_click " ] ,
" password " : False ,
" category " : " tool " ,
} ,
feat(browser): add Camofox local anti-detection browser backend (#4008)
Camofox-browser is a self-hosted Node.js server wrapping Camoufox
(Firefox fork with C++ fingerprint spoofing). When CAMOFOX_URL is set,
all 11 browser tools route through the Camofox REST API instead of
the agent-browser CLI.
Maps 1:1 to the existing browser tool interface:
- Navigate, snapshot, click, type, scroll, back, press, close
- Get images, vision (screenshot + LLM analysis)
- Console (returns empty with note — camofox limitation)
Setup: npm start in camofox-browser dir, or docker run -p 9377:9377
Then: CAMOFOX_URL=http://localhost:9377 in ~/.hermes/.env
Advantages over Browserbase (cloud):
- Free (no per-session API costs)
- Local (zero network latency for browser ops)
- Anti-detection at C++ level (bypasses Cloudflare/Google bot detection)
- Works offline, Docker-ready
Files:
- tools/browser_camofox.py: Full REST backend (~400 lines)
- tools/browser_tool.py: Routing at each tool function
- hermes_cli/config.py: CAMOFOX_URL env var entry
- tests/tools/test_browser_camofox.py: 20 tests
2026-03-30 13:18:42 -07:00
" CAMOFOX_URL " : {
" description " : " Camofox browser server URL for local anti-detection browsing (e.g. http://localhost:9377) " ,
" prompt " : " Camofox server URL " ,
" url " : " https://github.com/jo-inc/camofox-browser " ,
" tools " : [ " browser_navigate " , " browser_click " ] ,
" password " : False ,
" 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 " ,
} ,
2026-03-28 22:17:43 -07:00
" MATTERMOST_REQUIRE_MENTION " : {
" description " : " Require @mention in Mattermost channels (default: true). Set to false to respond to all messages. " ,
" prompt " : " Require @mention in channels " ,
" url " : None ,
" password " : False ,
" category " : " messaging " ,
} ,
" MATTERMOST_FREE_RESPONSE_CHANNELS " : {
" description " : " Comma-separated Mattermost channel IDs where bot responds without @mention " ,
" prompt " : " Free-response channel IDs (comma-separated) " ,
" url " : None ,
" password " : False ,
" 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
" 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-04-04 12:43:20 -05:00
" MATRIX_REQUIRE_MENTION " : {
" description " : " Require @mention in Matrix rooms (default: true). Set to false to respond to all messages. " ,
" prompt " : " Require @mention in rooms (true/false) " ,
" url " : None ,
" password " : False ,
" category " : " messaging " ,
" advanced " : True ,
} ,
" MATRIX_FREE_RESPONSE_ROOMS " : {
" description " : " Comma-separated Matrix room IDs where bot responds without @mention " ,
" prompt " : " Free-response room IDs (comma-separated) " ,
" url " : None ,
" password " : False ,
" category " : " messaging " ,
" advanced " : True ,
} ,
" MATRIX_AUTO_THREAD " : {
" description " : " Auto-create threads for messages in Matrix rooms (default: true) " ,
" prompt " : " Auto-create threads in rooms (true/false) " ,
" url " : None ,
" password " : False ,
" category " : " messaging " ,
" advanced " : True ,
} ,
2026-04-06 17:07:10 +05:30
" MATRIX_DEVICE_ID " : {
" description " : " Stable Matrix device ID for E2EE persistence across restarts (e.g. HERMES_BOT) " ,
" prompt " : " Matrix device ID (stable across restarts) " ,
" url " : None ,
" password " : False ,
" category " : " messaging " ,
" advanced " : True ,
} ,
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
}
2026-03-30 13:28:10 +09:00
if not _managed_nous_tools_enabled ( ) :
for _hidden_var in (
" FIRECRAWL_GATEWAY_URL " ,
" TOOL_GATEWAY_DOMAIN " ,
" TOOL_GATEWAY_SCHEME " ,
" TOOL_GATEWAY_USER_TOKEN " ,
) :
OPTIONAL_ENV_VARS . pop ( _hidden_var , None )
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
feat(skills): add skill config interface + llm-wiki skill (#5635)
Skills can now declare config.yaml settings via metadata.hermes.config
in their SKILL.md frontmatter. Values are stored under skills.config.*
namespace, prompted during hermes config migrate, shown in hermes config
show, and injected into the skill context at load time.
Also adds the llm-wiki skill (Karpathy's LLM Wiki pattern) as the first
skill to use the new config interface, declaring wiki.path.
Skill config interface (new):
- agent/skill_utils.py: extract_skill_config_vars(), discover_all_skill_config_vars(),
resolve_skill_config_values(), SKILL_CONFIG_PREFIX
- agent/skill_commands.py: _inject_skill_config() injects resolved values
into skill messages as [Skill config: ...] block
- hermes_cli/config.py: get_missing_skill_config_vars(), skill config
prompting in migrate_config(), Skill Settings in show_config()
LLM Wiki skill (skills/research/llm-wiki/SKILL.md):
- Three-layer architecture (raw sources, wiki pages, schema)
- Three operations (ingest, query, lint)
- Session orientation, page thresholds, tag taxonomy, update policy,
scaling guidance, log rotation, archiving workflow
Docs: creating-skills.md, configuration.md, skills.md, skills-catalog.md
Closes #5100
2026-04-06 13:49:13 -07:00
def get_missing_skill_config_vars ( ) - > List [ Dict [ str , Any ] ] :
""" Return skill-declared config vars that are missing or empty in config.yaml.
Scans all enabled skills for ` ` metadata . hermes . config ` ` entries , then checks
which ones are absent or empty under ` ` skills . config . < key > ` ` in the user ' s
config . yaml . Returns a list of dicts suitable for prompting .
"""
try :
from agent . skill_utils import discover_all_skill_config_vars , SKILL_CONFIG_PREFIX
except Exception :
return [ ]
all_vars = discover_all_skill_config_vars ( )
if not all_vars :
return [ ]
config = load_config ( )
missing : List [ Dict [ str , Any ] ] = [ ]
for var in all_vars :
# Skill config is stored under skills.config.<logical_key>
storage_key = f " { SKILL_CONFIG_PREFIX } . { var [ ' key ' ] } "
parts = storage_key . split ( " . " )
current = config
value = None
for part in parts :
if isinstance ( current , dict ) and part in current :
current = current [ part ]
value = current
else :
value = None
break
# Missing = key doesn't exist or is empty string
if value is None or ( isinstance ( value , str ) and not value . strip ( ) ) :
missing . append ( var )
return missing
2026-02-02 19:39:23 -08:00
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
2026-04-05 23:31:20 -07:00
# =============================================================================
# Config structure validation
# =============================================================================
# Fields that are valid at root level of config.yaml
_KNOWN_ROOT_KEYS = {
" _config_version " , " model " , " providers " , " fallback_model " ,
" fallback_providers " , " credential_pool_strategies " , " toolsets " ,
" agent " , " terminal " , " display " , " compression " , " delegation " ,
" auxiliary " , " custom_providers " , " memory " , " gateway " ,
}
# Valid fields inside a custom_providers list entry
_VALID_CUSTOM_PROVIDER_FIELDS = {
" name " , " base_url " , " api_key " , " api_mode " , " models " ,
" context_length " , " rate_limit_delay " ,
}
# Fields that look like they should be inside custom_providers, not at root
_CUSTOM_PROVIDER_LIKE_FIELDS = { " base_url " , " api_key " , " rate_limit_delay " , " api_mode " }
@dataclass
class ConfigIssue :
""" A detected config structure problem. """
severity : str # "error", "warning"
message : str
hint : str
def validate_config_structure ( config : Optional [ Dict [ str , Any ] ] = None ) - > List [ " ConfigIssue " ] :
""" Validate config.yaml structure and return a list of detected issues.
Catches common YAML formatting mistakes that produce confusing runtime
errors ( like " Unknown provider " ) instead of clear diagnostics .
Can be called with a pre - loaded config dict , or will load from disk .
"""
if config is None :
try :
config = load_config ( )
except Exception :
return [ ConfigIssue ( " error " , " Could not load config.yaml " , " Run ' hermes setup ' to create a valid config " ) ]
issues : List [ ConfigIssue ] = [ ]
# ── custom_providers must be a list, not a dict ──────────────────────
cp = config . get ( " custom_providers " )
if cp is not None :
if isinstance ( cp , dict ) :
issues . append ( ConfigIssue (
" error " ,
" custom_providers is a dict — it must be a YAML list (items prefixed with ' - ' ) " ,
" Change to: \n "
" custom_providers: \n "
" - name: my-provider \n "
" base_url: https://... \n "
" api_key: ... " ,
) )
# Check if dict keys look like they should be list-entry fields
cp_keys = set ( cp . keys ( ) ) if isinstance ( cp , dict ) else set ( )
suspicious = cp_keys & _CUSTOM_PROVIDER_LIKE_FIELDS
if suspicious :
issues . append ( ConfigIssue (
" warning " ,
f " Root-level keys { sorted ( suspicious ) } look like custom_providers entry fields " ,
" These should be indented under a ' - name: ... ' list entry, not at root level " ,
) )
elif isinstance ( cp , list ) :
# Validate each entry in the list
for i , entry in enumerate ( cp ) :
if not isinstance ( entry , dict ) :
issues . append ( ConfigIssue (
" warning " ,
f " custom_providers[ { i } ] is not a dict (got { type ( entry ) . __name__ } ) " ,
" Each entry should have at minimum: name, base_url " ,
) )
continue
if not entry . get ( " name " ) :
issues . append ( ConfigIssue (
" warning " ,
f " custom_providers[ { i } ] is missing ' name ' field " ,
" Add a name, e.g.: name: my-provider " ,
) )
if not entry . get ( " base_url " ) :
issues . append ( ConfigIssue (
" warning " ,
f " custom_providers[ { i } ] is missing ' base_url ' field " ,
" Add the API endpoint URL, e.g.: base_url: https://api.example.com/v1 " ,
) )
# ── fallback_model must be a top-level dict with provider + model ────
fb = config . get ( " fallback_model " )
if fb is not None :
if not isinstance ( fb , dict ) :
issues . append ( ConfigIssue (
" error " ,
f " fallback_model should be a dict with ' provider ' and ' model ' , got { type ( fb ) . __name__ } " ,
" Change to: \n "
" fallback_model: \n "
" provider: openrouter \n "
" model: anthropic/claude-sonnet-4 " ,
) )
elif fb :
if not fb . get ( " provider " ) :
issues . append ( ConfigIssue (
" warning " ,
" fallback_model is missing ' provider ' field — fallback will be disabled " ,
" Add: provider: openrouter (or another provider) " ,
) )
if not fb . get ( " model " ) :
issues . append ( ConfigIssue (
" warning " ,
" fallback_model is missing ' model ' field — fallback will be disabled " ,
" Add: model: anthropic/claude-sonnet-4 (or another model) " ,
) )
# ── Check for fallback_model accidentally nested inside custom_providers ──
if isinstance ( cp , dict ) and " fallback_model " not in config and " fallback_model " in ( cp or { } ) :
issues . append ( ConfigIssue (
" error " ,
" fallback_model appears inside custom_providers instead of at root level " ,
" Move fallback_model to the top level of config.yaml (no indentation) " ,
) )
# ── model section: should exist when custom_providers is configured ──
model_cfg = config . get ( " model " )
if cp and not model_cfg :
issues . append ( ConfigIssue (
" warning " ,
" custom_providers defined but no ' model ' section — Hermes won ' t know which provider to use " ,
" Add a model section: \n "
" model: \n "
" provider: custom \n "
" default: your-model-name \n "
" base_url: https://... " ,
) )
# ── Root-level keys that look misplaced ──────────────────────────────
for key in config :
if key . startswith ( " _ " ) :
continue
if key not in _KNOWN_ROOT_KEYS and key in _CUSTOM_PROVIDER_LIKE_FIELDS :
issues . append ( ConfigIssue (
" warning " ,
f " Root-level key ' { key } ' looks misplaced — should it be under ' model: ' or inside a ' custom_providers ' entry? " ,
f " Move ' { key } ' under the appropriate section " ,
) )
return issues
def print_config_warnings ( config : Optional [ Dict [ str , Any ] ] = None ) - > None :
""" Print config structure warnings to stderr at startup.
Called early in CLI and gateway init so users see problems before
they hit cryptic " Unknown provider " errors . Prints nothing if
config is healthy .
"""
try :
issues = validate_config_structure ( config )
except Exception :
return
if not issues :
return
import sys
lines = [ " \033 [33m⚠ Config issues detected in config.yaml: \033 [0m " ]
for ci in issues :
marker = " \033 [31m✗ \033 [0m " if ci . severity == " error " else " \033 [33m⚠ \033 [0m "
lines . append ( f " { marker } { ci . message } " )
lines . append ( " \033 [2mRun ' hermes doctor ' for fix suggestions. \033 [0m " )
sys . stderr . write ( " \n " . join ( lines ) + " \n \n " )
2026-02-02 19:39:23 -08:00
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
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
# ── Version 11 → 12: migrate custom_providers list → providers dict ──
if current_ver < 12 :
config = load_config ( )
custom_list = config . get ( " custom_providers " )
if isinstance ( custom_list , list ) and custom_list :
providers_dict = config . get ( " providers " , { } )
if not isinstance ( providers_dict , dict ) :
providers_dict = { }
migrated_count = 0
for entry in custom_list :
if not isinstance ( entry , dict ) :
continue
old_name = entry . get ( " name " , " " )
old_url = entry . get ( " base_url " , " " ) or entry . get ( " url " , " " ) or " "
old_key = entry . get ( " api_key " , " " )
if not old_url :
continue # skip entries with no URL
# Generate a kebab-case key from the display name
key = old_name . strip ( ) . lower ( ) . replace ( " " , " - " ) . replace ( " ( " , " " ) . replace ( " ) " , " " )
# Remove consecutive hyphens and trailing hyphens
while " -- " in key :
key = key . replace ( " -- " , " - " )
key = key . strip ( " - " )
if not key :
# Fallback: derive from URL hostname
try :
from urllib . parse import urlparse
parsed = urlparse ( old_url )
key = ( parsed . hostname or " endpoint " ) . replace ( " . " , " - " )
except Exception :
key = f " endpoint- { migrated_count } "
# Don't overwrite existing entries
if key in providers_dict :
key = f " { key } - { migrated_count } "
new_entry = { " api " : old_url }
if old_name :
new_entry [ " name " ] = old_name
if old_key and old_key not in ( " no-key " , " no-key-required " , " " ) :
new_entry [ " api_key " ] = old_key
# Carry over model and api_mode if present
if entry . get ( " model " ) :
new_entry [ " default_model " ] = entry [ " model " ]
if entry . get ( " api_mode " ) :
new_entry [ " transport " ] = entry [ " api_mode " ]
providers_dict [ key ] = new_entry
migrated_count + = 1
if migrated_count > 0 :
config [ " providers " ] = providers_dict
# Remove the old list
del config [ " custom_providers " ]
save_config ( config )
if not quiet :
print ( f " ✓ Migrated { migrated_count } custom provider(s) to providers: section " )
for key in list ( providers_dict . keys ( ) ) [ - migrated_count : ] :
ep = providers_dict [ key ]
print ( f " → { key } : { ep . get ( ' api ' , ' ' ) } " )
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 ( " \n Let ' 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 )
feat(skills): add skill config interface + llm-wiki skill (#5635)
Skills can now declare config.yaml settings via metadata.hermes.config
in their SKILL.md frontmatter. Values are stored under skills.config.*
namespace, prompted during hermes config migrate, shown in hermes config
show, and injected into the skill context at load time.
Also adds the llm-wiki skill (Karpathy's LLM Wiki pattern) as the first
skill to use the new config interface, declaring wiki.path.
Skill config interface (new):
- agent/skill_utils.py: extract_skill_config_vars(), discover_all_skill_config_vars(),
resolve_skill_config_values(), SKILL_CONFIG_PREFIX
- agent/skill_commands.py: _inject_skill_config() injects resolved values
into skill messages as [Skill config: ...] block
- hermes_cli/config.py: get_missing_skill_config_vars(), skill config
prompting in migrate_config(), Skill Settings in show_config()
LLM Wiki skill (skills/research/llm-wiki/SKILL.md):
- Three-layer architecture (raw sources, wiki pages, schema)
- Three operations (ingest, query, lint)
- Session orientation, page thresholds, tag taxonomy, update policy,
scaling guidance, log rotation, archiving workflow
Docs: creating-skills.md, configuration.md, skills.md, skills-catalog.md
Closes #5100
2026-04-06 13:49:13 -07:00
# ── Skill-declared config vars ──────────────────────────────────────
# Skills can declare config.yaml settings they need via
# metadata.hermes.config in their SKILL.md frontmatter.
# Prompt for any that are missing/empty.
missing_skill_config = get_missing_skill_config_vars ( )
if missing_skill_config and interactive and not quiet :
print ( f " \n { len ( missing_skill_config ) } skill setting(s) not configured: " )
for var in missing_skill_config :
skill_name = var . get ( " skill " , " unknown " )
print ( f " • { var [ ' key ' ] } — { var [ ' description ' ] } (from skill: { skill_name } ) " )
print ( )
try :
answer = input ( " Configure skill settings? [y/N]: " ) . strip ( ) . lower ( )
except ( EOFError , KeyboardInterrupt ) :
answer = " n "
if answer in ( " y " , " yes " ) :
print ( )
config = load_config ( )
try :
from agent . skill_utils import SKILL_CONFIG_PREFIX
except Exception :
SKILL_CONFIG_PREFIX = " skills.config "
for var in missing_skill_config :
default = var . get ( " default " , " " )
default_hint = f " (default: { default } ) " if default else " "
value = input ( f " { var [ ' prompt ' ] } { default_hint } : " ) . strip ( )
if not value and default :
value = str ( default )
if value :
storage_key = f " { SKILL_CONFIG_PREFIX } . { var [ ' key ' ] } "
_set_nested ( config , storage_key , value )
results [ " config_added " ] . append ( var [ " key " ] )
print ( f " ✓ Saved { var [ ' key ' ] } = { value } " )
else :
results [ " warnings " ] . append (
f " Skipped { var [ ' key ' ] } — skill ' { var . get ( ' skill ' , ' ? ' ) } ' may ask for it later "
)
print ( )
save_config ( config )
else :
print ( " Set later with: hermes config set <key> <value> " )
2026-02-02 19:39:23 -08:00
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-31 12:54:22 -07:00
def _normalize_root_model_keys ( config : Dict [ str , Any ] ) - > Dict [ str , Any ] :
""" Move stale root-level provider/base_url into model section.
Some users ( or older code ) placed ` ` provider : ` ` and ` ` base_url : ` ` at the
config root instead of inside ` ` model : ` ` . These root - level keys are only
used as a fallback when the corresponding ` ` model . * ` ` key is empty — they
never override an existing ` ` model . provider ` ` or ` ` model . base_url ` ` .
After migration the root - level keys are removed so they can ' t cause
confusion on subsequent loads .
"""
# Only act if there are root-level keys to migrate
has_root = any ( config . get ( k ) for k in ( " provider " , " base_url " ) )
if not has_root :
return config
config = dict ( config )
model = config . get ( " model " )
if not isinstance ( model , dict ) :
model = { " default " : model } if model else { }
config [ " model " ] = model
for key in ( " provider " , " base_url " ) :
root_val = config . get ( key )
if root_val and not model . get ( key ) :
model [ key ] = root_val
config . pop ( key , None )
return config
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-31 12:54:22 -07:00
return _expand_env_vars ( _normalize_root_model_keys ( _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
2026-04-06 17:17:57 -07:00
# openai-codex (OAuth — hermes auth) — OpenAI Codex
# nous (OAuth — hermes auth) — Nous Portal
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
# 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-04-06 17:17:57 -07:00
# openai-codex (OAuth — hermes auth) — OpenAI Codex
# nous (OAuth — hermes auth) — 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-31 12:54:22 -07:00
normalized = _normalize_root_model_keys ( _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-04-05 12:00:53 -07:00
def remove_env_value ( key : str ) - > bool :
""" Remove a key from ~/.hermes/.env and os.environ.
Returns True if the key was found and removed , False otherwise .
"""
if is_managed ( ) :
managed_error ( f " remove { key } " )
return False
if not _ENV_VAR_NAME_RE . match ( key ) :
raise ValueError ( f " Invalid environment variable name: { key !r} " )
env_path = get_env_path ( )
if not env_path . exists ( ) :
os . environ . pop ( key , None )
return False
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 :
lines = f . readlines ( )
lines = _sanitize_env_lines ( lines )
new_lines = [ line for line in lines if not line . strip ( ) . startswith ( f " { key } = " ) ]
found = len ( new_lines ) < len ( lines )
if found :
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 ( new_lines )
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 )
os . environ . pop ( key , None )
return found
2026-03-13 02:09:52 -07:00
def save_anthropic_oauth_token ( value : str , save_fn = None ) :
""" Persist an Anthropic OAuth/setup token and clear the API-key slot. """
writer = save_fn or save_env_value
writer ( " ANTHROPIC_TOKEN " , value )
writer ( " ANTHROPIC_API_KEY " , " " )
2026-03-14 19:38:55 -07:00
def use_anthropic_claude_code_credentials ( save_fn = None ) :
""" Use Claude Code ' s own credential files instead of persisting env tokens. """
writer = save_fn or save_env_value
writer ( " ANTHROPIC_TOKEN " , " " )
writer ( " ANTHROPIC_API_KEY " , " " )
2026-03-13 02:09:52 -07:00
def save_anthropic_api_key ( value : str , save_fn = None ) :
""" Persist an Anthropic API key and clear the OAuth/setup-token slot. """
writer = save_fn or save_env_value
writer ( " ANTHROPIC_API_KEY " , value )
writer ( " ANTHROPIC_TOKEN " , " " )
2026-03-13 03:14:04 -07:00
def save_env_value_secure ( key : str , value : str ) - > Dict [ str , Any ] :
save_env_value ( key , value )
return {
" success " : True ,
" stored_as " : key ,
" validated " : False ,
}
2026-02-02 19:01:51 -08:00
def get_env_value ( key : str ) - > Optional [ str ] :
""" Get a value from ~/.hermes/.env or environment. """
# Check environment first
if key in os . environ :
return os . environ [ key ]
# Then check .env file
env_vars = load_env ( )
return env_vars . get ( key )
# =============================================================================
# Config display
# =============================================================================
def redact_key ( key : str ) - > str :
""" Redact an API key for display. """
if not key :
return color ( " (not set) " , Colors . DIM )
if len ( key ) < 12 :
return " *** "
return key [ : 4 ] + " ... " + key [ - 4 : ]
def show_config ( ) :
""" Display current configuration. """
config = load_config ( )
print ( )
print ( color ( " ┌─────────────────────────────────────────────────────────┐ " , Colors . CYAN ) )
2026-02-20 21:25:04 -08:00
print ( color ( " │ ⚕ Hermes Configuration │ " , Colors . CYAN ) )
2026-02-02 19:01:51 -08:00
print ( color ( " └─────────────────────────────────────────────────────────┘ " , Colors . CYAN ) )
# Paths
print ( )
print ( color ( " ◆ Paths " , Colors . CYAN , Colors . BOLD ) )
print ( f " Config: { get_config_path ( ) } " )
print ( f " Secrets: { get_env_path ( ) } " )
print ( f " Install: { get_project_root ( ) } " )
# API Keys
print ( )
print ( color ( " ◆ API Keys " , Colors . CYAN , Colors . BOLD ) )
keys = [
( " OPENROUTER_API_KEY " , " OpenRouter " ) ,
2026-02-23 23:21:33 +00:00
( " VOICE_TOOLS_OPENAI_KEY " , " OpenAI (STT/TTS) " ) ,
2026-03-28 17:35:53 -07:00
( " EXA_API_KEY " , " Exa " ) ,
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 ) } " )
feat(skills): add skill config interface + llm-wiki skill (#5635)
Skills can now declare config.yaml settings via metadata.hermes.config
in their SKILL.md frontmatter. Values are stored under skills.config.*
namespace, prompted during hermes config migrate, shown in hermes config
show, and injected into the skill context at load time.
Also adds the llm-wiki skill (Karpathy's LLM Wiki pattern) as the first
skill to use the new config interface, declaring wiki.path.
Skill config interface (new):
- agent/skill_utils.py: extract_skill_config_vars(), discover_all_skill_config_vars(),
resolve_skill_config_values(), SKILL_CONFIG_PREFIX
- agent/skill_commands.py: _inject_skill_config() injects resolved values
into skill messages as [Skill config: ...] block
- hermes_cli/config.py: get_missing_skill_config_vars(), skill config
prompting in migrate_config(), Skill Settings in show_config()
LLM Wiki skill (skills/research/llm-wiki/SKILL.md):
- Three-layer architecture (raw sources, wiki pages, schema)
- Three operations (ingest, query, lint)
- Session orientation, page thresholds, tag taxonomy, update policy,
scaling guidance, log rotation, archiving workflow
Docs: creating-skills.md, configuration.md, skills.md, skills-catalog.md
Closes #5100
2026-04-06 13:49:13 -07:00
# Skill config
try :
from agent . skill_utils import discover_all_skill_config_vars , resolve_skill_config_values
skill_vars = discover_all_skill_config_vars ( )
if skill_vars :
resolved = resolve_skill_config_values ( skill_vars )
print ( )
print ( color ( " ◆ Skill Settings " , Colors . CYAN , Colors . BOLD ) )
for var in skill_vars :
key = var [ " key " ]
value = resolved . get ( key , " " )
skill_name = var . get ( " skill " , " " )
display_val = str ( value ) if value else color ( " (not set) " , Colors . DIM )
print ( f " { key : <20s } { display_val } { color ( f ' [ { skill_name } ] ' , Colors . DIM ) } " )
except Exception :
pass
2026-02-02 19:01:51 -08:00
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-31 08:48:54 +09:00
' EXA_API_KEY ' , ' PARALLEL_API_KEY ' , ' FIRECRAWL_API_KEY ' , ' FIRECRAWL_API_URL ' ,
2026-03-26 15:27:27 -07:00
' FIRECRAWL_GATEWAY_URL ' , ' TOOL_GATEWAY_DOMAIN ' , ' TOOL_GATEWAY_SCHEME ' ,
' TOOL_GATEWAY_USER_TOKEN ' , ' TAVILY_API_KEY ' ,
2026-03-17 04:28:03 -07:00
' 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 " ,
2026-03-26 15:27:27 -07:00
" terminal.modal_mode " : " TERMINAL_MODAL_MODE " ,
2026-02-26 20:02:46 -08:00
" 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 )
2026-03-31 13:32:54 -04:00
if not key or value is None :
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 )