6.6 KiB

name version description author tags
hermes-web-cli-main-architecture 1.0 Server-side architecture patterns for hermes-web-cli main.py — subprocess communication, session persistence, dynamic path resolution, and config handling. Hermes Agent
hermes-web-cli
main.py
subprocess
session-persistence
path-resolution

Hermes Web CLI Main.py Architecture Patterns

Overview

This skill documents the server-side architecture patterns for main.py in hermes-web-cli modules. Covers subprocess communication with hermes CLI, session persistence, dynamic path resolution, and configuration defaults.

1. Subprocess Communication with Hermes CLI

Pattern: Non-interactive CLI invocation

When calling the hermes CLI from the web server via subprocess, you MUST use non-interactive mode to get a single response without polluting user session lists.

cmd = [
    python_path, "-m", "hermes",
    "chat", "-q", request.message,
    "--source", "tool"
]

Critical flags:

  • -q (quiet): Returns a single response instead of entering interactive chat mode
  • --source tool: Marks the message as coming from a tool integration, not a user chat session

Why this matters:

  • Without -q, the subprocess enters interactive mode and never returns
  • Without --source tool, messages appear in the user's session history, confusing the session list

Working subprocess pattern:

result = subprocess.run(
    cmd,
    cwd=BASE_HERMES_PATH,
    capture_output=True,
    text=True,
    timeout=120,
    env={**os.environ, "PYTHONIOENCODING": "utf-8"}
)
response_text = result.stdout.strip()

API Endpoints using subprocess:

  • POST /api/sessions/{session_id}/messages — send message via hermes CLI
  • POST /api/services/test — test service connection via hermes CLI

2. Session Persistence Pattern

Problem: In-memory dict loses data on server restart

The global_sessions dict is used for session management but is lost on restart.

Solution: JSON file persistence with threading lock

import threading
import json
import os

# In-memory cache
global_sessions = {}
SESSIONS_FILE = os.path.join(os.path.dirname(__file__), "data", "sessions.json")
sessions_lock = threading.Lock()

def load_sessions():
    """Load sessions from JSON file on startup."""
    global global_sessions
    if os.path.exists(SESSIONS_FILE):
        try:
            with open(SESSIONS_FILE, 'r') as f:
                global_sessions = json.load(f)
        except Exception as e:
            print(f"Warning: Could not load sessions file: {e}")

def save_sessions():
    """Save sessions to JSON file (call after modifications)."""
    os.makedirs(os.path.dirname(SESSIONS_FILE), exist_ok=True)
    with open(SESSIONS_FILE, 'w') as f:
        json.dump(global_sessions, f, indent=2, default=str)

# Load on startup
load_sessions()

# Use lock for thread-safe modifications
with sessions_lock:
    global_sessions[session_id] = session_data
    save_sessions()

Key rules:

  • Always wrap modifications in with sessions_lock:
  • Call save_sessions() immediately after modifying global_sessions
  • Use default=str in json.dump to handle datetime objects
  • Create data directory with exist_ok=True

3. Dynamic Path Resolution

Problem: Hardcoded paths break across environments

Solution: Multi-level fallback path resolution

def _resolve_hermes_home() -> str:
    """Find the hermes home directory using multiple strategies."""
    # 1. Environment variable (highest priority)
    hermes_home = os.environ.get("HERMES_HOME")
    if hermes_home and os.path.exists(hermes_home):
        return hermes_home
    
    # 2. Use the official function
    try:
        from hermes.config import get_hermes_home
        home = get_hermes_home()
        if home and os.path.exists(home):
            return home
    except ImportError:
        pass
    
    # 3. Relative to current file
    current_dir = os.path.dirname(os.path.abspath(__file__))
    candidate = os.path.abspath(os.path.join(current_dir, "..", ".."))
    if os.path.exists(os.path.join(candidate, "hermes")):
        return candidate
    
    # 4. Fallback to default
    return os.path.expanduser("~/.hermes")

BASE_HERMES_PATH = _resolve_hermes_home()

Python interpreter path:

# Dynamic venv python path
PYTHON_PATH = os.path.join(BASE_HERMES_PATH, ".venv", "bin", "python3")
if not os.path.exists(PYTHON_PATH):
    PYTHON_PATH = "python3"  # Fallback to system python

4. Configuration Default Values

Problem: Missing keys in config.yaml cause KeyError

Solution: Provide complete defaults with merge logic

DEFAULT_CONFIG = {
    "hermes_web_cli": {
        "enabled": True,
        "hermes_path": "",  # Empty = auto-detect
        "auth_method": "header",  # 'header' or 'bearer'
        "api_key": "",
        "allowed_ips": [],
        "rate_limit": 100,
    }
}

def get_config() -> dict:
    config = copy.deepcopy(DEFAULT_CONFIG)
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, 'r') as f:
            user_config = yaml.safe_load(f) or {}
        # Deep merge user config over defaults
        deep_merge(config, user_config)
    return config

Critical defaults that prevent errors:

  • auth_method: Must default to 'header' to prevent KeyError in auth checks
  • All list/dict fields should default to empty [] or {}

5. User ID Propagation

Pattern: Thread-local user context for subprocess calls

When the web server handles authenticated requests, the user ID must be propagated to subprocess calls:

# In request handler
user_id = request.headers.get("X-User-Id", "anonymous")

# Set as environment variable for subprocess
env = {**os.environ, "HERMES_USER_ID": user_id}
result = subprocess.run(cmd, env=env, ...)

Common Pitfalls

  1. Missing -q flag: Causes subprocess to hang in interactive mode
  2. Missing --source tool: Pollutes user's session list with tool-generated messages
  3. No threading lock: Race conditions on global_sessions cause data corruption
  4. Hardcoded paths: Breaks when hermes is installed in non-standard locations
  5. Missing config defaults: KeyError when config.yaml is incomplete
  6. No session file directory creation: FileNotFoundError on first save

Verification Checklist

  • All subprocess calls use -q --source tool flags
  • Session modifications are wrapped in with sessions_lock:
  • save_sessions() called after every modification
  • Path resolution uses multi-level fallback
  • Config defaults include auth_method: "header"
  • Data directory created with exist_ok=True
  • JSON dump uses default=str for datetime handling