From 16553be59d6ec4e1fbdadce2877b1a437f0528ae Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Fri, 3 Apr 2026 13:00:28 -0700 Subject: [PATCH] fix: profile creation fallback when hermes_cli unavailable (Docker) When hermes-agent is not discoverable (common in Docker), create_profile_api() raised a hard RuntimeError while list and delete already had manual fallbacks. Changes: - Add _create_profile_fallback() that bootstraps profile directory structure directly (matching upstream hermes_cli.profiles: 8 subdirs + config clone) - Extract _validate_profile_name() so validation works without hermes_cli - Add constants _PROFILE_ID_RE, _PROFILE_DIRS, _CLONE_CONFIG_FILES matching upstream hermes-agent - Remove :ro from docker-compose.yml hermes home mount so profiles dir is writable inside the container Closes #44 Co-Authored-By: Claude Opus 4.6 (1M context) --- api/profiles.py | 72 ++++++++++++++++++++++++++++++++++++++-------- docker-compose.yml | 4 +-- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/api/profiles.py b/api/profiles.py index f74cd91..579ad78 100644 --- a/api/profiles.py +++ b/api/profiles.py @@ -10,9 +10,19 @@ HERMES_HOME at import time. """ import json import os +import re +import shutil import threading from pathlib import Path +# ── Constants (match hermes_cli.profiles upstream) ───────────────────────── +_PROFILE_ID_RE = re.compile(r'^[a-z0-9][a-z0-9_-]{0,63}$') +_PROFILE_DIRS = [ + 'memories', 'sessions', 'skills', 'skins', + 'logs', 'plans', 'workspace', 'cron', +] +_CLONE_CONFIG_FILES = ['config.yaml', '.env', 'SOUL.md'] + # ── Module state ──────────────────────────────────────────────────────────── _active_profile = 'default' _profile_lock = threading.Lock() @@ -245,22 +255,60 @@ def _default_profile_dict() -> dict: } +def _validate_profile_name(name: str): + """Validate profile name format (matches hermes_cli.profiles upstream).""" + if name == 'default': + raise ValueError("Cannot create a profile named 'default' -- it is the built-in profile.") + if not _PROFILE_ID_RE.match(name): + raise ValueError( + f"Invalid profile name {name!r}. " + "Must match [a-z0-9][a-z0-9_-]{0,63}" + ) + + +def _create_profile_fallback(name: str, clone_from: str = None, + clone_config: bool = False) -> Path: + """Create a profile directory without hermes_cli (Docker/standalone fallback).""" + profile_dir = _DEFAULT_HERMES_HOME / 'profiles' / name + if profile_dir.exists(): + raise FileExistsError(f"Profile '{name}' already exists.") + + # Bootstrap directory structure + profile_dir.mkdir(parents=True, exist_ok=True) + for subdir in _PROFILE_DIRS: + (profile_dir / subdir).mkdir(parents=True, exist_ok=True) + + # Clone config files from source profile if requested + if clone_config and clone_from: + if clone_from == 'default': + source_dir = _DEFAULT_HERMES_HOME + else: + source_dir = _DEFAULT_HERMES_HOME / 'profiles' / clone_from + if source_dir.is_dir(): + for filename in _CLONE_CONFIG_FILES: + src = source_dir / filename + if src.exists(): + shutil.copy2(src, profile_dir / filename) + + return profile_dir + + def create_profile_api(name: str, clone_from: str = None, clone_config: bool = False) -> dict: """Create a new profile. Returns the new profile info dict.""" - try: - from hermes_cli.profiles import create_profile, validate_profile_name - except ImportError: - raise RuntimeError('Profile management requires hermes-agent to be installed.') + _validate_profile_name(name) - validate_profile_name(name) - create_profile( - name, - clone_from=clone_from, - clone_config=clone_config, - clone_all=False, - no_alias=True, - ) + try: + from hermes_cli.profiles import create_profile + create_profile( + name, + clone_from=clone_from, + clone_config=clone_config, + clone_all=False, + no_alias=True, + ) + except ImportError: + _create_profile_fallback(name, clone_from, clone_config) # Find and return the newly created profile info for p in list_profiles_api(): diff --git a/docker-compose.yml b/docker-compose.yml index 82a1f51..2b4fded 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,8 @@ services: volumes: # Persist session data, settings, and projects across restarts - hermes-data:/data - # Mount hermes-agent for full agent features (optional) - - ${HERMES_HOME:-${HOME}/.hermes}:/root/.hermes:ro + # Mount hermes home for agent features and profile management + - ${HERMES_HOME:-${HOME}/.hermes}:/root/.hermes environment: - HERMES_WEBUI_HOST=0.0.0.0 - HERMES_WEBUI_PORT=8787