--- name: hermes-web-cli-main-architecture version: 1.0 description: Server-side architecture patterns for hermes-web-cli main.py — subprocess communication, session persistence, dynamic path resolution, and config handling. author: Hermes Agent tags: [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. ```python 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: ```python 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 ```python 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 ```python 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: ```python # 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 ```python 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: ```python # 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