fix(cli): respect HERMES_HOME in all remaining hardcoded ~/.hermes paths

Several files resolved paths via Path.home() / ".hermes" or
os.path.expanduser("~/.hermes/..."), bypassing the HERMES_HOME
environment variable. This broke isolation when running multiple
Hermes instances with distinct HERMES_HOME directories.

Replace all hardcoded paths with calls to get_hermes_home() from
hermes_cli.config, consistent with the rest of the codebase.

Files fixed:
- tools/process_registry.py (processes.json)
- gateway/pairing.py (pairing/)
- gateway/sticker_cache.py (sticker_cache.json)
- gateway/channel_directory.py (channel_directory.json, sessions.json)
- gateway/config.py (gateway.json, config.yaml, sessions_dir)
- gateway/mirror.py (sessions/)
- gateway/hooks.py (hooks/)
- gateway/platforms/base.py (image_cache/, audio_cache/, document_cache/)
- gateway/platforms/whatsapp.py (whatsapp/session)
- gateway/delivery.py (cron/output)
- agent/auxiliary_client.py (auth.json)
- agent/prompt_builder.py (SOUL.md)
- cli.py (config.yaml, images/, pastes/, history)
- run_agent.py (logs/)
- tools/environments/base.py (sandboxes/)
- tools/environments/modal.py (modal_snapshots.json)
- tools/environments/singularity.py (singularity_snapshots.json)
- tools/tts_tool.py (audio_cache)
- hermes_cli/status.py (cron/jobs.json, sessions.json)
- hermes_cli/gateway.py (logs/, whatsapp session)
- hermes_cli/main.py (whatsapp/session)

Tests updated to use HERMES_HOME env var instead of patching Path.home().

Closes #892

(cherry picked from commit 78ac1bba43b8b74a934c6172f2c29bb4d03164b9)
This commit is contained in:
0xIbra 2026-03-11 07:31:41 +01:00 committed by teknium1
parent 2bf6b7ad1a
commit 437ec17125
23 changed files with 77 additions and 51 deletions

View file

@ -41,6 +41,7 @@ from typing import Any, Dict, List, Optional, Tuple
from openai import OpenAI from openai import OpenAI
from hermes_cli.config import get_hermes_home
from hermes_constants import OPENROUTER_BASE_URL from hermes_constants import OPENROUTER_BASE_URL
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -73,7 +74,7 @@ auxiliary_is_nous: bool = False
_OPENROUTER_MODEL = "google/gemini-3-flash-preview" _OPENROUTER_MODEL = "google/gemini-3-flash-preview"
_NOUS_MODEL = "gemini-3-flash" _NOUS_MODEL = "gemini-3-flash"
_NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1" _NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1"
_AUTH_JSON_PATH = Path.home() / ".hermes" / "auth.json" _AUTH_JSON_PATH = get_hermes_home() / "auth.json"
# Codex fallback: uses the Responses API (the only endpoint the Codex # Codex fallback: uses the Responses API (the only endpoint the Codex
# OAuth token can access) with a fast model for auxiliary tasks. # OAuth token can access) with a fast model for auxiliary tasks.

View file

@ -420,7 +420,7 @@ def build_context_files_prompt(cwd: Optional[str] = None) -> str:
soul_path = candidate soul_path = candidate
break break
if not soul_path: if not soul_path:
global_soul = Path.home() / ".hermes" / "SOUL.md" global_soul = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "SOUL.md"
if global_soul.exists(): if global_soul.exists():
soul_path = global_soul soul_path = global_soul

16
cli.py
View file

@ -96,7 +96,7 @@ def _load_prefill_messages(file_path: str) -> List[Dict[str, Any]]:
return [] return []
path = Path(file_path).expanduser() path = Path(file_path).expanduser()
if not path.is_absolute(): if not path.is_absolute():
path = Path.home() / ".hermes" / path path = _hermes_home / path
if not path.exists(): if not path.exists():
logger.warning("Prefill messages file not found: %s", path) logger.warning("Prefill messages file not found: %s", path)
return [] return []
@ -141,8 +141,8 @@ def load_cli_config() -> Dict[str, Any]:
Environment variables take precedence over config file values. Environment variables take precedence over config file values.
Returns default values if no config file exists. Returns default values if no config file exists.
""" """
# Check user config first (~/.hermes/config.yaml) # Check user config first ({HERMES_HOME}/config.yaml)
user_config_path = Path.home() / '.hermes' / 'config.yaml' user_config_path = _hermes_home / 'config.yaml'
project_config_path = Path(__file__).parent / 'cli-config.yaml' project_config_path = Path(__file__).parent / 'cli-config.yaml'
# Use user config if it exists, otherwise project config # Use user config if it exists, otherwise project config
@ -1037,7 +1037,7 @@ def save_config_value(key_path: str, value: any) -> bool:
True if successful, False otherwise True if successful, False otherwise
""" """
# Use the same precedence as load_cli_config: user config first, then project config # Use the same precedence as load_cli_config: user config first, then project config
user_config_path = Path.home() / '.hermes' / 'config.yaml' user_config_path = _hermes_home / 'config.yaml'
project_config_path = Path(__file__).parent / 'cli-config.yaml' project_config_path = Path(__file__).parent / 'cli-config.yaml'
config_path = user_config_path if user_config_path.exists() else project_config_path config_path = user_config_path if user_config_path.exists() else project_config_path
@ -1259,7 +1259,7 @@ class HermesCLI:
self.session_id = f"{timestamp_str}_{short_uuid}" self.session_id = f"{timestamp_str}_{short_uuid}"
# History file for persistent input recall across sessions # History file for persistent input recall across sessions
self._history_file = Path.home() / ".hermes_history" self._history_file = _hermes_home / ".hermes_history"
self._last_invalidate: float = 0.0 # throttle UI repaints self._last_invalidate: float = 0.0 # throttle UI repaints
self._app = None self._app = None
self._secret_state = None self._secret_state = None
@ -1778,7 +1778,7 @@ class HermesCLI:
""" """
from hermes_cli.clipboard import save_clipboard_image from hermes_cli.clipboard import save_clipboard_image
img_dir = Path.home() / ".hermes" / "images" img_dir = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "images"
self._image_counter += 1 self._image_counter += 1
ts = datetime.now().strftime("%Y%m%d_%H%M%S") ts = datetime.now().strftime("%Y%m%d_%H%M%S")
img_path = img_dir / f"clip_{ts}_{self._image_counter}.png" img_path = img_dir / f"clip_{ts}_{self._image_counter}.png"
@ -2074,7 +2074,7 @@ class HermesCLI:
terminal_cwd = os.getenv("TERMINAL_CWD", os.getcwd()) terminal_cwd = os.getenv("TERMINAL_CWD", os.getcwd())
terminal_timeout = os.getenv("TERMINAL_TIMEOUT", "60") terminal_timeout = os.getenv("TERMINAL_TIMEOUT", "60")
user_config_path = Path.home() / '.hermes' / 'config.yaml' user_config_path = _hermes_home / 'config.yaml'
project_config_path = Path(__file__).parent / 'cli-config.yaml' project_config_path = Path(__file__).parent / 'cli-config.yaml'
if user_config_path.exists(): if user_config_path.exists():
config_path = user_config_path config_path = user_config_path
@ -4255,7 +4255,7 @@ class HermesCLI:
if line_count >= 5 and chars_added > 1 and not text.startswith('/'): if line_count >= 5 and chars_added > 1 and not text.startswith('/'):
_paste_counter[0] += 1 _paste_counter[0] += 1
# Save to temp file # Save to temp file
paste_dir = Path(os.path.expanduser("~/.hermes/pastes")) paste_dir = _hermes_home / "pastes"
paste_dir.mkdir(parents=True, exist_ok=True) paste_dir.mkdir(parents=True, exist_ok=True)
paste_file = paste_dir / f"paste_{_paste_counter[0]}_{datetime.now().strftime('%H%M%S')}.txt" paste_file = paste_dir / f"paste_{_paste_counter[0]}_{datetime.now().strftime('%H%M%S')}.txt"
paste_file.write_text(text, encoding="utf-8") paste_file.write_text(text, encoding="utf-8")

View file

@ -12,9 +12,11 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DIRECTORY_PATH = Path.home() / ".hermes" / "channel_directory.json" DIRECTORY_PATH = get_hermes_home() / "channel_directory.json"
def _session_entry_id(origin: Dict[str, Any]) -> Optional[str]: def _session_entry_id(origin: Dict[str, Any]) -> Optional[str]:
@ -129,7 +131,7 @@ def _build_slack(adapter) -> List[Dict[str, str]]:
def _build_from_sessions(platform_name: str) -> List[Dict[str, str]]: def _build_from_sessions(platform_name: str) -> List[Dict[str, str]]:
"""Pull known channels/contacts from sessions.json origin data.""" """Pull known channels/contacts from sessions.json origin data."""
sessions_path = Path.home() / ".hermes" / "sessions" / "sessions.json" sessions_path = get_hermes_home() / "sessions" / "sessions.json"
if not sessions_path.exists(): if not sessions_path.exists():
return [] return []

View file

@ -16,6 +16,8 @@ from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from enum import Enum from enum import Enum
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -151,7 +153,7 @@ class GatewayConfig:
reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"]) reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"])
# Storage paths # Storage paths
sessions_dir: Path = field(default_factory=lambda: Path.home() / ".hermes" / "sessions") sessions_dir: Path = field(default_factory=lambda: get_hermes_home() / "sessions")
# Delivery settings # Delivery settings
always_log_local: bool = True # Always save cron outputs to local files always_log_local: bool = True # Always save cron outputs to local files
@ -246,7 +248,7 @@ class GatewayConfig:
if "default_reset_policy" in data: if "default_reset_policy" in data:
default_policy = SessionResetPolicy.from_dict(data["default_reset_policy"]) default_policy = SessionResetPolicy.from_dict(data["default_reset_policy"])
sessions_dir = Path.home() / ".hermes" / "sessions" sessions_dir = get_hermes_home() / "sessions"
if "sessions_dir" in data: if "sessions_dir" in data:
sessions_dir = Path(data["sessions_dir"]) sessions_dir = Path(data["sessions_dir"])
@ -274,7 +276,8 @@ def load_gateway_config() -> GatewayConfig:
config = GatewayConfig() config = GatewayConfig()
# Try loading from ~/.hermes/gateway.json # Try loading from ~/.hermes/gateway.json
gateway_config_path = Path.home() / ".hermes" / "gateway.json" _home = get_hermes_home()
gateway_config_path = _home / "gateway.json"
if gateway_config_path.exists(): if gateway_config_path.exists():
try: try:
with open(gateway_config_path, "r", encoding="utf-8") as f: with open(gateway_config_path, "r", encoding="utf-8") as f:
@ -288,7 +291,7 @@ def load_gateway_config() -> GatewayConfig:
# for session reset policy since that's where hermes setup writes it. # for session reset policy since that's where hermes setup writes it.
try: try:
import yaml import yaml
config_yaml_path = Path.home() / ".hermes" / "config.yaml" config_yaml_path = _home / "config.yaml"
if config_yaml_path.exists(): if config_yaml_path.exists():
with open(config_yaml_path, encoding="utf-8") as f: with open(config_yaml_path, encoding="utf-8") as f:
yaml_cfg = yaml.safe_load(f) or {} yaml_cfg = yaml.safe_load(f) or {}
@ -481,7 +484,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
def save_gateway_config(config: GatewayConfig) -> None: def save_gateway_config(config: GatewayConfig) -> None:
"""Save gateway configuration to ~/.hermes/gateway.json.""" """Save gateway configuration to ~/.hermes/gateway.json."""
gateway_config_path = Path.home() / ".hermes" / "gateway.json" gateway_config_path = get_hermes_home() / "gateway.json"
gateway_config_path.parent.mkdir(parents=True, exist_ok=True) gateway_config_path.parent.mkdir(parents=True, exist_ok=True)
with open(gateway_config_path, "w", encoding="utf-8") as f: with open(gateway_config_path, "w", encoding="utf-8") as f:

View file

@ -15,6 +15,8 @@ from dataclasses import dataclass
from typing import Dict, List, Optional, Any, Union from typing import Dict, List, Optional, Any, Union
from enum import Enum from enum import Enum
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_PLATFORM_OUTPUT = 4000 MAX_PLATFORM_OUTPUT = 4000
@ -116,7 +118,7 @@ class DeliveryRouter:
""" """
self.config = config self.config = config
self.adapters = adapters or {} self.adapters = adapters or {}
self.output_dir = Path.home() / ".hermes" / "cron" / "output" self.output_dir = get_hermes_home() / "cron" / "output"
def resolve_targets( def resolve_targets(
self, self,
@ -256,7 +258,7 @@ class DeliveryRouter:
def _save_full_output(self, content: str, job_id: str) -> Path: def _save_full_output(self, content: str, job_id: str) -> Path:
"""Save full cron output to disk and return the file path.""" """Save full cron output to disk and return the file path."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
out_dir = Path.home() / ".hermes" / "cron" / "output" out_dir = get_hermes_home() / "cron" / "output"
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / f"{job_id}_{timestamp}.txt" path = out_dir / f"{job_id}_{timestamp}.txt"
path.write_text(content) path.write_text(content)

View file

@ -26,8 +26,10 @@ from typing import Any, Callable, Dict, List, Optional
import yaml import yaml
from hermes_cli.config import get_hermes_home
HOOKS_DIR = Path(os.path.expanduser("~/.hermes/hooks"))
HOOKS_DIR = get_hermes_home() / "hooks"
class HookRegistry: class HookRegistry:

View file

@ -15,9 +15,11 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SESSIONS_DIR = Path.home() / ".hermes" / "sessions" _SESSIONS_DIR = get_hermes_home() / "sessions"
_SESSIONS_INDEX = _SESSIONS_DIR / "sessions.json" _SESSIONS_INDEX = _SESSIONS_DIR / "sessions.json"

View file

@ -25,6 +25,8 @@ import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from hermes_cli.config import get_hermes_home
# Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion # Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion
ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
@ -39,7 +41,7 @@ LOCKOUT_SECONDS = 3600 # Lockout duration after too many failures
MAX_PENDING_PER_PLATFORM = 3 # Max pending codes per platform MAX_PENDING_PER_PLATFORM = 3 # Max pending codes per platform
MAX_FAILED_ATTEMPTS = 5 # Failed approvals before lockout MAX_FAILED_ATTEMPTS = 5 # Failed approvals before lockout
PAIRING_DIR = Path(os.path.expanduser("~/.hermes/pairing")) PAIRING_DIR = get_hermes_home() / "pairing"
def _secure_write(path: Path, data: str) -> None: def _secure_write(path: Path, data: str) -> None:

View file

@ -25,6 +25,7 @@ sys.path.insert(0, str(_Path(__file__).resolve().parents[2]))
from gateway.config import Platform, PlatformConfig from gateway.config import Platform, PlatformConfig
from gateway.session import SessionSource, build_session_key from gateway.session import SessionSource, build_session_key
from hermes_cli.config import get_hermes_home
GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = ( GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
@ -42,8 +43,8 @@ GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
# (e.g. Telegram file URLs expire after ~1 hour). # (e.g. Telegram file URLs expire after ~1 hour).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Default location: ~/.hermes/image_cache/ # Default location: {HERMES_HOME}/image_cache/
IMAGE_CACHE_DIR = Path(os.path.expanduser("~/.hermes/image_cache")) IMAGE_CACHE_DIR = get_hermes_home() / "image_cache"
def get_image_cache_dir() -> Path: def get_image_cache_dir() -> Path:
@ -125,7 +126,7 @@ def cleanup_image_cache(max_age_hours: int = 24) -> int:
# here so the STT tool (OpenAI Whisper) can transcribe them from local files. # here so the STT tool (OpenAI Whisper) can transcribe them from local files.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
AUDIO_CACHE_DIR = Path(os.path.expanduser("~/.hermes/audio_cache")) AUDIO_CACHE_DIR = get_hermes_home() / "audio_cache"
def get_audio_cache_dir() -> Path: def get_audio_cache_dir() -> Path:
@ -184,7 +185,7 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg") -> str:
# here so the agent can reference them by local file path. # here so the agent can reference them by local file path.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
DOCUMENT_CACHE_DIR = Path(os.path.expanduser("~/.hermes/document_cache")) DOCUMENT_CACHE_DIR = get_hermes_home() / "document_cache"
SUPPORTED_DOCUMENT_TYPES = { SUPPORTED_DOCUMENT_TYPES = {
".pdf": "application/pdf", ".pdf": "application/pdf",

View file

@ -26,6 +26,8 @@ _IS_WINDOWS = platform.system() == "Windows"
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -132,7 +134,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
) )
self._session_path: Path = Path(config.extra.get( self._session_path: Path = Path(config.extra.get(
"session_path", "session_path",
Path.home() / ".hermes" / "whatsapp" / "session" get_hermes_home() / "whatsapp" / "session"
)) ))
self._message_queue: asyncio.Queue = asyncio.Queue() self._message_queue: asyncio.Queue = asyncio.Queue()
self._bridge_log_fh = None self._bridge_log_fh = None

View file

@ -14,8 +14,10 @@ import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from hermes_cli.config import get_hermes_home
CACHE_PATH = Path(os.path.expanduser("~/.hermes/sticker_cache.json"))
CACHE_PATH = get_hermes_home() / "sticker_cache.json"
# Vision prompt for describing stickers -- kept concise to save tokens # Vision prompt for describing stickers -- kept concise to save tokens
STICKER_VISION_PROMPT = ( STICKER_VISION_PROMPT = (

View file

@ -13,7 +13,7 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent.resolve() PROJECT_ROOT = Path(__file__).parent.parent.resolve()
from hermes_cli.config import get_env_value, save_env_value from hermes_cli.config import get_env_value, get_hermes_home, save_env_value
from hermes_cli.setup import ( from hermes_cli.setup import (
print_header, print_info, print_success, print_warning, print_error, print_header, print_info, print_success, print_warning, print_error,
prompt, prompt_choice, prompt_yes_no, prompt, prompt_choice, prompt_yes_no,
@ -283,7 +283,7 @@ def systemd_status(deep: bool = False):
def generate_launchd_plist() -> str: def generate_launchd_plist() -> str:
python_path = get_python_path() python_path = get_python_path()
working_dir = str(PROJECT_ROOT) working_dir = str(PROJECT_ROOT)
log_dir = Path.home() / ".hermes" / "logs" log_dir = get_hermes_home() / "logs"
log_dir.mkdir(parents=True, exist_ok=True) log_dir.mkdir(parents=True, exist_ok=True)
return f"""<?xml version="1.0" encoding="UTF-8"?> return f"""<?xml version="1.0" encoding="UTF-8"?>
@ -380,7 +380,7 @@ def launchd_status(deep: bool = False):
print("✗ Gateway service is not loaded") print("✗ Gateway service is not loaded")
if deep: if deep:
log_file = Path.home() / ".hermes" / "logs" / "gateway.log" log_file = get_hermes_home() / "logs" / "gateway.log"
if log_file.exists(): if log_file.exists():
print() print()
print("Recent logs:") print("Recent logs:")
@ -557,7 +557,7 @@ def _platform_status(platform: dict) -> str:
val = get_env_value(token_var) val = get_env_value(token_var)
if token_var == "WHATSAPP_ENABLED": if token_var == "WHATSAPP_ENABLED":
if val and val.lower() == "true": if val and val.lower() == "true":
session_file = Path.home() / ".hermes" / "whatsapp" / "session" / "creds.json" session_file = get_hermes_home() / "whatsapp" / "session" / "creds.json"
if session_file.exists(): if session_file.exists():
return "configured + paired" return "configured + paired"
return "enabled, not paired" return "enabled, not paired"

View file

@ -648,7 +648,7 @@ def cmd_whatsapp(args):
print("✓ Bridge dependencies already installed") print("✓ Bridge dependencies already installed")
# ── Step 5: Check for existing session ─────────────────────────────── # ── Step 5: Check for existing session ───────────────────────────────
session_dir = Path.home() / ".hermes" / "whatsapp" / "session" session_dir = get_hermes_home() / "whatsapp" / "session"
session_dir.mkdir(parents=True, exist_ok=True) session_dir.mkdir(parents=True, exist_ok=True)
if (session_dir / "creds.json").exists(): if (session_dir / "creds.json").exists():

View file

@ -12,7 +12,7 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent.resolve() PROJECT_ROOT = Path(__file__).parent.parent.resolve()
from hermes_cli.colors import Colors, color from hermes_cli.colors import Colors, color
from hermes_cli.config import get_env_path, get_env_value from hermes_cli.config import get_env_path, get_env_value, get_hermes_home
from hermes_constants import OPENROUTER_MODELS_URL from hermes_constants import OPENROUTER_MODELS_URL
def check_mark(ok: bool) -> str: def check_mark(ok: bool) -> str:
@ -267,7 +267,7 @@ def show_status(args):
print() print()
print(color("◆ Scheduled Jobs", Colors.CYAN, Colors.BOLD)) print(color("◆ Scheduled Jobs", Colors.CYAN, Colors.BOLD))
jobs_file = Path.home() / ".hermes" / "cron" / "jobs.json" jobs_file = get_hermes_home() / "cron" / "jobs.json"
if jobs_file.exists(): if jobs_file.exists():
import json import json
try: try:
@ -287,7 +287,7 @@ def show_status(args):
print() print()
print(color("◆ Sessions", Colors.CYAN, Colors.BOLD)) print(color("◆ Sessions", Colors.CYAN, Colors.BOLD))
sessions_file = Path.home() / ".hermes" / "sessions" / "sessions.json" sessions_file = get_hermes_home() / "sessions" / "sessions.json"
if sessions_file.exists(): if sessions_file.exists():
import json import json
try: try:

View file

@ -408,7 +408,7 @@ class AIAgent:
# Persistent error log -- always writes WARNING+ to ~/.hermes/logs/errors.log # Persistent error log -- always writes WARNING+ to ~/.hermes/logs/errors.log
# so tool failures, API errors, etc. are inspectable after the fact. # so tool failures, API errors, etc. are inspectable after the fact.
from agent.redact import RedactingFormatter from agent.redact import RedactingFormatter
_error_log_dir = Path.home() / ".hermes" / "logs" _error_log_dir = _hermes_home / "logs"
_error_log_dir.mkdir(parents=True, exist_ok=True) _error_log_dir.mkdir(parents=True, exist_ok=True)
_error_log_path = _error_log_dir / "errors.log" _error_log_path = _error_log_dir / "errors.log"
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler

View file

@ -1,6 +1,7 @@
"""Tests for gateway/channel_directory.py — channel resolution and display.""" """Tests for gateway/channel_directory.py — channel resolution and display."""
import json import json
import os
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
@ -122,7 +123,7 @@ class TestResolveChannelName:
class TestBuildFromSessions: class TestBuildFromSessions:
def _write_sessions(self, tmp_path, sessions_data): def _write_sessions(self, tmp_path, sessions_data):
"""Write sessions.json at the path _build_from_sessions expects.""" """Write sessions.json at the path _build_from_sessions expects."""
sessions_path = tmp_path / ".hermes" / "sessions" / "sessions.json" sessions_path = tmp_path / "sessions" / "sessions.json"
sessions_path.parent.mkdir(parents=True) sessions_path.parent.mkdir(parents=True)
sessions_path.write_text(json.dumps(sessions_data)) sessions_path.write_text(json.dumps(sessions_data))
@ -152,7 +153,7 @@ class TestBuildFromSessions:
}, },
}) })
with patch.object(Path, "home", return_value=tmp_path): with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
entries = _build_from_sessions("telegram") entries = _build_from_sessions("telegram")
assert len(entries) == 2 assert len(entries) == 2
@ -161,7 +162,7 @@ class TestBuildFromSessions:
assert "Bob" in names assert "Bob" in names
def test_missing_sessions_file(self, tmp_path): def test_missing_sessions_file(self, tmp_path):
with patch.object(Path, "home", return_value=tmp_path): with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
entries = _build_from_sessions("telegram") entries = _build_from_sessions("telegram")
assert entries == [] assert entries == []
@ -171,7 +172,7 @@ class TestBuildFromSessions:
"s2": {"origin": {"platform": "telegram", "chat_id": "123", "chat_name": "X"}}, "s2": {"origin": {"platform": "telegram", "chat_id": "123", "chat_name": "X"}},
}) })
with patch.object(Path, "home", return_value=tmp_path): with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
entries = _build_from_sessions("telegram") entries = _build_from_sessions("telegram")
assert len(entries) == 1 assert len(entries) == 1

View file

@ -808,7 +808,7 @@ class TestTryAttachClipboardImage:
with patch("hermes_cli.clipboard.save_clipboard_image", return_value=True): with patch("hermes_cli.clipboard.save_clipboard_image", return_value=True):
cli._try_attach_clipboard_image() cli._try_attach_clipboard_image()
path = cli._attached_images[0] path = cli._attached_images[0]
assert path.parent == Path.home() / ".hermes" / "images" assert path.parent == Path(os.environ["HERMES_HOME"]) / "images"
assert path.name.startswith("clip_") assert path.name.startswith("clip_")
assert path.suffix == ".png" assert path.suffix == ".png"

View file

@ -5,18 +5,20 @@ import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from hermes_cli.config import get_hermes_home
def get_sandbox_dir() -> Path: def get_sandbox_dir() -> Path:
"""Return the host-side root for all sandbox storage (Docker workspaces, """Return the host-side root for all sandbox storage (Docker workspaces,
Singularity overlays/SIF cache, etc.). Singularity overlays/SIF cache, etc.).
Configurable via TERMINAL_SANDBOX_DIR. Defaults to ~/.hermes/sandboxes/. Configurable via TERMINAL_SANDBOX_DIR. Defaults to {HERMES_HOME}/sandboxes/.
""" """
custom = os.getenv("TERMINAL_SANDBOX_DIR") custom = os.getenv("TERMINAL_SANDBOX_DIR")
if custom: if custom:
p = Path(custom) p = Path(custom)
else: else:
p = Path.home() / ".hermes" / "sandboxes" p = get_hermes_home() / "sandboxes"
p.mkdir(parents=True, exist_ok=True) p.mkdir(parents=True, exist_ok=True)
return p return p

View file

@ -13,12 +13,13 @@ import uuid
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from hermes_cli.config import get_hermes_home
from tools.environments.base import BaseEnvironment from tools.environments.base import BaseEnvironment
from tools.interrupt import is_interrupted from tools.interrupt import is_interrupted
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SNAPSHOT_STORE = Path.home() / ".hermes" / "modal_snapshots.json" _SNAPSHOT_STORE = get_hermes_home() / "modal_snapshots.json"
def _load_snapshots() -> Dict[str, str]: def _load_snapshots() -> Dict[str, str]:

View file

@ -16,12 +16,13 @@ import uuid
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from hermes_cli.config import get_hermes_home
from tools.environments.base import BaseEnvironment from tools.environments.base import BaseEnvironment
from tools.interrupt import is_interrupted from tools.interrupt import is_interrupted
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SNAPSHOT_STORE = Path.home() / ".hermes" / "singularity_snapshots.json" _SNAPSHOT_STORE = get_hermes_home() / "singularity_snapshots.json"
def _load_snapshots() -> Dict[str, str]: def _load_snapshots() -> Dict[str, str]:

View file

@ -47,11 +47,13 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Checkpoint file for crash recovery (gateway only) # Checkpoint file for crash recovery (gateway only)
CHECKPOINT_PATH = Path(os.path.expanduser("~/.hermes/processes.json")) CHECKPOINT_PATH = get_hermes_home() / "processes.json"
# Limits # Limits
MAX_OUTPUT_CHARS = 200_000 # 200KB rolling output buffer MAX_OUTPUT_CHARS = 200_000 # 200KB rolling output buffer

View file

@ -65,7 +65,7 @@ DEFAULT_ELEVENLABS_VOICE_ID = "pNInz6obpgDQGcFmaJgB" # Adam
DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2" DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2"
DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts" DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts"
DEFAULT_OPENAI_VOICE = "alloy" DEFAULT_OPENAI_VOICE = "alloy"
DEFAULT_OUTPUT_DIR = os.path.expanduser("~/.hermes/audio_cache") DEFAULT_OUTPUT_DIR = str(Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "audio_cache")
MAX_TEXT_LENGTH = 4000 MAX_TEXT_LENGTH = 4000